def check_users(project, users, timeout=5): address = project.get("connection_check_address") if not address: logger.debug("no connection check address for project {0}".format(project['name'])) raise Return(()) http_client = AsyncHTTPClient() request = HTTPRequest( address, method="POST", body=urlencode({ 'users': json.dumps(list(users)) }), request_timeout=timeout ) try: response = yield http_client.fetch(request) except Exception as err: logger.error(err) raise Return((None, None)) else: if response.code != 200: raise Return((None, None)) try: content = [str(x) for x in json.loads(response.body)] except Exception as err: logger.error(err) raise Return((None, err)) raise Return((content, None))
def collect_expired_connections(self): """ Find all expired connections in projects to check them later. """ projects, error = yield self.structure.project_list() if error: logger.error(error) raise Return((None, error)) for project in projects: project_id = project['_id'] expired_connections, error = yield self.collect_project_expired_connections(project) if error: logger.error(error) continue if project_id not in self.expired_connections: self.expired_connections[project_id] = { "users": set(), "checked_at": None } current_expired_connections = self.expired_connections[project_id]["users"] self.expired_connections[project_id]["users"] = current_expired_connections | expired_connections raise Return((True, None))
def check_expired_connections(self): """ For each project ask web application about users whose connections expired. Close connections of deactivated users and keep valid users' connections. """ projects, error = yield self.structure.project_list() if error: raise Return((None, error)) checks = [] for project in projects: if project.get('connection_check', False): checks.append(self.check_project_expired_connections(project)) try: # run all checks in parallel yield checks except Exception as err: logger.error(err) tornado.ioloop.IOLoop.instance().add_timeout( time.time() + self.CONNECTION_EXPIRE_CHECK_INTERVAL, self.check_expired_connections) raise Return((True, None))
def collect_expired_connections(self): """ Find all expired connections in projects to check them later. """ projects, error = yield self.structure.project_list() if error: logger.error(error) raise Return((None, error)) for project in projects: project_id = project['_id'] expired_connections, error = yield self.collect_project_expired_connections( project) if error: logger.error(error) continue if project_id not in self.expired_connections: self.expired_connections[project_id] = { "users": set(), "checked_at": None } current_expired_connections = self.expired_connections[project_id][ "users"] self.expired_connections[project_id][ "users"] = current_expired_connections | expired_connections raise Return((True, None))
def post(self): result, error = {}, None params = params_from_request(self.request) project = params.pop('project') method = params.pop('method') data = params.get('data') if data is not None: try: data = json_decode(data) except Exception as e: logger.error(e) else: params["data"] = data project = self.application.get_project(project) if not project: error = self.application.PROJECT_NOT_FOUND else: result, error = yield self.application.process_call(project, method, params) self.set_header("Content-Type", "application/json") self.finish(json_encode({ "body": result, "error": error }))
def check_users(project, users, timeout=5): address = project.get("connection_check_address") if not address: logger.debug("no connection check address for project {0}".format( project['name'])) raise Return(()) http_client = AsyncHTTPClient() request = HTTPRequest(address, method="POST", body=urlencode( {'users': json.dumps(list(users))}), request_timeout=timeout) try: response = yield http_client.fetch(request) except Exception as err: logger.error(err) raise Return((None, None)) else: if response.code != 200: raise Return((None, None)) try: content = [str(x) for x in json.loads(response.body)] except Exception as err: logger.error(err) raise Return((None, err)) raise Return((content, None))
def process_publish(self, project, params, allowed_namespaces=None, client=None): """ Publish message into appropriate channel. """ if allowed_namespaces is None: project_namespaces, error = yield self.structure.get_project_namespaces(project) if error: raise Return((None, error)) allowed_namespaces = dict((x['name'], x) for x in project_namespaces) message, error = yield self.prepare_message( project, allowed_namespaces, params, client ) if error: raise Return((None, error)) if isinstance(message, dict): # event prepared for publishing result, error = yield self.publish_message( message, allowed_namespaces ) if error: raise Return((None, error)) for callback in self.post_publish_callbacks: result, error = yield callback(message) if error: logger.error(str(error)) else: # message is error description raise Return((None, message)) raise Return((True, None))
def process_publish(self, project, params, allowed_namespaces=None, client_id=None): """ Publish message into appropriate channel. """ if allowed_namespaces is None: project_namespaces, error = yield self.structure.get_project_namespaces(project) if error: raise Return((None, error)) allowed_namespaces = dict((x['name'], x) for x in project_namespaces) message, error = yield self.prepare_message( project, allowed_namespaces, params, client_id ) if error: raise Return((None, error)) if isinstance(message, dict): # event prepared for publishing result, error = yield self.publish_message( message, allowed_namespaces ) if error: raise Return((None, error)) for callback in self.post_publish_callbacks: result, error = yield callback(message) if error: logger.error(str(error)) else: # message is error description raise Return((None, message)) raise Return((True, None))
def update_structure_because_of_inconsistency(self): # try to update structure at least after a second logger.error("structure inconsistent, will try to update after {0} milliseconds".format( self.recover_interval )) self.structure_recover.stop() yield self.update()
def check_expired_connections(self): """ For each project ask web application about users whose connections expired. Close connections of deactivated users and keep valid users' connections. """ projects, error = yield self.structure.project_list() if error: raise Return((None, error)) checks = [] for project in projects: if project.get('connection_check', False): checks.append(self.check_project_expired_connections(project)) try: # run all checks in parallel yield checks except Exception as err: logger.error(err) tornado.ioloop.IOLoop.instance().add_timeout( time.time()+self.CONNECTION_EXPIRE_CHECK_INTERVAL, self.check_expired_connections ) raise Return((True, None))
def connect(self): """ Connect from scratch """ try: self.subscriber.connect(host=self.host, port=self.port) self.publisher.connect(host=self.host, port=self.port) self.worker.connect(host=self.host, port=self.port) if self.options.redis_api: self.listener.connect(host=self.host, port=self.port) except Exception as e: logger.error("error connecting to Redis server: %s" % (str(e))) else: if self.password: self.subscriber.auth(self.password, callback=self.on_auth) self.publisher.auth(self.password, callback=self.on_auth) self.worker.auth(self.password, callback=self.on_auth) if self.options.redis_api: self.listener.auth(self.password, callback=self.on_auth) self.subscriber.select(self.db, callback=self.on_subscriber_select) self.publisher.select(self.db, callback=self.on_select) self.worker.select(self.db, callback=self.on_select) if self.options.redis_api: self.listener.select(self.db, callback=self.on_listener_select) self.connection_check.stop() self.connection_check.start()
def on_listener_select(self, res): if res != self.OK_RESPONSE: # error returned logger.error("select database failed: {0}".format(res)) self._need_reconnect = True return self.process_api_messages()
def _publish(self, channel, message): try: self.publisher.publish(channel, message) except StreamClosedError as e: self._need_reconnect = True logger.error(e) return False else: return True
def process_object(self, obj, project, is_owner_request): response = Response() try: validate(obj, req_schema) except ValidationError as e: response.error = str(e) raise Return(response) req_id = obj.get("uid", None) method = obj.get("method") params = obj.get("params") response.uid = req_id response.method = method schema = server_api_schema if is_owner_request and self.application.OWNER_API_PROJECT_PARAM in params: project_id = params[self.application.OWNER_API_PROJECT_PARAM] project, error = yield self.application.structure.get_project_by_id( project_id ) if error: logger.error(error) response.error = self.application.INTERNAL_SERVER_ERROR if not project: response.error = self.application.PROJECT_NOT_FOUND try: params.pop(self.application.OWNER_API_PROJECT_PARAM) except KeyError: pass if not is_owner_request and method in owner_api_methods: response.error = self.application.PERMISSION_DENIED if not response.error: if method not in schema: response.error = self.application.METHOD_NOT_FOUND else: try: validate(params, schema[method]) except ValidationError as e: response.error = str(e) else: result, error = yield self.application.process_call( project, method, params ) response.body = result response.error = error raise Return(response)
def process_api_object(self, obj, project, is_owner_request): response = Response() try: validate(obj, req_schema) except ValidationError as e: response.error = str(e) raise Return(response) req_id = obj.get("uid", None) method = obj.get("method") params = obj.get("params") response.uid = req_id response.method = method schema = server_api_schema if is_owner_request and self.OWNER_API_PROJECT_PARAM in params: project_id = params[self.OWNER_API_PROJECT_PARAM] project, error = yield self.structure.get_project_by_id( project_id ) if error: logger.error(error) response.error = self.INTERNAL_SERVER_ERROR if not project: response.error = self.PROJECT_NOT_FOUND try: params.pop(self.OWNER_API_PROJECT_PARAM) except KeyError: pass if not is_owner_request and method in owner_api_methods: response.error = self.PERMISSION_DENIED if not response.error: if method not in schema: response.error = self.METHOD_NOT_FOUND else: try: validate(params, schema[method]) except ValidationError as e: response.error = str(e) else: result, error = yield self.process_call( project, method, params ) response.body = result response.error = error raise Return(response)
def publish(self, channel, message, method=None): """ Publish message into channel of stream. """ method = method or self.DEFAULT_PUBLISH_METHOD message["message_type"] = method to_publish = json_encode(message) try: self.publisher.publish(channel, to_publish) except StreamClosedError as e: self._need_reconnect = True logger.error(e)
def update_channel_info(self, body, channel): """ Try to extract channel specific user info from response body and keep it for channel. """ try: info = json_decode(body) except Exception as e: logger.error(str(e)) info = {} self.channel_info[channel] = info
def update_channel_user_info(self, body, channel): """ Try to extract channel specific user info from response body and keep it for channel. """ try: info = json_decode(body) except Exception as e: logger.error(str(e)) info = {} self.channel_user_info[channel] = info
def update_channel_user_info(self, body, namespace_name, channel): """ Try to extract channel specific user info from response body and keep it for channel. """ try: info = json_decode(body) except Exception as e: logger.error(str(e)) info = {} self.channel_user_info.setdefault(namespace_name, {}) self.channel_user_info[namespace_name][channel] = info
def validate_token(token, secret_key, project_id, user, timestamp, user_info): try: is_valid_token = auth.check_client_token( token, secret_key, project_id, user, timestamp, user_info=user_info ) except Exception as err: logger.error(err) return "invalid connection parameters" if not is_valid_token: return "invalid token" return None
def connect(self): """ Connect from scratch if connection not established. """ subscriber_connect = False publisher_connect = False worker_connect = False listener_connect = False try: if not self.subscriber.is_connected(): subscriber_connect = True self.subscriber.connect(host=self.host, port=self.port) if not self.publisher.is_connected(): publisher_connect = True self.publisher.connect(host=self.host, port=self.port) if not self.worker.is_connected(): worker_connect = True self.worker.connect(host=self.host, port=self.port) if self.options.redis_api: if not self.listener.is_connected(): listener_connect = True self.listener.connect(host=self.host, port=self.port) except Exception as e: logger.error("error connecting to Redis server: %s" % (str(e))) else: if self.password: if subscriber_connect: self.subscriber.auth(self.password, callback=self.on_auth) if publisher_connect: self.publisher.auth(self.password, callback=self.on_auth) if worker_connect: self.worker.auth(self.password, callback=self.on_auth) if self.options.redis_api: if listener_connect: self.listener.auth(self.password, callback=self.on_auth) if subscriber_connect: self.subscriber.select(self.db, callback=self.on_subscriber_select) if publisher_connect: self.publisher.select(self.db, callback=self.on_select) if worker_connect: self.worker.select(self.db, callback=self.on_select) if self.options.redis_api: if listener_connect: self.listener.select(self.db, callback=self.on_listener_select) self.connection_check.stop() self.connection_check.start()
def on_subscriber_select(self, res): """ After selecting subscriber database subscribe on channels """ if res != self.OK_RESPONSE: # error returned logger.error("pubsub select database for subscriber: {0}".format(res)) return self.subscriber.subscribe(CONTROL_CHANNEL, callback=self.dispatch_published_message) self.subscriber.subscribe(ADMIN_CHANNEL, callback=self.dispatch_published_message) for subscription in self.subscriptions.copy(): if subscription not in self.subscriptions: continue self.subscriber.subscribe(subscription, callback=self.dispatch_published_message)
def close_sock(self, pause=True, pause_value=1): """ Force closing connection. """ if pause: # sleep for a while before closing connection to prevent mass invalid reconnects yield sleep(pause_value) try: if self.sock: self.sock.close() else: yield self.close() except Exception as err: logger.error(err) raise Return((True, None))
def on_subscriber_select(self, res): """ After selecting subscriber database subscribe on channels """ if res != self.OK_RESPONSE: # error returned logger.error("select database failed: {0}".format(res)) self._need_reconnect = True return self.subscriber.subscribe(self.admin_channel_name, callback=self.on_redis_message) self.subscriber.subscribe(self.control_channel_name, callback=self.on_redis_message) for subscription in self.subscriptions.copy(): if subscription not in self.subscriptions: continue self.subscriber.subscribe(subscription, callback=self.on_redis_message)
def post_actions(self): params = params_from_request(self.request) method = params.pop('method') params.pop('_xsrf') data = params.get('data', None) if data is not None: try: data = json_decode(data) except Exception as e: logger.error(e) else: params["data"] = data result, error = yield self.application.process_call( self.project, method, params) self.set_header("Content-Type", "application/json") self.finish(json_encode({"body": result, "error": error}))
def update_user_info(self, body, namespace_name, channel): """ Try to extract user info from response body and remember it for namespace and channel. """ try: info = json_decode(body) except Exception as e: logger.error(str(e)) info = {} user_info = { 'user_id': self.user, 'client_id': self.uid, 'data': info } self.user_info.setdefault(namespace_name, {}) self.user_info[namespace_name][channel] = json_encode(user_info)
def connect(self): """ Connect to Redis. Do not even try to connect if State is faked. """ if self.fake: return try: self.client.connect(host=self.host, port=self.port) except Exception as e: logger.error("error connecting to Redis server: %s" % (str(e))) else: if self.db and isinstance(self.db, int): self.client.select(self.db, callback=self.on_select) self.connection_check.stop() self.connection_check.start()
def connect(self): """ Connect from scratch, resubscribe on channels etc """ try: self.subscriber.connect(host=self.host, port=self.port) self.publisher.connect(host=self.host, port=self.port) except Exception as e: logger.error("error connecting to Redis server: %s" % (str(e))) else: if self.db: self.subscriber.select(self.db, callback=self.on_subscriber_select) self.publisher.select(self.db, callback=self.on_publisher_select) else: self.on_subscriber_select(self.OK_RESPONSE) self.on_publisher_select(self.OK_RESPONSE) self.connection_check.stop() self.connection_check.start()
def on_subscriber_select(self, res): """ After selecting subscriber database subscribe on channels """ if res != self.OK_RESPONSE: # error returned logger.error("select database for subscriber: {0}".format(res)) return self.subscriber.subscribe(CONTROL_CHANNEL, callback=self.dispatch_published_message) self.subscriber.subscribe(ADMIN_CHANNEL, callback=self.dispatch_published_message) for subscription in self.subscriptions.copy(): if subscription not in self.subscriptions: continue self.subscriber.subscribe(subscription, callback=self.dispatch_published_message)
def post_actions(self): params = params_from_request(self.request) method = params.pop('method') params.pop('_xsrf') data = params.get('data', None) if data is not None: try: data = json_decode(data) except Exception as e: logger.error(e) else: params["data"] = data result, error = yield self.application.process_call(self.project, method, params) self.set_header("Content-Type", "application/json") self.finish(json_encode({ "body": result, "error": error }))
def message_received(self, message): """ Called when message from client received. """ multi_response = MultiResponse() try: data = json_decode(message) except ValueError: logger.error('malformed JSON data') yield self.close_sock() raise Return((True, None)) if isinstance(data, dict): # single object request response, err = yield self.process_obj(data) multi_response.add(response) if err: # error occurred, connection must be closed logger.error(err) yield self.sock.send(multi_response.as_message()) yield self.close_sock() raise Return((True, None)) elif isinstance(data, list): # multiple object request if len(data) > self.application.CLIENT_API_MESSAGE_LIMIT: logger.info("client API message limit exceeded") yield self.close_sock() raise Return((True, None)) for obj in data: response, err = yield self.process_obj(obj) multi_response.add(response) if err: # close connection in case of any error logger.error(err) yield self.sock.send(multi_response.as_message()) yield self.send_disconnect_message() yield self.close_sock() raise Return((True, None)) else: logger.error('data not list and not dictionary') yield self.close_sock() raise Return((True, None)) yield self.send(multi_response.as_message()) raise Return((True, None))
def on_error(self, error): logger.error(str(error)) self._CONSISTENT = False raise Return((None, error))
def on_error(self, error): logger.error(str(error)) self._consistent = False raise Return((None, error))
def on_select(self, res): if res != "OK": logger.error("state select database: {0}".format(res))
def on_error(error): """ General error wrapper. """ logger.error(str(error)) raise Return((None, error))
def on_select(self, res): if res != self.OK_RESPONSE: logger.error("select database failed: {0}".format(res)) self._need_reconnect = True
def stop_running(msg): """ Called only during initialization when critical error occurred. """ logger.error(msg) sys.exit(1)
def on_auth(self, res): if res != self.OK_RESPONSE: logger.error("auth failed: {0}".format(res))
def on_api_message(self, redis_message): """ Got message from Redis, dispatch it into right message handler. """ try: message = json_decode(redis_message[1]) except ValueError: logger.error("Redis API - malformed JSON") return if not isinstance(message, dict): logger.error("Redis API - object expected") return project_id = message.get("project") if not project_id: logger.error("Redis API - project required") return data = message.get("data") if not data: logger.error("Redis API - data required") project, error = yield self.application.structure.get_project_by_id(project_id) if error: logger.error("Redis API - server error") return if not project: logger.error("Redis API - project not found") return _, error = yield self.application.process_api_data(project, data, False) if error: logger.error(error)
def handle_connect(self, params): """ Authenticate client's connection, initialize required variables in case of successful authentication. """ if self.application.collector: self.application.collector.incr('connect') self.application.collector.incr(self.sock.session.transport_name) if self.is_authenticated: raise Return((self.uid, None)) project_id = params["project"] user = params["user"] info = params.get("info", "{}") if not self.application.INSECURE: token = params["token"] timestamp = params["timestamp"] else: token = timestamp = None project, error = yield self.application.get_project(project_id) if error: raise Return((None, error)) secret_key = project['secret_key'] if not self.application.INSECURE: error_msg = self.validate_token( token, secret_key, project_id, user, timestamp, info ) if error_msg: raise Return((None, error_msg)) if info: try: info = json_decode(info) except Exception as err: logger.error("malformed JSON data in user_info") logger.error(err) info = {} else: info = {} if not self.application.INSECURE: try: timestamp = int(timestamp) except ValueError: raise Return((None, "invalid timestamp")) else: # we are not interested in timestamp in case of insecure mode so just # set it to current timestamp timestamp = int(time.time()) self.user = user self.project_id = project_id self.timestamp = timestamp time_to_expire = None if not self.application.INSECURE and project.get('connection_check', False): now = time.time() time_to_expire = self.timestamp + project.get("connection_lifetime", 3600) - now if time_to_expire <= 0: raise Return(({"client": None, "expired": True, "ttl": project.get("connection_lifetime", 3600)}, None)) # Welcome to Centrifuge dear Connection! self.is_authenticated = True self.default_info = { 'user_id': self.user, 'client_id': self.uid, 'default_info': info, 'channel_info': None } self.channels = {} self.presence_ping_task = PeriodicCallback( self.send_presence_ping, self.application.engine.presence_ping_interval ) self.presence_ping_task.start() self.application.add_connection(project_id, self.user, self.uid, self) if time_to_expire: self.expire_timeout = IOLoop.current().add_timeout( time.time() + project.get("connection_lifetime", 3600), self.expire ) body = { "client": self.uid, "expired": False, "ttl": project.get("connection_lifetime", 3600) if project.get("connection_check", False) else None } raise Return((body, None))
def on_error(self, error): logger.error(error) self._consistent = False self.structure_recover.start() raise Return((None, error))
def post(self, project_id): """ Handle API HTTP requests. """ if not self.request.body: raise tornado.web.HTTPError(400, log_message="empty request") sign = self.get_argument('sign', None) if not sign: raise tornado.web.HTTPError(400, log_message="no data sign") encoded_data = self.get_argument('data', None) if not encoded_data: raise tornado.web.HTTPError(400, log_message="no data") is_owner_request = False if project_id == self.application.MAGIC_PROJECT_ID: # API request aims to be from superuser is_owner_request = True if is_owner_request: # use api secret key from configuration to check sign secret = self.application.settings["config"].get("api_secret") if not secret: raise tornado.web.HTTPError( 501, log_message="no api_secret in configuration file") project = None else: project, error = yield self.application.structure.get_project_by_id( project_id) if error: raise tornado.web.HTTPError(500, log_message=str(error)) if not project: raise tornado.web.HTTPError(404, log_message="project not found") # use project secret key to validate sign secret = project['secret_key'] is_valid = auth.check_sign(secret, project_id, encoded_data, sign) if not is_valid: raise tornado.web.HTTPError(401, log_message="unauthorized") data = auth.decode_data(encoded_data) if not data: raise tornado.web.HTTPError(400, log_message="malformed data") response = Response() try: validate(data, req_schema) except ValidationError as e: response.error = str(e) else: req_id = data.get("uid", None) method = data.get("method") params = data.get("params") response.uid = req_id response.method = method schema = server_api_schema if is_owner_request and self.application.MAGIC_PROJECT_PARAM in params: project_id = params[self.application.MAGIC_PROJECT_PARAM] project, error = yield self.application.structure.get_project_by_id( project_id) if error: logger.error(error) response.error = self.application.INTERNAL_SERVER_ERROR if not project: response.error = self.application.PROJECT_NOT_FOUND try: params.pop(self.application.MAGIC_PROJECT_PARAM) except KeyError: pass if not is_owner_request and method in owner_api_methods: response.error = self.application.PERMISSION_DENIED if not response.error: if method not in schema: response.error = self.application.METHOD_NOT_FOUND else: try: validate(params, schema[method]) except ValidationError as e: response.error = str(e) else: result, error = yield self.application.process_call( project, method, params) response.body = result response.error = error self.json_response(response.as_message())
def authorize(self, auth_address, project, channel): """ Send POST request to web application to ask it if current client has a permission to subscribe on channel. """ project_id = self.project_id http_client = AsyncHTTPClient() request = HTTPRequest( auth_address, method="POST", body=urlencode({ 'user': self.user, 'channel': channel }), request_timeout=1 ) max_auth_attempts = project.get('max_auth_attempts') back_off_interval = project.get('back_off_interval') back_off_max_timeout = project.get('back_off_max_timeout') attempts = 0 while attempts < max_auth_attempts: # get current timeout for project current_attempts = self.application.back_off.setdefault(project_id, 0) factor = random.randint(0, 2**current_attempts-1) timeout = factor*back_off_interval if timeout > back_off_max_timeout: timeout = back_off_max_timeout # wait before next authorization request attempt yield sleep(float(timeout)/1000) try: response = yield http_client.fetch(request) except HTTPError as err: if err.code == 403: # access denied for this client raise Return((False, None)) else: # let it fail and try again after some timeout logger.info("{0} status code when fetching auth address {1}".format( err.code, auth_address )) except Exception as err: logger.error("error fetching auth address {0}".format(auth_address)) logger.exception(err) raise Return((False, None)) else: # reset back-off attempts self.application.back_off[project_id] = 0 if response.code == 200: # auth successful self.update_channel_user_info(response.body, channel) raise Return((True, None)) else: # access denied for this client raise Return((False, None)) attempts += 1 self.application.back_off[project_id] += 1 raise Return((False, None))
def on_select(self, res): if res != self.OK_RESPONSE: logger.error("state select database: {0}".format(res))
def handle_connect(self, params): """ Authenticate client's connection, initialize required variables in case of successful authentication. """ if self.application.collector: self.application.collector.incr('connect') self.application.collector.incr(self.sock.session.transport_name) if self.is_authenticated: raise Return((self.uid, None)) token = params["token"] user = params["user"] project_id = params["project"] timestamp = params["timestamp"] user_info = params.get("info") project, error = yield self.application.get_project(project_id) if error: raise Return((None, error)) secret_key = project['secret_key'] error_msg = self.validate_token(token, secret_key, project_id, user, timestamp, user_info) if error_msg: raise Return((None, error_msg)) if user_info is not None: try: user_info = json_decode(user_info) except Exception as err: logger.error("malformed JSON data in user_info") logger.error(err) user_info = None try: timestamp = int(timestamp) except ValueError: raise Return((None, "invalid timestamp")) self.user = user self.examined_at = timestamp if self.is_connection_must_be_checked(project, self.examined_at): # connection expired - this is a rare case when Centrifuge went offline # for a while or client turned on his computer from sleeping mode. # put this client into the queue of connections waiting for # permission to reconnect with expired credentials. To avoid waiting # client must reconnect with actual credentials i.e. reload browser # window. if project_id not in self.application.expired_reconnections: self.application.expired_reconnections[project_id] = [] self.application.expired_reconnections[project_id].append(self) if project_id not in self.application.expired_connections: self.application.expired_connections[project_id] = { "users": set(), "checked_at": None } self.application.expired_connections[project_id]["users"].add(user) self.connect_queue = toro.Queue(maxsize=1) value = yield self.connect_queue.get() if not value: yield self.close_sock() raise Return((None, self.application.UNAUTHORIZED)) else: self.connect_queue = None # Welcome to Centrifuge dear Connection! self.is_authenticated = True self.project_id = project_id self.token = token self.default_user_info = { 'user_id': self.user, 'client_id': self.uid, 'default_info': user_info, 'channel_info': None } self.channels = {} self.presence_ping_task = PeriodicCallback( self.send_presence_ping, self.application.engine.presence_ping_interval ) self.presence_ping_task.start() self.application.add_connection(project_id, self.user, self.uid, self) raise Return((self.uid, None))
def handle_connect(self, params): """ Authenticate client's connection, initialize required variables in case of successful authentication. """ if self.application.collector: self.application.collector.incr('connect') self.application.collector.incr(self.sock.session.transport_name) if self.is_authenticated: raise Return((self.uid, None)) project_name = params["project"] user = params["user"] info = params.get("info", "") if not self.application.INSECURE: token = params["token"] timestamp = params["timestamp"] else: token = timestamp = None project = self.application.get_project(project_name) if not project: raise Return((None, self.application.PROJECT_NOT_FOUND)) secret = project['secret'] if not self.application.INSECURE: error_msg = self.validate_token( token, secret, project_name, user, timestamp, info ) if error_msg: raise Return((None, error_msg)) if info: try: info = json_decode(info) except Exception as err: logger.error("malformed JSON data in user_info") logger.error(err) info = {} else: info = {} if not self.application.INSECURE: try: timestamp = int(timestamp) except ValueError: raise Return((None, "invalid timestamp")) else: # we are not interested in timestamp in case of insecure mode so just # set it to current timestamp timestamp = int(time.time()) self.user = user self.project_name = project_name self.timestamp = timestamp time_to_expire = None if not self.application.INSECURE and project['connection_lifetime'] > 0: now = time.time() conn_lifetime = project["connection_lifetime"] time_to_expire = self.timestamp + conn_lifetime - now if time_to_expire <= 0: raise Return(({"client": None, "expired": True, "ttl": conn_lifetime}, None)) # Welcome to Centrifuge dear Connection! self.is_authenticated = True self.default_info = { 'user': self.user, 'client': self.uid, 'default_info': info, 'channel_info': None } self.channels = {} self.presence_ping_task = PeriodicCallback( self.send_presence_ping, self.application.engine.presence_ping_interval ) self.presence_ping_task.start() self.application.add_connection(project_name, self.user, self.uid, self) conn_lifetime = project["connection_lifetime"] if time_to_expire: self.expire_timeout = IOLoop.current().add_timeout( time.time() + conn_lifetime, self.expire ) body = { "client": self.uid, "expired": False, "ttl": conn_lifetime if project["connection_lifetime"] > 0 else None } raise Return((body, None))
def handle_connect(self, params): """ Authenticate client's connection, initialize required variables in case of successful authentication. """ if self.application.collector: self.application.collector.incr('connect') self.application.collector.incr(self.sock.session.transport_name) if self.is_authenticated: raise Return((self.uid, None)) token = params["token"] user = params["user"] project_id = params["project"] timestamp = params["timestamp"] user_info = params.get("info") project, error = yield self.application.get_project(project_id) if error: raise Return((None, error)) secret_key = project['secret_key'] try: client_token = auth.get_client_token(secret_key, project_id, user, timestamp, user_info=user_info) except Exception as err: logger.error(err) raise Return((None, "invalid connection parameters")) if token != client_token: raise Return((None, "invalid token")) if user_info is not None: try: user_info = json_decode(user_info) except Exception as err: logger.error("malformed JSON data in user_info") logger.error(err) user_info = None try: timestamp = int(timestamp) except ValueError: raise Return((None, "invalid timestamp")) now = time.time() self.user = user self.examined_at = timestamp connection_check = project.get('connection_check', False) if connection_check and self.examined_at + project.get( "connection_lifetime", 24 * 365 * 3600) < now: # connection expired - this is a rare case when Centrifuge went offline # for a while or client turned on his computer from sleeping mode. # put this client into the queue of connections waiting for # permission to reconnect with expired credentials. To avoid waiting # client must reconnect with actual credentials i.e. reload browser # window. if project_id not in self.application.expired_reconnections: self.application.expired_reconnections[project_id] = [] self.application.expired_reconnections[project_id].append(self) if project_id not in self.application.expired_connections: self.application.expired_connections[project_id] = { "users": set(), "checked_at": None } self.application.expired_connections[project_id]["users"].add(user) self.connect_queue = toro.Queue(maxsize=1) value = yield self.connect_queue.get() if not value: yield self.close_sock() raise Return((None, self.application.UNAUTHORIZED)) else: self.connect_queue = None # Welcome to Centrifuge dear Connection! self.is_authenticated = True self.project_id = project_id self.token = token self.default_user_info = { 'user_id': self.user, 'client_id': self.uid, 'default_info': user_info, 'channel_info': None } self.channels = {} self.presence_ping_task = PeriodicCallback( self.send_presence_ping, self.application.engine.presence_ping_interval) self.presence_ping_task.start() self.application.add_connection(project_id, self.user, self.uid, self) raise Return((self.uid, None))