diff options
author | James M Snell <jasnell@gmail.com> | 2021-03-25 12:19:30 -0700 |
---|---|---|
committer | Myles Borins <mylesborins@github.com> | 2021-04-04 15:27:04 -0400 |
commit | daa8a7bbcf2a65358427649ed8c2b64732e74fad (patch) | |
tree | 99f97ff5a92ac774bb24d9a5d73caf01aac535c8 | |
parent | a4169ce519a601cb17e85c66aa240c2ea22b5934 (diff) | |
download | node-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.md | 45 | ||||
-rw-r--r-- | doc/api/worker_threads.md | 1 | ||||
-rw-r--r-- | lib/internal/socketaddress.js | 153 | ||||
-rw-r--r-- | lib/net.js | 5 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | src/env.h | 2 | ||||
-rw-r--r-- | src/node_errors.h | 2 | ||||
-rw-r--r-- | src/node_sockaddr.cc | 143 | ||||
-rw-r--r-- | src/node_sockaddr.h | 54 | ||||
-rw-r--r-- | test/parallel/test-socketaddress.js | 110 |
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, @@ -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', @@ -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' +}); |