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);
-}