diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..242a78c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,41 @@ +# Configuración básica de Caddy para solu23.cloud +solu23.cloud { + # Raíz del sitio + root * /var/www/sol23_placeholder + + # Habilitar compresión + encode gzip + + # Servir archivos estáticos + file_server + + # Página de error personalizada (opcional) + handle_errors { + respond "{http.error.status_code} {http.error.status_text}" + } + + # Headers de seguridad + header { + # Habilitar HSTS + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + # Prevenir clickjacking + X-Frame-Options "SAMEORIGIN" + # Prevenir MIME sniffing + X-Content-Type-Options "nosniff" + # XSS Protection + X-XSS-Protection "1; mode=block" + # Referrer Policy + Referrer-Policy "strict-origin-when-cross-origin" + } + + # Logs (opcional) + log { + output file /var/log/caddy/solu23.log + format json + } +} + +# Redirección de www a no-www (opcional) +www.solu23.cloud { + redir https://solu23.cloud{uri} permanent +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e324677 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + caddy: + image: caddy:2-alpine + container_name: caddy_solu23 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" # HTTP/3 + volumes: + # Monta el Caddyfile + - ./Caddyfile:/etc/caddy/Caddyfile + # Monta los archivos del sitio + - ./:/var/www/sol23_placeholder + # Datos persistentes de Caddy (certificados SSL) + - caddy_data:/data + - caddy_config:/config + # Logs + - ./logs:/var/log/caddy + networks: + - caddy_network + +volumes: + caddy_data: + driver: local + caddy_config: + driver: local + +networks: + caddy_network: + driver: bridge diff --git a/health_checker b/health_checker new file mode 100644 index 0000000..f3a50ab --- /dev/null +++ b/health_checker @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import requests +import json +from datetime import datetime, timezone +import os +import subprocess # <-- CAMBIO: Importar para ejecutar comandos del sistema + +# --- Variables y Configuración --- + +webhook_env_var = os.getenv('WEBHOOK_URLS', '') +WEBHOOK_URLS = [url.strip() for url in webhook_env_var.split(',') if url.strip()] + +INTERNOS = { + "vps_soul23": "31.97.41.188", # <-- CAMBIO: Añadido el VPS para el ping + "coolify": "https://aperture.soul23.cloud", + "formbricks": "https://feedback.soul23.cloud", + "vikunja": "https://tasks.soul23.cloud", + "gokapi": "https://wetrans.soul23.cloud", + "n8n_flows": "https://flows.soul23.cloud", + "ap_pos": "https://apos.soul23.cloud" +} + +EXTERNOS = { + "openai": "https://status.openai.com/", + "google_gemini": "https://aistudio.google.com/status", + "canva": "https://www.canvastatus.com/", + "tiktok": "https://www.tiktok.com", + "facebook": "https://www.facebook.com", + "instagram": "https://www.instagram.com", + "telegram": "https://core.telegram.org", + "whatsapp": "https://www.whatsapp.com", + "x": "https://www.x.com", + "youtube": "https://www.youtube.com" +} + +STATUSPAGE_SERVICES = ["openai", "canva"] + +# --- Funciones de Verificación de Estado --- + +def check_url(url): + try: + r = requests.get(url, timeout=8) + return r.status_code + except: + return 0 + +# <-- CAMBIO: Nueva función para hacer ping al VPS +def check_vps_ping(ip_address): + """ + Envía un único paquete ICMP (ping) a la IP especificada. + Devuelve un estado basado en si el host respondió. + """ + try: + # Comando de ping para Linux (que es lo que usan los runners de GitHub): + # -c 1: Enviar solo 1 paquete. + # -W 2: Esperar un máximo de 2 segundos por una respuesta. + command = ["ping", "-c", "1", "-W", "2", ip_address] + + # Ejecuta el comando, ocultando la salida para mantener los logs limpios. + result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Un código de retorno 0 significa que el ping fue exitoso. + if result.returncode == 0: + return f"🟢 OK (VPS Reachable)" + else: + return f"🔴 Caído (VPS Unreachable)" + except Exception: + # Esto podría pasar si el comando 'ping' no estuviera disponible. + return f"🔴 Error (Ping command failed)" + +def check_formbricks_health(base_url): + health_url = f"{base_url.rstrip('/')}/health" + try: + response = requests.get(health_url, timeout=8) + if response.status_code == 200: + try: + data = response.json() + if data.get("status") == "ok": + return f"🟢 OK (API Health: ok)" + else: + return f"🟡 Advertencia (API Health: {data.get('status', 'unknown')})" + except json.JSONDecodeError: + return "🟡 Advertencia (Respuesta no es JSON válido)" + else: + return f"🔴 Caído (Health Endpoint: {response.status_code})" + except requests.RequestException as e: + return f"🔴 Caído (Error de red: {e})" + + +def get_statuspage_status(base_url): + api_url = f"{base_url.rstrip('/')}/api/v2/summary.json" + try: + response = requests.get(api_url, timeout=8) + if response.status_code == 200: + data = response.json() + status_description = data.get('status', {}).get('description') + if status_description: + status_indicator = data.get('status', {}).get('indicator') + if status_indicator == 'none': + return f"🟢 OK ({status_description})" + else: + return f"🟡 Advertencia ({status_description})" + return "🟡 Advertencia (Respuesta JSON inesperada)" + else: + return f"🔴 Caído (API Status: {response.status_code})" + except requests.RequestException as e: + return f"🔴 Caído (Error de red: {e})" + except json.JSONDecodeError: + return f"🔴 Caído (No se pudo decodificar JSON)" + + +def get_gemini_status(status_url): + incidents_url = "https://status.cloud.google.com/incidents.json" + try: + response = requests.get(incidents_url, timeout=8) + if response.status_code == 200: + incidents = response.json() + gemini_incident = any('Vertex AI' in i.get('service_name', '') and i.get('end') is None for i in incidents) + if not gemini_incident: + return "🟢 OK (Sin incidentes reportados para Vertex AI)" + else: + return "🟡 Advertencia (Incidente activo en Vertex AI)" + else: + return f"🔴 Caído ({response.status_code})" + except requests.RequestException as e: + return f"🔴 Caído (Error: {e})" + + +def human_state(code): + if code == 200: + return f"🟢 OK ({code})" + if code in (301, 302, 307, 308, 401, 403, 404): + return f"🟡 Advertencia ({code})" + return f"🔴 Caído ({code})" + +# --- Lógica Principal --- + +def build_section(diccionario): + salida = {} + for nombre, url_or_ip in diccionario.items(): + # <-- CAMBIO: Añadida la lógica para el ping del VPS + if nombre == 'vps_soul23': + status_message = check_vps_ping(url_or_ip) + salida[f"{nombre}_url"] = url_or_ip # Guardamos la IP + elif nombre in STATUSPAGE_SERVICES: + status_message = get_statuspage_status(url_or_ip) + elif nombre == 'google_gemini': + status_message = get_gemini_status(url_or_ip) + elif nombre == 'formbricks': + status_message = check_formbricks_health(url_or_ip) + else: + status = check_url(url_or_ip) + salida[f"{nombre}_url"] = url_or_ip + salida[f"{nombre}_status"] = status + salida[f"{nombre}_state"] = human_state(status) + continue + + # Asignación para los casos especiales + salida[f"{nombre}_url"] = url_or_ip + salida[f"{nombre}_status"] = status_message + salida[f"{nombre}_state"] = status_message + return salida + + +def main(): + if not WEBHOOK_URLS: + print("⚠️ Advertencia: La variable de entorno WEBHOOK_URLS no está configurada o está vacía.") + + resultado = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "internos": build_section(INTERNOS), + "externos": build_section(EXTERNOS) + } + + print("\n--- Enviando a Webhooks ---") + for url in WEBHOOK_URLS: + try: + requests.post(url, json=resultado, timeout=10) + print(f"✅ Resultado enviado exitosamente a: {url}") + except requests.RequestException as e: + print(f"❌ Error al enviar al webhook {url}: {e}") + + print("\n--- Payload Enviado ---") + print(json.dumps(resultado, indent=4)) + + +if __name__ == "__main__": + main() \ No newline at end of file