@arcjet/ip
Advanced tools
| import type { ProxyService } from "./index.js"; | ||
| /** | ||
| * Cloudflare IPv4 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v4/ | ||
| */ | ||
| export declare const cloudflareIpv4Ranges: ReadonlyArray<string>; | ||
| /** | ||
| * Cloudflare IPv6 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v6/ | ||
| */ | ||
| export declare const cloudflareIpv6Ranges: ReadonlyArray<string>; | ||
| /** | ||
| * Configuration for {@linkcode cloudflare}. | ||
| */ | ||
| export interface CloudflareOptions { | ||
| /** | ||
| * IP addresses and CIDR ranges that identify Cloudflare | ||
| * (optional; defaults to the ranges bundled with this package). | ||
| * | ||
| * Override this only if the bundled ranges are out of date for your setup. | ||
| */ | ||
| ranges?: ReadonlyArray<string> | null | undefined; | ||
| } | ||
| /** | ||
| * Describe Cloudflare as a trusted proxy in front of your application. | ||
| * | ||
| * Pass the result in the `proxies` array. When a request reaches your platform | ||
| * from a Cloudflare IP, Arcjet will read the real client IP from the | ||
| * `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the | ||
| * Cloudflare edge address as the client. The header is only trusted when the | ||
| * connecting address is within Cloudflare's ranges, so it cannot be spoofed by | ||
| * clients connecting directly to your platform. | ||
| * | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy service descriptor to include in the `proxies` array. | ||
| */ | ||
| export declare function cloudflare(options?: CloudflareOptions | null | undefined): ProxyService; |
| // Cloudflare publishes its IP ranges at the following URLs. These change | ||
| // infrequently (roughly annually). They are bundled here so users don't have to | ||
| // fetch and paste them, and verified against the live lists by | ||
| // `scripts/verify-ranges.ts` (run on a schedule in CI). | ||
| // | ||
| // IPv4: https://www.cloudflare.com/ips-v4/ | ||
| // IPv6: https://www.cloudflare.com/ips-v6/ | ||
| // | ||
| // last verified: 2026-06-08 | ||
| /** | ||
| * Cloudflare IPv4 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v4/ | ||
| */ | ||
| const cloudflareIpv4Ranges = [ | ||
| "173.245.48.0/20", | ||
| "103.21.244.0/22", | ||
| "103.22.200.0/22", | ||
| "103.31.4.0/22", | ||
| "141.101.64.0/18", | ||
| "108.162.192.0/18", | ||
| "190.93.240.0/20", | ||
| "188.114.96.0/20", | ||
| "197.234.240.0/22", | ||
| "198.41.128.0/17", | ||
| "162.158.0.0/15", | ||
| "104.16.0.0/13", | ||
| "104.24.0.0/14", | ||
| "172.64.0.0/13", | ||
| "131.0.72.0/22", | ||
| ]; | ||
| /** | ||
| * Cloudflare IPv6 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v6/ | ||
| */ | ||
| const cloudflareIpv6Ranges = [ | ||
| "2400:cb00::/32", | ||
| "2606:4700::/32", | ||
| "2803:f800::/32", | ||
| "2405:b500::/32", | ||
| "2405:8100::/32", | ||
| "2a06:98c0::/29", | ||
| "2c0f:f248::/32", | ||
| ]; | ||
| /** | ||
| * Describe Cloudflare as a trusted proxy in front of your application. | ||
| * | ||
| * Pass the result in the `proxies` array. When a request reaches your platform | ||
| * from a Cloudflare IP, Arcjet will read the real client IP from the | ||
| * `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the | ||
| * Cloudflare edge address as the client. The header is only trusted when the | ||
| * connecting address is within Cloudflare's ranges, so it cannot be spoofed by | ||
| * clients connecting directly to your platform. | ||
| * | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy service descriptor to include in the `proxies` array. | ||
| */ | ||
| function cloudflare(options) { | ||
| // An empty `ranges` array is treated as "not provided" and falls back to the | ||
| // bundled defaults, matching how `isTrustedProxy` treats an empty `proxies` | ||
| // list. Trusting an empty list would silently disable Cloudflare detection | ||
| // (no edge address would ever match), so we never let it through. | ||
| const ranges = options && Array.isArray(options.ranges) && options.ranges.length > 0 | ||
| ? [...options.ranges] | ||
| : [...cloudflareIpv4Ranges, ...cloudflareIpv6Ranges]; | ||
| return { | ||
| kind: "service", | ||
| name: "cloudflare", | ||
| // CIDR range strings are parsed by `findIp` (where `parseProxy` lives) so | ||
| // this module stays free of a runtime dependency on `index.ts`. | ||
| ranges, | ||
| // CF-Connecting-IPv6: | ||
| // https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 | ||
| // CF-Connecting-IP: | ||
| // https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip | ||
| clientIp: [ | ||
| { header: "cf-connecting-ipv6", format: "ip" }, | ||
| { header: "cf-connecting-ip", format: "ip" }, | ||
| ], | ||
| }; | ||
| } | ||
| export { cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges }; |
+197
| type Ipv4Tuple = [number, number, number, number]; | ||
| type Ipv6Tuple = [ | ||
| number, | ||
| number, | ||
| number, | ||
| number, | ||
| number, | ||
| number, | ||
| number, | ||
| number | ||
| ]; | ||
| declare class Ipv4Cidr { | ||
| type: "v4"; | ||
| partSize: 8; | ||
| parts: Readonly<Ipv4Tuple>; | ||
| bits: number; | ||
| constructor(parts: Ipv4Tuple, bits: number); | ||
| contains(ip: Array<number>): boolean; | ||
| } | ||
| declare class Ipv6Cidr { | ||
| type: "v6"; | ||
| partSize: 16; | ||
| parts: Readonly<Ipv6Tuple>; | ||
| bits: number; | ||
| constructor(parts: Ipv6Tuple, bits: number); | ||
| contains(ip: Array<number>): boolean; | ||
| } | ||
| /** | ||
| * Parse CIDR addresses and keep non-CIDR IP addresses. | ||
| * | ||
| * @param value | ||
| * Value to parse. | ||
| * @returns | ||
| * Parsed {@linkcode Cidr} if range or given `value` if IP. | ||
| */ | ||
| export declare function parseProxy(value: string): string | Cidr; | ||
| /** | ||
| * Parse a list of trusted proxies. | ||
| * | ||
| * CIDR range strings are parsed to {@linkcode Cidr} (so they match by range); | ||
| * plain IP strings and {@linkcode ProxyService} objects (such as those created | ||
| * by {@linkcode cloudflare}) are passed through unchanged. Use this to | ||
| * normalize the `proxies` option before handing it to {@linkcode findIp}. | ||
| * | ||
| * @param proxies | ||
| * Trusted proxies to parse. | ||
| * @returns | ||
| * Parsed proxies. | ||
| */ | ||
| export declare function parseProxies(proxies: ReadonlyArray<string | ProxyService>): Array<string | Cidr | ProxyService>; | ||
| /** | ||
| * Socket-like interface. | ||
| */ | ||
| interface PartialSocket { | ||
| remoteAddress?: string | null | undefined; | ||
| } | ||
| /** | ||
| * Interface that looks like info. | ||
| */ | ||
| interface PartialInfo { | ||
| remoteAddress?: string | null | undefined; | ||
| } | ||
| interface PartialIdentiy { | ||
| sourceIp?: string | null | undefined; | ||
| } | ||
| /** | ||
| * Interface that looks like a request context. | ||
| */ | ||
| interface PartialRequestContext { | ||
| identity?: PartialIdentiy | null | undefined; | ||
| } | ||
| /** | ||
| * Interface with `headers`. | ||
| */ | ||
| export type HeaderLike = { | ||
| /** | ||
| * Headers. | ||
| */ | ||
| headers: Headers | Record<string, string[] | string | undefined>; | ||
| }; | ||
| /** | ||
| * Interface that looks like a request, | ||
| * of which `headers` is required and several other fields may exist. | ||
| */ | ||
| export type RequestLike = { | ||
| /** | ||
| * Some platforms pass `info`. | ||
| */ | ||
| info?: PartialInfo | null | undefined; | ||
| /** | ||
| * Some platforms such as Cloudflare and Vercel provide `ip` directly on | ||
| * `request`. | ||
| */ | ||
| ip?: unknown; | ||
| /** | ||
| * Some platforms pass info in `requestContext`. | ||
| */ | ||
| requestContext?: PartialRequestContext | null | undefined; | ||
| /** | ||
| * Some platforms pass a `socket`. | ||
| */ | ||
| socket?: PartialSocket | null | undefined; | ||
| } & HeaderLike; | ||
| /** | ||
| * Platform name. | ||
| */ | ||
| export type Platform = "cloudflare" | "firebase" | "fly-io" | "render" | "vercel"; | ||
| /** | ||
| * Format of a client IP header set by a proxy service. | ||
| * | ||
| * - `"ip"` — a single IP address (e.g. `CF-Connecting-IP`). | ||
| * - `"ips"` — an `X-Forwarded-For`-style comma separated list, parsed | ||
| * tail-to-head. | ||
| */ | ||
| export type ClientIpFormat = "ip" | "ips"; | ||
| /** | ||
| * A client IP header set by a proxy service. | ||
| */ | ||
| export interface ClientIpHeader { | ||
| /** | ||
| * Header name (lower-case). | ||
| */ | ||
| header: string; | ||
| /** | ||
| * How to parse the header value. | ||
| */ | ||
| format: ClientIpFormat; | ||
| } | ||
| /** | ||
| * A trusted proxy service (such as Cloudflare) sitting in front of the | ||
| * application. | ||
| * | ||
| * Identified by IP range, with the header(s) that carry the real client IP. | ||
| * Create one with a helper such as {@linkcode cloudflare} and include it in the | ||
| * `proxies` array. | ||
| */ | ||
| export interface ProxyService { | ||
| /** | ||
| * Discriminant marking this entry as a proxy service. | ||
| */ | ||
| kind: "service"; | ||
| /** | ||
| * Name of the service (such as `"cloudflare"`). | ||
| */ | ||
| name: string; | ||
| /** | ||
| * IP addresses and CIDR ranges that identify this service. | ||
| */ | ||
| ranges: ReadonlyArray<string | Cidr>; | ||
| /** | ||
| * Header(s) this service uses to relay the real client IP, in priority order. | ||
| */ | ||
| clientIp: ReadonlyArray<ClientIpHeader>; | ||
| } | ||
| /** | ||
| * Configuration. | ||
| */ | ||
| export interface Options { | ||
| /** | ||
| * Platform the code is running on; | ||
| * used to allow only known more trustworthy headers. | ||
| */ | ||
| platform?: Platform | null | undefined; | ||
| /** | ||
| * Trusted proxies. | ||
| * | ||
| * IP addresses and CIDR ranges are treated as trusted load balancers or | ||
| * proxies and skipped when finding the client IP. Proxy services created with | ||
| * a helper such as {@linkcode cloudflare} additionally declare which header | ||
| * carries the real client IP. | ||
| */ | ||
| proxies?: ReadonlyArray<string | Cidr | ProxyService> | null | undefined; | ||
| } | ||
| /** | ||
| * Find a client IP address on a request-like object. | ||
| * | ||
| * @param request | ||
| * Request-like object. | ||
| * @param [options] | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Found IP address; empty string if not found. | ||
| */ | ||
| export declare function findIp(request: RequestLike, options?: Options | null | undefined): string; | ||
| /** | ||
| * One of the CIDR ranges. | ||
| */ | ||
| export type Cidr = Ipv4Cidr | Ipv6Cidr; | ||
| export { cloudflare } from "./cloudflare.js"; | ||
| export type { CloudflareOptions } from "./cloudflare.js"; | ||
| /** | ||
| * Find an IP address. | ||
| * | ||
| * @deprecated | ||
| * Use the named export `findIp` instead. | ||
| */ | ||
| export default findIp; |
+940
| export { cloudflare } from './cloudflare.js'; | ||
| 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 isProxyService(value) { | ||
| return (typeof value === "object" && | ||
| value !== null && | ||
| "kind" in value && | ||
| value.kind === "service"); | ||
| } | ||
| // Resolve a candidate IP found in a (trusted) header or platform field. | ||
| // | ||
| // Returns `undefined` when the candidate is not a usable global IP, so callers | ||
| // can continue to the next candidate exactly as before. When services are | ||
| // configured and the candidate is the edge address of one of them, the real | ||
| // client IP is read from that service's declared header(s) instead. | ||
| // | ||
| // The service header carries the same trust level as the candidate it is read | ||
| // from. For platform- or socket-attested candidates (`request.ip`, | ||
| // `socket.remoteAddress`, the platform-specific header branches) that means the | ||
| // service header cannot be spoofed by a client connecting directly. For | ||
| // candidates pulled from a generic header walk (e.g. the `x-forwarded-for` | ||
| // fallback when no `platform` is set) it carries no more trust than the | ||
| // surrounding header walk already does — a direct client can forge both. | ||
| function resolveCandidate(candidate, services, proxies, headers) { | ||
| if (!isGlobalIp(candidate, proxies)) { | ||
| return undefined; | ||
| } | ||
| if (services.length === 0) { | ||
| return candidate; | ||
| } | ||
| // The candidate already passed `isGlobalIp` above, so it is a routable IP. | ||
| // It is therefore the edge address of a service exactly when it falls inside | ||
| // that service's ranges, i.e. when treating those ranges as proxies makes it | ||
| // no longer "global". This reuses the same matching as trusted proxies. | ||
| const service = services.find((service) => !isGlobalIp(candidate, service.ranges)); | ||
| if (!service) { | ||
| return candidate; | ||
| } | ||
| // The candidate is a known service edge but we can't read its headers, so we | ||
| // can't recover the real client IP. Skip it rather than returning the proxy. | ||
| if (typeof headers !== "object" || headers === null) { | ||
| return undefined; | ||
| } | ||
| for (const { header, format } of service.clientIp) { | ||
| const value = getHeader(headers, header); | ||
| // `parseXForwardedFor` trims each item; mirror that for the single-IP | ||
| // format so a header value padded with whitespace still parses. | ||
| const items = format === "ip" | ||
| ? [typeof value === "string" ? value.trim() : value] | ||
| : parseXForwardedFor(value).reverse(); | ||
| for (const item of items) { | ||
| // The client IP from a service header is the real client, so we don't | ||
| // resolve services again (avoiding loops); only filter trusted proxies. | ||
| if (isGlobalIp(item, proxies)) { | ||
| return item; | ||
| } | ||
| } | ||
| } | ||
| // The candidate is a known service edge but none of its headers held a usable | ||
| // client IP. Skip it rather than returning the proxy address. | ||
| return undefined; | ||
| } | ||
| 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; | ||
| } | ||
| // 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. | ||
| function cidrContains(cidr, ip) { | ||
| let part = 0; | ||
| let shift; | ||
| let cidrBits = cidr.bits; | ||
| while (cidrBits > 0) { | ||
| shift = cidr.partSize - cidrBits; | ||
| if (shift < 0) { | ||
| shift = 0; | ||
| } | ||
| if (ip[part] >> shift !== cidr.parts[part] >> shift) { | ||
| return false; | ||
| } | ||
| cidrBits -= cidr.partSize; | ||
| part += 1; | ||
| } | ||
| return true; | ||
| } | ||
| class Ipv4Cidr { | ||
| type = "v4"; | ||
| partSize = 8; | ||
| parts; | ||
| bits; | ||
| constructor(parts, bits) { | ||
| this.bits = bits; | ||
| this.parts = parts; | ||
| Object.freeze(this); | ||
| } | ||
| contains(ip) { | ||
| return cidrContains(this, ip); | ||
| } | ||
| } | ||
| class Ipv6Cidr { | ||
| type = "v6"; | ||
| partSize = 16; | ||
| parts; | ||
| bits; | ||
| constructor(parts, bits) { | ||
| this.bits = bits; | ||
| this.parts = parts; | ||
| Object.freeze(this); | ||
| } | ||
| contains(ip) { | ||
| return cidrContains(this, 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("/"); | ||
| } | ||
| /** | ||
| * Parse CIDR addresses and keep non-CIDR IP addresses. | ||
| * | ||
| * @param value | ||
| * Value to parse. | ||
| * @returns | ||
| * Parsed {@linkcode Cidr} if range or given `value` if IP. | ||
| */ | ||
| function parseProxy(value) { | ||
| if (isCidr(value)) { | ||
| return parseCidr(value); | ||
| } | ||
| else { | ||
| return value; | ||
| } | ||
| } | ||
| /** | ||
| * Parse a list of trusted proxies. | ||
| * | ||
| * CIDR range strings are parsed to {@linkcode Cidr} (so they match by range); | ||
| * plain IP strings and {@linkcode ProxyService} objects (such as those created | ||
| * by {@linkcode cloudflare}) are passed through unchanged. Use this to | ||
| * normalize the `proxies` option before handing it to {@linkcode findIp}. | ||
| * | ||
| * @param proxies | ||
| * Trusted proxies to parse. | ||
| * @returns | ||
| * Parsed proxies. | ||
| */ | ||
| function parseProxies(proxies) { | ||
| return proxies.map((proxy) => typeof proxy === "string" ? parseProxy(proxy) : 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. | ||
| /** | ||
| * Find a client IP address on a request-like object. | ||
| * | ||
| * @param request | ||
| * Request-like object. | ||
| * @param [options] | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Found IP address; empty string if not found. | ||
| */ | ||
| function findIp(request, options) { | ||
| const { platform, proxies: rawProxies } = options || {}; | ||
| const proxies = []; | ||
| const services = []; | ||
| if (Array.isArray(rawProxies)) { | ||
| for (const cidrOrIp of rawProxies) { | ||
| if (typeof cidrOrIp === "string") { | ||
| proxies.push(parseProxy(cidrOrIp)); | ||
| } | ||
| if (isIpv4Cidr(cidrOrIp) || isIpv6Cidr(cidrOrIp)) { | ||
| proxies.push(cidrOrIp); | ||
| } | ||
| if (isProxyService(cidrOrIp)) { | ||
| // Parse any CIDR range strings to `Cidr` so they match by range rather | ||
| // than exact string equality. | ||
| const ranges = []; | ||
| for (const range of cidrOrIp.ranges) { | ||
| ranges.push(typeof range === "string" ? parseProxy(range) : range); | ||
| } | ||
| services.push({ ...cidrOrIp, ranges }); | ||
| } | ||
| } | ||
| } | ||
| // 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`. | ||
| const ipFromRequest = resolveCandidate(request.ip, services, proxies, request.headers); | ||
| if (ipFromRequest) { | ||
| return ipFromRequest; | ||
| } | ||
| const socketRemoteAddress = resolveCandidate(request.socket?.remoteAddress, services, proxies, request.headers); | ||
| if (socketRemoteAddress) { | ||
| return socketRemoteAddress; | ||
| } | ||
| const infoRemoteAddress = resolveCandidate(request.info?.remoteAddress, services, proxies, request.headers); | ||
| if (infoRemoteAddress) { | ||
| return infoRemoteAddress; | ||
| } | ||
| // AWS Api Gateway + Lambda | ||
| const requestContextIdentitySourceIp = resolveCandidate(request.requestContext?.identity?.sourceIp, services, proxies, request.headers); | ||
| if (requestContextIdentitySourceIp) { | ||
| 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 ""; | ||
| } | ||
| // Firebase https://github.com/arcjet/arcjet-js/issues/5383 | ||
| if (platform === "firebase") { | ||
| const fahClientIp = resolveCandidate(getHeader(request.headers, "x-fah-client-ip"), services, proxies, request.headers); | ||
| if (fahClientIp) { | ||
| return fahClientIp; | ||
| } | ||
| // https://cloud.google.com/functions/docs/reference/headers#x-forwarded-for | ||
| // (and https://github.com/arcjet/arcjet-js/issues/5383). | ||
| // The last are probably going to be proxies which have to be filtered with | ||
| // `proxies`. | ||
| const xForwardedFor = getHeader(request.headers, "x-forwarded-for"); | ||
| const xForwardedForItems = parseXForwardedFor(xForwardedFor); | ||
| for (const item of xForwardedForItems.reverse()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) { | ||
| return resolved; | ||
| } | ||
| } | ||
| // 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 = resolveCandidate(getHeader(request.headers, "fly-client-ip"), services, proxies, request.headers); | ||
| if (flyClientIp) { | ||
| 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 = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers); | ||
| if (xRealIp) { | ||
| 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()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) { | ||
| return resolved; | ||
| } | ||
| } | ||
| // 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()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) { | ||
| return resolved; | ||
| } | ||
| } | ||
| // 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 = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers); | ||
| if (trueClientIp) { | ||
| 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 ""; | ||
| } | ||
| // 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()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) { | ||
| return resolved; | ||
| } | ||
| } | ||
| // Standard headers used by Amazon EC2, Heroku, and others. | ||
| const xClientIp = resolveCandidate(getHeader(request.headers, "x-client-ip"), services, proxies, request.headers); | ||
| if (xClientIp) { | ||
| return xClientIp; | ||
| } | ||
| // DigitalOcean. | ||
| // DO-Connecting-IP: https://www.digitalocean.com/community/questions/app-platform-client-ip | ||
| const doConnectingIp = resolveCandidate(getHeader(request.headers, "do-connecting-ip"), services, proxies, request.headers); | ||
| if (doConnectingIp) { | ||
| return doConnectingIp; | ||
| } | ||
| // Fastly and Firebase hosting header (When forwared to cloud function) | ||
| // Fastly-Client-IP | ||
| const fastlyClientIp = resolveCandidate(getHeader(request.headers, "fastly-client-ip"), services, proxies, request.headers); | ||
| if (fastlyClientIp) { | ||
| return fastlyClientIp; | ||
| } | ||
| // Akamai | ||
| // True-Client-IP | ||
| const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers); | ||
| if (trueClientIp) { | ||
| return trueClientIp; | ||
| } | ||
| // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies | ||
| // X-Real-IP | ||
| const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers); | ||
| if (xRealIp) { | ||
| return xRealIp; | ||
| } | ||
| // Rackspace LB and Riverbed's Stingray? | ||
| const xClusterClientIp = resolveCandidate(getHeader(request.headers, "x-cluster-client-ip"), services, proxies, request.headers); | ||
| if (xClusterClientIp) { | ||
| return xClusterClientIp; | ||
| } | ||
| const xForwarded = resolveCandidate(getHeader(request.headers, "x-forwarded"), services, proxies, request.headers); | ||
| if (xForwarded) { | ||
| return xForwarded; | ||
| } | ||
| const forwardedFor = resolveCandidate(getHeader(request.headers, "forwarded-for"), services, proxies, request.headers); | ||
| if (forwardedFor) { | ||
| return forwardedFor; | ||
| } | ||
| const forwarded = resolveCandidate(getHeader(request.headers, "forwarded"), services, proxies, request.headers); | ||
| if (forwarded) { | ||
| 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 = resolveCandidate(getHeader(request.headers, "x-appengine-user-ip"), services, proxies, request.headers); | ||
| if (xAppEngineUserIp) { | ||
| return xAppEngineUserIp; | ||
| } | ||
| return ""; | ||
| } | ||
| export { findIp as default, findIp, parseProxies, parseProxy }; |
+31
-40
| { | ||
| "name": "@arcjet/ip", | ||
| "version": "1.7.0-rc.1", | ||
| "version": "1.7.0", | ||
| "description": "Arcjet utilities for finding the originating IP of a request", | ||
@@ -8,6 +8,12 @@ "keywords": [ | ||
| "ip", | ||
| "util", | ||
| "utility" | ||
| "utility", | ||
| "util" | ||
| ], | ||
| "license": "Apache-2.0", | ||
| "homepage": "https://arcjet.com", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/arcjet/arcjet-js.git", | ||
| "directory": "ip" | ||
| }, | ||
| "bugs": { | ||
@@ -17,3 +23,2 @@ "url": "https://github.com/arcjet/arcjet-js/issues", | ||
| }, | ||
| "license": "Apache-2.0", | ||
| "author": { | ||
@@ -24,39 +29,21 @@ "name": "Arcjet", | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/arcjet/arcjet-js.git", | ||
| "directory": "ip" | ||
| "engines": { | ||
| "node": ">=22.21.0 <23 || >=24.5.0" | ||
| }, | ||
| "type": "module", | ||
| "main": "./index.js", | ||
| "types": "./index.d.ts", | ||
| "files": [ | ||
| "dist" | ||
| "cloudflare.d.ts", | ||
| "cloudflare.js", | ||
| "index.d.ts", | ||
| "index.js" | ||
| ], | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "./cloudflare": { | ||
| "types": "./dist/cloudflare.d.ts", | ||
| "default": "./dist/cloudflare.js" | ||
| }, | ||
| "./cloudflare.js": { | ||
| "types": "./dist/cloudflare.d.ts", | ||
| "default": "./dist/cloudflare.js" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "tag": "latest" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsdown", | ||
| "typecheck": "tsgo --noEmit", | ||
| "build": "rollup --config rollup.config.js", | ||
| "generate": "npm run build && node scripts/verify-ranges.ts --write", | ||
| "test-api": "node --test -- test/*.test.ts", | ||
| "test-coverage": "node --experimental-test-coverage --test --test-coverage-branches=100 --test-coverage-exclude \"test/**/*.{js,ts}\" --test-coverage-functions=100 --test-coverage-lines=100 -- test/*.test.ts", | ||
| "test": "npm run build && npm run test-coverage", | ||
| "lint": "eslint .", | ||
| "test-api": "node --test -- test/*.test.js", | ||
| "test-coverage": "node --experimental-test-coverage --test --test-coverage-branches=100 --test-coverage-exclude \"test/**/*.{js,ts}\" --test-coverage-functions=100 --test-coverage-lines=100 -- test/*.test.js", | ||
| "test": "npm run build && npm run lint && npm run test-coverage", | ||
| "verify-ranges": "npm run build && node scripts/verify-ranges.ts" | ||
@@ -66,9 +53,13 @@ }, | ||
| "devDependencies": { | ||
| "@arcjet/eslint-config": "1.7.0", | ||
| "@arcjet/rollup-config": "1.7.0", | ||
| "@rollup/wasm-node": "4.62.2", | ||
| "@types/node": "22.19.21", | ||
| "tsdown": "0.22.3", | ||
| "typescript": "6.0.3" | ||
| "eslint": "9.39.4", | ||
| "typescript": "5.9.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22.21.0 <23 || >=24.5.0" | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "tag": "latest" | ||
| } | ||
| } |
| import { ProxyService } from "./index.js"; | ||
| //#region src/cloudflare.d.ts | ||
| /** | ||
| * Cloudflare IPv4 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v4/ | ||
| */ | ||
| declare const cloudflareIpv4Ranges: ReadonlyArray<string>; | ||
| /** | ||
| * Cloudflare IPv6 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v6/ | ||
| */ | ||
| declare const cloudflareIpv6Ranges: ReadonlyArray<string>; | ||
| /** | ||
| * Configuration for {@linkcode cloudflare}. | ||
| */ | ||
| interface CloudflareOptions { | ||
| /** | ||
| * IP addresses and CIDR ranges that identify Cloudflare | ||
| * (optional; defaults to the ranges bundled with this package). | ||
| * | ||
| * Override this only if the bundled ranges are out of date for your setup. | ||
| */ | ||
| ranges?: ReadonlyArray<string> | null | undefined; | ||
| } | ||
| /** | ||
| * Describe Cloudflare as a trusted proxy in front of your application. | ||
| * | ||
| * Pass the result in the `proxies` array. When a request reaches your platform | ||
| * from a Cloudflare IP, Arcjet will read the real client IP from the | ||
| * `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the | ||
| * Cloudflare edge address as the client. The header is only trusted when the | ||
| * connecting address is within Cloudflare's ranges, so it cannot be spoofed by | ||
| * clients connecting directly to your platform. | ||
| * | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy service descriptor to include in the `proxies` array. | ||
| */ | ||
| declare function cloudflare(options?: CloudflareOptions | null | undefined): ProxyService; | ||
| //#endregion | ||
| export { CloudflareOptions, cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges }; |
| //#region src/cloudflare.ts | ||
| /** | ||
| * Cloudflare IPv4 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v4/ | ||
| */ | ||
| const cloudflareIpv4Ranges = [ | ||
| "173.245.48.0/20", | ||
| "103.21.244.0/22", | ||
| "103.22.200.0/22", | ||
| "103.31.4.0/22", | ||
| "141.101.64.0/18", | ||
| "108.162.192.0/18", | ||
| "190.93.240.0/20", | ||
| "188.114.96.0/20", | ||
| "197.234.240.0/22", | ||
| "198.41.128.0/17", | ||
| "162.158.0.0/15", | ||
| "104.16.0.0/13", | ||
| "104.24.0.0/14", | ||
| "172.64.0.0/13", | ||
| "131.0.72.0/22" | ||
| ]; | ||
| /** | ||
| * Cloudflare IPv6 ranges. | ||
| * | ||
| * Source: https://www.cloudflare.com/ips-v6/ | ||
| */ | ||
| const cloudflareIpv6Ranges = [ | ||
| "2400:cb00::/32", | ||
| "2606:4700::/32", | ||
| "2803:f800::/32", | ||
| "2405:b500::/32", | ||
| "2405:8100::/32", | ||
| "2a06:98c0::/29", | ||
| "2c0f:f248::/32" | ||
| ]; | ||
| /** | ||
| * Describe Cloudflare as a trusted proxy in front of your application. | ||
| * | ||
| * Pass the result in the `proxies` array. When a request reaches your platform | ||
| * from a Cloudflare IP, Arcjet will read the real client IP from the | ||
| * `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the | ||
| * Cloudflare edge address as the client. The header is only trusted when the | ||
| * connecting address is within Cloudflare's ranges, so it cannot be spoofed by | ||
| * clients connecting directly to your platform. | ||
| * | ||
| * @param options | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Proxy service descriptor to include in the `proxies` array. | ||
| */ | ||
| function cloudflare(options) { | ||
| return { | ||
| kind: "service", | ||
| name: "cloudflare", | ||
| ranges: options && Array.isArray(options.ranges) && options.ranges.length > 0 ? [...options.ranges] : [...cloudflareIpv4Ranges, ...cloudflareIpv6Ranges], | ||
| clientIp: [{ | ||
| header: "cf-connecting-ipv6", | ||
| format: "ip" | ||
| }, { | ||
| header: "cf-connecting-ip", | ||
| format: "ip" | ||
| }] | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges }; |
-184
| import { CloudflareOptions, cloudflare } from "./cloudflare.js"; | ||
| //#region src/index.d.ts | ||
| type Ipv4Tuple = [number, number, number, number]; | ||
| type Ipv6Tuple = [number, number, number, number, number, number, number, number]; | ||
| declare class Ipv4Cidr { | ||
| type: "v4"; | ||
| partSize: 8; | ||
| parts: Readonly<Ipv4Tuple>; | ||
| bits: number; | ||
| constructor(parts: Ipv4Tuple, bits: number); | ||
| contains(ip: Array<number>): boolean; | ||
| } | ||
| declare class Ipv6Cidr { | ||
| type: "v6"; | ||
| partSize: 16; | ||
| parts: Readonly<Ipv6Tuple>; | ||
| bits: number; | ||
| constructor(parts: Ipv6Tuple, bits: number); | ||
| contains(ip: Array<number>): boolean; | ||
| } | ||
| /** | ||
| * Parse CIDR addresses and keep non-CIDR IP addresses. | ||
| * | ||
| * @param value | ||
| * Value to parse. | ||
| * @returns | ||
| * Parsed {@linkcode Cidr} if range or given `value` if IP. | ||
| */ | ||
| declare function parseProxy(value: string): string | Cidr; | ||
| /** | ||
| * Parse a list of trusted proxies. | ||
| * | ||
| * CIDR range strings are parsed to {@linkcode Cidr} (so they match by range); | ||
| * plain IP strings and {@linkcode ProxyService} objects (such as those created | ||
| * by {@linkcode cloudflare}) are passed through unchanged. Use this to | ||
| * normalize the `proxies` option before handing it to {@linkcode findIp}. | ||
| * | ||
| * @param proxies | ||
| * Trusted proxies to parse. | ||
| * @returns | ||
| * Parsed proxies. | ||
| */ | ||
| declare function parseProxies(proxies: ReadonlyArray<string | ProxyService>): Array<string | Cidr | ProxyService>; | ||
| /** | ||
| * Socket-like interface. | ||
| */ | ||
| interface PartialSocket { | ||
| remoteAddress?: string | null | undefined; | ||
| } | ||
| /** | ||
| * Interface that looks like info. | ||
| */ | ||
| interface PartialInfo { | ||
| remoteAddress?: string | null | undefined; | ||
| } | ||
| interface PartialIdentiy { | ||
| sourceIp?: string | null | undefined; | ||
| } | ||
| /** | ||
| * Interface that looks like a request context. | ||
| */ | ||
| interface PartialRequestContext { | ||
| identity?: PartialIdentiy | null | undefined; | ||
| } | ||
| /** | ||
| * Interface with `headers`. | ||
| */ | ||
| type HeaderLike = { | ||
| /** | ||
| * Headers. | ||
| */ | ||
| headers: Headers | Record<string, string[] | string | undefined>; | ||
| }; | ||
| /** | ||
| * Interface that looks like a request, | ||
| * of which `headers` is required and several other fields may exist. | ||
| */ | ||
| type RequestLike = { | ||
| /** | ||
| * Some platforms pass `info`. | ||
| */ | ||
| info?: PartialInfo | null | undefined; | ||
| /** | ||
| * Some platforms such as Cloudflare and Vercel provide `ip` directly on | ||
| * `request`. | ||
| */ | ||
| ip?: unknown; | ||
| /** | ||
| * Some platforms pass info in `requestContext`. | ||
| */ | ||
| requestContext?: PartialRequestContext | null | undefined; | ||
| /** | ||
| * Some platforms pass a `socket`. | ||
| */ | ||
| socket?: PartialSocket | null | undefined; | ||
| } & HeaderLike; | ||
| /** | ||
| * Platform name. | ||
| */ | ||
| type Platform = "cloudflare" | "firebase" | "fly-io" | "render" | "vercel"; | ||
| /** | ||
| * Format of a client IP header set by a proxy service. | ||
| * | ||
| * - `"ip"` — a single IP address (e.g. `CF-Connecting-IP`). | ||
| * - `"ips"` — an `X-Forwarded-For`-style comma separated list, parsed | ||
| * tail-to-head. | ||
| */ | ||
| type ClientIpFormat = "ip" | "ips"; | ||
| /** | ||
| * A client IP header set by a proxy service. | ||
| */ | ||
| interface ClientIpHeader { | ||
| /** | ||
| * Header name (lower-case). | ||
| */ | ||
| header: string; | ||
| /** | ||
| * How to parse the header value. | ||
| */ | ||
| format: ClientIpFormat; | ||
| } | ||
| /** | ||
| * A trusted proxy service (such as Cloudflare) sitting in front of the | ||
| * application. | ||
| * | ||
| * Identified by IP range, with the header(s) that carry the real client IP. | ||
| * Create one with a helper such as {@linkcode cloudflare} and include it in the | ||
| * `proxies` array. | ||
| */ | ||
| interface ProxyService { | ||
| /** | ||
| * Discriminant marking this entry as a proxy service. | ||
| */ | ||
| kind: "service"; | ||
| /** | ||
| * Name of the service (such as `"cloudflare"`). | ||
| */ | ||
| name: string; | ||
| /** | ||
| * IP addresses and CIDR ranges that identify this service. | ||
| */ | ||
| ranges: ReadonlyArray<string | Cidr>; | ||
| /** | ||
| * Header(s) this service uses to relay the real client IP, in priority order. | ||
| */ | ||
| clientIp: ReadonlyArray<ClientIpHeader>; | ||
| } | ||
| /** | ||
| * Configuration. | ||
| */ | ||
| interface Options { | ||
| /** | ||
| * Platform the code is running on; | ||
| * used to allow only known more trustworthy headers. | ||
| */ | ||
| platform?: Platform | null | undefined; | ||
| /** | ||
| * Trusted proxies. | ||
| * | ||
| * IP addresses and CIDR ranges are treated as trusted load balancers or | ||
| * proxies and skipped when finding the client IP. Proxy services created with | ||
| * a helper such as {@linkcode cloudflare} additionally declare which header | ||
| * carries the real client IP. | ||
| */ | ||
| proxies?: ReadonlyArray<string | Cidr | ProxyService> | null | undefined; | ||
| } | ||
| /** | ||
| * Find a client IP address on a request-like object. | ||
| * | ||
| * @param request | ||
| * Request-like object. | ||
| * @param [options] | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Found IP address; empty string if not found. | ||
| */ | ||
| declare function findIp(request: RequestLike, options?: Options | null | undefined): string; | ||
| /** | ||
| * One of the CIDR ranges. | ||
| */ | ||
| type Cidr = Ipv4Cidr | Ipv6Cidr; | ||
| //#endregion | ||
| export { Cidr, ClientIpFormat, ClientIpHeader, type CloudflareOptions, HeaderLike, Options, Platform, ProxyService, RequestLike, cloudflare, findIp as default, findIp, parseProxies, parseProxy }; |
-441
| import { cloudflare } from "./cloudflare.js"; | ||
| //#region src/index.ts | ||
| function parseXForwardedFor(value) { | ||
| if (typeof value !== "string") return []; | ||
| const forwardedIps = []; | ||
| 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 isProxyService(value) { | ||
| return typeof value === "object" && value !== null && "kind" in value && value.kind === "service"; | ||
| } | ||
| function resolveCandidate(candidate, services, proxies, headers) { | ||
| if (!isGlobalIp(candidate, proxies)) return; | ||
| if (services.length === 0) return candidate; | ||
| const service = services.find((service) => !isGlobalIp(candidate, service.ranges)); | ||
| if (!service) return candidate; | ||
| if (typeof headers !== "object" || headers === null) return; | ||
| for (const { header, format } of service.clientIp) { | ||
| const value = getHeader(headers, header); | ||
| const items = format === "ip" ? [typeof value === "string" ? value.trim() : value] : parseXForwardedFor(value).reverse(); | ||
| for (const item of items) if (isGlobalIp(item, proxies)) return item; | ||
| } | ||
| } | ||
| 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; | ||
| } | ||
| function cidrContains(cidr, ip) { | ||
| let part = 0; | ||
| let shift; | ||
| let cidrBits = cidr.bits; | ||
| while (cidrBits > 0) { | ||
| shift = cidr.partSize - cidrBits; | ||
| if (shift < 0) shift = 0; | ||
| if (ip[part] >> shift !== cidr.parts[part] >> shift) return false; | ||
| cidrBits -= cidr.partSize; | ||
| part += 1; | ||
| } | ||
| return true; | ||
| } | ||
| var Ipv4Cidr = class { | ||
| type = "v4"; | ||
| partSize = 8; | ||
| parts; | ||
| bits; | ||
| constructor(parts, bits) { | ||
| this.bits = bits; | ||
| this.parts = parts; | ||
| Object.freeze(this); | ||
| } | ||
| contains(ip) { | ||
| return cidrContains(this, ip); | ||
| } | ||
| }; | ||
| var Ipv6Cidr = class { | ||
| type = "v6"; | ||
| partSize = 16; | ||
| parts; | ||
| bits; | ||
| constructor(parts, bits) { | ||
| this.bits = bits; | ||
| this.parts = parts; | ||
| Object.freeze(this); | ||
| } | ||
| contains(ip) { | ||
| return cidrContains(this, ip); | ||
| } | ||
| }; | ||
| function parseCidr(cidr) { | ||
| 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("/"); | ||
| } | ||
| /** | ||
| * Parse CIDR addresses and keep non-CIDR IP addresses. | ||
| * | ||
| * @param value | ||
| * Value to parse. | ||
| * @returns | ||
| * Parsed {@linkcode Cidr} if range or given `value` if IP. | ||
| */ | ||
| function parseProxy(value) { | ||
| if (isCidr(value)) return parseCidr(value); | ||
| else return value; | ||
| } | ||
| /** | ||
| * Parse a list of trusted proxies. | ||
| * | ||
| * CIDR range strings are parsed to {@linkcode Cidr} (so they match by range); | ||
| * plain IP strings and {@linkcode ProxyService} objects (such as those created | ||
| * by {@linkcode cloudflare}) are passed through unchanged. Use this to | ||
| * normalize the `proxies` option before handing it to {@linkcode findIp}. | ||
| * | ||
| * @param proxies | ||
| * Trusted proxies to parse. | ||
| * @returns | ||
| * Parsed proxies. | ||
| */ | ||
| function parseProxies(proxies) { | ||
| return proxies.map((proxy) => typeof proxy === "string" ? parseProxy(proxy) : 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]; | ||
| } | ||
| var Parser = class { | ||
| 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) { | ||
| if (typeof p.readGivenChar(sep) === "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 !== void 0; 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) => { | ||
| return p.readNumber(10, 3, false); | ||
| }); | ||
| if (result === void 0) return; | ||
| else groups.push(result); | ||
| } | ||
| return groups; | ||
| }); | ||
| } | ||
| readIpv6Address() { | ||
| const readGroups = (p, groups) => { | ||
| const limit = groups.length; | ||
| for (const i of groups.keys()) { | ||
| 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) => { | ||
| const head = /* @__PURE__ */ new Uint16Array(8); | ||
| const [headSize, headIpv4] = readGroups(p, head); | ||
| if (headSize === 8) return head; | ||
| if (headIpv4) return; | ||
| if (typeof p.readGivenChar(":") === "undefined") return; | ||
| if (typeof p.readGivenChar(":") === "undefined") return; | ||
| const tail = /* @__PURE__ */ 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, void 0, true); | ||
| }); | ||
| } | ||
| readScopeId() { | ||
| return this.readAtomically((p) => { | ||
| if (typeof p.readGivenChar("%") !== "undefined") return p.readNumber(10, void 0, 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; | ||
| parser.readPort(); | ||
| if (parser.state.length !== 0) return false; | ||
| if (octets[0] === 0) return false; | ||
| 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; | ||
| if (octets[0] === 127) return false; | ||
| if (octets[0] === 100 && (octets[1] & 192) === 64) return false; | ||
| if (octets[0] === 169 && octets[1] === 254) return false; | ||
| if (octets[0] === 192 && octets[1] === 0 && octets[2] === 0) return false; | ||
| 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; | ||
| if (octets[0] === 198 && (octets[1] & 254) === 18) return false; | ||
| const isBroadcast = u32FromBytes(octets) === IPV4_BROADCAST; | ||
| if ((octets[0] & 240) === 240 && !isBroadcast) return false; | ||
| 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; | ||
| parser.readScopeId(); | ||
| if (parser.state.length !== 0) return false; | ||
| 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; | ||
| if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 1) return false; | ||
| if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 65535) return false; | ||
| if (segments[0] === 100 && segments[1] === 65435 && segments[2] === 1) return false; | ||
| if (segments[0] === 256 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0) return false; | ||
| if (segments[0] === 8193 && segments[1] < 512) { | ||
| if (segments[0] === 8193 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 1) return true; | ||
| if (segments[0] === 8193 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 2) return true; | ||
| if (segments[0] === 8193 && segments[1] === 3) return true; | ||
| if (segments[0] === 8193 && segments[1] === 4 && segments[2] === 274) return true; | ||
| if (segments[0] === 8193 && segments[1] >= 32 && segments[1] <= 47) return true; | ||
| return false; | ||
| } | ||
| if (segments[0] === 8193 && segments[1] === 3512) return false; | ||
| if ((segments[0] & 65024) === 64512) return false; | ||
| if ((segments[0] & 65472) === 65152) 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; | ||
| } | ||
| } | ||
| /** | ||
| * Find a client IP address on a request-like object. | ||
| * | ||
| * @param request | ||
| * Request-like object. | ||
| * @param [options] | ||
| * Configuration (optional). | ||
| * @returns | ||
| * Found IP address; empty string if not found. | ||
| */ | ||
| function findIp(request, options) { | ||
| const { platform, proxies: rawProxies } = options || {}; | ||
| const proxies = []; | ||
| const services = []; | ||
| if (Array.isArray(rawProxies)) for (const cidrOrIp of rawProxies) { | ||
| if (typeof cidrOrIp === "string") proxies.push(parseProxy(cidrOrIp)); | ||
| if (isIpv4Cidr(cidrOrIp) || isIpv6Cidr(cidrOrIp)) proxies.push(cidrOrIp); | ||
| if (isProxyService(cidrOrIp)) { | ||
| const ranges = []; | ||
| for (const range of cidrOrIp.ranges) ranges.push(typeof range === "string" ? parseProxy(range) : range); | ||
| services.push({ | ||
| ...cidrOrIp, | ||
| ranges | ||
| }); | ||
| } | ||
| } | ||
| const ipFromRequest = resolveCandidate(request.ip, services, proxies, request.headers); | ||
| if (ipFromRequest) return ipFromRequest; | ||
| const socketRemoteAddress = resolveCandidate(request.socket?.remoteAddress, services, proxies, request.headers); | ||
| if (socketRemoteAddress) return socketRemoteAddress; | ||
| const infoRemoteAddress = resolveCandidate(request.info?.remoteAddress, services, proxies, request.headers); | ||
| if (infoRemoteAddress) return infoRemoteAddress; | ||
| const requestContextIdentitySourceIp = resolveCandidate(request.requestContext?.identity?.sourceIp, services, proxies, request.headers); | ||
| if (requestContextIdentitySourceIp) return requestContextIdentitySourceIp; | ||
| if (typeof request.headers !== "object" || request.headers === null) return ""; | ||
| if (platform === "cloudflare") { | ||
| const cfConnectingIpv6 = getHeader(request.headers, "cf-connecting-ipv6"); | ||
| if (isGlobalIpv6(cfConnectingIpv6, proxies)) return cfConnectingIpv6; | ||
| const cfConnectingIp = getHeader(request.headers, "cf-connecting-ip"); | ||
| if (isGlobalIp(cfConnectingIp, proxies)) return cfConnectingIp; | ||
| return ""; | ||
| } | ||
| if (platform === "firebase") { | ||
| const fahClientIp = resolveCandidate(getHeader(request.headers, "x-fah-client-ip"), services, proxies, request.headers); | ||
| if (fahClientIp) return fahClientIp; | ||
| const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for")); | ||
| for (const item of xForwardedForItems.reverse()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) return resolved; | ||
| } | ||
| return ""; | ||
| } | ||
| if (platform === "fly-io") { | ||
| const flyClientIp = resolveCandidate(getHeader(request.headers, "fly-client-ip"), services, proxies, request.headers); | ||
| if (flyClientIp) return flyClientIp; | ||
| return ""; | ||
| } | ||
| if (platform === "vercel") { | ||
| const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers); | ||
| if (xRealIp) return xRealIp; | ||
| const xVercelForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-vercel-forwarded-for")); | ||
| for (const item of xVercelForwardedForItems.reverse()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) return resolved; | ||
| } | ||
| const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for")); | ||
| for (const item of xForwardedForItems.reverse()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) return resolved; | ||
| } | ||
| return ""; | ||
| } | ||
| if (platform === "render") { | ||
| const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers); | ||
| if (trueClientIp) return trueClientIp; | ||
| return ""; | ||
| } | ||
| const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for")); | ||
| for (const item of xForwardedForItems.reverse()) { | ||
| const resolved = resolveCandidate(item, services, proxies, request.headers); | ||
| if (resolved) return resolved; | ||
| } | ||
| const xClientIp = resolveCandidate(getHeader(request.headers, "x-client-ip"), services, proxies, request.headers); | ||
| if (xClientIp) return xClientIp; | ||
| const doConnectingIp = resolveCandidate(getHeader(request.headers, "do-connecting-ip"), services, proxies, request.headers); | ||
| if (doConnectingIp) return doConnectingIp; | ||
| const fastlyClientIp = resolveCandidate(getHeader(request.headers, "fastly-client-ip"), services, proxies, request.headers); | ||
| if (fastlyClientIp) return fastlyClientIp; | ||
| const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers); | ||
| if (trueClientIp) return trueClientIp; | ||
| const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers); | ||
| if (xRealIp) return xRealIp; | ||
| const xClusterClientIp = resolveCandidate(getHeader(request.headers, "x-cluster-client-ip"), services, proxies, request.headers); | ||
| if (xClusterClientIp) return xClusterClientIp; | ||
| const xForwarded = resolveCandidate(getHeader(request.headers, "x-forwarded"), services, proxies, request.headers); | ||
| if (xForwarded) return xForwarded; | ||
| const forwardedFor = resolveCandidate(getHeader(request.headers, "forwarded-for"), services, proxies, request.headers); | ||
| if (forwardedFor) return forwardedFor; | ||
| const forwarded = resolveCandidate(getHeader(request.headers, "forwarded"), services, proxies, request.headers); | ||
| if (forwarded) return forwarded; | ||
| const xAppEngineUserIp = resolveCandidate(getHeader(request.headers, "x-appengine-user-ip"), services, proxies, request.headers); | ||
| if (xAppEngineUserIp) return xAppEngineUserIp; | ||
| return ""; | ||
| } | ||
| //#endregion | ||
| export { cloudflare, findIp as default, findIp, parseProxies, parseProxy }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
67051
45.73%1261
71.8%0
-100%6
100%1
Infinity%