class WebAPI(object): def __init__(self, oauth_config=None): self.app = Flask(__name__) self.app.response_class = IppResponse self.app.before_first_request(self.torun) self.sockets = Sockets(self.app) self.socks = dict() self.port = 0 # authentication/authorisation self._oauth_config = oauth_config self._authorize = self.default_authorize self._authenticate = self.default_authenticate self.add_routes(self, basepath='') # If the `fix_proxy` option is "enabled", interpret it as "use one proxy" if _config.get('fix_proxy') == 'enabled': if HAS_WERKZEUG_MIDDLEWARE: self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) else: self.app.wsgi_app = ProxyFix(self.app.wsgi_app) # If the `fix_proxy` option is a number, interpret it as the number of proxies to trust (Werkzeug>=0.15.0) elif HAS_WERKZEUG_MIDDLEWARE: try: proxy_count = int(_config.get('fix_proxy')) if proxy_count > 0: self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_for=proxy_count, x_proto=proxy_count, x_host=proxy_count, x_port=proxy_count, x_prefix=proxy_count) except ValueError: # The `fix_proxy` option wasn't a number, so leave it off pass def add_routes(self, routesObject, basepath): assert not basepath.endswith('/'), "basepath must not end with a slash" def dummy(f): @wraps(f) def inner(*args, **kwargs): return f(*args, **kwargs) return inner def getbases(cls): bases = list(cls.__bases__) for x in cls.__bases__: bases += getbases(x) return bases # Populate a list of API paths to be used later for disambiguation api_paths = [] for cls in [ routesObject.__class__, ] + getbases(routesObject.__class__): for attr in cls.__dict__.keys(): routesMethod = getattr(routesObject, attr) if hasattr(routesMethod, "common_path"): api_paths.append(routesMethod.common_path) for cls in [ routesObject.__class__, ] + getbases(routesObject.__class__): for attr in cls.__dict__.keys(): routesMethod = getattr(routesObject, attr) if callable(routesMethod): endpoint = "{}_{}".format(basepath.replace('/', '_'), routesMethod.__name__) if hasattr(routesMethod, 'app_methods' ) and routesMethod.app_methods is not None: methods = routesMethod.app_methods else: methods = ["GET", "HEAD"] if hasattr(routesMethod, 'app_headers' ) and routesMethod.app_headers is not None: headers = routesMethod.app_headers else: headers = [] crossdomain_methods = [] if hasattr(routesMethod, "common_path"): stripped_path = routesMethod.common_path.rstrip("/") trailing_slash = routesMethod.common_path.endswith("/") if trailing_slash and stripped_path not in api_paths or not trailing_slash: crossdomain_methods = [ "OPTIONS", ] if hasattr(routesMethod, "secure_route"): self.app.route( basepath + routesMethod.secure_route, endpoint=endpoint, methods=methods + crossdomain_methods)(crossdomain( origin=routesMethod.app_origin, methods=methods, headers=headers + [ 'Content-Type', 'Authorization', 'token', ])(returns_requires_auth(routesMethod))) elif hasattr(routesMethod, "response_route"): self.app.route(basepath + routesMethod.response_route, endpoint=endpoint, methods=["GET", "POST", "HEAD"] + crossdomain_methods)(crossdomain( origin='*', methods=['GET', 'POST', 'HEAD'], headers=[ 'Content-Type', 'Authorization', ])(returns_response(routesMethod))) elif hasattr(routesMethod, "app_route"): if routesMethod.app_auto_json: self.app.route(basepath + routesMethod.app_route, endpoint=endpoint, methods=methods + crossdomain_methods)(crossdomain( origin=routesMethod.app_origin, methods=methods, headers=headers + [ 'Content-Type', 'Authorization', ])(returns_json(routesMethod))) else: self.app.route(basepath + routesMethod.app_route, endpoint=endpoint, methods=methods + crossdomain_methods)(crossdomain( origin=routesMethod.app_origin, methods=methods, headers=headers + [ 'Content-Type', 'Authorization', ])(dummy(routesMethod))) elif hasattr(routesMethod, "app_file_route"): self.app.route(basepath + routesMethod.app_file_route, endpoint=endpoint, methods=methods + crossdomain_methods)( crossdomain( origin='*', methods=methods, headers=headers + [ 'Content-Type', 'Authorization', ])(returns_file(routesMethod))) elif hasattr(routesMethod, "app_resource_route"): f = crossdomain(origin='*', methods=methods, headers=headers + [ 'Content-Type', 'Authorization', 'api-key', ])(returns_json( obj_path_access(routesMethod))) self.app.route(basepath + routesMethod.app_resource_route, methods=methods + crossdomain_methods, endpoint=endpoint)(f) f.__name__ = endpoint + '_path' self.app.route(basepath + routesMethod.app_resource_route + '<path:path>/', methods=methods + crossdomain_methods, endpoint=f.__name__)(f) elif hasattr(routesMethod, "socket_path"): websocket_opened = getattr(routesObject, "on_websocket_connect", None) if websocket_opened is None: self.sockets.route( basepath + routesMethod.socket_path, endpoint=endpoint)(self.handle_sock( expects_json(routesMethod), self.socks)) else: self.sockets.route( basepath + routesMethod.socket_path, endpoint=endpoint)(websocket_opened( expects_json(routesMethod))) elif hasattr(routesMethod, "errorhandler_args"): if routesMethod.errorhandler_args: self.app.errorhandler( *routesMethod.errorhandler_args, **routesMethod.errorhandler_kwargs)( crossdomain(origin='*', methods=[ "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD" ])(dummy(routesMethod))) else: for n in range(400, 600): try: # Apply errorhandlers for all known error codes self.app.errorhandler(n)(crossdomain( origin='*', methods=[ "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD" ])(dummy(routesMethod))) except KeyError: # Some error codes aren't valid pass @errorhandler() def error(self, e): if request.method == 'HEAD': if isinstance(e, HTTPException): return IppResponse('', status=e.code) else: return IppResponse('', 500) bm = request.accept_mimetypes.best_match( ['application/json', 'text/html']) (exceptionType, exceptionParam, trace) = sys.exc_info() if bm == 'text/html': if isinstance(e, HTTPException): if e.code == 400: status_code = e.code response_title = "{}: {}".format(e.code, e.description) else: return e.get_response() else: status_code = 500 response_title = '500: Internal Exception' return IppResponse(highlight( '\n'.join( traceback.format_exception(exceptionType, exceptionParam, trace)), PythonTracebackLexer(), HtmlFormatter(linenos='table', full=True, title=response_title)), status=status_code, mimetype='text/html') else: headers = e.headers if hasattr(e, 'headers') else None if isinstance(e, HTTPException): status_code = e.code error_description = e.description elif isinstance(e, AuthlibBaseError): status_code = e.status_code if getattr(e, "status_code", None) else 400 error_description = str(e) else: status_code = 500 error_description = 'Internal Error' response = { 'code': status_code, 'error': error_description, 'debug': str({ 'traceback': [str(x) for x in traceback.extract_tb(trace)], 'exception': [ str(x) for x in traceback.format_exception_only( exceptionType, exceptionParam) ] }) } return IppResponse(json.dumps(response), status=status_code, mimetype='application/json', headers=headers) def torun(self): # pragma: no cover pass def stop(self): # pragma: no cover pass def handle_sock(self, func, socks): @wraps(func) def inner_func(ws, **kwds): sock_uuid = uuid.uuid4() socks[sock_uuid] = ws print("Opening Websocket {} at path /, Receiving ...".format( sock_uuid)) while True: try: message = ws.receive() except Exception: message = None if message is not None: func(ws, message, **kwds) continue else: print("Websocket {} closed".format(sock_uuid)) del socks[sock_uuid] break return inner_func def default_authorize(self, token): if self._oauth_config is not None: # Ensure the user is permitted to use function loginserver = self._oauth_config['loginserver'] proxies = self._oauth_config['proxies'] whitelist = self._oauth_config['access_whitelist'] result = proxied_request(uri="{}/check-token".format(loginserver), headers={'token': token}, proxies=proxies) json_payload = json.loads(result[1]) return json_payload['userid'] in whitelist else: return True def default_authenticate(self, token): if self._oauth_config is not None: # Validate the token that the webapp sends loginserver = self._oauth_config['loginserver'] proxies = self._oauth_config['proxies'] if token is None: return False result = proxied_request(uri="{}/check-token".format(loginserver), headers={'token': token}, proxies=proxies) return result[0].code == 200 and json.loads( result[1])['token'] == token else: return True def authorize(self, f): self._authorize = f return f def authenticate(self, f): self._authenticate = f return f
class WebAPI(object): def __init__(self, oauth_config=None): self.app = Flask(__name__) self.app.response_class = IppResponse self.app.before_first_request(self.torun) self.sockets = Sockets(self.app) self.socks = dict() self.port = 0 # authentication/authorisation self._oauth_config = oauth_config self._authorize = self.default_authorize self._authenticate = self.default_authenticate self.add_routes(self, basepath='') def add_routes(self, cl, basepath): assert not basepath.endswith('/'), "basepath must not end with a slash" def dummy(f): @wraps(f) def inner(*args, **kwargs): return f(*args, **kwargs) return inner def getbases(cl): bases = list(cl.__bases__) for x in cl.__bases__: bases += getbases(x) return bases for klass in [ cl.__class__, ] + getbases(cl.__class__): for name in klass.__dict__.keys(): value = getattr(cl, name) if callable(value): endpoint = "{}_{}".format(basepath.replace('/', '_'), value.__name__) if hasattr(value, "secure_route"): self.app.route(basepath + value.secure_route, endpoint=endpoint, methods=value.app_methods + ["OPTIONS"])(crossdomain( origin=value.app_origin, methods=value.app_methods, headers=value.app_headers + [ 'Content-Type', 'token', ])(returns_requires_auth(value))) elif hasattr(value, "response_route"): self.app.route( basepath + value.response_route, endpoint=endpoint, methods=["GET", "POST", "HEAD", "OPTIONS"])( crossdomain(origin='*', methods=['GET', 'POST', 'HEAD'], headers=[ 'Content-Type', ])(returns_response(value))) elif hasattr(value, "app_route"): if value.app_auto_json: if value.app_methods: self.app.route( basepath + value.app_route, endpoint=endpoint, methods=value.app_methods + [ "OPTIONS", ])(crossdomain(origin=value.app_origin, methods=value.app_methods, headers=value.app_headers + [ "Content-Type", ])(returns_json(value))) else: self.app.route( basepath + value.app_route, endpoint=endpoint, methods=["GET", "HEAD", "OPTIONS"])(crossdomain( origin=value.app_origin, methods=["GET", "HEAD"], headers=value.app_headers + [ "Content-Type", ])(returns_json(value))) else: if value.app_methods: self.app.route(basepath + value.app_route, endpoint=endpoint, methods=value.app_methods + [ "OPTIONS", ])(crossdomain( origin=value.app_origin, methods=value.app_methods, headers=[ "Content-Type", ])(dummy(value))) else: self.app.route( basepath + value.app_route, endpoint=endpoint, methods=["GET", "HEAD", "OPTIONS"])( crossdomain(origin=value.app_origin, methods=["GET", "HEAD"], headers=[ "Content-Type", ])(dummy(value))) elif hasattr(value, "app_file_route"): self.app.route(basepath + value.app_file_route, endpoint=endpoint, methods=value.app_methods)(crossdomain( origin='*', methods=['GET', 'HEAD'], headers=[ 'Content-Type', ])(returns_file(value))) elif hasattr(value, "app_resource_route"): if value.app_methods: f = crossdomain(origin='*', methods=value.app_methods, headers=[ "Content-Type", "api-key", ])(returns_json( obj_path_access(value))) self.app.route(basepath + value.app_resource_route, methods=value.app_methods + [ "OPTIONS", ], endpoint=endpoint)(f) f.__name__ = endpoint + '_path' self.app.route(basepath + value.app_resource_route + '<path:path>/', methods=value.app_methods + [ "OPTIONS", ], endpoint=f.__name__)(f) else: f = crossdomain(origin='*', methods=["GET", "HEAD"], headers=[ "Content-Type", "api-key", ])(returns_json( obj_path_access(value))) self.app.route(basepath + value.app_resource_route, methods=["GET", "HEAD", "OPTIONS"], endpoint=f.__name__)(f) f.__name__ = endpoint + '_path' self.app.route(basepath + value.app_resource_route + '<path:path>/', methods=["GET", "HEAD", "OPTIONS"], endpoint=f.__name__)(f) elif hasattr(value, "sockets_on"): socket_recv_gen = getattr(cl, "on_websocket_connect", None) if socket_recv_gen is None: f = self.handle_sock(value, self.socks) else: f = socket_recv_gen(value) self.sockets.route(basepath + value.sockets_on, endpoint=endpoint)(f) elif hasattr(value, "errorhandler_args"): if value.errorhandler_args: self.app.errorhandler( *value.errorhandler_args, **value.errorhandler_kwargs)(crossdomain( origin='*', methods=[ "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD" ])(dummy(value))) else: for n in range(400, 600): self.app.errorhandler(n)(crossdomain( origin='*', methods=[ "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD" ])(dummy(value))) @errorhandler(301) def __redirect(self, e): return e.get_response(request.environ) @errorhandler() def error(self, e): if request.method == 'HEAD': if isinstance(e, HTTPException): return IppResponse('', status=e.code) else: return IppResponse('', 500) bm = request.accept_mimetypes.best_match( ['application/json', 'text/html']) if bm == 'text/html': if isinstance(e, HTTPException): if e.code == 400: (t, v, tb) = sys.exc_info() return IppResponse(highlight( '\n'.join(traceback.format_exception(t, v, tb)), PythonTracebackLexer(), HtmlFormatter(linenos='table', full=True, title="{}: {}".format( e.code, e.description))), status=e.code, mimetype='text/html') return e.get_response() (t, v, tb) = sys.exc_info() return IppResponse(highlight( '\n'.join(traceback.format_exception(t, v, tb)), PythonTracebackLexer(), HtmlFormatter(linenos='table', full=True, title='500: Internal Exception')), status=500, mimetype='text/html') else: t, v, tb = sys.exc_info() if isinstance(e, HTTPException): response = { 'code': e.code, 'error': e.description, 'debug': { 'traceback': [x for x in traceback.extract_tb(tb)], 'exception': [x for x in traceback.format_exception_only(t, v)] } } return IppResponse(json.dumps(response), status=e.code, mimetype='application/json') response = { 'code': 500, 'error': 'Internal Error', 'debug': { 'traceback': [x for x in traceback.extract_tb(tb)], 'exception': [x for x in traceback.format_exception_only(t, v)] } } return IppResponse(json.dumps(response), status=500, mimetype='application/json') def torun(self): pass def stop(self): pass def handle_sock(self, func, socks): @wraps(func) def inner_func(ws): sock_uuid = uuid.uuid4() socks[sock_uuid] = ws print "Opening Websocket {} at path /, Receiving ...".format( sock_uuid) while True: try: message = ws.receive() except: message = None if message is not None: func(ws, message) continue else: print "Websocket {} closed".format(sock_uuid) del socks[sock_uuid] break return inner_func def default_authorize(self, token): if self._oauth_config is not None: # Ensure the user is permitted to use function print('authorizing: {}'.format(token)) loginserver = self._oauth_config['loginserver'] proxies = self._oauth_config['proxies'] whitelist = self._oauth_config['access_whitelist'] result = proxied_request(uri="{}/check-token".format(loginserver), headers={'token': token}, proxies=proxies) json_payload = json.loads(result[1]) return json_payload['userid'] in whitelist else: return True def default_authenticate(self, token): if self._oauth_config is not None: # Validate the token that the webapp sends loginserver = self._oauth_config['loginserver'] proxies = self._oauth_config['proxies'] print('authenticating: {}'.format(token)) if token is None: return False result = proxied_request(uri="{}/check-token".format(loginserver), headers={'token': token}, proxies=proxies) print('token result code: {}'.format(result[0].code)) print('result payload: {}'.format(result[1])) return result[0].code == 200 and json.loads( result[1])['token'] == token else: return True def authorize(self, f): self._authorize = f return f def authenticate(self, f): self._authenticate = f return f
class Han: def __init__(self, flask_app, initial_state, debug=False): self.current_state_key = key = uuid() self.state_entries = [initial_state_entry(key, initial_state)] self.handlers = {} self.state_updates = [] self.sockets = Sockets(flask_app) self.add_api() if debug: self.add_debug_api(flask_app) def add_api(self): "Add the websocket routes to let Han communicate with the frontend." self.sockets.route("/state/<path>")(self.state_updates_route) self.sockets.route("/action")(self.actions_route) def add_debug_api(self, app): app.route("/debug")(self.debug_route) app.route("/debug/states")(self.return_states_route) app.route("/debug/state", methods=["POST"])(self.set_state_route) @property def state_entry(self): "Getter for the 'state_entry' property." for state_entry in self.state_entries: if state_entry.key == self.current_state_key: return state_entry raise KeyError("Current state doesn't exist!") @property def state(self): return self.state_entry.state @staticmethod def debug_route(): "Send the Debug HTML page." return flask.send_file("han/debug/debug.html") def set_state_route(self): "Debug route for setting the current state." data = flask.request.get_json(force=True) self.current_state_key = data["id"] entry = self.state_entry self.update_all_clients(entry.path, entry.new_data) return flask.jsonify({"message": "Done!"}) def update_all_clients(self, path, new_state): "Send a state update to all clients." for update_queue in self.state_updates: update_queue.put(StateUpdate(path, new_state)) def return_states_route(self): "Return a list of states, along with the actions which caused them." return flask.jsonify({ "states": [{ "state": entry.state, "action": entry.action, "id": entry.key, "diff": { "path": entry.path, "data": entry.new_data, "old_data": entry.old_data } } for entry in self.state_entries] }) def dispatch_action(self, action): "Dispatch an action to a handler, if there is one. Update our state." properties = {k: v for (k, v) in action.items() if k != "type"} handler, input_path, output_path = self.handlers[action["type"]] input_data = value_at(self.state, input_path) output_data = handler(input_data, **properties) self.update_all_clients(output_path, output_data) self.set_state_at(output_path, output_data, action) def set_state_at(self, path, new_data, action): "Write `data` to the state at the given JSON path." # What happens if the current state isn't the last entry? if self.state_entries[-1].key != self.current_state_key: raise NotImplementedError( "Illegal state: write some branch logic!") # Store the data. old_data = value_at(self.state, path) # Set, or completely replace, the current state. if path == "$": new_state = new_data else: new_state = set_value_at(self.state, path, new_data) # Create a new state entry with the data changes. new_id = uuid() self.state_entries.append( StateEntry(new_id, action, new_state, path, new_data, old_data)) self.current_state_key = new_id def state_updates_route(self, socket, path): "Send state updates over a websocket connection." updates = Queue() self.state_updates.append(updates) # Send an initial state. socket.send(json.dumps(value_at(self.state, path))) while not socket.closed: state_update = updates.get() if mutual_contains(path, state_update.path): socket.send(json.dumps(value_at(self.state, path))) def actions_route(self, socket): "Receive actions over a websocket connection." while not socket.closed: action = socket.receive() if action: self.dispatch_action(json.loads(action)) def dle(self, action, input_path="$", output_path=None): "Return something which maps a given function to an action." def map_action_to(function): """ When `action` is dispatched, we should call `function` with arguments `state[input_path]` and any properties in the action. Then take the result, and assign it to `state[output_path]`. """ out_path = output_path or input_path self.handlers[action.name] = ActionHandler(function, input_path, out_path) return function return map_action_to