tinyweb/gateway.py
lichenblankie 4899819597 added bookmark auth, CSP, per-session CSRF
- Bookmark endpoint now requires a secret token (stored in settings)
- Style reset moved from GET to POST with CSRF protection
- Open redirect prevention in _redirect() helper
- Import capped at 100 URLs to prevent abuse
- page_tags cleaned up on delete + PRAGMA foreign_keys enabled
- CSP, X-Frame-Options, X-Content-Type-Options on all responses
- CSRF tokens now per-session via double-submit cookie pattern
- Tag names URL-decoded for special characters
- Gateway forwards cookies in request data
2026-06-05 05:29:35 +00:00

167 lines
5.3 KiB
Python

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 <destination_hash>")
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()