]> git.mar77i.info Git - hublib/commitdiff
big cleanup and refactoring #2: scramble master ws uri again
authormar77i <mar77i@protonmail.ch>
Sun, 9 Jun 2024 21:46:57 +0000 (23:46 +0200)
committermar77i <mar77i@protonmail.ch>
Sun, 9 Jun 2024 23:23:09 +0000 (01:23 +0200)
hub/app.py
hub/static.py [moved from hub/hubapp.py with 71% similarity]
hub/utils.py
hub/websocket.py
webroot/first/master.html.j2
webroot/first/master.js
webroot/index.html.j2

index 2f1b84936f1247f3832a4714b4dca3fc8982ffc4..5192a9187bf0ca5bbdbc879c0ec6c1abdc678ec7 100644 (file)
@@ -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)
similarity index 71%
rename from hub/hubapp.py
rename to hub/static.py
index 591856ce81c86bfb68977abe527d6fd4a325b653..8c50f77915fef08adfaa8e8536c3bc13a81de71c 100644 (file)
@@ -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}"
index b66fd97992c224a41e37928e5b68fc997349eb0e..3e91e239b6ad753beea77fcd90e22ab3e313cab1 100644 (file)
@@ -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.
index 0ecf9875e29a5ba5f66665a1b54ef30bd2a41f9a..89a7e2f3cfa4fd79baed82bc75c6581620eccab5 100644 (file)
@@ -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", ())
         ]
index 5a471de9a5f12d060f4400687b4b9975ae849f3a..1cb22e13540674594933517a7832f9b33ffbe369 100644 (file)
@@ -10,7 +10,7 @@
     <body>
         <script src="{% static 'common.js', 'root' %}"></script>
         <script type="text/javascript">
-            var ws_uri = "{% static 'ws_master' %}";
+            var ws_uri = "{% static '/ws_master' %}";
         </script>
         <script src="{% static 'master.js' %}"></script>
         <h1>Chat Master</h1>
index ada5f960ee0f2f70aac284d5296f099c6fdb3ede..ef163101a73e6129181b9d4fa61209e615881876 100644 (file)
 
         document.body.append(common.tag("div")); // output
         document.body.append(input_div);
-        write("connected to " + ws_uri);
+        write("connected to ws_master");
     }
 
     function close() {
index ec1f96c6bb36c4816bf44b7644393dffae36ae1e..78064060851f3ca2b33569f9ef14355b5fdb183e 100644 (file)
@@ -10,7 +10,7 @@
     <body>
         <h1>App list</h1>
         <ul>
-        {% for name, hubapp in static_file.hubapp.app.hubapps.items() if name != "root" %}
+        {% for name, hubapp in static_file.hubapp.hubapps.items() %}
             <li>
                 <span>{{ name }}</span>
                 <span><a href="{% static '/master.html', hubapp %}">master</a></span>