#!/bin/sh
# script/gitea-setup: build the image, bring up deploy/gitea under a project
# name of its own, and check it:
#   - that a page load, a push and a clone sent through traefik pass through
#     rproxy, and that servewaf checks them;
#   - that gitea logs the visitor's address, not rproxy's;
#   - that a request from a container on the shared network skips the checks;
#   - that with servewaf stopped, gitea still answers through rproxy.
# It publishes no port of traefik's, since this host's ports 80 and 443 may be
# taken. It reaches traefik at its address on the shared network instead, so
# its own requests come from the network's gateway, which servewaf checks like
# any visitor. Two runs at once on one docker host cannot both start, since
# both would use the subnet compose.yaml names. The containers, the network,
# the volumes, the image and the files the script makes are removed however it
# ends; the gitea and traefik images stay, as the base images of a build do.
# It needs network access. script/check does not run this.
set -eu

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"

# Named after this run, so that runs in other clones on the same host
# never touch each other's.
NAME="$("$SCRIPT_DIR/projectname")-gitea-setup-$$"
IMAGE="$NAME-image"
NETWORK="${NAME}_shared"
VISITOR="$NAME-visitor"

# The site's host name and gitea's image, as deploy/gitea/compose.yaml names
# them. gitea's image also runs the container on the shared network.
SITE="git.example.org"
# gitea 1.27.3, 2026-08-29
GITEA_IMAGE="gitea/gitea@sha256:87a67ee09d3ae0d1df5fda5dcda3e2a1f9236a45b0a59025d6e00e46adc43bef"

# gitea's one user, who owns the repository.
PASSWORD="owner-password"

# The files the script makes, and traefik's and the gateway's addresses on the
# shared network once the setup runs.
WORK=""
traefik=""
gateway=""

cleanup() {
    # So that a signal cannot stop it partway through.
    trap '' HUP INT TERM
    docker rm --force "$VISITOR" >/dev/null 2>&1 || true
    if [ -n "$WORK" ]; then
        compose down --volumes --remove-orphans --timeout 5 >/dev/null 2>&1 || true
        rm -rf "$WORK"
    fi
    docker rmi --force "$IMAGE" >/dev/null 2>&1 || true
}

# compose <argument>...: docker compose on the setup, under this run's project
# name, with the image built here and no port of traefik's published.
compose() {
    SMALLWEBWAF_IMAGE="$IMAGE" docker compose --project-name "$NAME" \
        --file "$ROOT/deploy/gitea/compose.yaml" --file "$WORK/no-ports.yaml" "$@"
}

fail() {
    echo "gitea-setup: $*; the last lines of each container's output:" >&2
    compose logs --no-color --tail 30 >&2 || true
    exit 1
}

# wait_for <what fails> <command>...: run the command every second until
# it succeeds, for at most a minute.
wait_for() {
    failure="$1"
    shift
    tries=0
    until "$@"; do
        tries=$((tries + 1))
        [ "$tries" -lt 60 ] || fail "$failure"
        sleep 1
    done
}

# logged <service> <text>...: a line of the service's output holds every text,
# in any order.
logged() {
    lines="$(compose logs --no-color --no-log-prefix "$1" 2>&1)"
    shift
    for text in "$@"; do
        lines="$(printf '%s\n' "$lines" | grep -F -e "$text")" || return 1
    done
}

# send <path> <curl option>...: send a request for path to the site, at
# traefik's address, and print the status of the answer, 000 for none. traefik
# answers with a certificate it made itself, since the setup names no
# certificate resolver.
send() {
    path="$1"
    shift
    curl --silent --show-error --insecure --max-time 30 --output /dev/null \
        --write-out '%{http_code}' --resolve "$SITE:443:$traefik" "$@" \
        "https://$SITE$path" || true
}

# routed: traefik passes a request for the site's front page on to gitea.
routed() {
    [ "$(send / 2>/dev/null)" = 200 ]
}

# site_git <git argument>...: run git as the owner, without the host's own git
# settings, reaching the site at traefik's address. git gives up on a transfer
# slower than 1000 bytes a second for 30 seconds.
site_git() {
    GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 git \
        -c user.name=owner -c user.email=owner@example.invalid \
        -c http.sslVerify=false -c "http.curloptResolve=$SITE:443:$traefik" \
        -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 "$@"
}

# address_of <service>: print the service's address on the shared network.
address_of() {
    docker inspect --format "{{(index .NetworkSettings.Networks \"$NETWORK\").IPAddress}}" \
        "$(compose ps --quiet "$1")"
}

