#!/usr/bin/env bash # UniFi UDM Pro — Classic API helper (login flow con session cookie + CSRF). # # Usa user/pass de un admin con rol "Site View Only" para autenticarse vía # POST /api/auth/login y cachea la sesión por 25 min en .cache/. La protección # read-only REAL la da ese rol en el UDM, no este script. # # Uso: # query-classic.sh [curl args...] # # El puede ser: # - Path corto: /stat/health → /proxy/network/api/s//stat/health # - Path /proxy o /api: se usa tal cual # - URL completa: se usa tal cual set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="$SKILL_DIR/.env" CACHE_DIR="$SKILL_DIR/.cache" COOKIE_FILE="$CACHE_DIR/cookies.txt" CSRF_FILE="$CACHE_DIR/csrf" SESSION_TTL_SEC=1500 # 25 min — UniFi OS sessions duran más, pero margen de seguridad if [[ ! -f "$ENV_FILE" ]]; then echo "ERROR: $ENV_FILE no existe." >&2 exit 1 fi set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a : "${UNIFI_HOST:?UNIFI_HOST no definido en .env}" : "${UNIFI_USERNAME:?UNIFI_USERNAME no definido en .env (necesario para Classic API)}" : "${UNIFI_PASSWORD:?UNIFI_PASSWORD no definido en .env}" UNIFI_SITE_NAME="${UNIFI_SITE_NAME:-default}" PATH_ARG="${1:-}" shift || true if [[ -z "$PATH_ARG" ]]; then cat >&2 < [curl args...] Ejemplos: query-classic.sh /stat/health # health WAN/uplink query-classic.sh /stat/sysinfo # info del controller query-classic.sh /stat/sta # clientes con bytes/signal/SSID query-classic.sh /stat/event # eventos recientes query-classic.sh /stat/alarm # alarmas query-classic.sh /list/wlanconf # WLANs / SSIDs query-classic.sh /rest/firewallrule # firewall rules query-classic.sh /rest/portforward # port forwards EOF exit 2 fi # ────────────────────────────────────────────────────────────────────────── # READ-ONLY GUARD — espejo del de query.sh. Defensa en profundidad. # La protección real es el rol "Site View Only" del admin (server-side 403). # ────────────────────────────────────────────────────────────────────────── for arg in "$@"; do case "$arg" in -X|--request|-XPOST|-XPUT|-XDELETE|-XPATCH) echo "ERROR: la skill 'unifi' es read-only. -X/--request bloqueado." >&2 exit 3 ;; -d|--data|--data-raw|--data-binary|--data-urlencode|-T|--upload-file) echo "ERROR: la skill 'unifi' es read-only. -d/--data/-T bloqueado." >&2 exit 3 ;; esac done mkdir -p "$CACHE_DIR" chmod 700 "$CACHE_DIR" 2>/dev/null || true # ¿Sesión cacheada y fresca? needs_login=true if [[ -f "$COOKIE_FILE" && -f "$CSRF_FILE" && -s "$COOKIE_FILE" && -s "$CSRF_FILE" ]]; then AGE=$(python -c "import os,time; print(int(time.time() - os.path.getmtime('$COOKIE_FILE')))" 2>/dev/null || echo 99999) if [[ "$AGE" -lt "$SESSION_TTL_SEC" ]]; then needs_login=false fi fi if $needs_login; then rm -f "$COOKIE_FILE" "$CSRF_FILE" # Construimos el body con Python para evitar problemas de escaping con caracteres especiales en el password. LOGIN_BODY=$(python -c "import json,os; print(json.dumps({'username':os.environ['UNIFI_USERNAME'],'password':os.environ['UNIFI_PASSWORD'],'rememberMe':False}))") HEADERS_TMP=$(mktemp) HTTP_CODE=$(curl -sS -k -o /dev/null -D "$HEADERS_TMP" -w "%{http_code}" \ -c "$COOKIE_FILE" \ -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$LOGIN_BODY" \ "https://${UNIFI_HOST}/api/auth/login") if [[ "$HTTP_CODE" != "200" ]]; then echo "ERROR: login falló (HTTP $HTTP_CODE)" >&2 cat "$HEADERS_TMP" >&2 rm -f "$HEADERS_TMP" "$COOKIE_FILE" exit 1 fi CSRF=$(grep -i "^x-csrf-token:" "$HEADERS_TMP" | tail -1 | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r\n') rm -f "$HEADERS_TMP" if [[ -z "$CSRF" ]]; then echo "ERROR: login OK pero no se recibió X-CSRF-Token." >&2 rm -f "$COOKIE_FILE" exit 1 fi printf '%s' "$CSRF" > "$CSRF_FILE" chmod 600 "$COOKIE_FILE" "$CSRF_FILE" 2>/dev/null || true fi CSRF=$(cat "$CSRF_FILE") # Build URL if [[ "$PATH_ARG" =~ ^https?:// ]]; then URL="$PATH_ARG" elif [[ "$PATH_ARG" == /proxy/* || "$PATH_ARG" == /api/* ]]; then URL="https://${UNIFI_HOST}${PATH_ARG}" else URL="https://${UNIFI_HOST}/proxy/network/api/s/${UNIFI_SITE_NAME}${PATH_ARG}" fi exec curl -sS -k \ -b "$COOKIE_FILE" \ -H "X-CSRF-Token: ${CSRF}" \ -H "Accept: application/json" \ "$@" \ "$URL"