190 lines
10 KiB
Python
190 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Enterprise Lighthouse foundation: registry, preflight, and audit only.
|
|
|
|
This service is deliberately not a remote shell. It accepts node admission
|
|
requests and validates fixed operation plans; execution remains with a future
|
|
GLSV node connector after human authorization.
|
|
"""
|
|
import hmac
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import time
|
|
import uuid
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from ipaddress import ip_address
|
|
|
|
DB = os.environ.get("LIGHTHOUSE_DB", "/var/lib/guanghu-enterprise-lighthouse/lighthouse.db")
|
|
TOKEN = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "")
|
|
HOST = os.environ.get("LIGHTHOUSE_BIND", "127.0.0.1")
|
|
PORT = int(os.environ.get("LIGHTHOUSE_PORT", "8031"))
|
|
FIXED_ACTIONS = {"health_check", "backup", "deploy_release", "restart_service", "rollback"}
|
|
DOMAINS = {
|
|
"DOMAIN-ZS": "零感域", "DOMAIN-MAIN": "光湖主域", "DOMAIN-SUB": "光湖分域",
|
|
"DOMAIN-ZERO": "光湖零域", "DOMAIN-FIFTH": "第五域",
|
|
}
|
|
|
|
|
|
def now():
|
|
return int(time.time())
|
|
|
|
|
|
def connection():
|
|
os.makedirs(os.path.dirname(DB), exist_ok=True)
|
|
db = sqlite3.connect(DB)
|
|
db.row_factory = sqlite3.Row
|
|
db.executescript("""
|
|
CREATE TABLE IF NOT EXISTS domains (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, state TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS intakes (
|
|
id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, state TEXT NOT NULL,
|
|
human_name TEXT NOT NULL, email TEXT NOT NULL, server_ip TEXT NOT NULL,
|
|
domain_id TEXT NOT NULL, persona_ids TEXT NOT NULL, repository_urls TEXT NOT NULL,
|
|
hosting_mode TEXT NOT NULL, notes TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, intake_id TEXT, state TEXT NOT NULL,
|
|
display_name TEXT NOT NULL, server_ip TEXT NOT NULL, allowed_actions TEXT NOT NULL,
|
|
public_key_fingerprint TEXT, last_heartbeat INTEGER, created_at INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS audit (
|
|
id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL
|
|
);
|
|
""")
|
|
for domain_id, name in DOMAINS.items():
|
|
db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, 'PENDING_ENTRY_NODE', ?)", (domain_id, name, now()))
|
|
db.commit()
|
|
return db
|
|
|
|
|
|
def audit(db, kind, payload):
|
|
db.execute("INSERT INTO audit VALUES (?, ?, ?, ?)", (str(uuid.uuid4()), now(), kind, json.dumps(payload, ensure_ascii=False)))
|
|
db.commit()
|
|
|
|
|
|
def parse_json(handler):
|
|
length = int(handler.headers.get("Content-Length", "0"))
|
|
if not 0 < length <= 50_000:
|
|
raise ValueError("request body must be between 1 and 50000 bytes")
|
|
return json.loads(handler.rfile.read(length).decode("utf-8"))
|
|
|
|
|
|
def valid_email(value):
|
|
return isinstance(value, str) and len(value) <= 254 and value.count("@") == 1
|
|
|
|
|
|
def require_admin(handler):
|
|
received = handler.headers.get("X-Lighthouse-Admin-Token", "")
|
|
return bool(TOKEN) and hmac.compare_digest(received, TOKEN)
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
server_version = "GuanghuEnterpriseLighthouse/1.0"
|
|
|
|
def log_message(self, fmt, *args):
|
|
print("[lighthouse] " + fmt % args)
|
|
|
|
def respond(self, status, body):
|
|
encoded = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(encoded)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.end_headers()
|
|
self.wfile.write(encoded)
|
|
|
|
def do_GET(self):
|
|
db = connection()
|
|
try:
|
|
if self.path == "/health":
|
|
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-lighthouse", "mode": "registry-and-preflight-only", "execution": "disabled"})
|
|
if self.path == "/v1/status":
|
|
counts = {row["state"]: row["count"] for row in db.execute("SELECT state, COUNT(*) AS count FROM nodes GROUP BY state")}
|
|
return self.respond(200, {"ok": True, "domains": [dict(row) for row in db.execute("SELECT id,name,state FROM domains ORDER BY id")], "node_counts": counts, "fixed_actions": sorted(FIXED_ACTIONS), "raw_shell": "rejected"})
|
|
if self.path == "/v1/nodes":
|
|
return self.respond(200, {"ok": True, "nodes": [dict(row) for row in db.execute("SELECT id,domain_id,state,display_name,allowed_actions,last_heartbeat,created_at FROM nodes ORDER BY created_at DESC")]})
|
|
return self.respond(404, {"ok": False, "error": "not found"})
|
|
finally:
|
|
db.close()
|
|
|
|
def do_POST(self):
|
|
if not require_admin(self):
|
|
return self.respond(401, {"ok": False, "error": "admin authorization required"})
|
|
try:
|
|
payload = parse_json(self)
|
|
except (ValueError, json.JSONDecodeError) as error:
|
|
return self.respond(400, {"ok": False, "error": str(error)})
|
|
if "cmd" in payload or "shell" in payload or "command" in payload:
|
|
return self.respond(400, {"ok": False, "error": "raw commands are never accepted by the lighthouse"})
|
|
db = connection()
|
|
try:
|
|
if self.path == "/v1/intakes":
|
|
required = ("human_name", "email", "server_ip", "domain_id")
|
|
if any(not payload.get(field) for field in required) or payload["domain_id"] not in DOMAINS or not valid_email(payload["email"]):
|
|
return self.respond(400, {"ok": False, "error": "human_name, valid email, server_ip, and known domain_id are required"})
|
|
try:
|
|
ip_address(payload["server_ip"])
|
|
except ValueError:
|
|
return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"})
|
|
intake_id = "INTAKE-" + uuid.uuid4().hex[:12].upper()
|
|
db.execute("INSERT INTO intakes VALUES (?, ?, 'PENDING_REVIEW', ?, ?, ?, ?, ?, ?, ?, ?)", (
|
|
intake_id, now(), payload["human_name"].strip(), payload["email"].strip(), payload["server_ip"], payload["domain_id"],
|
|
json.dumps(payload.get("persona_ids", [])), json.dumps(payload.get("repository_urls", [])),
|
|
payload.get("hosting_mode", "own"), payload.get("notes", ""),
|
|
))
|
|
audit(db, "intake_created", {"intake_id": intake_id, "domain_id": payload["domain_id"]})
|
|
return self.respond(201, {"ok": True, "intake_id": intake_id, "state": "PENDING_REVIEW", "next": "sovereign approval, human email confirmation, then node connector enrollment"})
|
|
if self.path == "/v1/nodes/bootstrap":
|
|
"""Register a manually verified routing node without enabling execution.
|
|
|
|
This exists for the controlled migration of an already verified
|
|
domain entry. It deliberately cannot make a node ACTIVE: the
|
|
GLSV connector, server-local key enrollment, and human email
|
|
confirmation remain required before any operation can proceed.
|
|
"""
|
|
required = ("id", "domain_id", "display_name", "server_ip")
|
|
if any(not isinstance(payload.get(field), str) or not payload[field].strip() for field in required):
|
|
return self.respond(400, {"ok": False, "error": "id, domain_id, display_name, and server_ip are required"})
|
|
if payload["domain_id"] not in DOMAINS:
|
|
return self.respond(400, {"ok": False, "error": "domain_id is not registered"})
|
|
try:
|
|
ip_address(payload["server_ip"])
|
|
except ValueError:
|
|
return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"})
|
|
node_id = payload["id"].strip()
|
|
if db.execute("SELECT 1 FROM nodes WHERE id=?", (node_id,)).fetchone():
|
|
return self.respond(409, {"ok": False, "error": "node id is already registered"})
|
|
db.execute("INSERT INTO nodes VALUES (?, ?, NULL, 'CONNECTED_PENDING_CONNECTOR', ?, ?, ?, NULL, ?, ?)", (
|
|
node_id, payload["domain_id"], payload["display_name"].strip(), payload["server_ip"],
|
|
json.dumps(["health_check"]), now(), now(),
|
|
))
|
|
db.execute("UPDATE domains SET state='ENTRY_NODE_CONNECTED' WHERE id=?", (payload["domain_id"],))
|
|
audit(db, "node_bootstrap_connected", {"node_id": node_id, "domain_id": payload["domain_id"], "mode": "manual_verified_routing_only"})
|
|
return self.respond(201, {"ok": True, "node_id": node_id, "state": "CONNECTED_PENDING_CONNECTOR", "execution": "disabled until GLSV connector enrollment and human authorization"})
|
|
if self.path == "/v1/preflight":
|
|
action, node_id = payload.get("action"), payload.get("target_node_id")
|
|
if action not in FIXED_ACTIONS or not node_id:
|
|
return self.respond(400, {"ok": False, "error": "fixed action and target_node_id are required", "allowed_actions": sorted(FIXED_ACTIONS)})
|
|
node = db.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone()
|
|
if not node:
|
|
return self.respond(404, {"ok": False, "decision": "REJECT", "reason": "target node is not registered"})
|
|
allowed = json.loads(node["allowed_actions"])
|
|
reasons = []
|
|
if node["state"] != "ACTIVE": reasons.append("target node is not ACTIVE")
|
|
if action not in allowed: reasons.append("action is not in the node allowlist")
|
|
if action in {"deploy_release", "restart_service", "rollback"} and not payload.get("rollback_plan"): reasons.append("rollback_plan is required")
|
|
if action in {"deploy_release", "restart_service"} and not payload.get("backup_reference"): reasons.append("backup_reference is required")
|
|
decision = "ALLOW_FOR_AUTHORIZATION" if not reasons else "REJECT"
|
|
result = {"ok": not reasons, "decision": decision, "reasons": reasons, "execution": "not performed; GLSV human authorization still required"}
|
|
audit(db, "preflight", {"target_node_id": node_id, "action": action, "decision": decision, "reasons": reasons})
|
|
return self.respond(200, result)
|
|
return self.respond(404, {"ok": False, "error": "not found"})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(f"Enterprise Lighthouse listening on {HOST}:{PORT}")
|
|
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|