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 diff --git a/README.md b/README.md index 22e5819..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); @@ -58,10 +59,17 @@ 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.isValidAndPrivate('0x7f.1'); // false +ip.normalizeStrict('0x7f.1'); // throw error +var normalized = ip.normalizeLax('0x7f.1'); // 127.0.0.1 +ip.isPrivate(normalized); // true ``` ### License diff --git a/lib/ip.js b/lib/ip.js index 9022443..9850e17 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,28 @@ 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.isValid = function (addr) { + return net.isIP(addr) !== 0; }; -ip.isV6Format = function (ip) { - return ipv6Regex.test(ip); +ip.normalizeStrict = function (addr) { + 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 }); + return address; }; function _normalizeFamily(family) { @@ -306,26 +320,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.normalizeStrict(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) @@ -337,16 +338,26 @@ ip.isPublic = function (addr) { return !ip.isPrivate(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)); +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); - 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); @@ -443,6 +454,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')) { @@ -469,6 +482,7 @@ ip.normalizeToLong = function (addr) { switch (n) { case 1: + if (parts[0] > 0xffffffff) return -1; val = parts[0]; break; case 2: @@ -489,3 +503,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); +}; diff --git a/test/api-test.js b/test/api-test.js index 0db838d..ecdfa75 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); @@ -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')); }); }); @@ -469,41 +469,110 @@ describe('IP library for node.js', () => { }); }); - // 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); - }); + describe('normalizeStrict() method', () => { + it('should keep valid IPv4 addresses', () => { + assert.equal(ip.normalizeStrict('1.1.1.1'), '1.1.1.1'); + }); - it('should return true for octal representation "0177.0.1"', () => { - assert.equal(ip.isLoopback('0177.0.1'), true); - }); + it('should normalize IPv6 leading zeros', () => { + assert.equal(ip.normalizeStrict('00:0::000:01'), '::1'); + }); - it('should return true for octal representation "0177.1"', () => { - assert.equal(ip.isLoopback('0177.1'), true); - }); + 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'); + }); - // 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); + it('should reject malformed addresses', () => { + assert.throws(() => ip.normalizeStrict('127.0.1')); + assert.throws(() => ip.normalizeStrict('0x7f.1')); + assert.throws(() => ip.normalizeStrict('012.1')); + }); }); - // IPv4 loopback in hexadecimal notation - it('should return true for hexadecimal representation "0x7f.0.1"', () => { - assert.equal(ip.isLoopback('0x7f.0.1'), true); + 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'); + }); }); - // IPv4 loopback in hexadecimal notation - it('should return true for hexadecimal representation "0x7f.1"', () => { - assert.equal(ip.isLoopback('0x7f.1'), true); + 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); + }); }); - // IPv4 loopback as a single long integer - it('should return true for single long integer representation "2130706433"', () => { - assert.equal(ip.isLoopback('2130706433'), 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); + }); }); - // IPv4 non-loopback address - it('should return false for "192.168.1.1"', () => { - assert.equal(ip.isLoopback('192.168.1.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); + }); }); });