import { Request } from 'express'; const IPV4_REGEX = /^(?:\d{1,3}\.){3}\d{1,3}$/; const normalizeIpValue = (value: string): string | null => { if (!value) return null; const trimmed = value.trim(); if (!trimmed) return null; const lower = trimmed.toLowerCase(); if (lower.startsWith('::ffff:')) { const candidate = lower.slice(7); if (IPV4_REGEX.test(candidate)) { return candidate; } } return trimmed; }; const isIpv4 = (value: string): boolean => { if (!value) return false; if (!IPV4_REGEX.test(value)) return false; return value.split('.').every((part) => Number.parseInt(part, 10) <= 255); }; const extractHeaderIps = (value?: string | string[]): string[] => { if (!value) return []; const raw = Array.isArray(value) ? value.join(',') : value; return raw .split(',') .map((entry) => entry.trim()) .filter(Boolean); }; export function extractClientIp(req: Request): string { const headersToCheck = [ 'cf-connecting-ip', 'x-forwarded-for', 'x-real-ip', ] as const; let firstIpv4: string | undefined; let fallbackIp: string | undefined; for (const header of headersToCheck) { const entries = extractHeaderIps(req.headers[header]); for (const entry of entries) { const normalized = normalizeIpValue(entry); if (!normalized) continue; if (!fallbackIp) { fallbackIp = normalized; } if (!firstIpv4 && isIpv4(normalized)) { firstIpv4 = normalized; break; } } if (firstIpv4) { break; } } const socketIp = req.socket?.remoteAddress && normalizeIpValue(req.socket.remoteAddress); if (!fallbackIp && socketIp) { fallbackIp = socketIp; } if (!firstIpv4 && socketIp && isIpv4(socketIp)) { firstIpv4 = socketIp; } return firstIpv4 ?? fallbackIp ?? ''; }