#!/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 '')}" : "${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 '')}" # 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 ''" >&2 exit 1 ;; *) echo "FILES_PASSWORD_HASH is not a recognised hash (want \$6\$… from: openssl passwd -6 '')" >&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.