Skip to content

Commit

Permalink
nope-ip removal - Phase 3
Browse files Browse the repository at this point in the history
`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 <[email protected]>
  • Loading branch information
liranmauda committed Feb 10, 2025
1 parent b89f7bc commit 7227447
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 40 deletions.
17 changes: 9 additions & 8 deletions src/rpc/stun.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}


Expand All @@ -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];
Expand Down Expand Up @@ -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);
}
Expand Down
107 changes: 107 additions & 0 deletions src/test/unit_tests/jest_tests/test_net_utils.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
43 changes: 12 additions & 31 deletions src/util/http_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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]/;
Expand Down
135 changes: 134 additions & 1 deletion src/util/net_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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}
*/
Expand All @@ -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<ArrayBuffer>} 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<ArrayBuffer>} 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<ArrayBuffer>} 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<ArrayBuffer>} 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));
}
Expand All @@ -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;

0 comments on commit 7227447

Please sign in to comment.