#!/usr/bin/env python3 """ Cisco MAC Monitor HTTPS Service =============================== Servicio HTTPS que expone la funcionalidad de monitorización de MACs vía REST API. Mantiene todos los logs informativos pero devuelve solo JSON limpio en las respuestas. Usa POST para credenciales seguras y soporte SSL/TLS. Endpoints: POST /mac-monitor - Body JSON con credenciales GET /health - Health check GET /metrics - Métricas del servicio Autor: Sistema de Monitorización de Red Fecha: Noviembre 2024 """ import json import logging import sys import ssl import os from datetime import datetime from typing import Dict, Any from urllib.parse import parse_qs, urlparse from http.server import HTTPServer, BaseHTTPRequestHandler import threading import signal import time # Importar las clases existentes del cisco_mac_monitor from cisco_mac_monitor import CiscoSSHConnector, CiscoMacTableParser, ZabbixLLDFormatter class CiscoMacHTTPHandler(BaseHTTPRequestHandler): """Handler HTTP para el servicio de monitorización MAC""" def __init__(self, *args, **kwargs): self.logger = logging.getLogger(__name__) super().__init__(*args, **kwargs) def do_GET(self): """Maneja peticiones GET - Solo endpoints públicos""" try: if self.path == '/health' or self.path == '/': self._handle_health() elif self.path == '/metrics': self._handle_metrics() elif self.path.startswith('/mac-monitor'): self._send_error(405, "Método no permitido. Use POST con JSON body para /mac-monitor") else: self._send_error(404, "Endpoint no encontrado") except Exception as e: self.logger.error(f"Error procesando petición GET: {str(e)}") self._send_error(500, f"Error interno: {str(e)}") def do_POST(self): """Maneja peticiones POST - Endpoint seguro de monitorización""" try: if self.path == '/mac-monitor': self._handle_mac_monitor_post() else: self._send_error(404, "Endpoint no encontrado") except Exception as e: self.logger.error(f"Error procesando petición POST: {str(e)}") self._send_error(500, f"Error interno: {str(e)}") def _handle_mac_monitor_post(self): """Maneja el endpoint POST de monitorización MAC con JSON body""" try: # Leer Content-Length content_length = int(self.headers.get('Content-Length', 0)) if content_length == 0: self._send_error(400, "Body JSON requerido") return # Leer body JSON post_data = self.rfile.read(content_length) try: json_data = json.loads(post_data.decode('utf-8')) except json.JSONDecodeError as e: self._send_error(400, f"JSON inválido: {str(e)}") return # Validar parámetros requeridos required_fields = ['host', 'username', 'password'] missing_fields = [] for field in required_fields: if field not in json_data or not json_data[field]: missing_fields.append(field) if missing_fields: self._send_error(400, f"Campos faltantes en JSON: {', '.join(missing_fields)}") return # Extraer parámetros host = json_data['host'] username = json_data['username'] password = json_data['password'] hostname = json_data.get('hostname', host) port = int(json_data.get('port', 22)) timeout = int(json_data.get('timeout', 30)) self.logger.info(f"Iniciando monitorización para {host}") # Ejecutar monitorización result = self._execute_mac_monitoring(host, username, password, hostname, port, timeout) if result: self._send_json_response(result) self.logger.info(f"Monitorización completada para {host}") else: self._send_error(500, "Error obteniendo datos del dispositivo") except ValueError as e: self._send_error(400, f"Parámetro inválido: {str(e)}") except Exception as e: self.logger.error(f"Error en monitorización POST: {str(e)}") self._send_error(500, f"Error de monitorización: {str(e)}") def _execute_mac_monitoring(self, host: str, username: str, password: str, hostname: str, port: int, timeout: int) -> Dict[str, Any]: """Ejecuta el proceso de monitorización MAC""" # Inicializar componentes ssh_connector = CiscoSSHConnector( host=host, username=username, password=password, port=port, timeout=timeout ) mac_parser = CiscoMacTableParser() zabbix_formatter = ZabbixLLDFormatter(hostname) try: # Conectar al dispositivo if not ssh_connector.connect(): self.logger.error(f"No se pudo establecer conexión SSH con {host}") return None # Obtener tabla MAC mac_entries = mac_parser.get_mac_table(ssh_connector) if not mac_entries: self.logger.warning(f"No se obtuvieron entradas de tabla MAC de {host}") # Obtener la lista de interfaces detectadas all_interfaces = getattr(mac_parser, 'detected_interfaces', []) # Formatear para Zabbix incluyendo todas las interfaces zabbix_data = zabbix_formatter.format_for_zabbix(mac_entries, all_interfaces) return zabbix_data finally: ssh_connector.disconnect() def _handle_health(self): """Endpoint de health check""" health_data = { "status": "healthy", "service": "cisco-mac-monitor-https", "timestamp": datetime.now().isoformat(), "version": "2.1-https", "ssl_enabled": hasattr(self.server, 'socket') and isinstance(self.server.socket, ssl.SSLSocket) } self._send_json_response(health_data) def _handle_metrics(self): """Endpoint básico de métricas""" metrics_data = { "service": "cisco-mac-monitor-https", "uptime_seconds": time.time() - start_time, "requests_total": getattr(self.server, 'request_count', 0), "timestamp": datetime.now().isoformat(), "ssl_enabled": hasattr(self.server, 'socket') and isinstance(self.server.socket, ssl.SSLSocket) } self._send_json_response(metrics_data) def _send_json_response(self, data: Dict[str, Any]): """Envía respuesta JSON""" json_data = json.dumps(data, indent=2, ensure_ascii=False) self.send_response(200) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Content-Length', len(json_data.encode('utf-8'))) self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json_data.encode('utf-8')) def _send_error(self, code: int, message: str): """Envía respuesta de error""" error_data = { "error": message, "code": code, "timestamp": datetime.now().isoformat() } json_data = json.dumps(error_data, indent=2, ensure_ascii=False) self.send_response(code) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Content-Length', len(json_data.encode('utf-8'))) self.end_headers() self.wfile.write(json_data.encode('utf-8')) self.logger.warning(f"Error {code}: {message}") def log_message(self, format, *args): """Sobrescribir para evitar logs HTTP automáticos""" # Los logs HTTP automáticos se manejan mediante nuestro logger pass class CiscoMacHTTPSServer: """Servidor HTTPS para el servicio de monitorización MAC""" def __init__(self, host='0.0.0.0', port=8443, ssl_cert=None, ssl_key=None): self.host = host self.port = port self.ssl_cert = ssl_cert self.ssl_key = ssl_key self.httpd = None self.server_thread = None self.logger = logging.getLogger(__name__) self.running = False def _generate_self_signed_cert(self): """Genera certificado autofirmado si no se proporciona""" cert_file = '/tmp/cisco_mac_server.crt' key_file = '/tmp/cisco_mac_server.key' if os.path.exists(cert_file) and os.path.exists(key_file): self.logger.info("Usando certificado autofirmado existente") return cert_file, key_file self.logger.info("Generando certificado autofirmado...") try: import subprocess # Generar key privada result = subprocess.run([ 'openssl', 'genrsa', '-out', key_file, '2048' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: self.logger.error(f"Error generando key: {result.stderr.decode()}") return None, None # Generar certificado autofirmado result = subprocess.run([ 'openssl', 'req', '-new', '-x509', '-key', key_file, '-out', cert_file, '-days', '365', '-subj', '/C=ES/ST=Madrid/L=Madrid/O=CiscoMonitor/CN=cisco-mac-monitor' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: self.logger.error(f"Error generando certificado: {result.stderr.decode()}") return None, None # Cambiar permisos os.chmod(key_file, 0o600) os.chmod(cert_file, 0o644) self.logger.info(f"Certificado autofirmado generado: {cert_file}") return cert_file, key_file except subprocess.CalledProcessError as e: self.logger.error(f"Error generando certificado: {e}") return None, None except Exception as e: self.logger.error(f"Error en subprocess: {e}") return None, None except FileNotFoundError: self.logger.warning("OpenSSL no disponible, funcionando en modo HTTP") return None, None def start(self): """Inicia el servidor HTTPS""" try: self.httpd = HTTPServer((self.host, self.port), CiscoMacHTTPHandler) self.httpd.request_count = 0 # Configurar SSL si está disponible ssl_enabled = False if self.ssl_cert and self.ssl_key: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(self.ssl_cert, self.ssl_key) self.httpd.socket = ssl_context.wrap_socket(self.httpd.socket, server_side=True) ssl_enabled = True self.logger.info(f"SSL habilitado con certificados personalizados") else: # Intentar generar certificado autofirmado cert_file, key_file = self._generate_self_signed_cert() if cert_file and key_file: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(cert_file, key_file) self.httpd.socket = ssl_context.wrap_socket(self.httpd.socket, server_side=True) ssl_enabled = True self.logger.info(f"SSL habilitado con certificado autofirmado") protocol = "https" if ssl_enabled else "http" self.logger.info(f"Iniciando servidor {protocol.upper()} en {self.host}:{self.port}") # Configurar shutdown graceful signal.signal(signal.SIGTERM, self._signal_handler) signal.signal(signal.SIGINT, self._signal_handler) self.running = True self.logger.info(f"Servidor disponible en {protocol}://{self.host}:{self.port}") self.logger.info("Endpoints disponibles:") self.logger.info(f" POST /mac-monitor - Body JSON con credenciales") self.logger.info(f" GET /health") self.logger.info(f" GET /metrics") if ssl_enabled: self.logger.info("⚠️ Usando certificado autofirmado - ignorar warnings SSL") # Iniciar servidor self.httpd.serve_forever() except Exception as e: self.logger.error(f"Error iniciando servidor: {str(e)}") raise def stop(self): """Detiene el servidor HTTP""" if self.httpd and self.running: self.logger.info("Deteniendo servidor HTTP...") self.running = False self.httpd.shutdown() self.httpd.server_close() self.logger.info("Servidor HTTP detenido") def _signal_handler(self, signum, frame): """Maneja señales de sistema para shutdown graceful""" self.logger.info(f"Recibida señal {signum}, deteniendo servidor...") self.stop() def setup_logging(debug: bool = False) -> logging.Logger: """Configura el sistema de logging""" level = logging.DEBUG if debug else logging.INFO # Formato de logging formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) # Handler para stderr handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) # Configurar logger principal logger = logging.getLogger() logger.setLevel(level) logger.addHandler(handler) # Reducir verbosidad de paramiko si no está en debug if not debug: logging.getLogger('paramiko').setLevel(logging.WARNING) return logging.getLogger(__name__) def main(): """Función principal""" import argparse parser = argparse.ArgumentParser( description='Cisco MAC Monitor HTTPS Service', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Ejemplos de uso: %(prog)s --host 0.0.0.0 --port 8443 %(prog)s --host 127.0.0.1 --port 8443 --debug %(prog)s --ssl-cert /path/to/cert.pem --ssl-key /path/to/key.pem Endpoints: POST /mac-monitor - Body JSON: {"host": "192.168.1.10", "username": "admin", "password": "cisco123"} GET /health GET /metrics Ejemplo de petición: curl -k -X POST "https://localhost:8443/mac-monitor" \ -H "Content-Type: application/json" \ -d '{"host":"192.168.1.10","username":"admin","password":"cisco123"}' """ ) parser.add_argument('--host', default='0.0.0.0', help='IP de escucha del servidor (default: 0.0.0.0)') parser.add_argument('--port', '-p', type=int, default=8443, help='Puerto de escucha del servidor (default: 8443)') parser.add_argument('--ssl-cert', help='Archivo de certificado SSL (opcional)') parser.add_argument('--ssl-key', help='Archivo de clave privada SSL (opcional)') parser.add_argument('--debug', '-d', action='store_true', help='Habilitar modo debug') args = parser.parse_args() # Configurar logging logger = setup_logging(args.debug) # Variable global para métricas global start_time start_time = time.time() # Crear y iniciar servidor server = CiscoMacHTTPSServer( host=args.host, port=args.port, ssl_cert=args.ssl_cert, ssl_key=args.ssl_key ) try: server.start() except KeyboardInterrupt: logger.info("Interrupción por teclado recibida") except Exception as e: logger.error(f"Error fatal: {str(e)}") sys.exit(1) finally: server.stop() if __name__ == '__main__': main()