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>
6.0 KiB
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.netin 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 (headisois the configured default viaAVATAR_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. Seeplan/03-drasl.mdfor 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 pingsMC_STATUS_TARGET). LandingstatusApi./api/mcstatus/players→ internal/players— registered-players roster, sanitized[{uuid,name}], ~60s cache (mc-status admin-logs-in to DraslGET /players). LandingrosterApi./healthz— mc-status liveness (internal).
handle {}→/srv/wwwstatic (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 atGET /api/session/auth/?action=redirect&label=files. Withoutaction=redirectthe request falls through to the credential check, fails with empty creds and 303s back to?action=redirect— so a bare/api/session/auth/?label=filesreturning a 303 is normal, not a bug.POSTwithuser/passwordauthenticates.GET /api/config→result.originmust behttps://files.${BASE_DOMAIN}. It's built as(force_ssl ? https:// : http://) + general.host, so a scheme ingeneral.hostyieldshttp://https://…and silently breaks every browser while the server keeps answering 200. Check this after any config change./api/*— JSON API. Requires the headerX-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=filesmatters. The attribute mapping is keyed by the connection label; auth without it yieldsbackend 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 whenRequireInvite=true; admins fromDefaultAdminsare promoted at startup, seeplan/03-drasl.md)GET /players— admin only, full rosterPATCH /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; pingsMC_STATUS_TARGETfor live status.mc-backup→ RCONminecraft:25575.