The SPA computes its redirect origin as (force_ssl ? "https://" : "http://") + general.host. With host set to "https://files.${BASE_DOMAIN}" that produced "http://https://files.ulicraft.net", so every client-side redirect broke and the page never routed anywhere. Server-side this was invisible: SecureOrigin strips the scheme before the Host check, so `curl /` returned 200, the API answered, and the uptime monitor stayed green — only real browsers failed. The tell is a `POST /report?...msg=Redirecting to http://https://...` line in the log. Set host to the bare hostname and force_ssl=true (which the origin needs to build https://, and which only emits an HSTS header — it does not redirect, so it can't loop behind the plain-http nginx→caddy hop). Verified: origin is now "https://files.ulicraft.net"; login + ls still work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
4.9 KiB
Bash
Executable File
96 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# render-config.sh — turn *.tmpl files into real configs via envsubst.
|
|
# Substitutes ONLY a small whitelist so literal $-syntax that belongs to the
|
|
# target file (caddy, toml) is left untouched.
|
|
# Halts on any unset var or missing template (cautious: no half-rendered output).
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.." # repo root
|
|
|
|
[ -f .env ] && { set -a; . ./.env; set +a; }
|
|
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
|
|
|
# REGISTRATION_MODE (invite|open) -> Drasl RequireInvite boolean. drasl is
|
|
# TOML-only with no env support, so we derive the literal here and substitute it.
|
|
: "${REGISTRATION_MODE:=invite}"
|
|
case "$REGISTRATION_MODE" in
|
|
invite) REGISTRATION_REQUIRE_INVITE=true ;;
|
|
open) REGISTRATION_REQUIRE_INVITE=false ;;
|
|
*) echo "REGISTRATION_MODE must be 'invite' or 'open' (got '$REGISTRATION_MODE')" >&2; exit 1 ;;
|
|
esac
|
|
export REGISTRATION_REQUIRE_INVITE
|
|
|
|
render() { # $1=template $2=output
|
|
[ -f "$1" ] || { echo "missing template: $1" >&2; exit 1; }
|
|
envsubst '${BASE_DOMAIN} ${REGISTRATION_REQUIRE_INVITE}' < "$1" > "$2"
|
|
echo "rendered $2"
|
|
}
|
|
|
|
render drasl/config/config.toml.tmpl drasl/config/config.toml
|
|
render nmsr/config.toml.tmpl nmsr/config.toml
|
|
|
|
# ── filestash (files.${BASE_DOMAIN}) ────────────────────────────────────
|
|
# Filestash has NO env-var config: config.json is the source of truth and the
|
|
# admin console writes it back at runtime. We render it and mount it read-only,
|
|
# so git stays authoritative (the console renders but cannot save — by design).
|
|
#
|
|
# general.host in the template is a BARE HOSTNAME (files.$BASE_DOMAIN, no
|
|
# scheme) and general.force_ssl is true. The SPA builds its redirect origin as
|
|
# (force_ssl ? "https://" : "http://") + host, so a scheme in `host` produces
|
|
# "http://https://files..." and every client-side redirect breaks. The
|
|
# server-side Host check strips the scheme first, so curl/monitors see a healthy
|
|
# 200 and ONLY browsers fail. Don't "fix" it by putting the URL back.
|
|
#
|
|
# FILES_AUTH picks the identity_provider block. The params fields are JSON
|
|
# *strings* holding escaped JSON (that's Filestash's schema, not a typo) —
|
|
# built here rather than in the template because the shape differs per mode.
|
|
: "${FILES_AUTH:=htpasswd}"
|
|
: "${FILES_SECRET_KEY:?FILES_SECRET_KEY unset (openssl rand -hex 16)}"
|
|
: "${FILES_ADMIN_PASSWORD_HASH:?FILES_ADMIN_PASSWORD_HASH unset (docker run --rm caddy:alpine caddy hash-password --plaintext '<pw>')}"
|
|
: "${FILES_LOCAL_SECRET:?FILES_LOCAL_SECRET unset (openssl rand -hex 24) — unlocks the admin-gated local backend}"
|
|
|
|
case "$FILES_AUTH" in
|
|
htpasswd)
|
|
: "${FILES_USER:?FILES_USER unset (shared login for players)}"
|
|
: "${FILES_PASSWORD_HASH:?FILES_PASSWORD_HASH unset (openssl passwd -6 '<pw>')}"
|
|
# The htpasswd plugin's bcrypt branch only accepts $2a$ — a $2y$ hash (what
|
|
# `htpasswd -B` emits) silently fails every login. $6$ (openssl passwd -6)
|
|
# is the safe, explicitly-supported format. Halt rather than ship a config
|
|
# nobody can log into.
|
|
case "$FILES_PASSWORD_HASH" in
|
|
'$6$'*|'$5$'*|'$2a$'*|'$apr1$'*|'$1$'*) ;;
|
|
'$2y$'*|'$2b$'*)
|
|
echo "FILES_PASSWORD_HASH is bcrypt '${FILES_PASSWORD_HASH:0:4}' — the htpasswd plugin only accepts \$2a\$." >&2
|
|
echo "Use: openssl passwd -6 '<password>'" >&2
|
|
exit 1 ;;
|
|
*) echo "FILES_PASSWORD_HASH is not a recognised hash (want \$6\$… from: openssl passwd -6 '<password>')" >&2; exit 1 ;;
|
|
esac
|
|
FILES_IDP_TYPE="htpasswd"
|
|
FILES_IDP_PARAMS="{\\\"type\\\":\\\"htpasswd\\\",\\\"users\\\":\\\"${FILES_USER}:${FILES_PASSWORD_HASH}\\\"}"
|
|
;;
|
|
passthrough|none)
|
|
# NO LOGIN — anyone who reaches the vhost gets in. Only safe once this host
|
|
# is NOT reachable from the internet (see plan/20-files.md).
|
|
echo "!! FILES_AUTH=$FILES_AUTH — filestash will have NO authentication." >&2
|
|
echo "!! Only valid if files.${BASE_DOMAIN} is unreachable from the internet." >&2
|
|
FILES_IDP_TYPE="passthrough"
|
|
FILES_IDP_PARAMS="{\\\"type\\\":\\\"passthrough\\\",\\\"strategy\\\":\\\"direct\\\"}"
|
|
;;
|
|
*)
|
|
echo "FILES_AUTH must be 'htpasswd', 'passthrough' or 'none' (got '$FILES_AUTH')" >&2; exit 1 ;;
|
|
esac
|
|
export FILES_IDP_TYPE FILES_IDP_PARAMS
|
|
|
|
envsubst '${BASE_DOMAIN} ${FILES_IDP_TYPE} ${FILES_IDP_PARAMS} ${FILES_ADMIN_PASSWORD_HASH} ${FILES_SECRET_KEY} ${FILES_LOCAL_SECRET}' \
|
|
< filestash/config.json.tmpl > filestash/config.json
|
|
|
|
# The hashes contain '$' and the params fields are nested JSON — one bad escape
|
|
# yields a config filestash silently ignores. Fail loudly instead.
|
|
python3 -c 'import json,sys; json.load(open("filestash/config.json"))' \
|
|
|| { echo "filestash/config.json is not valid JSON — refusing to ship it" >&2; exit 1; }
|
|
echo "rendered filestash/config.json (auth=$FILES_AUTH)"
|
|
|
|
# Server mods are no longer rendered here — they come from the distribution repo
|
|
# via tooling/sync-server-mods.sh (run separately in the deploy flow). See
|
|
# plan/04-mods.md.
|