#!/usr/bin/env python3 """Busfleet Hub — registration server for kit-connect devices. Listens on :8080. Routes: GET /api/register/ — assign tunnel port, return JSON POST /api/authorize-key — authorize device's tunnel key GET /health — health check """ import json import subprocess import sys from http.server import HTTPServer, BaseHTTPRequestHandler REGISTER_SH = "/opt/busfleet-hub/register.sh" class HubHandler(BaseHTTPRequestHandler): """Handle hub registration and key authorization requests.""" def do_GET(self): # Route: /api/register/ if self.path.startswith("/api/register/"): device_id = self.path.split("/api/register/", 1)[1].strip("/") if not device_id: self.send_json_error(400, "Missing device ID") return try: result = subprocess.run( [REGISTER_SH, device_id], capture_output=True, text=True, timeout=15, ) status = 200 if result.returncode == 0 else 500 self.send_json(status, result.stdout.strip()) except subprocess.TimeoutExpired: self.send_json_error(504, "Registration timed out") except Exception as exc: self.send_json_error(500, str(exc)) return # Health check if self.path in ("/", "/health"): self.send_json(200, '{"status":"ok","service":"busfleet-hub"}') return self.send_json_error(404, "Not found") def do_POST(self): # Route: /api/authorize-key if self.path.startswith("/api/authorize-key"): content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length).decode() if content_length else "{}" try: data = json.loads(body) except json.JSONDecodeError: self.send_json_error(400, "Invalid JSON body") return device_id = data.get("device_id", "").strip() pubkey = data.get("pubkey", "").strip() if not device_id or not pubkey: self.send_json_error(400, "Missing device_id or pubkey") return # Basic validation: key must look like an SSH public key if not pubkey.startswith(("ssh-", "ecdsa-", "sk-")) or len(pubkey) < 80: self.send_json_error(400, "Invalid SSH public key format") return try: result = subprocess.run( [REGISTER_SH, "--authorize-key", device_id, pubkey], capture_output=True, text=True, timeout=15, ) status = 200 if result.returncode == 0 else 500 self.send_json(status, result.stdout.strip()) except subprocess.TimeoutExpired: self.send_json_error(504, "Key authorization timed out") except Exception as exc: self.send_json_error(500, str(exc)) return self.send_json_error(404, "Not found") def send_json(self, status, body): """Send a JSON response.""" self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body.encode() if isinstance(body, str) else body) def send_json_error(self, status, message): """Send a JSON error response.""" self.send_json(status, json.dumps({"error": message})) def log_message(self, fmt, *args): print(f"[hub] {args[0]}", file=sys.stderr) def main(): port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 server = HTTPServer(("0.0.0.0", port), HubHandler) print(f"Busfleet Hub listening on :{port}", flush=True) try: server.serve_forever() except KeyboardInterrupt: print("\nShutting down.", flush=True) server.shutdown() if __name__ == "__main__": main()