Files
TRIMSP/scripts/deploy-web.sh
T
salman 5a8bd70957 Harden singleTrimSP.cgi: traversal fix, watchdog, pruner, archive lock, caps
- P0 path-traversal fix: run-name class is dot-free with \z anchors, so
  '.', '..', and %0A-trailing values can never escape TRIMSP_TMPROOT. The
  deployed path allowlist is derived from the configured root instead of the
  old hardcoded /tmp prefix.
- Replacing external  with an alarm() watchdog that kills the
  engine's whole process group (setpgrp + kill KILL -pid), matching
  muSRFlow; describe_result maps -2/-1/signal to 124/127/128+sig.
- prune_stale_jobs() ages out dirs (TrimSP.cfg marker only), orphaned .tgz,
  and leaked per-request logs; safe in shared /tmp (symlinks skipped,
  remove_tree safe mode, never touches non-trimsp dirs).
- Archive is serialized per run with flock (.trimsp-lock); per-point engine
  runs stay concurrent.
- Symlink/non-dir run dirs refused; mode 0700 so results are not
  world-readable. read/download also reject symlinked targets.
- Request caps: MAX_REQUEST_LINES 20000, MAX_REQUEST_BYTES 4 MiB -> 413.
- action=health deployment self-check; env overrides TRIMSP_BIN, TMPROOT,
  CMD_TIMEOUT, JOB_MAXAGE (sed anchor for the binary path kept intact).
- Wire contract preserved: ok/phase/message/exitCode/archiveUrl/log JSON,
  text read, gzip download, real HTTP statuses. No TrimSPweb.js change.
- New tests/cgi-tests.sh drives the real CGI offline (18 cases: traversal,
  %0A, escaping round-trip, success/fail/hang engine, 124 watchdog, archive+-
  download, pruner, symlink refusal, 413). deploy-web.sh now verifies the
  sed substitution embedded the selected engine path.
2026-09-26 16:32:38 +02:00

