diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..13a73dc --- /dev/null +++ b/SPEC.md @@ -0,0 +1,33 @@ +# Endpoint Monitor Service Contract + +## Architecture + +A single FastAPI process exposes typed REST routes, delegates process-local state to an `asyncio.Lock` protected store, and delegates outbound checks to a checker that validates and pins DNS results independently on every redirect hop. The production fetcher uses `aiohttp` with a per-hop pinned resolver, normal TLS hostname verification, proxy/environment settings disabled, manual redirects, and a total bounded timeout. + +This service intentionally has no authentication. It must therefore only be exposed where that is acceptable. State is volatile, is not shared between workers, and disappears on restart; run exactly one worker. + +## Resource and routes + +A monitor has UUID `id`, `name`, HTTP(S) `url`, creation/update timestamps, integer `revision`, and nullable `current_status`. Status is `unknown`, `up`, or `down`; a check records HTTP status (if any), elapsed milliseconds, check time, and a stable error category/message (if any). + +* `POST /monitors` -> 201 +* `GET /monitors` -> 200 +* `GET /monitors/{id}` -> 200 or 404 +* `PUT /monitors/{id}` -> 200 or 404; status is reset when URL changes +* `DELETE /monitors/{id}` -> 204 or 404 +* `POST /monitors/{id}/check` -> 200 or 404. Network and policy failures are completed checks represented as `down`, not transport errors from this API. +* `GET /monitors/{id}/status` -> 200 or 404 +* `GET /health` -> liveness +* `GET /ready` -> readiness and explicit process-local storage mode + +Validation errors use FastAPI's 422 response. Application errors have `{"detail":{"code":...,"message":...}}`. A check updates status atomically only if the monitor still exists at the same revision; its response says whether it was applied, preventing stale in-flight checks from overwriting an edit. + +## Security and logging + +Only HTTP and HTTPS are accepted. Userinfo is forbidden. Before **each** request and redirect, every DNS answer is classified with `ipaddress`; any non-global, private, loopback, link-local, multicast, reserved, or unspecified answer rejects the hop. Literal IPs follow the same rule. The validated addresses are pinned into that hop's connector, preventing a second resolver lookup/DNS rebinding. Redirects are manual and bounded. Total, connect, and read timeouts are bounded. Environment proxies are disabled. + +Logs are one-line JSON. URLs are sanitized to scheme/host/port/path; query, fragment, and userinfo never enter structured fields. Error strings are normalized and never include exception text that could echo a secret-bearing URL. + +## Project layout + +`app/` contains configuration, models, locked storage, SSRF policy, outbound checker, logging, and API assembly. `tests/` exercises API/store/checker policy with fake outbound I/O. `pyproject.toml`, Docker assets, and README provide developer and runtime workflows.