def __init__(self, handler): _path = msg.join_path(self._path, '__init__') self.handler = handler self.pub_subs = { 'w': self.handler.ws_pub_sub, 'd': mb, 'l': self.handler.local_pub_sub, } for attr_name in dir(self): attribute = getattr(self, attr_name) if hasattr(attribute, 'msg_types'): for _type, channels in attribute.msg_types: msg.code_debug( _path, 'Adding action: %r ...' % attribute ) self.register_action_in( msg_type=_type, action=attribute, channels=channels) finalize( self, msg.code_debug, self._path, 'Deleting WSClass {0} from {0.handler} ' '...'.format(self) )
def send_message(self, message): """Send a message ``self.send_function`` is used if it was specified during object creation. If not, ``self.execute_actions`` is used. :param dict message: The message to be sent. :raises MsgIsNotDictError: If ``message`` is not a dictionary. :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If ``self.send_function`` wasn't specified during object creation and there's no registered action for this message type. """ _path = msg.join_path(self._path, 'send_message') if not isinstance(message, dict): raise MsgIsNotDictError(message) if 'type' not in message: raise NoMessageTypeError(message) msg.code_debug(_path, 'Sending message: {}.'.format(message)) if self.send_function is None: self.execute_actions(message) else: IOLoop.current().spawn_callback(self.send_function, message)
def end(self): """Clean up the associated objects This coroutine calls :meth:`src.wsclass.WSClass.end` for all objects in ``self.ws_objects`` and it removes ``self`` from ``self.__class__.clients``. This coroutine is setup to be called when the WebSocket connection closes or when the program ends. """ try: exceptions = [] for ws_object in self.ws_objects.values(): try: yield ws_object.end() except: exceptions.append(exc_info()) for exception in exceptions: print_exception(*exception) self.__class__.clients.discard(self) msg.code_debug( msg.join_path(__name__, self.end.__qualname__), 'Connection closed! {0} ' '({0.request.remote_ip})'.format(self)) except: raise
def end_room_usage(self, user, is_teacher, is_student): try: room_code = self.handler.room_code room = yield room_code.room # Room Deassign Course if is_teacher and \ user.course_id is not None and \ user.instances == 1: yield room.deassign_course(user.course_id) # Leave seat if is_student and not self.dont_leave_seat: yield room.leave_seat(room_code.seat_id) except AttributeError: if not hasattr(self.handler, 'room_code') or \ self.handler.room_code is None: msg.code_warning( msg.join_path(__name__, self.end.__qualname__), "room_code wasn't initialized at " "{.__class__.__name__}'s " "end.".format(self)) else: raise
def end_room_usage(self, user, is_teacher, is_student): try: room_code = self.handler.room_code room = yield room_code.room # Room Deassign Course if is_teacher and \ user.course_id is not None and \ user.instances == 1: yield room.deassign_course(user.course_id) # Leave seat if is_student and not self.dont_leave_seat: yield room.leave_seat(room_code.seat_id) except AttributeError: if not hasattr(self.handler, 'room_code') or \ self.handler.room_code is None: msg.code_warning( msg.join_path( __name__, self.end.__qualname__), "room_code wasn't initialized at " "{.__class__.__name__}'s " "end.".format(self) ) else: raise
def request_disc_doc(self): _path = msg.join_path(self._path, "request_disc_doc") def _req_disc_doc(): dd = self.disc_doc_client.request("https://accounts.google.com/.well-known/" "openid-configuration", "GET") return self.decode_httplib2_json(dd) if self.disc_doc is None: msg.code_debug(_path, "Requesting discovery document ...") with ThreadPoolExecutor(1) as thread: self.__class__.disc_doc = thread.submit(_req_disc_doc) self.disc_doc = yield self.__class__.disc_doc # Este yield tiene que ir dentro, ya que el # thread no se comenzará a ejecutar si no se # yieldea y no se puede comenzar a ejecutar # fuera del with ya que ahí no existe ... o # algo asi XD :C self.__class__.disc_doc = None msg.code_debug(_path, "self.__class__.disc_doc = None") msg.code_debug(_path, "Discovery document arrived!") else: msg.code_debug(_path, "Waiting for discovery document ...") self.disc_doc = yield self.disc_doc msg.code_debug(_path, "Got the discovery document!")
def request_disc_doc(self): _path = msg.join_path(self._path, 'request_disc_doc') def _req_disc_doc(): dd = self.disc_doc_client.request( 'https://accounts.google.com/.well-known/' 'openid-configuration', 'GET') return self.decode_httplib2_json(dd) if self.disc_doc is None: msg.code_debug(_path, 'Requesting discovery document ...') with ThreadPoolExecutor(1) as thread: self.__class__.disc_doc = thread.submit(_req_disc_doc) self.disc_doc = \ yield self.__class__.disc_doc # Este yield tiene que ir dentro, ya que el # thread no se comenzará a ejecutar si no se # yieldea y no se puede comenzar a ejecutar # fuera del with ya que ahí no existe ... o # algo asi XD :C self.__class__.disc_doc = None msg.code_debug(_path, 'self.__class__.disc_doc = None') msg.code_debug(_path, 'Discovery document arrived!') else: msg.code_debug(_path, 'Waiting for discovery document ...') self.disc_doc = yield self.disc_doc msg.code_debug(_path, 'Got the discovery document!')
def end(self): """Clean up the associated objects This coroutine calls :meth:`src.wsclass.WSClass.end` for all objects in ``self.ws_objects`` and it removes ``self`` from ``self.__class__.clients``. This coroutine is setup to be called when the WebSocket connection closes or when the program ends. """ try: exceptions = [] for ws_object in self.ws_objects.values(): try: yield ws_object.end() except: exceptions.append( exc_info() ) for exception in exceptions: print_exception(*exception) self.__class__.clients.discard(self) msg.code_debug( msg.join_path( __name__, self.end.__qualname__), 'Connection closed! {0} ' '({0.request.remote_ip})'.format(self) ) except: raise
def get(self): _path = msg.join_path(self._path, 'get') try: redirect_uri = urlunparse((self.get_scheme(), self.request.host, conf.login_path, '', '', '')) # remember the user for a longer period of time remember = self.get_argument('remember', False) room_code = self.get_argument('room_code', False) state = jwt.encode({ 'remember': remember, 'room_code': room_code }, secrets['simple']) flow = oa2_client.OAuth2WebServerFlow( google_secrets['web']['client_id'], google_secrets['web']['client_secret'], scope='openid profile', redirect_uri=redirect_uri, state=state) auth_code = self.get_argument('code', False) if not auth_code: auth_uri = flow.step1_get_authorize_url() self.redirect(auth_uri) else: with ThreadPoolExecutor(1) as thread: credentials = yield thread.submit(flow.step2_exchange, auth_code) # Intercambiar el codigo antes que nada para # evitar ataques yield self.request_disc_doc() userinfo_endpoint = \ self.disc_doc['userinfo_endpoint'] http_auth = credentials.authorize(httplib2.Http()) with ThreadPoolExecutor(1) as thread: userinfo = yield thread.submit(http_auth.request, userinfo_endpoint) userinfo = self.decode_httplib2_json(userinfo) # https://developers.google.com/+/api/ # openidconnect/getOpenIdConnect user = yield db.User.from_google_userinfo(userinfo) token = jwt.encode({ 'id': user.id, 'exp': self.get_exp() }, user.secret) msg.code_debug(_path, 'Rendering login.html ...') self.render('login.html', token=token) except oa2_client.FlowExchangeError: self.render('boxes.html', classes={'system'}, critical='Error de autenticación!')
def create(cls, name, svg_path): try: _path = msg.join_path(cls._path, 'create') ElementTree.register_namespace('', NS['svg']) elem_tree = yield run_in_thread( ElementTree.parse, svg_path) seat_circles = elem_tree.iterfind( ".//svg:circle[@class='seat']", NS) seat_attributes = (c.attrib for c in seat_circles) seats = {a['id']: {'used': False, 'x': a['cx'], 'y': a['cy']} for a in seat_attributes} self = yield super().create(name) self.setattr( '_map', elem_tree.getroot() ) yield self.store_dict( { 'map_source': ElementTree.tostring( self.map, encoding="unicode"), 'seats': seats } ) code = yield Code.create( room_id=self.id, code_type=CodeType.room) codes = [code] for seat in self.seats: code = yield Code.create( room_id=self.id, code_type=CodeType.seat, seat_id=seat ) codes.append(code) return (self, codes) except DuplicateKeyError: raise except OperationFailure: msg.obj_creation_error(_path, cls, name=name, svg_path=svg_path) cls.coll.remove(name) raise
def initialize(self): _path = msg.join_path(self._path, "initialize") msg.code_debug(_path, "New connection established! {0} " "({0.request.remote_ip})".format(self)) self.local_pub_sub = OwnerPubSub(name="local_pub_sub") self.ws_pub_sub = OwnerPubSub(name="ws_pub_sub", send_function=self.write_message) self.ws_objects = {ws_class: ws_class(self) for ws_class in self.ws_classes} self.__class__.clients.add(self) self.__class__.client_count += 1 self.clean_closed = False self.ping_timeout_handle = None
class RouterWSC(src.wsclass.WSClass): _path = msg.join_path(_path, 'RouterWSC') @subscribe('toFrontend', channels={'d', 'l', 'w'}) def to_frontend(self, message): self.redirect_content_to('w', message) @subscribe('toDatabase', channels={'l'}) def to_database(self, message): self.redirect_content_to('d', message) @subscribe('toLocal', channels={'d'}) def to_local(self, message): self.redirect_content_to('l', message)
class OwnerPubSub(PubSub): # Used by inherited methods. _path = msg.join_path(_path, 'OwnerPubSub') def __init__(self, name='owner_pub_sub_instance', send_function=None): super().__init__(name, send_function) self.owners = {} def register(self, msg_type, action, owner=None): super().register(msg_type, action) if owner in self.owners: self.owners[owner].add((msg_type, action)) else: self.owners[owner] = {(msg_type, action)} def remove_owner(self, owner): """Remove all actions registered by ``owner``. :param object owner: Object that owns a set of actions registeres in this PubSub object. :raises UnrecognizedOwnerError: If ``owner`` wasn't previously registered in this PubSubobject. """ try: for msg_type, action in self.owners[owner]: self.remove(msg_type, action) del self.owners[owner] except NoActionForMsgTypeError: warn("This method tried to remove an action " "that was registered by an owner, but now " "isn't in ``self.actions``. This may be " "caused because two owners registered the " "same action. Please review your code. " "This may be a source of errors.") except KeyError as ke: if owner not in self.owners: uoe = UnrecognizedOwnerError( 'Owner {owner} is not registered in ' 'the {ps.name} PubSub ' 'object.'.format(owner=owner, ps=self)) raise uoe from ke else: raise
def create(cls, room_id, code_type, seat_id=None): """Create a new code. .. todo:: * review error handling. """ _path = msg.join_path(cls._path, 'create') def creat_err_msg(): msg.obj_creation_error(_path, cls, room_id=room_id, code_type=code_type, seat_id=seat_id) if not isinstance(code_type, CodeType): raise TypeError if seat_id is not None and \ not isinstance(seat_id, str): raise TypeError if (code_type is CodeType.room) == \ (seat_id is not None): raise ValueError try: # Throws NoObjectReturnedFromDB room = yield Room.get(room_id) self, code = yield cls._gen_new_code() self.setattr('_room', room) # Throws OperationFailure yield self.store_dict({ 'room_id': room_id, 'code_type': code_type.value, 'seat_id': seat_id }) return self except NoObjectReturnedFromDB: creat_err_msg() raise OperationFailure except OperationFailure: creat_err_msg() cls.coll.remove(code) raise
def create(cls, room_id, code_type, seat_id=None): """Create a new code. .. todo:: * review error handling. """ _path = msg.join_path(cls._path, 'create') def creat_err_msg(): msg.obj_creation_error(_path, cls, room_id=room_id, code_type=code_type, seat_id=seat_id) if not isinstance(code_type, CodeType): raise TypeError if seat_id is not None and \ not isinstance(seat_id, str): raise TypeError if (code_type is CodeType.room) == \ (seat_id is not None): raise ValueError try: # Throws NoObjectReturnedFromDB room = yield Room.get(room_id) self, code = yield cls._gen_new_code() self.setattr('_room', room) # Throws OperationFailure yield self.store_dict( {'room_id': room_id, 'code_type': code_type.value, 'seat_id': seat_id}) return self except NoObjectReturnedFromDB: creat_err_msg() raise OperationFailure except OperationFailure: creat_err_msg() cls.coll.remove(code) raise
def get(self): _path = msg.join_path(self._path, "get") try: redirect_uri = urlunparse((self.get_scheme(), self.request.host, conf.login_path, "", "", "")) # remember the user for a longer period of time remember = self.get_argument("remember", False) room_code = self.get_argument("room_code", False) state = jwt.encode({"remember": remember, "room_code": room_code}, secrets["simple"]) flow = oa2_client.OAuth2WebServerFlow( google_secrets["web"]["client_id"], google_secrets["web"]["client_secret"], scope="openid profile", redirect_uri=redirect_uri, state=state, ) auth_code = self.get_argument("code", False) if not auth_code: auth_uri = flow.step1_get_authorize_url() self.redirect(auth_uri) else: with ThreadPoolExecutor(1) as thread: credentials = yield thread.submit(flow.step2_exchange, auth_code) # Intercambiar el codigo antes que nada para # evitar ataques yield self.request_disc_doc() userinfo_endpoint = self.disc_doc["userinfo_endpoint"] http_auth = credentials.authorize(httplib2.Http()) with ThreadPoolExecutor(1) as thread: userinfo = yield thread.submit(http_auth.request, userinfo_endpoint) userinfo = self.decode_httplib2_json(userinfo) # https://developers.google.com/+/api/ # openidconnect/getOpenIdConnect user = yield db.User.from_google_userinfo(userinfo) token = jwt.encode({"id": user.id, "exp": self.get_exp()}, user.secret) msg.code_debug(_path, "Rendering login.html ...") self.render("login.html", token=token) except oa2_client.FlowExchangeError: self.render("boxes.html", classes={"system"}, critical="Error de autenticación!")
def create(cls, name, svg_path): try: _path = msg.join_path(cls._path, 'create') ElementTree.register_namespace('', NS['svg']) elem_tree = yield run_in_thread(ElementTree.parse, svg_path) seat_circles = elem_tree.iterfind(".//svg:circle[@class='seat']", NS) seat_attributes = (c.attrib for c in seat_circles) seats = { a['id']: { 'used': False, 'x': a['cx'], 'y': a['cy'] } for a in seat_attributes } self = yield super().create(name) self.setattr('_map', elem_tree.getroot()) yield self.store_dict({ 'map_source': ElementTree.tostring(self.map, encoding="unicode"), 'seats': seats }) code = yield Code.create(room_id=self.id, code_type=CodeType.room) codes = [code] for seat in self.seats: code = yield Code.create(room_id=self.id, code_type=CodeType.seat, seat_id=seat) codes.append(code) return (self, codes) except DuplicateKeyError: raise except OperationFailure: msg.obj_creation_error(_path, cls, name=name, svg_path=svg_path) cls.coll.remove(name) raise
def execute_actions(self, message): """Execute actions associated to the type of message :param dict message: The message to be sent :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If there's no registered action for this message type. :raises NotDictError: If ``message`` is not an instance of ``dict``. """ _path = msg.join_path(self._path, 'execute_actions') msg.code_debug( _path, 'Message arrived: {}.'.format(message) ) try: for action in self.actions[message['type']]: IOLoop.current().spawn_callback(action, message) except TypeError as te: if not isinstance(message, dict): nde = NotDictError('message', message) raise nde from te else: raise except KeyError as ke: if 'type' not in message: raise NoMessageTypeError(message) from ke elif message['type'] not in self.actions: nafmte = NoActionForMsgTypeError(message, self.name) raise nafmte from ke else: raise
def send_message(self, message): """Send a message ``self.send_function`` is used if it was specified during object creation. If not, ``self.execute_actions`` is used. The message will be sent on the next iteration of the IOLoop. If you need to send the message immediately use "self.send_function" directly. :param dict message: The message to be sent. :raises NotDictError: If ``message`` is not a dictionary. :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If ``self.send_function`` wasn't specified during object creation and there's no registered action for this message type. """ _path = msg.join_path(self._path, 'send_message') if not isinstance(message, dict): raise NotDictError('message', message) if 'type' not in message: raise NoMessageTypeError(message) msg.code_debug( _path, 'Sending message: {}.'.format(message) ) if self.send_function is None: self.execute_actions(message) else: IOLoop.current().spawn_callback( self.send_function, message)
def initialize(self): _path = msg.join_path(self._path, 'initialize') msg.code_debug( _path, 'New connection established! {0} ' '({0.request.remote_ip})'.format(self)) self.local_pub_sub = OwnerPubSub(name='local_pub_sub') self.ws_pub_sub = OwnerPubSub(name='ws_pub_sub', send_function=self.write_message) self.ws_objects = { ws_class: ws_class(self) for ws_class in self.ws_classes } self.__class__.clients.add(self) self.__class__.client_count += 1 self.clean_closed = False self.ping_timeout_handle = None
def execute_actions(self, message): """Execute actions associated to the type of message :param dict message: The message to be sent :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If there's no registered action for this message type. :raises NotDictError: If ``message`` is not an instance of ``dict``. """ _path = msg.join_path(self._path, 'execute_actions') msg.code_debug(_path, 'Message arrived: {}.'.format(message)) try: for action in self.actions[message['type']]: IOLoop.current().spawn_callback(action, message) except TypeError as te: if not isinstance(message, dict): nde = NotDictError('message', message) raise nde from te else: raise except KeyError as ke: if 'type' not in message: raise NoMessageTypeError(message) from ke elif message['type'] not in self.actions: nafmte = NoActionForMsgTypeError(message, self.name) raise nafmte from ke else: raise
class Code(DBObject): """Interface to the ``codes`` collection. .. todo:: * Review error handling. """ coll = db.codes _path = msg.join_path(_path, 'Code') @classmethod @coroutine def _gen_new_code(cls): for i in range(200): try: code = random_word(5, ascii_lowercase + digits) self = yield super().create(code) return (self, code) except DuplicateKeyError: msg.try_new_id_after_dup_obj_in_db(cls.path + '.create') else: msg.exhausted_tries(cls.path + '.create') @classmethod @coroutine def create(cls, room_id, code_type, seat_id=None): """Create a new code. .. todo:: * review error handling. """ _path = msg.join_path(cls._path, 'create') def creat_err_msg(): msg.obj_creation_error(_path, cls, room_id=room_id, code_type=code_type, seat_id=seat_id) if not isinstance(code_type, CodeType): raise TypeError if seat_id is not None and \ not isinstance(seat_id, str): raise TypeError if (code_type is CodeType.room) == \ (seat_id is not None): raise ValueError try: # Throws NoObjectReturnedFromDB room = yield Room.get(room_id) self, code = yield cls._gen_new_code() self.setattr('_room', room) # Throws OperationFailure yield self.store_dict({ 'room_id': room_id, 'code_type': code_type.value, 'seat_id': seat_id }) return self except NoObjectReturnedFromDB: creat_err_msg() raise OperationFailure except OperationFailure: creat_err_msg() cls.coll.remove(code) raise def __str__(self): return '<%s -> (%s, %s)>' % (self.id, self.room_id, self.seat_id) @property @coroutine def room(self): if not hasattr(self, '_room'): room = yield Room.get(self.room_id) self.setattr('_room', room) return self._room @property def code_type(self): return CodeType(self._data['code_type'])
# but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from warnings import warn from tornado.ioloop import IOLoop from src import messages as msg from src.exceptions import NotDictError _path = msg.join_path("src", "pub_sub") class PubSub(object): """Implement the PubSub pattern. This class is designed to implement the PubSub pattern, in a way that is compatible with Tornado's coroutines. .. automethod:: __init__ """ _path = msg.join_path(_path, "PubSub") def __init__(self, name="pub_sub_instance", send_function=None):
class Room(DBObject): coll = db.rooms _path = msg.join_path(_path, 'Room') defaults = {} @classmethod @coroutine def create(cls, name, svg_path): try: _path = msg.join_path(cls._path, 'create') ElementTree.register_namespace('', NS['svg']) elem_tree = yield run_in_thread(ElementTree.parse, svg_path) seat_circles = elem_tree.iterfind(".//svg:circle[@class='seat']", NS) seat_attributes = (c.attrib for c in seat_circles) seats = { a['id']: { 'used': False, 'x': a['cx'], 'y': a['cy'] } for a in seat_attributes } self = yield super().create(name) self.setattr('_map', elem_tree.getroot()) yield self.store_dict({ 'map_source': ElementTree.tostring(self.map, encoding="unicode"), 'seats': seats }) code = yield Code.create(room_id=self.id, code_type=CodeType.room) codes = [code] for seat in self.seats: code = yield Code.create(room_id=self.id, code_type=CodeType.seat, seat_id=seat) codes.append(code) return (self, codes) except DuplicateKeyError: raise except OperationFailure: msg.obj_creation_error(_path, cls, name=name, svg_path=svg_path) cls.coll.remove(name) raise def __str__(self): return self.name @coroutine def assign_course(self, course_id): """Adds a course from the courses set. :raises ConditionNotMetError: If the document no longer exists in the database. """ yield self.modify({'$addToSet': {'courses': course_id}}) @coroutine def deassign_course(self, course_id): """Remove a course from the courses set. :raises ConditionNotMetError: If the document no longer exists in the database. """ yield self.modify({'$pull': {'courses': course_id}}) @coroutine def _modify_seat(self, seat_id, use): try: used = 'seats.{}.used'.format(seat_id) yield self.modify_if({used: not use}, {'$set': {used: use}}) except ConditionNotMetError as e: cnme = ConditionNotMetError('The seat {} is{} used.'.format( seat_id, '' if use else ' not')) raise cnme from e @coroutine def use_seat(self, seat_id): yield self._modify_seat(seat_id, use=True) @coroutine def leave_seat(self, seat_id): yield self._modify_seat(seat_id, use=False) @property def name(self): return self.id @property def map(self): if not hasattr(self, '_map'): self.setattr('_map', ElementTree.fromstring(self.map_source)) return self._map
class LoginHandler(RequestHandler): _path = msg.join_path(_path, 'LoginHandler') disc_doc = None # google's discovery document disc_doc_client = httplib2.Http('.disc_doc.cache') # https://developers.google.com/api-client-library/ # python/guide/thread_safety @coroutine def get(self): _path = msg.join_path(self._path, 'get') try: redirect_uri = urlunparse((self.get_scheme(), self.request.host, conf.login_path, '', '', '')) # remember the user for a longer period of time remember = self.get_argument('remember', False) room_code = self.get_argument('room_code', False) state = jwt.encode({ 'remember': remember, 'room_code': room_code }, secrets['simple']) flow = oa2_client.OAuth2WebServerFlow( google_secrets['web']['client_id'], google_secrets['web']['client_secret'], scope='openid profile', redirect_uri=redirect_uri, state=state) auth_code = self.get_argument('code', False) if not auth_code: auth_uri = flow.step1_get_authorize_url() self.redirect(auth_uri) else: with ThreadPoolExecutor(1) as thread: credentials = yield thread.submit(flow.step2_exchange, auth_code) # Intercambiar el codigo antes que nada para # evitar ataques yield self.request_disc_doc() userinfo_endpoint = \ self.disc_doc['userinfo_endpoint'] http_auth = credentials.authorize(httplib2.Http()) with ThreadPoolExecutor(1) as thread: userinfo = yield thread.submit(http_auth.request, userinfo_endpoint) userinfo = self.decode_httplib2_json(userinfo) # https://developers.google.com/+/api/ # openidconnect/getOpenIdConnect user = yield db.User.from_google_userinfo(userinfo) token = jwt.encode({ 'id': user.id, 'exp': self.get_exp() }, user.secret) msg.code_debug(_path, 'Rendering login.html ...') self.render('login.html', token=token) except oa2_client.FlowExchangeError: self.render('boxes.html', classes={'system'}, critical='Error de autenticación!') def get_scheme(self): if 'Scheme' in self.request.headers: return self.request.headers['Scheme'] else: return self.request.protocol @property def state(self): if hasattr(self, '_state'): return self._state state = self.get_argument('state', None) if state: self._state = jwt.decode(state, secrets['simple']) else: self._state = None return self._state def get_exp(self): if self.state: delta = conf.long_account_exp \ if self.state['remember'] else \ conf.short_account_exp return datetime.utcnow() + timedelta(**delta) else: return conf.short_account_exp @coroutine def request_disc_doc(self): _path = msg.join_path(self._path, 'request_disc_doc') def _req_disc_doc(): dd = self.disc_doc_client.request( 'https://accounts.google.com/.well-known/' 'openid-configuration', 'GET') return self.decode_httplib2_json(dd) if self.disc_doc is None: msg.code_debug(_path, 'Requesting discovery document ...') with ThreadPoolExecutor(1) as thread: self.__class__.disc_doc = thread.submit(_req_disc_doc) self.disc_doc = \ yield self.__class__.disc_doc # Este yield tiene que ir dentro, ya que el # thread no se comenzará a ejecutar si no se # yieldea y no se puede comenzar a ejecutar # fuera del with ya que ahí no existe ... o # algo asi XD :C self.__class__.disc_doc = None msg.code_debug(_path, 'self.__class__.disc_doc = None') msg.code_debug(_path, 'Discovery document arrived!') else: msg.code_debug(_path, 'Waiting for discovery document ...') self.disc_doc = yield self.disc_doc msg.code_debug(_path, 'Got the discovery document!') def decode_httplib2_json(self, response): return json.loads(response[1].decode('utf-8'))
from functools import partialmethod import jwt from tornado.gen import coroutine from pymongo.errors import OperationFailure import src from controller import MSGHandler from backend_modules import router from src import messages as msg from src.db import User, Code, NoObjectReturnedFromDB, \ ConditionNotMetError, Course, Room from src.pub_sub import MalformedMessageError from src.wsclass import subscribe _path = msg.join_path('panels', 'user') MSGHandler.send_user_not_loaded_error = partialmethod( MSGHandler.send_error, 'userNotLoaded', description='There was no loaded user when ' 'this message arrived.') MSGHandler.send_room_not_loaded_error = partialmethod( MSGHandler.send_error, 'roomNotLoaded', description='There was no loaded room when ' 'this message arrived.') def _logout_and_close(self, reason):
# -*- coding: UTF-8 -*- from warnings import warn from tornado.ioloop import IOLoop from src import messages as msg from src.exceptions import NotDictError _path = msg.join_path('src', 'pub_sub') class PubSub(object): """Implement the PubSub pattern. This class is designed to implement the PubSub pattern, in a way that is compatible with Tornado's coroutines. .. automethod:: __init__ """ _path = msg.join_path(_path, 'PubSub') def __init__(self, name='pub_sub_instance', send_function=None): """Initialize the new PubSub object. :param str name: Name used to identify this object in debug messages.
class UserWSC(src.wsclass.WSClass): """Process user messages""" _path = msg.join_path(_path, 'UserWSC') @staticmethod def should_run_room_deassign_course(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): return was_teacher and has_course and (is_student or is_teacher and distinct_room) @staticmethod def should_run_user_deassign_course(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): student_condition = \ was_student and ( not is_student or distinct_room) teacher_condition = \ was_teacher and not is_none and ( not is_teacher or distinct_room) return has_course and (student_condition or teacher_condition) @staticmethod def should_run_use_seat(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): return is_student @staticmethod def should_run_logout_other_instances(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): return \ was_student or \ is_student or \ was_none and not is_none or \ is_teacher and distinct_room @staticmethod def should_run_load_course(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): return \ has_course and not distinct_room and ( was_student and is_student or was_teacher and is_teacher ) @staticmethod def should_run_redirect_to_teacher_view(has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room): return was_teacher and is_none def __init__(self, handler): super().__init__(handler) self.session_start_ok = False self.block_logout = False self.user_state_at_exclusive_login = None self.dont_leave_seat = False @coroutine def load_user(self, token): try: uid = jwt.decode(token, verify=False)['id'] user = yield User.get(uid) jwt.decode(token, user.secret) self.handler.user = user except NoObjectReturnedFromDB as norfdb: ite = jwt.InvalidTokenError( 'No user was found in the database for ' 'this token.') raise ite from norfdb @subscribe('teacherMessage', 'l') @coroutine def redirect_message_if_user_is_teacher(self, message, content=True): """Redirects a message if the user is a teacher. This coroutine redirects a message to the local channel only if the current user is a teacher. :param dict message: The message that should be redirected if the user is a teacher. :param bool content: If ``True``, just the object corresponding to the ``'content'`` key of ``message`` will be sent. If ``False``, the whole message will be sent. :raises MalformedMessageError: If ``content`` is ``True``, but ``message`` doesn't have the ``'content'`` key. :raises NotDictError: If ``message`` is not a dictionary. :raises NoMessageTypeError: If the message or it's content doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If ``send_function`` of the ``PubSub`` object wasn't specified during object creation and there's no registered action for this message type. :raises AttributeError: If the user is not yet loaded or if the user is ``None``. """ try: if self.handler.user.status == 'room': self.redirect_to('l', message, content) except: raise @subscribe('logout', 'l') def logout(self, message): try: if self.block_logout: self.block_logout = False else: self.user_state_at_exclusive_login = \ message.get( 'user_state_at_exclusive_login') self.dont_leave_seat = message.get('dont_leave_seat', False) self.handler.logout_and_close(message['reason']) except KeyError as ke: if 'reason' not in message: mme = MalformedMessageError( "'reason' key not found in message.") raise mme from ke else: raise @subscribe('userMessage', channels={'w', 'l'}) def send_user_message(self, message): """Send a message to all instances of a single user. The user id is appended to the message type and ``message`` is sent through the database. .. todo:: * Change the message type of the subscription to be organised by namespace. * Change this method so that it uses self.handler.user_msg_type. """ try: message['type'] = '{}({})'.format(message['type'], self.handler.user.id) self.redirect_to('d', message) except MalformedMessageError: self.handler.send_malformed_message_error(message) msg.malformed_message(_path, message) except AttributeError: if not hasattr(self.handler, 'user'): self.handler.send_user_not_loaded_error(message) else: raise @subscribe('user.message.frontend.send', channels={'w', 'l'}) def send_frontend_user_message(self, message): """Send a message to all clients of a single user. .. todo:: * Review the error handling and documentation of this funcion. """ try: self.pub_subs['d'].send_message({ 'type': self.handler.user_msg_type, 'content': { 'type': 'toFrontend', 'content': message['content'] } }) except: raise def sub_to_user_messages(self): """Route messages of the same user to the local PS. This method subscribes :meth:`backend_modules.router.RouterWSC.to_local` to the messages of type ``userMessage(uid)`` coming from the database pub-sub. Where ``uid`` is the current user id. After the execution of this method, ``self.handler.user_msg_type`` contains the message type to be used to send messages to all instances of the user. """ self.handler.user_msg_type = \ 'userMessage({})'.format(self.handler.user.id) router_object = self.handler.ws_objects[router.RouterWSC] self.register_action_in(self.handler.user_msg_type, action=router_object.to_local, channels={'d'}) def send_session_start_error(self, message, causes): """Send a session error message to the client. :param dict message: The message that caused the error. :param str causes: A string explaining the posible causes of the error. """ try: self.handler.send_error( 'session.start.error', message, 'La sesión no se ha podido iniciar. ' + causes) except TypeError as e: if not isinstance(message, dict): te = TypeError('message should be a dictionary.') raise te from e elif not isinstance(causes, str): te = TypeError('causes should be a string.') raise te from e else: raise @coroutine def load_room_code(self, room_code_str): """Load the room code and room from the db. ..todo:: * Write error handling. """ if room_code_str == 'none': room_code = None room = None code_type = 'none' room_name = None seat_id = None else: room_code = yield Code.get(room_code_str) room = yield room_code.room code_type = room_code.code_type.value room_name = room.name seat_id = room_code.seat_id self.handler.room_code = room_code self.handler.room = room return (code_type, room_name, seat_id) def redirect_to_teacher_view(self, room_code, message): """Redirect the client to the current teacher view. .. todo: * Use send_session_start_error instead of handler.send_error. """ if room_code == 'none': err_msg = "Can't redirect user to the " \ "teacher view. This can be caused by an " \ "inconsistency in the database." self.handler.send_error('databaseInconsistency', message, err_msg) raise Exception(err_msg) self.pub_subs['w'].send_message({ 'type': 'replaceLocation', 'url': room_code }) @subscribe('session.start', 'w') @coroutine def startSession(self, message): """Start a user session .. todo:: * Add a variable that indicates the last stage that was executed successfully. So that the finally clause can clean the mess properly. """ try: yield self.load_user(message['token']) self.sub_to_user_messages() code_type, room_name, seat_id = \ yield self.load_room_code( message['room_code']) user = self.handler.user room_code = self.handler.room_code room = self.handler.room course_id = user.course_id has_course = course_id is not None was_none = (user.status == 'none') was_student = (user.status == 'seat') was_teacher = (user.status == 'room') is_none = (code_type == 'none') is_student = (code_type == 'seat') is_teacher = (code_type == 'room') distinct_room = not (room_name is None or user.room_name is None or room_name == user.room_name) same_seat = (seat_id == user.seat_id) and \ not distinct_room transition_data = (has_course, was_none, was_student, was_teacher, is_none, is_student, is_teacher, distinct_room) # Redirect to Teacher View if self.should_run_redirect_to_teacher_view(*transition_data): self.redirect_to_teacher_view(user.room_code, message) # The rest of the code is not executed. return # Room Deassign Course '''This should always run before "User Deassign Course"''' if self.should_run_room_deassign_course(*transition_data): if distinct_room: r = yield Room.get(user.room_name) else: r = room yield r.deassign_course(course_id) # User Deassign Course if self.should_run_user_deassign_course(*transition_data): yield user.deassign_course() # Logout Other Instances if self.should_run_logout_other_instances(*transition_data): self.block_logout = True self.pub_subs['d'].send_message({ 'type': self.handler.user_msg_type, 'content': { 'type': 'logout', 'reason': 'exclusiveLogin', 'user_state_at_exclusive_login': user._data, 'dont_leave_seat': same_seat, } }) # Use Seat if self.should_run_use_seat(*transition_data) \ and (not same_seat or user.instances == 0): yield room.use_seat(room_code.seat_id) # Load Course if self.should_run_load_course(*transition_data): self.handler.course = yield Course.get(course_id) # Increase Instances yield self.handler.user.increase_instances() yield self.handler.user.store_dict({ 'status': code_type, 'room_code': message['room_code'], 'room_name': room_name, 'seat_id': seat_id, }) except jwt.InvalidTokenError: self.handler.logout_and_close('invalidToken') except ConditionNotMetError: self.send_session_start_error( message, 'Es probable que este error se daba a que ' 'el asiento que desea registrar ya está ' 'usado.') except OperationFailure: self.send_session_start_error( message, 'Una operación de la base de datos ha ' 'fallado.') except KeyError: keys_in_message = all( map(lambda k: k in message, ('token', 'room_code'))) if not keys_in_message: self.handler.send_malformed_message_error(message) else: raise else: self.session_start_ok = True self.pub_subs['w'].send_message({ 'type': 'session.start.ok', 'code_type': code_type, 'course_id': user.course_id }) finally: if not self.session_start_ok: pass @subscribe('getUserName', 'w') def get_user_name(self, message): """Send the user's name to the client. .. todo:: * Re-raise attribute error * review error handling. """ try: name = self.handler.user.name self.pub_subs['w'].send_message({'type': 'userName', 'name': name}) except AttributeError: self.handler.send_user_not_loaded_error(message) @coroutine def end_room_usage(self, user, is_teacher, is_student): try: room_code = self.handler.room_code room = yield room_code.room # Room Deassign Course if is_teacher and \ user.course_id is not None and \ user.instances == 1: yield room.deassign_course(user.course_id) # Leave seat if is_student and not self.dont_leave_seat: yield room.leave_seat(room_code.seat_id) except AttributeError: if not hasattr(self.handler, 'room_code') or \ self.handler.room_code is None: msg.code_warning( msg.join_path(__name__, self.end.__qualname__), "room_code wasn't initialized at " "{.__class__.__name__}'s " "end.".format(self)) else: raise @coroutine def end(self): try: yield super().end() if not self.session_start_ok: return if self.user_state_at_exclusive_login: user = User(self.user_state_at_exclusive_login) else: user = self.handler.user yield user.sync('instances') is_teacher = (user.status == 'room') is_student = (user.status == 'seat') # Room Deassign Course # Leave seat yield self.end_room_usage(user, is_teacher, is_student) # User Deassign Course yield user.deassign_course(if_last_instance=is_teacher) # Decrease Instances # (can modify status and course_id) yield user.decrease_instances() except: raise
from functools import partialmethod import jwt from tornado.gen import coroutine from pymongo.errors import OperationFailure import src from controller import MSGHandler from backend_modules import router from src import messages as msg from src.db import User, Code, NoObjectReturnedFromDB, \ ConditionNotMetError, Course, Room from src.pub_sub import MalformedMessageError from src.wsclass import subscribe _path = msg.join_path('panels', 'user') MSGHandler.send_user_not_loaded_error = partialmethod( MSGHandler.send_error, 'userNotLoaded', description='There was no loaded user when ' 'this message arrived.' ) MSGHandler.send_room_not_loaded_error = partialmethod( MSGHandler.send_error, 'roomNotLoaded', description='There was no loaded room when ' 'this message arrived.' )
# This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from warnings import warn from tornado.ioloop import IOLoop from src import messages as msg from src.exceptions import NotDictError _path = msg.join_path('src', 'pub_sub') class PubSub(object): """Implement the PubSub pattern. This class is designed to implement the PubSub pattern, in a way that is compatible with Tornado's coroutines. .. automethod:: __init__ """ _path = msg.join_path(_path, 'PubSub') def __init__(self, name='pub_sub_instance', send_function=None): """Initialize the new PubSub object.
class MSGHandler(WebSocketHandler): """Serve the WebSocket clients. An instance of this class is created every time a client connects using WebSocket. The instances of this class deliver messages to a group of objects specialized in attending a group of messages. """ _path = msg.join_path(_path, 'MSGHandler') ws_classes = [] clients = set() client_count = 0 # Total clients that have connected @classmethod @coroutine def stop(cls): for client in cls.clients.copy(): yield client.end() def initialize(self): _path = msg.join_path(self._path, 'initialize') msg.code_debug( _path, 'New connection established! {0} ' '({0.request.remote_ip})'.format(self)) self.local_pub_sub = OwnerPubSub(name='local_pub_sub') self.ws_pub_sub = OwnerPubSub(name='ws_pub_sub', send_function=self.write_message) self.ws_objects = { ws_class: ws_class(self) for ws_class in self.ws_classes } self.__class__.clients.add(self) self.__class__.client_count += 1 self.clean_closed = False self.ping_timeout_handle = None def open(self): IOLoop.current().spawn_callback(self.on_pong, b'1') @classmethod def add_class(cls, wsclass): cls.ws_classes.append(wsclass) @classmethod def broadcast(cls, message): for client in cls.clients: client.ws_pub_sub.send_message(message) def on_message(self, message): """Process messages when they arrive. :param str message: The received message. This should be a valid json document. """ try: # Throws ValueError message = json.loads(message) self.ws_pub_sub.execute_actions(message) except NoActionForMsgTypeError: self.send_error( 'noActionForMsgType', message, "The client has sent a message for which " "there is no associated action.") msg.no_action_for_msg_type(_path, message) except (NotDictError, NoMessageTypeError, ValueError): self.send_malformed_message_error(message) msg.malformed_message(_path, message) @coroutine def on_pong(self, data): """Clear the timeout, sleep, and send a new ping. .. todo:: * Document the times used in this method. The calculations are in my black notebook XD. """ try: if self.ping_timeout_handle is not None: IOLoop.current().remove_timeout(self.ping_timeout_handle) yield sleep(conf.ping_sleep) self.ping(b'1') self.ping_timeout_handle = \ IOLoop.current().call_later( conf.ping_timeout, self.close) except WebSocketClosedError: pass except: raise def send_error(self, critical_type, message, description): self.ws_pub_sub.send_message({ 'type': 'critical', 'critical_type': critical_type, 'message': message, 'description': description }) send_malformed_message_error = partialmethod( send_error, 'malformedMessage', description="The client has sent a message which " "either isn't in JSON format, is not a " "single JSON object, does not have a " "'type' field or at least one " "attribute is not consistent with the " "others.") def write_message(self, message, binary=False): try: super().write_message(message, binary) except WebSocketClosedError: if not self.clean_closed: raise @coroutine def end(self): """Clean up the associated objects This coroutine calls :meth:`src.wsclass.WSClass.end` for all objects in ``self.ws_objects`` and it removes ``self`` from ``self.__class__.clients``. This coroutine is setup to be called when the WebSocket connection closes or when the program ends. """ try: exceptions = [] for ws_object in self.ws_objects.values(): try: yield ws_object.end() except: exceptions.append(exc_info()) for exception in exceptions: print_exception(*exception) self.__class__.clients.discard(self) msg.code_debug( msg.join_path(__name__, self.end.__qualname__), 'Connection closed! {0} ' '({0.request.remote_ip})'.format(self)) except: raise @coroutine def on_close(self): try: yield self.end() except: raise
def get(self): _path = msg.join_path(self._path, 'get') try: redirect_uri = urlunparse( (self.get_scheme(), self.request.host, conf.login_path, '', '', '') ) # remember the user for a longer period of time remember = self.get_argument('remember', False) room_code = self.get_argument('room_code', False) state = jwt.encode({'remember': remember, 'room_code': room_code}, secrets['simple']) flow = oa2_client.OAuth2WebServerFlow( google_secrets['web']['client_id'], google_secrets['web']['client_secret'], scope='openid profile', redirect_uri=redirect_uri, state=state) auth_code = self.get_argument('code', False) if not auth_code: auth_uri = flow.step1_get_authorize_url() self.redirect(auth_uri) else: with ThreadPoolExecutor(1) as thread: credentials = yield thread.submit( flow.step2_exchange, auth_code) # Intercambiar el codigo antes que nada para # evitar ataques yield self.request_disc_doc() userinfo_endpoint = \ self.disc_doc['userinfo_endpoint'] http_auth = credentials.authorize( httplib2.Http()) with ThreadPoolExecutor(1) as thread: userinfo = yield thread.submit( http_auth.request, userinfo_endpoint) userinfo = self.decode_httplib2_json( userinfo) # https://developers.google.com/+/api/ # openidconnect/getOpenIdConnect user = yield db.User.from_google_userinfo( userinfo) token = jwt.encode({'id': user.id, 'exp': self.get_exp()}, user.secret) msg.code_debug(_path, 'Rendering login.html ...') self.render('login.html', token=token) except oa2_client.FlowExchangeError: self.render('boxes.html', classes={'system-panel'}, critical='Error de autenticación!')
class PubSub(object): """Implement the PubSub pattern. This class is designed to implement the PubSub pattern, in a way that is compatible with Tornado's coroutines. .. automethod:: __init__ """ _path = msg.join_path(_path, 'PubSub') def __init__(self, name='pub_sub_instance', send_function=None): """Initialize the new PubSub object. :param str name: Name used to identify this object in debug messages. :param callable send_function: Function or coroutine to be called when sending a message. This function should not be called directly. When sending a message, call ``send_message`` instead of ``send_function``. """ self.name = name self.send_function = send_function self.actions = {} def __str__(self): return self.name def __repr__(self): """Return a string representation of the object.""" t = '{0.__class__.__name__}' \ "('{0.name}', {0.send_function.__qualname__})" return t.format(self) def register(self, msg_type, action): if msg_type in self.actions: self.actions[msg_type].add(action) else: self.actions[msg_type] = {action} def send_message(self, message): """Send a message ``self.send_function`` is used if it was specified during object creation. If not, ``self.execute_actions`` is used. The message will be sent on the next iteration of the IOLoop. If you need to send the message immediately use "self.send_function" directly. :param dict message: The message to be sent. :raises NotDictError: If ``message`` is not a dictionary. :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If ``self.send_function`` wasn't specified during object creation and there's no registered action for this message type. """ _path = msg.join_path(self._path, 'send_message') if not isinstance(message, dict): raise NotDictError('message', message) if 'type' not in message: raise NoMessageTypeError(message) msg.code_debug(_path, 'Sending message: {}.'.format(message)) if self.send_function is None: self.execute_actions(message) else: IOLoop.current().spawn_callback(self.send_function, message) def execute_actions(self, message): """Execute actions associated to the type of message :param dict message: The message to be sent :raises NoMessageTypeError: If ``message`` doesn't have the ``'type'`` key. :raises NoActionForMsgTypeError: If there's no registered action for this message type. :raises NotDictError: If ``message`` is not an instance of ``dict``. """ _path = msg.join_path(self._path, 'execute_actions') msg.code_debug(_path, 'Message arrived: {}.'.format(message)) try: for action in self.actions[message['type']]: IOLoop.current().spawn_callback(action, message) except TypeError as te: if not isinstance(message, dict): nde = NotDictError('message', message) raise nde from te else: raise except KeyError as ke: if 'type' not in message: raise NoMessageTypeError(message) from ke elif message['type'] not in self.actions: nafmte = NoActionForMsgTypeError(message, self.name) raise nafmte from ke else: raise def remove(self, msg_type, action): """Remove the action asociated with ``msg_type``. :param str msg_type: The message type that is asociated with ``action``. :param callable action: A function that was previously registered to ``msg_type``. :raises NoActionForMsgTypeError: If there's no registered action for this message type. """ try: self.actions[msg_type].discard(action) if not self.actions[msg_type]: del self.actions[msg_type] except KeyError as ke: if msg_type not in self.actions: nafmte = NoActionForMsgTypeError(msg_type, self.name) raise nafmte from ke else: raise