i2p n smol fixes
This commit is contained in:
parent
3b75d3c6f0
commit
2ede94fa2f
@ -16,7 +16,7 @@ zkac-node user create alice
|
||||
zkac-node user create bob
|
||||
|
||||
# Bob shares one contact string with Alice out-of-band:
|
||||
# zkac-node user show bob
|
||||
# zkac-node user show bob --peer 127.0.0.1:9810
|
||||
|
||||
# 2. Alice runs a server; pin its public key for clients
|
||||
zkac-node serve alice --port 9800 &
|
||||
@ -26,8 +26,7 @@ zkac-node server pin bob localhost:9800 --key <SERVER_PK_HEX>
|
||||
# 3. Alice creates a registry and grants Bob a role directly (needs Bob's contact string)
|
||||
zkac-node registry create alice localhost:9800 --roles analyst,operator
|
||||
zkac-node grant alice --server localhost:9800 \
|
||||
--registry <REGISTRY_ID> --role analyst --to-contact "$BOB_CONTACT" \
|
||||
--peer 127.0.0.1:9810
|
||||
--registry <REGISTRY_ID> --role analyst --to "$BOB_CONTACT"
|
||||
|
||||
# 4. Bob listens for direct p2p delivery
|
||||
zkac-node p2p-listen bob --host 127.0.0.1 --port 9810
|
||||
@ -53,7 +52,7 @@ zkac-node auth bob --registry <REGISTRY_ID> --role analyst --server localhost:98
|
||||
| `registry update <id> <server> --registry R --add-roles …` | Add roles |
|
||||
| `registry get <id> <server> --registry R` | Fetch registry state |
|
||||
| `registry list <id>` | List registries this user owns locally |
|
||||
| `grant <id> --server S --registry R --role X --to-contact <blob> --peer host:port` | Admin direct p2p grant (single-share recipient contact) |
|
||||
| `grant <id> --server S --registry R --role X --to <blob>` | Admin direct p2p grant (recipient contact bundle must include peer endpoint) |
|
||||
| `p2p-listen <id> [--host H --port P]` | Receive one direct p2p grant and store credential |
|
||||
| `credentials list <id>` | List local credentials |
|
||||
| `auth <id> --registry R --role X [--server S]` | Authenticated session |
|
||||
@ -106,6 +105,9 @@ Connectivity sanity check:
|
||||
|
||||
```bash
|
||||
zkac-node net check exampledestination.b32.i2p:9800
|
||||
zkac-node net check exampledestination.b32.i2p:9800 --handshake --key <SERVER_PK_HEX>
|
||||
# or use an existing pin:
|
||||
zkac-node net check exampledestination.b32.i2p:9800 --handshake --userid alice
|
||||
```
|
||||
|
||||
With proxy:
|
||||
|
||||
@ -61,6 +61,10 @@ def _proxy_target() -> tuple[str, int] | None:
|
||||
return _parse_host_port(raw, "ZKAC_SOCKS5_PROXY")
|
||||
|
||||
|
||||
def _is_i2p_host(host: str) -> bool:
|
||||
return host.strip().lower().endswith(".i2p")
|
||||
|
||||
|
||||
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
@ -100,7 +104,9 @@ def _socks5_connect(sock: socket.socket, host: str, port: int):
|
||||
|
||||
def _connect(host: str, port: int) -> socket.socket:
|
||||
proxy = _proxy_target()
|
||||
if proxy is None:
|
||||
# Only route through SOCKS5 for I2P destinations. This keeps local/direct
|
||||
# peer delivery (e.g. 127.0.0.1) working even when ZKAC_SOCKS5_PROXY is set.
|
||||
if proxy is None or not _is_i2p_host(host):
|
||||
return socket.create_connection((host, port))
|
||||
sock = socket.create_connection(proxy)
|
||||
try:
|
||||
@ -111,23 +117,52 @@ def _connect(host: str, port: int) -> socket.socket:
|
||||
raise
|
||||
|
||||
|
||||
def net_check(target: str, timeout_s: float = 8.0) -> dict:
|
||||
"""Connectivity diagnostic for direct/server endpoints, with optional SOCKS5."""
|
||||
def net_check(
|
||||
target: str,
|
||||
timeout_s: float = 8.0,
|
||||
*,
|
||||
handshake: bool = False,
|
||||
userid: str | None = None,
|
||||
server_key_hex: str | None = None,
|
||||
) -> dict:
|
||||
"""Connectivity diagnostic for direct/server endpoints, with optional SOCKS5 and handshake."""
|
||||
host, port = _parse_server(target)
|
||||
proxy = _proxy_target()
|
||||
via = "direct" if proxy is None else f"socks5:{proxy[0]}:{proxy[1]}"
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout_s)
|
||||
try:
|
||||
if proxy is None:
|
||||
sock.connect((host, port))
|
||||
via = "direct"
|
||||
else:
|
||||
sock.connect(proxy)
|
||||
_socks5_connect(sock, host, port)
|
||||
via = f"socks5:{proxy[0]}:{proxy[1]}"
|
||||
return {"ok": True, "target": target, "via": via}
|
||||
|
||||
result = {"ok": True, "target": target, "via": via}
|
||||
if handshake:
|
||||
if server_key_hex and userid:
|
||||
return {
|
||||
"ok": False,
|
||||
"target": target,
|
||||
"via": via,
|
||||
"error": "use either server_key_hex or userid for handshake, not both",
|
||||
}
|
||||
if server_key_hex:
|
||||
server_pk = zkac.PublicKey.from_bytes(bytes.fromhex(server_key_hex))
|
||||
elif userid:
|
||||
server_pk = _resolve_server_pk(userid, target)
|
||||
else:
|
||||
return {
|
||||
"ok": False,
|
||||
"target": target,
|
||||
"via": via,
|
||||
"error": "handshake check requires --key <hex> or --userid <id>",
|
||||
}
|
||||
node = zkac.Node(zkac.Keypair())
|
||||
_ = client_handshake_anon(sock, node, server_pk)
|
||||
result["handshake"] = "ok"
|
||||
return result
|
||||
except Exception as exc:
|
||||
via = "direct" if proxy is None else f"socks5:{proxy[0]}:{proxy[1]}"
|
||||
return {"ok": False, "target": target, "via": via, "error": str(exc)}
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
@ -34,12 +34,14 @@ def _cmd_user_list(_args):
|
||||
|
||||
def _cmd_user_show(args):
|
||||
ident = store.load_identity(args.userid)
|
||||
contact = store.export_contact_bundle(args.userid)
|
||||
contact = store.export_contact_bundle(args.userid, peer=args.peer)
|
||||
print(f"user: {args.userid}")
|
||||
print(f" issuance pk: {ident['issuance_pk'].hex()}")
|
||||
print(f" p2p transport pk: {ident['transport_pk'].hex()}")
|
||||
print(" share contact:")
|
||||
print(f" {contact}")
|
||||
if args.peer:
|
||||
print(f" contact peer endpoint: {args.peer}")
|
||||
|
||||
owned = [
|
||||
{"registry_id": r, **store.load_admin(args.userid, r)}
|
||||
@ -109,19 +111,18 @@ def _cmd_registry_list(args):
|
||||
# ── grant ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _cmd_grant(args):
|
||||
to = args.to
|
||||
peer_key = args.peer_key
|
||||
if args.to_contact:
|
||||
if args.to or args.peer_key:
|
||||
raise RuntimeError("use either --to-contact OR (--to and --peer-key), not both")
|
||||
parsed = store.parse_contact_bundle(args.to_contact)
|
||||
to = parsed["issuance_pk_hex"]
|
||||
peer_key = parsed["transport_pk_hex"]
|
||||
elif not args.to or not args.peer_key:
|
||||
raise RuntimeError("grant requires --to and --peer-key (or --to-contact)")
|
||||
parsed = store.parse_contact_bundle(args.to)
|
||||
to = parsed["issuance_pk_hex"]
|
||||
peer_key = parsed["transport_pk_hex"]
|
||||
peer = parsed.get("peer")
|
||||
if not peer:
|
||||
raise RuntimeError(
|
||||
"contact bundle is missing peer endpoint. "
|
||||
"Ask recipient to regenerate with: zkac-node user show <userid> --peer <host:port>"
|
||||
)
|
||||
|
||||
result = client.grant_p2p(
|
||||
args.userid, args.server, args.registry, args.role, to, args.peer, peer_key,
|
||||
args.userid, args.server, args.registry, args.role, to, peer, peer_key,
|
||||
)
|
||||
print(f"granted {args.role!r} to {to[:16]}…")
|
||||
print(f" delivery: direct p2p ({result['peer']})")
|
||||
@ -158,11 +159,19 @@ def _cmd_auth(args):
|
||||
|
||||
|
||||
def _cmd_net_check(args):
|
||||
resp = client.net_check(args.target, timeout_s=args.timeout)
|
||||
resp = client.net_check(
|
||||
args.target,
|
||||
timeout_s=args.timeout,
|
||||
handshake=args.handshake,
|
||||
userid=args.userid,
|
||||
server_key_hex=args.key,
|
||||
)
|
||||
if resp.get("ok"):
|
||||
print("network check: ok")
|
||||
print(f" target: {resp['target']}")
|
||||
print(f" via: {resp['via']}")
|
||||
if args.handshake:
|
||||
print(" handshake: ok")
|
||||
return
|
||||
print("network check: failed")
|
||||
print(f" target: {resp['target']}")
|
||||
@ -190,6 +199,7 @@ def main():
|
||||
|
||||
c = user_sub.add_parser("show", help="show user keys + registries + credentials")
|
||||
c.add_argument("userid")
|
||||
c.add_argument("--peer", default=None, help="optional recipient p2p host:port to embed in contact bundle")
|
||||
c.set_defaults(func=_cmd_user_show)
|
||||
|
||||
# serve
|
||||
@ -243,10 +253,7 @@ def main():
|
||||
c.add_argument("--server", required=True, help="host:port")
|
||||
c.add_argument("--registry", required=True)
|
||||
c.add_argument("--role", required=True)
|
||||
c.add_argument("--to", default=None, help="recipient issuance public key (hex)")
|
||||
c.add_argument("--to-contact", default=None, help="recipient one-string contact bundle")
|
||||
c.add_argument("--peer", required=True, help="recipient p2p host:port")
|
||||
c.add_argument("--peer-key", default=None, help="recipient p2p transport public key (hex)")
|
||||
c.add_argument("--to", required=True, help="recipient contact bundle")
|
||||
c.set_defaults(func=_cmd_grant)
|
||||
|
||||
# credentials
|
||||
@ -277,6 +284,9 @@ def main():
|
||||
c = net_sub.add_parser("check", help="test TCP reachability (direct or SOCKS5 proxy)")
|
||||
c.add_argument("target", help="host:port (supports .b32.i2p via SOCKS5)")
|
||||
c.add_argument("--timeout", type=float, default=8.0, help="connect timeout in seconds")
|
||||
c.add_argument("--handshake", action="store_true", help="also run anonymous ZKAC handshake")
|
||||
c.add_argument("--userid", default=None, help="client userid to load pinned server key for --handshake")
|
||||
c.add_argument("--key", default=None, help="server public key hex for --handshake")
|
||||
c.set_defaults(func=_cmd_net_check)
|
||||
|
||||
args = p.parse_args()
|
||||
|
||||
@ -61,14 +61,16 @@ def load_identity(userid: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def export_contact_bundle(userid: str) -> str:
|
||||
def export_contact_bundle(userid: str, peer: str | None = None) -> str:
|
||||
"""One-string public contact bundle for out-of-band sharing."""
|
||||
ident = load_identity(userid)
|
||||
payload = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"issuance_pk_hex": ident["issuance_pk"].hex(),
|
||||
"transport_pk_hex": ident["transport_pk"].hex(),
|
||||
}
|
||||
if peer:
|
||||
payload["peer"] = peer
|
||||
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
@ -83,7 +85,8 @@ def parse_contact_bundle(bundle: str) -> dict:
|
||||
except Exception as exc:
|
||||
raise ValueError("invalid contact bundle encoding") from exc
|
||||
|
||||
if data.get("v") != 1:
|
||||
version = data.get("v")
|
||||
if version not in (1, 2):
|
||||
raise ValueError("unsupported contact bundle version")
|
||||
issuance_hex = data.get("issuance_pk_hex", "")
|
||||
transport_hex = data.get("transport_pk_hex", "")
|
||||
@ -98,10 +101,17 @@ def parse_contact_bundle(bundle: str) -> dict:
|
||||
raise ValueError("issuance public key must be 32 bytes")
|
||||
if len(transport) != 32:
|
||||
raise ValueError("transport public key must be 32 bytes")
|
||||
return {
|
||||
parsed = {
|
||||
"issuance_pk_hex": issuance_hex,
|
||||
"transport_pk_hex": transport_hex,
|
||||
}
|
||||
if version == 2:
|
||||
peer = data.get("peer")
|
||||
if peer is not None and not isinstance(peer, str):
|
||||
raise ValueError("invalid contact bundle peer field")
|
||||
if isinstance(peer, str) and peer.strip():
|
||||
parsed["peer"] = peer.strip()
|
||||
return parsed
|
||||
|
||||
|
||||
def list_users() -> list[str]:
|
||||
|
||||
322
demo/zkac_admin_serve.py
Normal file
322
demo/zkac_admin_serve.py
Normal file
@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP admin debug dashboard for ``zkac-node serve``.
|
||||
|
||||
Runs the ZKAC TCP node in a background thread and serves a read-only web UI on
|
||||
another port. Intended for loopback + I2P server-tunnel forwarding.
|
||||
|
||||
**Security:** This page is fully transparent (registry metadata, live sessions,
|
||||
public keys). Do not expose it to untrusted networks without tunnel ACLs.
|
||||
|
||||
Usage::
|
||||
|
||||
uv sync --extra demo
|
||||
uv run python demo/zkac_admin_serve.py alice \\
|
||||
--node-host 127.0.0.1 --node-port 9800 \\
|
||||
--web-host 127.0.0.1 --web-port 8766
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, jsonify, render_template_string, request
|
||||
|
||||
from zkac_cli.paths import user_dir
|
||||
from zkac_cli.server import serve
|
||||
from zkac_cli.server_debug import (
|
||||
ServerDebugState,
|
||||
collect_registry_debug,
|
||||
data_dir_tree,
|
||||
server_key_meta,
|
||||
)
|
||||
|
||||
_PAGE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{% if refresh_s %}
|
||||
<meta http-equiv="refresh" content="{{ refresh_s }}"/>
|
||||
{% endif %}
|
||||
<title>ZKAC node — admin debug</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1419;
|
||||
--panel: #1a2332;
|
||||
--border: #2d3d52;
|
||||
--text: #e7ecf3;
|
||||
--muted: #8b9cb3;
|
||||
--accent: #5b9bd5;
|
||||
--ok: #6cbf6c;
|
||||
--warn: #e6b35a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
padding: 1.25rem 1.5rem 2rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 600; margin: 0 0 0.25rem; }
|
||||
.sub { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.25rem; }
|
||||
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
||||
section {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
}
|
||||
section h2 {
|
||||
margin: 0 0 0.65rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
pre, .mono {
|
||||
font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
|
||||
font-size: 0.78rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background: #0c1017;
|
||||
border-radius: 6px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin: 0;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
||||
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); }
|
||||
th { color: var(--muted); font-weight: 500; }
|
||||
.pill { display: inline-block; padding: 0.12rem 0.45rem; border-radius: 999px; font-size: 0.72rem; }
|
||||
.pill-live { background: #1e3d2a; color: var(--ok); }
|
||||
.pill-warn { background: #3d3518; color: var(--warn); }
|
||||
a { color: var(--accent); }
|
||||
.links { margin-top: 1rem; font-size: 0.88rem; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ZKAC node — admin debug</h1>
|
||||
<p class="sub">
|
||||
User <strong>{{ snap.userid }}</strong> · TCP
|
||||
<span class="mono">{{ snap.listen.host }}:{{ snap.listen.port }}</span>
|
||||
· uptime {{ snap.uptime_s }}s
|
||||
{% if refresh_s %}
|
||||
· auto-refresh {{ refresh_s }}s
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<section>
|
||||
<h2>Status</h2>
|
||||
<table>
|
||||
<tr><th>Data directory</th><td class="mono">{{ snap.data_dir }}</td></tr>
|
||||
<tr><th>Started (wall)</th><td class="mono">{{ snap.started_wall }}</td></tr>
|
||||
<tr><th>Server public key</th><td class="mono">{{ snap.server_public_key_hex or "—" }}</td></tr>
|
||||
<tr><th>Registries (boot)</th><td>{{ snap.registries_loaded_boot }}</td></tr>
|
||||
<tr><th>Active TCP sessions</th><td>
|
||||
<span class="pill pill-live">{{ snap.active_connection_count }} live</span>
|
||||
</td></tr>
|
||||
<tr><th>Process</th><td class="mono">pid={{ proc.pid }} threads={{ proc.threads }}</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Server key file</h2>
|
||||
<pre>{{ sk_meta | tojson(indent=2) }}</pre>
|
||||
</section>
|
||||
|
||||
<section style="grid-column: 1 / -1;">
|
||||
<h2>Registries (on disk + parsed)</h2>
|
||||
{% if reg %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID (file)</th>
|
||||
<th>version</th>
|
||||
<th>state hash</th>
|
||||
<th>bytes (state / cert)</th>
|
||||
<th>parse</th>
|
||||
</tr>
|
||||
{% for r in reg %}
|
||||
<tr>
|
||||
<td class="mono">{{ r.file_registry_id_hex[:16] }}…</td>
|
||||
<td>{{ r.get("version", "—") }}</td>
|
||||
<td class="mono">{% if r.get("state_hash_hex") %}{{ r.state_hash_hex[:24] }}…{% else %}—{% endif %}</td>
|
||||
<td>{{ r.state_bytes }} / {{ r.cert_bytes }}</td>
|
||||
<td>{% if r.parsed_ok %}<span class="pill pill-live">ok</span>{% else %}<span class="pill pill-warn">fail</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No registry state files yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section style="grid-column: 1 / -1;">
|
||||
<h2>Session connections (live)</h2>
|
||||
{% if snap.active_connections %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>id</th><th>peer</th><th>phase</th><th>op</th><th>transcript</th>
|
||||
<th>auth registry</th><th>role</th><th>mgmt #</th><th>echo bytes</th>
|
||||
</tr>
|
||||
{% for c in snap.active_connections %}
|
||||
<tr>
|
||||
<td class="mono">{{ c.id }}</td>
|
||||
<td class="mono">{{ c.peer }}</td>
|
||||
<td>{{ c.phase }}</td>
|
||||
<td>{{ c.hello_op or "—" }}</td>
|
||||
<td class="mono">{% if c.transcript_hash_hex %}{{ c.transcript_hash_hex[:20] }}…{% else %}—{% endif %}</td>
|
||||
<td class="mono">{% if c.auth_registry_hex %}{{ c.auth_registry_hex[:16] }}…{% else %}—{% endif %}</td>
|
||||
<td class="mono">{% if c.auth_role_hex %}{{ c.auth_role_hex[:16] }}…{% else %}—{% endif %}</td>
|
||||
<td>{{ c.mgmt_commands or 0 }}</td>
|
||||
<td>{{ c.bytes_echoed or 0 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No active connections.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section style="grid-column: 1 / -1;">
|
||||
<h2>Recent connections</h2>
|
||||
{% if snap.recent_connections %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>id</th><th>peer</th><th>phase</th><th>mgmt #</th><th>echo bytes</th><th>error</th>
|
||||
</tr>
|
||||
{% for c in snap.recent_connections %}
|
||||
<tr>
|
||||
<td class="mono">{{ c.id }}</td>
|
||||
<td class="mono">{{ c.peer }}</td>
|
||||
<td>{{ c.phase }}</td>
|
||||
<td>{{ c.mgmt_commands or 0 }}</td>
|
||||
<td>{{ c.bytes_echoed or 0 }}</td>
|
||||
<td class="mono">{{ c.error or "—" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No recent disconnects recorded.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section style="grid-column: 1 / -1;">
|
||||
<h2>Data directory tree (debug)</h2>
|
||||
<pre>{{ files | tojson(indent=2) }}</pre>
|
||||
</section>
|
||||
|
||||
<section style="grid-column: 1 / -1;">
|
||||
<h2>Full debug JSON</h2>
|
||||
<p class="sub" style="margin-top:0">Machine-readable snapshot (same as <a href="/api/debug.json">/api/debug.json</a>).</p>
|
||||
<pre>{{ full_json }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="links">
|
||||
<a href="?refresh=3">Auto-refresh 3s</a> ·
|
||||
<a href="?refresh=5">5s</a> ·
|
||||
<a href="?">static</a> ·
|
||||
<a href="/api/debug.json">debug.json</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def _full_payload(debug: ServerDebugState, data_dir: Path) -> dict:
|
||||
snap = debug.snapshot()
|
||||
reg = collect_registry_debug(data_dir)
|
||||
sk = server_key_meta(data_dir)
|
||||
files = data_dir_tree(data_dir)
|
||||
return {
|
||||
"snapshot": snap,
|
||||
"server_key_file": sk,
|
||||
"registries": reg,
|
||||
"data_dir_files": files,
|
||||
"process": {"pid": os.getpid(), "threads": threading.active_count()},
|
||||
"generated_wall": time.time(),
|
||||
}
|
||||
|
||||
|
||||
def create_app(debug: ServerDebugState, data_dir: Path) -> Flask:
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
refresh = request.args.get("refresh", "").strip()
|
||||
refresh_s = int(refresh) if refresh.isdigit() and 1 <= int(refresh) <= 60 else None
|
||||
data = _full_payload(debug, data_dir)
|
||||
snap = data["snapshot"]
|
||||
proc = data["process"]
|
||||
full_json = json.dumps(data, indent=2, sort_keys=True)
|
||||
return render_template_string(
|
||||
_PAGE,
|
||||
snap=snap,
|
||||
reg=data["registries"],
|
||||
sk_meta=data["server_key_file"],
|
||||
files=data["data_dir_files"],
|
||||
full_json=full_json,
|
||||
proc=proc,
|
||||
refresh_s=refresh_s,
|
||||
)
|
||||
|
||||
@app.get("/api/debug.json")
|
||||
def api_debug():
|
||||
return jsonify(_full_payload(debug, data_dir))
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return Response("ok", mimetype="text/plain")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="ZKAC node TCP + HTTP admin debug dashboard")
|
||||
p.add_argument("userid", help="user whose ~/.zkac/<userid>/server/ holds node state")
|
||||
p.add_argument("--data-dir", default=None, help="override server data directory")
|
||||
p.add_argument("--node-host", default="127.0.0.1")
|
||||
p.add_argument("--node-port", type=int, default=9800)
|
||||
p.add_argument("--web-host", default="127.0.0.1")
|
||||
p.add_argument("--web-port", type=int, default=8766)
|
||||
args = p.parse_args()
|
||||
|
||||
data_dir = Path(args.data_dir) if args.data_dir else user_dir(args.userid) / "server"
|
||||
data_dir = data_dir.resolve()
|
||||
debug = ServerDebugState(userid=args.userid, data_dir=str(data_dir))
|
||||
|
||||
t = threading.Thread(
|
||||
target=serve,
|
||||
kwargs={
|
||||
"data_dir": str(data_dir),
|
||||
"host": args.node_host,
|
||||
"port": args.node_port,
|
||||
"debug": debug,
|
||||
},
|
||||
name="zkac-serve",
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
time.sleep(0.15)
|
||||
if not t.is_alive():
|
||||
print("ZKAC node thread died on startup; check stderr above.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
app = create_app(debug, data_dir)
|
||||
print(f"ZKAC TCP node: {args.node_host}:{args.node_port} (data {data_dir})")
|
||||
print(f"Admin debug UI: http://{args.web_host}:{args.web_port}/")
|
||||
print("Warning: admin UI exposes live sessions and registry metadata.", file=sys.stderr)
|
||||
app.run(host=args.web_host, port=args.web_port, debug=False, threaded=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user