Path: blob/master/src/packages/util/get-client-ip-address.ts
5818 views
import { isIP } from "net";1import { getClientIp } from "request-ip";23export function getClientIpAddress(req: {4headers: Record<string, string | string[] | undefined>;5}): string | undefined {6// Try manual extraction for headers not supported by request-ip7const headersToCheck = [8"cf-connecting-ip", // prioritize cloudflare9"x-client-ip",10"x-forwarded-for",11"fastly-client-ip",12"true-client-ip",13"x-real-ip",14"x-cluster-client-ip",15"appengine-user-ip",16];1718// Check each header (case-insensitive)19for (const headerName of headersToCheck) {20const headerValue = getHeaderValue(req.headers, headerName);21if (headerValue) {22// Handle comma-separated values (like X-Forwarded-For)23const ips = headerValue.split(",").map((ip) => ip.trim());24for (const ip of ips) {25const processedIp = normalizeIPAddress(ip);26if (isIP(processedIp)) {27return processedIp;28}29}30}31}3233// Try request-ip package as fallback34const ip = getClientIp(req);35if (ip && isIP(ip)) {36return ip;37}3839// Fallback "Forwarded" header parsing, because this is not merged:40// https://github.com/pbojinov/request-ip/pull/7141const forwardedHeader = getHeaderValue(req.headers, "forwarded");42if (forwardedHeader) {43// Split by comma for multiple forwarded entries, trimming each entry44const forwardedEntries = forwardedHeader.split(",").map(entry => entry.trim());4546for (const entry of forwardedEntries) {47// Split by semicolon for parameters, trimming each parameter48const params = entry.split(";").map(param => param.trim());4950for (const param of params) {51if (param.toLowerCase().startsWith("for=")) {52let ipVal = param.substring(4).trim();5354// Remove quotes if present55if (ipVal.startsWith('"') && ipVal.endsWith('"')) {56ipVal = ipVal.slice(1, -1);57}5859// Normalize IP address (remove brackets and ports)60ipVal = normalizeIPAddress(ipVal);6162if (isIP(ipVal)) {63return ipVal;64}65}66}67}68}6970return undefined;71}7273// Helper function to normalize IP address by removing brackets and ports74function normalizeIPAddress(ip: string): string {75let processedIp = ip.trim();7677// Remove IPv6 brackets if present (do this first!)78const bracketStart = processedIp.startsWith("[");79const closingBracketIndex = processedIp.indexOf("]");80const hasPortAfterBracket = closingBracketIndex > 0 && processedIp[closingBracketIndex + 1] === ":";81if (bracketStart && hasPortAfterBracket) {82// Extract IPv6 part and port: [2001:db8::1]:8080 -> 2001:db8::1:808083processedIp = processedIp.substring(1, closingBracketIndex) + processedIp.substring(closingBracketIndex + 1);84} else if (processedIp.startsWith("[") && processedIp.endsWith("]")) {85// Simple bracket removal: [2001:db8::1] -> 2001:db8::186processedIp = processedIp.slice(1, -1);87}8889// Strip port if present (handles both IPv4:port and IPv6:port)90if (processedIp.includes(":")) {91const lastColonIndex = processedIp.lastIndexOf(":");92if (lastColonIndex > 0) {93const potentialPort = processedIp.substring(lastColonIndex + 1);94// If the part after the last colon looks like a port number95if (/^\d+$/.test(potentialPort)) {96const potentialIP = processedIp.substring(0, lastColonIndex);97if (isIP(potentialIP)) {98processedIp = potentialIP;99}100}101}102}103104return processedIp;105}106107// Helper function to get header value case-insensitively108function getHeaderValue(109headers: Record<string, string | string[] | undefined>,110name: string,111): string | undefined {112const lowerName = name.toLowerCase();113114// Check exact match first115const exactMatch = headers[lowerName];116if (exactMatch) {117return Array.isArray(exactMatch) ? exactMatch[0] : exactMatch;118}119120// Check case-insensitive match121for (const [key, value] of Object.entries(headers)) {122if (key.toLowerCase() === lowerName && value) {123return Array.isArray(value) ? value[0] : value;124}125}126127return undefined;128}129130131