112 lines
4.9 KiB
Python
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()
|
|
|