Skip to content

Commit 971d5b4

Browse files
committed
net: implement a socket address parser
1 parent 685d7ad commit 971d5b4

10 files changed

Lines changed: 674 additions & 103 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { SocketAddress } = require('net');
5+
6+
const inputs = {
7+
'ipv4': [
8+
'127.0.0.1',
9+
'10.168.209.250',
10+
'255.255.255.255',
11+
],
12+
'ipv4-port': [
13+
'127.0.0.1:80',
14+
'10.168.209.250:8080',
15+
'255.255.255.255:65535',
16+
],
17+
'ipv6': [
18+
'[::1]',
19+
'[2001:db8::1]',
20+
'[fe80::1ff:fe23:4567:890a]',
21+
],
22+
'ipv6-port': [
23+
'[::1]:80',
24+
'[2001:db8::1]:8080',
25+
'[::ffff:127.0.0.1]:65535',
26+
],
27+
};
28+
29+
const bench = common.createBenchmark(main, {
30+
n: [1e6],
31+
input: Object.keys(inputs),
32+
});
33+
34+
function main({ n, input }) {
35+
const values = inputs[input];
36+
const length = values.length;
37+
38+
bench.start();
39+
for (let i = 0; i < n; i++) {
40+
SocketAddress.parse(values[i % length]);
41+
}
42+
bench.end(n);
43+
}

doc/api/net.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,41 @@ added:
288288
added:
289289
- v23.4.0
290290
- v22.13.0
291+
changes:
292+
- version: REPLACEME
293+
pr-url: https://github.com/nodejs/node/pull/00000
294+
description: Input is now parsed strictly. URL syntax and legacy IPv4
295+
formats such as octal, hexadecimal and shorthand notation
296+
are no longer accepted.
291297
-->
292298

293299
* `input` {string} An input string containing an IP address and optional port,
294300
e.g. `123.1.2.3:1234` or `[1::1]:1234`.
295301
* Returns: {net.SocketAddress} Returns a `SocketAddress` if parsing was successful.
296302
Otherwise returns `undefined`.
297303

298-
The address portion of `input` must be a valid IPv4 or IPv6 address as
299-
recognized by the [WHATWG URL host parser][], and `input` may contain only
300-
hexadecimal digits, `x`, `.`, `:`, `[`, and `]`. Anything else returns
301-
`undefined`, including host names such as `example.com`, other URL components
302-
such as `user@1.2.3.4` or `1.2.3.4/foo`, whitespace, control characters,
303-
percent-encoding, and non-ASCII characters.
304+
The entire input must match one of the following forms:
305+
306+
```text
307+
socket-address = ipv4-socket-address / ipv6-socket-address
308+
ipv4-socket-address = ipv4-address [ ":" port ]
309+
ipv6-socket-address = "[" ipv6-address [ "%" scope-id ] "]" [ ":" port ]
310+
311+
ipv4-address = octet 3( "." octet )
312+
octet = 1*3DIGIT ; no leading zeros; value <= 255
313+
ipv6-address = RFC 4291 textual form: groups of 1*4HEXDIG (leading
314+
zeros allowed, case-insensitive), "::" compression, and
315+
an optional trailing embedded ipv4-address
316+
port = 1*DIGIT ; leading zeros allowed; value <= 65535
317+
scope-id = 1*DIGIT ; leading zeros allowed; value <= 4294967295
318+
```
319+
320+
Anything else returns `undefined`, including URL components such as userinfo,
321+
paths, queries and fragments, surrounding whitespace, host names, non-ASCII
322+
digits, and the legacy IPv4 notations that permit octal (`0177.0.0.1`),
323+
hexadecimal (`0x7f.0.0.1`), integer (`2130706433`) and shorthand (`127.1`)
324+
addresses. An IPv6 zone id must be numeric; interface names such as
325+
`%eth0` are not accepted.
304326

305327
## Class: `net.Server`
306328

@@ -2294,7 +2316,6 @@ net.isIPv6('fhqwhgads'); // returns false
22942316
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
22952317
[Readable Stream]: stream.md#class-streamreadable
22962318
[Transferring TCP handles to other threads]: #transferring-tcp-handles-to-other-threads
2297-
[WHATWG URL host parser]: https://url.spec.whatwg.org/#host-parsing
22982319
[`'close'`]: #event-close
22992320
[`'connect'`]: #event-connect
23002321
[`'connection'`]: #event-connection

