/** * Função utilitária para extrair o IP do cliente de um Request HTTP * Sem usar APIs externas - usa apenas headers HTTP */ /** * Extrai o IP do cliente de um Request HTTP * Considera headers como X-Forwarded-For, X-Real-IP, etc. */ export function getClientIP(request: Request): string | undefined { // Headers que podem conter o IP do cliente (case-insensitive) const getHeader = (name: string): string | null => { // Tentar diferentes variações de case const variations = [ name, name.toLowerCase(), name.toUpperCase(), name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() ]; for (const variation of variations) { const value = request.headers.get(variation); if (value) return value; } // As variações de case já cobrem a maioria dos casos // Se não encontrou, retorna null return null; }; const forwardedFor = getHeader('x-forwarded-for'); const realIP = getHeader('x-real-ip'); const cfConnectingIP = getHeader('cf-connecting-ip'); // Cloudflare const trueClientIP = getHeader('true-client-ip'); // Cloudflare Enterprise const xClientIP = getHeader('x-client-ip'); const forwarded = getHeader('forwarded'); const remoteAddr = getHeader('remote-addr'); // Log para debug console.log('Procurando IP nos headers:', { 'x-forwarded-for': forwardedFor, 'x-real-ip': realIP, 'cf-connecting-ip': cfConnectingIP, 'true-client-ip': trueClientIP, 'x-client-ip': xClientIP, forwarded: forwarded, 'remote-addr': remoteAddr }); // Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain) // O primeiro IP é geralmente o IP original do cliente if (forwardedFor) { const ips = forwardedFor.split(',').map((ip) => ip.trim()); // Pegar o primeiro IP válido for (const ip of ips) { if (isValidIP(ip)) { console.log('IP encontrado em X-Forwarded-For:', ip); return ip; } } } // Forwarded header (RFC 7239) if (forwarded) { // Formato: for=192.0.2.60;proto=http;by=203.0.113.43 const forMatch = forwarded.match(/for=([^;,\s]+)/i); if (forMatch && forMatch[1]) { const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6 if (isValidIP(ip)) { console.log('IP encontrado em Forwarded:', ip); return ip; } } } // Outros headers com IP único if (realIP && isValidIP(realIP)) { console.log('IP encontrado em X-Real-IP:', realIP); return realIP; } if (cfConnectingIP && isValidIP(cfConnectingIP)) { console.log('IP encontrado em CF-Connecting-IP:', cfConnectingIP); return cfConnectingIP; } if (trueClientIP && isValidIP(trueClientIP)) { console.log('IP encontrado em True-Client-IP:', trueClientIP); return trueClientIP; } if (xClientIP && isValidIP(xClientIP)) { console.log('IP encontrado em X-Client-IP:', xClientIP); return xClientIP; } if (remoteAddr && isValidIP(remoteAddr)) { console.log('IP encontrado em Remote-Addr:', remoteAddr); return remoteAddr; } // Tentar extrair do URL (último recurso) try { const url = new URL(request.url); // Se o servidor estiver configurado para passar IP via query param const ipFromQuery = url.searchParams.get('ip'); if (ipFromQuery && isValidIP(ipFromQuery)) { console.log('IP encontrado em query param:', ipFromQuery); return ipFromQuery; } } catch { // Ignorar erro de parsing do URL } console.log('Nenhum IP válido encontrado nos headers'); return undefined; } /** * Valida se uma string é um endereço IP válido (IPv4 ou IPv6) */ function isValidIP(ip: string): boolean { if (!ip || ip.length === 0) { return false; } // Validar IPv4 const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; if (ipv4Regex.test(ip)) { const parts = ip.split('.'); return parts.every((part) => { const num = parseInt(part, 10); return num >= 0 && num <= 255; }); } // Validar IPv6 (formato simplificado) const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; if (ipv6Regex.test(ip)) { return true; } // Validar IPv6 comprimido (com ::) const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/; if (ipv6CompressedRegex.test(ip)) { return true; } return false; }