import: contenido inicial de la skill unifi
This commit is contained in:
139
scripts/query-classic.sh
Normal file
139
scripts/query-classic.sh
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/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"
|
||||
107
scripts/query.sh
Normal file
107
scripts/query.sh
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# UniFi UDM Pro — Integration API helper.
|
||||
#
|
||||
# Uso:
|
||||
# query.sh <path> [curl args...]
|
||||
#
|
||||
# El <path> puede ser:
|
||||
# - Path corto: /sites → se prefija /proxy/network/integration/v1
|
||||
# - Path /proxy: /proxy/network/... → se usa tal cual (sirve para Classic API)
|
||||
# - URL completa: https://... → se usa tal cual
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="$SKILL_DIR/.env"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "ERROR: $ENV_FILE no existe." >&2
|
||||
echo " cp $SKILL_DIR/.env.example $ENV_FILE y completá UNIFI_API_KEY." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${UNIFI_HOST:?UNIFI_HOST no definido en .env}"
|
||||
: "${UNIFI_API_KEY:?UNIFI_API_KEY no definido en .env}"
|
||||
: "${UNIFI_SITE:?UNIFI_SITE no definido en .env (debe ser el UUID del site, no el nombre)}"
|
||||
|
||||
PATH_ARG="${1:-}"
|
||||
shift || true
|
||||
|
||||
if [[ -z "$PATH_ARG" ]]; then
|
||||
cat >&2 <<EOF
|
||||
Uso: query.sh <path> [curl args...]
|
||||
|
||||
Ejemplos:
|
||||
query.sh /sites
|
||||
query.sh /sites/{site}/devices
|
||||
query.sh /sites/{site}/clients
|
||||
query.sh /sites/{site}/devices/<deviceId>/statistics/latest
|
||||
|
||||
El placeholder {site} se reemplaza por \$UNIFI_SITE (UUID del site, en .env).
|
||||
Para Classic API usar query-classic.sh (login flow distinto).
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# READ-ONLY GUARD — la skill es read-only por diseño. Bloqueamos cualquier
|
||||
# intento de cambiar el verbo HTTP o mandar body. La protección REAL la da
|
||||
# el rol "Site View Only" del admin en el UDM (que también devuelve 403);
|
||||
# este guard es defensa en profundidad para fallar rápido sin salir a la red.
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
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
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# READ-ONLY GUARD — la skill es read-only por diseño. Bloqueamos cualquier
|
||||
# intento de cambiar el verbo HTTP o mandar body. La protección REAL la da
|
||||
# el rol "Site View Only" del admin en el UDM (que también devuelve 403);
|
||||
# este guard es defensa en profundidad para evitar errores aun antes de
|
||||
# salir a la red.
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
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
|
||||
echo " Si REALMENTE necesitás escribir, llamá a curl directo (no via este script)" >&2
|
||||
echo " y sabé lo que estás haciendo." >&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
|
||||
|
||||
# Expandir el placeholder {site} con el UUID del site (Integration API usa UUID, no nombre).
|
||||
PATH_ARG="${PATH_ARG//\{site\}/$UNIFI_SITE}"
|
||||
|
||||
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/integration/v1${PATH_ARG}"
|
||||
fi
|
||||
|
||||
exec curl -sS -k \
|
||||
-H "X-API-KEY: ${UNIFI_API_KEY}" \
|
||||
-H "Accept: application/json" \
|
||||
"$@" \
|
||||
"$URL"
|
||||
Reference in New Issue
Block a user