152 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright (C) 2026 Zaher Salman
# SPDX-License-Identifier: GPL-3.0-or-later
set -euo pipefail
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
web_target="${TRIMSP_WEB_TARGET:-/var/www/html/TRIMSP-NL}"
cgi_target="${TRIMSP_CGI_TARGET:-/var/www/cgi-bin/singleTrimSP.cgi}"
trimsp_binary="${TRIMSP_BIN_PATH:-/usr/local/bin/trimspNL}"
plotly_source="$project_root/node_modules/plotly.js-dist/plotly.js"
plotly_cdn="${TRIMSP_PLOTLY_CDN:-https://cdn.jsdelivr.net/npm/plotly.js-dist@1.58.5/plotly.js}"
# Staging files created by this script, removed on exit so a CDN download does
# not leak into /tmp after an early failure.
tmp_files=""
trap 'rm -f $tmp_files' EXIT
if [[ ! -x "$trimsp_binary" ]]; then
printf 'Installed TRIM.SP-NL binary is not executable: %s\n' "$trimsp_binary" >&2
printf 'Install it first or set TRIMSP_BIN_PATH.\n' >&2
exit 1
fi
if [[ ! -f "$plotly_source" ]]; then
printf 'Plotly browser bundle not found locally: %s\n' "$plotly_source" >&2
printf 'Downloading bundle from CDN instead ...\n' >&2
if ! command -v curl >/dev/null 2>&1; then
printf 'curl is required to download the Plotly bundle.\n' >&2
printf 'Install curl, run npm install, or set TRIMSP_PLOTLY_CDN.\n' >&2
exit 1
fi
plotly_tmp="$(mktemp)"
tmp_files="$tmp_files $plotly_tmp"
if ! curl -fsSL "$plotly_cdn" -o "$plotly_tmp"; then
printf 'Failed to download Plotly bundle from %s\n' "$plotly_cdn" >&2
printf 'Run npm install before deploying.\n' >&2
exit 1
fi
printf 'Downloaded Plotly bundle from %s\n' "$plotly_cdn"
plotly_source="$plotly_tmp"
fi
perl -c "$project_root/singleTrimSP.cgi" >/dev/null
# Install only files used by the browser application. Electron-specific code,
# package metadata, tests, and Fortran sources stay outside the document root.
install -d -m 0755 "$web_target" "$(dirname "$cgi_target")"
install -m 0644 \
"$project_root/TrimSP.html" \
"$project_root/quick-start.html" \
"$project_root/TrimSPlib.js" \
"$project_root/TrimSPweb.js" \
"$project_root/myplots.js" \
"$project_root/ZGUI.css" \
"$project_root/plotly.css" \
"$project_root/icon.png" \
"$web_target/"
install -m 0644 "$plotly_source" "$web_target/plotly.js"
ln -sfn TrimSP.html "$web_target/index.html"
# Brand the installed help page with the current release version, matching the
# About dialog in the Electron edition. The source keeps a __TRIMSP_VERSION__
# token that renders as "development build" when left unsubstituted; the
# installed copy is rewritten with the version from package.json. Idempotent:
# an already-branded file contains no token, so re-running matches nothing.
if command -v node >/dev/null 2>&1; then
version="$(node -p 'require(process.argv[1]).version' "$project_root/package.json")"
tmp_qs="$(mktemp)"
tmp_files="$tmp_files $tmp_qs"
sed "s#__TRIMSP_VERSION__#$version#g" "$web_target/quick-start.html" > "$tmp_qs"
install -m 0644 "$tmp_qs" "$web_target/quick-start.html"
printf 'Branded %s with TRIMSP-NL version %s\n' "$web_target/quick-start.html" "$version"
else
printf 'node not found; quick-start.html keeps the source version token\n'
fi
# Keep the repository CGI generic while embedding the selected installed
# binary path in the deployed copy.
cgi_stage="$(mktemp)"
tmp_files="$tmp_files $cgi_stage"
escaped_trimsp_binary="${trimsp_binary//\\/\\\\}"
escaped_trimsp_binary="${escaped_trimsp_binary//&/\\&}"
escaped_trimsp_binary="${escaped_trimsp_binary//|/\\|}"
escaped_trimsp_binary="${escaped_trimsp_binary//\"/\\\"}"
sed 's|^my \$trimsp_bin = .*|my $trimsp_bin = "'"$escaped_trimsp_binary"'";|' \
"$project_root/singleTrimSP.cgi" > "$cgi_stage"
perl -c "$cgi_stage" >/dev/null
install -m 0755 "$cgi_stage" "$cgi_target"
# The installed CGI must embed the selected engine path exactly; a future
# source layout change could silently break the sed substitution above.
if ! grep -Fq 'my $trimsp_bin = "'"$trimsp_binary"'";' "$cgi_target"; then
printf 'Deployed CGI does not embed the selected engine path %s\n' "$trimsp_binary" >&2
exit 1
fi
# The browser assets and the CGI must be readable (and, for the CGI,
# executable) by the web server. On the live server the web space is owned by
# the apache user; on a developer machine the files may instead be owned by
# the deploying user and served straight from a user-owned tree. Determine the
# owner to hand the files to, preferring an explicit override, then a real
# web-server account, and keeping the current owner when it is already the
# deploying user (the common developer-machine case, where the deploying user
# simply owns the web space and chowning to a nonexistent apache user would
# fail).
web_owner="${TRIMSP_WEB_OWNER:-}"
web_group="${TRIMSP_WEB_GROUP:-$web_owner}"
if [ -z "$web_owner" ]; then
# Detect from the target filesystem: if the deployed tree is already owned
# by the invoking user, leave ownership untouched.
if [ "$(stat -c %u "$web_target")" = "$(id -u)" ]; then
web_owner=""
web_group=""
else
# Prefer a real web-server account when one exists.
for cand in apache www-data httpd; do
if id "$cand" >/dev/null 2>&1; then
web_owner="$cand"
web_group="$cand"
break
fi
done
# With no matching account, fall back to the invoking user (the trees
# were written by this deploy, so they are already owned by them).
[ -z "$web_owner" ] && web_owner="$(id -un)" && web_group="$(id -gn)"
fi
fi
if [ -n "$web_owner" ]; then
chown -R "$web_owner:$web_group" "$web_target/"
chown "$web_owner:$web_group" "$cgi_target"
chmod -R u+rX "$web_target/" 2>/dev/null || true
fi
if command -v restorecon >/dev/null 2>&1; then
restorecon -RF "$web_target" "$cgi_target" || true
fi
printf 'Installed TRIMSP-NL web assets in %s\n' "$web_target"
printf 'Installed TRIMSP-NL CGI runner at %s\n' "$cgi_target"
printf 'Using installed TRIM.SP-NL engine at %s\n' "$trimsp_binary"
if [ -n "$web_owner" ]; then
printf 'Ownership set to %s:%s\n' "$web_owner" "$web_group"
else
printf 'Ownership left unchanged (deployed by the owning user)\n'
fi
printf '\nThe web server must provide:\n'
printf ' /cgi-bin/singleTrimSP.cgi -> %s\n' "$cgi_target"
printf 'For SELinux deployments, verify CGI execution and write access with:\n'
printf ' ausearch -m avc -ts recent\n'