2018-01-29 13:47:21 +01:00
|
|
|
import os
|
2023-05-06 22:04:48 +02:00
|
|
|
import pathlib
|
|
|
|
import secrets
|
2019-02-07 14:26:10 +01:00
|
|
|
import threading
|
2018-01-04 02:45:23 +01:00
|
|
|
|
|
|
|
from multiprocessing import Process
|
2018-07-08 21:36:58 +02:00
|
|
|
|
2021-09-16 17:53:40 +02:00
|
|
|
try:
|
2023-05-05 00:07:13 +02:00
|
|
|
from websockets.exceptions import ConnectionClosed # type: ignore
|
2023-02-08 00:46:50 +01:00
|
|
|
from websockets import serve as websocket_serve # type: ignore
|
2021-09-16 17:53:40 +02:00
|
|
|
except ImportError:
|
2023-02-08 00:46:50 +01:00
|
|
|
from websockets import ConnectionClosed, serve as websocket_serve # type: ignore
|
2021-09-16 17:53:40 +02:00
|
|
|
|
2023-05-05 00:07:13 +02:00
|
|
|
import waitress
|
|
|
|
|
2019-02-23 21:19:00 +01:00
|
|
|
from platypush.backend import Backend
|
2019-02-24 00:11:35 +01:00
|
|
|
from platypush.backend.http.app import application
|
2023-05-02 21:24:50 +02:00
|
|
|
from platypush.bus.redis import RedisBus
|
2023-05-06 22:04:48 +02:00
|
|
|
from platypush.config import Config
|
2019-02-23 21:19:00 +01:00
|
|
|
from platypush.context import get_or_create_event_loop
|
2023-02-08 00:46:50 +01:00
|
|
|
from platypush.utils import get_ssl_server_context
|
2018-01-04 02:45:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HttpBackend(Backend):
|
2018-06-26 00:16:39 +02:00
|
|
|
"""
|
2021-02-22 01:20:01 +01:00
|
|
|
The HTTP backend is a general-purpose web server.
|
2018-06-26 00:16:39 +02:00
|
|
|
|
2021-02-22 01:20:01 +01:00
|
|
|
Example configuration:
|
2018-06-26 00:16:39 +02:00
|
|
|
|
2021-02-22 01:20:01 +01:00
|
|
|
.. code-block:: yaml
|
|
|
|
|
|
|
|
backend.http:
|
|
|
|
# Default HTTP listen port
|
|
|
|
port: 8008
|
|
|
|
# Default websocket port
|
|
|
|
websocket_port: 8009
|
|
|
|
# External folders that will be exposed over `/resources/<name>`
|
|
|
|
resource_dirs:
|
|
|
|
photos: /mnt/hd/photos
|
|
|
|
videos: /mnt/hd/videos
|
|
|
|
music: /mnt/hd/music
|
|
|
|
|
|
|
|
You can leverage this backend:
|
|
|
|
|
|
|
|
* To execute Platypush commands via HTTP calls. In order to do so:
|
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* Register a user to Platypush through the web panel (usually served on ``http://host:8008/``).
|
2021-02-22 01:20:01 +01:00
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* Generate a token for your user, either through the web panel (Settings -> Generate Token) or via API:
|
2021-02-22 01:20:01 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
|
|
|
curl -XPOST -H 'Content-Type: application/json' -d '
|
|
|
|
{
|
|
|
|
"username": "$YOUR_USER",
|
|
|
|
"password": "$YOUR_PASSWORD"
|
|
|
|
}' http://host:8008/auth
|
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* Execute actions through the ``/execute`` endpoint:
|
2021-02-22 01:20:01 +01:00
|
|
|
|
|
|
|
.. code-block:: shell
|
|
|
|
|
|
|
|
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
|
|
|
{
|
|
|
|
"type": "request",
|
|
|
|
"action": "tts.say",
|
|
|
|
"args": {
|
|
|
|
"text": "This is a test"
|
|
|
|
}
|
2021-02-22 02:53:20 +01:00
|
|
|
}' http://host:8008/execute
|
2018-06-26 00:16:39 +02:00
|
|
|
|
2019-05-15 09:31:04 +02:00
|
|
|
* To interact with your system (and control plugins and backends) through the Platypush web panel,
|
2021-02-22 01:20:01 +01:00
|
|
|
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 display a fullscreen dashboard with custom widgets.
|
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* Widgets are available as Vue.js components under ``platypush/backend/http/webapp/src/components/widgets``.
|
2021-02-22 01:20:01 +01:00
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* 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``:
|
2018-06-26 00:16:39 +02:00
|
|
|
|
2021-02-22 01:20:01 +01:00
|
|
|
.. code-block:: xml
|
2018-06-26 00:16:39 +02:00
|
|
|
|
2021-02-22 01:20:01 +01:00
|
|
|
<Dashboard>
|
|
|
|
<!-- Display the following widgets on the same row. Each row consists of 12 columns.
|
|
|
|
You can specify the width of each widget either through class name (e.g. col-6 means
|
|
|
|
6 columns out of 12, e.g. half the size of the row) or inline style
|
|
|
|
(e.g. `style="width: 50%"`). -->
|
|
|
|
<Row>
|
|
|
|
<!-- Show a calendar widget with the upcoming events. It requires the `calendar` plugin to
|
|
|
|
be enabled and configured. -->
|
|
|
|
<Calendar class="col-6" />
|
|
|
|
|
|
|
|
<!-- Show the current track and other playback info. It requires `music.mpd` plugin or any
|
|
|
|
other music plugin enabled. -->
|
|
|
|
<Music class="col-3" />
|
|
|
|
|
2022-04-27 14:52:41 +02:00
|
|
|
<!-- Show current date, time and weather.
|
|
|
|
It requires a `weather` plugin or backend enabled -->
|
2021-02-22 01:20:01 +01:00
|
|
|
<DateTimeWeather class="col-3" />
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
<!-- Display the following widgets on a second row -->
|
|
|
|
<Row>
|
|
|
|
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
2022-04-27 14:52:41 +02:00
|
|
|
explicitly exposed as an HTTP resource through the backend
|
|
|
|
`resource_dirs` attribute. -->
|
2021-02-22 01:20:01 +01:00
|
|
|
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
|
|
|
|
|
|
|
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
|
|
|
`http.poll` backend -->
|
|
|
|
<RssNews class="col-6" db="sqlite:////path/to/your/rss.db" />
|
|
|
|
</Row>
|
|
|
|
</Dashboard>
|
|
|
|
|
2021-02-22 01:35:42 +01:00
|
|
|
* The dashboard will be accessible under ``http://host:8008/dashboard/<name>``, where ``name=main`` if for
|
|
|
|
example you stored your template under ``~/.config/platypush/dashboards/main.xml``.
|
2021-02-22 01:20:01 +01:00
|
|
|
|
|
|
|
* 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
|
|
|
|
|
|
|
|
from platypush.context import get_plugin
|
|
|
|
from platypush.event.hook import hook
|
|
|
|
from platypush.message.event.http.hook import WebhookEvent
|
|
|
|
|
|
|
|
hook_token = 'abcdefabcdef'
|
|
|
|
|
|
|
|
# Expose the hook under the /hook/lights_toggle endpoint
|
|
|
|
@hook(WebhookEvent, hook='lights_toggle')
|
|
|
|
def lights_toggle(event, **context):
|
|
|
|
# Do any checks on the request
|
|
|
|
assert event.headers.get('X-Token') == hook_token, 'Unauthorized'
|
|
|
|
|
|
|
|
# Run some actions
|
|
|
|
lights = get_plugin('light.hue')
|
|
|
|
lights.toggle()
|
2019-02-23 21:19:00 +01:00
|
|
|
|
|
|
|
Any plugin can register custom routes under ``platypush/backend/http/app/routes/plugins``.
|
|
|
|
Any additional route is managed as a Flask blueprint template and the `.py`
|
|
|
|
module can expose lists of routes to the main webapp through the
|
|
|
|
``__routes__`` object (a list of Flask blueprints).
|
|
|
|
|
2021-02-22 01:20:01 +01:00
|
|
|
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=<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: <VALUE>``.
|
|
|
|
It can provided either over on the ``X-Token`` header or as a ``GET`` ``?token=<TOKEN>`` parameter.
|
|
|
|
* **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header
|
|
|
|
(cookie name: ``session_token``).
|
2018-07-08 21:36:58 +02:00
|
|
|
|
2018-06-26 00:16:39 +02:00
|
|
|
"""
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2019-02-23 21:19:00 +01:00
|
|
|
_DEFAULT_HTTP_PORT = 8008
|
|
|
|
_DEFAULT_WEBSOCKET_PORT = 8009
|
2019-02-07 14:26:10 +01:00
|
|
|
|
2022-04-27 14:52:41 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
port=_DEFAULT_HTTP_PORT,
|
|
|
|
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
|
|
|
bind_address='0.0.0.0',
|
|
|
|
disable_websocket=False,
|
|
|
|
resource_dirs=None,
|
|
|
|
ssl_cert=None,
|
|
|
|
ssl_key=None,
|
|
|
|
ssl_cafile=None,
|
|
|
|
ssl_capath=None,
|
|
|
|
maps=None,
|
2023-05-06 22:04:48 +02:00
|
|
|
secret_key_file=None,
|
2023-05-05 00:07:13 +02:00
|
|
|
flask_args=None,
|
2023-02-08 00:46:50 +01:00
|
|
|
**kwargs,
|
2022-04-27 14:52:41 +02:00
|
|
|
):
|
2018-06-26 00:16:39 +02:00
|
|
|
"""
|
|
|
|
:param port: Listen port for the web server (default: 8008)
|
|
|
|
:type port: int
|
|
|
|
|
|
|
|
:param websocket_port: Listen port for the websocket server (default: 8009)
|
|
|
|
:type websocket_port: int
|
|
|
|
|
2021-02-23 23:07:35 +01:00
|
|
|
:param bind_address: Address/interface to bind to (default: 0.0.0.0, accept connection from any IP)
|
|
|
|
:type bind_address: str
|
|
|
|
|
2018-06-26 00:16:39 +02:00
|
|
|
:param disable_websocket: Disable the websocket interface (default: False)
|
|
|
|
:type disable_websocket: bool
|
|
|
|
|
2018-11-01 23:43:02 +01:00
|
|
|
:param ssl_cert: Set it to the path of your certificate file if you want to enable HTTPS (default: None)
|
|
|
|
:type ssl_cert: str
|
|
|
|
|
|
|
|
:param ssl_key: Set it to the path of your key file if you want to enable HTTPS (default: None)
|
|
|
|
:type ssl_key: str
|
|
|
|
|
2019-05-15 09:31:04 +02:00
|
|
|
:param ssl_cafile: Set it to the path of your certificate authority file if you want to enable HTTPS
|
|
|
|
(default: None)
|
2018-11-01 23:43:02 +01:00
|
|
|
:type ssl_cafile: str
|
|
|
|
|
2019-05-15 09:31:04 +02:00
|
|
|
:param ssl_capath: Set it to the path of your certificate authority directory if you want to enable HTTPS
|
|
|
|
(default: None)
|
2018-11-01 23:43:02 +01:00
|
|
|
:type ssl_capath: str
|
|
|
|
|
2018-12-30 18:40:03 +01:00
|
|
|
:param resource_dirs: Static resources directories that will be
|
|
|
|
accessible through ``/resources/<path>``. It is expressed as a map
|
|
|
|
where the key is the relative path under ``/resources`` to expose and
|
|
|
|
the value is the absolute path to expose.
|
|
|
|
:type resource_dirs: dict[str, str]
|
|
|
|
|
2023-05-06 22:04:48 +02:00
|
|
|
:param secret_key_file: Path to the file containing the secret key that will be used by Flask
|
|
|
|
(default: ``~/.local/share/platypush/flask.secret.key``).
|
|
|
|
:type secret_key_file: str
|
|
|
|
|
2023-05-05 00:07:13 +02:00
|
|
|
:param flask_args: Extra key-value arguments that should be passed to the Flask service.
|
|
|
|
:type flask_args: dict[str, str]
|
2018-06-26 00:16:39 +02:00
|
|
|
"""
|
|
|
|
|
2018-01-04 02:45:23 +01:00
|
|
|
super().__init__(**kwargs)
|
2018-05-04 03:24:35 +02:00
|
|
|
|
2018-01-04 02:45:23 +01:00
|
|
|
self.port = port
|
2018-01-29 13:47:21 +01:00
|
|
|
self.websocket_port = websocket_port
|
2019-05-15 09:31:04 +02:00
|
|
|
self.maps = maps or {}
|
2019-02-21 16:15:06 +01:00
|
|
|
self.server_proc = None
|
2018-01-29 16:34:00 +01:00
|
|
|
self.disable_websocket = disable_websocket
|
2018-01-29 13:47:21 +01:00
|
|
|
self.websocket_thread = None
|
2021-02-23 23:07:35 +01:00
|
|
|
self._websocket_loop = None
|
2023-01-01 13:16:46 +01:00
|
|
|
self._service_registry_thread = None
|
2021-02-23 23:07:35 +01:00
|
|
|
self.bind_address = bind_address
|
2019-05-15 09:31:04 +02:00
|
|
|
|
|
|
|
if resource_dirs:
|
2022-04-27 14:52:41 +02:00
|
|
|
self.resource_dirs = {
|
|
|
|
name: os.path.abspath(os.path.expanduser(d))
|
|
|
|
for name, d in resource_dirs.items()
|
|
|
|
}
|
2019-05-15 09:31:04 +02:00
|
|
|
else:
|
|
|
|
self.resource_dirs = {}
|
|
|
|
|
2018-01-29 13:47:21 +01:00
|
|
|
self.active_websockets = set()
|
2023-05-05 00:07:13 +02:00
|
|
|
self.flask_args = flask_args or {}
|
2022-04-27 14:52:41 +02:00
|
|
|
self.ssl_context = (
|
|
|
|
get_ssl_server_context(
|
|
|
|
ssl_cert=ssl_cert,
|
|
|
|
ssl_key=ssl_key,
|
|
|
|
ssl_cafile=ssl_cafile,
|
|
|
|
ssl_capath=ssl_capath,
|
|
|
|
)
|
|
|
|
if ssl_cert
|
|
|
|
else None
|
|
|
|
)
|
2018-07-08 12:13:43 +02:00
|
|
|
|
2023-05-06 22:04:48 +02:00
|
|
|
self._workdir: str = Config.get('workdir') # type: ignore
|
|
|
|
assert self._workdir, 'The workdir is not set'
|
|
|
|
|
|
|
|
self.secret_key_file = os.path.expanduser(
|
|
|
|
secret_key_file
|
|
|
|
or os.path.join(self._workdir, 'flask.secret.key') # type: ignore
|
|
|
|
)
|
2023-02-08 00:46:50 +01:00
|
|
|
protocol = 'https' if ssl_cert else 'http'
|
|
|
|
self.local_base_url = f'{protocol}://localhost:{self.port}'
|
2020-02-06 01:04:36 +01:00
|
|
|
self._websocket_lock_timeout = 10
|
|
|
|
self._websocket_lock = threading.RLock()
|
|
|
|
self._websocket_locks = {}
|
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
def send_message(self, *_, **__):
|
2018-06-06 20:09:18 +02:00
|
|
|
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2019-02-23 21:19:00 +01:00
|
|
|
def on_stop(self):
|
2022-04-27 14:52:41 +02:00
|
|
|
"""On backend stop"""
|
2020-08-14 00:34:13 +02:00
|
|
|
super().on_stop()
|
2018-06-06 20:09:18 +02:00
|
|
|
self.logger.info('Received STOP event on HttpBackend')
|
2018-01-29 13:47:21 +01:00
|
|
|
|
2019-02-21 16:15:06 +01:00
|
|
|
if self.server_proc:
|
2023-05-05 00:07:13 +02:00
|
|
|
self.server_proc.terminate()
|
|
|
|
self.server_proc.join(timeout=10)
|
|
|
|
if self.server_proc.is_alive():
|
2019-02-25 10:52:48 +01:00
|
|
|
self.server_proc.kill()
|
2023-05-05 00:07:13 +02:00
|
|
|
if self.server_proc.is_alive():
|
|
|
|
self.logger.info(
|
|
|
|
'HTTP server process may be still alive at termination'
|
|
|
|
)
|
2019-02-25 10:52:48 +01:00
|
|
|
else:
|
2023-05-05 00:07:13 +02:00
|
|
|
self.logger.info('HTTP server process terminated')
|
2021-02-23 23:07:35 +01:00
|
|
|
|
2022-04-27 14:52:41 +02:00
|
|
|
if (
|
|
|
|
self.websocket_thread
|
|
|
|
and self.websocket_thread.is_alive()
|
|
|
|
and self._websocket_loop
|
|
|
|
):
|
2021-02-23 23:07:35 +01:00
|
|
|
self._websocket_loop.stop()
|
|
|
|
self.logger.info('HTTP websocket service terminated')
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2023-01-01 13:16:46 +01:00
|
|
|
if self._service_registry_thread and self._service_registry_thread.is_alive():
|
|
|
|
self._service_registry_thread.join(timeout=5)
|
|
|
|
self._service_registry_thread = None
|
|
|
|
|
2020-02-06 01:04:36 +01:00
|
|
|
def _acquire_websocket_lock(self, ws):
|
|
|
|
try:
|
2022-04-27 14:52:41 +02:00
|
|
|
acquire_ok = self._websocket_lock.acquire(
|
|
|
|
timeout=self._websocket_lock_timeout
|
|
|
|
)
|
2020-02-06 01:04:36 +01:00
|
|
|
if not acquire_ok:
|
|
|
|
raise TimeoutError('Websocket lock acquire timeout')
|
|
|
|
|
|
|
|
addr = ws.remote_address
|
|
|
|
if addr not in self._websocket_locks:
|
|
|
|
self._websocket_locks[addr] = threading.RLock()
|
|
|
|
finally:
|
|
|
|
self._websocket_lock.release()
|
|
|
|
|
2022-04-27 14:52:41 +02:00
|
|
|
acquire_ok = self._websocket_locks[addr].acquire(
|
|
|
|
timeout=self._websocket_lock_timeout
|
|
|
|
)
|
2020-02-06 01:04:36 +01:00
|
|
|
if not acquire_ok:
|
2023-02-08 00:46:50 +01:00
|
|
|
raise TimeoutError(f'Websocket on address {addr} not ready to receive data')
|
2020-02-06 01:04:36 +01:00
|
|
|
|
|
|
|
def _release_websocket_lock(self, ws):
|
2020-02-06 19:30:40 +01:00
|
|
|
try:
|
2022-04-27 14:52:41 +02:00
|
|
|
acquire_ok = self._websocket_lock.acquire(
|
|
|
|
timeout=self._websocket_lock_timeout
|
|
|
|
)
|
2020-02-06 19:30:40 +01:00
|
|
|
if not acquire_ok:
|
|
|
|
raise TimeoutError('Websocket lock acquire timeout')
|
|
|
|
|
|
|
|
addr = ws.remote_address
|
|
|
|
if addr in self._websocket_locks:
|
2020-02-06 01:04:36 +01:00
|
|
|
self._websocket_locks[addr].release()
|
2020-02-06 19:30:40 +01:00
|
|
|
except Exception as e:
|
2022-04-27 14:52:41 +02:00
|
|
|
self.logger.warning(
|
2023-02-08 00:46:50 +01:00
|
|
|
'Unhandled exception while releasing websocket lock: %s', e
|
2022-04-27 14:52:41 +02:00
|
|
|
)
|
2020-02-06 19:30:40 +01:00
|
|
|
finally:
|
|
|
|
self._websocket_lock.release()
|
2020-02-06 01:04:36 +01:00
|
|
|
|
2018-01-29 13:47:21 +01:00
|
|
|
def notify_web_clients(self, event):
|
2022-04-27 14:52:41 +02:00
|
|
|
"""Notify all the connected web clients (over websocket) of a new event"""
|
|
|
|
|
2019-05-15 09:31:04 +02:00
|
|
|
async def send_event(ws):
|
2018-10-20 19:27:15 +02:00
|
|
|
try:
|
2020-02-06 01:04:36 +01:00
|
|
|
self._acquire_websocket_lock(ws)
|
2019-05-15 09:31:04 +02:00
|
|
|
await ws.send(str(event))
|
2018-10-20 19:27:15 +02:00
|
|
|
except Exception as e:
|
2023-02-08 00:46:50 +01:00
|
|
|
self.logger.warning('Error on websocket send_event: %s', e)
|
2020-02-06 01:04:36 +01:00
|
|
|
finally:
|
|
|
|
self._release_websocket_lock(ws)
|
2018-01-29 13:47:21 +01:00
|
|
|
|
2018-10-26 21:55:49 +02:00
|
|
|
loop = get_or_create_event_loop()
|
2019-05-15 09:31:04 +02:00
|
|
|
wss = self.active_websockets.copy()
|
2020-02-06 19:30:40 +01:00
|
|
|
|
2020-02-06 01:04:36 +01:00
|
|
|
for _ws in wss:
|
2018-01-29 13:47:21 +01:00
|
|
|
try:
|
2020-02-06 01:04:36 +01:00
|
|
|
loop.run_until_complete(send_event(_ws))
|
2021-09-16 17:53:40 +02:00
|
|
|
except ConnectionClosed:
|
2022-04-27 14:52:41 +02:00
|
|
|
self.logger.warning(
|
2023-02-08 00:46:50 +01:00
|
|
|
'Websocket client %s connection lost', _ws.remote_address
|
2022-04-27 14:52:41 +02:00
|
|
|
)
|
2020-02-06 01:04:36 +01:00
|
|
|
self.active_websockets.remove(_ws)
|
|
|
|
if _ws.remote_address in self._websocket_locks:
|
|
|
|
del self._websocket_locks[_ws.remote_address]
|
2018-01-29 13:47:21 +01:00
|
|
|
|
|
|
|
def websocket(self):
|
2022-04-27 14:52:41 +02:00
|
|
|
"""Websocket main server"""
|
2018-01-29 16:34:00 +01:00
|
|
|
|
2018-01-29 13:47:21 +01:00
|
|
|
async def register_websocket(websocket, path):
|
2022-04-27 14:52:41 +02:00
|
|
|
address = (
|
|
|
|
websocket.remote_address
|
|
|
|
if websocket.remote_address
|
2018-11-01 23:57:50 +01:00
|
|
|
else '<unknown client>'
|
2022-04-27 14:52:41 +02:00
|
|
|
)
|
2018-11-01 23:57:50 +01:00
|
|
|
|
2022-04-27 14:52:41 +02:00
|
|
|
self.logger.info(
|
2023-02-08 00:46:50 +01:00
|
|
|
'New websocket connection from %s on path %s', address, path
|
2022-04-27 14:52:41 +02:00
|
|
|
)
|
2018-01-29 13:47:21 +01:00
|
|
|
self.active_websockets.add(websocket)
|
|
|
|
|
2018-05-06 11:38:24 +02:00
|
|
|
try:
|
|
|
|
await websocket.recv()
|
2021-09-16 17:53:40 +02:00
|
|
|
except ConnectionClosed:
|
2023-02-08 00:46:50 +01:00
|
|
|
self.logger.info('Websocket client %s closed connection', address)
|
2018-05-06 11:38:24 +02:00
|
|
|
self.active_websockets.remove(websocket)
|
2020-02-06 01:04:36 +01:00
|
|
|
if address in self._websocket_locks:
|
|
|
|
del self._websocket_locks[address]
|
2018-01-29 13:47:21 +01:00
|
|
|
|
2018-11-01 23:57:50 +01:00
|
|
|
websocket_args = {}
|
|
|
|
if self.ssl_context:
|
|
|
|
websocket_args['ssl'] = self.ssl_context
|
|
|
|
|
2021-02-23 23:07:35 +01:00
|
|
|
self._websocket_loop = get_or_create_event_loop()
|
|
|
|
self._websocket_loop.run_until_complete(
|
2022-04-27 14:52:41 +02:00
|
|
|
websocket_serve(
|
|
|
|
register_websocket,
|
|
|
|
self.bind_address,
|
|
|
|
self.websocket_port,
|
2023-02-08 00:46:50 +01:00
|
|
|
**websocket_args,
|
2022-04-27 14:52:41 +02:00
|
|
|
)
|
|
|
|
)
|
2021-02-23 23:07:35 +01:00
|
|
|
self._websocket_loop.run_forever()
|
2018-01-29 13:47:21 +01:00
|
|
|
|
2023-05-06 22:04:48 +02:00
|
|
|
def _get_secret_key(self, _create=False):
|
|
|
|
if _create:
|
|
|
|
self.logger.info('Creating web server secret key')
|
|
|
|
pathlib.Path(self.secret_key_file).parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(self.secret_key_file, 'w') as f:
|
|
|
|
f.write(secrets.token_urlsafe(32))
|
|
|
|
|
|
|
|
os.chmod(self.secret_key_file, 0o600)
|
|
|
|
return secrets.token_urlsafe(32)
|
|
|
|
|
|
|
|
try:
|
|
|
|
with open(self.secret_key_file, 'r') as f:
|
|
|
|
return f.read()
|
|
|
|
except IOError as e:
|
|
|
|
if not _create:
|
|
|
|
return self._get_secret_key(_create=True)
|
|
|
|
|
|
|
|
raise e
|
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
def _web_server_proc(self):
|
2019-02-23 21:19:00 +01:00
|
|
|
def proc():
|
2023-02-08 00:46:50 +01:00
|
|
|
self.logger.info('Starting local web server on port %s', self.port)
|
2019-02-23 21:19:00 +01:00
|
|
|
kwargs = {
|
2021-02-23 23:07:35 +01:00
|
|
|
'host': self.bind_address,
|
2019-02-23 21:19:00 +01:00
|
|
|
'port': self.port,
|
|
|
|
}
|
2018-11-01 23:43:02 +01:00
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
assert isinstance(
|
|
|
|
self.bus, RedisBus
|
|
|
|
), 'The HTTP backend only works if backed by a Redis bus'
|
|
|
|
|
2021-03-06 19:22:13 +01:00
|
|
|
application.config['redis_queue'] = self.bus.redis_queue
|
2023-05-06 22:04:48 +02:00
|
|
|
application.secret_key = self._get_secret_key()
|
|
|
|
|
2019-02-23 21:19:00 +01:00
|
|
|
if self.ssl_context:
|
|
|
|
kwargs['ssl_context'] = self.ssl_context
|
2023-05-05 00:07:13 +02:00
|
|
|
if self.flask_args:
|
|
|
|
kwargs.update(self.flask_args)
|
2019-02-23 21:19:00 +01:00
|
|
|
|
2023-05-05 00:07:13 +02:00
|
|
|
waitress.serve(application, **kwargs)
|
2019-02-23 21:19:00 +01:00
|
|
|
|
|
|
|
return proc
|
2018-11-01 23:43:02 +01:00
|
|
|
|
2023-01-01 13:16:46 +01:00
|
|
|
def _register_service(self):
|
2021-10-24 02:51:05 +02:00
|
|
|
try:
|
|
|
|
self.register_service(port=self.port)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.warning('Could not register the Zeroconf service')
|
|
|
|
self.logger.exception(e)
|
2018-01-29 16:34:00 +01:00
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
def _start_websocket_server(self):
|
2018-01-29 16:34:00 +01:00
|
|
|
if not self.disable_websocket:
|
2019-02-24 12:35:26 +01:00
|
|
|
self.logger.info('Initializing websocket interface')
|
2023-02-08 00:46:50 +01:00
|
|
|
self.websocket_thread = threading.Thread(
|
|
|
|
target=self.websocket,
|
|
|
|
name='WebsocketServer',
|
|
|
|
)
|
2018-01-29 16:34:00 +01:00
|
|
|
self.websocket_thread.start()
|
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
def _start_zeroconf_service(self):
|
|
|
|
self._service_registry_thread = threading.Thread(
|
|
|
|
target=self._register_service,
|
|
|
|
name='ZeroconfService',
|
|
|
|
)
|
|
|
|
self._service_registry_thread.start()
|
|
|
|
|
|
|
|
def _run_web_server(self):
|
2023-05-05 00:07:13 +02:00
|
|
|
self.server_proc = Process(target=self._web_server_proc(), name='WebServer')
|
|
|
|
self.server_proc.start()
|
|
|
|
self.server_proc.join()
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2023-05-02 21:24:50 +02:00
|
|
|
def run(self):
|
|
|
|
super().run()
|
|
|
|
|
|
|
|
self._start_websocket_server()
|
|
|
|
self._start_zeroconf_service()
|
|
|
|
self._run_web_server()
|
2023-01-01 13:16:46 +01:00
|
|
|
|
2018-01-04 02:45:23 +01:00
|
|
|
|
2019-01-07 15:34:31 +01:00
|
|
|
# vim:sw=4:ts=4:et:
|