Files
skill-unifi/scripts/query-classic.sh

140 lines
4.8 KiB
Bash

#!/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 <path> [curl args...]
#
# El <path> puede ser:
# - Path corto: /stat/health → /proxy/network/api/s/<UNIFI_SITE_NAME>/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 <<EOF
Uso: query-classic.sh <path> [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"