From c79ac3b097154f7c89d349696a299347e020baa8 Mon Sep 17 00:00:00 2001 From: James M Snell <jasnell@gmail.com> Date: Fri, 29 Nov 2024 07:59:46 -0800 Subject: [PATCH] net: add SocketAddress.parse Adds a new `net.SocketAddress.parse(...)` API. ```js const addr = SocketAddress.parse('123.123.123.123:1234'); console.log(addr.address); // 123.123.123.123 console.log(addr.port); 1234 ``` --- doc/api/net.md | 11 ++ lib/internal/socketaddress.js | 34 +++- test/parallel/test-socketaddress.js | 237 ++++++++++++++++------------ 3 files changed, 182 insertions(+), 100 deletions(-) diff --git a/doc/api/net.md b/doc/api/net.md index 7d8a27bf0b1769..c7f5ec335362d6 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -235,6 +235,17 @@ added: * Type {number} +### `SocketAddress.parse(input)` + +<!-- YAML +added: REPLACEME +--> + +* `input` {string} An input string containing an IP address and optional port, + e.g. `123.1.2.3:1234` or `[1::1]:1234`. +* Returns: {net.SocketAddress} Returns a `SocketAddress` if parsing was successful. + Otherwise returns `undefined`. + ## Class: `net.Server` <!-- YAML diff --git a/lib/internal/socketaddress.js b/lib/internal/socketaddress.js index e432c8a7d7593a..7fbe63980a0226 100644 --- a/lib/internal/socketaddress.js +++ b/lib/internal/socketaddress.js @@ -37,6 +37,8 @@ const { kDeserialize, } = require('internal/worker/js_transferable'); +const { URL } = require('internal/url'); + const kHandle = Symbol('kHandle'); const kDetail = Symbol('kDetail'); @@ -74,7 +76,7 @@ class SocketAddress { validatePort(port, 'options.port'); validateUint32(flowlabel, 'options.flowlabel', false); - this[kHandle] = new _SocketAddress(address, port, type, flowlabel); + this[kHandle] = new _SocketAddress(address, port | 0, type, flowlabel | 0); this[kDetail] = this[kHandle].detail({ address: undefined, port: undefined, @@ -138,6 +140,36 @@ class SocketAddress { flowlabel: this.flowlabel, }; } + + /** + * Parse an "${ip}:${port}" formatted string into a SocketAddress. + * Returns undefined if the input cannot be successfully parsed. + * @param {string} input + * @returns {SocketAddress|undefined} + */ + static parse(input) { + validateString(input, 'input'); + // While URL.parse is not expected to throw, there are several + // other pieces here that do... the destucturing, the SocketAddress + // constructor, etc. So we wrap this in a try/catch to be safe. + try { + const { + hostname: address, + port, + } = URL.parse(`http://${input}`); + if (address.startsWith('[') && address.endsWith(']')) { + return new SocketAddress({ + address: address.slice(1, -1), + port: port | 0, + family: 'ipv6', + }); + } + return new SocketAddress({ address, port: port | 0 }); + } catch { + // Ignore errors here. Return undefined if the input cannot + // be successfully parsed or is not a proper socket address. + } + } } class InternalSocketAddress { diff --git a/test/parallel/test-socketaddress.js b/test/parallel/test-socketaddress.js index b6d9946271fa52..bd117cc6b5edc1 100644 --- a/test/parallel/test-socketaddress.js +++ b/test/parallel/test-socketaddress.js @@ -17,121 +17,160 @@ const { const { internalBinding } = require('internal/test/binding'); const { SocketAddress: _SocketAddress, - AF_INET + AF_INET, } = internalBinding('block_list'); -{ - const sa = new SocketAddress(); - strictEqual(sa.address, '127.0.0.1'); - strictEqual(sa.port, 0); - strictEqual(sa.family, 'ipv4'); - strictEqual(sa.flowlabel, 0); +const { describe, it } = require('node:test'); - const mc = new MessageChannel(); - mc.port1.onmessage = common.mustCall(({ data }) => { - ok(SocketAddress.isSocketAddress(data)); +describe('net.SocketAddress...', () => { - strictEqual(data.address, '127.0.0.1'); - strictEqual(data.port, 0); - strictEqual(data.family, 'ipv4'); - strictEqual(data.flowlabel, 0); + it('is cloneable', () => { + const sa = new SocketAddress(); + strictEqual(sa.address, '127.0.0.1'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); - mc.port1.close(); + const mc = new MessageChannel(); + mc.port1.onmessage = common.mustCall(({ data }) => { + ok(SocketAddress.isSocketAddress(data)); + + strictEqual(data.address, '127.0.0.1'); + strictEqual(data.port, 0); + strictEqual(data.family, 'ipv4'); + strictEqual(data.flowlabel, 0); + + mc.port1.close(); + }); + mc.port2.postMessage(sa); }); - mc.port2.postMessage(sa); -} - -{ - const sa = new SocketAddress({}); - strictEqual(sa.address, '127.0.0.1'); - strictEqual(sa.port, 0); - strictEqual(sa.family, 'ipv4'); - strictEqual(sa.flowlabel, 0); -} - -{ - const sa = new SocketAddress({ - address: '123.123.123.123', + + it('has reasonable defaults', () => { + const sa = new SocketAddress({}); + strictEqual(sa.address, '127.0.0.1'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); }); - strictEqual(sa.address, '123.123.123.123'); - strictEqual(sa.port, 0); - strictEqual(sa.family, 'ipv4'); - strictEqual(sa.flowlabel, 0); -} - -{ - const sa = new SocketAddress({ - address: '123.123.123.123', - port: 80 + + it('interprets simple ipv4 correctly', () => { + const sa = new SocketAddress({ + address: '123.123.123.123', + }); + strictEqual(sa.address, '123.123.123.123'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); }); - strictEqual(sa.address, '123.123.123.123'); - strictEqual(sa.port, 80); - strictEqual(sa.family, 'ipv4'); - strictEqual(sa.flowlabel, 0); -} - -{ - const sa = new SocketAddress({ - family: 'ipv6' + + it('sets the port correctly', () => { + const sa = new SocketAddress({ + address: '123.123.123.123', + port: 80 + }); + strictEqual(sa.address, '123.123.123.123'); + strictEqual(sa.port, 80); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); }); - strictEqual(sa.address, '::'); - strictEqual(sa.port, 0); - strictEqual(sa.family, 'ipv6'); - strictEqual(sa.flowlabel, 0); -} - -{ - const sa = new SocketAddress({ - family: 'ipv6', - flowlabel: 1, + + it('interprets simple ipv6 correctly', () => { + const sa = new SocketAddress({ + family: 'ipv6' + }); + strictEqual(sa.address, '::'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv6'); + strictEqual(sa.flowlabel, 0); }); - strictEqual(sa.address, '::'); - strictEqual(sa.port, 0); - strictEqual(sa.family, 'ipv6'); - strictEqual(sa.flowlabel, 1); -} - -[1, false, 'hello'].forEach((i) => { - throws(() => new SocketAddress(i), { - code: 'ERR_INVALID_ARG_TYPE' + + it('uses the flowlabel correctly', () => { + const sa = new SocketAddress({ + family: 'ipv6', + flowlabel: 1, + }); + strictEqual(sa.address, '::'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv6'); + strictEqual(sa.flowlabel, 1); }); -}); -[1, false, {}, [], 'test'].forEach((family) => { - throws(() => new SocketAddress({ family }), { - code: 'ERR_INVALID_ARG_VALUE' + it('validates input correctly', () => { + [1, false, 'hello'].forEach((i) => { + throws(() => new SocketAddress(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, false, {}, [], 'test'].forEach((family) => { + throws(() => new SocketAddress({ family }), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + [1, false, {}, []].forEach((address) => { + throws(() => new SocketAddress({ address }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [-1, false, {}, []].forEach((port) => { + throws(() => new SocketAddress({ port }), { + code: 'ERR_SOCKET_BAD_PORT' + }); + }); + + throws(() => new SocketAddress({ flowlabel: -1 }), { + code: 'ERR_OUT_OF_RANGE' + }); }); -}); -[1, false, {}, []].forEach((address) => { - throws(() => new SocketAddress({ address }), { - code: 'ERR_INVALID_ARG_TYPE' + it('InternalSocketAddress correctly inherits from SocketAddress', () => { + // Test that the internal helper class InternalSocketAddress correctly + // inherits from SocketAddress and that it does not throw when its properties + // are accessed. + + const address = '127.0.0.1'; + const port = 8080; + const flowlabel = 0; + const handle = new _SocketAddress(address, port, AF_INET, flowlabel); + const addr = new InternalSocketAddress(handle); + ok(addr instanceof SocketAddress); + strictEqual(addr.address, address); + strictEqual(addr.port, port); + strictEqual(addr.family, 'ipv4'); + strictEqual(addr.flowlabel, flowlabel); }); -}); -[-1, false, {}, []].forEach((port) => { - throws(() => new SocketAddress({ port }), { - code: 'ERR_SOCKET_BAD_PORT' + it('SocketAddress.parse() works as expected', () => { + const good = [ + { input: '1.2.3.4', address: '1.2.3.4', port: 0, family: 'ipv4' }, + { input: '192.168.257:1', address: '192.168.1.1', port: 1, family: 'ipv4' }, + { input: '256', address: '0.0.1.0', port: 0, family: 'ipv4' }, + { input: '999999999:12', address: '59.154.201.255', port: 12, family: 'ipv4' }, + { input: '0xffffffff', address: '255.255.255.255', port: 0, family: 'ipv4' }, + { input: '0x.0x.0', address: '0.0.0.0', port: 0, family: 'ipv4' }, + { input: '[1:0::]', address: '1::', port: 0, family: 'ipv6' }, + { input: '[1::8]:123', address: '1::8', port: 123, family: 'ipv6' }, + ]; + + good.forEach((i) => { + const addr = SocketAddress.parse(i.input); + strictEqual(addr.address, i.address); + strictEqual(addr.port, i.port); + strictEqual(addr.family, i.family); + }); + + const bad = [ + 'not an ip', + 'abc.123', + '259.1.1.1', + '12:12:12', + ]; + + bad.forEach((i) => { + strictEqual(SocketAddress.parse(i), undefined); + }); }); -}); -throws(() => new SocketAddress({ flowlabel: -1 }), { - code: 'ERR_OUT_OF_RANGE' }); - -{ - // Test that the internal helper class InternalSocketAddress correctly - // inherits from SocketAddress and that it does not throw when its properties - // are accessed. - - const address = '127.0.0.1'; - const port = 8080; - const flowlabel = 0; - const handle = new _SocketAddress(address, port, AF_INET, flowlabel); - const addr = new InternalSocketAddress(handle); - ok(addr instanceof SocketAddress); - strictEqual(addr.address, address); - strictEqual(addr.port, port); - strictEqual(addr.family, 'ipv4'); - strictEqual(addr.flowlabel, flowlabel); -}