# -*- coding: utf-8 -*- """Dev static server for the built frontend + /api reverse proxy to backend :8090. Used because `npm run serve` cannot run inside the sandbox (node realpath on home dir is blocked). Run: python dev-proxy.py (serves ./dist on :8080, proxies /api -> http://localhost:8090) """ import http.server import os import socketserver import sys import urllib.error import urllib.request DIST = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dist') BACKEND = 'http://localhost:8090' PORT = int(os.environ.get('FRONT_PORT', '8080')) MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', } class Handler(http.server.BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' def do_GET(self): if self.path.startswith('/api/'): self.proxy() else: self.serve_static() def do_POST(self): if self.path.startswith('/api/'): self.proxy() else: self.send_error(404, 'Not Found') def serve_static(self): path = self.path.split('?')[0] if path in ('', '/'): path = '/index.html' rel = path.lstrip('/') fp = os.path.normpath(os.path.join(DIST, rel)) if not fp.startswith(DIST): self.send_error(403) return if os.path.isdir(fp): fp = os.path.join(fp, 'index.html') if not os.path.isfile(fp): fp = os.path.join(DIST, 'index.html') # SPA fallback try: with open(fp, 'rb') as f: data = f.read() except OSError: self.send_error(404) return ext = os.path.splitext(fp)[1].lower() self.send_response(200) self.send_header('Content-Type', MIME.get(ext, 'application/octet-stream')) self.send_header('Content-Length', str(len(data))) self.end_headers() self.wfile.write(data) def proxy(self): url = BACKEND + self.path body = None headers = {} if self.command == 'POST': length = int(self.headers.get('Content-Length') or 0) body = self.rfile.read(length) ct = self.headers.get('Content-Type') if ct: headers['Content-Type'] = ct req = urllib.request.Request(url, data=body, method=self.command, headers=headers) try: with urllib.request.urlopen(req, timeout=180) as resp: data = resp.read() self.send_response(resp.status) self._copy_header(resp, 'Content-Type') self._copy_header(resp, 'Content-Disposition') self.send_header('Content-Length', str(len(data))) self.end_headers() self.wfile.write(data) except urllib.error.HTTPError as e: data = e.read() self.send_response(e.code) self._copy_header(e, 'Content-Type') self.send_header('Content-Length', str(len(data))) self.end_headers() self.wfile.write(data) except Exception as e: # noqa: BLE001 self.send_error(502, str(e)) def _copy_header(self, resp, name): val = resp.headers.get(name) if val: self.send_header(name, val) def log_message(self, fmt, *args): sys.stderr.write('%s - %s\n' % (self.address_string(), fmt % args)) if __name__ == '__main__': socketserver.TCPServer.allow_reuse_address = True with socketserver.ThreadingTCPServer(('', PORT), Handler) as httpd: print('serving %s on :%d, proxy /api -> %s' % (DIST, PORT, BACKEND), flush=True) httpd.serve_forever()