#!/usr/bin/env bash # Registra subdominios en Cloudflare DNS y opcionalmente en el tunnel. # # Modos: # ./add-domain.sh [sub2 ...] → A record directo (sin tunnel) # ./add-domain.sh --tunnel [sub2 ...] → CNAME al tunnel + ingress # # Ejemplos: # ./add-domain.sh immich → A 193.70.84.224, solo DNS # ./add-domain.sh --tunnel immich obsidian → CNAME tunnel + ingress set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" if [ ! -f "$ENV_FILE" ]; then echo "ERROR: No se encontró .env en $SCRIPT_DIR" exit 1 fi set -a; source "$ENV_FILE"; set +a # ── Modo ──────────────────────────────────────────────────────────────────── MODE="direct" if [ "${1:-}" = "--tunnel" ]; then MODE="tunnel" shift fi if [ $# -eq 0 ]; then echo "Uso: $0 [--tunnel] [subdominio2 ...]" exit 1 fi # ── Validar variables obligatorias ────────────────────────────────────────── REQUIRED_VARS="CLOUDFLARE_API_TOKEN CLOUDFLARE_ZONE_ID CLOUDFLARE_DOMAIN" [ "$MODE" = "tunnel" ] && REQUIRED_VARS="$REQUIRED_VARS CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_TUNNEL_ID" for VAR in $REQUIRED_VARS; do if [ -z "${!VAR:-}" ] || [[ "${!VAR}" == change-me* ]]; then echo "ERROR: $VAR no está configurado en .env" exit 1 fi done SERVER_IP="${CLOUDFLARE_SERVER_IP:-}" if [ "$MODE" = "direct" ] && [ -z "$SERVER_IP" ]; then # Autodetectar IP pública del servidor SERVER_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || true) if [ -z "$SERVER_IP" ]; then echo "ERROR: No se pudo detectar la IP del servidor. Define CLOUDFLARE_SERVER_IP en .env" exit 1 fi echo "→ IP del servidor autodetectada: $SERVER_IP" fi # ── Modo tunnel: obtener ingress actual ───────────────────────────────────── TUNNEL_CNAME="${CLOUDFLARE_TUNNEL_ID:-}.cfargotunnel.com" CURRENT="" if [ "$MODE" = "tunnel" ]; then echo "→ Obteniendo configuración actual del tunnel..." CURRENT=$(curl -s \ "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/cfd_tunnel/${CLOUDFLARE_TUNNEL_ID}/configurations" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") if ! echo "$CURRENT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then echo "ERROR: No se pudo obtener la config del tunnel" echo "$CURRENT" exit 1 fi fi # ── Por cada subdominio ────────────────────────────────────────────────────── for SUB in "$@"; do FQDN="${SUB}.${CLOUDFLARE_DOMAIN}" echo "" echo "=== $FQDN ===" # Tunnel: añadir al ingress if [ "$MODE" = "tunnel" ]; then CURRENT=$(echo "$CURRENT" | python3 -c " import sys, json data = json.load(sys.stdin) ingress = data['result']['config']['ingress'] hostname = '${FQDN}' if any(r.get('hostname') == hostname for r in ingress): print(json.dumps(data), end='') print(' tunnel: ya existe, sin cambios', file=sys.stderr) sys.exit(0) catch_all = ingress.pop() ingress.append({'hostname': hostname, 'service': 'https://traefik:443', 'originRequest': {'noTLSVerify': True}}) ingress.append(catch_all) data['result']['config']['ingress'] = ingress print(json.dumps(data), end='') print(' tunnel: añadido', file=sys.stderr) ") DNS_PAYLOAD="{\"type\":\"CNAME\",\"name\":\"${SUB}\",\"content\":\"${TUNNEL_CNAME}\",\"ttl\":1,\"proxied\":true,\"comment\":\"Cloudflare Tunnel\"}" DNS_TARGET="$TUNNEL_CNAME (CNAME proxied)" else DNS_PAYLOAD="{\"type\":\"A\",\"name\":\"${SUB}\",\"content\":\"${SERVER_IP}\",\"ttl\":1,\"proxied\":false,\"comment\":\"Direct\"}" DNS_TARGET="$SERVER_IP (A record)" fi # DNS: crear o actualizar EXISTING=$(curl -s \ "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records?name=${FQDN}" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") RECORD_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'])" 2>/dev/null || true) if [ -n "$RECORD_ID" ]; then RESULT=$(curl -s -X PUT \ "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records/${RECORD_ID}" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "$DNS_PAYLOAD") MSG="actualizado" else RESULT=$(curl -s -X POST \ "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "$DNS_PAYLOAD") MSG="creado" fi if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then echo " DNS: $MSG → $DNS_TARGET" else echo " DNS ERROR:" echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('errors'))" fi done # ── Guardar ingress del tunnel ─────────────────────────────────────────────── if [ "$MODE" = "tunnel" ]; then echo "" echo "→ Guardando configuración del tunnel..." NEW_CONFIG=$(echo "$CURRENT" | python3 -c " import sys, json data = json.load(sys.stdin) print(json.dumps({'config': data['result']['config']})) ") RESULT=$(curl -s -X PUT \ "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/cfd_tunnel/${CLOUDFLARE_TUNNEL_ID}/configurations" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "$NEW_CONFIG") if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('success') else 1)" 2>/dev/null; then echo " Tunnel ingress actualizado ✓" else echo " ERROR al actualizar tunnel:" echo "$RESULT" exit 1 fi fi echo "" echo "Listo. DNS activo en ~30s."