151 lines
5.5 KiB
Python
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...")
|