/** * 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; }