From eb6d2371a0264f77bd9a0381aaccd655c8d5294d Mon Sep 17 00:00:00 2001 From: Lars Stadel Linnet Date: Mon, 21 Sep 2020 23:32:43 +0200 Subject: [PATCH 1/3] Restructured to work with specific headers and orders --- package.json | 1 + src/index.js | 146 ++++++++++++++++++++++---------------------------- test/index.js | 57 ++++++++++++++++++++ 3 files changed, 122 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index e673ef3..c577743 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "test": "nyc --reporter=html --reporter=text --check-coverage --lines=100 --statements=100 tape ./test/index.js" }, "dependencies": { + "extend": "^3.0.2", "is_js": "^0.9.0" }, "devDependencies": { diff --git a/src/index.js b/src/index.js index 95e854f..e0b79b9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,37 @@ const is = require('is_js'); +const extend = require('extend'); + +const DEFAULTS = { + sources: [ + // Standard headers used by Amazon EC2, Heroku, and others. + 'headers.x-client-ip', + // Load-balancers (AWS ELB) or proxies. + 'headers.x-forwarded-for', + // Cloudflare. + // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- + // CF-Connecting-IP - applied to every request to the origin. + 'headers.cf-connecting-ip', + // Fastly and Firebase hosting header (When forwared to cloud function) + 'headers.fastly-client-ip', + // Akamai and Cloudflare: True-Client-IP. + 'headers.true-client-ip', + // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. + 'headers.x-real-ip', + // (Rackspace LB and Riverbed's Stingray) + // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address + // https://splash.riverbed.com/docs/DOC-1926 + 'headers.x-cluster-client-ip', + 'headers.x-forwarded', + 'headers.forwarded-for', + 'headers.forwarded', + 'connection.remoteAddress', + 'connection.socket.remoteAddress', + 'socket.remoteAddress', + 'info.remoteAddress', + // AWS Api Gateway + Lambda + 'requestContext.identity.sourceIp', + ], +}; /** * Parse x-forwarded-for headers. @@ -7,10 +40,6 @@ const is = require('is_js'); * @return {string|null} First known IP address, if any. */ function getClientIpFromXForwardedFor(value) { - if (!is.existy(value)) { - return null; - } - if (is.not.string(value)) { throw new TypeError(`Expected a string, got "${typeof value}"`); } @@ -39,91 +68,44 @@ function getClientIpFromXForwardedFor(value) { return forwardedIps.find(is.ip); } +function getIpFromSource(req, keys) { + const key = keys.shift(); + if (!is.existy(req[key])) { + return null; + } + if (keys.length !== 0) { + return getIpFromSource(req[key], keys); + } + if (key === 'x-forwarded-for') { + return getClientIpFromXForwardedFor(req[key]); + } + return req[key]; +} + /** * Determine client IP address. * * @param req + * @param _options * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. */ -function getClientIp(req) { - - // Server is probably behind a proxy. - if (req.headers) { - - // Standard headers used by Amazon EC2, Heroku, and others. - if (is.ip(req.headers['x-client-ip'])) { - return req.headers['x-client-ip']; +function getClientIp(req, _options = {}) { + const options = extend(false, {}, DEFAULTS, _options); + const { sources } = options; + + // eslint-disable-next-line no-restricted-syntax + for (const key in sources) { + // eslint-disable-next-line no-prototype-builtins + if (Object.prototype.hasOwnProperty(sources, key)) { + // eslint-disable-next-line no-continue + continue; } - - // Load-balancers (AWS ELB) or proxies. - const xForwardedFor = getClientIpFromXForwardedFor(req.headers['x-forwarded-for']); - if (is.ip(xForwardedFor)) { - return xForwardedFor; - } - - // Cloudflare. - // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - // CF-Connecting-IP - applied to every request to the origin. - if (is.ip(req.headers['cf-connecting-ip'])) { - return req.headers['cf-connecting-ip']; + const source = sources[key]; + const keys = source.split('.'); + const ip = getIpFromSource(req, keys); + if (is.ip(ip)) { + return ip; } - - // Fastly and Firebase hosting header (When forwared to cloud function) - if (is.ip(req.headers['fastly-client-ip'])) { - return req.headers['fastly-client-ip']; - } - - // Akamai and Cloudflare: True-Client-IP. - if (is.ip(req.headers['true-client-ip'])) { - return req.headers['true-client-ip']; - } - - // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. - if (is.ip(req.headers['x-real-ip'])) { - return req.headers['x-real-ip']; - } - - // (Rackspace LB and Riverbed's Stingray) - // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address - // https://splash.riverbed.com/docs/DOC-1926 - if (is.ip(req.headers['x-cluster-client-ip'])) { - return req.headers['x-cluster-client-ip']; - } - - if (is.ip(req.headers['x-forwarded'])) { - return req.headers['x-forwarded']; - } - - if (is.ip(req.headers['forwarded-for'])) { - return req.headers['forwarded-for']; - } - - if (is.ip(req.headers.forwarded)) { - return req.headers.forwarded; - } - } - - // Remote address checks. - if (is.existy(req.connection)) { - if (is.ip(req.connection.remoteAddress)) { - return req.connection.remoteAddress; - } - if (is.existy(req.connection.socket) && is.ip(req.connection.socket.remoteAddress)) { - return req.connection.socket.remoteAddress; - } - } - - if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) { - return req.socket.remoteAddress; - } - - if (is.existy(req.info) && is.ip(req.info.remoteAddress)) { - return req.info.remoteAddress; - } - - // AWS Api Gateway + Lambda - if (is.existy(req.requestContext) && is.existy(req.requestContext.identity) && is.ip(req.requestContext.identity.sourceIp)) { - return req.requestContext.identity.sourceIp; } return null; @@ -150,7 +132,7 @@ function mw(options) { const ip = getClientIp(req); Object.defineProperty(req, attributeName, { get: () => ip, - configurable: true + configurable: true, }); next(); }; diff --git a/test/index.js b/test/index.js index 63ce88b..1d113e5 100644 --- a/test/index.js +++ b/test/index.js @@ -519,3 +519,60 @@ test('android request to AWS EBS app (x-forwarded-for)', (t) => { }); }); }); + +test('Limited header checks', (t) => { + t.plan(1); + const wanted = '1.1.1.2'; + const requestMock = { + headers: { + host: '[redacted]', + 'x-real-ip': '172.31.41.116', + 'x-forwarded-for': '107.77.213.113, 172.31.41.116', + 'accept-encoding': 'gzip', + 'user-agent': 'okhttp/3.4.1', + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https', + }, + requestContext: { + identity: { + sourceIp: '1.1.1.2', + }, + }, + }; + + const found = requestIp.getClientIp(requestMock, { + sources: [ + 'requestContext.identity.sourceIp', + ], + }); + + t.equal(found, wanted); +}); + +test('Rearranged header order', (t) => { + t.plan(2); + const wanted = '2.3.3.4'; + const wanted2 = '32.144.5.106'; + const requestMock = { + headers: { + 'true-client-ip': '2.3.3.4', + 'x-client-ip': '32.144.5.106', + }, + }; + + let found = requestIp.getClientIp(requestMock, { + sources: [ + 'headers.true-client-ip', + 'headers.x-client-ip', + ], + }); + t.equal(found, wanted); + + found = requestIp.getClientIp(requestMock, { + sources: [ + 'headers.x-client-ip', + 'headers.true-client-ip', + ], + }); + t.equal(found, wanted2); +}); From 266fde32786cefae00b4a4f17a8f696b8147f36c Mon Sep 17 00:00:00 2001 From: Lars Stadel Linnet Date: Tue, 22 Sep 2020 09:45:17 +0200 Subject: [PATCH 2/3] Added some base function documentation --- src/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/index.js b/src/index.js index e0b79b9..b5bf0fa 100644 --- a/src/index.js +++ b/src/index.js @@ -68,6 +68,13 @@ function getClientIpFromXForwardedFor(value) { return forwardedIps.find(is.ip); } +/** + * Parse object tree and fetch relevant key. + * + * @param req + * @param keys + * @returns {string|null|*} - The value from the object tree. + */ function getIpFromSource(req, keys) { const key = keys.shift(); if (!is.existy(req[key])) { From 5dac7167e601463e6112a58920ebbd39fc4e4c96 Mon Sep 17 00:00:00 2001 From: Lars Stadel Linnet Date: Tue, 22 Sep 2020 13:55:51 +0200 Subject: [PATCH 3/3] Updated dist file --- dist/index.js | 131 +++++++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/dist/index.js b/dist/index.js index ea468ac..34886f4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,8 +1,26 @@ "use strict"; -function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } var is = require('is_js'); + +var extend = require('extend'); + +var DEFAULTS = { + sources: [// Standard headers used by Amazon EC2, Heroku, and others. + 'headers.x-client-ip', // Load-balancers (AWS ELB) or proxies. + 'headers.x-forwarded-for', // Cloudflare. + // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- + // CF-Connecting-IP - applied to every request to the origin. + 'headers.cf-connecting-ip', // Fastly and Firebase hosting header (When forwared to cloud function) + 'headers.fastly-client-ip', // Akamai and Cloudflare: True-Client-IP. + 'headers.true-client-ip', // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. + 'headers.x-real-ip', // (Rackspace LB and Riverbed's Stingray) + // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address + // https://splash.riverbed.com/docs/DOC-1926 + 'headers.x-cluster-client-ip', 'headers.x-forwarded', 'headers.forwarded-for', 'headers.forwarded', 'connection.remoteAddress', 'connection.socket.remoteAddress', 'socket.remoteAddress', 'info.remoteAddress', // AWS Api Gateway + Lambda + 'requestContext.identity.sourceIp'] +}; /** * Parse x-forwarded-for headers. * @@ -10,12 +28,7 @@ var is = require('is_js'); * @return {string|null} First known IP address, if any. */ - function getClientIpFromXForwardedFor(value) { - if (!is.existy(value)) { - return null; - } - if (is.not.string(value)) { throw new TypeError("Expected a string, got \"".concat(_typeof(value), "\"")); } // x-forwarded-for may return multiple IP addresses in the format: @@ -45,92 +58,60 @@ function getClientIpFromXForwardedFor(value) { return forwardedIps.find(is.ip); } /** - * Determine client IP address. + * Parse object tree and fetch relevant key. * * @param req - * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. + * @param keys + * @returns {string|null|*} - The value from the object tree. */ -function getClientIp(req) { - // Server is probably behind a proxy. - if (req.headers) { - // Standard headers used by Amazon EC2, Heroku, and others. - if (is.ip(req.headers['x-client-ip'])) { - return req.headers['x-client-ip']; - } // Load-balancers (AWS ELB) or proxies. - - - var xForwardedFor = getClientIpFromXForwardedFor(req.headers['x-forwarded-for']); - - if (is.ip(xForwardedFor)) { - return xForwardedFor; - } // Cloudflare. - // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - // CF-Connecting-IP - applied to every request to the origin. - - - if (is.ip(req.headers['cf-connecting-ip'])) { - return req.headers['cf-connecting-ip']; - } // Fastly and Firebase hosting header (When forwared to cloud function) - +function getIpFromSource(req, keys) { + var key = keys.shift(); - if (is.ip(req.headers['fastly-client-ip'])) { - return req.headers['fastly-client-ip']; - } // Akamai and Cloudflare: True-Client-IP. - - - if (is.ip(req.headers['true-client-ip'])) { - return req.headers['true-client-ip']; - } // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies. + if (!is.existy(req[key])) { + return null; + } + if (keys.length !== 0) { + return getIpFromSource(req[key], keys); + } - if (is.ip(req.headers['x-real-ip'])) { - return req.headers['x-real-ip']; - } // (Rackspace LB and Riverbed's Stingray) - // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address - // https://splash.riverbed.com/docs/DOC-1926 + if (key === 'x-forwarded-for') { + return getClientIpFromXForwardedFor(req[key]); + } + return req[key]; +} +/** + * Determine client IP address. + * + * @param req + * @param _options + * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. + */ - if (is.ip(req.headers['x-cluster-client-ip'])) { - return req.headers['x-cluster-client-ip']; - } - if (is.ip(req.headers['x-forwarded'])) { - return req.headers['x-forwarded']; - } +function getClientIp(req) { + var _options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - if (is.ip(req.headers['forwarded-for'])) { - return req.headers['forwarded-for']; - } + var options = extend(false, {}, DEFAULTS, _options); + var sources = options.sources; // eslint-disable-next-line no-restricted-syntax - if (is.ip(req.headers.forwarded)) { - return req.headers.forwarded; + for (var key in sources) { + // eslint-disable-next-line no-prototype-builtins + if (Object.prototype.hasOwnProperty(sources, key)) { + // eslint-disable-next-line no-continue + continue; } - } // Remote address checks. + var source = sources[key]; + var keys = source.split('.'); + var ip = getIpFromSource(req, keys); - if (is.existy(req.connection)) { - if (is.ip(req.connection.remoteAddress)) { - return req.connection.remoteAddress; + if (is.ip(ip)) { + return ip; } - - if (is.existy(req.connection.socket) && is.ip(req.connection.socket.remoteAddress)) { - return req.connection.socket.remoteAddress; - } - } - - if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) { - return req.socket.remoteAddress; - } - - if (is.existy(req.info) && is.ip(req.info.remoteAddress)) { - return req.info.remoteAddress; - } // AWS Api Gateway + Lambda - - - if (is.existy(req.requestContext) && is.existy(req.requestContext.identity) && is.ip(req.requestContext.identity.sourceIp)) { - return req.requestContext.identity.sourceIp; } return null;