#!/bin/sh
# Temporary browser access to the Helium fixture's loopback-only VirtualBox
# VRDE endpoint. This is a manual-recovery helper, never a normal test channel.
set -eu

helper_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repo_root=$(CDPATH= cd -- "$helper_dir/../.." && pwd)
runtime_dir=$helper_dir/.runtime
compose_file=$helper_dir/compose.yaml
mapping_template=$helper_dir/user-mapping.xml.in
project=rvbox-rdp-access

: "${RDP_ACCESS_BIND:=127.0.0.1}"
: "${RDP_ACCESS_HTTP_PORT:=5002}"
: "${RDP_ACCESS_TUNNEL_PORT:=54001}"
: "${RDP_ACCESS_PUBLIC_HOST:=localhost}"
: "${RDP_ACCESS_WEB_USER:=rvboxtest}"
: "${RDP_ACCESS_RDP_USER:=rvboxtest}"
: "${RVBOX_TEST_VBOX_HOST:=helium-remote}"
: "${RDP_ACCESS_VRDE_HOST:=127.0.0.1}"
: "${RDP_ACCESS_VRDE_PORT:=3389}"

usage() {
    cat <<'EOF'
usage: test/rdp-access/rdp-access ACTION [OPTIONS]

Actions:
  up       start a private VRDE SSH tunnel and self-signed HTTPS Guacamole
  status   show gateway, tunnel, and fixture status without changing anything
  url      print the current browser URL
  logs     follow or print Compose logs (pass Docker Compose log options)
  down     stop containers and the private SSH tunnel; retain generated state
  clean    run down and delete generated state; pass --images to also remove
           the exact unused Guacamole/nginx images

up options:
  --bind ADDRESS        listener address (default 127.0.0.1; use 0.0.0.0 only
                        for a temporary, deliberately public endpoint)
  --http-port PORT      HTTPS listener port (default 5002)
  --tunnel-port PORT    private VRDE tunnel port (default 54001)
  --public-host NAME    browser-visible hostname or IP for the URL and cert SAN
  --web-user USER       Guacamole and Windows account (default rvboxtest)
  --reset-auth          discard the saved password hash and prompt again
  --web-password-stdin read the password once from stdin instead of prompting

Environment equivalents: RDP_ACCESS_BIND, RDP_ACCESS_HTTP_PORT,
RDP_ACCESS_TUNNEL_PORT, RDP_ACCESS_PUBLIC_HOST, RDP_ACCESS_WEB_USER,
RDP_ACCESS_RDP_USER, RVBOX_TEST_VBOX_HOST, RDP_ACCESS_VRDE_HOST, and
RDP_ACCESS_VRDE_PORT.
EOF
}

fail() { printf '%s\n' "rdp-access: $*" >&2; exit 2; }

safe_name() {
    case $2 in ''|*[!A-Za-z0-9.-]*) fail "$1 contains unsupported characters" ;; esac
}

safe_port() {
    case $2 in ''|*[!0-9]*) fail "$1 must be a port number" ;; esac
    [ "$2" -ge 1024 ] && [ "$2" -le 65535 ] || fail "$1 must be between 1024 and 65535"
}

safe_bind() {
    case $1 in 127.0.0.1|0.0.0.0) ;; *) fail "--bind must be 127.0.0.1 or 0.0.0.0" ;; esac
}

compose() {
    RDP_ACCESS_RUNTIME_DIR=$runtime_dir \
    RDP_ACCESS_BIND=$RDP_ACCESS_BIND \
    RDP_ACCESS_HTTP_PORT=$RDP_ACCESS_HTTP_PORT \
    RDP_ACCESS_CERT_NAME=$RDP_ACCESS_PUBLIC_HOST \
    RDP_ACCESS_CERT_SAN=$cert_san \
    RDP_ACCESS_HOST_UID=$(id -u) \
    RDP_ACCESS_HOST_GID=$(id -g) \
    docker compose --project-name "$project" -f "$compose_file" "$@"
}

