summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2021-03-25 12:19:30 -0700
committerMyles Borins <mylesborins@github.com>2021-04-04 15:27:04 -0400
commitdaa8a7bbcf2a65358427649ed8c2b64732e74fad (patch)
tree99f97ff5a92ac774bb24d9a5d73caf01aac535c8
parenta4169ce519a601cb17e85c66aa240c2ea22b5934 (diff)
downloadnode-new-daa8a7bbcf2a65358427649ed8c2b64732e74fad.tar.gz
net: add SocketAddress class
Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/37917 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
-rw-r--r--doc/api/net.md45
-rw-r--r--doc/api/worker_threads.md1
-rw-r--r--lib/internal/socketaddress.js153
-rw-r--r--lib/net.js5
-rw-r--r--node.gyp1
-rw-r--r--src/env.h2
-rw-r--r--src/node_errors.h2
-rw-r--r--src/node_sockaddr.cc143
-rw-r--r--src/node_sockaddr.h54
-rw-r--r--test/parallel/test-socketaddress.js110
10 files changed, 516 insertions, 0 deletions
diff --git a/doc/api/net.md b/doc/api/net.md
index 50ea9ab6c4..c7f3ba5616 100644
--- a/doc/api/net.md
+++ b/doc/api/net.md
@@ -135,6 +135,51 @@ added: v15.0.0
The list of rules added to the blocklist.
+## Class: `net.SocketAddress`
+<!-- YAML
+added: REPLACEME
+-->
+### `new net.SocketAddress([options])`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `options` {Object}
+ * `address` {string} The network address as either an IPv4 or IPv6 string.
+ **Default**: `'127.0.0.1'` if `family` is `'ipv4'`; `'::'` if `family` is
+ `'ipv6'`.
+ * `family` {string} One of either `'ipv4'` or 'ipv6'`. **Default**: `'ipv4'`.
+ * `flowlabel` {number} An IPv6 flow-label used only if `family` is `'ipv6'`.
+ * `port` {number} An IP port.
+
+### `socketaddress.address`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type {string}
+
+### `socketaddress.family`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type {string} Either `'ipv4'` or `'ipv6'`.
+
+### `socketaddress.flowlabel`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type {number}
+
+### `socketaddress.port`
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type {number}
+
## Class: `net.Server`
<!-- YAML
added: v0.1.90
diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md
index 67af0cb337..891e2414b1 100644
--- a/doc/api/worker_threads.md
+++ b/doc/api/worker_threads.md
@@ -573,6 +573,7 @@ In particular, the significant differences to `JSON` are:
* {KeyObject}s,
* {MessagePort}s,
* {net.BlockList}s,
+ * {net.SocketAddress}es,
* {X509Certificate}s.
```js
diff --git a/lib/internal/socketaddress.js b/lib/internal/socketaddress.js
new file mode 100644
index 0000000000..d341abb297
--- /dev/null
+++ b/lib/internal/socketaddress.js
@@ -0,0 +1,153 @@
+'use strict';
+
+const {
+ ObjectSetPrototypeOf,
+ Symbol,
+} = primordials;
+
+const {
+ SocketAddress: _SocketAddress,
+ AF_INET,
+ AF_INET6,
+} = internalBinding('block_list');
+
+const {
+ validateObject,
+ validateString,
+ validatePort,
+ validateUint32,
+} = require('internal/validators');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_VALUE,
+ },
+} = require('internal/errors');
+
+const {
+ customInspectSymbol: kInspect,
+} = require('internal/util');
+
+const { inspect } = require('internal/util/inspect');
+
+const {
+ JSTransferable,
+ kClone,
+ kDeserialize,
+} = require('internal/worker/js_transferable');
+
+const kHandle = Symbol('kHandle');
+const kDetail = Symbol('kDetail');
+
+class SocketAddress extends JSTransferable {
+ static isSocketAddress(value) {
+ return value?.[kHandle] !== undefined;
+ }
+
+ constructor(options = {}) {
+ super();
+ validateObject(options, 'options');
+ const {
+ family = 'ipv4',
+ address = (family === 'ipv4' ? '127.0.0.1' : '::'),
+ port = 0,
+ flowlabel = 0,
+ } = options;
+
+ let type;
+ switch (family) {
+ case 'ipv4':
+ type = AF_INET;
+ break;
+ case 'ipv6':
+ type = AF_INET6;
+ break;
+ default:
+ throw new ERR_INVALID_ARG_VALUE('options.family', family);
+ }
+
+ validateString(address, 'options.address');
+ validatePort(port, 'options.port');
+ validateUint32(flowlabel, 'options.flowlabel', false);
+
+ this[kHandle] = new _SocketAddress(address, port, type, flowlabel);
+ this[kDetail] = this[kHandle].detail({
+ address: undefined,
+ port: undefined,
+ family: undefined,
+ flowlabel: undefined,
+ });
+ }
+
+ get address() {
+ return this[kDetail].address;
+ }
+
+ get port() {
+ return this[kDetail].port;
+ }
+
+ get family() {
+ return this[kDetail].family === AF_INET ? 'ipv4' : 'ipv6';
+ }
+
+ get flowlabel() {
+ // The flow label can be changed internally.
+ return this[kHandle].flowlabel();
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1
+ };
+
+ return `SocketAddress ${inspect(this.toJSON(), opts)}`;
+ }
+
+ [kClone]() {
+ const handle = this[kHandle];
+ return {
+ data: { handle },
+ deserializeInfo: 'internal/socketaddress:InternalSocketAddress',
+ };
+ }
+
+ [kDeserialize]({ handle }) {
+ this[kHandle] = handle;
+ this[kDetail] = handle.detail({
+ address: undefined,
+ port: undefined,
+ family: undefined,
+ flowlabel: undefined,
+ });
+ }
+
+ toJSON() {
+ return {
+ address: this.address,
+ port: this.port,
+ family: this.family,
+ flowlabel: this.flowlabel,
+ };
+ }
+}
+
+class InternalSocketAddress extends JSTransferable {
+ constructor(handle) {
+ super();
+ this[kHandle] = handle;
+ }
+}
+
+InternalSocketAddress.prototype.constructor =
+ SocketAddress.prototype.construtor;
+ObjectSetPrototypeOf(InternalSocketAddress.prototype, SocketAddress.prototype);
+
+module.exports = {
+ SocketAddress,
+ InternalSocketAddress,
+};
diff --git a/lib/net.js b/lib/net.js
index ca2e6085ac..a28310f512 100644
--- a/lib/net.js
+++ b/lib/net.js
@@ -122,6 +122,7 @@ const {
let cluster;
let dns;
let BlockList;
+let SocketAddress;
const { clearTimeout } = require('timers');
const { kTimeout } = require('internal/timers');
@@ -1751,6 +1752,10 @@ module.exports = {
BlockList ??= require('internal/blocklist').BlockList;
return BlockList;
},
+ get SocketAddress() {
+ SocketAddress ??= require('internal/socketaddress').SocketAddress;
+ return SocketAddress;
+ },
connect,
createConnection: connect,
createServer,
diff --git a/node.gyp b/node.gyp
index 2d76366e33..260b52e652 100644
--- a/node.gyp
+++ b/node.gyp
@@ -212,6 +212,7 @@
'lib/internal/repl/await.js',
'lib/internal/repl/history.js',
'lib/internal/repl/utils.js',
+ 'lib/internal/socketaddress.js',
'lib/internal/socket_list.js',
'lib/internal/source_map/prepare_stack_trace.js',
'lib/internal/source_map/source_map.js',
diff --git a/src/env.h b/src/env.h
index fbf254f647..ef6a81de7c 100644
--- a/src/env.h
+++ b/src/env.h
@@ -258,6 +258,7 @@ constexpr size_t kFsStatsBufferLength =
V(fingerprint256_string, "fingerprint256") \
V(fingerprint_string, "fingerprint") \
V(flags_string, "flags") \
+ V(flowlabel_string, "flowlabel") \
V(fragment_string, "fragment") \
V(function_string, "function") \
V(get_data_clone_error_string, "_getDataCloneError") \
@@ -465,6 +466,7 @@ constexpr size_t kFsStatsBufferLength =
V(script_context_constructor_template, v8::FunctionTemplate) \
V(secure_context_constructor_template, v8::FunctionTemplate) \
V(shutdown_wrap_template, v8::ObjectTemplate) \
+ V(socketaddress_constructor_template, v8::FunctionTemplate) \
V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \
V(qlogoutputstream_constructor_template, v8::ObjectTemplate) \
V(tcp_constructor_template, v8::FunctionTemplate) \
diff --git a/src/node_errors.h b/src/node_errors.h
index 1ae461f23d..291365fa3b 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -56,6 +56,7 @@ void OnFatalError(const char* location, const char* message);
V(ERR_CRYPTO_JOB_INIT_FAILED, Error) \
V(ERR_DLOPEN_FAILED, Error) \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
+ V(ERR_INVALID_ADDRESS, Error) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \
V(ERR_INVALID_ARG_TYPE, TypeError) \
@@ -143,6 +144,7 @@ ERRORS_WITH_CODE(V)
V(ERR_DLOPEN_FAILED, "DLOpen failed") \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \
"Context not associated with Node.js environment") \
+ V(ERR_INVALID_ADDRESS, "Invalid socket address") \
V(ERR_INVALID_MODULE, "No such module") \
V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \
V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \
diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc
index 653047cc59..e745589b12 100644
--- a/src/node_sockaddr.cc
+++ b/src/node_sockaddr.cc
@@ -3,6 +3,7 @@
#include "base64-inl.h"
#include "base_object-inl.h"
#include "memory_tracker-inl.h"
+#include "node_errors.h"
#include "uv.h"
#include <memory>
@@ -15,9 +16,11 @@ using v8::Array;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
+using v8::Int32;
using v8::Local;
using v8::MaybeLocal;
using v8::Object;
+using v8::Uint32;
using v8::Value;
namespace {
@@ -752,6 +755,8 @@ void SocketAddressBlockListWrap::Initialize(
GetConstructorTemplate(env),
Environment::SetConstructorFunctionFlag::NONE);
+ SocketAddressBase::Initialize(env, target);
+
NODE_DEFINE_CONSTANT(target, AF_INET);
NODE_DEFINE_CONSTANT(target, AF_INET6);
}
@@ -768,6 +773,144 @@ void SocketAddressBlockListWrap::TransferData::MemoryInfo(
blocklist_->MemoryInfo(tracker);
}
+bool SocketAddressBase::HasInstance(Environment* env, Local<Value> value) {
+ return GetConstructorTemplate(env)->HasInstance(value);
+}
+
+Local<FunctionTemplate> SocketAddressBase::GetConstructorTemplate(
+ Environment* env) {
+ Local<FunctionTemplate> tmpl = env->socketaddress_constructor_template();
+ if (tmpl.IsEmpty()) {
+ tmpl = env->NewFunctionTemplate(New);
+ tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "SocketAddress"));
+ tmpl->InstanceTemplate()->SetInternalFieldCount(
+ SocketAddressBase::kInternalFieldCount);
+ tmpl->Inherit(BaseObject::GetConstructorTemplate(env));
+ env->SetProtoMethod(tmpl, "detail", Detail);
+ env->SetProtoMethod(tmpl, "legacyDetail", LegacyDetail);
+ env->SetProtoMethodNoSideEffect(tmpl, "flowlabel", GetFlowLabel);
+ env->set_socketaddress_constructor_template(tmpl);
+ }
+ return tmpl;
+}
+
+void SocketAddressBase::Initialize(Environment* env, Local<Object> target) {
+ env->SetConstructorFunction(
+ target,
+ "SocketAddress",
+ GetConstructorTemplate(env),
+ Environment::SetConstructorFunctionFlag::NONE);
+}
+
+BaseObjectPtr<SocketAddressBase> SocketAddressBase::Create(
+ Environment* env,
+ std::shared_ptr<SocketAddress> address) {
+ Local<Object> obj;
+ if (!GetConstructorTemplate(env)
+ ->InstanceTemplate()
+ ->NewInstance(env->context()).ToLocal(&obj)) {
+ return BaseObjectPtr<SocketAddressBase>();
+ }
+
+ return MakeBaseObject<SocketAddressBase>(env, obj, std::move(address));
+}
+
+void SocketAddressBase::New(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args.IsConstructCall());
+ CHECK(args[0]->IsString()); // address
+ CHECK(args[1]->IsInt32()); // port
+ CHECK(args[2]->IsInt32()); // family
+ CHECK(args[3]->IsUint32()); // flow label
+
+ Utf8Value address(env->isolate(), args[0]);
+ int32_t port = args[1].As<Int32>()->Value();
+ int32_t family = args[2].As<Int32>()->Value();
+ uint32_t flow_label = args[3].As<Uint32>()->Value();
+
+ std::shared_ptr<SocketAddress> addr = std::make_shared<SocketAddress>();
+
+ if (!SocketAddress::New(family, *address, port, addr.get()))
+ return THROW_ERR_INVALID_ADDRESS(env);
+
+ addr->set_flow_label(flow_label);
+
+ new SocketAddressBase(env, args.This(), std::move(addr));
+}
+
+void SocketAddressBase::Detail(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsObject());
+ Local<Object> detail = args[0].As<Object>();
+
+ SocketAddressBase* base;
+ ASSIGN_OR_RETURN_UNWRAP(&base, args.Holder());
+
+ Local<Value> address;
+ if (!ToV8Value(env->context(), base->address_->address()).ToLocal(&address))
+ return;
+
+ if (detail->Set(env->context(), env->address_string(), address).IsJust() &&
+ detail->Set(
+ env->context(),
+ env->port_string(),
+ Int32::New(env->isolate(), base->address_->port())).IsJust() &&
+ detail->Set(
+ env->context(),
+ env->family_string(),
+ Int32::New(env->isolate(), base->address_->family())).IsJust() &&
+ detail->Set(
+ env->context(),
+ env->flowlabel_string(),
+ Uint32::New(env->isolate(), base->address_->flow_label()))
+ .IsJust()) {
+ args.GetReturnValue().Set(detail);
+ }
+}
+
+void SocketAddressBase::GetFlowLabel(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ SocketAddressBase* base;
+ ASSIGN_OR_RETURN_UNWRAP(&base, args.Holder());
+ args.GetReturnValue().Set(base->address_->flow_label());
+}
+
+void SocketAddressBase::LegacyDetail(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ SocketAddressBase* base;
+ ASSIGN_OR_RETURN_UNWRAP(&base, args.Holder());
+ args.GetReturnValue().Set(base->address_->ToJS(env));
+}
+
+SocketAddressBase::SocketAddressBase(
+ Environment* env,
+ Local<Object> wrap,
+ std::shared_ptr<SocketAddress> address)
+ : BaseObject(env, wrap),
+ address_(std::move(address)) {
+ MakeWeak();
+}
+
+void SocketAddressBase::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("address", address_);
+}
+
+std::unique_ptr<worker::TransferData>
+SocketAddressBase::CloneForMessaging() const {
+ return std::make_unique<TransferData>(this);
+}
+
+void SocketAddressBase::TransferData::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("address", address_);
+}
+
+BaseObjectPtr<BaseObject> SocketAddressBase::TransferData::Deserialize(
+ Environment* env,
+ v8::Local<v8::Context> context,
+ std::unique_ptr<worker::TransferData> self) {
+ return SocketAddressBase::Create(env, std::move(address_));
+}
+
} // namespace node
NODE_MODULE_CONTEXT_AWARE_INTERNAL(
diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h
index 670a8b78d8..62dcab6bad 100644
--- a/src/node_sockaddr.h
+++ b/src/node_sockaddr.h
@@ -148,6 +148,60 @@ class SocketAddress : public MemoryRetainer {
sockaddr_storage address_;
};
+class SocketAddressBase : public BaseObject {
+ public:
+ static bool HasInstance(Environment* env, v8::Local<v8::Value> value);
+ static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
+ Environment* env);
+ static void Initialize(Environment* env, v8::Local<v8::Object> target);
+ static BaseObjectPtr<SocketAddressBase> Create(
+ Environment* env,
+ std::shared_ptr<SocketAddress> address);
+
+ static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void Detail(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void LegacyDetail(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void GetFlowLabel(const v8::FunctionCallbackInfo<v8::Value>& args);
+
+ SocketAddressBase(
+ Environment* env,
+ v8::Local<v8::Object> wrap,
+ std::shared_ptr<SocketAddress> address);
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(SocketAddressBase);
+ SET_SELF_SIZE(SocketAddressBase);
+
+ TransferMode GetTransferMode() const override {
+ return TransferMode::kCloneable;
+ }
+ std::unique_ptr<worker::TransferData> CloneForMessaging() const override;
+
+ class TransferData : public worker::TransferData {
+ public:
+ inline explicit TransferData(const SocketAddressBase* wrap)
+ : address_(wrap->address_) {}
+
+ inline explicit TransferData(std::shared_ptr<SocketAddress> address)
+ : address_(std::move(address)) {}
+
+ BaseObjectPtr<BaseObject> Deserialize(
+ Environment* env,
+ v8::Local<v8::Context> context,
+ std::unique_ptr<worker::TransferData> self) override;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(SocketAddressBase::TransferData)
+ SET_SELF_SIZE(TransferData)
+
+ private:
+ std::shared_ptr<SocketAddress> address_;
+ };
+
+ private:
+ std::shared_ptr<SocketAddress> address_;
+};
+
template <typename T>
class SocketAddressLRU : public MemoryRetainer {
public:
diff --git a/test/parallel/test-socketaddress.js b/test/parallel/test-socketaddress.js
new file mode 100644
index 0000000000..4caf46dd3e
--- /dev/null
+++ b/test/parallel/test-socketaddress.js
@@ -0,0 +1,110 @@
+'use strict';
+
+const common = require('../common');
+const {
+ ok,
+ strictEqual,
+ throws,
+} = require('assert');
+const {
+ SocketAddress,
+} = require('net');
+
+{
+ const sa = new SocketAddress();
+ strictEqual(sa.address, '127.0.0.1');
+ strictEqual(sa.port, 0);
+ strictEqual(sa.family, 'ipv4');
+ strictEqual(sa.flowlabel, 0);
+
+ 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);
+}
+
+{
+ 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',
+ });
+ 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
+ });
+ 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'
+ });
+ strictEqual(sa.address, '::');
+ strictEqual(sa.port, 0);
+ strictEqual(sa.family, 'ipv6');
+ strictEqual(sa.flowlabel, 0);
+}
+
+{
+ 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, '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'
+});