Files
ulicraft-server-v1/plan/19-routes.md
Oier Bravo Urtasun 25df9f9398 feat(files): add Filestash player file share at files.${BASE_DOMAIN}
One shared login (htpasswd) for all players, writable. Config is rendered
from a template and mounted read-only, so git stays authoritative — the
admin console loads but cannot save, by design.

Filestash's OIDC/SAML middlewares are enterprise-only, so Keycloak SSO is
out; FILES_AUTH keeps htpasswd/passthrough switchable for later. Auth-off
(passthrough) is only valid once the host is off the public internet —
render-config.sh warns loudly when rendering it.

Three traps, all found by testing against the pinned image:
- the `local` backend is admin-gated: without LOCAL_BACKEND_SECRET in the
  connection params every login fails with "backend error - Not Allowed"
- the htpasswd plugin only accepts $2a$ bcrypt, so a $2y$ hash from
  `htpasswd -B` fails every login silently — use `openssl passwd -6`.
  render-config.sh rejects the wrong format rather than shipping it
- APPLICATION_URL/ADMIN_PASSWORD env make filestash rewrite config.json at
  boot, which fails on the :ro mount — both live in the rendered config

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

5.4 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.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/mcstatusmc-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. GET /api/session/auth/?label=files renders the login form; POST the same URL with user/password authenticates.
  • /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 /playersadmin 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).
  • nmsrhttp://auth.${BASE_DOMAIN} (mojang-compat session/textures/api).
  • mc-statushttp://drasl:25585/drasl/api/v2 (DRASL_API_URL) for the roster; pings MC_STATUS_TARGET for live status.
  • mc-backup → RCON minecraft:25575.