From 84c03742794363a39499dc53b5068478b8922de0 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Fri, 1 Sep 2023 23:39:37 +0200 Subject: [PATCH 01/16] use node own isIP fn --- index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index aaa7671..6ae211f 100644 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ module.exports.compile = compile const forwarded = require('@fastify/forwarded') const ipaddr = require('ipaddr.js') +const { isIP } = require('net') /** * Variables. @@ -32,7 +33,6 @@ const ipaddr = require('ipaddr.js') */ const DIGIT_REGEXP = /^[0-9]+$/ -const isip = ipaddr.isValid const parseip = ipaddr.parse /** @@ -165,7 +165,7 @@ function parseipNotation (note) { ? note.substring(0, pos) : note - if (!isip(str)) { + if (isIP(str) === 0) { throw new TypeError('invalid IP address: ' + str) } @@ -188,7 +188,7 @@ function parseipNotation (note) { range = max } else if (DIGIT_REGEXP.test(range)) { range = parseInt(range, 10) - } else if (ip.kind() === 'ipv4' && isip(range)) { + } else if (ip.kind() === 'ipv4' && isIP(range) !== 0) { range = parseNetmask(range) } else { range = null @@ -259,7 +259,7 @@ function trustNone () { function trustMulti (subnets) { return function trust (addr) { - if (!isip(addr)) return false + if (isIP(addr) === 0) return false const ip = parseip(addr) let ipconv @@ -312,7 +312,7 @@ function trustSingle (subnet) { const subnetrange = subnet[1] return function trust (addr) { - if (!isip(addr)) return false + if (isIP(addr) === 0) return false let ip = parseip(addr) const kind = ip.kind() From 2df7d83050288e4eaecb5ba85099423062517d88 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Fri, 1 Sep 2023 23:58:29 +0200 Subject: [PATCH 02/16] precompute subnet --- index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 6ae211f..34abc81 100644 --- a/index.js +++ b/index.js @@ -176,7 +176,9 @@ function parseipNotation (note) { ip = ip.toIPv4Address() } - const max = ip.kind() === 'ipv6' + const kind = ip.kind() + + const max = kind === 'ipv6' ? 128 : 32 @@ -188,7 +190,7 @@ function parseipNotation (note) { range = max } else if (DIGIT_REGEXP.test(range)) { range = parseInt(range, 10) - } else if (ip.kind() === 'ipv4' && isIP(range) !== 0) { + } else if (kind === 'ipv4' && isIP(range) !== 0) { range = parseNetmask(range) } else { range = null @@ -198,7 +200,7 @@ function parseipNotation (note) { throw new TypeError('invalid range on address: ' + note) } - return [ip, range] + return [ip, range, kind] } /** @@ -269,8 +271,8 @@ function trustMulti (subnets) { for (var i = 0; i < subnets.length; i++) { const subnet = subnets[i] const subnetip = subnet[0] - const subnetkind = subnetip.kind() const subnetrange = subnet[1] + const subnetkind = subnet[2] let trusted = ip if (kind !== subnetkind) { @@ -307,9 +309,9 @@ function trustMulti (subnets) { function trustSingle (subnet) { const subnetip = subnet[0] - const subnetkind = subnetip.kind() - const subnetisipv4 = subnetkind === 'ipv4' const subnetrange = subnet[1] + const subnetkind = subnet[2] + const subnetisipv4 = subnetkind === 'ipv4' return function trust (addr) { if (isIP(addr) === 0) return false From a0b7f1c4bf38046adde213f203edc34f1d8dacee Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 01:26:44 +0200 Subject: [PATCH 03/16] more improvements --- README.md | 3 +-- benchmark/compiling.js | 1 + benchmark/kind.js | 1 + benchmark/matching.js | 1 + index.js | 56 ++++++++++++++++++++++++++++++------------ package.json | 1 + test/all.js | 10 +++++++- 7 files changed, 54 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5757412..ceeb7b5 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,9 @@ returned. ```js proxyaddr(req, function (addr) { return addr === '127.0.0.1' }) -proxyaddr(req, function (addr, i) { return i < 1 }) ``` -The `trust` arugment may also be a single IP address string or an +The `trust` argument may also be a single IP address string or an array of trusted addresses, as plain IP addresses, CIDR-formatted strings, or IP/netmask strings. diff --git a/benchmark/compiling.js b/benchmark/compiling.js index 1bf7420..7638727 100644 --- a/benchmark/compiling.js +++ b/benchmark/compiling.js @@ -1,3 +1,4 @@ +'use strict' /** * Globals for benchmark.js diff --git a/benchmark/kind.js b/benchmark/kind.js index 3f254c5..239ccf9 100644 --- a/benchmark/kind.js +++ b/benchmark/kind.js @@ -1,3 +1,4 @@ +'use strict' /** * Globals for benchmark.js diff --git a/benchmark/matching.js b/benchmark/matching.js index 0ca7fb2..d584c68 100644 --- a/benchmark/matching.js +++ b/benchmark/matching.js @@ -1,3 +1,4 @@ +'use strict' /** * Globals for benchmark.js diff --git a/index.js b/index.js index 34abc81..3000529 100644 --- a/index.js +++ b/index.js @@ -33,7 +33,7 @@ const { isIP } = require('net') */ const DIGIT_REGEXP = /^[0-9]+$/ -const parseip = ipaddr.parse +const parseIp = ipaddr.parse /** * Pre-defined IP ranges. @@ -41,6 +41,7 @@ const parseip = ipaddr.parse */ const IP_RANGES = { + __proto__: null, linklocal: ['169.254.0.0/16', 'fe80::/10'], loopback: ['127.0.0.1/8', '::1/128'], uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7'] @@ -56,23 +57,25 @@ const IP_RANGES = { */ function alladdrs (req, trust) { - // get addresses - const addrs = forwarded(req) - if (!trust) { // Return all addresses - return addrs + return forwarded(req) } if (typeof trust !== 'function') { trust = compile(trust) } + // get addresses + const addrs = forwarded(req) + + const len = addrs.length - 1 /* eslint-disable no-var */ - for (var i = 0; i < addrs.length - 1; i++) { + for (var i = 0; i < len; i++) { if (trust(addrs[i], i)) continue addrs.length = i + 1 + break } return addrs @@ -104,7 +107,7 @@ function compile (val) { for (var i = 0; i < trust.length; i++) { val = trust[i] - if (!Object.prototype.hasOwnProperty.call(IP_RANGES, val)) { + if (val in IP_RANGES === false) { continue } @@ -129,7 +132,7 @@ function compileRangeSubnets (arr) { /* eslint-disable no-var */ for (var i = 0; i < arr.length; i++) { - rangeSubnets[i] = parseipNotation(arr[i]) + rangeSubnets[i] = parseIpNotation(arr[i]) } return rangeSubnets @@ -159,7 +162,7 @@ function compileTrust (rangeSubnets) { * @private */ -function parseipNotation (note) { +function parseIpNotation (note) { const pos = note.lastIndexOf('/') const str = pos !== -1 ? note.substring(0, pos) @@ -169,7 +172,7 @@ function parseipNotation (note) { throw new TypeError('invalid IP address: ' + str) } - let ip = parseip(str) + let ip = parseIp(str) if (pos === -1 && ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) { // Store as IPv4 @@ -211,7 +214,7 @@ function parseipNotation (note) { */ function parseNetmask (netmask) { - const ip = parseip(netmask) + const ip = parseIp(netmask) const kind = ip.kind() return kind === 'ipv4' @@ -236,10 +239,31 @@ function proxyaddr (req, trust) { throw new TypeError('trust argument is required') } - const addrs = alladdrs(req, trust) - const addr = addrs[addrs.length - 1] + if (typeof trust !== 'function') { + trust = compile(trust) + } + + // get addresses + const addrs = forwarded(req) + + switch (addrs.length) { + case 1: + return addrs[0] + case 2: + return trust(addrs[0], 0) + ? addrs[1] + : addrs[0] + default: { + /* eslint-disable no-var */ + for (var i = 0; i < addrs.length - 1; i++) { + if (trust(addrs[i], i)) continue + + return addrs[i] + } - return addr + return addrs[addrs.length - 1] + } + } } /** @@ -263,7 +287,7 @@ function trustMulti (subnets) { return function trust (addr) { if (isIP(addr) === 0) return false - const ip = parseip(addr) + const ip = parseIp(addr) let ipconv const kind = ip.kind() @@ -316,7 +340,7 @@ function trustSingle (subnet) { return function trust (addr) { if (isIP(addr) === 0) return false - let ip = parseip(addr) + let ip = parseIp(addr) const kind = ip.kind() if (kind !== subnetkind) { diff --git a/package.json b/package.json index 66fb887..3677bc4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "scripts": { "bench": "node benchmark/index.js", "lint": "standard", + "lint:fix": "standard --fix", "test": "npm run test:unit && npm run test:typescript", "test:typescript": "tsd", "test:unit": "tap" diff --git a/test/all.js b/test/all.js index 1942dc4..cc7cba6 100644 --- a/test/all.js +++ b/test/all.js @@ -8,7 +8,7 @@ test('argument req should be required', function (t) { t.end() }) -test('argument trustshould be optional', function (t) { +test('argument trust should be optional', function (t) { const req = createReq('127.0.0.1') t.doesNotThrow(proxyaddr.all.bind(null, req)) t.end() @@ -28,6 +28,14 @@ test('with x-forwarded-for header should include x-forwarded-for', function (t) t.end() }) +test('with x-forwarded-for header should include x-forwarded-for, using trust function', function (t) { + const req = createReq('127.0.0.1', { + 'x-forwarded-for': '10.0.0.1' + }) + t.same(proxyaddr.all(req, () => true), ['127.0.0.1', '10.0.0.1']) + t.end() +}) + test('with x-forwarded-for header should include x-forwarded-for in correct order', function (t) { const req = createReq('127.0.0.1', { 'x-forwarded-for': '10.0.0.1, 10.0.0.2' From ff3bf63ba00b03fa036aa964aed973b2e8c728fe Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 02:15:27 +0200 Subject: [PATCH 04/16] integrate prefixLengthFromSubnetMask --- index.js | 96 ++++++++++++++++++-------- test/prefix-length-from-subnet-mask.js | 47 +++++++++++++ 2 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 test/prefix-length-from-subnet-mask.js diff --git a/index.js b/index.js index 3000529..69573a6 100644 --- a/index.js +++ b/index.js @@ -7,17 +7,6 @@ 'use strict' -/** - * Module exports. - * @public - */ - -module.exports = proxyaddr -module.exports.default = proxyaddr -module.exports.proxyaddr = proxyaddr -module.exports.all = alladdrs -module.exports.compile = compile - /** * Module dependencies. * @private @@ -47,6 +36,20 @@ const IP_RANGES = { uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7'] } +// number of zeroes in octet +const NETMASK_PREFIX = { + __proto__: null, + '0': 8, + '128': 7, + '192': 6, + '224': 5, + '240': 4, + '248': 3, + '252': 2, + '254': 1, + '255': 0 +} + /** * Get all addresses in the request, optionally stopping * at the first untrusted. @@ -56,7 +59,7 @@ const IP_RANGES = { * @public */ -function alladdrs (req, trust) { +function alladdrs(req, trust) { if (!trust) { // Return all addresses return forwarded(req) @@ -88,7 +91,7 @@ function alladdrs (req, trust) { * @private */ -function compile (val) { +function compile(val) { if (!val) { throw new TypeError('argument is required') } @@ -127,7 +130,7 @@ function compile (val) { * @private */ -function compileRangeSubnets (arr) { +function compileRangeSubnets(arr) { const rangeSubnets = new Array(arr.length) /* eslint-disable no-var */ @@ -145,7 +148,7 @@ function compileRangeSubnets (arr) { * @private */ -function compileTrust (rangeSubnets) { +function compileTrust(rangeSubnets) { // Return optimized function based on length const len = rangeSubnets.length return len === 0 @@ -162,7 +165,7 @@ function compileTrust (rangeSubnets) { * @private */ -function parseIpNotation (note) { +function parseIpNotation(note) { const pos = note.lastIndexOf('/') const str = pos !== -1 ? note.substring(0, pos) @@ -193,8 +196,8 @@ function parseIpNotation (note) { range = max } else if (DIGIT_REGEXP.test(range)) { range = parseInt(range, 10) - } else if (kind === 'ipv4' && isIP(range) !== 0) { - range = parseNetmask(range) + } else if (kind === 'ipv4' && isIP(range) === 4) { + range = prefixLengthFromSubnetMask(range) } else { range = null } @@ -213,13 +216,34 @@ function parseIpNotation (note) { * @private */ -function parseNetmask (netmask) { - const ip = parseIp(netmask) - const kind = ip.kind() +function prefixLengthFromSubnetMask(netmask) { + let cidr = 0 - return kind === 'ipv4' - ? ip.prefixLengthFromSubnetMask() - : null + const octets = netmask.split('.') + let octet = octets[3] + let i = 3 + let zeros = 0 + let stop = false + + while (i > -1) { + if (octet in NETMASK_PREFIX) { + zeros = NETMASK_PREFIX[octet]; + if (stop && zeros !== 0) { + return null; + } + + if (zeros !== 8) { + stop = true; + } + + cidr += zeros; + } else { + return null; + } + octet = octets[--i]; + } + + return 32 - cidr; } /** @@ -230,7 +254,7 @@ function parseNetmask (netmask) { * @public */ -function proxyaddr (req, trust) { +function proxyaddr(req, trust) { if (!req) { throw new TypeError('req argument is required') } @@ -272,7 +296,7 @@ function proxyaddr (req, trust) { * @private */ -function trustNone () { +function trustNone() { return false } @@ -283,8 +307,8 @@ function trustNone () { * @private */ -function trustMulti (subnets) { - return function trust (addr) { +function trustMulti(subnets) { + return function trust(addr) { if (isIP(addr) === 0) return false const ip = parseIp(addr) @@ -331,13 +355,13 @@ function trustMulti (subnets) { * @private */ -function trustSingle (subnet) { +function trustSingle(subnet) { const subnetip = subnet[0] const subnetrange = subnet[1] const subnetkind = subnet[2] const subnetisipv4 = subnetkind === 'ipv4' - return function trust (addr) { + return function trust(addr) { if (isIP(addr) === 0) return false let ip = parseIp(addr) @@ -358,3 +382,15 @@ function trustSingle (subnet) { return ip.match(subnetip, subnetrange) } } + +/** + * Module exports. + * @public + */ + +module.exports = proxyaddr +module.exports.default = proxyaddr +module.exports.proxyaddr = proxyaddr +module.exports.all = alladdrs +module.exports.compile = compile +module.exports.prefixLengthFromSubnetMask = prefixLengthFromSubnetMask diff --git a/test/prefix-length-from-subnet-mask.js b/test/prefix-length-from-subnet-mask.js new file mode 100644 index 0000000..c35f60b --- /dev/null +++ b/test/prefix-length-from-subnet-mask.js @@ -0,0 +1,47 @@ +'use strict' + +const { test } = require('tap') +const { prefixLengthFromSubnetMask } = require('..') + +test('prefixLengthFromSubnetMask returns proper CIDR notation for standard IPv4 masks', (t) => { + t.plan(35) + + // positive cases + t.equal(prefixLengthFromSubnetMask('255.255.255.255'), 32); + t.equal(prefixLengthFromSubnetMask('255.255.255.254'), 31); + t.equal(prefixLengthFromSubnetMask('255.255.255.252'), 30); + t.equal(prefixLengthFromSubnetMask('255.255.255.248'), 29); + t.equal(prefixLengthFromSubnetMask('255.255.255.240'), 28); + t.equal(prefixLengthFromSubnetMask('255.255.255.224'), 27); + t.equal(prefixLengthFromSubnetMask('255.255.255.192'), 26); + t.equal(prefixLengthFromSubnetMask('255.255.255.128'), 25); + t.equal(prefixLengthFromSubnetMask('255.255.255.0'), 24); + t.equal(prefixLengthFromSubnetMask('255.255.254.0'), 23); + t.equal(prefixLengthFromSubnetMask('255.255.252.0'), 22); + t.equal(prefixLengthFromSubnetMask('255.255.248.0'), 21); + t.equal(prefixLengthFromSubnetMask('255.255.240.0'), 20); + t.equal(prefixLengthFromSubnetMask('255.255.224.0'), 19); + t.equal(prefixLengthFromSubnetMask('255.255.192.0'), 18); + t.equal(prefixLengthFromSubnetMask('255.255.128.0'), 17); + t.equal(prefixLengthFromSubnetMask('255.255.0.0'), 16); + t.equal(prefixLengthFromSubnetMask('255.254.0.0'), 15); + t.equal(prefixLengthFromSubnetMask('255.252.0.0'), 14); + t.equal(prefixLengthFromSubnetMask('255.248.0.0'), 13); + t.equal(prefixLengthFromSubnetMask('255.240.0.0'), 12); + t.equal(prefixLengthFromSubnetMask('255.224.0.0'), 11); + t.equal(prefixLengthFromSubnetMask('255.192.0.0'), 10); + t.equal(prefixLengthFromSubnetMask('255.128.0.0'), 9); + t.equal(prefixLengthFromSubnetMask('255.0.0.0'), 8); + t.equal(prefixLengthFromSubnetMask('254.0.0.0'), 7); + t.equal(prefixLengthFromSubnetMask('252.0.0.0'), 6); + t.equal(prefixLengthFromSubnetMask('248.0.0.0'), 5); + t.equal(prefixLengthFromSubnetMask('240.0.0.0'), 4); + t.equal(prefixLengthFromSubnetMask('224.0.0.0'), 3); + t.equal(prefixLengthFromSubnetMask('192.0.0.0'), 2); + t.equal(prefixLengthFromSubnetMask('128.0.0.0'), 1); + t.equal(prefixLengthFromSubnetMask('0.0.0.0'), 0); + + // negative cases + t.equal(prefixLengthFromSubnetMask('192.168.255.0'), null); + t.equal(prefixLengthFromSubnetMask('255.0.255.0'), null); +}) From 26e7925c628680129d1cb815ab286340ec699d83 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 02:23:43 +0200 Subject: [PATCH 05/16] fix titles --- test/base.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/base.js b/test/base.js index 2a31bba..003cca4 100644 --- a/test/base.js +++ b/test/base.js @@ -123,7 +123,7 @@ test('trust should be invoked as trust(addr, i)', function (t) { t.end() }) -test('with all trusted should return socket address wtesth no headers', function (t) { +test('with all trusted should return socket address with no headers', function (t) { const req = createReq('127.0.0.1') t.equal(proxyaddr(req, all), '127.0.0.1') t.end() @@ -145,13 +145,13 @@ test('with all trusted should return furthest header value', function (t) { t.end() }) -test('with none trusted should return socket address wtesth no headers', function (t) { +test('with none trusted should return socket address with no headers', function (t) { const req = createReq('127.0.0.1') t.equal(proxyaddr(req, none), '127.0.0.1') t.end() }) -test('with none trusted should return socket address wtesth headers', function (t) { +test('with none trusted should return socket address with headers', function (t) { const req = createReq('127.0.0.1', { 'x-forwarded-for': '10.0.0.1, 10.0.0.2' }) @@ -159,7 +159,7 @@ test('with none trusted should return socket address wtesth headers', function ( t.end() }) -test('with some trusted should return socket address wtesth no headers', function (t) { +test('with some trusted should return socket address with no headers', function (t) { const req = createReq('127.0.0.1') t.equal(proxyaddr(req, trust10x), '127.0.0.1') t.end() @@ -197,7 +197,7 @@ test('with some trusted should not skip untrusted', function (t) { t.end() }) -test('when given array should accept ltesteral IP addresses', function (t) { +test('when given array should accept literal IP addresses', function (t) { const req = createReq('10.0.0.1', { 'x-forwarded-for': '192.168.0.1, 10.0.0.2' }) @@ -227,7 +227,7 @@ test('when array empty should return socket address ', function (t) { t.end() }) -test('when array empty should return socket address wtesth headers', function (t) { +test('when array empty should return socket address with headers', function (t) { const req = createReq('127.0.0.1', { 'x-forwarded-for': '10.0.0.1, 10.0.0.2' }) @@ -235,7 +235,7 @@ test('when array empty should return socket address wtesth headers', function (t t.end() }) -test('when given IPv4 addresses should accept ltesteral IP addresses', function (t) { +test('when given IPv4 addresses should accept literal IP addresses', function (t) { const req = createReq('10.0.0.1', { 'x-forwarded-for': '192.168.0.1, 10.0.0.2' }) @@ -259,7 +259,7 @@ test('when given IPv4 addresses should accept netmask notation', function (t) { t.end() }) -test('when given IPv6 addresses should accept ltesteral IP addresses', function (t) { +test('when given IPv6 addresses should accept literal IP addresses', function (t) { const req = createReq('fe80::1', { 'x-forwarded-for': '2002:c000:203::1, fe80::2' }) @@ -323,7 +323,7 @@ test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped t.end() }) -test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped address mixed wtesth IPv6 CIDR', function (t) { +test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped address mixed with IPv6 CIDR', function (t) { const req = createReq('10.0.0.1', { 'x-forwarded-for': '192.168.0.1, 10.0.0.200' }) @@ -331,7 +331,7 @@ test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped t.end() }) -test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped address mixed wtesth IPv4 addresses', function (t) { +test('when IPv4-mapped IPv6 addresses should match CIDR notation for IPv4-mapped address mixed with IPv4 addresses', function (t) { const req = createReq('10.0.0.1', { 'x-forwarded-for': '192.168.0.1, 10.0.0.200' }) @@ -395,7 +395,7 @@ test('when socket address undefined should return undefined as address', functio t.end() }) -test('when socket address undefined should return undefined even wtesth trusted headers', function (t) { +test('when socket address undefined should return undefined even with trusted headers', function (t) { const req = createReq(undefined, { 'x-forwarded-for': '127.0.0.1, 10.0.0.1' }) From 94516ac6978ee574f3ed0a96d642e06d1ff53ac2 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 12:02:09 +0200 Subject: [PATCH 06/16] add isIPv4MappedIPv6Address --- index.js | 181 ++++++++++++++++--------- test/is-ipv4-mapped-ipv6-address.js | 35 +++++ test/prefix-length-from-subnet-mask.js | 76 +++++------ 3 files changed, 189 insertions(+), 103 deletions(-) create mode 100644 test/is-ipv4-mapped-ipv6-address.js diff --git a/index.js b/index.js index 69573a6..fe7a809 100644 --- a/index.js +++ b/index.js @@ -36,20 +36,6 @@ const IP_RANGES = { uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7'] } -// number of zeroes in octet -const NETMASK_PREFIX = { - __proto__: null, - '0': 8, - '128': 7, - '192': 6, - '224': 5, - '240': 4, - '248': 3, - '252': 2, - '254': 1, - '255': 0 -} - /** * Get all addresses in the request, optionally stopping * at the first untrusted. @@ -59,7 +45,7 @@ const NETMASK_PREFIX = { * @public */ -function alladdrs(req, trust) { +function alladdrs (req, trust) { if (!trust) { // Return all addresses return forwarded(req) @@ -91,7 +77,7 @@ function alladdrs(req, trust) { * @private */ -function compile(val) { +function compile (val) { if (!val) { throw new TypeError('argument is required') } @@ -130,7 +116,7 @@ function compile(val) { * @private */ -function compileRangeSubnets(arr) { +function compileRangeSubnets (arr) { const rangeSubnets = new Array(arr.length) /* eslint-disable no-var */ @@ -148,7 +134,7 @@ function compileRangeSubnets(arr) { * @private */ -function compileTrust(rangeSubnets) { +function compileTrust (rangeSubnets) { // Return optimized function based on length const len = rangeSubnets.length return len === 0 @@ -165,7 +151,7 @@ function compileTrust(rangeSubnets) { * @private */ -function parseIpNotation(note) { +function parseIpNotation (note) { const pos = note.lastIndexOf('/') const str = pos !== -1 ? note.substring(0, pos) @@ -177,7 +163,7 @@ function parseIpNotation(note) { let ip = parseIp(str) - if (pos === -1 && ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) { + if (pos === -1 && ip.kind() === 'ipv6' && isIPv4MappedIPv6Address(str)) { // Store as IPv4 ip = ip.toIPv4Address() } @@ -209,43 +195,6 @@ function parseIpNotation(note) { return [ip, range, kind] } -/** - * Parse netmask string into CIDR range. - * - * @param {String} netmask - * @private - */ - -function prefixLengthFromSubnetMask(netmask) { - let cidr = 0 - - const octets = netmask.split('.') - let octet = octets[3] - let i = 3 - let zeros = 0 - let stop = false - - while (i > -1) { - if (octet in NETMASK_PREFIX) { - zeros = NETMASK_PREFIX[octet]; - if (stop && zeros !== 0) { - return null; - } - - if (zeros !== 8) { - stop = true; - } - - cidr += zeros; - } else { - return null; - } - octet = octets[--i]; - } - - return 32 - cidr; -} - /** * Determine address of proxied request. * @@ -254,7 +203,7 @@ function prefixLengthFromSubnetMask(netmask) { * @public */ -function proxyaddr(req, trust) { +function proxyaddr (req, trust) { if (!req) { throw new TypeError('req argument is required') } @@ -296,7 +245,7 @@ function proxyaddr(req, trust) { * @private */ -function trustNone() { +function trustNone () { return false } @@ -307,13 +256,16 @@ function trustNone() { * @private */ -function trustMulti(subnets) { - return function trust(addr) { +function trustMulti (subnets) { + return function trust (addr) { if (isIP(addr) === 0) return false const ip = parseIp(addr) let ipconv const kind = ip.kind() + const isIPv4MappedAddress = kind === 'ipv6' + ? isIPv4MappedIPv6Address(addr) + : false /* eslint-disable no-var */ for (var i = 0; i < subnets.length; i++) { @@ -324,7 +276,7 @@ function trustMulti(subnets) { let trusted = ip if (kind !== subnetkind) { - if (subnetkind === 'ipv4' && !ip.isIPv4MappedAddress()) { + if (subnetkind === 'ipv4' && !isIPv4MappedAddress) { // Incompatible IP addresses continue } @@ -355,20 +307,23 @@ function trustMulti(subnets) { * @private */ -function trustSingle(subnet) { +function trustSingle (subnet) { const subnetip = subnet[0] const subnetrange = subnet[1] const subnetkind = subnet[2] const subnetisipv4 = subnetkind === 'ipv4' - return function trust(addr) { + return function trust (addr) { if (isIP(addr) === 0) return false let ip = parseIp(addr) const kind = ip.kind() + const isIPv4MappedAddress = kind === 'ipv6' + ? isIPv4MappedIPv6Address(addr) + : false if (kind !== subnetkind) { - if (subnetisipv4 && !ip.isIPv4MappedAddress()) { + if (subnetisipv4 && !isIPv4MappedAddress) { // Incompatible IP addresses return false } @@ -383,6 +338,101 @@ function trustSingle(subnet) { } } +// number of zeroes in octet +const NETMASK_PREFIX = { + __proto__: null, + 0: 8, + 128: 7, + 192: 6, + 224: 5, + 240: 4, + 248: 3, + 252: 2, + 254: 1, + 255: 0 +} + +/** + * Parse netmask string into CIDR range. + * + * @param {String} netmask + * @private + */ +function prefixLengthFromSubnetMask (netmask) { + let cidr = 0 + + const octets = netmask.split('.') + let octet = octets[3] + let i = 3 + let zeros = 0 + let stop = false + + while (i > -1) { + if (octet in NETMASK_PREFIX) { + zeros = NETMASK_PREFIX[octet] + if (stop && zeros !== 0) { + return null + } + + if (zeros !== 8) { + stop = true + } + + cidr += zeros + } else { + return null + } + octet = octets[--i] + } + + return 32 - cidr +} + +function isIPv4MappedIPv6Address (addr) { + if ( + addr[0] === ':' && + addr[1] === ':' + ) { + if ( + addr[2] === 'f' && + addr[3] === 'f' && + addr[4] === 'f' && + addr[5] === 'f' && + addr[6] === ':' + ) { + return true + } else if (isIP(addr.slice(2)) === 4) { + return true + } + } + + let group = 0 + for (var i = 0; i < addr.length; ++i) { + switch (addr[i]) { + case ':': + if (group === 5) { + return true + } + ++group + break + case '0': + if (group === 5) { + return false + } + break + case 'f': + if (group !== 5) { + return false + } + break + default: + return false + } + } + + return false +} + /** * Module exports. * @public @@ -394,3 +444,4 @@ module.exports.proxyaddr = proxyaddr module.exports.all = alladdrs module.exports.compile = compile module.exports.prefixLengthFromSubnetMask = prefixLengthFromSubnetMask +module.exports.isIPv4MappedIPv6Address = isIPv4MappedIPv6Address diff --git a/test/is-ipv4-mapped-ipv6-address.js b/test/is-ipv4-mapped-ipv6-address.js new file mode 100644 index 0000000..6586944 --- /dev/null +++ b/test/is-ipv4-mapped-ipv6-address.js @@ -0,0 +1,35 @@ +'use strict' + +const { test } = require('tap') +const { isIPv4MappedIPv6Address } = require('..') + +test('isIPv4MappedIPv6Address', (t) => { + t.plan(23) + + // positive cases + t.equal(isIPv4MappedIPv6Address('::ffff:'), true) + t.equal(isIPv4MappedIPv6Address('::ffff:192.168.1.10'), true) + t.equal(isIPv4MappedIPv6Address('::ffff:101:101'), true) + t.equal(isIPv4MappedIPv6Address('0:0:0:0:0:ffff:0101:0101'), true) + t.equal(isIPv4MappedIPv6Address('0000:0000:0000:0000:0000:ffff:0101:0101'), true) + t.equal(isIPv4MappedIPv6Address('::8.8.8.8'), true) + t.equal(isIPv4MappedIPv6Address('0:0:0:0:0:ffff:0:0'), true) + t.equal(isIPv4MappedIPv6Address('::ffff:c0a8:101'), true) + t.equal(isIPv4MappedIPv6Address('::ffff:192.168.1.1'), true) + + // negative cases + t.equal(isIPv4MappedIPv6Address('0000:0000:0000:0000:ffff:ffff:0101:0101'), false) + t.equal(isIPv4MappedIPv6Address('0000:0000:0000:ffff:ffff:ffff:0101:0101'), false) + t.equal(isIPv4MappedIPv6Address('f000:0000:0000:ffff:ffff:ffff:0101:0101'), false) + t.equal(isIPv4MappedIPv6Address('2001:0db8:85a3:0000:0000:8a2e:0370:7334'), false) + t.equal(isIPv4MappedIPv6Address('2001:db8:ff:abc:def:123b:456c:78d'), false) + t.equal(isIPv4MappedIPv6Address('invalid'), false) + t.equal(isIPv4MappedIPv6Address(''), false) + t.equal(isIPv4MappedIPv6Address('::'), false) + t.equal(isIPv4MappedIPv6Address('::1'), false) + t.equal(isIPv4MappedIPv6Address('0:ff::'), false) + t.equal(isIPv4MappedIPv6Address('::ff:0'), false) + t.equal(isIPv4MappedIPv6Address('::ff:0:0'), false) + t.equal(isIPv4MappedIPv6Address('0:0:0:0:0:0:ffff:0'), false) + t.equal(isIPv4MappedIPv6Address('::ff:ff:0:0:0'), false) +}) diff --git a/test/prefix-length-from-subnet-mask.js b/test/prefix-length-from-subnet-mask.js index c35f60b..8674b06 100644 --- a/test/prefix-length-from-subnet-mask.js +++ b/test/prefix-length-from-subnet-mask.js @@ -4,44 +4,44 @@ const { test } = require('tap') const { prefixLengthFromSubnetMask } = require('..') test('prefixLengthFromSubnetMask returns proper CIDR notation for standard IPv4 masks', (t) => { - t.plan(35) + t.plan(35) - // positive cases - t.equal(prefixLengthFromSubnetMask('255.255.255.255'), 32); - t.equal(prefixLengthFromSubnetMask('255.255.255.254'), 31); - t.equal(prefixLengthFromSubnetMask('255.255.255.252'), 30); - t.equal(prefixLengthFromSubnetMask('255.255.255.248'), 29); - t.equal(prefixLengthFromSubnetMask('255.255.255.240'), 28); - t.equal(prefixLengthFromSubnetMask('255.255.255.224'), 27); - t.equal(prefixLengthFromSubnetMask('255.255.255.192'), 26); - t.equal(prefixLengthFromSubnetMask('255.255.255.128'), 25); - t.equal(prefixLengthFromSubnetMask('255.255.255.0'), 24); - t.equal(prefixLengthFromSubnetMask('255.255.254.0'), 23); - t.equal(prefixLengthFromSubnetMask('255.255.252.0'), 22); - t.equal(prefixLengthFromSubnetMask('255.255.248.0'), 21); - t.equal(prefixLengthFromSubnetMask('255.255.240.0'), 20); - t.equal(prefixLengthFromSubnetMask('255.255.224.0'), 19); - t.equal(prefixLengthFromSubnetMask('255.255.192.0'), 18); - t.equal(prefixLengthFromSubnetMask('255.255.128.0'), 17); - t.equal(prefixLengthFromSubnetMask('255.255.0.0'), 16); - t.equal(prefixLengthFromSubnetMask('255.254.0.0'), 15); - t.equal(prefixLengthFromSubnetMask('255.252.0.0'), 14); - t.equal(prefixLengthFromSubnetMask('255.248.0.0'), 13); - t.equal(prefixLengthFromSubnetMask('255.240.0.0'), 12); - t.equal(prefixLengthFromSubnetMask('255.224.0.0'), 11); - t.equal(prefixLengthFromSubnetMask('255.192.0.0'), 10); - t.equal(prefixLengthFromSubnetMask('255.128.0.0'), 9); - t.equal(prefixLengthFromSubnetMask('255.0.0.0'), 8); - t.equal(prefixLengthFromSubnetMask('254.0.0.0'), 7); - t.equal(prefixLengthFromSubnetMask('252.0.0.0'), 6); - t.equal(prefixLengthFromSubnetMask('248.0.0.0'), 5); - t.equal(prefixLengthFromSubnetMask('240.0.0.0'), 4); - t.equal(prefixLengthFromSubnetMask('224.0.0.0'), 3); - t.equal(prefixLengthFromSubnetMask('192.0.0.0'), 2); - t.equal(prefixLengthFromSubnetMask('128.0.0.0'), 1); - t.equal(prefixLengthFromSubnetMask('0.0.0.0'), 0); + // positive cases + t.equal(prefixLengthFromSubnetMask('255.255.255.255'), 32) + t.equal(prefixLengthFromSubnetMask('255.255.255.254'), 31) + t.equal(prefixLengthFromSubnetMask('255.255.255.252'), 30) + t.equal(prefixLengthFromSubnetMask('255.255.255.248'), 29) + t.equal(prefixLengthFromSubnetMask('255.255.255.240'), 28) + t.equal(prefixLengthFromSubnetMask('255.255.255.224'), 27) + t.equal(prefixLengthFromSubnetMask('255.255.255.192'), 26) + t.equal(prefixLengthFromSubnetMask('255.255.255.128'), 25) + t.equal(prefixLengthFromSubnetMask('255.255.255.0'), 24) + t.equal(prefixLengthFromSubnetMask('255.255.254.0'), 23) + t.equal(prefixLengthFromSubnetMask('255.255.252.0'), 22) + t.equal(prefixLengthFromSubnetMask('255.255.248.0'), 21) + t.equal(prefixLengthFromSubnetMask('255.255.240.0'), 20) + t.equal(prefixLengthFromSubnetMask('255.255.224.0'), 19) + t.equal(prefixLengthFromSubnetMask('255.255.192.0'), 18) + t.equal(prefixLengthFromSubnetMask('255.255.128.0'), 17) + t.equal(prefixLengthFromSubnetMask('255.255.0.0'), 16) + t.equal(prefixLengthFromSubnetMask('255.254.0.0'), 15) + t.equal(prefixLengthFromSubnetMask('255.252.0.0'), 14) + t.equal(prefixLengthFromSubnetMask('255.248.0.0'), 13) + t.equal(prefixLengthFromSubnetMask('255.240.0.0'), 12) + t.equal(prefixLengthFromSubnetMask('255.224.0.0'), 11) + t.equal(prefixLengthFromSubnetMask('255.192.0.0'), 10) + t.equal(prefixLengthFromSubnetMask('255.128.0.0'), 9) + t.equal(prefixLengthFromSubnetMask('255.0.0.0'), 8) + t.equal(prefixLengthFromSubnetMask('254.0.0.0'), 7) + t.equal(prefixLengthFromSubnetMask('252.0.0.0'), 6) + t.equal(prefixLengthFromSubnetMask('248.0.0.0'), 5) + t.equal(prefixLengthFromSubnetMask('240.0.0.0'), 4) + t.equal(prefixLengthFromSubnetMask('224.0.0.0'), 3) + t.equal(prefixLengthFromSubnetMask('192.0.0.0'), 2) + t.equal(prefixLengthFromSubnetMask('128.0.0.0'), 1) + t.equal(prefixLengthFromSubnetMask('0.0.0.0'), 0) - // negative cases - t.equal(prefixLengthFromSubnetMask('192.168.255.0'), null); - t.equal(prefixLengthFromSubnetMask('255.0.255.0'), null); + // negative cases + t.equal(prefixLengthFromSubnetMask('192.168.255.0'), null) + t.equal(prefixLengthFromSubnetMask('255.0.255.0'), null) }) From 31a49691df06246b6fa363aa18791391203f2128 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 12:04:26 +0200 Subject: [PATCH 07/16] rename test files --- .taprc | 6 ++---- test/.eslintrc.yml | 2 -- test/{all.js => all.test.js} | 0 test/{base.js => base.test.js} | 0 test/{compile.js => compile.test.js} | 0 ...-ipv6-address.js => is-ipv4-mapped-ipv6-address.test.js} | 0 ...ubnet-mask.js => prefix-length-from-subnet-mask.test.js} | 0 7 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 test/.eslintrc.yml rename test/{all.js => all.test.js} (100%) rename test/{base.js => base.test.js} (100%) rename test/{compile.js => compile.test.js} (100%) rename test/{is-ipv4-mapped-ipv6-address.js => is-ipv4-mapped-ipv6-address.test.js} (100%) rename test/{prefix-length-from-subnet-mask.js => prefix-length-from-subnet-mask.test.js} (100%) diff --git a/.taprc b/.taprc index 2188e32..eb6eb3e 100644 --- a/.taprc +++ b/.taprc @@ -1,4 +1,2 @@ -ts: false -jsx: false -flow: false -coverage: true +files: + - test/**/*.test.js diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index 9808c3b..0000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/test/all.js b/test/all.test.js similarity index 100% rename from test/all.js rename to test/all.test.js diff --git a/test/base.js b/test/base.test.js similarity index 100% rename from test/base.js rename to test/base.test.js diff --git a/test/compile.js b/test/compile.test.js similarity index 100% rename from test/compile.js rename to test/compile.test.js diff --git a/test/is-ipv4-mapped-ipv6-address.js b/test/is-ipv4-mapped-ipv6-address.test.js similarity index 100% rename from test/is-ipv4-mapped-ipv6-address.js rename to test/is-ipv4-mapped-ipv6-address.test.js diff --git a/test/prefix-length-from-subnet-mask.js b/test/prefix-length-from-subnet-mask.test.js similarity index 100% rename from test/prefix-length-from-subnet-mask.js rename to test/prefix-length-from-subnet-mask.test.js From c4d4fb933f84e57c4657fff17e1032acc6ee374a Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 14:50:16 +0200 Subject: [PATCH 08/16] optimize prefixLengthFromSubnetMask --- benchmark/prefix-length-from-subnet-mask.js | 25 ++++++ index.js | 88 ++++++++++++--------- package.json | 3 +- test/prefix-length-from-subnet-mask.test.js | 11 ++- 4 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 benchmark/prefix-length-from-subnet-mask.js diff --git a/benchmark/prefix-length-from-subnet-mask.js b/benchmark/prefix-length-from-subnet-mask.js new file mode 100644 index 0000000..02e5723 --- /dev/null +++ b/benchmark/prefix-length-from-subnet-mask.js @@ -0,0 +1,25 @@ +'use strict' + +/** + * Globals for benchmark.js + */ +const { prefixLengthFromSubnetMask } = require('..') + +/** + * Module dependencies. + */ +const benchmark = require('benchmark') + +const suite = new benchmark.Suite() + +const ip = '255.255.254.0' + +suite.add('prefixLengthFromSubnetMask', function () { + prefixLengthFromSubnetMask(ip) +}) + +suite.on('cycle', function onCycle (event) { + console.log(String(event.target)) +}) + +suite.run({ async: false }) diff --git a/index.js b/index.js index fe7a809..869bb5d 100644 --- a/index.js +++ b/index.js @@ -338,18 +338,28 @@ function trustSingle (subnet) { } } -// number of zeroes in octet -const NETMASK_PREFIX = { - __proto__: null, - 0: 8, - 128: 7, - 192: 6, - 224: 5, - 240: 4, - 248: 3, - 252: 2, - 254: 1, - 255: 0 +function octetToZeros (ip, start, end) { + return ( + (ip[start] === '0' && 9) || + ((end - start) === 3 && ( + (ip[start] === '1' && ( + (ip[start + 1] === '2' && ip[start + 2] === '8' && 8) || + (ip[start + 1] === '9' && ip[start + 2] === '2' && 7) + )) || + (ip[start] === '2' && ( + (ip[start + 1] === '2' && ip[start + 2] === '4' && 6) || + (ip[start + 1] === '4' && ( + (ip[start + 2] === '0' && 5) || + (ip[start + 2] === '8' && 4) + )) || + (ip[start + 1] === '5' && ( + (ip[start + 2] === '2' && 3) || + (ip[start + 2] === '4' && 2) || + (ip[start + 2] === '5' && 1) + )) + )) + )) + ) || 0 } /** @@ -359,33 +369,37 @@ const NETMASK_PREFIX = { * @private */ function prefixLengthFromSubnetMask (netmask) { - let cidr = 0 - - const octets = netmask.split('.') - let octet = octets[3] - let i = 3 - let zeros = 0 let stop = false + let cidr = 0 - while (i > -1) { - if (octet in NETMASK_PREFIX) { - zeros = NETMASK_PREFIX[octet] - if (stop && zeros !== 0) { - return null - } - - if (zeros !== 8) { - stop = true - } - - cidr += zeros - } else { - return null - } - octet = octets[--i] - } - - return 32 - cidr + let end = netmask.length + let start = netmask.lastIndexOf('.', end - 1) + let zeros = octetToZeros(netmask, start + 1, end) + if (zeros === 0) return null + zeros !== 9 && (stop = true) + cidr += zeros + + end = start + start = netmask.lastIndexOf('.', end - 1) + zeros = octetToZeros(netmask, start + 1, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + end = start + start = netmask.lastIndexOf('.', end - 1) + zeros = octetToZeros(netmask, start + 1, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + end = start + zeros = octetToZeros(netmask, 0, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + return 36 - cidr } function isIPv4MappedIPv6Address (addr) { diff --git a/package.json b/package.json index 3677bc4..e0ea164 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "lint:fix": "standard --fix", "test": "npm run test:unit && npm run test:typescript", "test:typescript": "tsd", - "test:unit": "tap" + "test:unit": "tap", + "test:coverage": "tap --coverage-report=html" } } diff --git a/test/prefix-length-from-subnet-mask.test.js b/test/prefix-length-from-subnet-mask.test.js index 8674b06..8b8c6be 100644 --- a/test/prefix-length-from-subnet-mask.test.js +++ b/test/prefix-length-from-subnet-mask.test.js @@ -4,7 +4,7 @@ const { test } = require('tap') const { prefixLengthFromSubnetMask } = require('..') test('prefixLengthFromSubnetMask returns proper CIDR notation for standard IPv4 masks', (t) => { - t.plan(35) + t.plan(44) // positive cases t.equal(prefixLengthFromSubnetMask('255.255.255.255'), 32) @@ -42,6 +42,15 @@ test('prefixLengthFromSubnetMask returns proper CIDR notation for standard IPv4 t.equal(prefixLengthFromSubnetMask('0.0.0.0'), 0) // negative cases + t.equal(prefixLengthFromSubnetMask('255.255.255.253'), null) + t.equal(prefixLengthFromSubnetMask('168.192.255.0'), null) t.equal(prefixLengthFromSubnetMask('192.168.255.0'), null) t.equal(prefixLengthFromSubnetMask('255.0.255.0'), null) + t.equal(prefixLengthFromSubnetMask('255.0.255.42'), null) + t.equal(prefixLengthFromSubnetMask('255.0.42.0'), null) + t.equal(prefixLengthFromSubnetMask('255.42.0.0'), null) + t.equal(prefixLengthFromSubnetMask('42.0.0.0'), null) + t.equal(prefixLengthFromSubnetMask('255.255.42.254'), null) + t.equal(prefixLengthFromSubnetMask('255.255.192.255'), null) + t.equal(prefixLengthFromSubnetMask('192.255.255.255'), null) }) From 22d6d29d27a69b6288159af14deda09138e20a3e Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 15:41:52 +0200 Subject: [PATCH 09/16] reduce branches --- index.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 869bb5d..7d7a470 100644 --- a/index.js +++ b/index.js @@ -51,9 +51,7 @@ function alladdrs (req, trust) { return forwarded(req) } - if (typeof trust !== 'function') { - trust = compile(trust) - } + typeof trust !== 'function' && (trust = compile(trust)) // get addresses const addrs = forwarded(req) @@ -96,14 +94,12 @@ function compile (val) { for (var i = 0; i < trust.length; i++) { val = trust[i] - if (val in IP_RANGES === false) { - continue + if (val in IP_RANGES) { + // Splice in pre-defined range + val = IP_RANGES[val] + trust.splice.apply(trust, [i, 1].concat(val)) + i += val.length - 1 } - - // Splice in pre-defined range - val = IP_RANGES[val] - trust.splice.apply(trust, [i, 1].concat(val)) - i += val.length - 1 } return compileTrust(compileRangeSubnets(trust)) @@ -212,9 +208,7 @@ function proxyaddr (req, trust) { throw new TypeError('trust argument is required') } - if (typeof trust !== 'function') { - trust = compile(trust) - } + typeof trust !== 'function' && (trust = compile(trust)) // get addresses const addrs = forwarded(req) @@ -229,9 +223,9 @@ function proxyaddr (req, trust) { default: { /* eslint-disable no-var */ for (var i = 0; i < addrs.length - 1; i++) { - if (trust(addrs[i], i)) continue - - return addrs[i] + if (trust(addrs[i], i) === false) { + return addrs[i] + } } return addrs[addrs.length - 1] From de21f923d4664f5a1b16f02ea993a4e967f7d7fd Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 16:08:47 +0200 Subject: [PATCH 10/16] improve --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 7d7a470..c6145de 100644 --- a/index.js +++ b/index.js @@ -59,10 +59,10 @@ function alladdrs (req, trust) { const len = addrs.length - 1 /* eslint-disable no-var */ for (var i = 0; i < len; i++) { - if (trust(addrs[i], i)) continue - - addrs.length = i + 1 - break + if (trust(addrs[i], i) === false) { + addrs.length = i + 1 + break + } } return addrs From e2718540841c647a49a59392dfb2acc60efafe0c Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sat, 2 Sep 2023 19:20:18 +0200 Subject: [PATCH 11/16] minor changes --- index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index c6145de..309828a 100644 --- a/index.js +++ b/index.js @@ -113,10 +113,11 @@ function compile (val) { */ function compileRangeSubnets (arr) { - const rangeSubnets = new Array(arr.length) + const len = arr.length + const rangeSubnets = new Array(len) /* eslint-disable no-var */ - for (var i = 0; i < arr.length; i++) { + for (var i = 0; i < len; i++) { rangeSubnets[i] = parseIpNotation(arr[i]) } From abb0fc5c44a3c9cf617bdc5fa348add971eb2be0 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 10 Sep 2023 19:37:19 +0200 Subject: [PATCH 12/16] improve --- benchmark/prefix-length-from-subnet-mask.js | 2 +- index.js | 114 +------------------- lib/is-ipv4-mapped-ipv6-address.js | 58 ++++++++++ lib/prefix-length-from-subnet-mask.js | 69 ++++++++++++ test/is-ipv4-mapped-ipv6-address.test.js | 2 +- test/prefix-length-from-subnet-mask.test.js | 2 +- 6 files changed, 132 insertions(+), 115 deletions(-) create mode 100644 lib/is-ipv4-mapped-ipv6-address.js create mode 100644 lib/prefix-length-from-subnet-mask.js diff --git a/benchmark/prefix-length-from-subnet-mask.js b/benchmark/prefix-length-from-subnet-mask.js index 02e5723..990bd71 100644 --- a/benchmark/prefix-length-from-subnet-mask.js +++ b/benchmark/prefix-length-from-subnet-mask.js @@ -3,7 +3,7 @@ /** * Globals for benchmark.js */ -const { prefixLengthFromSubnetMask } = require('..') +const { prefixLengthFromSubnetMask } = require('../lib/prefix-length-from-subnet-mask') /** * Module dependencies. diff --git a/index.js b/index.js index 309828a..cdae776 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,8 @@ const forwarded = require('@fastify/forwarded') const ipaddr = require('ipaddr.js') const { isIP } = require('net') - +const { isIPv4MappedIPv6Address } = require('./lib/is-ipv4-mapped-ipv6-address') +const { prefixLengthFromSubnetMask } = require('./lib/prefix-length-from-subnet-mask') /** * Variables. * @private @@ -333,115 +334,6 @@ function trustSingle (subnet) { } } -function octetToZeros (ip, start, end) { - return ( - (ip[start] === '0' && 9) || - ((end - start) === 3 && ( - (ip[start] === '1' && ( - (ip[start + 1] === '2' && ip[start + 2] === '8' && 8) || - (ip[start + 1] === '9' && ip[start + 2] === '2' && 7) - )) || - (ip[start] === '2' && ( - (ip[start + 1] === '2' && ip[start + 2] === '4' && 6) || - (ip[start + 1] === '4' && ( - (ip[start + 2] === '0' && 5) || - (ip[start + 2] === '8' && 4) - )) || - (ip[start + 1] === '5' && ( - (ip[start + 2] === '2' && 3) || - (ip[start + 2] === '4' && 2) || - (ip[start + 2] === '5' && 1) - )) - )) - )) - ) || 0 -} - -/** - * Parse netmask string into CIDR range. - * - * @param {String} netmask - * @private - */ -function prefixLengthFromSubnetMask (netmask) { - let stop = false - let cidr = 0 - - let end = netmask.length - let start = netmask.lastIndexOf('.', end - 1) - let zeros = octetToZeros(netmask, start + 1, end) - if (zeros === 0) return null - zeros !== 9 && (stop = true) - cidr += zeros - - end = start - start = netmask.lastIndexOf('.', end - 1) - zeros = octetToZeros(netmask, start + 1, end) - if (zeros !== 1 && (stop === true || zeros === 0)) return null - stop === false && zeros !== 9 && (stop = true) - cidr += zeros - - end = start - start = netmask.lastIndexOf('.', end - 1) - zeros = octetToZeros(netmask, start + 1, end) - if (zeros !== 1 && (stop === true || zeros === 0)) return null - stop === false && zeros !== 9 && (stop = true) - cidr += zeros - - end = start - zeros = octetToZeros(netmask, 0, end) - if (zeros !== 1 && (stop === true || zeros === 0)) return null - stop === false && zeros !== 9 && (stop = true) - cidr += zeros - - return 36 - cidr -} - -function isIPv4MappedIPv6Address (addr) { - if ( - addr[0] === ':' && - addr[1] === ':' - ) { - if ( - addr[2] === 'f' && - addr[3] === 'f' && - addr[4] === 'f' && - addr[5] === 'f' && - addr[6] === ':' - ) { - return true - } else if (isIP(addr.slice(2)) === 4) { - return true - } - } - - let group = 0 - for (var i = 0; i < addr.length; ++i) { - switch (addr[i]) { - case ':': - if (group === 5) { - return true - } - ++group - break - case '0': - if (group === 5) { - return false - } - break - case 'f': - if (group !== 5) { - return false - } - break - default: - return false - } - } - - return false -} - /** * Module exports. * @public @@ -452,5 +344,3 @@ module.exports.default = proxyaddr module.exports.proxyaddr = proxyaddr module.exports.all = alladdrs module.exports.compile = compile -module.exports.prefixLengthFromSubnetMask = prefixLengthFromSubnetMask -module.exports.isIPv4MappedIPv6Address = isIPv4MappedIPv6Address diff --git a/lib/is-ipv4-mapped-ipv6-address.js b/lib/is-ipv4-mapped-ipv6-address.js new file mode 100644 index 0000000..3db59ab --- /dev/null +++ b/lib/is-ipv4-mapped-ipv6-address.js @@ -0,0 +1,58 @@ +'use strict' + +const { isIP } = require('net') + +/** + * Determine if `addr` is a IPv4-mapped IPv6 address. + * + * @param {string} addr + * @returns {boolean} + */ +function isIPv4MappedIPv6Address (addr) { + if ( + addr[0] === ':' && + addr[1] === ':' + ) { + if ( + addr[2] === 'f' && + addr[3] === 'f' && + addr[4] === 'f' && + addr[5] === 'f' && + addr[6] === ':' + ) { + return true + } else if (isIP(addr.slice(2)) === 4) { + return true + } + } + + let group = 0 + for (let i = 0; i < addr.length; ++i) { + switch (addr[i]) { + case ':': + if (group === 5) { + return true + } + ++group + break + case '0': + if (group === 5) { + return false + } + break + case 'f': + if (group !== 5) { + return false + } + break + default: + return false + } + } + + return false +} + +module.exports = { + isIPv4MappedIPv6Address +} diff --git a/lib/prefix-length-from-subnet-mask.js b/lib/prefix-length-from-subnet-mask.js new file mode 100644 index 0000000..bc2e85e --- /dev/null +++ b/lib/prefix-length-from-subnet-mask.js @@ -0,0 +1,69 @@ +'use strict' + +/** + * Parse netmask string into CIDR range. + * + * @param {String} netmask + * @private + */ +function prefixLengthFromSubnetMask (netmask) { + let stop = false + let cidr = 0 + + let end = netmask.length + let start = netmask.lastIndexOf('.', end - 1) + let zeros = octetToZeros(netmask, start + 1, end) + if (zeros === 0) return null + zeros !== 9 && (stop = true) + cidr += zeros + + end = start + start = netmask.lastIndexOf('.', end - 1) + zeros = octetToZeros(netmask, start + 1, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + end = start + start = netmask.lastIndexOf('.', end - 1) + zeros = octetToZeros(netmask, start + 1, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + end = start + zeros = octetToZeros(netmask, 0, end) + if (zeros !== 1 && (stop === true || zeros === 0)) return null + stop === false && zeros !== 9 && (stop = true) + cidr += zeros + + return 36 - cidr +} + +function octetToZeros (ip, start, end) { + return ( + (ip[start] === '0' && 9) || + ((end - start) === 3 && ( + (ip[start] === '1' && ( + (ip[start + 1] === '2' && ip[start + 2] === '8' && 8) || + (ip[start + 1] === '9' && ip[start + 2] === '2' && 7) + )) || + (ip[start] === '2' && ( + (ip[start + 1] === '2' && ip[start + 2] === '4' && 6) || + (ip[start + 1] === '4' && ( + (ip[start + 2] === '0' && 5) || + (ip[start + 2] === '8' && 4) + )) || + (ip[start + 1] === '5' && ( + (ip[start + 2] === '2' && 3) || + (ip[start + 2] === '4' && 2) || + (ip[start + 2] === '5' && 1) + )) + )) + )) + ) || 0 +} + +module.exports = { + prefixLengthFromSubnetMask +} diff --git a/test/is-ipv4-mapped-ipv6-address.test.js b/test/is-ipv4-mapped-ipv6-address.test.js index 6586944..c8b3ad0 100644 --- a/test/is-ipv4-mapped-ipv6-address.test.js +++ b/test/is-ipv4-mapped-ipv6-address.test.js @@ -1,7 +1,7 @@ 'use strict' const { test } = require('tap') -const { isIPv4MappedIPv6Address } = require('..') +const { isIPv4MappedIPv6Address } = require('../lib/is-ipv4-mapped-ipv6-address.js') test('isIPv4MappedIPv6Address', (t) => { t.plan(23) diff --git a/test/prefix-length-from-subnet-mask.test.js b/test/prefix-length-from-subnet-mask.test.js index 8b8c6be..ac5356e 100644 --- a/test/prefix-length-from-subnet-mask.test.js +++ b/test/prefix-length-from-subnet-mask.test.js @@ -1,7 +1,7 @@ 'use strict' const { test } = require('tap') -const { prefixLengthFromSubnetMask } = require('..') +const { prefixLengthFromSubnetMask } = require('../lib/prefix-length-from-subnet-mask') test('prefixLengthFromSubnetMask returns proper CIDR notation for standard IPv4 masks', (t) => { t.plan(44) From c1b6a71af0ff7604bf5366334edfd225d7f13ace Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 10 Sep 2023 19:40:47 +0200 Subject: [PATCH 13/16] fix require --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index cdae776..03c7952 100644 --- a/index.js +++ b/index.js @@ -12,9 +12,9 @@ * @private */ +const { isIP } = require('node:net') const forwarded = require('@fastify/forwarded') const ipaddr = require('ipaddr.js') -const { isIP } = require('net') const { isIPv4MappedIPv6Address } = require('./lib/is-ipv4-mapped-ipv6-address') const { prefixLengthFromSubnetMask } = require('./lib/prefix-length-from-subnet-mask') /** From ae10784982cba6a70dd4b1fd99bc355cded678af Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 10 Sep 2023 19:57:39 +0200 Subject: [PATCH 14/16] extract isInteger --- index.js | 14 ++++---------- lib/is-integer.js | 15 +++++++++++++++ test/is-integer.test.js | 13 +++++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 lib/is-integer.js create mode 100644 test/is-integer.test.js diff --git a/index.js b/index.js index 03c7952..e017dd2 100644 --- a/index.js +++ b/index.js @@ -13,17 +13,11 @@ */ const { isIP } = require('node:net') -const forwarded = require('@fastify/forwarded') -const ipaddr = require('ipaddr.js') +const { forwarded } = require('@fastify/forwarded') +const { parse: parseIp } = require('ipaddr.js') const { isIPv4MappedIPv6Address } = require('./lib/is-ipv4-mapped-ipv6-address') const { prefixLengthFromSubnetMask } = require('./lib/prefix-length-from-subnet-mask') -/** - * Variables. - * @private - */ - -const DIGIT_REGEXP = /^[0-9]+$/ -const parseIp = ipaddr.parse +const { isInteger } = require('./lib/is-integer') /** * Pre-defined IP ranges. @@ -178,7 +172,7 @@ function parseIpNotation (note) { if (range === null) { range = max - } else if (DIGIT_REGEXP.test(range)) { + } else if (isInteger(range)) { range = parseInt(range, 10) } else if (kind === 'ipv4' && isIP(range) === 4) { range = prefixLengthFromSubnetMask(range) diff --git a/lib/is-integer.js b/lib/is-integer.js new file mode 100644 index 0000000..26b0e6c --- /dev/null +++ b/lib/is-integer.js @@ -0,0 +1,15 @@ +'use strict' + +const isIntegerRE = /^[0-9]+$/ + +/** + * @function isInteger + * @description Test if a string is an integer + * @param {string} str + * @returns {boolean} + */ +const isInteger = isIntegerRE.test.bind(isIntegerRE) + +module.exports = { + isInteger +} diff --git a/test/is-integer.test.js b/test/is-integer.test.js new file mode 100644 index 0000000..2896cf0 --- /dev/null +++ b/test/is-integer.test.js @@ -0,0 +1,13 @@ +'use strict' + +const { test } = require('tap') +const { isInteger } = require('../lib/is-integer.js') + +test('isInteger', function (t) { + t.plan(3) + + t.ok(isInteger('1')) + t.ok(isInteger('1337')) + + t.notOk(isInteger('1.1')) +}) From 8ad7c344a1ef573b4532e604486dff0e039f0d41 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 10 Sep 2023 20:28:01 +0200 Subject: [PATCH 15/16] improve --- index.js | 53 ++++++++++++++++++++------- lib/is-ipv4-mapped-ipv6-address.js | 2 +- lib/prefix-length-from-subnet-mask.js | 8 +++- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index e017dd2..5ec4173 100644 --- a/index.js +++ b/index.js @@ -31,25 +31,32 @@ const IP_RANGES = { uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7'] } +/** + * @callback TrustFunction + * @param {string} addr + * @param {number} i + * @returns {boolean} + */ + /** * Get all addresses in the request, optionally stopping * at the first untrusted. * * @param {Object} request - * @param {Function|Array|String} [trust] + * @param {TrustFunction|Array|String} [trust] * @public */ -function alladdrs (req, trust) { +function alladdrs (request, trust) { if (!trust) { // Return all addresses - return forwarded(req) + return forwarded(request) } typeof trust !== 'function' && (trust = compile(trust)) // get addresses - const addrs = forwarded(req) + const addrs = forwarded(request) const len = addrs.length - 1 /* eslint-disable no-var */ @@ -67,6 +74,7 @@ function alladdrs (req, trust) { * Compile argument into trust function. * * @param {Array|String} val + * @returns {TrustFunction} * @private */ @@ -75,6 +83,9 @@ function compile (val) { throw new TypeError('argument is required') } + /** + * @type {string[]} + */ let trust if (typeof val === 'string') { @@ -103,8 +114,9 @@ function compile (val) { /** * Compile `arr` elements into range subnets. * - * @param {Array} arr * @private + * + * @param {Array} arr */ function compileRangeSubnets (arr) { @@ -122,8 +134,10 @@ function compileRangeSubnets (arr) { /** * Compile range subnet array into trust function. * - * @param {Array} rangeSubnets * @private + * + * @param {Array} rangeSubnets + * @returns {TrustFunction} */ function compileTrust (rangeSubnets) { @@ -139,8 +153,9 @@ function compileTrust (rangeSubnets) { /** * Parse IP notation string into range subnet. * - * @param {String} note * @private + * + * @param {String} note */ function parseIpNotation (note) { @@ -166,6 +181,9 @@ function parseIpNotation (note) { ? 128 : 32 + /** + * @type {number|string} + */ let range = pos !== -1 ? note.substring(pos + 1, note.length) : null @@ -190,13 +208,14 @@ function parseIpNotation (note) { /** * Determine address of proxied request. * - * @param {Object} request - * @param {Function|Array|String} trust * @public + * + * @param {Object} request + * @param {TrustFunction|Array|String} trust */ -function proxyaddr (req, trust) { - if (!req) { +function proxyaddr (request, trust) { + if (!request) { throw new TypeError('req argument is required') } @@ -207,7 +226,7 @@ function proxyaddr (req, trust) { typeof trust !== 'function' && (trust = compile(trust)) // get addresses - const addrs = forwarded(req) + const addrs = forwarded(request) switch (addrs.length) { case 1: @@ -233,6 +252,8 @@ function proxyaddr (req, trust) { * Static trust function to trust nothing. * * @private + * + * @type {TrustFunction} */ function trustNone () { @@ -242,8 +263,10 @@ function trustNone () { /** * Compile trust function for multiple subnets. * - * @param {Array} subnets * @private + * + * @param {Array} subnets + * @returns {TrustFunction} */ function trustMulti (subnets) { @@ -293,8 +316,10 @@ function trustMulti (subnets) { /** * Compile trust function for single subnet. * - * @param {Object} subnet * @private + * + * @param {Object} subnet + * @returns {TrustFunction} */ function trustSingle (subnet) { diff --git a/lib/is-ipv4-mapped-ipv6-address.js b/lib/is-ipv4-mapped-ipv6-address.js index 3db59ab..78fc65c 100644 --- a/lib/is-ipv4-mapped-ipv6-address.js +++ b/lib/is-ipv4-mapped-ipv6-address.js @@ -1,6 +1,6 @@ 'use strict' -const { isIP } = require('net') +const { isIP } = require('node:net') /** * Determine if `addr` is a IPv4-mapped IPv6 address. diff --git a/lib/prefix-length-from-subnet-mask.js b/lib/prefix-length-from-subnet-mask.js index bc2e85e..73b4a02 100644 --- a/lib/prefix-length-from-subnet-mask.js +++ b/lib/prefix-length-from-subnet-mask.js @@ -4,7 +4,7 @@ * Parse netmask string into CIDR range. * * @param {String} netmask - * @private + * @returns {Number} */ function prefixLengthFromSubnetMask (netmask) { let stop = false @@ -40,6 +40,12 @@ function prefixLengthFromSubnetMask (netmask) { return 36 - cidr } +/** + * @param {String} ip + * @param {Number} start + * @param {Number} end + * @returns {Number} + */ function octetToZeros (ip, start, end) { return ( (ip[start] === '0' && 9) || From 73c11be4d486784c125980148b39ce2fe21eeeff Mon Sep 17 00:00:00 2001 From: uzlopak Date: Sun, 10 Sep 2023 21:03:50 +0200 Subject: [PATCH 16/16] fix linting --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 5ec4173..d386c8c 100644 --- a/index.js +++ b/index.js @@ -317,7 +317,7 @@ function trustMulti (subnets) { * Compile trust function for single subnet. * * @private - * + * * @param {Object} subnet * @returns {TrustFunction} */