#!/usr/bin/env bash
# httpd-example.sh - a full worked analysis of a real daemon: Apache httpd.
#
#   examples/httpd-example.sh [-o OUTDIR] [-p PORT]
#
# httpd is the useful example because it does everything that makes a daemon
# hard to analyse, and each one has a matching answer in this kit:
#
#   * it loads most of its code with dlopen()  -> ldd tells you almost nothing
#   * it forks a pool of workers               -> a trace without -f sees nothing
#   * it re-reads config and rotates logs      -> file access continues for ever
#   * it execs CGI programs                    -> child processes with their own access
#   * systemd gives it PrivateTmp and SELinux  -> paths differ from what you expect
#
# It starts its OWN httpd instance on 127.0.0.1:PORT with its own ServerRoot, so
# a system httpd (or your perf-test instance) keeps running untouched. Nothing
# outside OUTDIR is modified, and the instance is stopped on exit.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$HERE")"
TOOLS="$ROOT/tools"
PORT=8008
OUTDIR=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    -o) OUTDIR="$2"; shift 2 ;;
    -p) PORT="$2"; shift 2 ;;
    -h|--help) sed -n '2,20p' "$0"; exit 0 ;;
    *) echo "unknown option: $1" >&2; exit 1 ;;
  esac
done

HTTPD="$(command -v httpd || echo /usr/sbin/httpd)"
[[ -x "$HTTPD" ]] || { echo "httpd is not installed - dnf install httpd" >&2; exit 1; }
MODDIR="/usr/lib64/httpd/modules"
[[ -d "$MODDIR" ]] || { echo "no module directory at $MODDIR" >&2; exit 1; }
command -v curl >/dev/null || { echo "curl is needed to generate requests" >&2; exit 1; }

# httpd refuses to serve pages as root, so the workers need an unprivileged
# account. Prefer the one the httpd package already creates.
if [[ $EUID -eq 0 ]]; then
  for u in apache nobody daemon; do
    if id "$u" >/dev/null 2>&1; then RUNUSER="$u"; break; fi
  done
  : "${RUNUSER:?no unprivileged account (apache/nobody/daemon) to run workers as}"
  RUNGROUP="$(id -gn "$RUNUSER")"
else
  RUNUSER="$(id -un)"; RUNGROUP="$(id -gn)"
fi

OUTDIR="${OUTDIR:-$ROOT/out/httpd-$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$OUTDIR"
# absolute from here on: httpd needs absolute paths in its config, and the
# ancestor walk below would never terminate on a relative one
OUTDIR="$(readlink -f "$OUTDIR")"
SRV="$OUTDIR/server"

# The workers run as $RUNUSER, and httpd walks every parent directory of
# DocumentRoot looking for .htaccess. If any ancestor is not traversable by that
# user, every request 403s and the interesting part of the example never runs.
# Check up front rather than leaving you to read it out of the error log.
traversable() {
  local d prev
  d="$(readlink -f "$1")"
  [[ $EUID -eq 0 ]] || return 0
  while [[ -n "$d" && "$d" != "/" ]]; do
    runuser -u "$RUNUSER" -- test -x "$d" 2>/dev/null || return 1
    prev="$d"
    d="$(dirname "$d")"
    [[ "$d" == "$prev" ]] && break   # dirname stops changing: stop walking
  done
  return 0
}
if ! traversable "$OUTDIR"; then
  ALT="/var/tmp/elf-httpd-example.$$"
  echo "== note     : $RUNUSER cannot traverse $OUTDIR, so the server root goes to"
  echo "==            $ALT instead (reports still land in OUTDIR)."
  SRV="$ALT"
fi
mkdir -p "$SRV"/{conf,logs,htdocs,cgi-bin,run}

echo "== httpd    : $HTTPD ($("$HTTPD" -v | head -1 | sed 's/Server version: //'))"
echo "== instance : 127.0.0.1:$PORT under $SRV (workers as $RUNUSER)"
echo "== outdir   : $OUTDIR"

# ---------------------------------------------------------------- the server --
cat > "$SRV/conf/httpd.conf" <<CONF
# Minimal private instance, only for analysis. Not a hardening example.
ServerName  127.0.0.1
ServerRoot  "$SRV"
Listen      127.0.0.1:$PORT
PidFile     "$SRV/run/httpd.pid"

LoadModule mpm_event_module   $MODDIR/mod_mpm_event.so
LoadModule authz_core_module  $MODDIR/mod_authz_core.so
LoadModule unixd_module       $MODDIR/mod_unixd.so
LoadModule log_config_module  $MODDIR/mod_log_config.so
LoadModule mime_module        $MODDIR/mod_mime.so
LoadModule dir_module         $MODDIR/mod_dir.so
LoadModule alias_module       $MODDIR/mod_alias.so
LoadModule cgi_module         $MODDIR/mod_cgi.so

User  $RUNUSER
Group $RUNGROUP

TypesConfig /etc/mime.types
DocumentRoot "$SRV/htdocs"
DirectoryIndex index.html

ErrorLog  "$SRV/logs/error_log"
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog "$SRV/logs/access_log" common

<Directory "$SRV/htdocs">
    Require all granted
