Skip to content

Commit

Permalink
net: add SocketAddress.parse
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
jasnell committed Nov 29, 2024
1 parent 61b077d commit c79ac3b
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 100 deletions.
11 changes: 11 additions & 0 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion lib/internal/socketaddress.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const {
kDeserialize,
} = require('internal/worker/js_transferable');

const { URL } = require('internal/url');

const kHandle = Symbol('kHandle');
const kDetail = Symbol('kDetail');

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
237 changes: 138 additions & 99 deletions test/parallel/test-socketaddress.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

0 comments on commit c79ac3b

Please sign in to comment.