socket_path=$runtime_dir/ssh-control.socket
session_file=$runtime_dir/session.env
cert_name_file=$runtime_dir/cert-name

stop_tunnel() {
    if [ -S "$socket_path" ]; then
        ssh -S "$socket_path" -O exit "$RVBOX_TEST_VBOX_HOST" >/dev/null 2>&1 || true
    fi
    rm -f "$socket_path"
}

gateway_for_network() {
    docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' "${project}_default"
}

assert_fixture_running() {
    fixture_status=$("$repo_root/scripts/windows/test-host" status) || fail "fixture identity check failed"
    printf '%s\n' "$fixture_status"
    case $fixture_status in *'state=running') ;; *) fail "VM is not running; prepare it first with scripts/windows/test-host prepare --run-id interactive-rdp" ;; esac
}

password_hash_from_terminal() {
    password=
    restore_tty=false
    if [ "$password_stdin" = true ]; then
        # Password files commonly omit a final newline. POSIX read returns
        # non-zero at that EOF even after assigning the final nonempty line.
        IFS= read -r password || [ -n "$password" ] || fail "could not read password from stdin"
    else
        [ -t 0 ] || fail "stdin is not a terminal; use --web-password-stdin"
        printf 'Fixture password for %s: ' "$RDP_ACCESS_WEB_USER" >&2
        stty -echo
        restore_tty=true
        trap 'test "$restore_tty" = true && stty echo || true' EXIT HUP INT TERM
        IFS= read -r password || fail "could not read password"
        stty echo
        restore_tty=false
        trap - EXIT HUP INT TERM
        printf '\n' >&2
    fi
    [ -n "$password" ] || fail "password must not be empty"
    hash=$(printf '%s' "$password" | docker run --rm -i --entrypoint md5sum alpine:3.20 | awk '{print $1}')
    unset password
    case $hash in ''|*[!0-9a-f]*) fail "could not generate password hash" ;; esac
    [ "${#hash}" -eq 32 ] || fail "could not generate password hash"
    printf '%s\n' "$hash"
}

render_mapping() {
    umask 077
    mkdir -p "$runtime_dir/config" "$runtime_dir/tls"
    chmod 700 "$runtime_dir" "$runtime_dir/config" "$runtime_dir/tls"
    if [ "$reset_auth" = true ]; then rm -f "$runtime_dir/config/user-mapping.xml"; fi
    if [ -s "$runtime_dir/config/user-mapping.xml" ]; then
        password_hash=$(sed -n 's/.*password="\([0-9a-f][0-9a-f]*\)".*/\1/p' "$runtime_dir/config/user-mapping.xml" | head -n 1)
        case $password_hash in ''|*[!0-9a-f]*) password_hash=$(password_hash_from_terminal) ;; esac
        [ "${#password_hash}" -eq 32 ] || password_hash=$(password_hash_from_terminal)
    else
        password_hash=$(password_hash_from_terminal)
    fi
    sed \
        -e "s/@WEB_USER@/$RDP_ACCESS_WEB_USER/g" \
        -e "s/@WEB_PASSWORD_MD5@/$password_hash/g" \
        -e "s/@DOCKER_GATEWAY@/$docker_gateway/g" \
        -e "s/@TUNNEL_PORT@/$RDP_ACCESS_TUNNEL_PORT/g" \
        -e "s/@RDP_USER@/$RDP_ACCESS_RDP_USER/g" \
        "$mapping_template" >"$runtime_dir/config/user-mapping.xml"
    chmod 600 "$runtime_dir/config/user-mapping.xml"
    if [ -f "$cert_name_file" ] && [ "$(cat "$cert_name_file")" != "$RDP_ACCESS_PUBLIC_HOST" ]; then
        rm -f "$runtime_dir/tls/cert.pem" "$runtime_dir/tls/key.pem"
    fi
    printf '%s\n' "$RDP_ACCESS_PUBLIC_HOST" >"$cert_name_file"
    chmod 600 "$cert_name_file"
    printf 'url=https://%s:%s/guacamole/\n' "$RDP_ACCESS_PUBLIC_HOST" "$RDP_ACCESS_HTTP_PORT" >"$session_file"
    printf 'docker_gateway=%s\n' "$docker_gateway" >>"$session_file"
    printf 'tunnel_port=%s\n' "$RDP_ACCESS_TUNNEL_PORT" >>"$session_file"
    chmod 600 "$session_file"
}

