From a12839ce8fe46f0c2c0e98a37deebba05ea404b5 Mon Sep 17 00:00:00 2001 From: mar77i Date: Sun, 9 Jun 2024 23:46:57 +0200 Subject: [PATCH] big cleanup and refactoring #2: scramble master ws uri again --- hub/app.py | 58 ++++++++++++++++++++------- hub/{hubapp.py => static.py} | 77 +++++++++++------------------------- hub/utils.py | 30 ++++++++------ hub/websocket.py | 36 ++++++++--------- webroot/first/master.html.j2 | 2 +- webroot/first/master.js | 2 +- webroot/index.html.j2 | 2 +- 7 files changed, 105 insertions(+), 102 deletions(-) rename hub/{hubapp.py => static.py} (71%) diff --git a/hub/app.py b/hub/app.py index 2f1b849..5192a91 100644 --- a/hub/app.py +++ b/hub/app.py @@ -1,7 +1,10 @@ import socket import sys from argparse import ArgumentParser +from base64 import urlsafe_b64encode +from hashlib import pbkdf2_hmac from pathlib import Path +from secrets import token_urlsafe from subprocess import run from traceback import print_exception from urllib.parse import urlunsplit @@ -9,28 +12,55 @@ from urllib.parse import urlunsplit from falcon.asgi import App as FalconApp from falcon.constants import MEDIA_HTML from jinja2 import Environment, FileSystemLoader, select_autoescape +from redis.asyncio import StrictRedis from uvicorn import Config, Server -from .hubapp import RootApp, HubApp +from .static import HubApp, TemplateTreeFileApp -class App(FalconApp): - def __init__(self, secret, **kwargs): +class App(TemplateTreeFileApp, FalconApp): + @classmethod + def scan_files(cls, base_dir): + for path in base_dir.iterdir(): + if not path.is_dir() and not cls.is_ignored_filename(path): + yield path + + def scramble(self, value): + if isinstance(value, str): + value = value.encode() + secret = self.secret + if isinstance(secret, str): + secret = secret.encode() + return urlsafe_b64encode( + pbkdf2_hmac("sha512", value, secret, 221100) + ).rstrip(b"=").decode("ascii") + + def uri_tail(self, path: Path) -> str: + return self.scramble(super().uri_tail(path)) + + def __init__(self, base_dir, secret, **kwargs): + from .utils import get_redis_pass + + self.secret = secret or token_urlsafe(64) kwargs.setdefault("media_type", MEDIA_HTML) - super().__init__(**kwargs) - self.base_dir = Path(__file__).parents[1] / "webroot" + FalconApp.__init__(self, **kwargs) + TemplateTreeFileApp.__init__( + self, self, base_dir, "/derp", "root" + ) self.env = Environment( loader=FileSystemLoader(self.base_dir), autoescape=select_autoescape(), extensions=["hub.utils.StaticTag"], ) - self.hubapps = {"root": RootApp(self, self.base_dir, "/derp", secret)} + self.hubapps = {} + self.conn = StrictRedis( + username="default", password=get_redis_pass("/etc/redis/redis.conf") + ) for base_dir in self.base_dir.iterdir(): - if not base_dir.is_dir() or RootApp.is_ignored_filename(base_dir): - continue - self.hubapps[base_dir.name] = HubApp( - self, base_dir, base_uri=f"/derp/{base_dir.name}" - ) + if base_dir.is_dir() and not self.is_ignored_filename(base_dir): + self.hubapps[base_dir.name] = HubApp( + self, base_dir, base_uri=f"/derp/{base_dir.name}" + ) self.add_error_handler(Exception, self.print_exception) async def print_exception(self, req, resp, ex, params): @@ -44,7 +74,7 @@ class HubServer(Server): async def startup(self, sockets: list[socket.socket] | None = None) -> None: await super().startup(sockets) - root_app = self.config.loaded_app.app.hubapps["root"] + root_app = self.config.loaded_app.app print("Secret:", root_app.secret) for uri, file in root_app.files_per_uris.items(): if file.name == "index.html": @@ -65,8 +95,8 @@ async def main(): ap.add_argument("--secret") ap.add_argument("--browser", default="xdg-open") args = ap.parse_args() - app = App(args.secret) - await app.hubapps["root"].setup() + app = App(Path(__file__).parents[1] / "webroot", args.secret) + await app.conn.set("client_id", 0) config = Config(app, port=5000, log_level="info") config.setup_event_loop() hs = HubServer(config, browser=args.browser) diff --git a/hub/hubapp.py b/hub/static.py similarity index 71% rename from hub/hubapp.py rename to hub/static.py index 591856c..8c50f77 100644 --- a/hub/hubapp.py +++ b/hub/static.py @@ -1,12 +1,8 @@ -from base64 import urlsafe_b64encode -from hashlib import pbkdf2_hmac from pathlib import Path -from secrets import token_urlsafe from falcon.constants import MEDIA_HTML, MEDIA_JS, MEDIA_TEXT from falcon.status_codes import HTTP_OK from jinja2 import Template -from redis.asyncio import StrictRedis from .websocket import WebSocketApp @@ -53,7 +49,7 @@ class TreeFileApp: Map a directory tree base_dir to a base_uri and serve it statically. Map index.html files to their relative roots: index.html to "/" (root uri) to index.html and - "/a" (directory uri) to a/index.html + a/index.html to "/a" (directory uri) """ @staticmethod def is_ignored_filename(path): @@ -94,17 +90,24 @@ class TreeFileApp: uri_tail = uri_tail[:-len(index_suffix)] return f"{self.base_uri}/{uri_tail.lstrip('/')}" - def __init__(self, app, base_dir, base_uri="/"): - self.app = app + @staticmethod + def create_name(root_base_uri, base_uri): + if base_uri.startswith(root_base_uri): + base_uri = base_uri[len(root_base_uri):] + return base_uri.replace("/", ".").strip(".") + + def __init__(self, root, base_dir, base_uri="/", name=None): + self.root = root self.base_dir = base_dir self.base_uri = base_uri.rstrip("/") - self.name = self.base_uri.replace("/", ".").strip(".") or "root" + self.name = name or self.create_name(self.root.base_uri, self.base_uri) + assert self.name, self.name self.files_per_uris = {} for path in self.scan_files(base_dir): static_file = self.get_file(path) uri = self.uri(static_file.path) self.files_per_uris[uri] = static_file - app.add_route(uri, self) + root.add_route(uri, self) async def on_get(self, req, resp): resource = self.files_per_uris[req.path] @@ -126,7 +129,7 @@ class StaticTemplateFile(StaticFile): def get(self) -> str: mtime = self.path.stat().st_mtime if mtime != self.mtime: - env = self.hubapp.app.env + env = self.hubapp.root.env self.content = env.get_template( str(self.path.relative_to(env.loader.searchpath[0])) ) @@ -152,45 +155,13 @@ class TemplateTreeFileApp(TreeFileApp): return uri_tail -class RootApp(TemplateTreeFileApp): - @classmethod - def scan_files(cls, base_dir): - for path in base_dir.iterdir(): - if not path.is_dir() and not cls.is_ignored_filename(path): - yield path - - def __init__(self, app, base_dir, base_uri="/", secret=None): - from .utils import get_redis_pass - - self.secret = secret or token_urlsafe(64) - super().__init__(app, base_dir, base_uri) - self.conn = StrictRedis(username="default", password=get_redis_pass("/etc/redis/redis.conf")) - - async def setup(self): - await self.conn.set("client_id", 0) - - def scramble(self, value): - if isinstance(value, str): - value = value.encode() - secret = self.secret - if isinstance(secret, str): - secret = secret.encode() - return urlsafe_b64encode( - pbkdf2_hmac("sha512", value, secret, 221100) - ).rstrip(b"=").decode("ascii") - - def uri_tail(self, path: Path) -> str: - return self.scramble(super().uri_tail(path)) - - class HubApp(TemplateTreeFileApp): def __init__(self, app, base_dir, base_uri="/"): super().__init__(app, base_dir, base_uri) self.ws_app = WebSocketApp(self) - self.ws_uris = { - self.uri(Path(f"ws_{value}")): value for value in ("client", "master") - } - for uri, suffix in self.ws_uris.items(): + for suffix in ("client", "master"): + uri = self.uri(Path(f"ws_{suffix}")) + self.files_per_uris[uri] = self.ws_app app.add_route(uri, self.ws_app, suffix=suffix) @staticmethod @@ -200,15 +171,13 @@ class HubApp(TemplateTreeFileApp): pos = basename.find(".") if pos != -1: basename = basename[:pos] - return basename in "master" + return basename in ("master", "ws_master") def uri(self, path: Path) -> str: uri = super().uri(path) - if self.is_master_uri(path): - pos = uri.rstrip("/").rfind("/") - scrambled_uri = self.app.hubapps["root"].scramble(uri) - if pos == -1: - uri = scrambled_uri - else: - uri = f"{uri[:pos]}/{scrambled_uri}" - return uri + if not self.is_master_uri(path): + return uri + pos = uri.rstrip("/").rfind("/") + assert pos >= 0 + scrambled_uri = self.root.scramble(uri) + return f"{uri[:pos]}/{scrambled_uri}" diff --git a/hub/utils.py b/hub/utils.py index b66fd97..3e91e23 100644 --- a/hub/utils.py +++ b/hub/utils.py @@ -1,32 +1,38 @@ from jinja2_simple_tags import StandaloneTag -from .hubapp import TreeFileApp +from .static import TreeFileApp class StaticTag(StandaloneTag): tags = {"static"} + @staticmethod + def get_hubapp(static_file, hubapp): + h = static_file.hubapp + if hubapp == "root": + return h.root + elif isinstance(hubapp, str): + return h.root.hubapps[hubapp] + elif isinstance(hubapp, TreeFileApp): + return hubapp + return static_file.hubapp + def render( self, filename: str = "", hubapp: str | TreeFileApp | None = None - ): + ) -> str: """ If filename starts with '/', interpret the path as relative to hubapp.base_dir, otherwise assume the path is relative to the current file. """ static_file = self.context["static_file"] - h = static_file.hubapp - if isinstance(hubapp, str): - h = h.app.hubapps[hubapp] - elif isinstance(hubapp, TreeFileApp): - h = hubapp - del hubapp - if filename.startswith("/") or h != static_file.hubapp: - path = h.base_dir / filename.lstrip("/") + hubapp = self.get_hubapp(static_file, hubapp) + if filename.startswith("/") or hubapp != static_file.hubapp: + path = hubapp.base_dir / filename.lstrip("/") else: path = static_file.path.parent / filename - return h.uri(path) + return hubapp.uri(path) -def get_redis_pass(redis_conf): +def get_redis_pass(redis_conf: str) -> str: """ Poor man's redis credentials: read the password from redis_conf. Requires redis being configured with a `requirepass` password set. diff --git a/hub/websocket.py b/hub/websocket.py index 0ecf987..89a7e2f 100644 --- a/hub/websocket.py +++ b/hub/websocket.py @@ -9,24 +9,24 @@ from traceback import print_exception from falcon import WebSocketDisconnected -class BaseWebSocketApp: +class WebSocketApp: def __init__(self, hubapp): - self.hubapp = hubapp - self.conn = self.hubapp.app.hubapps["root"].conn - - def task_done(self): - self.task = None + self.name = hubapp.name + self.conn = hubapp.root.conn @staticmethod - async def process_websocket(conn, web_socket, extra_data={}, recipients=[]): + async def process_websocket(conn, web_socket, extra_data=None, recipients=None): try: while True: data = json.loads(await web_socket.receive_text()) - data.update(extra_data) + if extra_data: + data.update(extra_data) if callable(recipients): current_recipients = recipients(data) - else: + elif recipients: current_recipients = recipients + else: + raise ValueError("no recipients specified") for recipient in current_recipients: await conn.publish(recipient, pickle.dumps(data)) except (CancelledError, WebSocketDisconnected): @@ -76,11 +76,9 @@ class BaseWebSocketApp: if callable(leave_cb): await leave_cb() - -class WebSocketApp(BaseWebSocketApp): - async def join_leave_client_notify(self, redis, action, client_id): + async def client_notify(self, redis, action, client_id): await redis.publish( - f"{self.hubapp.name}-master", + f"{self.name}-master", pickle.dumps({"action": action, "client_id": client_id}), ) @@ -89,25 +87,25 @@ class WebSocketApp(BaseWebSocketApp): return await self.on_websocket( req, web_socket, - f"{self.hubapp.name}-client-{client_id}", + f"{self.name}-client-{client_id}", { "extra_data": {"client_id": client_id}, - "recipients": [f"{self.hubapp.name}-master"], + "recipients": [f"{self.name}-master"], }, - partial(self.join_leave_client_notify, self.conn, "join", client_id), - partial(self.join_leave_client_notify, self.conn, "leave", client_id), + partial(self.client_notify, self.conn, "join", client_id), + partial(self.client_notify, self.conn, "leave", client_id), ) async def on_websocket_master(self, req, web_socket): return await self.on_websocket( req, web_socket, - f"{self.hubapp.name}-master", + f"{self.name}-master", {"recipients": self.get_master_recipients}, ) def get_master_recipients(self, data): return [ - f"{self.hubapp.name}-client-{int(client_id)}" + f"{self.name}-client-{int(client_id)}" for client_id in data.pop("client_ids", ()) ] diff --git a/webroot/first/master.html.j2 b/webroot/first/master.html.j2 index 5a471de..1cb22e1 100644 --- a/webroot/first/master.html.j2 +++ b/webroot/first/master.html.j2 @@ -10,7 +10,7 @@

Chat Master

diff --git a/webroot/first/master.js b/webroot/first/master.js index ada5f96..ef16310 100644 --- a/webroot/first/master.js +++ b/webroot/first/master.js @@ -109,7 +109,7 @@ document.body.append(common.tag("div")); // output document.body.append(input_div); - write("connected to " + ws_uri); + write("connected to ws_master"); } function close() { diff --git a/webroot/index.html.j2 b/webroot/index.html.j2 index ec1f96c..7806406 100644 --- a/webroot/index.html.j2 +++ b/webroot/index.html.j2 @@ -10,7 +10,7 @@

App list