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>
This commit is contained in:
14
CLAUDE.md
14
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
|
hand-edit `mods-sides.json` to `client`. No Modrinth/packwiz, no network at
|
||||||
classify/sync time. See `plan/04-mods.md`.
|
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:
|
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
|
- The `local` backend is **admin-gated**: without `LOCAL_BACKEND_SECRET` in the
|
||||||
connection params, every login fails with `backend error - Not Allowed` (looks
|
connection params, every login fails with `backend error - Not Allowed` (looks
|
||||||
like bad credentials, isn't).
|
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**,
|
purpose). Single-file bind mount ⇒ **`--force-recreate` after re-rendering**,
|
||||||
never `restart`.
|
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
|
### Memory sizing
|
||||||
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
|
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
|
||||||
`USE_AIKAR_FLAGS: TRUE`.
|
`USE_AIKAR_FLAGS: TRUE`.
|
||||||
|
|||||||
12
README.md
12
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
|
Only valid once the host is off the public internet: an unauthenticated
|
||||||
*writable* share on a public domain is an open upload relay.
|
*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
|
- **`FILES_PASSWORD_HASH` must be `$6$`** (`openssl passwd -6`). The htpasswd
|
||||||
plugin's bcrypt branch only matches `$2a$`, so a `$2y$` hash from
|
plugin's bcrypt branch only matches `$2a$`, so a `$2y$` hash from
|
||||||
`htpasswd -B` fails *every* login, silently. `render-config.sh` rejects it.
|
`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
|
- **Don't set `APPLICATION_URL`/`ADMIN_PASSWORD` env** — either makes Filestash
|
||||||
rewrite `config.json` at boot, which fails against the `:ro` mount.
|
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)
|
## Join (guests)
|
||||||
|
|
||||||
1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS.
|
1. Open `https://${BASE_DOMAIN}`, download FjordLauncherUnlocked for your OS.
|
||||||
|
|||||||
@@ -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.
|
(Settings → Status Pages) so guests can self-check before pinging you.
|
||||||
- Optional companion HTTP monitors for the web stack:
|
- Optional companion HTTP monitors for the web stack:
|
||||||
`https://auth.${BASE_DOMAIN}`, `https://distribution.${BASE_DOMAIN}`,
|
`https://auth.${BASE_DOMAIN}`, `https://distribution.${BASE_DOMAIN}`,
|
||||||
`https://${BASE_DOMAIN}` — a red `distribution.` here explains client
|
`https://${BASE_DOMAIN}`, `https://files.${BASE_DOMAIN}` — a red
|
||||||
mod-mismatch join failures.
|
`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".
|
||||||
|
|||||||
@@ -65,8 +65,16 @@ base `https://auth.${BASE_DOMAIN}/drasl/api/v2` (`site.draslApiUrl`):
|
|||||||
login** for every player (`FILES_AUTH=htpasswd`), writable. Full design:
|
login** for every player (`FILES_AUTH=htpasswd`), writable. Full design:
|
||||||
`plan/20-files.md`.
|
`plan/20-files.md`.
|
||||||
|
|
||||||
- `/` — SPA. `GET /api/session/auth/?label=files` renders the login form;
|
- `/` — SPA. The login form renders **only** at
|
||||||
`POST` the same URL with `user`/`password` authenticates.
|
`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`
|
- `/api/*` — JSON API. Requires the header `X-Requested-With: XmlHttpRequest`
|
||||||
(the SPA sends it); without it Filestash's intrusion detection 403s the
|
(the SPA sends it); without it Filestash's intrusion detection 403s the
|
||||||
request. Relevant when curling it by hand.
|
request. Relevant when curling it by hand.
|
||||||
|
|||||||
Reference in New Issue
Block a user