140 lines
4.8 KiB
Bash
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"
|