ctf-ad-mirror-tool-rust/docs/api.md

3.4 KiB

HTTP contract

JSON API, UTF-8. Authenticated sessions use an HttpOnly SameSite=Strict cookie. Mutating authenticated requests require X-CSRF-Token from login or session response. Browser requests must be same-origin. No secrets returned in snapshots. All failures return { "error": "human readable explanation" } with non-2xx status. A snapshot is the source of truth; never assume a mutation succeeded before its 2xx response.

Routes

  • GET / — embedded UI shell (login shown until session validated).
  • GET /healthz — process readiness {status:"ok"} (not upstream checker health).
  • POST /api/login JSON {password:string} → {authenticated:true,csrf:string}, Set-Cookie. 401 invalid, 429 limited.
  • GET /api/session → {authenticated:true,csrf:string}, 401 expired/missing.
  • POST /api/logout → {ok:true}, clears cookie.
  • GET /api/snapshot → full snapshot below.
  • GET /api/events — SSE, event name snapshot, full snapshot every second, heartbeat 15s. Session expiry closes stream. EventSource reconnects automatically; recheck session after errors.
  • PUT /api/services/{id} — full ServiceConfig JSON; id must match path. Atomically save and hot-apply. → {ok:true}. Validation/bind error: 400; persistence failure: 400 with explanation, runtime rollback attempted. Stopped listener drains established sessions. Changing primary, replicas, policy or auto mode does not restart a running listener. Existing sessions keep their previous route.
  • DELETE /api/services/{id} — requires enabled:false first; drains/removes then → {ok:true}.
  • PUT /api/teams/{id} — full Team JSON; new team or rename. Addresses of existing ids are immutable; add another id instead. → {ok:true}.

ServiceConfig:

{"id":"notes","name":"Notes","listen":"0.0.0.0:18080","upstream_port":8080,"primary":"a","replicas":["b"],"enabled":false,"auto_switch":false,"replica_policy":"isolate"}

Team:

{"id":"a","name":"Team A","address":"192.0.2.10"}

Snapshot

{
 "now_ms": 1792700000000,
 "teams": [{"id":"a","name":"Team A","address":"192.0.2.10"}],
 "services": [{
   "id":"notes","name":"Notes","listen":"0.0.0.0:18080","upstream_port":8080,
   "primary":"a","replicas":[],"enabled":true,"auto_switch":false,"replica_policy":"isolate",
   "engine":{"running":true,"last_error":null,"stats":{"accepted":5,"active":1,"completed":4,"connect_errors":0,"io_errors":0,"rejected":0,"bytes_up":1234,"bytes_down":5678,"replica_disconnects":0,"replica_errors":0,"replica_bytes":0}},
   "health":{"a":{"reachable":true,"healthy":true,"consecutive_failures":0,"consecutive_successes":2,"checked_at_ms":1792700000000,"latency_ms":2}},
   "scoreboard":{"a":{"status":101,"stolen":3,"lost":0}}
 }],
 "scoreboard":{"enabled":true,"connected":true,"kind":"forcad","round":5,"updated_at_ms":1792700000000,"stale":false,"last_error":null,"teams":[],"tasks":[],"entries":{}},
 "events":[{"at_ms":1792700000000,"service":"notes","message":"Настройки сохранены"}]
}

Compute displayed bandwidth from positive byte-counter deltas / now_ms deltas, reset baseline when counters decrease. Show unknown health until checked. Scoreboard badges must indicate stale data; never imply stale checker state is current. Replica counters mean explicit disconnections/errors, not dropped arbitrary packets. replica_policy: strict uses backpressure, isolate disconnects a lagging replica per session.