diff --git a/dashboard.py b/dashboard.py index 47bc4cb..37838b4 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,18 +1,26 @@ #!/usr/bin/env python3 """ Lightweight test server and transparent proxy for the Records API. -Serves index.html on http://127.0.0.1:8080 and adds CORS headers to Axum requests. +Serves index.html on http://127.0.0.1:8080 and transparently proxies +requests to cluster instances, bypassing self-signed TLS certificate checks. """ from http.server import SimpleHTTPRequestHandler, HTTPServer import urllib.request import urllib.error +import urllib.parse +import ssl +import json +import os -PORT = 8080 +PORT = int(os.environ.get("DASHBOARD_PORT", 8080)) + +# Create an SSL context that ignores self-signed certificate errors +SSL_CTX = ssl._create_unverified_context() class ProxyHandler(SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, HEAD') self.send_header('Access-Control-Allow-Headers', '*') super().end_headers() @@ -20,11 +28,123 @@ class ProxyHandler(SimpleHTTPRequestHandler): self.send_response(204) self.end_headers() + def do_GET(self): + if self.path.startswith('/proxy'): + self.handle_proxy('GET') + else: + super().do_GET() + + def do_HEAD(self): + if self.path.startswith('/proxy'): + self.handle_proxy('HEAD') + else: + super().do_HEAD() + + def do_POST(self): + if self.path.startswith('/proxy'): + self.handle_proxy('POST') + else: + self.send_error(405, "Method Not Allowed") + + def do_PUT(self): + if self.path.startswith('/proxy'): + self.handle_proxy('PUT') + else: + self.send_error(405, "Method Not Allowed") + + def do_DELETE(self): + if self.path.startswith('/proxy'): + self.handle_proxy('DELETE') + else: + self.send_error(405, "Method Not Allowed") + + def extract_target_url(self): + parsed = urllib.parse.urlparse(self.path) + if parsed.path.rstrip('/') == '/proxy': + if '?url=' in self.path: + return urllib.parse.unquote(self.path.split('?url=', 1)[1]) + qs = urllib.parse.parse_qs(parsed.query) + return qs.get('url', [None])[0] + elif parsed.path.startswith('/proxy/'): + rest = self.path[len('/proxy/'):] + if rest.startswith('http://') or rest.startswith('https://'): + return rest + parts = rest.split('/', 1) + host_or_port = parts[0] + subpath = '/' + parts[1] if len(parts) > 1 else '' + if parsed.query: + subpath += '?' + parsed.query + if host_or_port.isdigit(): + return f"https://127.0.0.1:{host_or_port}{subpath}" + return f"https://{host_or_port}{subpath}" + return None + + def handle_proxy(self, method): + target_url = self.extract_target_url() + if not target_url: + self.send_response(400) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'error': 'Missing or invalid target URL'}).encode()) + return + + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) if content_length > 0 else None + + # Filter hop-by-hop headers and compression to ensure clean raw data transfer + req_headers = {} + for k, v in self.headers.items(): + if k.lower() in ('host', 'content-length', 'connection', 'transfer-encoding', 'accept-encoding'): + continue + req_headers[k] = v + + req = urllib.request.Request(target_url, data=body, headers=req_headers, method=method) + + try: + with urllib.request.urlopen(req, context=SSL_CTX, timeout=15) as resp: + status_code = resp.status + resp_headers = resp.getheaders() + resp_body = resp.read() if method != 'HEAD' else b'' + except urllib.error.HTTPError as e: + status_code = e.code + resp_headers = e.headers.items() + resp_body = e.read() if method != 'HEAD' else b'' + except urllib.error.URLError as e: + self.send_response(502) + self.send_header('Content-Type', 'application/json') + self.end_headers() + err_msg = json.dumps({'error': f'Failed to connect to backend: {e.reason}'}).encode() + self.wfile.write(err_msg) + return + except Exception as e: + self.send_response(500) + self.send_header('Content-Type', 'application/json') + self.end_headers() + err_msg = json.dumps({'error': f'Proxy exception: {str(e)}'}).encode() + self.wfile.write(err_msg) + return + + self.send_response(status_code) + for k, v in resp_headers: + if k.lower() in ('transfer-encoding', 'connection', 'content-length', + 'access-control-allow-origin', 'access-control-allow-methods', + 'access-control-allow-headers'): + continue + self.send_header(k, v) + + if method != 'HEAD': + self.send_header('Content-Length', str(len(resp_body))) + self.end_headers() + if method != 'HEAD' and resp_body: + self.wfile.write(resp_body) + + if __name__ == '__main__': print(f"🚀 Dashboard running at http://127.0.0.1:{PORT}") + print(f"🔒 TLS unverified proxy active at /proxy?url=") print("Press Ctrl+C to stop.") server = HTTPServer(('127.0.0.1', PORT), ProxyHandler) try: server.serve_forever() except KeyboardInterrupt: - print("\nStopping...") \ No newline at end of file + print("\nStopping...") diff --git a/index.html b/index.html index 7043552..95e4e03 100644 --- a/index.html +++ b/index.html @@ -176,16 +176,21 @@
- +
- +
+
+ +
@@ -286,6 +291,30 @@