From 08fe8c9c53e7c8e4fca245e7aeaff383253a7819 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 14 Jul 2026 18:31:46 +0200 Subject: [PATCH] docs(files): record the general.host trap + that a green monitor proves nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 14 +++++++++++++- README.md | 12 +++++++++++- plan/10-uptime-kuma.md | 12 ++++++++++-- plan/19-routes.md | 12 ++++++++++-- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c24c45..ac57b3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,8 +106,14 @@ automatically; cosmetic-client mods that declare `BOTH` stay `both` until you hand-edit `mods-sides.json` to `client`. No Modrinth/packwiz, no network at classify/sync time. See `plan/04-mods.md`. -### Filestash (files.) — three traps +### Filestash (files.) — four traps Full list in `plan/20-files.md`; the ones that cost real time: +- **`general.host` is a BARE HOSTNAME** (`files.${BASE_DOMAIN}`, no scheme) + + `force_ssl: true`. The SPA builds its origin as `scheme + host`, so a URL there + yields `http://https://files...` and every client-side redirect dies. The + server strips the scheme before its own Host check, so **curl and the uptime + monitor stay green while every browser is broken**. Cost a debugging round on + 2026-07-14. - The `local` backend is **admin-gated**: without `LOCAL_BACKEND_SECRET` in the connection params, every login fails with `backend error - Not Allowed` (looks like bad credentials, isn't). @@ -117,6 +123,12 @@ Full list in `plan/20-files.md`; the ones that cost real time: purpose). Single-file bind mount ⇒ **`--force-recreate` after re-rendering**, never `restart`. +**Verifying an SPA:** a 200 on `/` proves the server is up, not that the app +works. After any filestash config change, check the browser-facing values +(`curl /api/config` → `origin`) and re-read `docker compose logs filestash` — +the broken redirect announced itself there (`msg=Redirecting to http://https://`) +while every other check passed. + ### Memory sizing ~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`, `USE_AIKAR_FLAGS: TRUE`. diff --git a/README.md b/README.md index efca293..430d873 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,13 @@ Full design, deploy steps and gotchas: **`plan/20-files.md`**. The short version Only valid once the host is off the public internet: an unauthenticated *writable* share on a public domain is an open upload relay. -Three traps that cost real time (all enforced or documented in the tooling): +Four traps that cost real time (all enforced or documented in the tooling): +- **`general.host` is a bare hostname**, no scheme (`files.${BASE_DOMAIN}`), with + `force_ssl: true`. The SPA builds its redirect origin as `scheme + host`, so a + URL there produces `http://https://files...` and every client-side redirect + breaks. The server strips the scheme before its *own* Host check, so `curl` + and the uptime monitor stay **green while every browser is broken**. - **`FILES_PASSWORD_HASH` must be `$6$`** (`openssl passwd -6`). The htpasswd plugin's bcrypt branch only matches `$2a$`, so a `$2y$` hash from `htpasswd -B` fails *every* login, silently. `render-config.sh` rejects it. @@ -192,6 +197,11 @@ Three traps that cost real time (all enforced or documented in the tooling): - **Don't set `APPLICATION_URL`/`ADMIN_PASSWORD` env** — either makes Filestash rewrite `config.json` at boot, which fails against the `:ro` mount. +> Verifying this service: a 200 on `/` only proves the server is up. Check the +> browser-facing origin — `curl -s -H 'X-Requested-With: XmlHttpRequest` +> `https://files.${BASE_DOMAIN}/api/config | jq -r .result.origin` must print +> `https://files.${BASE_DOMAIN}` — and re-read `docker compose logs filestash`. + ## Join (guests) 1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS. diff --git a/plan/10-uptime-kuma.md b/plan/10-uptime-kuma.md index 52e8e7e..445173e 100644 --- a/plan/10-uptime-kuma.md +++ b/plan/10-uptime-kuma.md @@ -51,5 +51,13 @@ monitor stays green. Run both to tell *server down* apart from *path broken*. (Settings → Status Pages) so guests can self-check before pinging you. - Optional companion HTTP monitors for the web stack: `https://auth.${BASE_DOMAIN}`, `https://distribution.${BASE_DOMAIN}`, - `https://${BASE_DOMAIN}` — a red `distribution.` here explains client - mod-mismatch join failures. + `https://${BASE_DOMAIN}`, `https://files.${BASE_DOMAIN}` — a red + `distribution.` here explains client mod-mismatch join failures. + +> **An HTTP monitor cannot see an SPA break.** It asserts "the server returned +> 200", which is a much weaker claim than "the app works". `files.` (Filestash) +> proved this on 2026-07-14: a bad `general.host` made every client-side redirect +> resolve to `http://https://files…`, so the site was unusable in every browser +> while `/` kept returning 200 and the monitor stayed **green**. If you want a +> monitor with teeth on a JS app, use Kuma's **Keyword** type against a string +> the *rendered* page must contain, and don't read green as "users can log in". diff --git a/plan/19-routes.md b/plan/19-routes.md index 3ad5497..1c3cfb4 100644 --- a/plan/19-routes.md +++ b/plan/19-routes.md @@ -65,8 +65,16 @@ base `https://auth.${BASE_DOMAIN}/drasl/api/v2` (`site.draslApiUrl`): 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. +- `/` — 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.