UNPKG

@arcjet/ip

Version:

Arcjet utilities for finding the originating IP of a request

804 lines (802 loc) 30.5 kB
function parseXForwardedFor(value) { if (typeof value !== "string") { return []; } const forwardedIps = []; // As per MDN X-Forwarded-For Headers documentation at // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For // The `x-forwarded-for` header may return one or more IP addresses as // "client IP, proxy 1 IP, proxy 2 IP", so we want to split by the comma and // trim each item. for (const item of value.split(",")) { forwardedIps.push(item.trim()); } return forwardedIps; } function isIPv4Cidr(cidr) { return (typeof cidr === "object" && cidr !== null && "type" in cidr && typeof cidr.type === "string" && cidr.type === "v4" && "contains" in cidr && typeof cidr.contains === "function"); } function isIPv6Cidr(cidr) { return (typeof cidr === "object" && cidr !== null && "type" in cidr && typeof cidr.type === "string" && cidr.type === "v6" && "contains" in cidr && typeof cidr.contains === "function"); } function isTrustedProxy(ip, segments, proxies) { if (Array.isArray(proxies) && proxies.length > 0) { return proxies.some((proxy) => { if (typeof proxy === "string") { return proxy === ip; } if (isIPv4Tuple(segments) && isIPv4Cidr(proxy)) { return proxy.contains(segments); } if (isIPv6Tuple(segments) && isIPv6Cidr(proxy)) { return proxy.contains(segments); } return false; }); } return false; } class CIDR { // Based on CIDR matching implementation in `ipaddr.js` // Source code: // https://github.com/whitequark/ipaddr.js/blob/08c2cd41e2cb3400683cbd503f60421bfdf66921/lib/ipaddr.js#L107-L130 // // Licensed: The MIT License (MIT) // Copyright (C) 2011-2017 whitequark <whitequark@whitequark.org> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. contains(ip) { let part = 0; let shift; let cidrBits = this.bits; while (cidrBits > 0) { shift = this.partSize - cidrBits; if (shift < 0) { shift = 0; } if (ip[part] >> shift !== this.parts[part] >> shift) { return false; } cidrBits -= this.partSize; part += 1; } return true; } } class IPv4CIDR extends CIDR { type = "v4"; partSize = 8; parts; bits; constructor(parts, bits) { super(); this.bits = bits; this.parts = parts; Object.freeze(this); } contains(ip) { return super.contains(ip); } } class IPv6CIDR extends CIDR { type = "v6"; partSize = 16; parts; bits; constructor(parts, bits) { super(); this.bits = bits; this.parts = parts; Object.freeze(this); } contains(ip) { return super.contains(ip); } } function parseCIDR(cidr) { // Pre-condition: `cidr` has be verified to have at least one `/` const cidrParts = cidr.split("/"); if (cidrParts.length !== 2) { throw new Error("invalid CIDR address: must be exactly 2 parts"); } const parser = new Parser(cidrParts[0]); const maybeIPv4 = parser.readIPv4Address(); if (isIPv4Tuple(maybeIPv4)) { const bits = parseInt(cidrParts[1], 10); if (isNaN(bits) || bits < 0 || bits > 32) { throw new Error("invalid CIDR address: incorrect amount of bits"); } return new IPv4CIDR(maybeIPv4, bits); } const maybeIPv6 = parser.readIPv6Address(); if (isIPv6Tuple(maybeIPv6)) { const bits = parseInt(cidrParts[1], 10); if (isNaN(bits) || bits < 0 || bits > 128) { throw new Error("invalid CIDR address: incorrect amount of bits"); } return new IPv6CIDR(maybeIPv6, bits); } throw new Error("invalid CIDR address: could not parse IP address"); } function isCIDR(address) { return address.includes("/"); } // Converts a string that looks like a CIDR address into the corresponding class // while ignoring non-CIDR IP addresses. function parseProxy(proxy) { if (isCIDR(proxy)) { return parseCIDR(proxy); } else { return proxy; } } function isIPv4Tuple(segements) { if (typeof segements === "undefined") { return false; } return segements.length === 4; } function isIPv6Tuple(segements) { if (typeof segements === "undefined") { return false; } return segements.length === 8; } function u16FromBytes(bytes) { const u8 = new Uint8Array(bytes); return new Uint16Array(u8.buffer)[0]; } function u32FromBytes(bytes) { const u8 = new Uint8Array(bytes); return new Uint32Array(u8.buffer)[0]; } // This Parser and "is global" comparisons are a TypeScript implementation of // similar code in the Rust stdlib with only slight deviations as noted. // // We want to mirror Rust's logic as close as possible, because we'll be relying // on its implementation when we add a Wasm library to determine IPs and only // falling back to JavaScript in non-Wasm environments. // // Parser source: // https://github.com/rust-lang/rust/blob/07921b50ba6dcb5b2984a1dba039a38d85bffba2/library/core/src/net/parser.rs#L34 // Comparison source: // https://github.com/rust-lang/rust/blob/87e1447aadaa2899ff6ccabe1fa669eb50fb60a1/library/core/src/net/ip_addr.rs#L749 // https://github.com/rust-lang/rust/blob/87e1447aadaa2899ff6ccabe1fa669eb50fb60a1/library/core/src/net/ip_addr.rs#L1453 // // Licensed: The MIT License (MIT) // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: The above copyright // notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. class Parser { state; constructor(input) { this.state = input; } readAtomically(inner) { const state = this.state; const result = inner(this); if (typeof result === "undefined") { this.state = state; } return result; } peakChar() { return this.state[0]; } readChar() { const b = this.state[0]; this.state = this.state.slice(1); return b; } readGivenChar(target) { return this.readAtomically((p) => { const c = p.readChar(); if (c === target) { return c; } }); } readSeparator(sep, index, inner) { return this.readAtomically((p) => { if (index > 0) { const c = p.readGivenChar(sep); if (typeof c === "undefined") { return; } } return inner(p); }); } readNumber(radix, maxDigits, allowZeroPrefix = false) { return this.readAtomically((p) => { let result = 0; let digitCount = 0; const hasLeadingZero = p.peakChar() === "0"; function nextCharAsDigit() { return p.readAtomically((p) => { const c = p.readChar(); if (c) { const n = parseInt(c, radix); if (!isNaN(n)) { return n; } } }); } for (let digit = nextCharAsDigit(); digit !== undefined; digit = nextCharAsDigit()) { result = result * radix; result = result + digit; digitCount += 1; if (typeof maxDigits !== "undefined") { if (digitCount > maxDigits) { return; } } } if (digitCount === 0) { return; } else if (!allowZeroPrefix && hasLeadingZero && digitCount > 1) { return; } else { return result; } }); } readIPv4Address() { return this.readAtomically((p) => { const groups = []; for (let idx = 0; idx < 4; idx++) { const result = p.readSeparator(".", idx, (p) => { // Disallow octal number in IP string // https://tools.ietf.org/html/rfc6943#section-3.1.1 return p.readNumber(10, 3, false); }); if (result === undefined) { return; } else { groups.push(result); } } return groups; }); } readIPv6Address() { // Read a chunk of an IPv6 address into `groups`. Returns the number of // groups read, along with a bool indicating if an embedded trailing IPv4 // address was read. Specifically, read a series of colon-separated IPv6 // groups (0x0000 - 0xFFFF), with an optional trailing embedded IPv4 address const readGroups = (p, groups) => { const limit = groups.length; for (const i of groups.keys()) { // Try to read a trailing embedded IPv4 address. There must be at least // two groups left if (i < limit - 1) { const ipv4 = p.readSeparator(":", i, (p) => p.readIPv4Address()); if (isIPv4Tuple(ipv4)) { const [one, two, three, four] = ipv4; groups[i + 0] = u16FromBytes([one, two]); groups[i + 1] = u16FromBytes([three, four]); return [i + 2, true]; } } const group = p.readSeparator(":", i, (p) => p.readNumber(16, 4, true)); if (typeof group !== "undefined") { groups[i] = group; } else { return [i, false]; } } return [groups.length, false]; }; return this.readAtomically((p) => { // Read the front part of the address; either the whole thing, or up // to the first :: const head = new Uint16Array(8); const [headSize, headIPv4] = readGroups(p, head); if (headSize === 8) { return head; } // IPv4 part is not allowed before `::` if (headIPv4) { return; } // Read `::` if previous code parsed less than 8 groups. // `::` indicates one or more groups of 16 bits of zeros. if (typeof p.readGivenChar(":") === "undefined") { return; } if (typeof p.readGivenChar(":") === "undefined") { return; } // Read the back part of the address. The :: must contain at least one // set of zeroes, so our max length is 7. const tail = new Uint16Array(7); const limit = 8 - (headSize + 1); const [tailSize, _] = readGroups(p, tail.subarray(0, limit)); head.set(tail.slice(0, tailSize), 8 - tailSize); return head; }); } readPort() { return this.readAtomically((p) => { if (typeof p.readGivenChar(":") !== "undefined") { return p.readNumber(10, undefined, true); } }); } readScopeId() { return this.readAtomically((p) => { if (typeof p.readGivenChar("%") !== "undefined") { return p.readNumber(10, undefined, true); } }); } } const IPV4_BROADCAST = u32FromBytes([255, 255, 255, 255]); function isGlobalIPv4(s, proxies) { if (typeof s !== "string") { return false; } const parser = new Parser(s); const octets = parser.readIPv4Address(); if (!isIPv4Tuple(octets)) { return false; } if (isTrustedProxy(s, octets, proxies)) { return false; } // Rust doesn't check the remaining state when parsing an IPv4. However, we // want to ensure we have exactly an IP (with optionally a port), so we parse // it and then check remaining parser state. parser.readPort(); if (parser.state.length !== 0) { return false; } // "This network" if (octets[0] === 0) { return false; } // Private IPv4 address ranges if (octets[0] === 10) { return false; } if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) { return false; } if (octets[0] === 192 && octets[1] === 168) { return false; } // Loopback address if (octets[0] === 127) { return false; } // Shared range if (octets[0] === 100 && (octets[1] & 0b1100_0000) === 0b0100_0000) { return false; } // Link-local range if (octets[0] === 169 && octets[1] === 254) { return false; } // addresses reserved for future protocols (`192.0.0.0/24`) if (octets[0] === 192 && octets[1] === 0 && octets[2] === 0) { return false; } // Documentation ranges if (octets[0] === 192 && octets[1] === 0 && octets[2] === 2) { return false; } if (octets[0] === 198 && octets[1] === 51 && octets[2] === 100) { return false; } if (octets[0] === 203 && octets[1] === 0 && octets[2] === 113) { return false; } // Benchmarking range if (octets[0] === 198 && (octets[1] & 0xfe) === 18) { return false; } const isBroadcast = u32FromBytes(octets) === IPV4_BROADCAST; // Reserved range if ((octets[0] & 240) === 240 && !isBroadcast) { return false; } // Broadcast address if (isBroadcast) { return false; } for (const octet of octets) { if (octet < 0 || octet > 255) { return false; } } return true; } function isGlobalIPv6(s, proxies) { if (typeof s !== "string") { return false; } const parser = new Parser(s); const segments = parser.readIPv6Address(); if (!isIPv6Tuple(segments)) { return false; } if (isTrustedProxy(s, segments, proxies)) { return false; } // Rust doesn't check the remaining state when parsing an IPv6. However, we // want to ensure we have exactly an IP (with optionally a scope id), so we // parse it and then check remaining parser state. // TODO: We don't support an IPv6 address with a port because that seems to // require wrapping the address and scope in `[]`, e.g. `[:ffff%1]:8080` parser.readScopeId(); if (parser.state.length !== 0) { return false; } // Unspecified address if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 0) { return false; } // Loopback address if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 0x1) { return false; } // IPv4-mapped Address (`::ffff:0:0/96`) if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0xffff) { return false; } // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) if (segments[0] === 0x64 && segments[1] === 0xff9b && segments[2] === 1) { return false; } // Discard-Only Address Block (`100::/64`) if (segments[0] === 0x100 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0) { return false; } // IETF Protocol Assignments (`2001::/23`) if (segments[0] === 0x2001 && segments[1] < 0x200) { // Port Control Protocol Anycast (`2001:1::1`) if (segments[0] === 0x2001 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 1) { return true; } // Traversal Using Relays around NAT Anycast (`2001:1::2`) if (segments[0] === 0x2001 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 2) { return true; } // AMT (`2001:3::/32`) if (segments[0] === 0x2001 && segments[1] === 3) { return true; } // AS112-v6 (`2001:4:112::/48`) if (segments[0] === 0x2001 && segments[1] === 4 && segments[2] === 0x112) { return true; } // ORCHIDv2 (`2001:20::/28`) if (segments[0] === 0x2001 && segments[1] >= 0x20 && segments[1] <= 0x2f) { return true; } // Benchmarking range (and others) return false; } // Documentation range if (segments[0] === 0x2001 && segments[1] === 0xdb8) { return false; } // Unique local range if ((segments[0] & 0xfe00) === 0xfc00) { return false; } // Unicast link local range if ((segments[0] & 0xffc0) === 0xfe80) { return false; } return true; } function isGlobalIP(s, proxies) { if (isGlobalIPv4(s, proxies)) { return true; } if (isGlobalIPv6(s, proxies)) { return true; } return false; } function isHeaders(val) { return typeof val.get === "function"; } function getHeader(headers, headerKey) { if (isHeaders(headers)) { return headers.get(headerKey); } else { const headerValue = headers[headerKey]; if (Array.isArray(headerValue)) { return headerValue.join(","); } else { return headerValue; } } } // Heavily based on https://github.com/pbojinov/request-ip // // Licensed: The MIT License (MIT) Copyright (c) 2022 Petar Bojinov - // petarbojinov+github@gmail.com // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: The above copyright // notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. function findIP(request, options = {}) { const { platform, proxies } = options; // Prefer anything available via the platform over headers since headers can // be set by users. Only if we don't have an IP available in `request` do we // search the `headers`. if (isGlobalIP(request.ip, proxies)) { return request.ip; } const socketRemoteAddress = request.socket?.remoteAddress; if (isGlobalIP(socketRemoteAddress, proxies)) { return socketRemoteAddress; } const infoRemoteAddress = request.info?.remoteAddress; if (isGlobalIP(infoRemoteAddress, proxies)) { return infoRemoteAddress; } // AWS Api Gateway + Lambda const requestContextIdentitySourceIP = request.requestContext?.identity?.sourceIp; if (isGlobalIP(requestContextIdentitySourceIP, proxies)) { return requestContextIdentitySourceIP; } // Validate we have some object for `request.headers` if (typeof request.headers !== "object" || request.headers === null) { return ""; } // Platform-specific headers should only be accepted when we can determine // that we are running on that platform. For example, the `CF-Connecting-IP` // header should only be accepted when running on Cloudflare; otherwise, it // can be spoofed. if (platform === "cloudflare") { // CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 const cfConnectingIPv6 = getHeader(request.headers, "cf-connecting-ipv6"); if (isGlobalIPv6(cfConnectingIPv6, proxies)) { return cfConnectingIPv6; } // CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip const cfConnectingIP = getHeader(request.headers, "cf-connecting-ip"); if (isGlobalIP(cfConnectingIP, proxies)) { return cfConnectingIP; } // If we are using a platform check and don't have a Global IP, we exit // early with an empty IP since the more generic headers shouldn't be // trusted over the platform-specific headers. return ""; } // Fly.io: https://fly.io/docs/machines/runtime-environment/#fly_app_name if (platform === "fly-io") { // Fly-Client-IP: https://fly.io/docs/networking/request-headers/#fly-client-ip const flyClientIP = getHeader(request.headers, "fly-client-ip"); if (isGlobalIP(flyClientIP, proxies)) { return flyClientIP; } // If we are using a platform check and don't have a Global IP, we exit // early with an empty IP since the more generic headers shouldn't be // trusted over the platform-specific headers. return ""; } if (platform === "vercel") { // https://vercel.com/docs/edge-network/headers/request-headers#x-real-ip // Also used by `@vercel/functions`, see: // https://github.com/vercel/vercel/blob/d7536d52c87712b1b3f83e4b0fd535a1fb7e384c/packages/functions/src/headers.ts#L12 const xRealIP = getHeader(request.headers, "x-real-ip"); if (isGlobalIP(xRealIP, proxies)) { return xRealIP; } // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-forwarded-for // By default, it seems this will be 1 address, but they discuss trusted // proxy forwarding so we try to parse it like normal. See // https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip const xVercelForwardedFor = getHeader(request.headers, "x-vercel-forwarded-for"); const xVercelForwardedForItems = parseXForwardedFor(xVercelForwardedFor); // As per MDN X-Forwarded-For Headers documentation at // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For // We may find more than one IP in the `x-forwarded-for` header. Since the // first IP will be closest to the user (and the most likely to be spoofed), // we want to iterate tail-to-head so we reverse the list. for (const item of xVercelForwardedForItems.reverse()) { if (isGlobalIP(item, proxies)) { return item; } } // https://vercel.com/docs/edge-network/headers/request-headers#x-forwarded-for // By default, it seems this will be 1 address, but they discuss trusted // proxy forwarding so we try to parse it like normal. See // https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip const xForwardedFor = getHeader(request.headers, "x-forwarded-for"); const xForwardedForItems = parseXForwardedFor(xForwardedFor); // As per MDN X-Forwarded-For Headers documentation at // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For // We may find more than one IP in the `x-forwarded-for` header. Since the // first IP will be closest to the user (and the most likely to be spoofed), // we want to iterate tail-to-head so we reverse the list. for (const item of xForwardedForItems.reverse()) { if (isGlobalIP(item, proxies)) { return item; } } // If we are using a platform check and don't have a Global IP, we exit // early with an empty IP since the more generic headers shouldn't be // trusted over the platform-specific headers. return ""; } if (platform === "render") { // True-Client-IP: https://community.render.com/t/what-number-of-proxies-sit-in-front-of-an-express-app-deployed-on-render/35981/2 const trueClientIP = getHeader(request.headers, "true-client-ip"); if (isGlobalIP(trueClientIP, proxies)) { return trueClientIP; } // If we are using a platform check and don't have a Global IP, we exit // early with an empty IP since the more generic headers shouldn't be // trusted over the platform-specific headers. return ""; } // Standard headers used by Amazon EC2, Heroku, and others. const xClientIP = getHeader(request.headers, "x-client-ip"); if (isGlobalIP(xClientIP, proxies)) { return xClientIP; } // Load-balancers (AWS ELB) or proxies. const xForwardedFor = getHeader(request.headers, "x-forwarded-for"); const xForwardedForItems = parseXForwardedFor(xForwardedFor); // As per MDN X-Forwarded-For Headers documentation at // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For // We may find more than one IP in the `x-forwarded-for` header. Since the // first IP will be closest to the user (and the most likely to be spoofed), // we want to iterate tail-to-head so we reverse the list. for (const item of xForwardedForItems.reverse()) { if (isGlobalIP(item, proxies)) { return item; } } // DigitalOcean. // DO-Connecting-IP: https://www.digitalocean.com/community/questions/app-platform-client-ip const doConnectingIP = getHeader(request.headers, "do-connecting-ip"); if (isGlobalIP(doConnectingIP, proxies)) { return doConnectingIP; } // Fastly and Firebase hosting header (When forwared to cloud function) // Fastly-Client-IP const fastlyClientIP = getHeader(request.headers, "fastly-client-ip"); if (isGlobalIP(fastlyClientIP, proxies)) { return fastlyClientIP; } // Akamai // True-Client-IP const trueClientIP = getHeader(request.headers, "true-client-ip"); if (isGlobalIP(trueClientIP, proxies)) { return trueClientIP; } // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies // X-Real-IP const xRealIP = getHeader(request.headers, "x-real-ip"); if (isGlobalIP(xRealIP, proxies)) { return xRealIP; } // Rackspace LB and Riverbed's Stingray? const xClusterClientIP = getHeader(request.headers, "x-cluster-client-ip"); if (isGlobalIP(xClusterClientIP, proxies)) { return xClusterClientIP; } const xForwarded = getHeader(request.headers, "x-forwarded"); if (isGlobalIP(xForwarded, proxies)) { return xForwarded; } const forwardedFor = getHeader(request.headers, "forwarded-for"); if (isGlobalIP(forwardedFor, proxies)) { return forwardedFor; } const forwarded = getHeader(request.headers, "forwarded"); if (isGlobalIP(forwarded, proxies)) { return forwarded; } // Google Cloud App Engine // X-Appengine-User-IP: https://cloud.google.com/appengine/docs/standard/reference/request-headers?tab=node.js const xAppEngineUserIP = getHeader(request.headers, "x-appengine-user-ip"); if (isGlobalIP(xAppEngineUserIP, proxies)) { return xAppEngineUserIP; } return ""; } export { findIP as default, parseProxy };