From 7227447029393f6371e78e55ce599355d6d704d7 Mon Sep 17 00:00:00 2001 From: liranmauda Date: Sun, 9 Feb 2025 17:29:08 +0200 Subject: [PATCH] nope-ip removal - Phase 3 `stun.js`: - Replaced `node-ip` with native `net_utils` calls `http_utils.js`: - Refractored `no_proxy_list` - Replaced `node-ip.cidr` with native `net_utils.is_cidr` call `net_utils.js`: - Fixed a bug where `is_fqdn("loclhost")` returned falsely true - Add `is_cidr`, `ip_toString`, and `ip_toBuffer` - Added helper functions for `ip_toBuffer` - Adding `test_net_utils.test.js` Signed-off-by: liranmauda --- src/rpc/stun.js | 17 +-- .../jest_tests/test_net_utils.test.js | 107 ++++++++++++++ src/util/http_utils.js | 43 ++---- src/util/net_utils.js | 135 +++++++++++++++++- 4 files changed, 262 insertions(+), 40 deletions(-) create mode 100644 src/test/unit_tests/jest_tests/test_net_utils.test.js diff --git a/src/rpc/stun.js b/src/rpc/stun.js index 92cafcde6c..d2fb9ce7ea 100644 --- a/src/rpc/stun.js +++ b/src/rpc/stun.js @@ -2,14 +2,14 @@ /* eslint-disable no-bitwise */ 'use strict'; +const url = require('url'); const _ = require('lodash'); const util = require('util'); -const P = require('../util/promise'); -const url = require('url'); const dgram = require('dgram'); const crypto = require('crypto'); -const ip_module = require('ip'); const chance = require('chance')(); +const P = require('../util/promise'); +const net_utils = require('../util/net_utils'); // https://tools.ietf.org/html/rfc5389 const stun = { @@ -435,7 +435,7 @@ function encode_attrs(buffer, attrs) { function decode_attr_mapped_addr(buffer, start, end) { const family = (buffer.readUInt16BE(start) === 0x02) ? 6 : 4; const port = buffer.readUInt16BE(start + 2); - const address = ip_module.toString(buffer, start + 4, family); + const address = net_utils.ip_toString(buffer, start + 4, family); return { family: 'IPv' + family, @@ -463,7 +463,7 @@ function decode_attr_xor_mapped_addr(buffer, start, end) { xor_buf[i] = addr_buf[i] ^ buffer[k]; k += 1; } - const address = ip_module.toString(xor_buf, 0, family); + const address = net_utils.ip_toString(xor_buf, 0, family); return { family: 'IPv' + family, @@ -510,7 +510,7 @@ function encode_attr_mapped_addr(addr, buffer, offset, end) { // xor the port against the magic key buffer.writeUInt16BE(addr.port, offset + 2); - ip_module.toBuffer(addr.address, buffer, offset + 4); + net_utils.ip_toBuffer(addr.address, buffer, offset + 4); } @@ -524,7 +524,7 @@ function encode_attr_xor_mapped_addr(addr, buffer, offset, end) { // xor the port against the magic key buffer.writeUInt16BE(addr.port ^ buffer.readUInt16BE(stun.XOR_KEY_OFFSET), offset + 2); - ip_module.toBuffer(addr.address, buffer, offset + 4); + net_utils.ip_toBuffer(addr.address, buffer, offset + 4); let k = stun.XOR_KEY_OFFSET; for (let i = offset + 4; i < end; ++i) { buffer[i] ^= buffer[k]; @@ -615,7 +615,8 @@ function test() { } return Promise.all([ P.ninvoke(socket, 'send', req, 0, req.length, stun_url.port, stun_url.hostname), - P.ninvoke(socket, 'send', ind, 0, ind.length, stun_url.port, stun_url.hostname)]) + P.ninvoke(socket, 'send', ind, 0, ind.length, stun_url.port, stun_url.hostname) + ]) .then(() => P.delay(stun.INDICATION_INTERVAL * chance.floating(stun.INDICATION_JITTER))) .then(loop); } diff --git a/src/test/unit_tests/jest_tests/test_net_utils.test.js b/src/test/unit_tests/jest_tests/test_net_utils.test.js new file mode 100644 index 0000000000..6a7f71d676 --- /dev/null +++ b/src/test/unit_tests/jest_tests/test_net_utils.test.js @@ -0,0 +1,107 @@ +/* Copyright (C) 2016 NooBaa */ +'use strict'; + +const os = require('os'); +const net_utils = require('../../../util/net_utils'); + +describe('IP Utils', () => { + it('is_ip should correctly identify IP addresses', () => { + expect(net_utils.is_ip('192.168.1.1')).toBe(true); + expect(net_utils.is_ip('::1')).toBe(true); + expect(net_utils.is_ip('not_an_ip')).toBe(false); + expect(net_utils.is_ip('256.256.256.256')).toBe(false); + }); + + it('is_fqdn should correctly identify FQDNs', () => { + expect(net_utils.is_fqdn('example.com')).toBe(true); + expect(net_utils.is_fqdn('sub.example.com')).toBe(true); + expect(net_utils.is_fqdn('localhost')).toBe(false); + expect(net_utils.is_fqdn('invalid_domain-.com')).toBe(false); + }); + + it('is_cidr should correctly identify CIDR notation', () => { + expect(net_utils.is_cidr('192.168.1.0/24')).toBe(true); + expect(net_utils.is_cidr('10.0.0.0/8')).toBe(true); + expect(net_utils.is_cidr('2001:db8::/32')).toBe(true); + expect(net_utils.is_cidr('192.168.1.300/24')).toBe(false); + expect(net_utils.is_cidr('invalid_cidr')).toBe(false); + expect(net_utils.is_cidr('192.168.1.1')).toBe(false); + }); + + it('is_localhost should correctly identify localhost addresses', () => { + expect(net_utils.is_localhost('127.0.0.1')).toBe(true); + expect(net_utils.is_localhost('::1')).toBe(true); + expect(net_utils.is_localhost('localhost')).toBe(true); + expect(net_utils.is_localhost('192.168.1.1')).toBe(false); + }); + + it('unwrap_ipv6 should remove IPv6 prefix', () => { + expect(net_utils.unwrap_ipv6('::ffff:192.168.1.1')).toBe('192.168.1.1'); + expect(net_utils.unwrap_ipv6('::1')).toBe('::1'); + expect(net_utils.unwrap_ipv6('2001:db8::ff00:42:8329')).toBe('2001:db8::ff00:42:8329'); + }); + + it('ip_toLong should convert IPv4 to long', () => { + expect(net_utils.ip_toLong('192.168.1.1')).toBe(3232235777); + expect(net_utils.ip_toLong('0.0.0.0')).toBe(0); + expect(net_utils.ip_toLong('255.255.255.255')).toBe(4294967295); + }); + + it('ip_to_long should handle both IPv4 and IPv6-mapped IPv4', () => { + expect(net_utils.ip_to_long('192.168.1.1')).toBe(3232235777); + expect(net_utils.ip_to_long('::ffff:192.168.1.1')).toBe(3232235777); + }); + + it('find_ifc_containing_address should find interface containing the address', () => { + const networkInterfacesMock = { + eth0: [ + { family: 'IPv4', cidr: '192.168.1.0/24', address: '192.168.1.2' }, + { family: 'IPv6', cidr: 'fe80::/64', address: 'fe80::1' }, + ], + lo: [{ family: 'IPv4', cidr: '127.0.0.0/8', address: '127.0.0.1' }], + }; + jest.spyOn(os, 'networkInterfaces').mockReturnValue(networkInterfacesMock); + + expect(net_utils.find_ifc_containing_address('192.168.1.5')).toEqual({ ifc: 'eth0', info: networkInterfacesMock.eth0[0] }); + expect(net_utils.find_ifc_containing_address('127.0.0.1')).toEqual({ ifc: 'lo', info: networkInterfacesMock.lo[0] }); + expect(net_utils.find_ifc_containing_address('8.8.8.8')).toBeUndefined(); + }); + + it('ip_toString should convert buffers to IP strings', () => { + expect(net_utils.ip_toString(Buffer.from([192, 168, 1, 1]), 0, 4)).toBe('192.168.1.1'); + expect(net_utils.ip_toString(Buffer.from([0, 0, 0, 0]), 0, 4)).toBe('0.0.0.0'); + expect(net_utils.ip_toString(Buffer.from([255, 255, 255, 255]), 0, 4)).toBe('255.255.255.255'); + expect(net_utils.ip_toString(Buffer.from([32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), 0, 16)).toBe('2001:db8::1'); + expect(() => net_utils.ip_toString(Buffer.from([192, 168, 1, 1]), undefined, 4)).toThrow('Offset is required'); + }); + + it('ipv4_to_buffer should convert IPv4 string to buffer', () => { + const buff = Buffer.alloc(4); + expect(net_utils.ipv4_to_buffer('192.168.1.1', buff, 0)).toEqual(Buffer.from([192, 168, 1, 1])); + }); + + it('ipv6_to_buffer should convert expanded IPv6 string to buffer', () => { + const buff = Buffer.alloc(16); + expect(net_utils.ipv6_to_buffer('2001:0db8:0000:0000:0000:0000:0000:0001', buff, 0)).toEqual( + Buffer.from([32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + ); + }); + + it('expend_ipv6 should expand IPv6 shorthand notation', () => { + expect(net_utils.expend_ipv6('::')).toEqual(['0', '0', '0', '0', '0', '0', '0', '0']); + expect(net_utils.expend_ipv6('2001:db8::1')).toEqual(['2001', 'db8', '0', '0', '0', '0', '0', '1']); + expect(net_utils.expend_ipv6('2001:db8::ff00:42:8329')).toEqual(['2001', 'db8', '0', '0', '0', 'ff00', '42', '8329']); + expect(net_utils.expend_ipv6('::1')).toEqual(['0', '0', '0', '0', '0', '0', '0', '1']); + expect(net_utils.expend_ipv6('2001:0db8:85a3::8a2e:370:7334')).toEqual(['2001', '0db8', '85a3', '0', '0', '8a2e', '370', '7334']); + expect(net_utils.expend_ipv6('::ffff:192.168.1.1')).toEqual(['0', '0', '0', '0', '0', 'ffff', 'c0a8', '0101']); + }); + + it('ip_toBuffer should convert IP strings to buffer', () => { + expect(net_utils.ip_toBuffer('10.0.0.1', Buffer.alloc(4), 0)).toEqual(Buffer.from([10, 0, 0, 1])); + expect(net_utils.ip_toBuffer('2001:db8::1', Buffer.alloc(16), 0)).toEqual( + Buffer.from([32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + ); + expect(() => net_utils.ip_toBuffer('invalid_ip', Buffer.alloc(16), 0)).toThrow('Invalid IP address: invalid_ip'); + expect(() => net_utils.ip_toBuffer('10.0.0.1')).toThrow('Offset is required'); + }); +}); diff --git a/src/util/http_utils.js b/src/util/http_utils.js index 390b8975dd..aeeea08270 100644 --- a/src/util/http_utils.js +++ b/src/util/http_utils.js @@ -19,6 +19,7 @@ const dbg = require('./debug_module')(__filename); const config = require('../../config'); const xml_utils = require('./xml_utils'); const jwt_utils = require('./jwt_utils'); +const net_utils = require('./net_utils'); const time_utils = require('./time_utils'); const cloud_utils = require('./cloud_utils'); const ssl_utils = require('../util/ssl_utils'); @@ -45,38 +46,18 @@ const https_proxy_agent = HTTPS_PROXY ? const unsecured_https_proxy_agent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY, { rejectUnauthorized: false }) : null; -const no_proxy_list = - (NO_PROXY ? NO_PROXY.split(',') : []).map(addr => { - if (net.isIPv4(addr) || net.isIPv6(addr)) { - return { - kind: 'IP', - addr - }; - } - - try { - ip_module.cidr(addr); - return { - kind: 'CIDR', - addr - }; - } catch { - // noop - } - - if (addr.startsWith('.')) { - return { - kind: 'FQDN_SUFFIX', - addr - }; - } - - return { - kind: 'FQDN', - addr - }; - }); +const no_proxy_list = (NO_PROXY ? NO_PROXY.split(',') : []).map(addr => { + let kind = 'FQDN'; + if (net.isIPv4(addr) || net.isIPv6(addr)) { + kind = 'IP'; + } else if (net_utils.is_cidr(addr)) { + kind = 'CIDR'; + } else if (addr.startsWith('.')) { + kind = 'FQDN_SUFFIX'; + } + return { kind, addr }; +}); const parse_xml_to_js = xml2js.parseStringPromise; const non_printable_regexp = /[\x00-\x1F]/; diff --git a/src/util/net_utils.js b/src/util/net_utils.js index 7610322fb2..94f051e003 100644 --- a/src/util/net_utils.js +++ b/src/util/net_utils.js @@ -8,6 +8,7 @@ const ip_module = require('ip'); const fqdn_regexp = /^(?=^.{1,253}$)(^(((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9])|((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63})$)/; function is_fqdn(target) { + if (target === 'localhost') return false; if (target && fqdn_regexp.test(target)) { return true; } @@ -16,6 +17,19 @@ function is_fqdn(target) { } /** + * is_cidr will check if the address is CIDR + * @param {string} ip + */ +function is_cidr(ip) { + const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$|^[a-fA-F0-9:]+\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/; + if (!cidrRegex.test(ip)) return false; + const address = ip.split("/")[0]; + if (!net.isIP(address)) return false; + return true; +} + +/** + * is_localhost will check if the address is localhost * @param {string} address * @returns {boolean} */ @@ -34,12 +48,123 @@ function unwrap_ipv6(ip) { return ip; } -//the name ip_toLong consist of camel case and underscore, to indicate that toLong is the function we had in node-ip +/** + * the name ip_toLong consist of camel case and underscore, to indicate that toLong is the function we had in node-ip + * ip_toLong will take ip address and convert it to long + * @param {string} ip + */ function ip_toLong(ip) { // eslint-disable-next-line no-bitwise return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; } +/** + * the name ip_toString consist of camel case and underscore, to indicate that toString is the function we had in node-ip + * ip_toString will take buffer and convert it to string + * @param {Buffer} buff + * @param {number} offset + * @param {number} length + */ +function ip_toString(buff, offset, length) { + if (offset === undefined) { + throw new Error('Offset is required'); + } + offset = Math.trunc(offset); + length = length ?? (buff.length - offset); + + if (length === 4) { // IPv4 + return Array.from(buff.subarray(offset, offset + length)).join('.'); + } else if (length === 16) { // IPv6 + const result = []; + for (let i = 0; i < length; i += 2) { + result.push(buff.readUInt16BE(offset + i).toString(16)); + } + + let ipv6 = result.join(':'); + ipv6 = ipv6.replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3'); + ipv6 = ipv6.replace(/:{3,4}/, '::'); + return ipv6; + } +} + +/** + * ipv4_to_buffer will take ipv4 address and convert it to buffer + * @param {string} ip + * @param {Buffer} buff + * @param {number} offset + */ +function ipv4_to_buffer(ip, buff, offset) { + ip.split('.').forEach((byte, i) => { + // eslint-disable-next-line no-bitwise + buff[offset + i] = parseInt(byte, 10) & 0xff; + }); + return buff; +} + +/** + * ipv6_to_buffer will take ipv6 address and convert it to buffer + * @param {any} ip + * @param {Buffer} buff + * @param {number} offset + */ +function ipv6_to_buffer(ip, buff, offset) { + const sections = expend_ipv6(ip); + let i = 0; + sections.forEach(section => { + const word = parseInt(section, 16); + // eslint-disable-next-line no-bitwise + buff[offset + i] = (word >> 8) & 0xff; + // eslint-disable-next-line no-bitwise + buff[offset + i + 1] = word & 0xff; + i += 2; + }); + return buff; +} + +/** + * expend_ipv6 will take ipv6 address and expand it to array of 8 sections + * @param {string} ip + */ +function expend_ipv6(ip) { + const sections = ip.split(':'); + + if (sections[sections.length - 1].includes('.')) { + const ipv4Part = sections.pop(); + const v4_buffer = ipv4_to_buffer(ipv4Part, Buffer.alloc(4), 0); + sections.push(v4_buffer.subarray(0, 2).toString('hex')); + sections.push(v4_buffer.subarray(2, 4).toString('hex')); + } + + const emptyIndex = sections.indexOf(''); + if (emptyIndex !== -1) { + const missing = 8 - sections.length; + sections.splice(emptyIndex, 1, ...new Array(missing + 1).fill('0')); + } + + return sections.map(section => section || '0'); +} + +/** + * the name ip_toBuffer consist of camel case and underscore, to indicate that toBuffer is the function we had in node-ip + * @param {string} ip + * @param {Buffer} buff + * @param {number} offset + */ +function ip_toBuffer(ip, buff, offset) { + if (offset === undefined) { + throw new Error('Offset is required'); + } + + if (net.isIPv4(ip)) { + return ipv4_to_buffer(ip, buff || Buffer.alloc(offset + 4), offset); + } else if (net.isIPv6(ip)) { + return ipv6_to_buffer(ip, buff || Buffer.alloc(offset + 16), offset); + } + + throw new Error(`Invalid IP address: ${ip}`); +} + + function ip_to_long(ip) { return ip_toLong(unwrap_ipv6(ip)); } @@ -65,8 +190,16 @@ function find_ifc_containing_address(address) { exports.is_ip = is_ip; exports.is_fqdn = is_fqdn; +exports.is_cidr = is_cidr; exports.is_localhost = is_localhost; exports.unwrap_ipv6 = unwrap_ipv6; exports.ip_toLong = ip_toLong; +exports.ip_toString = ip_toString; +exports.ip_toBuffer = ip_toBuffer; exports.ip_to_long = ip_to_long; exports.find_ifc_containing_address = find_ifc_containing_address; + +/// EXPORTS FOR TESTING: +exports.ipv4_to_buffer = ipv4_to_buffer; +exports.ipv6_to_buffer = ipv6_to_buffer; +exports.expend_ipv6 = expend_ipv6;