quic: start adding in the internal quic js api

While the external API for QUIC is expected to be
the WebTransport API primarily, this provides the
internal API for QUIC that aligns with the native
C++ QUIC components.

PR-URL: https://github.com/nodejs/node/pull/53256
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
James M Snell 2024-06-01 20:11:58 -07:00
parent 103b8439ca
commit cdae315706
12 changed files with 3194 additions and 380 deletions

2
.nycrc
View File

@ -4,7 +4,7 @@
"test/**",
"tools/**",
"benchmark/**",
"deps/**"
"deps/**",
],
"reporter": [
"html",

View File

@ -3644,6 +3644,42 @@ removed: v10.0.0
The `node:repl` module was unable to parse data from the REPL history file.
<a id="ERR_QUIC_CONNECTION_FAILED"></a>
### `ERR_QUIC_CONNECTION_FAILED`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Establishing a QUIC connection failed.
<a id="ERR_QUIC_ENDPOINT_CLOSED"></a>
### `ERR_QUIC_ENDPOINT_CLOSED`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
A QUIC Endpoint closed with an error.
<a id="ERR_QUIC_OPEN_STREAM_FAILED"></a>
### `ERR_QUIC_OPEN_STREAM_FAILED`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Opening a QUIC stream failed.
<a id="ERR_SOCKET_CANNOT_SEND"></a>
### `ERR_SOCKET_CANNOT_SEND`

View File

@ -1644,6 +1644,9 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => {
E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
'%d is not a valid timestamp', TypeError);
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error);
E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error);
E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error);
E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error);
E('ERR_REQUIRE_ESM',
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {

2548
lib/internal/quic/quic.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -132,6 +132,9 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
"internal/http2/core", "internal/http2/compat",
"internal/streams/lazy_transform",
#endif // !HAVE_OPENSSL
#if !NODE_OPENSSL_HAS_QUIC
"internal/quic/quic",
#endif // !NODE_OPENSSL_HAS_QUIC
"sqlite", // Experimental.
"sys", // Deprecated.
"wasi", // Experimental.

View File

@ -233,6 +233,7 @@ bool SetOption(Environment* env,
Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
if (value->IsUndefined()) return Just(Endpoint::Options());
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
@ -656,6 +657,25 @@ void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH);
static constexpr auto CLOSECONTEXT_CLOSE =
static_cast<int>(CloseContext::CLOSE);
static constexpr auto CLOSECONTEXT_BIND_FAILURE =
static_cast<int>(CloseContext::BIND_FAILURE);
static constexpr auto CLOSECONTEXT_LISTEN_FAILURE =
static_cast<int>(CloseContext::LISTEN_FAILURE);
static constexpr auto CLOSECONTEXT_RECEIVE_FAILURE =
static_cast<int>(CloseContext::RECEIVE_FAILURE);
static constexpr auto CLOSECONTEXT_SEND_FAILURE =
static_cast<int>(CloseContext::SEND_FAILURE);
static constexpr auto CLOSECONTEXT_START_FAILURE =
static_cast<int>(CloseContext::START_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_CLOSE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_BIND_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_LISTEN_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_RECEIVE_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE);
SetConstructorFunction(realm->context(),
target,
"Endpoint",
@ -682,6 +702,7 @@ Endpoint::Endpoint(Environment* env,
udp_(this),
addrLRU_(options_.address_lru_size) {
MakeWeak();
STAT_RECORD_TIMESTAMP(Stats, created_at);
IF_QUIC_DEBUG(env) {
Debug(this, "Endpoint created. Options %s", options.ToString());
}
@ -704,6 +725,7 @@ SocketAddress Endpoint::local_address() const {
void Endpoint::MarkAsBusy(bool on) {
Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy");
if (on) STAT_INCREMENT(Stats, server_busy_count);
state_->busy = on ? 1 : 0;
}
@ -1087,6 +1109,7 @@ void Endpoint::Destroy(CloseContext context, int status) {
state_->bound = 0;
state_->receiving = 0;
BindingData::Get(env()).listening_endpoints.erase(this);
STAT_RECORD_TIMESTAMP(Stats, destroyed_at);
EmitClose(close_context_, close_status_);
}

View File

@ -1682,10 +1682,16 @@ void Session::EmitStream(BaseObjectPtr<Stream> stream) {
if (is_destroyed()) return;
if (!env()->can_call_into_js()) return;
CallbackScope<Session> cb_scope(this);
Local<Value> arg = stream->object();
auto isolate = env()->isolate();
Local<Value> argv[] = {
stream->object(),
Integer::NewFromUnsigned(isolate,
static_cast<uint32_t>(stream->direction())),
};
Debug(this, "Notifying JavaScript of stream created");
MakeCallback(BindingData::Get(env()).stream_created_callback(), 1, &arg);
MakeCallback(
BindingData::Get(env()).stream_created_callback(), arraysize(argv), argv);
}
void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
@ -2358,6 +2364,11 @@ void Session::InitPerContext(Realm* realm, Local<Object> target) {
NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX);
NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN);
NODE_DEFINE_STRING_CONSTANT(
target, "DEFAULT_CIPHERS", TLSContext::DEFAULT_CIPHERS);
NODE_DEFINE_STRING_CONSTANT(
target, "DEFAULT_GROUPS", TLSContext::DEFAULT_GROUPS);
#define V(name, _) IDX_STATS_SESSION_##name,
enum SessionStatsIdx { SESSION_STATS(V) IDX_STATS_SESSION_COUNT };
#undef V

View File

@ -274,7 +274,7 @@ crypto::SSLCtxPointer TLSContext::Initialize() {
break;
}
case Side::CLIENT: {
ctx_.reset(SSL_CTX_new(TLS_client_method()));
ctx.reset(SSL_CTX_new(TLS_client_method()));
CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0);
SSL_CTX_set_session_cache_mode(

View File

@ -1,76 +1,76 @@
// Flags: --expose-internals
// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');
const { internalBinding } = require('internal/test/binding');
const {
ok,
strictEqual,
deepStrictEqual,
} = require('node:assert');
const { hasQuic } = require('../common');
const {
SocketAddress: _SocketAddress,
AF_INET,
} = internalBinding('block_list');
const quic = internalBinding('quic');
describe,
it,
} = require('node:test');
quic.setCallbacks({
onEndpointClose: common.mustCall((...args) => {
deepStrictEqual(args, [0, 0]);
}),
describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async () => {
const {
ok,
strictEqual,
throws,
} = require('node:assert');
// The following are unused in this test
onSessionNew() {},
onSessionClose() {},
onSessionDatagram() {},
onSessionDatagramStatus() {},
onSessionHandshake() {},
onSessionPathValidation() {},
onSessionTicket() {},
onSessionVersionNegotiation() {},
onStreamCreated() {},
onStreamBlocked() {},
onStreamClose() {},
onStreamReset() {},
onStreamHeaders() {},
onStreamTrailers() {},
const {
SocketAddress,
} = require('net');
const {
Endpoint,
} = require('internal/quic/quic');
it('are reasonable and work as expected', async () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
}, {});
ok(!endpoint.state.isBound);
ok(!endpoint.state.isReceiving);
ok(!endpoint.state.isListening);
strictEqual(endpoint.address, undefined);
throws(() => endpoint.listen(123), {
code: 'ERR_INVALID_ARG_TYPE',
});
endpoint.listen();
throws(() => endpoint.listen(), {
code: 'ERR_INVALID_STATE',
});
ok(endpoint.state.isBound);
ok(endpoint.state.isReceiving);
ok(endpoint.state.isListening);
const address = endpoint.address;
ok(address instanceof SocketAddress);
strictEqual(address.address, '127.0.0.1');
strictEqual(address.family, 'ipv4');
strictEqual(address.flowlabel, 0);
ok(address.port !== 0);
ok(!endpoint.destroyed);
endpoint.destroy();
strictEqual(endpoint.closed, endpoint.close());
await endpoint.closed;
ok(endpoint.destroyed);
throws(() => endpoint.listen(), {
code: 'ERR_INVALID_STATE',
});
throws(() => { endpoint.busy = true; }, {
code: 'ERR_INVALID_STATE',
});
await endpoint[Symbol.asyncDispose]();
strictEqual(endpoint.address, undefined);
});
});
const endpoint = new quic.Endpoint({});
const state = new DataView(endpoint.state);
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING));
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING));
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND));
strictEqual(endpoint.address(), undefined);
endpoint.listen({});
ok(state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING));
ok(state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING));
ok(state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND));
const address = endpoint.address();
ok(address instanceof _SocketAddress);
const detail = address.detail({
address: undefined,
port: undefined,
family: undefined,
flowlabel: undefined,
});
strictEqual(detail.address, '127.0.0.1');
strictEqual(detail.family, AF_INET);
strictEqual(detail.flowlabel, 0);
ok(detail.port !== 0);
endpoint.closeGracefully();
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING));
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING));
ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND));
strictEqual(endpoint.address(), undefined);

