import sys import time import threading import RNS from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import parse_qs, urlparse APP_NAME = "tinyweb" ASPECTS = ["server"] GATEWAY_PORT = 8080 REQUEST_TIMEOUT = 60 class GatewayState: reticulum = None destination = None link = None link_lock = threading.Lock() local_dispatch = None # set when running inside app.py def resolve_destination(dest_hash_hex): dest_hash = bytes.fromhex(dest_hash_hex) if not RNS.Transport.has_path(dest_hash): RNS.Transport.request_path(dest_hash) print(f"Requesting path to {RNS.prettyhexrep(dest_hash)}...") elapsed = 0 while not RNS.Transport.has_path(dest_hash) and elapsed < 15: time.sleep(0.5) elapsed += 0.5 if not RNS.Transport.has_path(dest_hash): raise ConnectionError(f"Could not find path to {RNS.prettyhexrep(dest_hash)}") server_identity = RNS.Identity.recall(dest_hash) GatewayState.destination = RNS.Destination( server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, *ASPECTS, ) print(f"Resolved destination: {RNS.prettyhexrep(dest_hash)}") def ensure_link(): with GatewayState.link_lock: if GatewayState.link and GatewayState.link.status == RNS.Link.ACTIVE: return GatewayState.link print("Establishing link...") link = RNS.Link(GatewayState.destination) elapsed = 0 while link.status == RNS.Link.PENDING and elapsed < 15: time.sleep(0.25) elapsed += 0.25 if link.status != RNS.Link.ACTIVE: raise ConnectionError("Link establishment failed") GatewayState.link = link print("Link established") return link class GatewayHandler(BaseHTTPRequestHandler): def _forward(self, method): parsed = urlparse(self.path) query = parse_qs(parsed.query) body = {} if method == "POST": length = int(self.headers.get("Content-Length", 0)) raw = self.rfile.read(length).decode() body = parse_qs(raw) # Parse cookies cookies = {} cookie_header = self.headers.get("Cookie", "") if cookie_header: for part in cookie_header.split(";"): part = part.strip() if "=" in part: k, v = part.split("=", 1) cookies[k.strip()] = v.strip() request_data = { "method": method, "path": parsed.path, "query": query, "body": body, "cookies": cookies, "gateway_host": self.headers.get("Host", f"localhost:{GATEWAY_PORT}"), } try: if GatewayState.local_dispatch: resp = GatewayState.local_dispatch(request_data) else: link = ensure_link() receipt = link.request( "/tinyweb", data=request_data, timeout=REQUEST_TIMEOUT, ) # Wait for the response elapsed = 0 done_statuses = (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED, RNS.RequestReceipt.FAILED) while receipt.get_status() not in done_statuses and elapsed < REQUEST_TIMEOUT: time.sleep(0.1) elapsed += 0.1 if receipt.get_status() in (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED): resp = receipt.get_response() elif receipt.get_status() == RNS.RequestReceipt.FAILED: self.send_error(504, "Request to TinyWeb server failed") return else: self.send_error(504, "Request to TinyWeb server timed out") return self.send_response(resp["status"]) self.send_header("Content-Type", resp.get("content_type", "text/html; charset=utf-8")) for k, v in resp.get("headers", {}).items(): self.send_header(k, v) self.end_headers() resp_body = resp.get("body", "") if resp_body: self.wfile.write(resp_body.encode() if isinstance(resp_body, str) else resp_body) except ConnectionError as e: GatewayState.link = None self.send_error(502, f"Gateway error: {e}") except Exception as e: GatewayState.link = None self.send_error(502, f"Gateway error: {e}") def do_GET(self): self._forward("GET") def do_POST(self): self._forward("POST") def log_message(self, format, *args): print(f"[Gateway] {args[0]}") def main(): if len(sys.argv) < 2: print(f"Usage: python gateway.py ") print(f" The destination hash is printed by app.py on startup.") sys.exit(1) dest_hash = sys.argv[1].replace("<", "").replace(">", "") GatewayState.reticulum = RNS.Reticulum() resolve_destination(dest_hash) print(f"Gateway listening on http://localhost:{GATEWAY_PORT}") print(f"Open http://localhost:{GATEWAY_PORT} in your browser") HTTPServer(("127.0.0.1", GATEWAY_PORT), GatewayHandler).serve_forever() if __name__ == "__main__": main()