UNPKG

get-client-ip

Version:

📍 A Lightweight Utility for Extracting the Real Client IP Address from Incoming HTTP Requests

1 lines 13 kB
{"version":3,"sources":["../src/index.ts"],"names":["isIP"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { IncomingHttpHeaders } from \"node:http\";\nimport { isIP } from \"node:net\";\n\ntype NonEmptyArray<T> = [T, ...T[]];\n\n/** Minimal request interface — any object with headers and socket (Express v4, v5, etc.). */\nexport interface IpRequest {\n ip?: string;\n headers: IncomingHttpHeaders;\n socket?: { remoteAddress?: string };\n clientIp?: string;\n clientIps?: NonEmptyArray<string>;\n}\n\nfunction $isIP(ip: unknown): ip is string {\n return typeof ip === \"string\" && isIP(ip) !== 0;\n}\n\n/** Checks if an IP is a recognized private/proxy address. CGNAT (100.64.0.0/10, RFC 6598) is excluded — it's shared ISP address space, not a trusted local proxy. */\nfunction $isTrustedProxyAddress(ip: string): boolean {\n const ipVersion = isIP(ip);\n\n if (ipVersion === 4) {\n const [a, b] = ip.split(\".\").map((part) => Number.parseInt(part, 10));\n const secondOctet = b ?? -1;\n if (a === 10) return true;\n if (a === 127) return true;\n if (a === 169 && secondOctet === 254) return true;\n if (a === 192 && secondOctet === 168) return true;\n if (a === 172 && secondOctet >= 16 && secondOctet <= 31) return true;\n return false;\n }\n\n if (ipVersion === 6) {\n const lower = ip.toLowerCase();\n return (\n lower === \"::1\" ||\n lower.startsWith(\"fe80:\") ||\n lower.startsWith(\"fc\") ||\n lower.startsWith(\"fd\") ||\n lower.startsWith(\"::ffff:10.\") ||\n lower.startsWith(\"::ffff:127.\") ||\n lower.startsWith(\"::ffff:169.254.\") ||\n lower.startsWith(\"::ffff:192.168.\") ||\n /^::ffff:172\\.(1[6-9]|2\\d|3[0-1])\\./.test(lower)\n );\n }\n\n return false;\n}\n\nfunction $stripZoneId(ip: string): string {\n const idx = ip.indexOf(\"%\");\n return idx === -1 ? ip : ip.slice(0, idx);\n}\n\nfunction $normalizeIpCandidate(candidate: string): string | null {\n const trimmed = candidate.trim();\n if (!trimmed) return null;\n\n const unquoted = trimmed.replace(/^\"(.*)\"$/, \"$1\");\n\n const bracketed = unquoted.match(/^\\[([^\\]]+)\\](?::\\d+)?$/);\n if (bracketed?.[1]) {\n const ip = $stripZoneId(bracketed[1]);\n if (isIP(ip) !== 0) return ip;\n }\n\n const stripped = $stripZoneId(unquoted);\n if (isIP(stripped) !== 0) return stripped;\n\n const ipv4WithPort = unquoted.match(/^([^:]+):(\\d+)$/);\n if (ipv4WithPort?.[1] && isIP(ipv4WithPort[1]) === 4) return ipv4WithPort[1];\n\n return null;\n}\n\n// e.g. Forwarded: for=192.0.2.43;proto=http, for=\"[2001:db8:cafe::17]\"\nfunction $extractForwarded(value: string | string[]): NonEmptyArray<string> | null {\n const lines = Array.isArray(value) ? value : [value];\n const ips: string[] = [];\n\n for (const line of lines) {\n for (const segment of line.split(\",\")) {\n for (const directive of segment.split(\";\")) {\n const [rawKey, rawVal] = directive.split(\"=\", 2);\n if (!rawKey || !rawVal) continue;\n if (rawKey.trim().toLowerCase() !== \"for\") continue;\n const ip = $normalizeIpCandidate(rawVal);\n if (ip) ips.push(ip);\n }\n }\n }\n\n return ips.length > 0 ? (ips as NonEmptyArray<string>) : null;\n}\n\nfunction $extractHeaderIps(headerValue: string | string[]): NonEmptyArray<string> | null {\n const values = Array.isArray(headerValue) ? headerValue : [headerValue];\n const ips: string[] = [];\n\n for (const value of values) {\n for (const token of value.split(\",\")) {\n const ip = $normalizeIpCandidate(token);\n if (ip) ips.push(ip);\n }\n }\n\n return ips.length > 0 ? (ips as NonEmptyArray<string>) : null;\n}\n\n// CDN-injected headers first (high trust — overwritten by the CDN on every request),\n// then generic forwarding headers (lower trust — forwarded as-is from the client).\nconst LOOKUP_HEADERS = [\n \"cf-connecting-ip\",\n \"true-client-ip\",\n \"fastly-client-ip\",\n \"x-appengine-user-ip\",\n \"cf-pseudo-ipv4\",\n \"x-client-ip\",\n \"x-forwarded-for\",\n \"forwarded-for\",\n \"x-forwarded\",\n \"x-real-ip\",\n \"x-cluster-client-ip\",\n];\n\nfunction $extractIpFromHeaders(req: IpRequest): NonEmptyArray<string> | null {\n if ($isIP(req.ip)) return [req.ip];\n\n if (!req.headers) return null;\n\n const remoteAddress = req.socket?.remoteAddress;\n if (!$isIP(remoteAddress) || !$isTrustedProxyAddress(remoteAddress)) {\n return null;\n }\n\n if (typeof req.headers.forwarded === \"string\" || Array.isArray(req.headers.forwarded)) {\n const forwardedIps = $extractForwarded(req.headers.forwarded);\n if (forwardedIps) return forwardedIps;\n }\n\n for (const header of LOOKUP_HEADERS) {\n const ip = req.headers[header];\n if (!ip || !(typeof ip === \"string\" || Array.isArray(ip))) continue;\n\n const extractedIps = $extractHeaderIps(ip);\n if (extractedIps) return extractedIps;\n }\n\n return null;\n}\n\n// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Needed for Express middleware signature\n// biome-ignore-start lint/suspicious/noExplicitAny: Needed for Express next function\n\n/**\n * Extracts the client's IP address from an incoming Express request.\n *\n * This function works both as a standalone utility and as Express middleware.\n * It attempts to detect the IP by inspecting common proxy-related headers\n * such as `forwarded` (RFC 7239), `x-forwarded-for`, `x-real-ip`, and others. If no valid IP is found\n * in the headers, it falls back to `req.socket.remoteAddress`.\n * Header fallback is skipped when `req.socket.remoteAddress` is a public address,\n * reducing spoofing risk for direct internet-facing connections.\n *\n * When used as middleware, it populates:\n * - `req.clientIp`: The first valid IP address found.\n * - `req.clientIps`: A non-empty array of all valid IPs found.\n *\n * **Security note — trust boundary:** This function checks `req.ip` first (which\n * respects Express's `trust proxy` setting). When `req.ip` is falsy, it\n * reads forwarding headers **only** if the socket peer (`req.socket.remoteAddress`)\n * is a recognized private/proxy address. When the socket identity is absent or\n * public, headers are **not** trusted and the function falls back to\n * `req.socket.remoteAddress` directly. In production behind a reverse proxy,\n * configure Express's `trust proxy` so `req.ip` is populated correctly and\n * treated as the primary source of truth.\n *\n * @param req - The Express request object.\n * @param res - (Optional) The Express response object. Included to support middleware signature.\n * @param next - (Optional) The next function in the Express middleware chain.\n *\n * @returns The first detected client IP address as a string, or `undefined` if none is found.\n *\n * @throws Will throw an error if the `req` argument is not defined.\n *\n * @example\n * // Standalone usage:\n * app.get('/standalone-ip', (req, res) => {\n * const ip = getClientIp(req);\n * res.status(200).json({ ip });\n * });\n *\n * @example\n * // Middleware usage:\n * app.use(getClientIp);\n * app.get('/middleware-ip', (req, res) => {\n * res.status(200).json({ ip: req.clientIp, ips: req.clientIps });\n * });\n */\nexport function getClientIp(req: IpRequest, res?: unknown, next?: (...args: any[]) => void): string | undefined {\n if (!req) throw new Error(\"Request is undefined\");\n\n const ips = $extractIpFromHeaders(req);\n if (ips) {\n req.clientIp = ips[0];\n req.clientIps = ips;\n next?.();\n return ips[0];\n }\n\n const remoteAddress = req.socket?.remoteAddress;\n if ($isIP(remoteAddress)) {\n req.clientIp = remoteAddress;\n req.clientIps = [remoteAddress];\n next?.();\n return remoteAddress;\n }\n\n next?.();\n}\n\n// biome-ignore-end lint/correctness/noUnusedFunctionParameters: Needed for Express middleware signature\n// biome-ignore-end lint/suspicious/noExplicitAny: Needed for Express next function\n\ndeclare global {\n namespace Express {\n export interface Request {\n /** The first IP address extracted from the request. */\n clientIp?: string;\n /** All IP addresses extracted from the request. */\n clientIps?: NonEmptyArray<string>;\n }\n }\n}\n"]}