View File

@ -1,215 +1,230 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');
const { hasQuic } = require('../common');
const {
throws,
} = require('node:assert');
describe,
it,
} = require('node:test');
const { internalBinding } = require('internal/test/binding');
const quic = internalBinding('quic');
describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
const {
strictEqual,
throws,
} = require('node:assert');
quic.setCallbacks({
onEndpointClose() {},
onSessionNew() {},
onSessionClose() {},
onSessionDatagram() {},
onSessionDatagramStatus() {},
onSessionHandshake() {},
onSessionPathValidation() {},
onSessionTicket() {},
onSessionVersionNegotiation() {},
onStreamCreated() {},
onStreamBlocked() {},
onStreamClose() {},
onStreamReset() {},
onStreamHeaders() {},
onStreamTrailers() {},
});
const {
Endpoint,
} = require('internal/quic/quic');
throws(() => new quic.Endpoint(), {
code: 'ERR_INVALID_ARG_TYPE',
message: 'options must be an object'
});
const {
inspect,
} = require('util');
throws(() => new quic.Endpoint('a'), {
code: 'ERR_INVALID_ARG_TYPE',
message: 'options must be an object'
});
const callbackConfig = {
onsession() {},
session: {},
stream: {},
};
throws(() => new quic.Endpoint(null), {
code: 'ERR_INVALID_ARG_TYPE',
message: 'options must be an object'
});
throws(() => new quic.Endpoint(false), {
code: 'ERR_INVALID_ARG_TYPE',
message: 'options must be an object'
});
{
// Just Works... using all defaults
new quic.Endpoint({});
}
const cases = [
{
key: 'retryTokenExpiration',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'tokenExpiration',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxConnectionsPerHost',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxConnectionsTotal',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxStatelessResetsPerHost',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'addressLRUSize',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxRetries',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxPayloadSize',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'unacknowledgedPacketThreshold',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'validateAddress',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'disableStatelessReset',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'ipv6Only',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'cc',
valid: [
quic.CC_ALGO_RENO,
quic.CC_ALGO_CUBIC,
quic.CC_ALGO_BBR,
quic.CC_ALGO_BBR2,
quic.CC_ALGO_RENO_STR,
quic.CC_ALGO_CUBIC_STR,
quic.CC_ALGO_BBR_STR,
quic.CC_ALGO_BBR2_STR,
],
invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpReceiveBufferSize',
valid: [0, 1, 2, 3, 4, 1000],
invalid: [-1, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpSendBufferSize',
valid: [0, 1, 2, 3, 4, 1000],
invalid: [-1, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpTTL',
valid: [0, 1, 2, 3, 4, 255],
invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'resetTokenSecret',
valid: [
new Uint8Array(16),
new Uint16Array(8),
new Uint32Array(4),
],
invalid: [
'a', null, false, true, {}, [], () => {},
new Uint8Array(15),
new Uint8Array(17),
new ArrayBuffer(16),
],
},
{
key: 'tokenSecret',
valid: [
new Uint8Array(16),
new Uint16Array(8),
new Uint32Array(4),
],
invalid: [
'a', null, false, true, {}, [], () => {},
new Uint8Array(15),
new Uint8Array(17),
new ArrayBuffer(16),
],
},
{
// Unknown options are ignored entirely for any value type
key: 'ignored',
valid: ['a', null, false, true, {}, [], () => {}],
invalid: [],
},
];
for (const { key, valid, invalid } of cases) {
for (const value of valid) {
const options = {};
options[key] = value;
new quic.Endpoint(options);
}
for (const value of invalid) {
const options = {};
options[key] = value;
throws(() => new quic.Endpoint(options), {
code: 'ERR_INVALID_ARG_VALUE',
it('invalid options', async () => {
['a', null, false, NaN].forEach((i) => {
throws(() => new Endpoint(callbackConfig, i), {
code: 'ERR_INVALID_ARG_TYPE',
});
});
}
}
});
it('valid options', async () => {
// Just Works... using all defaults
new Endpoint(callbackConfig, {});
new Endpoint(callbackConfig);
new Endpoint(callbackConfig, undefined);
});
it('various cases', async () => {
const cases = [
{
key: 'retryTokenExpiration',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'tokenExpiration',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxConnectionsPerHost',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxConnectionsTotal',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxStatelessResetsPerHost',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'addressLRUSize',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxRetries',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxPayloadSize',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'unacknowledgedPacketThreshold',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'validateAddress',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'disableStatelessReset',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'ipv6Only',
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'cc',
valid: [
Endpoint.CC_ALGO_RENO,
Endpoint.CC_ALGO_CUBIC,
Endpoint.CC_ALGO_BBR,
Endpoint.CC_ALGO_RENO_STR,
Endpoint.CC_ALGO_CUBIC_STR,
Endpoint.CC_ALGO_BBR_STR,
],
invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpReceiveBufferSize',
valid: [0, 1, 2, 3, 4, 1000],
invalid: [-1, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpSendBufferSize',
valid: [0, 1, 2, 3, 4, 1000],
invalid: [-1, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpTTL',
valid: [0, 1, 2, 3, 4, 255],
invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'resetTokenSecret',
valid: [
new Uint8Array(16),
new Uint16Array(8),
new Uint32Array(4),
],
invalid: [
'a', null, false, true, {}, [], () => {},
new Uint8Array(15),
new Uint8Array(17),
new ArrayBuffer(16),
],
},
{
key: 'tokenSecret',
valid: [
new Uint8Array(16),
new Uint16Array(8),
new Uint32Array(4),
],
invalid: [
'a', null, false, true, {}, [], () => {},
new Uint8Array(15),
new Uint8Array(17),
new ArrayBuffer(16),
],
},
{
// Unknown options are ignored entirely for any value type
key: 'ignored',
valid: ['a', null, false, true, {}, [], () => {}],
invalid: [],
},
];
for (const { key, valid, invalid } of cases) {
for (const value of valid) {
const options = {};
options[key] = value;
new Endpoint(callbackConfig, options);
}
for (const value of invalid) {
const options = {};
options[key] = value;
throws(() => new Endpoint(callbackConfig, options), {
code: 'ERR_INVALID_ARG_VALUE',
});
}
}
});
it('endpoint can be ref/unrefed without error', async () => {
const endpoint = new Endpoint(callbackConfig, {});
endpoint.unref();
endpoint.ref();
endpoint.close();
await endpoint.closed;
});
it('endpoint can be inspected', async () => {
const endpoint = new Endpoint(callbackConfig, {});
strictEqual(typeof inspect(endpoint), 'string');
endpoint.close();
await endpoint.closed;
});
it('endpoint with object address', () => {
new Endpoint(callbackConfig, {
address: { host: '127.0.0.1:0' },
});
throws(() => new Endpoint(callbackConfig, { address: '127.0.0.1:0' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
});
});

View File

@ -1,79 +1,245 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');
const {
strictEqual,
} = require('node:assert');
const { internalBinding } = require('internal/test/binding');
const quic = internalBinding('quic');
const { hasQuic } = require('../common');
const {
IDX_STATS_ENDPOINT_CREATED_AT,
IDX_STATS_ENDPOINT_DESTROYED_AT,
IDX_STATS_ENDPOINT_BYTES_RECEIVED,
IDX_STATS_ENDPOINT_BYTES_SENT,
IDX_STATS_ENDPOINT_PACKETS_RECEIVED,
IDX_STATS_ENDPOINT_PACKETS_SENT,
IDX_STATS_ENDPOINT_SERVER_SESSIONS,
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
IDX_STATS_ENDPOINT_RETRY_COUNT,
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
IDX_STATS_ENDPOINT_COUNT,
IDX_STATE_ENDPOINT_BOUND,
IDX_STATE_ENDPOINT_BOUND_SIZE,
IDX_STATE_ENDPOINT_RECEIVING,
IDX_STATE_ENDPOINT_RECEIVING_SIZE,
IDX_STATE_ENDPOINT_LISTENING,
IDX_STATE_ENDPOINT_LISTENING_SIZE,
IDX_STATE_ENDPOINT_CLOSING,
IDX_STATE_ENDPOINT_CLOSING_SIZE,
IDX_STATE_ENDPOINT_BUSY,
IDX_STATE_ENDPOINT_BUSY_SIZE,
IDX_STATE_ENDPOINT_PENDING_CALLBACKS,
IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE,
} = quic;
describe,
it,
} = require('node:test');
const endpoint = new quic.Endpoint({});
describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
const {
Endpoint,
QuicStreamState,
QuicStreamStats,
SessionState,
SessionStats,
kFinishClose,
} = require('internal/quic/quic');
const state = new DataView(endpoint.state);
strictEqual(IDX_STATE_ENDPOINT_BOUND_SIZE, 1);
strictEqual(IDX_STATE_ENDPOINT_RECEIVING_SIZE, 1);
strictEqual(IDX_STATE_ENDPOINT_LISTENING_SIZE, 1);
strictEqual(IDX_STATE_ENDPOINT_CLOSING_SIZE, 1);
strictEqual(IDX_STATE_ENDPOINT_BUSY_SIZE, 1);
strictEqual(IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, 8);
const {
inspect,
} = require('util');
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BOUND), 0);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_RECEIVING), 0);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_LISTENING), 0);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_CLOSING), 0);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0);
strictEqual(state.getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS), 0n);
const {
deepStrictEqual,
strictEqual,
throws,
} = require('node:assert');
endpoint.markBusy(true);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 1);
endpoint.markBusy(false);
strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0);
it('endpoint state', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
});
const stats = new BigUint64Array(endpoint.stats);
strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n);
strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n);
strictEqual(IDX_STATS_ENDPOINT_COUNT, 13);
strictEqual(endpoint.state.isBound, false);
strictEqual(endpoint.state.isReceiving, false);
strictEqual(endpoint.state.isListening, false);
strictEqual(endpoint.state.isClosing, false);
strictEqual(endpoint.state.isBusy, false);
strictEqual(endpoint.state.pendingCallbacks, 0n);
deepStrictEqual(JSON.parse(JSON.stringify(endpoint.state)), {
isBound: false,
isReceiving: false,
isListening: false,
isClosing: false,
isBusy: false,
pendingCallbacks: '0',
});
endpoint.busy = true;
strictEqual(endpoint.state.isBusy, true);
endpoint.busy = false;
strictEqual(endpoint.state.isBusy, false);
it('state can be inspected without errors', () => {
strictEqual(typeof inspect(endpoint.state), 'string');
});
});
it('state is not readable after close', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
}, {});
endpoint.state[kFinishClose]();
throws(() => endpoint.state.isBound, {
name: 'Error',
});
});
it('state constructor argument is ArrayBuffer', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
}, {});
const Cons = endpoint.state.constructor;
throws(() => new Cons(1), {
code: 'ERR_INVALID_ARG_TYPE'
});
});
it('endpoint stats', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
});
strictEqual(typeof endpoint.stats.createdAt, 'bigint');
strictEqual(typeof endpoint.stats.destroyedAt, 'bigint');
strictEqual(typeof endpoint.stats.bytesReceived, 'bigint');
strictEqual(typeof endpoint.stats.bytesSent, 'bigint');
strictEqual(typeof endpoint.stats.packetsReceived, 'bigint');
strictEqual(typeof endpoint.stats.packetsSent, 'bigint');
strictEqual(typeof endpoint.stats.serverSessions, 'bigint');
strictEqual(typeof endpoint.stats.clientSessions, 'bigint');
strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint');
strictEqual(typeof endpoint.stats.retryCount, 'bigint');
strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint');
strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint');
strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint');
deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [
'createdAt',
'destroyedAt',
'bytesReceived',
'bytesSent',
'packetsReceived',
'packetsSent',
'serverSessions',
'clientSessions',
'serverBusyCount',
'retryCount',
'versionNegotiationCount',
'statelessResetCount',
'immediateCloseCount',
]);
it('stats can be inspected without errors', () => {
strictEqual(typeof inspect(endpoint.stats), 'string');
});
});
it('stats are still readble after close', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
}, {});
strictEqual(typeof endpoint.stats.toJSON(), 'object');
endpoint.stats[kFinishClose]();
strictEqual(typeof endpoint.stats.destroyedAt, 'bigint');
strictEqual(typeof endpoint.stats.toJSON(), 'object');
});
it('stats constructor argument is ArrayBuffer', () => {
const endpoint = new Endpoint({
onsession() {},
session: {},
stream: {},
}, {});
const Cons = endpoint.stats.constructor;
throws(() => new Cons(1), {
code: 'ERR_INVALID_ARG_TYPE',
});
});
// TODO(@jasnell): The following tests are largely incomplete.
// This is largely here to boost the code coverage numbers
// temporarily while the rest of the functionality is being
// implemented.
it('stream and session states', () => {
const streamState = new QuicStreamState(new ArrayBuffer(1024));
const sessionState = new SessionState(new ArrayBuffer(1024));
strictEqual(streamState.finSent, false);
strictEqual(streamState.finReceived, false);
strictEqual(streamState.readEnded, false);
strictEqual(streamState.writeEnded, false);
strictEqual(streamState.destroyed, false);
strictEqual(streamState.paused, false);
strictEqual(streamState.reset, false);
strictEqual(streamState.hasReader, false);
strictEqual(streamState.wantsBlock, false);
strictEqual(streamState.wantsHeaders, false);
strictEqual(streamState.wantsReset, false);
strictEqual(streamState.wantsTrailers, false);
strictEqual(sessionState.hasPathValidationListener, false);
strictEqual(sessionState.hasVersionNegotiationListener, false);
strictEqual(sessionState.hasDatagramListener, false);
strictEqual(sessionState.hasSessionTicketListener, false);
strictEqual(sessionState.isClosing, false);
strictEqual(sessionState.isGracefulClose, false);
strictEqual(sessionState.isSilentClose, false);
strictEqual(sessionState.isStatelessReset, false);
strictEqual(sessionState.isDestroyed, false);
strictEqual(sessionState.isHandshakeCompleted, false);
strictEqual(sessionState.isHandshakeConfirmed, false);
strictEqual(sessionState.isStreamOpenAllowed, false);
strictEqual(sessionState.isPrioritySupported, false);
strictEqual(sessionState.isWrapped, false);
strictEqual(sessionState.lastDatagramId, 0n);
strictEqual(typeof streamState.toJSON(), 'object');
strictEqual(typeof sessionState.toJSON(), 'object');
strictEqual(typeof inspect(streamState), 'string');
strictEqual(typeof inspect(sessionState), 'string');
});
it('stream and session stats', () => {
const streamStats = new QuicStreamStats(new ArrayBuffer(1024));
const sessionStats = new SessionStats(new ArrayBuffer(1024));
strictEqual(streamStats.createdAt, undefined);
strictEqual(streamStats.receivedAt, undefined);
strictEqual(streamStats.ackedAt, undefined);
strictEqual(streamStats.closingAt, undefined);
strictEqual(streamStats.destroyedAt, undefined);
strictEqual(streamStats.bytesReceived, undefined);
strictEqual(streamStats.bytesSent, undefined);
strictEqual(streamStats.maxOffset, undefined);
strictEqual(streamStats.maxOffsetAcknowledged, undefined);
strictEqual(streamStats.maxOffsetReceived, undefined);
strictEqual(streamStats.finalSize, undefined);
strictEqual(typeof streamStats.toJSON(), 'object');
strictEqual(typeof inspect(streamStats), 'string');
streamStats[kFinishClose]();
strictEqual(typeof sessionStats.createdAt, 'bigint');
strictEqual(typeof sessionStats.closingAt, 'bigint');
strictEqual(typeof sessionStats.destroyedAt, 'bigint');
strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint');
strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint');
strictEqual(typeof sessionStats.gracefulClosingAt, 'bigint');
strictEqual(typeof sessionStats.bytesReceived, 'bigint');
strictEqual(typeof sessionStats.bytesSent, 'bigint');
strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint');
strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint');
strictEqual(typeof sessionStats.uniInStreamCount, 'bigint');
strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint');
strictEqual(typeof sessionStats.lossRetransmitCount, 'bigint');
strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint');
strictEqual(typeof sessionStats.bytesInFlight, 'bigint');
strictEqual(typeof sessionStats.blockCount, 'bigint');
strictEqual(typeof sessionStats.cwnd, 'bigint');
strictEqual(typeof sessionStats.latestRtt, 'bigint');
strictEqual(typeof sessionStats.minRtt, 'bigint');
strictEqual(typeof sessionStats.rttVar, 'bigint');
strictEqual(typeof sessionStats.smoothedRtt, 'bigint');
strictEqual(typeof sessionStats.ssthresh, 'bigint');
strictEqual(typeof sessionStats.datagramsReceived, 'bigint');
strictEqual(typeof sessionStats.datagramsSent, 'bigint');
strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint');
strictEqual(typeof sessionStats.datagramsLost, 'bigint');
strictEqual(typeof sessionStats.toJSON(), 'object');
strictEqual(typeof inspect(sessionStats), 'string');
streamStats[kFinishClose]();
});
});

