Add self signed cert support to the test dashboard.
This commit is contained in:
1 parent
ff16548585
commit
be278b3c7c
3 files changed
+161
-14
No files matched your search
+124
-4
@@ -1,18 +1,26 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Lightweight test server and transparent proxy for the Records API.
|
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
|
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
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):
|
class ProxyHandler(SimpleHTTPRequestHandler):
|
||||||
def end_headers(self):
|
def end_headers(self):
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
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', '*')
|
self.send_header('Access-Control-Allow-Headers', '*')
|
||||||
super().end_headers()
|
super().end_headers()
|
||||||
|
|
||||||
@@ -20,11 +28,123 @@ class ProxyHandler(SimpleHTTPRequestHandler):
|
|||||||
self.send_response(204)
|
self.send_response(204)
|
||||||
self.end_headers()
|
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__':
|
if __name__ == '__main__':
|
||||||
print(f"🚀 Dashboard running at http://127.0.0.1:{PORT}")
|
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.")
|
print("Press Ctrl+C to stop.")
|
||||||
server = HTTPServer(('127.0.0.1', PORT), ProxyHandler)
|
server = HTTPServer(('127.0.0.1', PORT), ProxyHandler)
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nStopping...")
|
print("\nStopping...")
|
||||||
+31
-2
@@ -176,16 +176,21 @@
|
|||||||
<div style="display:flex; gap: 20px; align-items:center; flex-wrap:wrap; font-size:0.85rem;">
|
<div style="display:flex; gap: 20px; align-items:center; flex-wrap:wrap; font-size:0.85rem;">
|
||||||
<div style="flex:1; min-width:240px;">
|
<div style="flex:1; min-width:240px;">
|
||||||
<label>Primary Base URL:</label>
|
<label>Primary Base URL:</label>
|
||||||
<input type="text" id="primaryUrl" value="http://127.0.0.1:3000" style="margin-bottom:0;" />
|
<input type="text" id="primaryUrl" value="https://127.0.0.1:3000" style="margin-bottom:0;" />
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1; min-width:240px;">
|
<div style="flex:1; min-width:240px;">
|
||||||
<label>Replica Base URL:</label>
|
<label>Replica Base URL:</label>
|
||||||
<input type="text" id="replicaUrl" value="http://127.0.0.1:3001" style="margin-bottom:0;" />
|
<input type="text" id="replicaUrl" value="https://127.0.0.1:3001" style="margin-bottom:0;" />
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1; min-width:180px;">
|
<div style="flex:1; min-width:180px;">
|
||||||
<label>Replication Bearer Token:</label>
|
<label>Replication Bearer Token:</label>
|
||||||
<input type="password" id="replToken" value="change-me" style="margin-bottom:0;" />
|
<input type="password" id="replToken" value="change-me" style="margin-bottom:0;" />
|
||||||
</div>
|
</div>
|
||||||
|
<div style="min-width:220px; display:flex; align-items:flex-end;">
|
||||||
|
<label style="font-size:0.8rem; display:flex; align-items:center; gap:6px; cursor:pointer; padding-bottom:6px; color:var(--success);">
|
||||||
|
<input type="checkbox" id="trustAllCerts" checked style="width:auto; margin:0;" /> 🛡️ Trust Self-Signed Certs (Proxy)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -286,6 +291,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Transparently proxy external/HTTPS instance calls via dashboard proxy when self-signed cert trust is active
|
||||||
|
const nativeFetch = window.fetch;
|
||||||
|
window.fetch = function(input, init) {
|
||||||
|
const trustAll = document.getElementById('trustAllCerts')?.checked ?? true;
|
||||||
|
if (trustAll) {
|
||||||
|
const urlStr = typeof input === 'string' ? input : (input && input.url ? input.url : null);
|
||||||
|
if (urlStr) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(urlStr, window.location.href);
|
||||||
|
if (parsed.origin !== window.location.origin) {
|
||||||
|
const proxyBase = (window.location.protocol === 'file:') ? 'http://127.0.0.1:8080' : '';
|
||||||
|
const proxiedUrl = `${proxyBase}/proxy?url=${encodeURIComponent(urlStr)}`;
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
input = proxiedUrl;
|
||||||
|
} else {
|
||||||
|
input = new Request(proxiedUrl, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nativeFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
let primaryHeadSeq = 0;
|
let primaryHeadSeq = 0;
|
||||||
let replicaHeadSeq = 0;
|
let replicaHeadSeq = 0;
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+6
-8
@@ -51,14 +51,12 @@ for port in "$PRIMARY_PORT" "$REPLICA_PORT"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ! -x "$BIN" ]]; then
|
if [[ -f "$ROOT_DIR/Cargo.toml" ]]; then
|
||||||
if [[ -f "$ROOT_DIR/Cargo.toml" ]]; then
|
echo "Ensuring debug binary is up to date with cargo..."
|
||||||
echo "Binary not found at $BIN. Building debug binary with cargo..."
|
( cd "$ROOT_DIR" && cargo build )
|
||||||
( cd "$ROOT_DIR" && cargo build )
|
elif [[ ! -x "$BIN" ]]; then
|
||||||
else
|
echo "Error: Binary not found or not executable: $BIN" >&2
|
||||||
echo "Error: Binary not found or not executable: $BIN" >&2
|
exit 1
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Workspace & Schema Setup ---
|
# --- Workspace & Schema Setup ---
|
||||||
|
|||||||
Reference in new issue
Block a user