start_tunnel() {
    stop_tunnel
    ssh -M -S "$socket_path" -fN \
        -o BatchMode=yes \
        -o ExitOnForwardFailure=yes \
        -o ServerAliveInterval=30 \
        -o ServerAliveCountMax=3 \
        -L "$docker_gateway:$RDP_ACCESS_TUNNEL_PORT:$RDP_ACCESS_VRDE_HOST:$RDP_ACCESS_VRDE_PORT" \
        "$RVBOX_TEST_VBOX_HOST"
    ssh -S "$socket_path" -O check "$RVBOX_TEST_VBOX_HOST" >/dev/null 2>&1 || fail "private VRDE tunnel did not start"
}

action=${1-}
[ -n "$action" ] || { usage >&2; exit 2; }
shift || true
case $action in --help|-h) usage; exit 0 ;; esac

reset_auth=false
password_stdin=false
remove_images=false
while [ "$#" -gt 0 ]; do
    case $1 in
        --bind) [ "$#" -ge 2 ] || fail "--bind needs a value"; RDP_ACCESS_BIND=$2; shift 2 ;;
        --http-port) [ "$#" -ge 2 ] || fail "--http-port needs a value"; RDP_ACCESS_HTTP_PORT=$2; shift 2 ;;
        --tunnel-port) [ "$#" -ge 2 ] || fail "--tunnel-port needs a value"; RDP_ACCESS_TUNNEL_PORT=$2; shift 2 ;;
        --public-host) [ "$#" -ge 2 ] || fail "--public-host needs a value"; RDP_ACCESS_PUBLIC_HOST=$2; shift 2 ;;
        --web-user) [ "$#" -ge 2 ] || fail "--web-user needs a value"; RDP_ACCESS_WEB_USER=$2; RDP_ACCESS_RDP_USER=$2; shift 2 ;;
        --reset-auth) reset_auth=true; shift ;;
        --web-password-stdin) password_stdin=true; shift ;;
        --images) remove_images=true; shift ;;
        --help|-h) usage; exit 0 ;;
        *) break ;;
    esac
done

safe_bind "$RDP_ACCESS_BIND"
safe_port RDP_ACCESS_HTTP_PORT "$RDP_ACCESS_HTTP_PORT"
safe_port RDP_ACCESS_TUNNEL_PORT "$RDP_ACCESS_TUNNEL_PORT"
[ "$RDP_ACCESS_HTTP_PORT" != "$RDP_ACCESS_TUNNEL_PORT" ] || fail "HTTPS and tunnel ports must differ"
safe_name RDP_ACCESS_PUBLIC_HOST "$RDP_ACCESS_PUBLIC_HOST"
safe_name RDP_ACCESS_WEB_USER "$RDP_ACCESS_WEB_USER"
safe_name RDP_ACCESS_RDP_USER "$RDP_ACCESS_RDP_USER"
safe_name RVBOX_TEST_VBOX_HOST "$RVBOX_TEST_VBOX_HOST"
case $RDP_ACCESS_VRDE_HOST in 127.0.0.1|localhost) ;; *) fail "RDP_ACCESS_VRDE_HOST must be 127.0.0.1 or localhost" ;; esac
safe_port RDP_ACCESS_VRDE_PORT "$RDP_ACCESS_VRDE_PORT"

