From a069d23bb7545ead3eb092c5dab6ae82ec80016b Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 May 2023 02:40:32 +0200 Subject: [PATCH] [#260] Added ``/ws/requests`` websocket route. --- platypush/backend/http/__init__.py | 74 ++++++++++++++++------- platypush/backend/http/app/ws/requests.py | 49 +++++++++++++++ 2 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 platypush/backend/http/app/ws/requests.py diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index e298b204ed..3e03ba6b41 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -41,9 +41,11 @@ class HttpBackend(Backend): * To execute Platypush commands via HTTP calls. In order to do so: - * Register a user to Platypush through the web panel (usually served on ``http://host:8008/``). + * Register a user to Platypush through the web panel (usually + served on ``http://host:8008/``). - * Generate a token for your user, either through the web panel (Settings -> Generate Token) or via API: + * Generate a token for your user, either through the web panel + (Settings -> Generate Token) or via API: .. code-block:: shell @@ -66,16 +68,35 @@ class HttpBackend(Backend): } }' http://host:8008/execute - * To interact with your system (and control plugins and backends) through the Platypush web panel, - by default available on ``http://host:8008/``. Any configured plugin that has an available panel - plugin will be automatically added to the web panel. + * To interact with your system (and control plugins and backends) + through the Platypush web panel, by default available on + ``http://host:8008/``. Any configured plugin that has an available + panel plugin will be automatically added to the web panel. + + * To create asynchronous integrations with Platypush over websockets. + Two routes are available: + + * ``/ws/events`` - Subscribe to this websocket to receive the + events generated by the application. + * ``/ws/requests`` - Subscribe to this websocket to send commands + to Platypush and receive the response asynchronously. + + You will have to authenticate your connection to these websockets, + just like the ``/execute`` endpoint. In both cases, you can pass the + token either via ``Authorization: Bearer``, via the ``token`` query + string or body parameter, or leverage ``Authorization: Basic`` with + username and password (not advised), or use a valid ``session_token`` + cookie from an authenticated web panel session. * To display a fullscreen dashboard with custom widgets. - * Widgets are available as Vue.js components under ``platypush/backend/http/webapp/src/components/widgets``. + * Widgets are available as Vue.js components under + ``platypush/backend/http/webapp/src/components/widgets``. - * Explore their options (some may require some plugins or backends to be configured in order to work) and - create a new dashboard template under ``~/.config/platypush/dashboards``- e.g. ``main.xml``: + * Explore their options (some may require some plugins or backends + to be configured in order to work) and create a new dashboard + template under ``~/.config/platypush/dashboards``- e.g. + ``main.xml``: .. code-block:: xml @@ -111,13 +132,17 @@ class HttpBackend(Backend): - * The dashboard will be accessible under ``http://host:8008/dashboard/``, where ``name=main`` if for - example you stored your template under ``~/.config/platypush/dashboards/main.xml``. + * The dashboard will be accessible under + ``http://host:8008/dashboard/``, where ``name=main`` if for + example you stored your template under + ``~/.config/platypush/dashboards/main.xml``. - * To expose custom endpoints that can be called as web hooks by other applications and run some custom logic. - All you have to do in this case is to create a hook on a - :class:`platypush.message.event.http.hook.WebhookEvent` with the endpoint that you want to expose and store - it under e.g. ``~/.config/platypush/scripts/hooks.py``: + * To expose custom endpoints that can be called as web hooks by other + applications and run some custom logic. All you have to do in this case + is to create a hook on a + :class:`platypush.message.event.http.hook.WebhookEvent` with the + endpoint that you want to expose and store it under e.g. + ``~/.config/platypush/scripts/hooks.py``: .. code-block:: python @@ -142,16 +167,21 @@ class HttpBackend(Backend): module can expose lists of routes to the main webapp through the ``__routes__`` object (a list of Flask blueprints). - Security: Access to the endpoints requires at least one user to be registered. Access to the endpoints is regulated - in the following ways (with the exception of event hooks, whose logic is up to the user): + Security: Access to the endpoints requires at least one user to be + registered. Access to the endpoints is regulated in the following ways + (with the exception of event hooks, whose logic is up to the user): * **Simple authentication** - i.e. registered username and password. - * **JWT token** provided either over as ``Authorization: Bearer`` header or ``GET`` ``?token=`` - parameter. A JWT token can be generated either through the web panel or over the ``/auth`` endpoint. - * **Global platform token**, usually configured on the root of the ``config.yaml`` as ``token: ``. - It can provided either over on the ``X-Token`` header or as a ``GET`` ``?token=`` parameter. - * **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header - (cookie name: ``session_token``). + * **JWT token** provided either over as ``Authorization: Bearer`` + header or ``GET`` ``?token=`` parameter. A JWT token can be + generated either through the web panel or over the ``/auth`` + endpoint. + * **Global platform token**, usually configured on the root of the + ``config.yaml`` as ``token: ``. It can provided either over on + the ``X-Token`` header or as a ``GET`` ``?token=`` parameter. + * **Session token**, generated upon login, it can be used to + authenticate requests through the ``Cookie`` header (cookie name: + ``session_token``). """ diff --git a/platypush/backend/http/app/ws/requests.py b/platypush/backend/http/app/ws/requests.py new file mode 100644 index 0000000000..6fec50f76f --- /dev/null +++ b/platypush/backend/http/app/ws/requests.py @@ -0,0 +1,49 @@ +from threading import Thread, current_thread +from typing import Set +from typing_extensions import override + +from platypush.backend.http.app.utils import send_message +from platypush.message.request import Request + +from . import WSRoute, logger + + +class WSRequestsProxy(WSRoute): + """ + Websocket event proxy mapped to ``/ws/requests``. + """ + + _max_concurrent_requests: int = 10 + """ Maximum number of concurrent requests allowed on the same connection. """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._requests: Set[Thread] = set() + + @classmethod + @override + def app_name(cls) -> str: + return 'requests' + + def _handle_request(self, request: Request): + self._requests.add(current_thread()) + try: + response = send_message(request, wait_for_response=True) + self.send(str(response)) + finally: + self._requests.remove(current_thread()) + + def on_message(self, message): + if len(self._requests) > self._max_concurrent_requests: + logger.info('Too many concurrent requests on %s', self) + return + + try: + msg = Request.build(message) + assert isinstance(msg, Request), f'Expected {Request}, got {type(msg)}' + except Exception as e: + logger.info('Could not build request from %s: %s', message, e) + logger.exception(e) + return + + Thread(target=self._handle_request, args=(msg,)).start()