# passed_through <method> <path>: rproxy passed a request from the gateway for
# path to gitea on servewaf's allow, and servewaf checked it: its report line
# gives the client's counts and the Core Rule Set's score.
passed_through() {
    wait_for "rproxy logged no $1 $2 that servewaf allowed" logged rproxy \
        "\"method\":\"$1\"" "\"path\":\"$2\"" "\"client_ip\":\"$gateway\"" \
        '"disposition":"allow"'
    wait_for "servewaf logged no check of $1 $2" logged servewaf \
        '"servewaf_line":"report"' "\"method\":\"$1\"" "\"path\":\"$2\"" \
        "\"client_ip\":\"$gateway\"" '"action":"forward"' '"counts":' '"waf_score":'
}

main() {
    cd "$ROOT"
    trap cleanup EXIT
    trap 'exit 1' HUP INT TERM
    WORK="$(mktemp -d)"
    printf 'services:\n    traefik:\n        ports: !reset []\n' >"$WORK/no-ports.yaml"

    docker build --no-cache -t "$IMAGE" .

    # rproxy is healthy once gitea takes connections.
    compose up --detach --quiet-pull --wait --wait-timeout 120 ||
        fail "the setup did not start"
    traefik="$(address_of traefik)"
    gateway="$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' "$NETWORK")"
    wait_for "traefik did not route the site to rproxy" routed

    compose exec --user git gitea gitea admin user create --admin \
        --username owner --password "$PASSWORD" --email owner@example.invalid \
        --must-change-password=false >/dev/null || fail "creating gitea's user failed"
    status="$(send /api/v1/user/repos --user "owner:$PASSWORD" \
        --header "Content-Type: application/json" --data '{"name":"repo"}')"
    [ "$status" = 201 ] || fail "creating the repository was answered $status"

    status="$(send /owner/repo)"
    [ "$status" = 200 ] || fail "the repository's page was answered $status"
    passed_through GET /owner/repo
    echo "gitea-setup: a page load passes through rproxy, and servewaf checks it"

    wait_for "gitea did not log the visitor's address" logged gitea \
        "GET /owner/repo for $gateway:"
    echo "gitea-setup: gitea logs the visitor's address, not rproxy's"

    site_git init --quiet --initial-branch=main "$WORK/repo"
    echo "hello" >"$WORK/repo/README.md"
    site_git -C "$WORK/repo" add README.md
    site_git -C "$WORK/repo" commit --quiet --message "Add a README"
    site_git -C "$WORK/repo" push --quiet \
        "https://owner:$PASSWORD@$SITE/owner/repo.git" main ||
        fail "the push failed"
    passed_through POST /owner/repo.git/git-receive-pack
    echo "gitea-setup: a push passes through rproxy, and servewaf checks it"

    site_git clone --quiet "https://owner:$PASSWORD@$SITE/owner/repo.git" \
        "$WORK/clone" || fail "the clone failed"
    [ "$(cat "$WORK/clone/README.md")" = hello ] || fail "the clone lacks the README"
    passed_through POST /owner/repo.git/git-upload-pack
    echo "gitea-setup: a clone passes through rproxy, and servewaf checks it"

    # A container on the shared network, as sandcastle is, asks for the site
    # by its name, and so reaches traefik from its own address.
    status="$(docker run --rm --name "$VISITOR" --network "$NETWORK" \
        --entrypoint curl "$GITEA_IMAGE" --silent --insecure --max-time 30 \
        --output /dev/null --write-out '%{http_code}' "https://$SITE/api/v1/version" ||
        true)"
    [ "$status" = 200 ] || fail "the request from the shared network was answered $status"
    wait_for "servewaf logged no request from the shared network" logged servewaf \
        '"servewaf_line":"report"' '"path":"/api/v1/version"' '"action":"forward"'
    line="$(compose logs --no-color --no-log-prefix servewaf |
        grep -F '"servewaf_line":"report"' | grep -F '"path":"/api/v1/version"')"
    case "$line" in
        *"\"client_ip\":\"$gateway\""*)
            fail "the request from the shared network came from the gateway" ;;
        *'"counts":'* | *'"waf_score":'*)
            fail "servewaf checked the request from the shared network" ;;
    esac
    echo "gitea-setup: a request from the shared network skips the checks"

    compose stop servewaf >/dev/null 2>&1 || fail "servewaf did not stop"
    status="$(send /owner/repo)"
    [ "$status" = 200 ] ||
        fail "with servewaf stopped, the repository's page was answered $status"
    wait_for "rproxy logged no request SWRP_DEFAULT_DISPOSITION decided" \
        logged rproxy '"path":"/owner/repo"' '"default_disposition":true'
    echo "gitea-setup: with servewaf stopped, gitea answers through rproxy"
}

main "$@"
