Files
ulicraft-server-v1/plan/19-routes.md
Oier Bravo Urtasun 08fe8c9c53 docs(files): record the general.host trap + that a green monitor proves nothing
Propagate the bare-hostname fix (10b86ae) to the docs that still said "three
traps", and write down the meta-lesson it exposed.

The bug was invisible to every check we had: Filestash strips the scheme
before its own Host check, so `curl /` returned 200, the API answered, wrong
passwords were rejected, and the brand-new Uptime Kuma HTTP monitor stayed
green — while the SPA computed `http://https://files...` and no browser could
use the site. An HTTP monitor asserts "server returned 200", which is a much
weaker claim than "the app works".

- CLAUDE.md / README: four traps now, host-scheme first; add the post-change
  check (`/api/config` → origin must be the real https URL).
- 19-routes: login form only renders at ?action=redirect (a bare 303 is
  normal, not a bug); assert origin after config changes.
- 10-uptime-kuma: an HTTP monitor cannot see an SPA break — use a Keyword
  monitor if you want teeth, and don't read green as "users can log in".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:31:46 +02:00

117 lines
6.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` |
| `files.${BASE_DOMAIN}` | `60-files.caddy` | `filestash:8334` |
| `distribution.${BASE_DOMAIN}` | `30-distribution.caddy` | static `/srv/distribution` (other repo) |
---
## 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
---
## 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`.