Files
cloud-apache-container/scripts/entrypoint-lsphp.sh
T

236 lines
13 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
## entrypoint-lsphp.sh — PID 1 for cac-lsphp:phpNN.
##
## The per-site PHP backend for the SHARED OpenLiteSpeed tier. Runs lsphp in
## DETACHED LSAPI mode (`lsphp -b <addr:port>`) and nothing else — no
## webserver. The shared-ols container connects to this over the docker
## network (extProcessor type lsapi, address <this-container>:9000) exactly
## like the shared httpd connects to a cac-fpm container's php-fpm on :9000.
##
## Structurally identical to cac-fpm/cac-litespeed: same `uid`/`user` contract,
## the customer docroot mounted at /home/$user (so PHP sees /home/$user/public_html
## EXACTLY like the standalone tiers — true 1:1 drop-in for WordPress ABSPATH,
## config paths, and DB-stored absolute paths). The only difference is OLS lives
## in a separate container, so this PID 1 is lsphp itself.
##
## THE SYMLINK (see feedback_ols_lsapi_no_script_filename_remap): OLS has no
## ProxyFCGISetEnvIf-style remap — it hands lsphp exactly its vhost docRoot path.
## The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
## so its docRoot (and the SCRIPT_FILENAME it sends us) is
## /mnt/users/<user>/<domain>/public_html. We create a symlink
## /mnt/users/<user>/<domain> -> /home/$user so that path resolves to the real
## /home/$user/public_html files. PHP canonicalises the symlink, so
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
## 2026-06-10) — the customer never sees the /mnt/users path.
##
## THE $_SERVER STRINGS: the symlink makes paths RESOLVE, but the raw strings OLS
## put in $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] still read /mnt/users.
## The cac_path_parity extension (baked into the image, configured per-site
## below) rewrites those two at request start, so a site moved from cac-fpm to
## cac-lsphp sees byte-identical values. It replaced an auto_prepend_file
## normaliser that any site's own .user.ini silently displaced — see
## ext/cac-path-parity/cac_path_parity.c.
set -euo pipefail
: "${PHPVER:=83}"
: "${environment:=PROD}"
export CONTAINER_ROLE="lsphp_only"
export PHPVER environment
## ---- env validation (same contract as entrypoint-fpm / entrypoint-litespeed) ----
if [ -z "${uid:-}" ] || [ -z "${user:-}" ]; then
echo "FATAL: 'uid' and 'user' env vars are required (panel sets these from WHP_UID/WHP_USER)." >&2
exit 1
fi
: "${domain:=localhost}"
export user domain
LSPHP_BIN="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
if [ ! -x "$LSPHP_BIN" ]; then
echo "FATAL: lsphp binary not found at $LSPHP_BIN (PHPVER=$PHPVER)." >&2
exit 1
fi
## ---- user + directories (identical to entrypoint-litespeed.sh: docroot at
## /home/$user, the customer's bind-mounted domain dir) ----
if ! id -u "$user" >/dev/null 2>&1; then
useradd -u "$uid" -m -s /bin/bash "$user"
fi
mkdir -p "/home/$user/public_html" "/home/$user/logs/php-fpm"
## ---- compatibility symlink for the OLS-sent path ----
## OLS sends SCRIPT_FILENAME under /mnt/users/<user>/<safe-domain>/public_html
## (the shared-ols container's view). Point that at our real /home/$user mount so
## the path resolves. <safe-domain> matches the on-disk convention: wildcard
## `*.foo.com` is stored as `wildcard.foo.com`.
SAFE_DOMAIN="$domain"
case "$domain" in
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
esac
## Both of these get interpolated into generated php.ini fragments below. They
## are panel-validated and both already feed `ln -sfn` and the shared-ols vhost
## config, so a hostile value is not reachable today — this is the belt to that
## brace. A newline in $domain is an INI-DIRECTIVE INJECTION into the generated
## fragment (measured against the pre-fix script: domain=$'evil.com\nprecision =
## 7\n; ' put that directive in 99-cac-path-parity.ini and lsphp reported
## `precision => 7`); `$(...)` yields an ini parse error and `"` an empty value,
## and BOTH of those leave the
## path-parity extension INERT — the exact silent parity loss this whole change
## exists to eliminate. Quoting the emitted values (done below) neutralises
## newlines and quotes; it does NOT neutralise php.ini's own `${VAR}`
## interpolation, which is why the character class is checked as well.
INI_TOKENS_OK=yes
case "$user" in ''|*[!A-Za-z0-9._-]*) INI_TOKENS_OK=no ;; esac
case "$SAFE_DOMAIN" in ''|*[!A-Za-z0-9._-]*) INI_TOKENS_OK=no ;; esac
if [ "$INI_TOKENS_OK" != yes ]; then
echo "WARNING: entrypoint-lsphp: user/domain contain characters outside [A-Za-z0-9._-] — refusing to write the \$_SERVER path-parity mapping (the extension stays inert; requests are unaffected). user=$(printf '%q' "$user") domain=$(printf '%q' "$domain")" >&2
fi
## The exact path prefix the shared-ols container serves this site from — the
## string OLS puts in SCRIPT_FILENAME/DOCUMENT_ROOT. Used twice: for the symlink
## that makes it RESOLVE, and for the cac_path_parity mapping that makes it READ
## like cac-fpm. Deriving both from one variable keeps them in lockstep.
OLS_SITE_PATH="/mnt/users/$user/$SAFE_DOMAIN"
mkdir -p "/mnt/users/$user"
ln -sfn "/home/$user" "$OLS_SITE_PATH"
## ---- detached-lsphp pool sizing ----
# shellcheck source=/dev/null
source /scripts/detect-memory-lsphp.sh
## LSAPI tuning (spec §5.1). PHP_LSAPI_CHILDREN MUST equal the shared-ols vhost
## maxConns — the WHP panel writes both from the single fpm_max_children value,
## so they can't drift. LSAPI_MAX_IDLE is THE RAM win: idle children exit, so an
## idle site's footprint collapses toward baseline (ondemand-like).
export PHP_LSAPI_CHILDREN="${PHP_LSAPI_CHILDREN:-$LSAPI_CHILDREN}"
export PHP_LSAPI_MAX_REQUESTS="${PHP_LSAPI_MAX_REQUESTS:-500}"
export LSAPI_MAX_IDLE="${LSAPI_MAX_IDLE:-30}"
export LSAPI_EXTRA_CHILDREN="${LSAPI_EXTRA_CHILDREN:-5}"
export LSAPI_AVOID_FORK="${LSAPI_AVOID_FORK:-0}"
LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
## ---- .user.ini support ----
## php-lsapi compiles .user.ini support in but leaves it DISABLED by default:
## sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
## sets it in PHP_MINIT_FUNCTION(litespeed) when the PROCESS ENV contains
## LSPHP_ENABLE_USER_INI=on. Without it, lsphp never enters the user-ini chain
## at all — and does so SILENTLY, because `user_ini.filename` / `user_ini.cache_ttl`
## still report their core defaults in phpinfo(). Every other WHP PHP tier
## (cac, cac-fpm, cac-litespeed) honors .user.ini, so leaving it off here made
## the shared-ols tier quietly inconsistent: customer memory_limit /
## max_input_vars overrides were ignored, and — the reason this was found —
## Wordfence's `auto_prepend_file` WAF never loaded on ANY shared-ols site.
##
## Exported here rather than relying solely on the Dockerfile ENV because the
## runuser fallback below resets the environment; an export survives all three
## exec paths. Still overridable per-container (set LSPHP_ENABLE_USER_INI=off in
## the site's env) as an escape hatch for a site whose legacy cPanel-generated
## .user.ini has not been remediated yet.
export LSPHP_ENABLE_USER_INI="${LSPHP_ENABLE_USER_INI:-on}"
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND} | user_ini=${LSPHP_ENABLE_USER_INI}"
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
## "where's my PHP error log?" is answered identically across all site types.
## Capture lsphp's own info once and read both answers out of it. Probe with
## `-i` ONLY: lsphp is the LSAPI SAPI, not the CLI — it accepts just
## -[b|c|n|h|i|q|s|v|?] and answers `-m`/`-r` by printing usage and exiting 0, so
## a `lsphp -m | grep` test never matches and never errors either.
PATH_PARITY_MODE="none"
LSPHP_INFO=$("$LSPHP_BIN" -i 2>/dev/null || true)
SCAN_DIR=$(printf '%s\n' "$LSPHP_INFO" | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
if [ -n "$SCAN_DIR" ]; then
mkdir -p "$SCAN_DIR"
## Values emitted double-quoted via printf rather than interpolated into an
## unquoted heredoc — see the INI_TOKENS_OK note above for what that prevents.
{
echo '; rendered at container start by entrypoint-lsphp.sh'
printf 'error_log = "%s"\n' "/home/$user/logs/php-fpm/error.log"
echo 'log_errors = On'
} > "$SCAN_DIR/99-user-error-log.ini"
## ---- $_SERVER path parity with cac-fpm ----
## Point the cac_path_parity extension at THIS site's mapping. Same two
## values the compatibility symlink above is built from, so the rewrite and
## the symlink can never disagree.
##
## Both settings are PHP_INI_SYSTEM: a customer's .user.ini (PHP_INI_PERDIR /
## PHP_INI_USER only) cannot redirect or disable them, and the extension
## occupies no userland hook — so the customer's own auto_prepend_file (the
## Wordfence WAF on several live sites) keeps working untouched. That
## combination is why this is an extension: the previous auto_prepend_file
## normaliser was itself PHP_INI_PERDIR and any site with its own prepend
## silently displaced it, while making OUR prepend win would have disabled
## THEIRS. See ext/cac-path-parity/cac_path_parity.c.
if [ "$INI_TOKENS_OK" != yes ]; then
## Already warned above. Write NOTHING: neither the mapping (we will not
## generate ini from an unvetted string) nor the auto_prepend fallback (which
## would not be correct for such a site either). The extension stays inert,
## the request path is unaffected.
rm -f "$SCAN_DIR/99-cac-path-parity.ini" "$SCAN_DIR/99-cac-lsphp-normalize.ini"
PATH_PARITY_MODE="none (user/domain rejected)"
elif printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'; then
{
echo '; rendered at container start by entrypoint-lsphp.sh'
printf 'cac_path_parity.from = "%s"\n' "$OLS_SITE_PATH"
printf 'cac_path_parity.to = "%s"\n' "/home/$user"
} > "$SCAN_DIR/99-cac-path-parity.ini"
## Drop the pre-extension fallback if an older image left one here — the
## container filesystem survives a "docker restart", so an in-place upgrade
## must not keep a stale auto_prepend pointing at the old normaliser.
rm -f "$SCAN_DIR/99-cac-lsphp-normalize.ini"
PATH_PARITY_MODE="extension"
else
## Degraded fallback for an image built before the extension existed (or one
## where it failed to load). Restores the old, .user.ini-defeatable
## behaviour rather than losing normalisation entirely — but say so loudly,
## because in this mode parity is NOT guaranteed.
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
; rendered at container start by entrypoint-lsphp.sh (DEGRADED FALLBACK)
auto_prepend_file = /scripts/cac-lsphp-normalize.php
EOF
PATH_PARITY_MODE="auto_prepend (DEGRADED)"
echo "WARNING: entrypoint-lsphp: cac_path_parity extension not loadable in this image — falling back to the auto_prepend normaliser, which a site's own .user.ini auto_prepend_file will silently displace. Rebuild/repull cac-lsphp:php${PHPVER}." >&2
fi
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
## back to the baked lsphp-overrides.ini defaults when unset.
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
{
echo "; rendered at container start by entrypoint-lsphp.sh"
echo "; per-site override from WHP whp.sites.opcache_*_override"
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
} > "$SCAN_DIR/99-user-opcache.ini"
fi
else
## No scan dir means none of the per-site ini drop-ins land — including the
## path-parity mapping. Previously this failed silently; it must not, because
## the tier's cac-fpm parity guarantee is one of the things lost.
echo "WARNING: entrypoint-lsphp: lsphp reports no additional-ini scan dir — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied." >&2
fi
echo "entrypoint-lsphp: \$_SERVER path parity = ${PATH_PARITY_MODE} (${OLS_SITE_PATH} -> /home/${user})"
## ---- ownership ----
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
## as $user) can read code and write logs. Customer content is already
## customer-owned from the host side, so we don't recurse the whole (potentially
## large) tree on every boot.
touch "/home/$user/logs/php-fpm/error.log"
chown "$uid:$uid" "/home/$user" "/home/$user/public_html" "/home/$user/logs" "/home/$user/logs/php-fpm" "/home/$user/logs/php-fpm/error.log" 2>/dev/null || true
## ---- exec lsphp -b as the customer user (PID 1) ----
## Bind port is unprivileged (9000), so no root port-bind step is needed — start
## directly as $user. Prefer setpriv (util-linux, on the Ubuntu base); fall back
## to runuser. exec so lsphp becomes PID 1 and receives Docker's signals
## directly (clean stop/restart, matches the php-fpm container's lifecycle).
echo "entrypoint-lsphp: exec $LSPHP_BIN -b $LSPHP_BIND as $user (uid=$uid)"
if command -v setpriv >/dev/null 2>&1; then
exec setpriv --reuid "$uid" --regid "$uid" --init-groups "$LSPHP_BIN" -b "$LSPHP_BIND"
elif command -v runuser >/dev/null 2>&1; then
exec runuser -u "$user" -- "$LSPHP_BIN" -b "$LSPHP_BIND"
else
exec sudo -u "$user" -E "$LSPHP_BIN" -b "$LSPHP_BIND"
fi