#!/bin/bash
# LibrePortal CrowdSec helper — root-privileged ops for the host-side CrowdSec
# install (the agent + firewall bouncer the LibrePortal Traefik bouncer talks
# to). Installed root:root 0755 to /usr/local/lib/libreportal/ by init.sh.
# Self-contained: each action runs a FIXED set of ops with strictly-validated
# args, so the scoped sudoers needn't grant the manager blanket apt/cscli/tee/
# sed/chown on /etc/crowdsec or /var/log. The runtime calls in via
# `runCrowdsec <action> [args…]` (run_privileged.sh).

set -u

[[ $EUID -eq 0 ]] || { echo "libreportal-crowdsec: must run as root" >&2; exit 1; }

# Baked at install; unbaked copies keep the "__" sentinel.
SYSTEM_DIR="__SYSTEM_DIR__"
CONTAINERS_DIR="__CONTAINERS_DIR__"
MANAGER_USER="__MANAGER__"
[[ "$SYSTEM_DIR"     == *"__"* || -z "$SYSTEM_DIR"     ]] && SYSTEM_DIR="/libreportal-system"
[[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers"
[[ "$MANAGER_USER"   == *"__"* || -z "$MANAGER_USER"   ]] && MANAGER_USER="libreportal"

CFG_FILE="/etc/crowdsec/config.yaml"
BOUNCER_CFG="/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml"
TRAEFIK_KEY_FILE="/etc/crowdsec/traefik_bouncer.key"
HOST_LOGS=("/var/log/crowdsec.log" "/var/log/crowdsec-firewall-bouncer.log")

# --- install: add repo + apt install + initial config -------------------------
# Idempotent: skips repo add if /etc/apt/sources.list.d/crowdsec_*.list exists,
# skips apt install if cscli is on PATH, skips collections install if already
# present.
crowdsec_install() {
    if ! command -v cscli >/dev/null 2>&1; then
        if ! ls /etc/apt/sources.list.d/crowdsec_*.list >/dev/null 2>&1; then
            curl -fsSL https://install.crowdsec.net | bash >/dev/null 2>&1 || {
                echo "libreportal-crowdsec: failed to add CrowdSec apt repo" >&2; return 1
            }
        fi
        DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec </dev/null >/dev/null 2>&1 || {
            echo "libreportal-crowdsec: apt-get install crowdsec failed" >&2; return 1
        }
    fi
    if ! dpkg -s crowdsec-firewall-bouncer-nftables >/dev/null 2>&1; then
        DEBIAN_FRONTEND=noninteractive apt-get install -y -q crowdsec-firewall-bouncer-nftables </dev/null >/dev/null 2>&1 || {
            echo "libreportal-crowdsec: apt-get install firewall-bouncer failed" >&2; return 1
        }
    fi
    systemctl enable --now crowdsec >/dev/null 2>&1 || true
    systemctl enable --now crowdsec-firewall-bouncer >/dev/null 2>&1 || true

    # Baseline collections. cscli is idempotent ("already installed" exits 0).
    cscli collections install crowdsecurity/linux >/dev/null 2>&1 || true
    cscli collections install crowdsecurity/sshd  >/dev/null 2>&1 || true
    systemctl reload crowdsec >/dev/null 2>&1 || true
}

# --- services {enable|disable|restart} ----------------------------------------
crowdsec_services() {
    case "${1:-}" in
        enable)
            systemctl enable --now crowdsec || return 1
            systemctl enable --now crowdsec-firewall-bouncer || return 1
            ;;
        disable)
            systemctl disable --now crowdsec-firewall-bouncer >/dev/null 2>&1 || true
            systemctl disable --now crowdsec >/dev/null 2>&1 || true
            ;;
        restart)
            systemctl restart crowdsec || return 1
            ;;
        *) echo "libreportal-crowdsec: services {enable|disable|restart}" >&2; return 2 ;;
    esac
}

# --- capi {register|unregister|status} ----------------------------------------
# Community blocklist (subscribe + send anonymous attack signals).
crowdsec_capi() {
    case "${1:-}" in
        register)   cscli capi register   2>&1 ;;
        unregister) cscli capi unregister 2>&1 ;;
        status)     cscli capi status     2>&1 ;;
        *) echo "libreportal-crowdsec: capi {register|unregister|status}" >&2; return 2 ;;
    esac
}

# --- console {enroll <token>|disenroll|status} --------------------------------
# SaaS dashboard at app.crowdsec.net. Token strictly validated.
crowdsec_console() {
    case "${1:-}" in
        enroll)
            local token="${2:-}"
            [[ "$token" =~ ^[A-Za-z0-9_-]{8,128}$ ]] || {
                echo "libreportal-crowdsec: console enroll <token> — invalid token format" >&2
                return 1
            }
            cscli console enroll "$token" 2>&1
            ;;
        disenroll) cscli console disenroll 2>&1 ;;
        status)    cscli console status    2>&1 ;;
        *) echo "libreportal-crowdsec: console {enroll <token>|disenroll|status}" >&2; return 2 ;;
    esac
}

