#!/bin/bash
# vexor-rotate-ingest-token -- create or rotate the Vexor log-ingest token.
#
# The token authenticates log shippers against /api/v1/logs/push. It lives in
# two places that must always agree:
#
#   /etc/vexor/logs-ingest.env                     root:vexor 0640  (read by vexor-api)
#   /etc/nginx/conf.d/00-vexor-ingest-token.conf   root:root  0600  (enforced by nginx)
#
# Usage:
#   vexor-rotate-ingest-token --ensure    create only if no token exists yet
#   vexor-rotate-ingest-token             always generate a new token (rotate)
#
# Rotating invalidates the old token: every deployed log shipper stops being
# able to ship until it is reconfigured with the new one.
#
# vexor-api reads the env file on every request, so no service restart is
# needed -- only an nginx reload.
set -euo pipefail

ENV_FILE="/etc/vexor/logs-ingest.env"
MAP_FILE="/etc/nginx/conf.d/00-vexor-ingest-token.conf"
ENSURE=0

for arg in "$@"; do
    case "$arg" in
        --ensure) ENSURE=1 ;;
        -h|--help) sed -n '2,20p' "$0"; exit 0 ;;
        *) echo "unknown argument: $arg" >&2; exit 2 ;;
    esac
done

if [ "$(id -u)" -ne 0 ]; then
    echo "must run as root" >&2
    exit 1
fi

current_token() {
    [ -r "$ENV_FILE" ] || return 0
    sed -n 's/^VEXOR_LOGS_INGEST_TOKEN=//p' "$ENV_FILE" | tail -1
}

EXISTING="$(current_token)"

# --ensure is the install-time path: never disturb a working token, but do
# repair the nginx map if it drifted away from the env file.
if [ "$ENSURE" = "1" ] && [ -n "$EXISTING" ]; then
    if [ -f "$MAP_FILE" ] && grep -qF "\"Bearer ${EXISTING}\"" "$MAP_FILE"; then
        echo "ingest token already configured, leaving it alone"
        exit 0
    fi
    echo "ingest token exists but the nginx map is missing or stale, regenerating map"
    TOKEN="$EXISTING"
else
    # Do not lengthen this token. It becomes an nginx map key of the form
    # "Bearer <token>", and with the default map_hash_bucket_size of 64 nginx
    # refuses to start once that key exceeds 46 bytes (measured). 32 characters
    # gives a 39-byte key with room to spare, and still 192 bits of entropy.
    # Raising map_hash_bucket_size instead is not an option: it is an http-level
    # directive, so shipping it would clash on hosts that already set it.
    TOKEN="$(head -c 48 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n' | cut -c1-32)"
    if [ "${#TOKEN}" -ne 32 ]; then
        echo "failed to generate a 32-character token" >&2
        exit 1
    fi
fi

# Keep the previous contents so a failed nginx test can be rolled back.
BACKUP_DIR="$(mktemp -d)"
trap 'rm -rf "$BACKUP_DIR"' EXIT
[ -f "$ENV_FILE" ] && cp -a "$ENV_FILE" "$BACKUP_DIR/env" || true
[ -f "$MAP_FILE" ] && cp -a "$MAP_FILE" "$BACKUP_DIR/map" || true

restore() {
    if [ -f "$BACKUP_DIR/env" ]; then cp -a "$BACKUP_DIR/env" "$ENV_FILE"; else rm -f "$ENV_FILE"; fi
    if [ -f "$BACKUP_DIR/map" ]; then cp -a "$BACKUP_DIR/map" "$MAP_FILE"; else rm -f "$MAP_FILE"; fi
}

install -d -m 0755 /etc/vexor

TMP_ENV="$(mktemp "${ENV_FILE}.XXXXXX")"
cat > "$TMP_ENV" <<EOF
# Vexor log-ingest token. Generated by vexor-rotate-ingest-token.
# Manage this from the web UI under Logs -> Log shippers.
VEXOR_LOGS_INGEST_TOKEN=${TOKEN}
EOF
chmod 0640 "$TMP_ENV"
# The API runs as the vexor user and must be able to read this file.
if getent group vexor >/dev/null 2>&1; then
    chown root:vexor "$TMP_ENV"
else
    chown root:root "$TMP_ENV"
fi
mv -f "$TMP_ENV" "$ENV_FILE"

TMP_MAP="$(mktemp "${MAP_FILE}.XXXXXX")"
cat > "$TMP_MAP" <<EOF
# Generated by vexor-rotate-ingest-token. Do not edit by hand.
# The token itself lives in ${ENV_FILE}.
map \$http_authorization \$vexor_ingest_ok {
    default 0;
    "Bearer ${TOKEN}"  1;
}
EOF
chmod 0600 "$TMP_MAP"
chown root:root "$TMP_MAP"
mv -f "$TMP_MAP" "$MAP_FILE"

if ! nginx -t >/tmp/vexor-ingest-nginx-test.log 2>&1; then
    echo "nginx rejected the new configuration, rolling back:" >&2
    cat /tmp/vexor-ingest-nginx-test.log >&2
    restore
    exit 1
fi

# A reload returns as soon as nginx has been signalled, not when the new
# configuration is actually serving requests. Without waiting, a caller that
# rotates the token and immediately uses it gets a 401, and -- worse for a
# rotation -- the old token keeps being accepted for a moment. So confirm the
# new token is live before reporting success. A GET passes the same token gate
# as a real push but writes nothing to the log store.
wait_for_token_live() {
    local i code
    for i in $(seq 1 50); do
        code="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 2 \
            -H "Authorization: Bearer ${TOKEN}" \
            "https://127.0.0.1/api/v1/logs/push" 2>/dev/null || echo 000)"
        if [ "$code" != "401" ] && [ "$code" != "000" ]; then
            return 0
        fi
        sleep 0.2
    done
    return 1
}

if systemctl is-active --quiet nginx; then
    systemctl reload nginx
    if command -v curl >/dev/null 2>&1 && ! wait_for_token_live; then
        # The files on disk are correct and nginx accepted them, so this is a
        # slow reload rather than a failure. Do not roll back.
        echo "warning: nginx reloaded but the new token was not live yet" >&2
    fi
    echo "nginx reloaded"
else
    echo "nginx is not running, new token will apply when it starts"
fi

echo "ingest token written to ${ENV_FILE}"