</Directory>

ScriptAlias /cgi-bin/ "$SRV/cgi-bin/"
<Directory "$SRV/cgi-bin">
    Options +ExecCGI
    Require all granted
</Directory>
CONF

echo "hello from the analysed httpd instance" > "$SRV/htdocs/index.html"
head -c 2048 /dev/urandom | base64 > "$SRV/htdocs/payload.txt"
# a CGI script: this is what makes httpd exec() a child process we can catch
cat > "$SRV/cgi-bin/whoami.cgi" <<'CGI'
#!/bin/sh
echo "Content-Type: text/plain"
echo
echo "cgi pid=$$ user=$(id -un)"
cat /etc/hostname
CGI
chmod +x "$SRV/cgi-bin/whoami.cgi"
# workers drop to $RUNUSER, so the content has to be readable by them
chmod -R a+rX "$SRV/htdocs" "$SRV/cgi-bin"
chmod a+x "$SRV" "$SRV/htdocs" "$SRV/cgi-bin"

"$HTTPD" -t -d "$SRV" -f "$SRV/conf/httpd.conf" 2>&1 | sed 's/^/   configtest: /'

cleanup() {
  [[ -f "$SRV/run/httpd.pid" ]] && kill "$(cat "$SRV/run/httpd.pid")" 2>/dev/null
  pkill -f "$SRV/conf/httpd.conf" 2>/dev/null
  sleep 0.3
}
finish() {
  cleanup
  # if the server root had to be relocated, keep the evidence but not the litter
  if [[ "$SRV" == /var/tmp/elf-httpd-example.* ]]; then
    cp -r "$SRV" "$OUTDIR/server" 2>/dev/null
    rm -rf "$SRV"
  fi
}
trap finish EXIT

load() {   # generate requests against the instance while a trace is running
  local n="${1:-6}"
  for _ in $(seq 1 "$n"); do
    curl -s -o /dev/null "http://127.0.0.1:$PORT/" || true
    curl -s -o /dev/null "http://127.0.0.1:$PORT/payload.txt" || true
    curl -s -o /dev/null "http://127.0.0.1:$PORT/cgi-bin/whoami.cgi" || true
    curl -s -o /dev/null "http://127.0.0.1:$PORT/does-not-exist" || true
    sleep 0.4
  done
}

# ------------------------------------------------------------ 1. static only --
echo
echo "=== 1/5 static analysis of $HTTPD (does not run it)"
"$TOOLS/elf-static.sh" "$HTTPD" > "$OUTDIR/1-STATIC.md" 2>"$OUTDIR/1-static.err" \
  || echo "   (incomplete - see 1-static.err)"

# -------------------------------------------- 2. single-process startup+serve --
# -X keeps httpd in one process: the cleanest possible view of config parsing,
# module loading and request handling, with no fork noise.
echo "=== 2/5 tracing startup + requests in single-process mode (httpd -X)"
( sleep 2; load 3 ) &
LOADPID=$!
"$TOOLS/elf-trace.sh" -T 8 -o "$OUTDIR/2-singleproc" -- \
    "$HTTPD" -X -d "$SRV" -f "$SRV/conf/httpd.conf" >"$OUTDIR/2-trace.out" 2>&1
wait $LOADPID 2>/dev/null
cleanup

# --------------------------------------------------- 3. the real forking MPM --
# Normal operation: a parent that forks workers. This is the trace that answers
# "which processes does it start".
echo "=== 3/5 tracing the forking MPM (parent + workers)"
( sleep 3; load 3 ) &
LOADPID=$!
"$TOOLS/elf-trace.sh" -T 9 -o "$OUTDIR/3-forking" -- \
    "$HTTPD" -DFOREGROUND -d "$SRV" -f "$SRV/conf/httpd.conf" >"$OUTDIR/3-trace.out" 2>&1
wait $LOADPID 2>/dev/null
cleanup

# ------------------------------------------------- 4. observe it as a daemon --
echo "=== 4/5 snapshotting a running instance (no ptrace)"
"$HTTPD" -k start -d "$SRV" -f "$SRV/conf/httpd.conf" 2>/dev/null
sleep 1
HPID="$(cat "$SRV/run/httpd.pid" 2>/dev/null)"
if [[ -n "$HPID" && -d "/proc/$HPID" ]]; then
  ( load 2 ) &
  "$TOOLS/elf-snapshot.sh" -p "$HPID" -n 3 -i 1 -o "$OUTDIR/4-snapshot" >"$OUTDIR/4-snap.out" 2>&1
  wait 2>/dev/null
else
  echo "   could not start the daemon instance; skipping the snapshot"
fi
cleanup

# ------------------------------------------------------------ 5. the findings --
echo "=== 5/5 writing the findings"
"$HERE/httpd-findings.sh" "$OUTDIR" "$HTTPD" "$SRV" "$PORT" > "$OUTDIR/HTTPD-FINDINGS.md"

echo
echo "read this : $OUTDIR/HTTPD-FINDINGS.md"
echo "evidence  : $OUTDIR/{2-singleproc,3-forking}/strace.log"
ls -1 "$OUTDIR"
