plan/23-monitor.md already covered the design; this makes the monitor discoverable from the places someone actually starts reading. - README: stack table, vhost list, DNS names, layout, and a setup section covering the bcrypt credential, the no-published-port model, and the --hard-on-first-deploy requirement. - plan/00-overview.md: drop (PLANNED), add to the always-on service list, and note that it starts fine with Minecraft down (missing log and refused RCON are reported states, not fatal). - plan/19-routes.md: vhost row, the four GET routes with their contracts, and the internal mcnet edges (mc-status for rosters so it needs no Drasl creds, RCON for TPS, Kuma probing healthz around caddy). - caddy/Caddyfile: the snippet index had drifted — it listed five of nine vhosts. Added the four missing ones. Two gotchas promoted to CLAUDE.md because they cost real time and will recur: - Compose interpolates $-sequences in .env VALUES, so a bare bcrypt hash is silently corrupted before reaching the container, and the same bug bites again when writing one to a remote .env over ssh. Also records that caddy and filestash want opposite hash formats. - The monitor container runs as root deliberately: /data is drwxr-x--- owned by uid 1000, so a USER directive would make the tailer silently read nothing while still returning 200. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
8.0 KiB
Markdown
151 lines
8.0 KiB
Markdown
# 19 — HTTP route reference (nmsr, landing, auth)
|
|
|
|
> Quick map of the public + internal HTTP routes. caddy (`caddy/conf.d/*.caddy`)
|
|
> is the internal router; the host nginx terminates TLS and forwards each vhost
|
|
> to it by Host header. All `${BASE_DOMAIN}` = `ulicraft.net` in prod.
|
|
|
|
## Vhost → service (caddy)
|
|
|
|
| Vhost | caddy file | Upstream |
|
|
|-------|-----------|----------|
|
|
| `${BASE_DOMAIN}` (apex) | `10-static.caddy` | static `/srv/www` + `/srv/launcher` + mc-status |
|
|
| `auth.${BASE_DOMAIN}` | `00-core.caddy` | `drasl:25585` |
|
|
| `avatar.${BASE_DOMAIN}` | `40-avatar.caddy` | `nmsr:8080` |
|
|
| `status.${BASE_DOMAIN}` | `50-status.caddy` | `uptime-kuma:3001` |
|
|
| `upload.${BASE_DOMAIN}` | `60-upload.caddy` | `filestash:8334` |
|
|
| `files.${BASE_DOMAIN}` | `62-files.caddy` | static `/srv/files` (read-only, no-auth browse of the share) |
|
|
| `distribution.${BASE_DOMAIN}` | `30-distribution.caddy` | static `/srv/distribution` (other repo) |
|
|
| `generators.${BASE_DOMAIN}` | `70-generators.caddy` | `generators:80` (offline misode fork + data mirror) |
|
|
| `monitor.${BASE_DOMAIN}` | `80-monitor.caddy` | `monitor:8080` (admin log stream + dashboard, **basic_auth**) |
|
|
|
|
---
|
|
|
|
## NMSR — `avatar.${BASE_DOMAIN}` (skin/avatar renderer)
|
|
|
|
`reverse_proxy nmsr:8080`. Stateless renderer; pulls profiles+skins from Drasl
|
|
over mcnet (no public round-trip).
|
|
|
|
- `GET /{mode}/{uuid}?size=N` — render an avatar PNG.
|
|
- `mode` = any NMSR render mode (`headiso` is the configured default via
|
|
`AVATAR_MODE`; others incl. `head`, `face`, `fullbody`, `fullbodyiso`).
|
|
- `uuid` = Drasl player UUID; `size` = px.
|
|
- Landing usage: `${avatarUrl}/${avatarMode}/<uuid>?size=64|96`.
|
|
- **No per-request refresh.** A `?skin=<url>` override is **ignored** by this
|
|
build (tested). Skin changes propagate via the resolve cache only.
|
|
- Caching (`nmsr/config.toml`): `resolve_cache_duration = 60s` (uuid→profile,
|
|
bounds stale-avatar window), `texture_cache_duration = 48h` (safe — Drasl skin
|
|
URLs are content-hashed, new skin = new URL). Instant global flush = restart
|
|
nmsr. See `plan/03-drasl.md` for the Drasl-backed mojank wiring.
|
|
|
|
---
|
|
|
|
## Landing — `${BASE_DOMAIN}` (apex)
|
|
|
|
`10-static.caddy`. Three handlers:
|
|
|
|
- `handle_path /launcher/*` → `/srv/launcher` (file browse) — launcher downloads.
|
|
- `handle /api/mcstatus/*` → `uri strip_prefix /api/mcstatus` → `mc-status:8080`:
|
|
- `/api/mcstatus/v2/status/java/<addr>` → internal `/v2/status/java/` — live
|
|
server status JSON (mcstatus.io-compatible, `CORS: *`, `<addr>` ignored — it
|
|
only pings `MC_STATUS_TARGET`). Landing `statusApi`.
|
|
- `/api/mcstatus/players` → internal `/players` — registered-players roster,
|
|
sanitized `[{uuid,name}]`, ~60s cache (mc-status admin-logs-in to Drasl
|
|
`GET /players`). Landing `rosterApi`.
|
|
- `/healthz` — mc-status liveness (internal).
|
|
- `handle {}` → `/srv/www` static (Astro output). Pages: `/`, `/es`, `/eu`
|
|
(index), `/{lang}/register`, `/{lang}/account`, `/{lang}/fjord`.
|
|
|
|
Browser → Drasl **directly** (CORS-scoped to apex via Drasl `CORSAllowOrigins`),
|
|
base `https://auth.${BASE_DOMAIN}/drasl/api/v2` (`site.draslApiUrl`):
|
|
- `register.astro`: `POST /users` (register), `POST /login`, `PATCH /players/{uuid}` (set initial skin).
|
|
- `account.astro`: `POST /login`, `PATCH /players/{uuid}` with `{skinBase64,skinModel}` (upload) or `{deleteSkin:true}` (reset).
|
|
|
|
---
|
|
|
|
## Files — `files.${BASE_DOMAIN}` (Filestash player share)
|
|
|
|
`reverse_proxy filestash:8334`. Screenshots / schematics / maps. **One shared
|
|
login** for every player (`FILES_AUTH=htpasswd`), writable. Full design:
|
|
`plan/20-files.md`.
|
|
|
|
- `/` — SPA. The login form renders **only** at
|
|
`GET /api/session/auth/?action=redirect&label=files`. Without `action=redirect`
|
|
the request falls through to the credential check, fails with empty creds and
|
|
303s back to `?action=redirect` — so a bare `/api/session/auth/?label=files`
|
|
returning a 303 is **normal, not a bug**. `POST` with `user`/`password`
|
|
authenticates.
|
|
- `GET /api/config` → `result.origin` must be `https://files.${BASE_DOMAIN}`.
|
|
It's built as `(force_ssl ? https:// : http://) + general.host`, so a scheme in
|
|
`general.host` yields `http://https://…` and silently breaks every browser
|
|
while the server keeps answering 200. Check this after any config change.
|
|
- `/api/*` — JSON API. Requires the header `X-Requested-With: XmlHttpRequest`
|
|
(the SPA sends it); without it Filestash's intrusion detection 403s the
|
|
request. Relevant when curling it by hand.
|
|
- `/admin` — admin console, own bcrypt password (`auth.admin`). Deliberately
|
|
public; nginx throttles it (`limit_req`, 6r/m). It **cannot save** — the
|
|
config is a read-only mount.
|
|
- **`?label=files` matters.** The attribute mapping is keyed by the connection
|
|
label; auth without it yields `backend error - Not Allowed`.
|
|
|
|
Filestash blocks any request whose `Host` doesn't match its `general.host`
|
|
(`https://files.${BASE_DOMAIN}`) — nginx forwards the original Host and caddy
|
|
passes it through, so don't rewrite it anywhere in the chain.
|
|
|
|
---
|
|
|
|
## Auth / Drasl — `auth.${BASE_DOMAIN}`
|
|
|
|
`reverse_proxy drasl:25585`. One service exposes three surfaces:
|
|
|
|
- **Web UI:** `/` (login / register / profile pages), `/web/texture/skin/<sha256>.png`, `/web/texture/cape/<sha256>.png`.
|
|
- **Yggdrasil / authlib-injector:** `/authlib-injector` (API root — server JVM
|
|
agent + clients point here), `/session/minecraft/...`, mojang-compat
|
|
`/users/profiles/minecraft/{name}` and `/session/minecraft/profile/{uuid}`
|
|
(also what NMSR calls internally).
|
|
- **Drasl REST API v2** (`/drasl/api/v2`):
|
|
- `POST /login` `{username,password}` → `{apiToken}`
|
|
- `POST /users` `{username,password[,inviteCode]}` — register (invite-gated when `RequireInvite=true`; admins from `DefaultAdmins` are promoted at startup, see `plan/03-drasl.md`)
|
|
- `GET /players` — **admin only**, full roster
|
|
- `PATCH /players/{uuid}` — skin CRUD (own player, or admin)
|
|
- `POST /invites` — admin, mint invite codes
|
|
|
|
---
|
|
|
|
## Monitor — `monitor.${BASE_DOMAIN}` (admin only)
|
|
|
|
`reverse_proxy monitor:8080`, behind caddy `basic_auth` (bcrypt,
|
|
`MONITOR_USER` / `MONITOR_PASSWORD_HASH`). **The only vhost in the stack that is
|
|
gated at caddy**, because the service itself has no auth and publishes no port —
|
|
caddy is the sole way in. Per-host on/off via `MONITOR_ENABLED`.
|
|
|
|
Read-only: every route is a `GET`, and nothing mutates state.
|
|
|
|
- `GET /` — the whole UI (single HTML file embedded in the binary).
|
|
- `GET /api/log[?since=<seq>]` → `{lines[], seq, dropped, reset, total, tail{}}`.
|
|
Omit `since` for the full buffer without a spurious "dropped" notice.
|
|
`since` is a **sequence number, not a byte offset** — the log rotates on every
|
|
deploy. `reset: true` means discard what you have (fell behind, or the service
|
|
restarted and your cursor is in the future).
|
|
- `GET /api/summary` → all dashboard tiles in one response. Each part is
|
|
independently cached and degrades on its own; failures appear as sibling
|
|
`*_err` fields next to the last good value.
|
|
- `GET /healthz` → `ok`. Liveness only, deliberately not tied to the log file.
|
|
|
|
Full design + gotchas: `plan/23-monitor.md`.
|
|
|
|
---
|
|
|
|
## Internal (service → service, over mcnet)
|
|
|
|
- `minecraft` → `-javaagent authlib-injector=http://auth.${BASE_DOMAIN}/authlib-injector` (caddy mcnet alias → drasl).
|
|
- `nmsr` → `http://auth.${BASE_DOMAIN}` (mojang-compat session/textures/api).
|
|
- `mc-status` → `http://drasl:25585/drasl/api/v2` (`DRASL_API_URL`) for the roster; pings `MC_STATUS_TARGET` for live status.
|
|
- `mc-backup` → RCON `minecraft:25575`.
|
|
- `monitor` → `http://mc-status:8080` (`/v2/status/java/*` + `/players`) for the
|
|
status and roster tiles, so it needs no Drasl credentials of its own; RCON
|
|
`minecraft:25575` for TPS, restricted to a compile-time allowlist
|
|
(`neoforge tps`, `list`) that no HTTP input can reach. Reads
|
|
`mc_data:/data:ro` (log + `statfs`) and `./backups:ro` (backup freshness).
|
|
- `uptime-kuma` → can probe `http://monitor:8080/healthz` directly, bypassing
|
|
caddy, so the monitor vhost needs no unauthenticated path carve-out.
|