Files
pando/dashboard.py
T

151 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Lightweight test server and transparent proxy for the Pando 🌳 cluster.
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 = 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, PUT, DELETE, OPTIONS, HEAD')
self.send_header('Access-Control-Allow-Headers', '*')
super().end_headers()
def do_OPTIONS(self):
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=<target>")
print("Press Ctrl+C to stop.")
server = HTTPServer(('127.0.0.1', PORT), ProxyHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopping...")