# --- bouncer-traefik-init: cscli register + write key file --------------------
# Idempotent: if the bouncer is already registered, skips and prints the existing
# key-file marker so callers can decide whether to keep or rotate. Returns the
# fresh key on stdout when newly generated, "EXISTS" when already registered.
crowdsec_bouncer_traefik_init() {
    if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
        echo "EXISTS"
        return 0
    fi
    local key
    key=$(cscli bouncers add traefik-bouncer -o raw 2>&1 | tail -1)
    [[ -n "$key" && "$key" != *"error"* ]] || {
        echo "libreportal-crowdsec: cscli bouncers add failed: $key" >&2
        return 1
    }
    # Write the key file: bind-mounted RO into Traefik. Manager-owned so the
    # WebUI/config layer (running as the manager) can read it; mode 0600.
    printf '%s\n' "$key" > "$TRAEFIK_KEY_FILE"
    chown "${MANAGER_USER}:${MANAGER_USER}" "$TRAEFIK_KEY_FILE"
    chmod 0600 "$TRAEFIK_KEY_FILE"
    echo "GENERATED:$key"
}

# --- bind-lapi: set listen_uri to 0.0.0.0:8080 in config.yaml -----------------
# Traefik talks to LAPI via host.docker.internal:8080. Bouncer API key gates
# external access (HTTP 401 without it).
crowdsec_bind_lapi() {
    [[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; }
    if grep -qE 'listen_uri:[[:space:]]*0\.0\.0\.0:8080' "$CFG_FILE"; then
        echo "ALREADY_BOUND"; return 0
    fi
    sed -i 's|listen_uri:.*|listen_uri: 0.0.0.0:8080|' "$CFG_FILE"
}

# --- prometheus {on <addr> <port>|off} ----------------------------------------
# Flip CrowdSec's Prometheus metrics endpoint. `on` requires an addr + port the
# Prometheus container can reach; `off` rebinds to 127.0.0.1.
crowdsec_prometheus() {
    [[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; }
    case "${1:-}" in
        on)
            local addr="${2:-}" port="${3:-}"
            [[ "$addr" =~ ^[A-Za-z0-9.-]+$ ]]  || { echo "libreportal-crowdsec: invalid addr" >&2; return 1; }
            [[ "$port" =~ ^[0-9]+$ ]]          || { echo "libreportal-crowdsec: invalid port" >&2; return 1; }
            sed -i "/^prometheus:/,/^[^[:space:]#]/ {
                s|enabled:.*|enabled: true|
                s|listen_addr:.*|listen_addr: ${addr}|
                s|listen_port:.*|listen_port: ${port}|
            }" "$CFG_FILE"
            ;;
        off)
            sed -i "/^prometheus:/,/^[^[:space:]#]/ s|listen_addr:.*|listen_addr: 127.0.0.1|" "$CFG_FILE"
            ;;
        *) echo "libreportal-crowdsec: prometheus {on <addr> <port>|off}" >&2; return 2 ;;
    esac
}

# --- touch-host-logs: make crowdsec logs readable by the libreportal container --
crowdsec_touch_host_logs() {
    local l
    for l in "${HOST_LOGS[@]}"; do
        touch  "$l" 2>/dev/null || true
        chmod 0644 "$l" 2>/dev/null || true
    done
}

# --- bouncer-priority: set nftables ipv4/ipv6 priority to -100 -----------------
# Moved here from libreportal-appcfg.crowdsec_priority. Same transform; lives in
# the dedicated crowdsec helper so all CrowdSec-touching root ops are in one
# auditable file.
crowdsec_bouncer_priority() {
    [[ -f "$BOUNCER_CFG" ]] || { echo "libreportal-crowdsec: $BOUNCER_CFG not found" >&2; return 1; }
    cp "$BOUNCER_CFG" "${BOUNCER_CFG}.bak.$(date +%Y%m%d-%H%M%S)"
    awk -v p="-100" '
        BEGIN { in_v4=0; in_v6=0 }
        /^[[:space:]]*ipv4:/ { in_v4=1; in_v6=0; print; next }
        /^[[:space:]]*ipv6:/ { in_v6=1; in_v4=0; print; next }
        /^[a-zA-Z]/ { in_v4=0; in_v6=0 }
        in_v4 && /^[[:space:]]+priority:/ { sub(/priority:.*/, "priority: " p) }
        in_v6 && /^[[:space:]]+priority:/ { sub(/priority:.*/, "priority: " p) }
        { print }
    ' "$BOUNCER_CFG" > "${BOUNCER_CFG}.new" && mv "${BOUNCER_CFG}.new" "$BOUNCER_CFG"
}

action="${1:-}"; shift 2>/dev/null || true
case "$action" in
    install)               crowdsec_install ;;
    services)              crowdsec_services "${1:-}" ;;
    capi)                  crowdsec_capi "${1:-}" ;;
    console)               crowdsec_console "${1:-}" "${2:-}" ;;
    bouncer-traefik-init)  crowdsec_bouncer_traefik_init ;;
    bouncer-priority)      crowdsec_bouncer_priority ;;
    bind-lapi)             crowdsec_bind_lapi ;;
    prometheus)            crowdsec_prometheus "${1:-}" "${2:-}" "${3:-}" ;;
    touch-host-logs)       crowdsec_touch_host_logs ;;
    *)
        echo "usage: libreportal-crowdsec {install|services <enable|disable|restart>|capi <register|unregister|status>|console <enroll <token>|disenroll|status>|bouncer-traefik-init|bouncer-priority|bind-lapi|prometheus <on <addr> <port>|off>|touch-host-logs}" >&2
        exit 2
        ;;
esac
