Files
railwaysystem/scripts/qa/serve_frontend.py
T

79 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Serve production SPA and proxy only /prod-api to the task-owned loopback backend."""
import argparse
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import urllib.error
import urllib.request
DIST = Path(__file__).resolve().parents[2] / 'build_ui/dist'
class Handler(SimpleHTTPRequestHandler):
backend_port = 18096
def proxy(self):
path = self.path[len('/prod-api'):]
if not path.startswith('/') or path.startswith('//'):
self.send_error(400)
return
headers = {k: v for k, v in self.headers.items()
if k.lower() not in ['host', 'connection', 'content-length', 'transfer-encoding']}
length = int(self.headers.get('Content-Length', '0'))
if length < 0 or length > 10 * 1024 * 1024:
self.send_error(413)
return
request = urllib.request.Request('http://127.0.0.1:' + str(self.backend_port) + path,
data=self.rfile.read(length) if length else None, headers=headers, method=self.command)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
# Ignore all system/environment HTTP proxies, never follow a backend redirect.
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
try:
response = opener.open(request, timeout=30)
except urllib.error.HTTPError as error:
response = error
except (urllib.error.URLError, TimeoutError):
self.send_error(502, 'QA backend unavailable; no substituted API response')
return
with response:
data = response.read()
self.send_response(response.code)
for key, value in response.headers.items():
if key.lower() not in ['connection', 'transfer-encoding', 'content-length']:
self.send_header(key, value)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path.startswith('/prod-api/'):
return self.proxy()
requested = self.path.split('?', 1)[0]
if requested != '/' and not (DIST / requested.lstrip('/')).is_file() and '.' not in requested.rsplit('/', 1)[-1]:
self.path = '/index.html'
return super().do_GET()
def do_POST(self):
return self.proxy() if self.path.startswith('/prod-api/') else self.send_error(405)
do_PUT = do_POST
do_DELETE = do_POST
def log_message(self, *args):
# No query strings, headers, payloads, tokens or personal identifiers in access logs.
pass
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', type=int, default=18088)
parser.add_argument('--backend-port', type=int, default=18096)
args = parser.parse_args()
if not (DIST / 'index.html').is_file():
raise SystemExit('Build frontend before serving')
Handler.backend_port = args.backend_port
ThreadingHTTPServer(('127.0.0.1', args.port), partial(Handler, directory=str(DIST))).serve_forever()