@arcjet/ip
Advanced tools
Comparing version 1.0.0-alpha.13 to 1.0.0-alpha.14
@@ -19,3 +19,7 @@ interface PartialSocket { | ||
} | ||
declare function findIP(request: RequestLike, headers: Headers): string; | ||
export type Platform = "cloudflare" | "fly-io"; | ||
export interface Options { | ||
platform?: Platform; | ||
} | ||
declare function findIP(request: RequestLike, headers: Headers, options?: Options): string; | ||
export default findIP; |
157
index.js
@@ -256,20 +256,16 @@ function parseXForwardedFor(value) { | ||
} | ||
// TODO: Evaluate if we need to allow other IPv4 addresses in development, | ||
// such as "This network" | ||
if (process.env["NODE_ENV"] === "production") { | ||
// 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; | ||
} | ||
// 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 | ||
@@ -346,26 +342,22 @@ if (octets[0] === 100 && (octets[1] & 0b1100_0000) === 0b0100_0000) { | ||
} | ||
// TODO: Evaluate if we need to allow other IPv6 addresses in development, | ||
// such as "Unique local range" or "Unspecified address" | ||
if (process.env["NODE_ENV"] === "production") { | ||
// 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; | ||
} | ||
// 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`) | ||
@@ -465,6 +457,47 @@ if (segments[0] === 0x64 && segments[1] === 0xff9b && segments[2] === 1) { | ||
// SOFTWARE. | ||
function findIP(request, headers) { | ||
function findIP(request, headers, 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)) { | ||
return request.ip; | ||
} | ||
const socketRemoteAddress = request.socket?.remoteAddress; | ||
if (isGlobalIP(socketRemoteAddress)) { | ||
return socketRemoteAddress; | ||
} | ||
const infoRemoteAddress = request.info?.remoteAddress; | ||
if (isGlobalIP(infoRemoteAddress)) { | ||
return infoRemoteAddress; | ||
} | ||
// AWS Api Gateway + Lambda | ||
const requestContextIdentitySourceIP = request.requestContext?.identity?.sourceIp; | ||
if (isGlobalIP(requestContextIdentitySourceIP)) { | ||
return requestContextIdentitySourceIP; | ||
} | ||
// 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. | ||
const { platform } = options; | ||
if (platform === "cloudflare") { | ||
// CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 | ||
const cfConnectingIPv6 = headers.get("cf-connecting-ipv6"); | ||
if (isGlobalIPv6(cfConnectingIPv6)) { | ||
return cfConnectingIPv6; | ||
} | ||
// CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip | ||
const cfConnectingIP = headers.get("cf-connecting-ip"); | ||
if (isGlobalIP(cfConnectingIP)) { | ||
return cfConnectingIP; | ||
} | ||
} | ||
// 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 = headers.get("fly-client-ip"); | ||
if (isGlobalIP(flyClientIP)) { | ||
return flyClientIP; | ||
} | ||
} | ||
// Standard headers used by Amazon EC2, Heroku, and others. | ||
@@ -480,6 +513,6 @@ const xClientIP = headers.get("x-client-ip"); | ||
// 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. We want to | ||
// iterate left-to-right, since left-most IP will be closest to the client, | ||
// and we'll return the first public IP in the list. | ||
for (const item of xForwardedForItems) { | ||
// 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)) { | ||
@@ -489,9 +522,2 @@ return item; | ||
} | ||
// Cloudflare. | ||
// CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip | ||
const cfConnectingIP = headers.get("cf-connecting-ip"); | ||
if (isGlobalIP(cfConnectingIP)) { | ||
return cfConnectingIP; | ||
} | ||
// TODO: CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 | ||
// DigitalOcean. | ||
@@ -509,3 +535,3 @@ // DO-Connecting-IP: https://www.digitalocean.com/community/questions/app-platform-client-ip | ||
} | ||
// Akamai and Cloudflare | ||
// Akamai | ||
// True-Client-IP | ||
@@ -516,8 +542,2 @@ const trueClientIP = headers.get("true-client-ip"); | ||
} | ||
// Fly.io | ||
// Fly-Client-IP: https://fly.io/docs/networking/request-headers/#fly-client-ip | ||
const flyClientIP = headers.get("fly-client-ip"); | ||
if (isGlobalIP(flyClientIP)) { | ||
return flyClientIP; | ||
} | ||
// Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies | ||
@@ -552,21 +572,2 @@ // X-Real-IP | ||
} | ||
const socketRemoteAddress = request.socket?.remoteAddress; | ||
if (isGlobalIP(socketRemoteAddress)) { | ||
return socketRemoteAddress; | ||
} | ||
const infoRemoteAddress = request.info?.remoteAddress; | ||
if (isGlobalIP(infoRemoteAddress)) { | ||
return infoRemoteAddress; | ||
} | ||
// AWS Api Gateway + Lambda | ||
const requestContextIdentitySourceIP = request.requestContext?.identity?.sourceIp; | ||
if (isGlobalIP(requestContextIdentitySourceIP)) { | ||
return requestContextIdentitySourceIP; | ||
} | ||
// Cloudflare fallback | ||
// Cf-Pseudo-IPv4: https://blog.cloudflare.com/eliminating-the-last-reasons-to-not-enable-ipv6/#introducingpseudoipv4 | ||
const cfPseudoIPv4 = headers.get("cf-pseudo-ipv4"); | ||
if (isGlobalIP(cfPseudoIPv4)) { | ||
return cfPseudoIPv4; | ||
} | ||
return ""; | ||
@@ -573,0 +574,0 @@ } |
192
index.ts
@@ -307,20 +307,16 @@ function parseXForwardedFor(value?: string | null): string[] { | ||
// TODO: Evaluate if we need to allow other IPv4 addresses in development, | ||
// such as "This network" | ||
if (process.env["NODE_ENV"] === "production") { | ||
// 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; | ||
} | ||
// 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; | ||
} | ||
// Loopback address | ||
if (octets[0] === 127) { | ||
return false; | ||
} | ||
@@ -415,30 +411,26 @@ | ||
// TODO: Evaluate if we need to allow other IPv6 addresses in development, | ||
// such as "Unique local range" or "Unspecified address" | ||
if (process.env["NODE_ENV"] === "production") { | ||
// 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; | ||
} | ||
// 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-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; | ||
} | ||
@@ -566,2 +558,8 @@ | ||
export type Platform = "cloudflare" | "fly-io"; | ||
export interface Options { | ||
platform?: Platform; | ||
} | ||
// Heavily based on https://github.com/pbojinov/request-ip | ||
@@ -588,3 +586,10 @@ // | ||
// SOFTWARE. | ||
function findIP(request: RequestLike, headers: Headers): string { | ||
function findIP( | ||
request: RequestLike, | ||
headers: Headers, | ||
options: Options = {}, | ||
): string { | ||
// 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)) { | ||
@@ -594,2 +599,49 @@ return request.ip; | ||
const socketRemoteAddress = request.socket?.remoteAddress; | ||
if (isGlobalIP(socketRemoteAddress)) { | ||
return socketRemoteAddress; | ||
} | ||
const infoRemoteAddress = request.info?.remoteAddress; | ||
if (isGlobalIP(infoRemoteAddress)) { | ||
return infoRemoteAddress; | ||
} | ||
// AWS Api Gateway + Lambda | ||
const requestContextIdentitySourceIP = | ||
request.requestContext?.identity?.sourceIp; | ||
if (isGlobalIP(requestContextIdentitySourceIP)) { | ||
return requestContextIdentitySourceIP; | ||
} | ||
// 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. | ||
const { platform } = options; | ||
if (platform === "cloudflare") { | ||
// CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 | ||
const cfConnectingIPv6 = headers.get("cf-connecting-ipv6"); | ||
if (isGlobalIPv6(cfConnectingIPv6)) { | ||
return cfConnectingIPv6; | ||
} | ||
// CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip | ||
const cfConnectingIP = headers.get("cf-connecting-ip"); | ||
if (isGlobalIP(cfConnectingIP)) { | ||
return cfConnectingIP; | ||
} | ||
} | ||
// 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 = headers.get("fly-client-ip"); | ||
if (isGlobalIP(flyClientIP)) { | ||
return flyClientIP; | ||
} | ||
} | ||
// Standard headers used by Amazon EC2, Heroku, and others. | ||
@@ -606,6 +658,6 @@ const xClientIP = headers.get("x-client-ip"); | ||
// 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. We want to | ||
// iterate left-to-right, since left-most IP will be closest to the client, | ||
// and we'll return the first public IP in the list. | ||
for (const item of xForwardedForItems) { | ||
// 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)) { | ||
@@ -616,11 +668,2 @@ return item; | ||
// Cloudflare. | ||
// CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip | ||
const cfConnectingIP = headers.get("cf-connecting-ip"); | ||
if (isGlobalIP(cfConnectingIP)) { | ||
return cfConnectingIP; | ||
} | ||
// TODO: CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6 | ||
// DigitalOcean. | ||
@@ -640,3 +683,3 @@ // DO-Connecting-IP: https://www.digitalocean.com/community/questions/app-platform-client-ip | ||
// Akamai and Cloudflare | ||
// Akamai | ||
// True-Client-IP | ||
@@ -648,9 +691,2 @@ const trueClientIP = headers.get("true-client-ip"); | ||
// Fly.io | ||
// Fly-Client-IP: https://fly.io/docs/networking/request-headers/#fly-client-ip | ||
const flyClientIP = headers.get("fly-client-ip"); | ||
if (isGlobalIP(flyClientIP)) { | ||
return flyClientIP; | ||
} | ||
// Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies | ||
@@ -691,26 +727,2 @@ // X-Real-IP | ||
const socketRemoteAddress = request.socket?.remoteAddress; | ||
if (isGlobalIP(socketRemoteAddress)) { | ||
return socketRemoteAddress; | ||
} | ||
const infoRemoteAddress = request.info?.remoteAddress; | ||
if (isGlobalIP(infoRemoteAddress)) { | ||
return infoRemoteAddress; | ||
} | ||
// AWS Api Gateway + Lambda | ||
const requestContextIdentitySourceIP = | ||
request.requestContext?.identity?.sourceIp; | ||
if (isGlobalIP(requestContextIdentitySourceIP)) { | ||
return requestContextIdentitySourceIP; | ||
} | ||
// Cloudflare fallback | ||
// Cf-Pseudo-IPv4: https://blog.cloudflare.com/eliminating-the-last-reasons-to-not-enable-ipv6/#introducingpseudoipv4 | ||
const cfPseudoIPv4 = headers.get("cf-pseudo-ipv4"); | ||
if (isGlobalIP(cfPseudoIPv4)) { | ||
return cfPseudoIPv4; | ||
} | ||
return ""; | ||
@@ -717,0 +729,0 @@ } |
{ | ||
"name": "@arcjet/ip", | ||
"version": "1.0.0-alpha.13", | ||
"version": "1.0.0-alpha.14", | ||
"description": "Arcjet utilities for finding the originating IP of a request", | ||
@@ -44,7 +44,7 @@ "license": "Apache-2.0", | ||
"devDependencies": { | ||
"@arcjet/eslint-config": "1.0.0-alpha.13", | ||
"@arcjet/rollup-config": "1.0.0-alpha.13", | ||
"@arcjet/tsconfig": "1.0.0-alpha.13", | ||
"@arcjet/eslint-config": "1.0.0-alpha.14", | ||
"@arcjet/rollup-config": "1.0.0-alpha.14", | ||
"@arcjet/tsconfig": "1.0.0-alpha.14", | ||
"@jest/globals": "29.7.0", | ||
"@rollup/wasm-node": "4.17.2", | ||
"@rollup/wasm-node": "4.18.0", | ||
"@types/node": "18.18.0", | ||
@@ -51,0 +51,0 @@ "jest": "29.7.0", |
<a href="https://arcjet.com" target="_arcjet-home"> | ||
<picture> | ||
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/arcjet-logo-dark-planet-arrival.svg"> | ||
<img src="https://arcjet.com/arcjet-logo-light-planet-arrival.svg" alt="Arcjet Logo" height="144" width="auto"> | ||
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg"> | ||
<img src="https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg" alt="Arcjet Logo" height="128" width="auto"> | ||
</picture> | ||
@@ -6,0 +6,0 @@ </a> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
57134
1205