#!/usr/bin/env bash
# Purpose: from a SECOND computer on the home network, test what the chat service exposes:
#          the name resolves to the server's address, HTTPS verifies against the Caddy root
#          certificate, port 80 only redirects, the front-end's API refuses requests without
#          a login, and the ports the engine and front-end use inside the server are closed
#          to the network. Prints PASS or FAIL per check and exits non-zero on any FAIL.
# Platform: all (run it on a Linux, macOS or WSL2 machine that is NOT the server; from the
#           server itself the closed-port checks prove nothing)
# Minimum memory: 8 GB (the course's floor for this lab; the probe itself needs almost none)
# Assumes: curl on PATH; the root certificate exported in task 6 copied to this machine;
#          CHAT_HOST resolves on this machine the same way it does for the phone.
#
# Usage: bash probe-lan-exposure.sh --host NAME --expect-ip ADDRESS --cacert FILE
#                                   [--https-port N] [--http-port N] [--labbook FILE]
#   e.g. bash probe-lan-exposure.sh --host chat.home.arpa --expect-ip "$LAN_IP" \
#          --cacert ~/home-chat-root.crt --labbook labbook.md

set -euo pipefail

HOST=""
EXPECT_IP=""
CACERT=""
HTTPS_PORT=443
HTTP_PORT=80
LABBOOK=""
# Ports that must not answer on the server's home-network address: Ollama, Open WebUI and
# llama-server (8080), the Open WebUI quick start's 3000, and Caddy's admin API (2019).
CLOSED_PORTS=(11434 8080 3000 2019)

usage() { sed -n '2,17p' "$0"; }

while [ $# -gt 0 ]; do
  case "$1" in
    --host)       HOST="${2:?--host needs a name}"; shift 2 ;;
    --expect-ip)  EXPECT_IP="${2:?--expect-ip needs an address}"; shift 2 ;;
    --cacert)     CACERT="${2:?--cacert needs a file}"; shift 2 ;;
    --https-port) HTTPS_PORT="${2:?--https-port needs a number}"; shift 2 ;;
    --http-port)  HTTP_PORT="${2:?--http-port needs a number}"; shift 2 ;;
    --labbook)    LABBOOK="${2:?--labbook needs a file}"; shift 2 ;;
    -h|--help)    usage; exit 0 ;;
    *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
  esac
done

[ -n "$HOST" ] && [ -n "$EXPECT_IP" ] && [ -n "$CACERT" ] || { usage >&2; exit 2; }
command -v curl >/dev/null 2>&1 || { echo "curl is not on PATH." >&2; exit 1; }
[ -f "$CACERT" ] || { echo "No such certificate file: ${CACERT}" >&2; exit 1; }

PASSES=0
FAILS=0
pass() { PASSES=$((PASSES + 1)); printf 'PASS  %-22s %s\n' "$1" "$2"; }
fail() { FAILS=$((FAILS + 1)); printf 'FAIL  %-22s %s\n' "$1" "$2"; }

# curl exit codes used below: 0 success, 6 could not resolve host, 7 failed to connect
# (refused), 28 timed out, 35 TLS handshake failed, 60 certificate not verified.

# 1. Name resolution and TLS verification in one request.
BODY_FILE="$(mktemp)"
trap 'rm -f "$BODY_FILE"' EXIT
set +e
OUT="$(curl --silent --show-error --max-time 10 --cacert "$CACERT" \
  --output "$BODY_FILE" --write-out '%{http_code} %{remote_ip}' \
  "https://${HOST}:${HTTPS_PORT}/health" 2>&1)"
RC=$?
set -e
if [ "$RC" -eq 0 ]; then
  read -r CODE IP <<< "$OUT"
  if [ "$IP" != "$EXPECT_IP" ]; then
    fail "name-resolves" "${HOST} reached ${IP}, expected ${EXPECT_IP}"
  else
    pass "name-resolves" "${HOST} -> ${IP}"
  fi
  if [ "$CODE" = "200" ] && grep -q '"status":true' "$BODY_FILE"; then
    pass "https-verified" "certificate chains to ${CACERT}; /health says $(cat "$BODY_FILE")"
  else
    fail "https-verified" "TLS verified but /health returned HTTP ${CODE}"
  fi
else
  case "$RC" in
    6)  fail "name-resolves" "curl exit 6: ${HOST} does not resolve on this machine (DNS, task 1)" ;;
    7)  fail "https-verified" "curl exit 7: nothing accepts connections on ${HOST}:${HTTPS_PORT}" ;;
    28) fail "https-verified" "curl exit 28: timed out reaching ${HOST}:${HTTPS_PORT}" ;;
    60) fail "https-verified" "curl exit 60: certificate does not chain to ${CACERT} (task 6)" ;;
    *)  fail "https-verified" "curl exit ${RC}: ${OUT//$'\n'/ }" ;;
  esac
fi

# 2. Port 80 must only redirect to HTTPS.
set +e
OUT="$(curl --silent --max-time 10 --output /dev/null \
  --write-out '%{http_code} %{redirect_url}' "http://${HOST}:${HTTP_PORT}/")"
RC=$?
set -e
read -r CODE LOCATION _ <<< "${OUT} -"
if [ "$RC" -eq 0 ] && [ "$CODE" = "308" ] && [ "${LOCATION#https://}" != "$LOCATION" ]; then
  pass "http-redirects" "HTTP ${CODE} -> ${LOCATION}"
else
  fail "http-redirects" "expected HTTP 308 to https://, got curl exit ${RC}, HTTP ${CODE}"
fi

# 3. The front-end's proxy to the engine must demand a login.
set +e
CODE="$(curl --silent --max-time 10 --cacert "$CACERT" --output /dev/null \
  --write-out '%{http_code}' "https://${HOST}:${HTTPS_PORT}/ollama/api/tags")"
RC=$?
set -e
if [ "$RC" -eq 0 ] && [ "$CODE" = "401" ]; then
  pass "engine-api-needs-login" "/ollama/api/tags without a session: HTTP 401"
else
  fail "engine-api-needs-login" "expected HTTP 401, got curl exit ${RC}, HTTP ${CODE}"
fi

# 4. Nothing else may answer on the server's home-network address.
for PORT in "${CLOSED_PORTS[@]}"; do
  set +e
  curl --silent --max-time 5 --output /dev/null "http://${EXPECT_IP}:${PORT}/"
  RC=$?
  set -e
  case "$RC" in
    7)  pass "closed-${PORT}" "connection refused" ;;
    28) pass "closed-${PORT}" "no answer within 5 s (filtered)" ;;
    *)  fail "closed-${PORT}" "curl exit ${RC}: something answered on ${EXPECT_IP}:${PORT}" ;;
  esac
done

echo "${PASSES} passed, ${FAILS} failed"

if [ -n "$LABBOOK" ]; then
  printf '{"lab":"part-07/lab-private-chat-service-on-your-lan","step":"probe","host":"%s","passed":%s,"failed":%s,"taken":"%s"}\n' \
    "$HOST" "$PASSES" "$FAILS" "$(date -u +%Y%m%dT%H%M%SZ)" >> "$LABBOOK"
fi

[ "$FAILS" -eq 0 ]