lib/internal/socketaddress.js

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
const {
44
ObjectSetPrototypeOf,
5-
RegExpPrototypeExec,
65
Symbol,
76
} = primordials;
87

98
const {
109
SocketAddress: _SocketAddress,
10+
parseSocketAddress,
1111
AF_INET,
1212
AF_INET6,
1313
} = internalBinding('block_list');
@@ -38,13 +38,22 @@ const {
3838
kDeserialize,
3939
} = require('internal/worker/js_transferable');
4040

41-
const { URLParse } = require('internal/url');
42-
4341
const kHandle = Symbol('kHandle');
4442
const kDetail = Symbol('kDetail');
4543

46-
// The complete character set of an "${address}:${port}" input.
47-
const kValidInput = /^[0-9a-fA-FxX.:[\]]+$/;
44+
class InternalSocketAddress {
45+
constructor(handle) {
46+
markTransferMode(this, true, false);
47+
48+
this[kHandle] = handle;
49+
this[kDetail] = this[kHandle]?.detail({
50+
address: undefined,
51+
port: undefined,
52+
family: undefined,
53+
flowlabel: undefined,
54+
});
55+
}
56+
}
4857

4958
class SocketAddress {
5059
static isSocketAddress(value) {
@@ -153,41 +162,9 @@ class SocketAddress {
153162
*/
154163
static parse(input) {
155164
validateString(input, 'input');
156-
if (RegExpPrototypeExec(kValidInput, input) === null) return;
157-
// While URL.parse is not expected to throw, there are several
158-
// other pieces here that do... the destucturing, the SocketAddress
159-
// constructor, etc. So we wrap this in a try/catch to be safe.
160-
try {
161-
const {
162-
hostname: address,
163-
port,
164-
} = URLParse(`http://${input}`);
165-
if (address.startsWith('[') && address.endsWith(']')) {
166-
return new SocketAddress({
167-
address: address.slice(1, -1),
168-
port: port | 0,
169-
family: 'ipv6',
170-
});
171-
}
172-
return new SocketAddress({ address, port: port | 0 });
173-
} catch {
174-
// Ignore errors here. Return undefined if the input cannot
175-
// be successfully parsed or is not a proper socket address.
176-
}
177-
}
178-
}
179-
180-
class InternalSocketAddress {
181-
constructor(handle) {
182-
markTransferMode(this, true, false);
183-
184-
this[kHandle] = handle;
185-
this[kDetail] = this[kHandle]?.detail({
186-
address: undefined,
187-
port: undefined,
188-
family: undefined,
189-
flowlabel: undefined,
190-
});
165+
const handle = parseSocketAddress(input);
166+
if (handle === undefined) return undefined;
167+
return new InternalSocketAddress(handle);
191168
}
192169
}
193170

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
'src/node_shadow_realm.cc',
163163
'src/node_snapshotable.cc',
164164
'src/node_sockaddr.cc',
165+
'src/node_sockaddr_parser.cc',
165166
'src/node_stat_watcher.cc',
166167
'src/node_symbols.cc',
167168
'src/node_task_queue.cc',
@@ -302,6 +303,7 @@
302303
'src/node_snapshot_builder.h',
303304
'src/node_sockaddr.h',
304305
'src/node_sockaddr-inl.h',
306+
'src/node_sockaddr_parser.h',
305307
'src/node_stat_watcher.h',
306308
'src/node_union_bytes.h',
307309
'src/node_url.h',

src/node_sockaddr.cc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
#include "node_errors.h"
77
#include "node_hash.h"
88
#include "node_sockaddr-inl.h" // NOLINT(build/include_inline)
9+
#include "node_sockaddr_parser.h"
910
#include "uv.h"
1011

1112
#include <memory>
13+
#include <optional>
1214
#include <string>
15+
#include <string_view>
1316
#include <vector>
1417

1518
namespace node {
@@ -67,6 +70,31 @@ bool SocketAddress::New(int32_t family,
6770
family, host, port, reinterpret_cast<sockaddr_storage*>(addr->storage()));
6871
}
6972

73+
bool SocketAddress::Parse(std::string_view input, SocketAddress* addr) {
74+
std::optional<sockaddr_parser::parse_result> parsed =
75+
sockaddr_parser::ParseSocketAddress(input);
76+
if (!parsed.has_value()) return false;
77+
78+
CHECK_LE(parsed->host.size(), sockaddr_parser::kMaxHostLength);
79+
80+
char host[sockaddr_parser::kMaxHostLength + 1];
81+
host[parsed->host.copy(host, parsed->host.size())] = '\0';
82+
83+
if (!New(parsed->is_ipv6 ? AF_INET6 : AF_INET, host, parsed->port, addr)) {
84+
return false;
85+
}
86+
87+
// libuv resolves a zone id as an interface name, never as a number.
88+
// TODO(@araujogui): sin6_scope_id is neither exposed to JS nor hashed, so
89+
// scoped addresses do not round-trip and collide in BlockList.
90+
if (parsed->is_ipv6) {
91+
reinterpret_cast<sockaddr_in6*>(addr->storage())->sin6_scope_id =
92+
parsed->scope_id;
93+
}
94+
95+
return true;
96+
}
97+
7098
size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const {
7199
// Hash only the meaningful bytes (family + port + address), not the
72100
// full 128-byte sockaddr_storage.
@@ -82,6 +110,8 @@ size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const {
82110
case AF_INET6: {
83111
const sockaddr_in6* ipv6 =
84112
reinterpret_cast<const sockaddr_in6*>(addr.raw());
113+
// TODO(@araujogui): sin6_scope_id is not hashed, so addresses that
114+
// differ only by zone id collide.
85115
uint8_t buf[18];
86116
memcpy(buf, &ipv6->sin6_port, 2);
87117
memcpy(buf + 2, &ipv6->sin6_addr, 16);
@@ -757,6 +787,8 @@ void SocketAddressBase::Initialize(Environment* env, Local<Object> target) {
757787
"SocketAddress",
758788
GetConstructorTemplate(env),
759789
SetConstructorFunctionFlag::NONE);
790+
791+
SetMethod(env->context(), target, "parseSocketAddress", Parse);
760792
}
761793

762794
BaseObjectPtr<SocketAddressBase> SocketAddressBase::Create(
@@ -795,6 +827,20 @@ void SocketAddressBase::New(const FunctionCallbackInfo<Value>& args) {
795827
new SocketAddressBase(env, args.This(), std::move(addr));
796828
}
797829

830+
void SocketAddressBase::Parse(const FunctionCallbackInfo<Value>& args) {
831+
Environment* env = Environment::GetCurrent(args);
832+
CHECK(args[0]->IsString()); // input
833+
834+
Utf8Value input(env->isolate(), args[0]);
835+
836+
auto addr = std::make_shared<SocketAddress>();
837+
if (!SocketAddress::Parse(input.ToStringView(), addr.get())) return;
838+
839+
BaseObjectPtr<SocketAddressBase> base =
840+
SocketAddressBase::Create(env, std::move(addr));
841+
if (base) args.GetReturnValue().Set(base->object());
842+
}
843+
798844
void SocketAddressBase::Detail(const FunctionCallbackInfo<Value>& args) {
799845
Environment* env = Environment::GetCurrent(args);
800846
CHECK(args[0]->IsObject());

src/node_sockaddr.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <list>
1616
#include <memory>
1717
#include <string>
18+
#include <string_view>
1819
#include <unordered_map>
1920

2021
namespace node {
@@ -59,6 +60,9 @@ class SocketAddress : public MemoryRetainer {
5960

6061
static bool New(const char* host, uint32_t port, SocketAddress* addr);
6162

63+
// Returns true if parsing input as an "ip[:port]" socket address succeeded.
64+
static bool Parse(std::string_view input, SocketAddress* addr);
65+
6266
// Returns the port for an IPv4 or IPv6 address.
6367
inline static int GetPort(const sockaddr* addr);
6468
inline static int GetPort(const sockaddr_storage* addr);
@@ -157,6 +161,7 @@ class SocketAddressBase : public BaseObject {
157161
Environment* env, std::shared_ptr<SocketAddress> address);
158162

159163
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
164+
static void Parse(const v8::FunctionCallbackInfo<v8::Value>& args);
160165
static void Detail(const v8::FunctionCallbackInfo<v8::Value>& args);
161166
static void LegacyDetail(const v8::FunctionCallbackInfo<v8::Value>& args);
162167
static void GetFlowLabel(const v8::FunctionCallbackInfo<v8::Value>& args);

0 commit comments

Comments
 (0)