From 94f82bd4f2bace0c228bbbfe70073a71536fcbdf Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 16:59:20 +0800 Subject: [PATCH 1/6] lib: use Node.js net lib and reject malformed addresses --- lib/ip.js | 48 ++++++++++++++++-------------------------------- test/api-test.js | 42 ++---------------------------------------- 2 files changed, 18 insertions(+), 72 deletions(-) diff --git a/lib/ip.js b/lib/ip.js index 9022443..9993db8 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -1,6 +1,7 @@ const ip = exports; const { Buffer } = require('buffer'); const os = require('os'); +const net = require('net'); ip.toBuffer = function (ip, buff, offset) { offset = ~~offset; @@ -82,15 +83,17 @@ ip.toString = function (buff, offset, length) { return result; }; -const ipv4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/; -const ipv6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i; +ip.isV4Format = net.isIPv4; -ip.isV4Format = function (ip) { - return ipv4Regex.test(ip); -}; +ip.isV6Format = net.isIPv6; -ip.isV6Format = function (ip) { - return ipv6Regex.test(ip); +ip.normalize = function (addr) { + const family = net.isIPv4(addr) ? 'ipv4' : net.isIPv6(addr) ? 'ipv6' : null; + if (family === null) { + throw new Error(`Invalid ip address: ${addr}`); + } + const { address } = new net.SocketAddress({ address: addr, family }); + return address; }; function _normalizeFamily(family) { @@ -306,26 +309,13 @@ ip.isEqual = function (a, b) { }; ip.isPrivate = function (addr) { - // check loopback addresses first - if (ip.isLoopback(addr)) { - return true; - } - - // ensure the ipv4 address is valid - if (!ip.isV6Format(addr)) { - const ipl = ip.normalizeToLong(addr); - if (ipl < 0) { - throw new Error('invalid ipv4 address'); - } - // normalize the address for the private range checks that follow - addr = ip.fromLong(ipl); - } + addr = ip.normalize(addr); // check private ranges - return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) + return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) + || /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) - || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i - .test(addr) + || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^f[cd][0-9a-f]{2}:/i.test(addr) || /^fe80:/i.test(addr) @@ -338,15 +328,9 @@ ip.isPublic = function (addr) { }; ip.isLoopback = function (addr) { - // If addr is an IPv4 address in long integer form (no dots and no colons), convert it - if (!/\./.test(addr) && !/:/.test(addr)) { - addr = ip.fromLong(Number(addr)); - } + addr = ip.normalize(addr); - return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ - .test(addr) - || /^0177\./.test(addr) - || /^0x7f\./i.test(addr) + return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) || /^fe80::1$/i.test(addr) || /^::1$/.test(addr) || /^::$/.test(addr); diff --git a/test/api-test.js b/test/api-test.js index 0db838d..4ff0964 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -352,8 +352,8 @@ describe('IP library for node.js', () => { assert.equal(ip.isPrivate('fe80::1'), true); }); - it('should correctly identify hexadecimal IP addresses like \'0x7f.1\' as private', () => { - assert.equal(ip.isPrivate('0x7f.1'), true); + it('should reject hexadecimal IP addresses like "0x7f.1"', () => { + assert.throws(() => ip.isPrivate('0x7f.1')); }); }); @@ -468,42 +468,4 @@ describe('IP library for node.js', () => { assert.equal(ip.fromLong(4294967295), '255.255.255.255'); }); }); - - // IPv4 loopback in octal notation - it('should return true for octal representation "0177.0.0.1"', () => { - assert.equal(ip.isLoopback('0177.0.0.1'), true); - }); - - it('should return true for octal representation "0177.0.1"', () => { - assert.equal(ip.isLoopback('0177.0.1'), true); - }); - - it('should return true for octal representation "0177.1"', () => { - assert.equal(ip.isLoopback('0177.1'), true); - }); - - // IPv4 loopback in hexadecimal notation - it('should return true for hexadecimal representation "0x7f.0.0.1"', () => { - assert.equal(ip.isLoopback('0x7f.0.0.1'), true); - }); - - // IPv4 loopback in hexadecimal notation - it('should return true for hexadecimal representation "0x7f.0.1"', () => { - assert.equal(ip.isLoopback('0x7f.0.1'), true); - }); - - // IPv4 loopback in hexadecimal notation - it('should return true for hexadecimal representation "0x7f.1"', () => { - assert.equal(ip.isLoopback('0x7f.1'), true); - }); - - // IPv4 loopback as a single long integer - it('should return true for single long integer representation "2130706433"', () => { - assert.equal(ip.isLoopback('2130706433'), true); - }); - - // IPv4 non-loopback address - it('should return false for "192.168.1.1"', () => { - assert.equal(ip.isLoopback('192.168.1.1'), false); - }); }); From 6fde6b448847430493c465b811933c4b48536de1 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 17:31:18 +0800 Subject: [PATCH 2/6] lib: add normalizeStrict and normalizeLax --- lib/ip.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/ip.js b/lib/ip.js index 9993db8..a73c381 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -87,7 +87,7 @@ ip.isV4Format = net.isIPv4; ip.isV6Format = net.isIPv6; -ip.normalize = function (addr) { +ip.normalizeStrict = function (addr) { const family = net.isIPv4(addr) ? 'ipv4' : net.isIPv6(addr) ? 'ipv6' : null; if (family === null) { throw new Error(`Invalid ip address: ${addr}`); @@ -309,7 +309,7 @@ ip.isEqual = function (a, b) { }; ip.isPrivate = function (addr) { - addr = ip.normalize(addr); + addr = ip.normalizeStrict(addr); // check private ranges return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) @@ -328,7 +328,7 @@ ip.isPublic = function (addr) { }; ip.isLoopback = function (addr) { - addr = ip.normalize(addr); + addr = ip.normalizeStrict(addr); return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) || /^fe80::1$/i.test(addr) @@ -427,6 +427,8 @@ ip.fromLong = function (ipl) { }; ip.normalizeToLong = function (addr) { + if (typeof addr !== 'string') return -1; + const parts = addr.split('.').map(part => { // Handle hexadecimal format if (part.startsWith('0x') || part.startsWith('0X')) { @@ -473,3 +475,15 @@ ip.normalizeToLong = function (addr) { return val >>> 0; }; + +ip.normalizeLax = function(addr) { + if (ip.isV6Format(addr)) { + const { address } = new net.SocketAddress({ address: addr, family: 'ipv6' }); + return address; + } + const ipl = ip.normalizeToLong(addr); + if (ipl < 0) { + throw Error(`Invalid ip address: ${addr}`); + } + return ip.fromLong(ipl); +}; From 76ba2dfb92bfa576a2ac818ba36971d09ffc4d28 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 17:46:34 +0800 Subject: [PATCH 3/6] lib: add tests and examples --- README.md | 8 ++++++- test/api-test.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 22e5819..334f604 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,16 @@ ip.cidrSubnet('192.168.1.134/26') // range checking ip.cidrSubnet('192.168.1.134/26').contains('192.168.1.190') // true - // ipv4 long conversion ip.toLong('127.0.0.1'); // 2130706433 ip.fromLong(2130706433); // '127.0.0.1' + +// malformed addresses and normalization +ip.normalizeStrict('0::01'); // '::1' +ip.isPrivate('0x7f.1'); // throw error +ip.normalizeStrict('0x7f.1'); // throw error +var normalized = ip.normalizeLax('0x7f.1'); // 127.0.0.1 +ip.isPrivate(normalized); // true ``` ### License diff --git a/test/api-test.js b/test/api-test.js index 4ff0964..7a477a0 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -251,7 +251,7 @@ describe('IP library for node.js', () => { }); }); - describe('normalizeIpv4() method', () => { + describe('normalizeToLong() method', () => { // Testing valid inputs with different notations it('should correctly normalize "127.0.0.1"', () => { assert.equal(ip.normalizeToLong('127.0.0.1'), 2130706433); @@ -468,4 +468,64 @@ describe('IP library for node.js', () => { assert.equal(ip.fromLong(4294967295), '255.255.255.255'); }); }); + + describe('normalizeStrict() method', () => { + it('should keep valid IPv4 addresses', () => { + assert.equal(ip.normalizeStrict('1.1.1.1'), '1.1.1.1'); + }); + + it('should normalize IPv6 leading zeros', () => { + assert.equal(ip.normalizeStrict('00:0::000:01'), '::1'); + }); + + it('should normalize IPv6 letter casing', () => { + assert.equal(ip.normalizeStrict('aBCd::eF12'), 'abcd::ef12'); + }); + + it('should normalize IPv6 addresses with embedded IPv4 addresses', () => { + assert.equal(ip.normalizeStrict('::ffff:7f00:1'), '::ffff:127.0.0.1'); + assert.equal(ip.normalizeStrict('::1234:5678'), '::18.52.86.120'); + }); + + it('should reject malformed addresses', () => { + assert.throws(() => ip.normalizeStrict('127.0.1')); + assert.throws(() => ip.normalizeStrict('0x7f.1')); + assert.throws(() => ip.normalizeStrict('012.1')); + }); + }); + + describe('normalizeLax() method', () => { + it('should normalize hex and oct addresses', () => { + assert.equal(ip.normalizeLax('0x7f.0x0.0x0.0x1'), '127.0.0.1'); + assert.equal(ip.normalizeLax('012.34.0X56.0xAb'), '10.34.86.171'); + }); + + it('should normalize 3-part addresses', () => { + assert.equal(ip.normalizeLax('192.168.1'), '192.168.0.1'); + }); + + it('should normalize 2-part addresses', () => { + assert.equal(ip.normalizeLax('012.3'), '10.0.0.3'); + assert.equal(ip.normalizeLax('012.0xabcdef'), '10.171.205.239'); + }); + + it('should normalize single integer addresses', () => { + assert.equal(ip.normalizeLax('0x7f000001'), '127.0.0.1'); + assert.equal(ip.normalizeLax('123456789'), '7.91.205.21'); + assert.equal(ip.normalizeLax('01200034567'), '10.0.57.119'); + }); + + it('should throw on invalid addresses', () => { + assert.throws(() => ip.normalizeLax('127.0.0xabcde')); + assert.throws(() => ip.normalizeLax('12345678910')); + assert.throws(() => ip.normalizeLax('0o1200034567')); + assert.throws(() => ip.normalizeLax('127.0.0.0.1')); + assert.throws(() => ip.normalizeLax('127.0.0.-1')); + assert.throws(() => ip.normalizeLax('-1')); + }); + + it('should normalize IPv6 leading zeros', () => { + assert.equal(ip.normalizeStrict('00:0::000:01'), '::1'); + }); + }); }); From aeea96e445164c72607a9043e37bcc7bc8786ec9 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 17:47:19 +0800 Subject: [PATCH 4/6] lib: fix single integer address range checking --- lib/ip.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ip.js b/lib/ip.js index a73c381..5eff422 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -455,6 +455,7 @@ ip.normalizeToLong = function (addr) { switch (n) { case 1: + if (parts[0] > 0xffffffff) return -1; val = parts[0]; break; case 2: From b94e184d15f7353443eaa45b1ef75ed2ba824051 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 17:48:59 +0800 Subject: [PATCH 5/6] ci: remove Node 12 and add Node 20 --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a08879..f8e3eee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,12 +5,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [ 12, 14, 16, 18 ] + node: [ 14, 16, 18, 20 ] name: Node ${{ matrix.node }} sample steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm ci From 934fadee0080b9d201df95a3f7eead6c46e94017 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 21 Feb 2024 18:25:10 +0800 Subject: [PATCH 6/6] lib: add isValid(), isValidAndPublic(), isValidAndPrivate() --- README.md | 2 ++ lib/ip.js | 31 +++++++++++++++++++++++++++++-- test/api-test.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 334f604..0ab00ed 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ ip.or('192.168.1.134', '0.0.0.255') // 192.168.1.255 ip.isPrivate('127.0.0.1') // true ip.isV4Format('127.0.0.1'); // true ip.isV6Format('::ffff:127.0.0.1'); // true +ip.isValid('127.0.0.1'); // true // operate on buffers in-place var buf = new Buffer(128); @@ -65,6 +66,7 @@ ip.fromLong(2130706433); // '127.0.0.1' // malformed addresses and normalization ip.normalizeStrict('0::01'); // '::1' ip.isPrivate('0x7f.1'); // throw error +ip.isValidAndPrivate('0x7f.1'); // false ip.normalizeStrict('0x7f.1'); // throw error var normalized = ip.normalizeLax('0x7f.1'); // 127.0.0.1 ip.isPrivate(normalized); // true diff --git a/lib/ip.js b/lib/ip.js index 5eff422..9850e17 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -87,9 +87,20 @@ ip.isV4Format = net.isIPv4; ip.isV6Format = net.isIPv6; +ip.isValid = function (addr) { + return net.isIP(addr) !== 0; +}; + ip.normalizeStrict = function (addr) { - const family = net.isIPv4(addr) ? 'ipv4' : net.isIPv6(addr) ? 'ipv6' : null; - if (family === null) { + let family; + switch (net.isIP(addr)) { + case 4: + family = 'ipv4'; + break; + case 6: + family = 'ipv6'; + break; + default: throw new Error(`Invalid ip address: ${addr}`); } const { address } = new net.SocketAddress({ address: addr, family }); @@ -327,6 +338,22 @@ ip.isPublic = function (addr) { return !ip.isPrivate(addr); }; +ip.isValidAndPrivate = function (addr) { + try { + return ip.isPrivate(addr); + } catch { + return false; + } +}; + +ip.isValidAndPublic = function (addr) { + try { + return ip.isPublic(addr); + } catch { + return false; + } +}; + ip.isLoopback = function (addr) { addr = ip.normalizeStrict(addr); diff --git a/test/api-test.js b/test/api-test.js index 7a477a0..ecdfa75 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -528,4 +528,51 @@ describe('IP library for node.js', () => { assert.equal(ip.normalizeStrict('00:0::000:01'), '::1'); }); }); + + describe('isValid(), isV4Format()), isV6Format() methods', () => { + it('should validate ipv4 addresses', () => { + assert.equal(ip.isValid('1.1.1.1'), true); + assert.equal(ip.isValid('1.1.1.1.1'), false); + assert.equal(ip.isValid('1.1.1.256'), false); + assert.equal(ip.isValid('127.1'), false); + assert.equal(ip.isValid('127.0.0.01'), false); + assert.equal(ip.isValid('0x7f.0.0.1'), false); + assert.equal(ip.isV4Format('1.2.3.4'), true); + assert.equal(ip.isV6Format('1.2.3.4'), false); + }); + + it('should validate ipv6 addresses', () => { + assert.equal(ip.isValid('::1'), true); + assert.equal(ip.isValid('::1:1.2.3.4'), true); + assert.equal(ip.isValid('1::2::3'), false); + assert.equal(ip.isV4Format('::ffff:127.0.0.1'), false); + assert.equal(ip.isV6Format('::ffff:127.0.0.1'), true); + }); + }); + + describe('isValidAndPublic() method', () => { + it('should return true on valid public addresses', () => { + assert.equal(ip.isValidAndPublic('8.8.8.8'), true); + }); + it('should return false on invalid addresses', () => { + assert.equal(ip.isValidAndPublic('8.8.8'), false); + assert.equal(ip.isValidAndPublic('8.8.8.010'), false); + }); + it('should return false on valid private addresses', () => { + assert.equal(ip.isValidAndPublic('127.0.0.1'), false); + }); + }); + + describe('isValidAndPrivate() method', () => { + it('should return true on valid private addresses', () => { + assert.equal(ip.isValidAndPrivate('192.168.1.2'), true); + }); + it('should return false on invalid addresses', () => { + assert.equal(ip.isValidAndPrivate('127.1'), false); + assert.equal(ip.isValidAndPrivate('0x7f.0.0.1'), false); + }); + it('should return false on valid public addresses', () => { + assert.equal(ip.isValidAndPrivate('8.8.8.8'), false); + }); + }); });