import { isIP } from 'net' import type { NextRequest } from 'next/server' const DIRECT_IP_HEADERS = [ 'cf-connecting-ip', 'true-client-ip', 'fly-client-ip', 'fastly-client-ip' ] const FALLBACK_IP_HEADERS = [ 'x-real-ip', 'x-client-ip' ] const LIST_IP_HEADERS = [ 'x-forwarded-for', 'x-original-forwarded-for' ] export function getClientIp(req: NextRequest) { return getClientIpFromHeaders(req.headers) } export function getClientIpFromHeaders(headers: Headers) { const candidates: string[] = [] for (const header of DIRECT_IP_HEADERS) { const value = headers.get(header) if (value) { candidates.push(value) } } const forwarded = headers.get('forwarded') if (forwarded) { candidates.push(...parseForwardedHeader(forwarded)) } for (const header of LIST_IP_HEADERS) { const value = headers.get(header) if (value) { candidates.push(...value.split(',')) } } for (const header of FALLBACK_IP_HEADERS) { const value = headers.get(header) if (value) { candidates.push(value) } } for (const candidate of candidates) { const ip = normalizeIpCandidate(candidate) if (ip) { return ip } } return null } export function normalizeIpCandidate(value?: string | null) { if (!value) { return null } let candidate = value.trim() if (!candidate || candidate.toLowerCase() === 'unknown') { return null } if (candidate.startsWith('"') && candidate.endsWith('"')) { candidate = candidate.slice(1, -1) } if (candidate.startsWith('[')) { const closingBracketIndex = candidate.indexOf(']') if (closingBracketIndex > 0) { candidate = candidate.slice(1, closingBracketIndex) } } else { const colonCount = (candidate.match(/:/g) || []).length if (colonCount === 1 && candidate.includes('.')) { candidate = candidate.slice(0, candidate.lastIndexOf(':')) } } const zoneIndex = candidate.indexOf('%') if (zoneIndex > -1) { candidate = candidate.slice(0, zoneIndex) } candidate = candidate.trim() if (candidate.toLowerCase().startsWith('::ffff:')) { const ipv4 = candidate.slice(7) if (isIP(ipv4) === 4) { return ipv4 } } return isIP(candidate) ? candidate : null } export function isPrivateIp(ip: string) { const normalizedIp = normalizeIpCandidate(ip) if (!normalizedIp) { return false } if (isIP(normalizedIp) === 4) { const [first, second] = normalizedIp.split('.').map(Number) return first === 10 || first === 127 || first === 0 || (first === 172 && second >= 16 && second <= 31) || (first === 192 && second === 168) || (first === 169 && second === 254) || (first === 100 && second >= 64 && second <= 127) } const lowerIp = normalizedIp.toLowerCase() return lowerIp === '::1' || lowerIp === '::' || lowerIp.startsWith('fc') || lowerIp.startsWith('fd') || lowerIp.startsWith('fe8') || lowerIp.startsWith('fe9') || lowerIp.startsWith('fea') || lowerIp.startsWith('feb') } function parseForwardedHeader(value: string) { const candidates: string[] = [] for (const entry of value.split(',')) { for (const segment of entry.split(';')) { const [key, rawValue] = segment.split('=') if (key?.trim().toLowerCase() === 'for' && rawValue) { candidates.push(rawValue.trim()) } } } return candidates }