View File

@ -1,38 +1,47 @@
// Flags: --expose-internals
// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');
const { internalBinding } = require('internal/test/binding');
const quic = internalBinding('quic');
const { hasQuic } = require('../common');
const { throws } = require('assert');
const {
describe,
it,
} = require('node:test');
const callbacks = {
onEndpointClose() {},
onSessionNew() {},
onSessionClose() {},
onSessionDatagram() {},
onSessionDatagramStatus() {},
onSessionHandshake() {},
onSessionPathValidation() {},
onSessionTicket() {},
onSessionVersionNegotiation() {},
onStreamCreated() {},
onStreamBlocked() {},
onStreamClose() {},
onStreamReset() {},
onStreamHeaders() {},
onStreamTrailers() {},
};
// Fail if any callback is missing
for (const fn of Object.keys(callbacks)) {
// eslint-disable-next-line no-unused-vars
const { [fn]: _, ...rest } = callbacks;
throws(() => quic.setCallbacks(rest), {
code: 'ERR_MISSING_ARGS',
describe('quic internal setCallbacks', { skip: !hasQuic }, () => {
const { internalBinding } = require('internal/test/binding');
const quic = internalBinding('quic');
it('require all callbacks to be set', (t) => {
const callbacks = {
onEndpointClose() {},
onSessionNew() {},
onSessionClose() {},
onSessionDatagram() {},
onSessionDatagramStatus() {},
onSessionHandshake() {},
onSessionPathValidation() {},
onSessionTicket() {},
onSessionVersionNegotiation() {},
onStreamCreated() {},
onStreamBlocked() {},
onStreamClose() {},
onStreamReset() {},
onStreamHeaders() {},
onStreamTrailers() {},
};
// Fail if any callback is missing
for (const fn of Object.keys(callbacks)) {
// eslint-disable-next-line no-unused-vars
const { [fn]: _, ...rest } = callbacks;
t.assert.throws(() => quic.setCallbacks(rest), {
code: 'ERR_MISSING_ARGS',
});
}
// If all callbacks are present it should work
quic.setCallbacks(callbacks);
// Multiple calls should just be ignored.
quic.setCallbacks(callbacks);
});
}
// If all callbacks are present it should work
quic.setCallbacks(callbacks);
});