UNPKG

@grec0/mcp-s2s-asterisk

Version:

MCP server para asistente telefónico conversacional con Asterisk S2S

255 lines (254 loc) 8.04 kB
// 🔐 Middleware de Autenticación para MCP // Configuración de autenticación const authConfig = { apiKey: process.env.MCP_CALLBACK_API_KEY || 'mcp-default-key', allowedIps: process.env.MCP_ALLOWED_IPS?.split(',') || [], jwtSecret: process.env.MCP_JWT_SECRET || 'mcp-secret-key', tokenExpiration: parseInt(process.env.MCP_TOKEN_EXPIRATION || '3600') // 1 hora por defecto }; /** * Middleware para validar API Key */ export function validateApiKey(req, res, next) { const apiKey = req.headers['x-mcp-api-key']; if (!apiKey) { res.status(401).json({ error: 'API Key requerida', code: 'MISSING_API_KEY' }); return; } if (apiKey !== authConfig.apiKey) { res.status(403).json({ error: 'API Key inválida', code: 'INVALID_API_KEY' }); return; } next(); } /** * Middleware para validar IP permitidas */ export function validateAllowedIps(req, res, next) { // Si no hay IPs configuradas, permitir todas if (!authConfig.allowedIps || authConfig.allowedIps.length === 0) { next(); return; } const clientIp = getClientIp(req); if (!authConfig.allowedIps.includes(clientIp)) { console.warn(`🚫 IP no autorizada: ${clientIp}`); res.status(403).json({ error: 'IP no autorizada', code: 'FORBIDDEN_IP', ip: clientIp }); return; } console.log(`✅ IP autorizada: ${clientIp}`); next(); } /** * Middleware combinado para autenticación completa */ export function authenticate(req, res, next) { // Log de la petición console.log(`🔍 Auth check: ${req.method} ${req.path} from ${getClientIp(req)}`); // Validar IP primero validateAllowedIps(req, res, (ipError) => { if (ipError) return; // Luego validar API Key validateApiKey(req, res, (keyError) => { if (keyError) return; // Log de autenticación exitosa console.log(`🔓 Autenticación exitosa para ${req.path}`); next(); }); }); } /** * Middleware para validar que la petición viene del asistente realtime */ export function validateRealtimeAssistant(req, res, next) { const userAgent = req.headers['user-agent']; const expectedAgent = 'RealtimeAssistant'; if (!userAgent || !userAgent.includes(expectedAgent)) { console.warn(`🚫 User-Agent inválido: ${userAgent}`); res.status(403).json({ error: 'Petición no autorizada', code: 'INVALID_USER_AGENT' }); return; } next(); } /** * Middleware para rate limiting básico */ const requestCounts = new Map(); export function rateLimit(maxRequests = 10, windowMs = 60000) { return (req, res, next) => { const clientIp = getClientIp(req); const now = Date.now(); // Limpiar contadores expirados for (const [ip, data] of requestCounts.entries()) { if (now > data.resetTime) { requestCounts.delete(ip); } } // Obtener o crear contador para esta IP let ipData = requestCounts.get(clientIp); if (!ipData || now > ipData.resetTime) { ipData = { count: 0, resetTime: now + windowMs }; requestCounts.set(clientIp, ipData); } // Incrementar contador ipData.count++; // Verificar límite if (ipData.count > maxRequests) { console.warn(`🚫 Rate limit excedido para IP: ${clientIp}`); res.status(429).json({ error: 'Demasiadas peticiones', code: 'RATE_LIMIT_EXCEEDED', retryAfter: Math.ceil((ipData.resetTime - now) / 1000) }); return; } // Agregar headers informativos res.set({ 'X-RateLimit-Limit': maxRequests.toString(), 'X-RateLimit-Remaining': (maxRequests - ipData.count).toString(), 'X-RateLimit-Reset': new Date(ipData.resetTime).toISOString() }); next(); }; } /** * Middleware para logging de requests */ export function requestLogger(req, res, next) { const start = Date.now(); const clientIp = getClientIp(req); const userAgent = req.headers['user-agent'] || 'Unknown'; console.log(`📥 ${req.method} ${req.path} - IP: ${clientIp} - UA: ${userAgent}`); // Log de respuesta res.on('finish', () => { const duration = Date.now() - start; const statusEmoji = res.statusCode >= 400 ? '❌' : '✅'; console.log(`📤 ${statusEmoji} ${res.statusCode} ${req.method} ${req.path} - ${duration}ms`); }); next(); } /** * Middleware para validar formato JSON */ export function validateJsonPayload(req, res, next) { if (req.method === 'POST' || req.method === 'PUT') { const contentType = req.headers['content-type']; if (!contentType || !contentType.includes('application/json')) { res.status(400).json({ error: 'Content-Type debe ser application/json', code: 'INVALID_CONTENT_TYPE' }); return; } // Verificar que el body fue parseado correctamente if (req.body === undefined) { res.status(400).json({ error: 'Body JSON inválido', code: 'INVALID_JSON' }); return; } } next(); } /** * Middleware para manejo de errores */ export function errorHandler(err, req, res, next) { console.error(`💥 Error en ${req.method} ${req.path}:`, err); // Error de sintaxis JSON if (err instanceof SyntaxError && 'body' in err) { res.status(400).json({ error: 'JSON inválido en el body', code: 'INVALID_JSON_SYNTAX' }); return; } // Error genérico res.status(500).json({ error: 'Error interno del servidor', code: 'INTERNAL_ERROR', message: process.env.NODE_ENV === 'development' ? err.message : 'Error interno' }); } /** * Obtener la IP real del cliente */ function getClientIp(req) { return (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown'); } /** * Middleware para CORS básico */ export function enableCors(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, X-MCP-API-Key'); if (req.method === 'OPTIONS') { res.sendStatus(200); return; } next(); } /** * Obtener configuración de autenticación actual */ export function getAuthConfig() { return { ...authConfig }; } /** * Actualizar configuración de autenticación */ export function updateAuthConfig(newConfig) { Object.assign(authConfig, newConfig); console.log('🔧 Configuración de autenticación actualizada'); } /** * Validar token JWT (para uso futuro) */ export function validateJwtToken(token) { try { // Aquí iría la lógica de validación JWT // Por ahora retornamos válido para desarrollo return { valid: true, payload: { sub: 'realtime-assistant' } }; } catch (error) { return { valid: false }; } } /** * Limpiar datos de rate limiting antiguos (ejecutar periódicamente) */ export function cleanupRateLimitData() { const now = Date.now(); let cleaned = 0; for (const [ip, data] of requestCounts.entries()) { if (now > data.resetTime) { requestCounts.delete(ip); cleaned++; } } if (cleaned > 0) { console.log(`🧹 Limpieza de rate limiting: ${cleaned} entradas eliminadas`); } } // Ejecutar limpieza cada 5 minutos setInterval(cleanupRateLimitData, 5 * 60 * 1000);