Files
ulicraft-server-v1/plan/19-routes.md
T
oierandClaude Opus 4.8 16523e4f62 feat(files): move filestash to upload., serve files. as read-only static browse
Filestash cannot be served under a subpath (its SPA hardcodes /api, /assets,
/login at root), so it moves to its own subdomain upload.${BASE_DOMAIN}. The
apex-level ask was a static, no-auth file browser at files.${BASE_DOMAIN}: caddy
now file_server browse's the same ${FILES_DATA_DIR} share read-only there.

- filestash general.host -> upload.; nginx upload. vhost keeps the 2g body +
  upload streaming + /admin throttle; new files. vhost is a plain static proxy.
- caddy: 60-files.caddy split into 60-upload.caddy (filestash) + 62-files.caddy
  (file_server browse of /srv/files); caddy mounts FILES_DATA_DIR :ro.
- LE_SUBDOMAINS += upload (both upload. and files. are vhosts needing certs).

Security: files. is unconditionally public read (no auth), independent of
FILES_AUTH. The htpasswd login only gates writes via upload. Documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:03:33 +02:00

6.2 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
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)

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.