#!/usr/bin/env bash ## render-shared-ols-config.sh — assemble httpd_config.conf for the shared-ols ## tier from the per-site files the WHP panel drops into $SITES_ROOT. ## ## WHY THIS EXISTS: OpenLiteSpeed has NO top-level `include` directive (unlike ## Apache's IncludeOptional that shared-httpd relies on). So we cannot just drop ## per-vhost files in a dir and have OLS pick them up — the listener `map` lines ## and the vhost stanzas must live IN httpd_config.conf. This script is the ## "include" OLS lacks: it concatenates the panel's per-site pieces into one ## valid httpd_config.conf, then the caller issues `lswsctrl restart`. ## (Empirically established 2026-06-10 — see the OLS-tier PoC.) ## ## Per-site contract — the panel writes, for each site, a directory: ## $SITES_ROOT//vhconf.conf (rendered by the WHP panel from its own ## web-files/configs/shared-ols-vhconf-template.tpl ## — the single source of truth for vhost detail) ## $SITES_ROOT//site.meta (VHNAME=, VHROOT=, DOMAINS=a.com,www.a.com) ## This script turns each into a `virtualhost {configFile}` stanza + a listener ## `map` line. A site dir missing either file is skipped (logged). ## ## Idempotent: always rebuilds from the stock config, so re-runs never compound. set -euo pipefail LSWS_CONF=/usr/local/lsws/conf TPL_DIR=${TPL_DIR:-/etc/shared-ols-templates} SITES_ROOT=${SITES_ROOT:-$LSWS_CONF/shared-sites} LSCACHE_ROOT=${LSCACHE_ROOT:-/var/lscache} CERT_FILE=${CERT_FILE:-$LSWS_CONF/cert/shared-ols.crt} KEY_FILE=${KEY_FILE:-$LSWS_CONF/cert/shared-ols.key} export LSCACHE_ROOT OUT="$LSWS_CONF/httpd_config.conf" TMP="$LSWS_CONF/.httpd_config.conf.tmp.$$" STOCK="/usr/local/lsws/.conf/httpd_config.conf" mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" ## --- SERIALIZE concurrent renders + write ATOMICALLY --- ## The panel can fire two renders at once (parallel provisioning), and the ## in-container .htaccess watcher issues `lswsctrl restart` independently. If OLS ## (re)reads httpd_config.conf while it's half-written, it fails to parse and the ## whole tier 503s. So: (1) flock so only one render runs at a time; (2) build ## into $TMP and atomically `mv` into place at the end, so any concurrent OLS ## restart always sees a COMPLETE config (the old one until the instant of mv). exec 9>"$LSWS_CONF/.render.lock" ## Bounded wait (-w): if a previous render is hung, fail after 30s rather than ## blocking the panel's `docker exec` call (and thus the site-save request) ## indefinitely. The caller re-tries on the next change. flock -w 30 9 || { echo "render-shared-ols: could not acquire render lock within 30s" >&2; exit 1; } trap 'rm -f "$TMP"' EXIT ## Sweep any stale temp configs left by a prior SIGKILL (trap EXIT doesn't run on ## SIGKILL); each render uses a unique $$ suffix so this never races a live render. rm -f "$LSWS_CONF"/.httpd_config.conf.tmp.* 2>/dev/null || true ## From here on, build into $TMP (not $OUT). ## --- 1. start from a pristine stock config (idempotent) --- if [ ! -f "$STOCK" ]; then ## Some image builds keep the only copy at conf/; snapshot it once so future ## renders have a clean base to strip. mkdir -p "$(dirname "$STOCK")" cp "$OUT" "$STOCK" fi ## --- 2. strip stock blocks that conflict or would run PHP LOCALLY --- ## extProcessor lsphp (autoStart 1, uds) + the server scriptHandler are removed ## so this server NEVER executes PHP itself — all PHP goes to remote sidecars. ## listener HTTP/HTTPS + vhTemplate docker are removed (we add our own). awk ' /^listener HTTP \{/ { skip=1; next } /^listener HTTPS \{/ { skip=1; next } /^vhTemplate docker ?\{/ { skip=1; next } /^extProcessor lsphp ?\{/{ skip=1; next } /^scriptHandler ?\{/ { skip=1; next } skip && /^\}/ { skip=0; next } !skip { print } ' "$STOCK" > "$TMP" ## --- 3. append our server-level base (real-IP, cache module, no local PHP) --- { echo "" envsubst '${LSCACHE_ROOT}' < "$TPL_DIR/httpd_config_base.tpl" } >> "$TMP" ## --- 4. emit per-site vhost stanzas + collect listener map lines --- maps="" site_count=0 for meta in "$SITES_ROOT"/*/site.meta; do [ -e "$meta" ] || continue sdir=$(dirname "$meta") ## PARSE site.meta with sed — do NOT `source` it. The panel writes these values ## (derived from DB domains), so they should be safe, but sourcing paneldata as ## shell would execute any metacharacters as root in this container if a value ## ever slipped validation. sed extraction treats them as plain data. VHNAME=$(sed -n 's/^VHNAME=//p' "$meta" | head -1) VHROOT=$(sed -n 's/^VHROOT=//p' "$meta" | head -1) DOMAINS=$(sed -n 's/^DOMAINS=//p' "$meta" | head -1) if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2 continue fi { echo "" echo "virtualhost ${VHNAME} {" echo " vhRoot ${VHROOT}" echo " configFile ${sdir}/vhconf.conf" echo " allowSymbolLink 1" echo " enableScript 1" echo " restrained 1" echo "}" } >> "$TMP" maps="${maps} map ${VHNAME} ${DOMAINS}"$'\n' site_count=$((site_count + 1)) done ## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is ## valid with zero customer sites and HAProxy health checks (which hit by IP / ## unknown Host) get a 200. Exact-domain maps above win over this '*'. --- { echo "" echo "virtualhost _health {" echo " vhRoot /usr/local/lsws/shared-ols-health" echo " configFile /usr/local/lsws/shared-ols-health/vhconf.conf" echo " allowSymbolLink 1" echo " enableScript 0" echo "}" } >> "$TMP" maps="${maps} map _health *"$'\n' ## --- 6. listeners (HTTP :80 + HTTPS :443 self-signed) carrying ALL maps. ## HAProxy terminates real TLS and connects to this tier on :443 ssl verify ## none (same as shared-httpd), so :443 needs a cert — self-signed is fine. --- { echo "" echo "listener shared_http {" echo " address *:80" echo " secure 0" printf '%s' "$maps" echo "}" echo "" echo "listener shared_https {" echo " address *:443" echo " secure 1" echo " keyFile ${KEY_FILE}" echo " certFile ${CERT_FILE}" printf '%s' "$maps" echo "}" } >> "$TMP" ## --- 7. publish atomically. Validate the temp parses as non-empty, then mv into ## place (rename is atomic on the same filesystem) so a concurrent OLS restart ## never sees a half-written config. chown only the file we wrote — NOT a ## recursive chown of the whole conf tree (that was O(N-sites) on every single ## change; the per-site files are world-readable and owned correctly already). --- if [ ! -s "$TMP" ]; then echo "render-shared-ols: refusing to publish empty config" >&2 exit 1 fi chown lsadm:nogroup "$TMP" 2>/dev/null || true mv -f "$TMP" "$OUT" echo "render-shared-ols: wrote $OUT ($site_count customer vhost(s) + health)"