Files

112 lines
4.9 KiB
Python

"""Same-origin static UI/gateway for the isolated room demo; never proxies arbitrary URLs."""
import argparse
import json
import hint_library
import re
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlsplit
import urllib.request
import urllib.error
ROOT = Path(__file__).resolve().parent
ROUTES = {"/room/state": (18776, "/room/state"), "/room/trace": (18776, "/room/trace"),
"/room/command": (18776, "/room/command"), "/speech/health": (18778, "/health")}
class Handler(SimpleHTTPRequestHandler):
def list_directory(self, path):
self.send_error(403)
def relay(self):
path = urlsplit(self.path).path
route = ROUTES.get(path)
if re.fullmatch(r"/speech/audio/[a-f0-9]{64}\.wav", path):
route = (18778, path.removeprefix("/speech"))
if not route or (self.command == "POST" and path != "/room/command") or (self.command == "GET" and path == "/room/command"):
self.send_error(404)
return
if self.command == "POST":
origin = self.headers.get("Origin")
if origin and origin != "http://" + self.headers.get("Host", ""):
self.send_error(403, "Use the room demo page to send commands")
return
try:
size = int(self.headers.get("Content-Length", "0"))
if size < 0 or size > 65536:
self.send_error(413)
return
body = self.rfile.read(size) if self.command == "POST" else None
request = urllib.request.Request(f"http://127.0.0.1:{route[0]}{route[1]}",
data=body, method=self.command, headers={"Content-Type":"application/json", **({"Range":self.headers["Range"]} if "Range" in self.headers else {})})
try:
with urllib.request.urlopen(request, timeout=8) as response:
data, status = response.read(), response.status
content_type = response.headers.get("Content-Type","application/json")
content_range = response.headers.get("Content-Range")
except urllib.error.HTTPError as error:
data, status = error.read(), error.code
content_type, content_range = "text/plain", None
self.send_response(status)
self.send_header("Content-Type",content_type)
self.send_header("Accept-Ranges","bytes")
if content_range:
self.send_header("Content-Range",content_range)
self.send_header("Content-Length",str(len(data)))
self.send_header("Cache-Control","no-store")
self.end_headers()
self.wfile.write(data)
except (OSError, ValueError):
self.send_error(503, "Local lab service unavailable")
def hints(self):
try:
if self.command == "POST":
origin=self.headers.get("Origin")
if origin and origin != "http://" + self.headers.get("Host",""):
self.send_error(403)
return
size=int(self.headers.get("Content-Length","0"))
if not 0<size<=16384:
self.send_error(413)
return
result=hint_library.save(json.loads(self.rfile.read(size)))
else:
result=hint_library.read()
status=200
except hint_library.Conflict as error:
result,status={"error":str(error)},409
except (ValueError, OSError) as error:
result,status={"error":str(error)},400
data=json.dumps(result).encode()
self.send_response(status)
self.send_header("Content-Type","application/json")
self.send_header("Content-Length",str(len(data)))
self.send_header("Cache-Control","no-store")
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if urlsplit(self.path).path == "/hints":
return self.hints()
if urlsplit(self.path).path.startswith(("/room/", "/speech/")):
self.relay()
else:
super().do_GET()
def do_POST(self):
if urlsplit(self.path).path == "/hints":
return self.hints()
self.relay()
def end_headers(self):
self.send_header("X-Content-Type-Options","nosniff")
self.send_header("Cache-Control","no-store")
super().end_headers()
def log_message(self,*_): pass
if __name__ == "__main__":
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bind",default="127.0.0.1")
args=parser.parse_args()
server=ThreadingHTTPServer((args.bind,18777),partial(Handler,directory=str(ROOT/"gm/build/web")))
print(f"Room demo UI/gateway: http://{args.bind}:18777/?room=1",flush=True)
try: server.serve_forever()
finally: server.server_close()