Exemple #1
0
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
Exemple #3
0
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