case $RDP_ACCESS_PUBLIC_HOST in
    *[!0-9.]* ) cert_san="DNS:$RDP_ACCESS_PUBLIC_HOST" ;;
    * ) cert_san="IP:$RDP_ACCESS_PUBLIC_HOST" ;;
esac

[ "$remove_images" = false ] || [ "$action" = clean ] || fail "--images is valid only with clean"
[ "$reset_auth" = false ] || [ "$action" = up ] || fail "--reset-auth is valid only with up"
[ "$password_stdin" = false ] || [ "$action" = up ] || fail "--web-password-stdin is valid only with up"

case $action in
    up)
        [ "$#" -eq 0 ] || { usage >&2; fail "unknown up option $1"; }
        if [ "$RDP_ACCESS_BIND" = 0.0.0.0 ] && [ "$RDP_ACCESS_PUBLIC_HOST" = localhost ]; then
            fail "a public bind requires --public-host with the browser-visible hostname or IP"
        fi
        docker version >/dev/null
        docker compose version >/dev/null
        assert_fixture_running
        if compose ps -q | grep -q .; then
            fail "gateway already exists; use status or down first"
        fi
        mkdir -p "$runtime_dir"
        compose up -d guacd
        docker_gateway=$(gateway_for_network) || { compose down --remove-orphans; fail "could not determine private Docker gateway"; }
        render_mapping
        if ! start_tunnel; then
            compose down --remove-orphans
            fail "could not create private SSH tunnel"
        fi
        if ! compose run --rm certgen; then
            stop_tunnel
            compose down --remove-orphans
            fail "could not generate self-signed certificate"
        fi
        if ! compose up -d guacamole gateway; then
            stop_tunnel
            compose down --remove-orphans
            fail "could not start Guacamole gateway"
        fi
        printf 'Guacamole is ready at https://%s:%s/guacamole/\n' "$RDP_ACCESS_PUBLIC_HOST" "$RDP_ACCESS_HTTP_PORT"
        printf 'Accept the self-signed certificate warning, then sign in as %s with the fixture password.\n' "$RDP_ACCESS_WEB_USER"
        ;;
    status)
        [ "$#" -eq 0 ] || { usage >&2; fail "status accepts no options"; }
        "$repo_root/scripts/windows/test-host" status || true
        if [ -f "$session_file" ]; then sed -n '1p' "$session_file"; fi
        compose ps
        if [ -S "$socket_path" ] && ssh -S "$socket_path" -O check "$RVBOX_TEST_VBOX_HOST" >/dev/null 2>&1; then
            printf 'private_tunnel=active\n'
        else
            printf 'private_tunnel=inactive\n'
        fi
        ;;
    url)
        [ "$#" -eq 0 ] || { usage >&2; fail "url accepts no options"; }
        [ -f "$session_file" ] || fail "no saved gateway session; run up first"
        sed -n '1s/^url=//p' "$session_file"
        ;;
    logs)
        compose logs "$@"
        ;;
    down)
        [ "$#" -eq 0 ] || { usage >&2; fail "down accepts no options"; }
        stop_tunnel
        compose down --remove-orphans || true
        printf 'Temporary gateway and private tunnel stopped; generated certificate and password hash retained in %s.\n' "$runtime_dir"
        ;;
    clean)
        [ "$#" -eq 0 ] || { usage >&2; fail "clean accepts only --images"; }
        stop_tunnel
        compose down --remove-orphans || true
        case $runtime_dir in "$helper_dir"/.runtime) rm -rf "$runtime_dir" ;; *) fail "unsafe runtime path" ;; esac
        if [ "$remove_images" = true ]; then
            docker image rm guacamole/guacamole:1.6.0 guacamole/guacd:1.6.0 nginx:1.27-alpine >/dev/null 2>&1 || true
        fi
        printf 'Temporary gateway state removed.\n'
        ;;
    *) usage >&2; fail "unknown action $action" ;;
esac
