From e35578c73edc56b3503966c55c33cb2eba12238c Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 14 Oct 2026 08:15:01 +0000 Subject: [PATCH] =?UTF-8?q?pages:=20wildcard=20zones=20=E2=80=94=20.z?= =?UTF-8?q?one=20serves=20dir/,=20apex=20//=20redirects=20there?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/server.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/pages/server.py b/pages/server.py index 64a2fe7..e571f11 100644 --- a/pages/server.py +++ b/pages/server.py @@ -2,7 +2,9 @@ SITES maps hostnames to `org/repo`, or `org/repo#branch/subdir`, on GITEA (public repos only; cloned over https, no token). A clone lives under /data/____; `POST /_hook//` (a Gitea webhook) or the -periodic loop pulls it. Static files only: +periodic loop pulls it. WILDCARDS maps a zone to a site whose top-level directories are its subdomains: +`{"yolopreneur.lol": "teahubxyz/time#gh-pages/ventures"}` serves `.yolopreneur.lol` from `ventures//`, and +`yolopreneur.lol//` redirects there. Static files only: a path maps to the file, a directory to its index.html, anything missing to 404.html or a plain 404. No listing, no execution, nothing writes except git. stdlib only. """ @@ -19,6 +21,7 @@ GITEA = os.environ.get("GITEA_URL", "https://teahub.xyz").rstrip("/") DATA = Path(os.environ.get("PAGES_DATA", "/data")) SITES = json.loads(os.environ.get("PAGES_SITES") or "{}") # {"persimmons.app": "persimmons/site", ...} PULL_EVERY = int(os.environ.get("PAGES_PULL_SECONDS") or 300) +WILDCARDS = json.loads(os.environ.get("PAGES_WILDCARDS") or "{}") # {"zone": "org/repo#branch/dir"}: .zone -> dir/ TYPES = {".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon", @@ -65,7 +68,7 @@ def sync(site: str) -> str: def loop() -> None: while True: - for repo in set(SITES.values()): + for repo in set(SITES.values()) | set(WILDCARDS.values()): print(repo, sync(repo), flush=True) time.sleep(PULL_EVERY) @@ -76,13 +79,24 @@ class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, fmt, *args): # one line per request, no client address print(f"{self.headers.get('host', '-')} {self.command} {self.path.split('?')[0]} {args[1] if len(args) > 1 else ''}", flush=True) - def site(self) -> str | None: - host = (self.headers.get("host") or "").split(":")[0].lower() - return SITES.get(host) or SITES.get(host[4:] if host.startswith("www.") else "") + def host(self) -> str: + return (self.headers.get("host") or "").split(":")[0].lower() + + def site(self) -> tuple[str, str] | None: + """(site, subdir) for this request's host: an exact site, its www, or a wildcard subdomain.""" + host = self.host() + s = SITES.get(host) or SITES.get(host[4:] if host.startswith("www.") else "") + if s: + return s, "" + sub, _, zone = host.partition(".") + w = WILDCARDS.get(zone) + if w and sub and sub != "www" and "/" not in sub and ".." not in sub: + return w, sub + return None def do_POST(self): parts = urlsplit(self.path).path.strip("/").split("/") - hit = [s for s in SITES.values() if len(parts) == 3 and parts[0] == "_hook" and parse(s)[0] == f"{parts[1]}/{parts[2]}"] + hit = [s for s in [*SITES.values(), *WILDCARDS.values()] if len(parts) == 3 and parts[0] == "_hook" and parse(s)[0] == f"{parts[1]}/{parts[2]}"] if hit: self.rfile.read(int(self.headers.get("content-length") or 0)) outs = [sync(s) for s in dict.fromkeys(hit)] # every site on that repo (any branch) @@ -104,12 +118,23 @@ class Handler(http.server.BaseHTTPRequestHandler): if not head: self.wfile.write(body) return - repo = self.site() - if not repo: + found = self.site() + if not found: self.send_error(404, "no site for this host") return + repo, sub = found root = site_root(repo).resolve() rel = unquote(path).lstrip("/") + # on a wildcard zone's apex, //... lives at .zone: send people there + head = rel.split("/", 1)[0] + if not sub and self.host() in WILDCARDS and head and (root / head).is_dir() and ".." not in rel: + self.send_response(301); self.send_header("location", f"https://{head}.{self.host()}/" + rel[len(head):].lstrip("/")); self.end_headers() + return + if sub: + root = (root / sub).resolve() + if not root.is_dir(): + self.send_error(404, "no such venture") + return target = (root / rel).resolve() if rel else root if root not in target.parents and target != root: self.send_error(404)