mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
quic: complete the internal implementation of QUIC
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: https://github.com/nodejs/node/pull/62876 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Robert Nagy <ronagy@icloud.com>
This commit is contained in:
1 parent
2b112e70fe
commit
cf91d181fb
287 files changed
+26391
-2573
No files matched your search
Vendored
+9
-2
@@ -206,6 +206,7 @@
|
||||
'defines': [
|
||||
'BUILDING_NGHTTP3',
|
||||
'NGHTTP3_STATICLIB',
|
||||
'DEBUGBUILD',
|
||||
],
|
||||
'dependencies': [
|
||||
'ngtcp2'
|
||||
@@ -247,7 +248,10 @@
|
||||
},
|
||||
{
|
||||
'target_name': 'ngtcp2_test_server',
|
||||
'type': 'executable',
|
||||
# Disabled: ngtcp2 examples now require C++23 (<print>, <expected>,
|
||||
# std::println, std::expected) which is not yet supported on all
|
||||
# Node.js platforms. Re-enable when C++23 is available.
|
||||
'type': 'none',
|
||||
'cflags': [ '-Wno-everything' ],
|
||||
'include_dirs': [
|
||||
'',
|
||||
@@ -305,7 +309,10 @@
|
||||
},
|
||||
{
|
||||
'target_name': 'ngtcp2_test_client',
|
||||
'type': 'executable',
|
||||
# Disabled: ngtcp2 examples now require C++23 (<print>, <expected>,
|
||||
# std::println, std::expected) which is not yet supported on all
|
||||
# Node.js platforms. Re-enable when C++23 is available.
|
||||
'type': 'none',
|
||||
'cflags': [ '-Wno-everything' ],
|
||||
'include_dirs': [
|
||||
'',
|
||||
|
||||
@@ -2651,6 +2651,32 @@ added:
|
||||
|
||||
Opening a QUIC stream failed.
|
||||
|
||||
<a id="ERR_QUIC_STREAM_ABORTED"></a>
|
||||
|
||||
### `ERR_QUIC_STREAM_ABORTED`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
The Node.js error code for a [`QuicError`][] thrown to abort a QUIC stream
|
||||
or session with an explicit application or transport error code.
|
||||
|
||||
<a id="ERR_QUIC_STREAM_RESET"></a>
|
||||
|
||||
### `ERR_QUIC_STREAM_RESET`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
A QUIC stream was reset by the peer. The error includes the reset code
|
||||
provided by the peer.
|
||||
|
||||
<a id="ERR_QUIC_TRANSPORT_ERROR"></a>
|
||||
|
||||
### `ERR_QUIC_TRANSPORT_ERROR`
|
||||
@@ -4436,6 +4462,7 @@ An error occurred trying to allocate memory. This should never happen.
|
||||
[`MessagePort`]: worker_threads.md#class-messageport
|
||||
[`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
|
||||
[`Object.setPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
|
||||
[`QuicError`]: quic.md#class-quicerror
|
||||
[`REPL`]: repl.md
|
||||
[`ServerResponse`]: http.md#class-httpserverresponse
|
||||
[`Writable`]: stream.md#class-streamwritable
|
||||
|
||||
+1771
-50
File diff suppressed because it is too large.
Load diff
+71
-1
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
MathMax,
|
||||
MathMin,
|
||||
ObjectDefineProperties,
|
||||
@@ -536,13 +537,82 @@ function createBlobReaderStream(reader) {
|
||||
}, { highWaterMark: 0 });
|
||||
}
|
||||
|
||||
// Maximum number of chunks to collect in a single batch to prevent
|
||||
// unbounded memory growth when the DataQueue has a large burst of data.
|
||||
const kMaxBatchChunks = 16;
|
||||
|
||||
async function* createBlobReaderIterable(reader, options = {}) {
|
||||
const { getReadError } = options;
|
||||
let wakeup = PromiseWithResolvers();
|
||||
reader.setWakeup(wakeup.resolve);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const batch = [];
|
||||
let blocked = false;
|
||||
let eos = false;
|
||||
let error = null;
|
||||
|
||||
// Pull as many chunks as available synchronously.
|
||||
// reader.pull(callback) calls the callback synchronously via
|
||||
// MakeCallback, so we can collect multiple chunks per iteration
|
||||
// step without any async overhead.
|
||||
while (true) {
|
||||
let pullResult;
|
||||
reader.pull((status, buffer) => {
|
||||
pullResult = { status, buffer };
|
||||
});
|
||||
|
||||
if (pullResult.status === 0) {
|
||||
eos = true;
|
||||
break;
|
||||
}
|
||||
if (pullResult.status < 0) {
|
||||
error = typeof getReadError === 'function' ?
|
||||
getReadError(pullResult.status) :
|
||||
new ERR_INVALID_STATE('The reader is not readable');
|
||||
break;
|
||||
}
|
||||
if (pullResult.status === 2) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
ArrayPrototypePush(batch, new Uint8Array(pullResult.buffer));
|
||||
if (batch.length >= kMaxBatchChunks) break;
|
||||
}
|
||||
|
||||
if (batch.length > 0) {
|
||||
yield batch;
|
||||
}
|
||||
|
||||
if (eos) return;
|
||||
if (error) throw error;
|
||||
|
||||
if (blocked) {
|
||||
const fin = await wakeup.promise;
|
||||
wakeup = PromiseWithResolvers();
|
||||
reader.setWakeup(wakeup.resolve);
|
||||
// If the wakeup was triggered by FIN (EndReadable), the DataQueue
|
||||
// is capped. Continue the loop to pull again -- the next pull will
|
||||
// return EOS. Without this, a race between the data notification
|
||||
// and the FIN notification can leave the iterator waiting for a
|
||||
// wakeup that will never come.
|
||||
if (fin) continue;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.setWakeup(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Blob,
|
||||
createBlob,
|
||||
createBlobFromFilePath,
|
||||
createBlobReaderIterable,
|
||||
createBlobReaderStream,
|
||||
isBlob,
|
||||
kHandle,
|
||||
resolveObjectURL,
|
||||
TransferableBlob,
|
||||
createBlobReaderStream,
|
||||
};
|
||||
@@ -1691,6 +1691,9 @@ E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Er
|
||||
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_QUIC_STREAM_ABORTED', '%s', Error);
|
||||
E('ERR_QUIC_STREAM_RESET',
|
||||
'The QUIC stream was reset by the peer with error code %d', Error);
|
||||
E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error);
|
||||
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error);
|
||||
E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) {
|
||||
|
||||
@@ -1994,6 +1994,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
FileHandle,
|
||||
kHandle,
|
||||
kLocked,
|
||||
kRef,
|
||||
kUnref,
|
||||
};
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
|
||||
NODE_PERFORMANCE_ENTRY_TYPE_NET,
|
||||
NODE_PERFORMANCE_ENTRY_TYPE_DNS,
|
||||
NODE_PERFORMANCE_ENTRY_TYPE_QUIC,
|
||||
},
|
||||
installGarbageCollectionTracking,
|
||||
observerCounts,
|
||||
@@ -87,6 +88,7 @@ const kSupportedEntryTypes = ObjectFreeze([
|
||||
'mark',
|
||||
'measure',
|
||||
'net',
|
||||
'quic',
|
||||
'resource',
|
||||
]);
|
||||
|
||||
@@ -131,6 +133,7 @@ function getObserverType(type) {
|
||||
case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
|
||||
case 'net': return NODE_PERFORMANCE_ENTRY_TYPE_NET;
|
||||
case 'dns': return NODE_PERFORMANCE_ENTRY_TYPE_DNS;
|
||||
case 'quic': return NODE_PERFORMANCE_ENTRY_TYPE_QUIC;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const dc = require('diagnostics_channel');
|
||||
|
||||
const onEndpointCreatedChannel = dc.channel('quic.endpoint.created');
|
||||
const onEndpointListeningChannel = dc.channel('quic.endpoint.listen');
|
||||
const onEndpointClosingChannel = dc.channel('quic.endpoint.closing');
|
||||
const onEndpointClosedChannel = dc.channel('quic.endpoint.closed');
|
||||
const onEndpointErrorChannel = dc.channel('quic.endpoint.error');
|
||||
const onEndpointBusyChangeChannel = dc.channel('quic.endpoint.busy.change');
|
||||
const onEndpointClientSessionChannel = dc.channel('quic.session.created.client');
|
||||
const onEndpointServerSessionChannel = dc.channel('quic.session.created.server');
|
||||
const onSessionOpenStreamChannel = dc.channel('quic.session.open.stream');
|
||||
const onSessionReceivedStreamChannel = dc.channel('quic.session.received.stream');
|
||||
const onSessionSendDatagramChannel = dc.channel('quic.session.send.datagram');
|
||||
const onSessionUpdateKeyChannel = dc.channel('quic.session.update.key');
|
||||
const onSessionClosingChannel = dc.channel('quic.session.closing');
|
||||
const onSessionClosedChannel = dc.channel('quic.session.closed');
|
||||
const onSessionReceiveDatagramChannel = dc.channel('quic.session.receive.datagram');
|
||||
const onSessionReceiveDatagramStatusChannel = dc.channel('quic.session.receive.datagram.status');
|
||||
const onSessionPathValidationChannel = dc.channel('quic.session.path.validation');
|
||||
const onSessionNewTokenChannel = dc.channel('quic.session.new.token');
|
||||
const onSessionTicketChannel = dc.channel('quic.session.ticket');
|
||||
const onSessionVersionNegotiationChannel = dc.channel('quic.session.version.negotiation');
|
||||
const onSessionOriginChannel = dc.channel('quic.session.receive.origin');
|
||||
const onSessionHandshakeChannel = dc.channel('quic.session.handshake');
|
||||
const onSessionGoawayChannel = dc.channel('quic.session.goaway');
|
||||
const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected');
|
||||
const onStreamClosedChannel = dc.channel('quic.stream.closed');
|
||||
const onStreamHeadersChannel = dc.channel('quic.stream.headers');
|
||||
const onStreamTrailersChannel = dc.channel('quic.stream.trailers');
|
||||
const onStreamInfoChannel = dc.channel('quic.stream.info');
|
||||
const onStreamResetChannel = dc.channel('quic.stream.reset');
|
||||
const onStreamBlockedChannel = dc.channel('quic.stream.blocked');
|
||||
const onSessionErrorChannel = dc.channel('quic.session.error');
|
||||
const onEndpointConnectChannel = dc.channel('quic.endpoint.connect');
|
||||
|
||||
module.exports = {
|
||||
onEndpointCreatedChannel,
|
||||
onEndpointListeningChannel,
|
||||
onEndpointClosingChannel,
|
||||
onEndpointClosedChannel,
|
||||
onEndpointErrorChannel,
|
||||
onEndpointBusyChangeChannel,
|
||||
onEndpointClientSessionChannel,
|
||||
onEndpointServerSessionChannel,
|
||||
onSessionOpenStreamChannel,
|
||||
onSessionReceivedStreamChannel,
|
||||
onSessionSendDatagramChannel,
|
||||
onSessionUpdateKeyChannel,
|
||||
onSessionClosingChannel,
|
||||
onSessionClosedChannel,
|
||||
onSessionReceiveDatagramChannel,
|
||||
onSessionReceiveDatagramStatusChannel,
|
||||
onSessionPathValidationChannel,
|
||||
onSessionNewTokenChannel,
|
||||
onSessionTicketChannel,
|
||||
onSessionVersionNegotiationChannel,
|
||||
onSessionOriginChannel,
|
||||
onSessionHandshakeChannel,
|
||||
onSessionGoawayChannel,
|
||||
onSessionEarlyRejectedChannel,
|
||||
onStreamClosedChannel,
|
||||
onStreamHeadersChannel,
|
||||
onStreamTrailersChannel,
|
||||
onStreamInfoChannel,
|
||||
onStreamResetChannel,
|
||||
onStreamBlockedChannel,
|
||||
onSessionErrorChannel,
|
||||
onEndpointConnectChannel,
|
||||
};
|
||||
+2967
-402
File diff suppressed because it is too large.
Load diff
+282
-88
@@ -5,11 +5,29 @@ const {
|
||||
DataView,
|
||||
DataViewPrototypeGetBigInt64,
|
||||
DataViewPrototypeGetBigUint64,
|
||||
DataViewPrototypeGetByteLength,
|
||||
DataViewPrototypeGetUint16,
|
||||
DataViewPrototypeGetUint32,
|
||||
DataViewPrototypeGetUint8,
|
||||
DataViewPrototypeSetUint16,
|
||||
DataViewPrototypeSetUint32,
|
||||
DataViewPrototypeSetUint8,
|
||||
Float32Array,
|
||||
JSONStringify,
|
||||
Uint8Array,
|
||||
} = primordials;
|
||||
|
||||
// Determine native byte order. The shared state buffer is written by
|
||||
// C++ in native byte order, so DataView reads must match.
|
||||
const kIsLittleEndian = (() => {
|
||||
// -1 as float32 is 0xBF800000. On little-endian, the bytes are
|
||||
// [0x00, 0x00, 0x80, 0xBF], so byte[3] is 0xBF (non-zero).
|
||||
// On big-endian, the bytes are [0xBF, 0x80, 0x00, 0x00], so byte[3] is 0.
|
||||
const buf = new Float32Array(1);
|
||||
buf[0] = -1;
|
||||
return new Uint8Array(buf.buffer)[3] !== 0;
|
||||
})();
|
||||
|
||||
const {
|
||||
getOptionValue,
|
||||
} = require('internal/options');
|
||||
@@ -49,10 +67,7 @@ const {
|
||||
// prevent further updates to the buffer.
|
||||
|
||||
const {
|
||||
IDX_STATE_SESSION_PATH_VALIDATION,
|
||||
IDX_STATE_SESSION_VERSION_NEGOTIATION,
|
||||
IDX_STATE_SESSION_DATAGRAM,
|
||||
IDX_STATE_SESSION_SESSION_TICKET,
|
||||
IDX_STATE_SESSION_LISTENER_FLAGS,
|
||||
IDX_STATE_SESSION_CLOSING,
|
||||
IDX_STATE_SESSION_GRACEFUL_CLOSE,
|
||||
IDX_STATE_SESSION_SILENT_CLOSE,
|
||||
@@ -61,15 +76,22 @@ const {
|
||||
IDX_STATE_SESSION_HANDSHAKE_CONFIRMED,
|
||||
IDX_STATE_SESSION_STREAM_OPEN_ALLOWED,
|
||||
IDX_STATE_SESSION_PRIORITY_SUPPORTED,
|
||||
IDX_STATE_SESSION_HEADERS_SUPPORTED,
|
||||
IDX_STATE_SESSION_WRAPPED,
|
||||
IDX_STATE_SESSION_APPLICATION_TYPE,
|
||||
IDX_STATE_SESSION_NO_ERROR_CODE,
|
||||
IDX_STATE_SESSION_INTERNAL_ERROR_CODE,
|
||||
IDX_STATE_SESSION_MAX_DATAGRAM_SIZE,
|
||||
IDX_STATE_SESSION_LAST_DATAGRAM_ID,
|
||||
IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS,
|
||||
|
||||
IDX_STATE_ENDPOINT_BOUND,
|
||||
IDX_STATE_ENDPOINT_RECEIVING,
|
||||
IDX_STATE_ENDPOINT_LISTENING,
|
||||
IDX_STATE_ENDPOINT_CLOSING,
|
||||
IDX_STATE_ENDPOINT_BUSY,
|
||||
IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST,
|
||||
IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL,
|
||||
IDX_STATE_ENDPOINT_PENDING_CALLBACKS,
|
||||
|
||||
IDX_STATE_STREAM_ID,
|
||||
@@ -85,12 +107,13 @@ const {
|
||||
IDX_STATE_STREAM_WANTS_HEADERS,
|
||||
IDX_STATE_STREAM_WANTS_RESET,
|
||||
IDX_STATE_STREAM_WANTS_TRAILERS,
|
||||
IDX_STATE_STREAM_RECEIVED_EARLY_DATA,
|
||||
IDX_STATE_STREAM_WRITE_DESIRED_SIZE,
|
||||
IDX_STATE_STREAM_HIGH_WATER_MARK,
|
||||
IDX_STATE_STREAM_RESET_CODE,
|
||||
} = internalBinding('quic');
|
||||
|
||||
assert(IDX_STATE_SESSION_PATH_VALIDATION !== undefined);
|
||||
assert(IDX_STATE_SESSION_VERSION_NEGOTIATION !== undefined);
|
||||
assert(IDX_STATE_SESSION_DATAGRAM !== undefined);
|
||||
assert(IDX_STATE_SESSION_SESSION_TICKET !== undefined);
|
||||
assert(IDX_STATE_SESSION_LISTENER_FLAGS !== undefined);
|
||||
assert(IDX_STATE_SESSION_CLOSING !== undefined);
|
||||
assert(IDX_STATE_SESSION_GRACEFUL_CLOSE !== undefined);
|
||||
assert(IDX_STATE_SESSION_SILENT_CLOSE !== undefined);
|
||||
@@ -99,8 +122,12 @@ assert(IDX_STATE_SESSION_HANDSHAKE_COMPLETED !== undefined);
|
||||
assert(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED !== undefined);
|
||||
assert(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED !== undefined);
|
||||
assert(IDX_STATE_SESSION_PRIORITY_SUPPORTED !== undefined);
|
||||
assert(IDX_STATE_SESSION_HEADERS_SUPPORTED !== undefined);
|
||||
assert(IDX_STATE_SESSION_WRAPPED !== undefined);
|
||||
assert(IDX_STATE_SESSION_APPLICATION_TYPE !== undefined);
|
||||
assert(IDX_STATE_SESSION_NO_ERROR_CODE !== undefined);
|
||||
assert(IDX_STATE_SESSION_INTERNAL_ERROR_CODE !== undefined);
|
||||
assert(IDX_STATE_SESSION_MAX_DATAGRAM_SIZE !== undefined);
|
||||
assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined);
|
||||
assert(IDX_STATE_ENDPOINT_BOUND !== undefined);
|
||||
assert(IDX_STATE_ENDPOINT_RECEIVING !== undefined);
|
||||
@@ -121,6 +148,8 @@ assert(IDX_STATE_STREAM_WANTS_BLOCK !== undefined);
|
||||
assert(IDX_STATE_STREAM_WANTS_HEADERS !== undefined);
|
||||
assert(IDX_STATE_STREAM_WANTS_RESET !== undefined);
|
||||
assert(IDX_STATE_STREAM_WANTS_TRAILERS !== undefined);
|
||||
assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined);
|
||||
assert(IDX_STATE_STREAM_RESET_CODE !== undefined);
|
||||
|
||||
class QuicEndpointState {
|
||||
/** @type {DataView} */
|
||||
@@ -142,43 +171,69 @@ class QuicEndpointState {
|
||||
|
||||
/** @type {boolean} */
|
||||
get isBound() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BOUND);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isReceiving() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_RECEIVING);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isListening() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_LISTENING);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isClosing() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_CLOSING);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isBusy() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BUSY);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get maxConnectionsPerHost() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint16(
|
||||
this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, kIsLittleEndian);
|
||||
}
|
||||
|
||||
set maxConnectionsPerHost(val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint16(
|
||||
this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_PER_HOST, val, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get maxConnectionsTotal() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint16(
|
||||
this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, kIsLittleEndian);
|
||||
}
|
||||
|
||||
set maxConnectionsTotal(val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint16(
|
||||
this.#handle, IDX_STATE_ENDPOINT_MAX_CONNECTIONS_TOTAL, val, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of underlying callbacks that are pending. If the session
|
||||
* is closing, these are the number of callbacks that the session is
|
||||
* The number of underlying callbacks that are pending. If the endpoint
|
||||
* is closing, these are the number of callbacks that the endpoint is
|
||||
* waiting on before it can be closed.
|
||||
* @type {bigint}
|
||||
*/
|
||||
get pendingCallbacks() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS);
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS, kIsLittleEndian);
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -186,7 +241,7 @@ class QuicEndpointState {
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
if (this.#handle.byteLength === 0) return {};
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {};
|
||||
return {
|
||||
__proto__: null,
|
||||
isBound: this.isBound,
|
||||
@@ -194,6 +249,8 @@ class QuicEndpointState {
|
||||
isListening: this.isListening,
|
||||
isClosing: this.isClosing,
|
||||
isBusy: this.isBusy,
|
||||
maxConnectionsPerHost: this.maxConnectionsPerHost,
|
||||
maxConnectionsTotal: this.maxConnectionsTotal,
|
||||
pendingCallbacks: `${this.pendingCallbacks}`,
|
||||
};
|
||||
}
|
||||
@@ -202,11 +259,12 @@ class QuicEndpointState {
|
||||
if (depth < 0)
|
||||
return this;
|
||||
|
||||
if (this.#handle.byteLength === 0) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) {
|
||||
return 'QuicEndpointState { <Closed> }';
|
||||
}
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
@@ -224,7 +282,7 @@ class QuicEndpointState {
|
||||
[kFinishClose]() {
|
||||
// Snapshot the state into a new DataView since the underlying
|
||||
// buffer will be destroyed.
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
this.#handle = new DataView(new ArrayBuffer(0));
|
||||
}
|
||||
}
|
||||
@@ -247,118 +305,199 @@ class QuicSessionState {
|
||||
this.#handle = new DataView(buffer);
|
||||
}
|
||||
|
||||
// Listener flags are packed into a single uint32_t bitfield. The bit
|
||||
// positions must match the SessionListenerFlags enum in session.cc.
|
||||
static #LISTENER_PATH_VALIDATION = 1 << 0;
|
||||
static #LISTENER_DATAGRAM = 1 << 1;
|
||||
static #LISTENER_DATAGRAM_STATUS = 1 << 2;
|
||||
static #LISTENER_SESSION_TICKET = 1 << 3;
|
||||
static #LISTENER_NEW_TOKEN = 1 << 4;
|
||||
static #LISTENER_ORIGIN = 1 << 5;
|
||||
|
||||
#getListenerFlag(flag) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!(DataViewPrototypeGetUint32(
|
||||
this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian) & flag);
|
||||
}
|
||||
|
||||
#setListenerFlag(flag, val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
const current = DataViewPrototypeGetUint32(
|
||||
this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS, kIsLittleEndian);
|
||||
DataViewPrototypeSetUint32(
|
||||
this.#handle, IDX_STATE_SESSION_LISTENER_FLAGS,
|
||||
val ? (current | flag) : (current & ~flag), kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasPathValidationListener() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION);
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_PATH_VALIDATION);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set hasPathValidationListener(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasVersionNegotiationListener() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set hasVersionNegotiationListener(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0);
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_PATH_VALIDATION, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasDatagramListener() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM);
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_DATAGRAM);
|
||||
}
|
||||
set hasDatagramListener(val) {
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_DATAGRAM, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set hasDatagramListener(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0);
|
||||
get hasDatagramStatusListener() {
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_DATAGRAM_STATUS);
|
||||
}
|
||||
set hasDatagramStatusListener(val) {
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_DATAGRAM_STATUS, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasSessionTicketListener() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET);
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_SESSION_TICKET);
|
||||
}
|
||||
set hasSessionTicketListener(val) {
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_SESSION_TICKET, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set hasSessionTicketListener(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0);
|
||||
get hasNewTokenListener() {
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_NEW_TOKEN);
|
||||
}
|
||||
set hasNewTokenListener(val) {
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_NEW_TOKEN, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasOriginListener() {
|
||||
return this.#getListenerFlag(QuicSessionState.#LISTENER_ORIGIN);
|
||||
}
|
||||
set hasOriginListener(val) {
|
||||
this.#setListenerFlag(QuicSessionState.#LISTENER_ORIGIN, val);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isClosing() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_CLOSING);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isGracefulClose() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_GRACEFUL_CLOSE);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isSilentClose() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SILENT_CLOSE);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isStatelessReset() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isHandshakeCompleted() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_COMPLETED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isHandshakeConfirmed() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_CONFIRMED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isStreamOpenAllowed() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isPrioritySupported() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PRIORITY_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the negotiated application protocol supports headers.
|
||||
* Returns 0 (unknown), 1 (supported), or 2 (not supported).
|
||||
* @type {number}
|
||||
*/
|
||||
get headersSupported() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HEADERS_SUPPORTED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get isWrapped() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get applicationType() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_APPLICATION_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The negotiated application protocol's "no error" code, populated
|
||||
* by the C++ layer when the application is selected during ALPN
|
||||
* negotiation. For raw QUIC this is `0n`; for HTTP/3 this is
|
||||
* `0x100n` (`H3_NO_ERROR`).
|
||||
* @type {bigint}
|
||||
*/
|
||||
get noErrorCode() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(
|
||||
this.#handle, IDX_STATE_SESSION_NO_ERROR_CODE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/**
|
||||
* The negotiated application protocol's "internal error" code,
|
||||
* populated by the C++ layer when the application is selected
|
||||
* during ALPN negotiation. Used as the wire code for `RESET_STREAM`
|
||||
* frames when a stream is aborted without a more specific code.
|
||||
* For raw QUIC this is `0x1n` (NGTCP2_INTERNAL_ERROR); for HTTP/3
|
||||
* this is `0x102n` (`H3_INTERNAL_ERROR`).
|
||||
* @type {bigint}
|
||||
*/
|
||||
get internalErrorCode() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(
|
||||
this.#handle, IDX_STATE_SESSION_INTERNAL_ERROR_CODE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get maxDatagramSize() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint16(this.#handle, IDX_STATE_SESSION_MAX_DATAGRAM_SIZE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get lastDatagramId() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID);
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get maxPendingDatagrams() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint16(
|
||||
this.#handle, IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, kIsLittleEndian);
|
||||
}
|
||||
|
||||
set maxPendingDatagrams(val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint16(
|
||||
this.#handle, IDX_STATE_SESSION_MAX_PENDING_DATAGRAMS, val, kIsLittleEndian);
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -366,24 +505,31 @@ class QuicSessionState {
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
if (this.#handle.byteLength === 0) return {};
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {};
|
||||
return {
|
||||
__proto__: null,
|
||||
hasPathValidationListener: this.hasPathValidationListener,
|
||||
hasVersionNegotiationListener: this.hasVersionNegotiationListener,
|
||||
hasDatagramListener: this.hasDatagramListener,
|
||||
hasDatagramStatusListener: this.hasDatagramStatusListener,
|
||||
hasSessionTicketListener: this.hasSessionTicketListener,
|
||||
hasNewTokenListener: this.hasNewTokenListener,
|
||||
hasOriginListener: this.hasOriginListener,
|
||||
isClosing: this.isClosing,
|
||||
isGracefulClose: this.isGracefulClose,
|
||||
isSilentClose: this.isSilentClose,
|
||||
isStatelessReset: this.isStatelessReset,
|
||||
isDestroyed: this.isDestroyed,
|
||||
isHandshakeCompleted: this.isHandshakeCompleted,
|
||||
isHandshakeConfirmed: this.isHandshakeConfirmed,
|
||||
isStreamOpenAllowed: this.isStreamOpenAllowed,
|
||||
isPrioritySupported: this.isPrioritySupported,
|
||||
headersSupported: this.headersSupported,
|
||||
isWrapped: this.isWrapped,
|
||||
applicationType: this.applicationType,
|
||||
noErrorCode: `${this.noErrorCode}`,
|
||||
internalErrorCode: `${this.internalErrorCode}`,
|
||||
maxDatagramSize: `${this.maxDatagramSize}`,
|
||||
lastDatagramId: `${this.lastDatagramId}`,
|
||||
maxPendingDatagrams: this.maxPendingDatagrams,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,31 +537,37 @@ class QuicSessionState {
|
||||
if (depth < 0)
|
||||
return this;
|
||||
|
||||
if (this.#handle.byteLength === 0) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) {
|
||||
return 'QuicSessionState { <Closed> }';
|
||||
}
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
|
||||
return `QuicSessionState ${inspect({
|
||||
hasPathValidationListener: this.hasPathValidationListener,
|
||||
hasVersionNegotiationListener: this.hasVersionNegotiationListener,
|
||||
hasDatagramListener: this.hasDatagramListener,
|
||||
hasDatagramStatusListener: this.hasDatagramStatusListener,
|
||||
hasSessionTicketListener: this.hasSessionTicketListener,
|
||||
hasNewTokenListener: this.hasNewTokenListener,
|
||||
hasOriginListener: this.hasOriginListener,
|
||||
isClosing: this.isClosing,
|
||||
isGracefulClose: this.isGracefulClose,
|
||||
isSilentClose: this.isSilentClose,
|
||||
isStatelessReset: this.isStatelessReset,
|
||||
isDestroyed: this.isDestroyed,
|
||||
isHandshakeCompleted: this.isHandshakeCompleted,
|
||||
isHandshakeConfirmed: this.isHandshakeConfirmed,
|
||||
isStreamOpenAllowed: this.isStreamOpenAllowed,
|
||||
isPrioritySupported: this.isPrioritySupported,
|
||||
headersSupported: this.headersSupported,
|
||||
isWrapped: this.isWrapped,
|
||||
applicationType: this.applicationType,
|
||||
noErrorCode: this.noErrorCode,
|
||||
internalErrorCode: this.internalErrorCode,
|
||||
maxDatagramSize: this.maxDatagramSize,
|
||||
lastDatagramId: this.lastDatagramId,
|
||||
}, opts)}`;
|
||||
}
|
||||
@@ -423,7 +575,7 @@ class QuicSessionState {
|
||||
[kFinishClose]() {
|
||||
// Snapshot the state into a new DataView since the underlying
|
||||
// buffer will be destroyed.
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
this.#handle = new DataView(new ArrayBuffer(0));
|
||||
}
|
||||
}
|
||||
@@ -448,113 +600,152 @@ class QuicStreamState {
|
||||
|
||||
/** @type {bigint} */
|
||||
get id() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID);
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get pending() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PENDING);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get finSent() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_SENT);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get finReceived() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_RECEIVED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get readEnded() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_READ_ENDED);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get writeEnded() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WRITE_ENDED);
|
||||
}
|
||||
|
||||
|
||||
/** @type {boolean} */
|
||||
get reset() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RESET);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasOutbound() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_OUTBOUND);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get hasReader() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_READER);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get wantsBlock() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set wantsBlock(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get [kWantsHeaders]() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set [kWantsHeaders](val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get wantsReset() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set wantsReset(val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get [kWantsTrailers]() {
|
||||
if (this.#handle.byteLength === 0) return undefined;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
set [kWantsTrailers](val) {
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0);
|
||||
}
|
||||
|
||||
/** @type {boolean} */
|
||||
get early() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RECEIVED_EARLY_DATA);
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get resetCode() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetBigUint64(
|
||||
this.#handle, IDX_STATE_STREAM_RESET_CODE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get writeDesiredSize() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint32(
|
||||
this.#handle, IDX_STATE_STREAM_WRITE_DESIRED_SIZE, kIsLittleEndian);
|
||||
}
|
||||
|
||||
set writeDesiredSize(val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint32(
|
||||
this.#handle, IDX_STATE_STREAM_WRITE_DESIRED_SIZE, val, kIsLittleEndian);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get highWaterMark() {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return undefined;
|
||||
return DataViewPrototypeGetUint32(
|
||||
this.#handle, IDX_STATE_STREAM_HIGH_WATER_MARK, kIsLittleEndian);
|
||||
}
|
||||
|
||||
set highWaterMark(val) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
DataViewPrototypeSetUint32(
|
||||
this.#handle, IDX_STATE_STREAM_HIGH_WATER_MARK, val, kIsLittleEndian);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSONStringify(this.toJSON());
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
if (this.#handle.byteLength === 0) return {};
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return {};
|
||||
return {
|
||||
__proto__: null,
|
||||
id: `${this.id}`,
|
||||
@@ -568,6 +759,7 @@ class QuicStreamState {
|
||||
hasReader: this.hasReader,
|
||||
wantsBlock: this.wantsBlock,
|
||||
wantsReset: this.wantsReset,
|
||||
early: this.early,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -575,11 +767,12 @@ class QuicStreamState {
|
||||
if (depth < 0)
|
||||
return this;
|
||||
|
||||
if (this.#handle.byteLength === 0) {
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) {
|
||||
return 'QuicStreamState { <Closed> }';
|
||||
}
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
@@ -596,13 +789,14 @@ class QuicStreamState {
|
||||
hasReader: this.hasReader,
|
||||
wantsBlock: this.wantsBlock,
|
||||
wantsReset: this.wantsReset,
|
||||
early: this.early,
|
||||
}, opts)}`;
|
||||
}
|
||||
|
||||
[kFinishClose]() {
|
||||
// Snapshot the state into a new DataView since the underlying
|
||||
// buffer will be destroyed.
|
||||
if (this.#handle.byteLength === 0) return;
|
||||
if (DataViewPrototypeGetByteLength(this.#handle) === 0) return;
|
||||
this.#handle = new DataView(new ArrayBuffer(0));
|
||||
}
|
||||
}
|
||||
|
||||
+83
-21
@@ -58,11 +58,11 @@ const {
|
||||
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
|
||||
|
||||
IDX_STATS_SESSION_CREATED_AT,
|
||||
IDX_STATS_SESSION_DESTROYED_AT,
|
||||
IDX_STATS_SESSION_CLOSING_AT,
|
||||
IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
|
||||
IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
|
||||
IDX_STATS_SESSION_BYTES_RECEIVED,
|
||||
IDX_STATS_SESSION_BYTES_SENT,
|
||||
IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT,
|
||||
IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT,
|
||||
IDX_STATS_SESSION_UNI_IN_STREAM_COUNT,
|
||||
@@ -76,6 +76,15 @@ const {
|
||||
IDX_STATS_SESSION_RTTVAR,
|
||||
IDX_STATS_SESSION_SMOOTHED_RTT,
|
||||
IDX_STATS_SESSION_SSTHRESH,
|
||||
IDX_STATS_SESSION_PKT_SENT,
|
||||
IDX_STATS_SESSION_BYTES_SENT,
|
||||
IDX_STATS_SESSION_PKT_RECV,
|
||||
IDX_STATS_SESSION_BYTES_RECV,
|
||||
IDX_STATS_SESSION_PKT_LOST,
|
||||
IDX_STATS_SESSION_BYTES_LOST,
|
||||
IDX_STATS_SESSION_PING_RECV,
|
||||
IDX_STATS_SESSION_PKT_DISCARDED,
|
||||
|
||||
IDX_STATS_SESSION_DATAGRAMS_RECEIVED,
|
||||
IDX_STATS_SESSION_DATAGRAMS_SENT,
|
||||
IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED,
|
||||
@@ -108,11 +117,11 @@ assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT !== undefined);
|
||||
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
|
||||
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined);
|
||||
assert(IDX_STATS_SESSION_CREATED_AT !== undefined);
|
||||
assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined);
|
||||
assert(IDX_STATS_SESSION_CLOSING_AT !== undefined);
|
||||
assert(IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT !== undefined);
|
||||
assert(IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT !== undefined);
|
||||
assert(IDX_STATS_SESSION_BYTES_RECEIVED !== undefined);
|
||||
assert(IDX_STATS_SESSION_BYTES_SENT !== undefined);
|
||||
assert(IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT !== undefined);
|
||||
assert(IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT !== undefined);
|
||||
assert(IDX_STATS_SESSION_UNI_IN_STREAM_COUNT !== undefined);
|
||||
@@ -126,6 +135,14 @@ assert(IDX_STATS_SESSION_MIN_RTT !== undefined);
|
||||
assert(IDX_STATS_SESSION_RTTVAR !== undefined);
|
||||
assert(IDX_STATS_SESSION_SMOOTHED_RTT !== undefined);
|
||||
assert(IDX_STATS_SESSION_SSTHRESH !== undefined);
|
||||
assert(IDX_STATS_SESSION_PKT_SENT !== undefined);
|
||||
assert(IDX_STATS_SESSION_BYTES_SENT !== undefined);
|
||||
assert(IDX_STATS_SESSION_PKT_RECV !== undefined);
|
||||
assert(IDX_STATS_SESSION_BYTES_RECV !== undefined);
|
||||
assert(IDX_STATS_SESSION_PKT_LOST !== undefined);
|
||||
assert(IDX_STATS_SESSION_BYTES_LOST !== undefined);
|
||||
assert(IDX_STATS_SESSION_PING_RECV !== undefined);
|
||||
assert(IDX_STATS_SESSION_PKT_DISCARDED !== undefined);
|
||||
assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined);
|
||||
assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined);
|
||||
assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined);
|
||||
@@ -260,6 +277,7 @@ class QuicEndpointStats {
|
||||
return this;
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
@@ -286,7 +304,7 @@ class QuicEndpointStats {
|
||||
* True if this QuicEndpointStats object is still connected to the underlying
|
||||
* Endpoint stats source. If this returns false, then the stats object is
|
||||
* no longer being updated and should be considered stale.
|
||||
* @returns {boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isConnected() {
|
||||
return !this.#disconnected;
|
||||
@@ -308,7 +326,7 @@ class QuicSessionStats {
|
||||
|
||||
/**
|
||||
* @param {symbol} privateSymbol
|
||||
* @param {BigUint64Array} buffer
|
||||
* @param {ArrayBuffer} buffer
|
||||
*/
|
||||
constructor(privateSymbol, buffer) {
|
||||
// We use the kPrivateConstructor symbol to restrict the ability to
|
||||
@@ -327,6 +345,11 @@ class QuicSessionStats {
|
||||
return this.#handle[IDX_STATS_SESSION_CREATED_AT];
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get destroyedAt() {
|
||||
return this.#handle[IDX_STATS_SESSION_DESTROYED_AT];
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get closingAt() {
|
||||
return this.#handle[IDX_STATS_SESSION_CLOSING_AT];
|
||||
@@ -347,11 +370,6 @@ class QuicSessionStats {
|
||||
return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED];
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get bytesSent() {
|
||||
return this.#handle[IDX_STATS_SESSION_BYTES_SENT];
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get bidiInStreamCount() {
|
||||
return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
|
||||
@@ -373,7 +391,7 @@ class QuicSessionStats {
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get maxBytesInFlights() {
|
||||
get maxBytesInFlight() {
|
||||
return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
|
||||
}
|
||||
|
||||
@@ -417,6 +435,38 @@ class QuicSessionStats {
|
||||
return this.#handle[IDX_STATS_SESSION_SSTHRESH];
|
||||
}
|
||||
|
||||
get pktSent() {
|
||||
return this.#handle[IDX_STATS_SESSION_PKT_SENT];
|
||||
}
|
||||
|
||||
get bytesSent() {
|
||||
return this.#handle[IDX_STATS_SESSION_BYTES_SENT];
|
||||
}
|
||||
|
||||
get pktRecv() {
|
||||
return this.#handle[IDX_STATS_SESSION_PKT_RECV];
|
||||
}
|
||||
|
||||
get bytesRecv() {
|
||||
return this.#handle[IDX_STATS_SESSION_BYTES_RECV];
|
||||
}
|
||||
|
||||
get pktLost() {
|
||||
return this.#handle[IDX_STATS_SESSION_PKT_LOST];
|
||||
}
|
||||
|
||||
get bytesLost() {
|
||||
return this.#handle[IDX_STATS_SESSION_BYTES_LOST];
|
||||
}
|
||||
|
||||
get pingRecv() {
|
||||
return this.#handle[IDX_STATS_SESSION_PING_RECV];
|
||||
}
|
||||
|
||||
get pktDiscarded() {
|
||||
return this.#handle[IDX_STATS_SESSION_PKT_DISCARDED];
|
||||
}
|
||||
|
||||
/** @type {bigint} */
|
||||
get datagramsReceived() {
|
||||
return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
|
||||
@@ -449,17 +499,14 @@ class QuicSessionStats {
|
||||
// support BigInts.
|
||||
createdAt: `${this.createdAt}`,
|
||||
closingAt: `${this.closingAt}`,
|
||||
destroyedAt: `${this.destroyedAt}`,
|
||||
handshakeCompletedAt: `${this.handshakeCompletedAt}`,
|
||||
handshakeConfirmedAt: `${this.handshakeConfirmedAt}`,
|
||||
gracefulClosingAt: `${this.gracefulClosingAt}`,
|
||||
bytesReceived: `${this.bytesReceived}`,
|
||||
bytesSent: `${this.bytesSent}`,
|
||||
bidiInStreamCount: `${this.bidiInStreamCount}`,
|
||||
bidiOutStreamCount: `${this.bidiOutStreamCount}`,
|
||||
uniInStreamCount: `${this.uniInStreamCount}`,
|
||||
uniOutStreamCount: `${this.uniOutStreamCount}`,
|
||||
maxBytesInFlights: `${this.maxBytesInFlights}`,
|
||||
maxBytesInFlight: `${this.maxBytesInFlight}`,
|
||||
bytesInFlight: `${this.bytesInFlight}`,
|
||||
blockCount: `${this.blockCount}`,
|
||||
cwnd: `${this.cwnd}`,
|
||||
@@ -468,6 +515,14 @@ class QuicSessionStats {
|
||||
rttVar: `${this.rttVar}`,
|
||||
smoothedRtt: `${this.smoothedRtt}`,
|
||||
ssthresh: `${this.ssthresh}`,
|
||||
pktSent: `${this.pktSent}`,
|
||||
bytesSent: `${this.bytesSent}`,
|
||||
pktRecv: `${this.pktRecv}`,
|
||||
bytesRecv: `${this.bytesRecv}`,
|
||||
pktLost: `${this.pktLost}`,
|
||||
bytesLost: `${this.bytesLost}`,
|
||||
pingRecv: `${this.pingRecv}`,
|
||||
pktDiscarded: `${this.pktDiscarded}`,
|
||||
datagramsReceived: `${this.datagramsReceived}`,
|
||||
datagramsSent: `${this.datagramsSent}`,
|
||||
datagramsAcknowledged: `${this.datagramsAcknowledged}`,
|
||||
@@ -480,6 +535,7 @@ class QuicSessionStats {
|
||||
return this;
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
@@ -488,17 +544,14 @@ class QuicSessionStats {
|
||||
connected: this.isConnected,
|
||||
createdAt: this.createdAt,
|
||||
closingAt: this.closingAt,
|
||||
destroyedAt: this.destroyedAt,
|
||||
handshakeCompletedAt: this.handshakeCompletedAt,
|
||||
handshakeConfirmedAt: this.handshakeConfirmedAt,
|
||||
gracefulClosingAt: this.gracefulClosingAt,
|
||||
bytesReceived: this.bytesReceived,
|
||||
bytesSent: this.bytesSent,
|
||||
bidiInStreamCount: this.bidiInStreamCount,
|
||||
bidiOutStreamCount: this.bidiOutStreamCount,
|
||||
uniInStreamCount: this.uniInStreamCount,
|
||||
uniOutStreamCount: this.uniOutStreamCount,
|
||||
maxBytesInFlights: this.maxBytesInFlights,
|
||||
maxBytesInFlight: this.maxBytesInFlight,
|
||||
bytesInFlight: this.bytesInFlight,
|
||||
blockCount: this.blockCount,
|
||||
cwnd: this.cwnd,
|
||||
@@ -507,6 +560,14 @@ class QuicSessionStats {
|
||||
rttVar: this.rttVar,
|
||||
smoothedRtt: this.smoothedRtt,
|
||||
ssthresh: this.ssthresh,
|
||||
pktSent: this.pktSent,
|
||||
bytesSent: this.bytesSent,
|
||||
pktRecv: this.pktRecv,
|
||||
bytesRecv: this.bytesRecv,
|
||||
pktLost: this.pktLost,
|
||||
bytesLost: this.bytesLost,
|
||||
pingRecv: this.pingRecv,
|
||||
pktDiscarded: this.pktDiscarded,
|
||||
datagramsReceived: this.datagramsReceived,
|
||||
datagramsSent: this.datagramsSent,
|
||||
datagramsAcknowledged: this.datagramsAcknowledged,
|
||||
@@ -518,7 +579,7 @@ class QuicSessionStats {
|
||||
* True if this QuicSessionStats object is still connected to the underlying
|
||||
* Session stats source. If this returns false, then the stats object is
|
||||
* no longer being updated and should be considered stale.
|
||||
* @returns {boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isConnected() {
|
||||
return !this.#disconnected;
|
||||
@@ -638,11 +699,12 @@ class QuicStreamStats {
|
||||
return this;
|
||||
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
...options,
|
||||
depth: options.depth == null ? null : options.depth - 1,
|
||||
};
|
||||
|
||||
return `StreamStats ${inspect({
|
||||
return `QuicStreamStats ${inspect({
|
||||
connected: this.isConnected,
|
||||
createdAt: this.createdAt,
|
||||
openedAt: this.openedAt,
|
||||
@@ -662,7 +724,7 @@ class QuicStreamStats {
|
||||
* True if this QuicStreamStats object is still connected to the underlying
|
||||
* Stream stats source. If this returns false, then the stats object is
|
||||
* no longer being updated and should be considered stale.
|
||||
* @returns {boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isConnected() {
|
||||
return !this.#disconnected;
|
||||
|
||||
@@ -23,18 +23,26 @@ const {
|
||||
// Symbols used to hide various private properties and methods from the
|
||||
// public API.
|
||||
|
||||
const kAttachFileHandle = Symbol('kAttachFileHandle');
|
||||
const kBlocked = Symbol('kBlocked');
|
||||
const kConnect = Symbol('kConnect');
|
||||
const kDrain = Symbol('kDrain');
|
||||
const kDatagram = Symbol('kDatagram');
|
||||
const kDatagramStatus = Symbol('kDatagramStatus');
|
||||
const kEarlyDataRejected = Symbol('kEarlyDataRejected');
|
||||
const kFinishClose = Symbol('kFinishClose');
|
||||
const kGoaway = Symbol('kGoaway');
|
||||
const kHandshake = Symbol('kHandshake');
|
||||
const kHandshakeCompleted = Symbol('kHandshakeCompleted');
|
||||
const kHeaders = Symbol('kHeaders');
|
||||
const kKeylog = Symbol('kKeylog');
|
||||
const kListen = Symbol('kListen');
|
||||
const kQlog = Symbol('kQlog');
|
||||
const kNewSession = Symbol('kNewSession');
|
||||
const kNewStream = Symbol('kNewStream');
|
||||
const kOnHeaders = Symbol('kOnHeaders');
|
||||
const kOnTrailers = Symbol('kOwnTrailers');
|
||||
const kNewToken = Symbol('kNewToken');
|
||||
const kStreamCallbacks = Symbol('kStreamCallbacks');
|
||||
const kOrigin = Symbol('kOrigin');
|
||||
const kOwner = Symbol('kOwner');
|
||||
const kPathValidation = Symbol('kPathValidation');
|
||||
const kPrivateConstructor = Symbol('kPrivateConstructor');
|
||||
@@ -49,21 +57,29 @@ const kWantsHeaders = Symbol('kWantsHeaders');
|
||||
const kWantsTrailers = Symbol('kWantsTrailers');
|
||||
|
||||
module.exports = {
|
||||
kAttachFileHandle,
|
||||
kBlocked,
|
||||
kConnect,
|
||||
kDatagram,
|
||||
kDatagramStatus,
|
||||
kDrain,
|
||||
kEarlyDataRejected,
|
||||
kFinishClose,
|
||||
kGoaway,
|
||||
kHandshake,
|
||||
kHandshakeCompleted,
|
||||
kHeaders,
|
||||
kInspect,
|
||||
kKeylog,
|
||||
kKeyObjectHandle,
|
||||
kListen,
|
||||
kNewSession,
|
||||
kNewStream,
|
||||
kOnHeaders,
|
||||
kOnTrailers,
|
||||
kNewToken,
|
||||
kStreamCallbacks,
|
||||
kOrigin,
|
||||
kOwner,
|
||||
kQlog,
|
||||
kPathValidation,
|
||||
kPrivateConstructor,
|
||||
kRemoveSession,
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
connect,
|
||||
listen,
|
||||
QuicEndpoint,
|
||||
QuicError,
|
||||
QuicSession,
|
||||
QuicStream,
|
||||
CC_ALGO_RENO,
|
||||
@@ -52,6 +53,7 @@ module.exports = ObjectSeal(ObjectCreate(null, {
|
||||
connect: getEnumerableConstant(connect),
|
||||
listen: getEnumerableConstant(listen),
|
||||
QuicEndpoint: getEnumerableConstant(QuicEndpoint),
|
||||
QuicError: getEnumerableConstant(QuicError),
|
||||
QuicSession: getEnumerableConstant(QuicSession),
|
||||
QuicStream: getEnumerableConstant(QuicStream),
|
||||
constants: getEnumerableConstant(constants),
|
||||
|
||||
@@ -348,7 +348,6 @@
|
||||
'src/quic/bindingdata.cc',
|
||||
'src/quic/cid.cc',
|
||||
'src/quic/data.cc',
|
||||
'src/quic/logstream.cc',
|
||||
'src/quic/packet.cc',
|
||||
'src/quic/preferredaddress.cc',
|
||||
'src/quic/sessionticket.cc',
|
||||
@@ -357,6 +356,7 @@
|
||||
'src/quic/endpoint.cc',
|
||||
'src/quic/http3.cc',
|
||||
'src/quic/session.cc',
|
||||
'src/quic/session_manager.cc',
|
||||
'src/quic/streams.cc',
|
||||
'src/quic/tlscontext.cc',
|
||||
'src/quic/transportparams.cc',
|
||||
@@ -366,7 +366,6 @@
|
||||
'src/quic/cid.h',
|
||||
'src/quic/data.h',
|
||||
'src/quic/defs.h',
|
||||
'src/quic/logstream.h',
|
||||
'src/quic/packet.h',
|
||||
'src/quic/preferredaddress.h',
|
||||
'src/quic/sessionticket.h',
|
||||
@@ -376,6 +375,7 @@
|
||||
'src/quic/endpoint.h',
|
||||
'src/quic/http3.h',
|
||||
'src/quic/session.h',
|
||||
'src/quic/session_manager.h',
|
||||
'src/quic/streams.h',
|
||||
'src/quic/tlscontext.h',
|
||||
'src/quic/guard.h',
|
||||
|
||||
+27
-9
@@ -391,10 +391,11 @@ class NonIdempotentDataQueueReader final
|
||||
// If the collection of entries is empty, there's nothing currently left to
|
||||
// read. How we respond depends on whether the data queue has been capped
|
||||
// or not.
|
||||
|
||||
if (data_queue_->entries_.empty()) {
|
||||
// If the data_queue_ is empty, and not capped, then we can reasonably
|
||||
// expect more data to be provided later, but we don't know exactly when
|
||||
// that'll happe, so the proper response here is to return a blocked
|
||||
// that'll happen, so the proper response here is to return a blocked
|
||||
// status.
|
||||
if (!data_queue_->is_capped()) {
|
||||
std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {});
|
||||
@@ -437,8 +438,11 @@ class NonIdempotentDataQueueReader final
|
||||
CHECK(!pull_pending_);
|
||||
pull_pending_ = true;
|
||||
int status = current_reader->Pull(
|
||||
[this, next = std::move(next)](
|
||||
int status, const DataQueue::Vec* vecs, uint64_t count, Done done) {
|
||||
[this, next = std::move(next), options, data, count, max_count_hint](
|
||||
int status,
|
||||
const DataQueue::Vec* vecs,
|
||||
uint64_t vcount,
|
||||
Done done) mutable {
|
||||
pull_pending_ = false;
|
||||
|
||||
// In each of these cases, we do not expect that the source will
|
||||
@@ -446,13 +450,27 @@ class NonIdempotentDataQueueReader final
|
||||
CHECK_IMPLIES(status == bob::Status::STATUS_BLOCK ||
|
||||
status == bob::Status::STATUS_WAIT ||
|
||||
status == bob::Status::STATUS_EOS,
|
||||
vecs == nullptr && count == 0);
|
||||
vecs == nullptr && vcount == 0);
|
||||
if (status == bob::Status::STATUS_EOS) {
|
||||
data_queue_->entries_.erase(data_queue_->entries_.begin());
|
||||
ended_ = data_queue_->entries_.empty();
|
||||
current_reader_ = nullptr;
|
||||
if (!ended_) status = bob::Status::STATUS_CONTINUE;
|
||||
std::move(next)(status, nullptr, 0, [](uint64_t) {});
|
||||
if (!data_queue_->entries_.empty()) {
|
||||
// More entries remain. Pull from the next entry immediately
|
||||
// rather than returning empty CONTINUE, which would leave
|
||||
// callers with no data and no way to know they should retry.
|
||||
Pull(std::move(next), options, data, count, max_count_hint);
|
||||
} else if (!data_queue_->is_capped()) {
|
||||
// The queue is empty but not capped — more data may arrive
|
||||
// later. Return BLOCK so the consumer waits rather than
|
||||
// falsely treating this as end-of-stream.
|
||||
std::move(next)(
|
||||
bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {});
|
||||
} else {
|
||||
// Empty and capped — truly done.
|
||||
ended_ = true;
|
||||
std::move(next)(
|
||||
bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -461,7 +479,7 @@ class NonIdempotentDataQueueReader final
|
||||
if (data_queue_->HasBackpressureListeners()) {
|
||||
// How much did we actually read?
|
||||
size_t read = 0;
|
||||
for (uint64_t n = 0; n < count; n++) {
|
||||
for (uint64_t n = 0; n < vcount; n++) {
|
||||
read += vecs[n].len;
|
||||
}
|
||||
data_queue_->NotifyBackpressure(read);
|
||||
@@ -469,7 +487,7 @@ class NonIdempotentDataQueueReader final
|
||||
|
||||
// Now that we have updated this readers state, we can forward
|
||||
// everything on to the outer next.
|
||||
std::move(next)(status, vecs, count, std::move(done));
|
||||
std::move(next)(status, vecs, vcount, std::move(done));
|
||||
},
|
||||
options,
|
||||
data,
|
||||
|
||||
+2
-1
@@ -54,7 +54,8 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
|
||||
V(INSPECTOR_CLIENT) \
|
||||
V(INSPECTOR_PROFILER) \
|
||||
V(CODE_CACHE) \
|
||||
V(NGTCP2_DEBUG) \
|
||||
V(NGTCP2) \
|
||||
V(NGHTTP3) \
|
||||
V(SEA) \
|
||||
V(WASI) \
|
||||
V(MODULE) \
|
||||
|
||||
+27
-23
@@ -156,8 +156,7 @@ Local<FunctionTemplate> Blob::GetConstructorTemplate(Environment* env) {
|
||||
Isolate* isolate = env->isolate();
|
||||
tmpl = NewFunctionTemplate(isolate, nullptr);
|
||||
tmpl->InstanceTemplate()->SetInternalFieldCount(Blob::kInternalFieldCount);
|
||||
tmpl->SetClassName(
|
||||
FIXED_ONE_BYTE_STRING(env->isolate(), "Blob"));
|
||||
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Blob"));
|
||||
SetProtoMethod(isolate, tmpl, "getReader", GetReader);
|
||||
SetProtoMethod(isolate, tmpl, "slice", ToSlice);
|
||||
env->set_blob_constructor_template(tmpl);
|
||||
@@ -255,8 +254,7 @@ void Blob::New(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
|
||||
auto blob = Create(env, DataQueue::CreateIdempotent(std::move(entries)));
|
||||
if (blob)
|
||||
args.GetReturnValue().Set(blob->object());
|
||||
if (blob) args.GetReturnValue().Set(blob->object());
|
||||
}
|
||||
|
||||
void Blob::GetReader(const FunctionCallbackInfo<Value>& args) {
|
||||
@@ -278,8 +276,7 @@ void Blob::ToSlice(const FunctionCallbackInfo<Value>& args) {
|
||||
size_t start = args[0].As<Uint32>()->Value();
|
||||
size_t end = args[1].As<Uint32>()->Value();
|
||||
BaseObjectPtr<Blob> slice = blob->Slice(env, start, end);
|
||||
if (slice)
|
||||
args.GetReturnValue().Set(slice->object());
|
||||
if (slice) args.GetReturnValue().Set(slice->object());
|
||||
}
|
||||
|
||||
void Blob::MemoryInfo(MemoryTracker* tracker) const {
|
||||
@@ -343,6 +340,7 @@ void Blob::Reader::Pull(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Blob::Reader* reader;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&reader, args.This());
|
||||
reader->pull_pending_ = false;
|
||||
|
||||
CHECK(args[0]->IsFunction());
|
||||
Local<Function> fn = args[0].As<Function>();
|
||||
@@ -414,19 +412,31 @@ void Blob::Reader::Pull(const FunctionCallbackInfo<Value>& args) {
|
||||
void Blob::Reader::SetWakeup(const FunctionCallbackInfo<Value>& args) {
|
||||
Blob::Reader* reader;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&reader, args.This());
|
||||
if (args[0]->IsUndefined()) {
|
||||
reader->wakeup_.Reset();
|
||||
return;
|
||||
}
|
||||
CHECK(args[0]->IsFunction());
|
||||
reader->wakeup_.Reset(args.GetIsolate(), args[0].As<Function>());
|
||||
}
|
||||
|
||||
void Blob::Reader::NotifyPull() {
|
||||
void Blob::Reader::NotifyPull(bool fin) {
|
||||
if (wakeup_.IsEmpty() || !env()->can_call_into_js()) return;
|
||||
// FIN notifications always fire — they must not be suppressed by
|
||||
// pull_pending_ because there will be no further notifications to
|
||||
// wake the iterator. Regular data notifications respect pull_pending_
|
||||
// to coalesce multiple deliveries within a single packet.
|
||||
if (!fin && pull_pending_) return;
|
||||
pull_pending_ = true;
|
||||
HandleScope handle_scope(env()->isolate());
|
||||
Local<Function> fn = wakeup_.Get(env()->isolate());
|
||||
MakeCallback(fn, 0, nullptr);
|
||||
// Pass fin as the first argument so the JS iterator knows EOS is
|
||||
// imminent and should pull again without waiting for another wakeup.
|
||||
Local<Value> argv[] = {v8::Boolean::New(env()->isolate(), fin)};
|
||||
MakeCallback(fn, 1, argv);
|
||||
}
|
||||
|
||||
BaseObjectPtr<BaseObject>
|
||||
Blob::BlobTransferData::Deserialize(
|
||||
BaseObjectPtr<BaseObject> Blob::BlobTransferData::Deserialize(
|
||||
Environment* env,
|
||||
Local<Context> context,
|
||||
std::unique_ptr<worker::TransferData> self) {
|
||||
@@ -448,10 +458,10 @@ std::unique_ptr<worker::TransferData> Blob::CloneForMessaging() const {
|
||||
void Blob::StoreDataObject(const FunctionCallbackInfo<Value>& args) {
|
||||
Realm* realm = Realm::GetCurrent(args);
|
||||
|
||||
CHECK(args[0]->IsString()); // ID key
|
||||
CHECK(args[0]->IsString()); // ID key
|
||||
CHECK(Blob::HasInstance(realm->env(), args[1])); // Blob
|
||||
CHECK(args[2]->IsUint32()); // Length
|
||||
CHECK(args[3]->IsString()); // Type
|
||||
CHECK(args[2]->IsUint32()); // Length
|
||||
CHECK(args[3]->IsString()); // Type
|
||||
|
||||
BlobBindingData* binding_data = realm->GetBindingData<BlobBindingData>();
|
||||
Isolate* isolate = realm->isolate();
|
||||
@@ -531,12 +541,8 @@ void BlobBindingData::StoredDataObject::MemoryInfo(
|
||||
}
|
||||
|
||||
BlobBindingData::StoredDataObject::StoredDataObject(
|
||||
const BaseObjectPtr<Blob>& blob_,
|
||||
size_t length_,
|
||||
const std::string& type_)
|
||||
: blob(blob_),
|
||||
length(length_),
|
||||
type(type_) {}
|
||||
const BaseObjectPtr<Blob>& blob_, size_t length_, const std::string& type_)
|
||||
: blob(blob_), length(length_), type(type_) {}
|
||||
|
||||
BlobBindingData::BlobBindingData(Realm* realm, Local<Object> wrap)
|
||||
: SnapshotableObject(realm, wrap, type_int) {
|
||||
@@ -550,8 +556,7 @@ void BlobBindingData::MemoryInfo(MemoryTracker* tracker) const {
|
||||
}
|
||||
|
||||
void BlobBindingData::store_data_object(
|
||||
const std::string& uuid,
|
||||
const BlobBindingData::StoredDataObject& object) {
|
||||
const std::string& uuid, const BlobBindingData::StoredDataObject& object) {
|
||||
data_objects_[uuid] = object;
|
||||
}
|
||||
|
||||
@@ -566,8 +571,7 @@ void BlobBindingData::revoke_data_object(const std::string& uuid) {
|
||||
BlobBindingData::StoredDataObject BlobBindingData::get_data_object(
|
||||
const std::string& uuid) {
|
||||
auto entry = data_objects_.find(uuid);
|
||||
if (entry == data_objects_.end())
|
||||
return BlobBindingData::StoredDataObject {};
|
||||
if (entry == data_objects_.end()) return BlobBindingData::StoredDataObject{};
|
||||
return entry->second;
|
||||
}
|
||||
|
||||
|
||||
+8
-10
@@ -23,8 +23,7 @@ namespace node {
|
||||
|
||||
class Blob : public BaseObject {
|
||||
public:
|
||||
static void RegisterExternalReferences(
|
||||
ExternalReferenceRegistry* registry);
|
||||
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
|
||||
|
||||
static void CreatePerIsolateProperties(IsolateData* isolate_data,
|
||||
v8::Local<v8::ObjectTemplate> target);
|
||||
@@ -83,7 +82,7 @@ class Blob : public BaseObject {
|
||||
BaseObjectPtr<Blob> blob);
|
||||
static void Pull(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void SetWakeup(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
void NotifyPull();
|
||||
void NotifyPull(bool fin = false);
|
||||
|
||||
explicit Reader(Environment* env,
|
||||
v8::Local<v8::Object> obj,
|
||||
@@ -97,6 +96,7 @@ class Blob : public BaseObject {
|
||||
std::shared_ptr<DataQueue::Reader> inner_;
|
||||
BaseObjectPtr<Blob> strong_ptr_;
|
||||
bool eos_ = false;
|
||||
bool pull_pending_ = false;
|
||||
v8::Global<v8::Function> wakeup_;
|
||||
};
|
||||
|
||||
@@ -134,19 +134,17 @@ class BlobBindingData : public SnapshotableObject {
|
||||
|
||||
StoredDataObject() = default;
|
||||
|
||||
StoredDataObject(
|
||||
const BaseObjectPtr<Blob>& blob_,
|
||||
size_t length_,
|
||||
const std::string& type_);
|
||||
StoredDataObject(const BaseObjectPtr<Blob>& blob_,
|
||||
size_t length_,
|
||||
const std::string& type_);
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_SELF_SIZE(StoredDataObject)
|
||||
SET_MEMORY_INFO_NAME(StoredDataObject)
|
||||
};
|
||||
|
||||
void store_data_object(
|
||||
const std::string& uuid,
|
||||
const StoredDataObject& object);
|
||||
void store_data_object(const std::string& uuid,
|
||||
const StoredDataObject& object);
|
||||
|
||||
void revoke_data_object(const std::string& uuid);
|
||||
|
||||
|
||||
@@ -338,6 +338,7 @@ class FileHandle final : public AsyncWrap, public StreamBase {
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
int GetFD() override { return fd_; }
|
||||
const std::string& original_name() const { return original_name_; }
|
||||
|
||||
int Release();
|
||||
|
||||
|
||||
@@ -34,12 +34,13 @@ extern uint64_t performance_v8_start;
|
||||
V(LOOP_EXIT, "loopExit") \
|
||||
V(BOOTSTRAP_COMPLETE, "bootstrapComplete")
|
||||
|
||||
#define NODE_PERFORMANCE_ENTRY_TYPES(V) \
|
||||
V(GC, "gc") \
|
||||
V(HTTP, "http") \
|
||||
V(HTTP2, "http2") \
|
||||
V(NET, "net") \
|
||||
V(DNS, "dns")
|
||||
#define NODE_PERFORMANCE_ENTRY_TYPES(V) \
|
||||
V(GC, "gc") \
|
||||
V(HTTP, "http") \
|
||||
V(HTTP2, "http2") \
|
||||
V(NET, "net") \
|
||||
V(DNS, "dns") \
|
||||
V(QUIC, "quic")
|
||||
|
||||
enum PerformanceMilestone {
|
||||
#define V(name, _) NODE_PERFORMANCE_MILESTONE_##name,
|
||||
|
||||
+172
-183
@@ -40,41 +40,31 @@ SocketAddress FromUVHandle(F fn, const T& handle) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool SocketAddress::ToSockAddr(
|
||||
int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
sockaddr_storage* addr) {
|
||||
bool SocketAddress::ToSockAddr(int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
sockaddr_storage* addr) {
|
||||
switch (family) {
|
||||
case AF_INET:
|
||||
return uv_ip4_addr(
|
||||
host,
|
||||
port,
|
||||
reinterpret_cast<sockaddr_in*>(addr)) == 0;
|
||||
return uv_ip4_addr(host, port, reinterpret_cast<sockaddr_in*>(addr)) == 0;
|
||||
case AF_INET6:
|
||||
return uv_ip6_addr(
|
||||
host,
|
||||
port,
|
||||
reinterpret_cast<sockaddr_in6*>(addr)) == 0;
|
||||
return uv_ip6_addr(host, port, reinterpret_cast<sockaddr_in6*>(addr)) ==
|
||||
0;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
bool SocketAddress::New(
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr) {
|
||||
bool SocketAddress::New(const char* host, uint32_t port, SocketAddress* addr) {
|
||||
return New(AF_INET, host, port, addr) || New(AF_INET6, host, port, addr);
|
||||
}
|
||||
|
||||
bool SocketAddress::New(
|
||||
int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr) {
|
||||
return ToSockAddr(family, host, port,
|
||||
reinterpret_cast<sockaddr_storage*>(addr->storage()));
|
||||
bool SocketAddress::New(int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr) {
|
||||
return ToSockAddr(
|
||||
family, host, port, reinterpret_cast<sockaddr_storage*>(addr->storage()));
|
||||
}
|
||||
|
||||
size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const {
|
||||
@@ -102,6 +92,43 @@ size_t SocketAddress::Hash::operator()(const SocketAddress& addr) const {
|
||||
}
|
||||
}
|
||||
|
||||
size_t SocketAddress::IpHash::operator()(const SocketAddress& addr) const {
|
||||
// Hash only the IP address bytes, ignoring the port.
|
||||
switch (addr.family()) {
|
||||
case AF_INET: {
|
||||
const sockaddr_in* ipv4 =
|
||||
reinterpret_cast<const sockaddr_in*>(addr.raw());
|
||||
return HashBytes(reinterpret_cast<const uint8_t*>(&ipv4->sin_addr), 4);
|
||||
}
|
||||
case AF_INET6: {
|
||||
const sockaddr_in6* ipv6 =
|
||||
reinterpret_cast<const sockaddr_in6*>(addr.raw());
|
||||
return HashBytes(reinterpret_cast<const uint8_t*>(&ipv6->sin6_addr), 16);
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
bool SocketAddress::IpEqual::operator()(const SocketAddress& a,
|
||||
const SocketAddress& b) const {
|
||||
if (a.family() != b.family()) return false;
|
||||
switch (a.family()) {
|
||||
case AF_INET: {
|
||||
const sockaddr_in* a4 = reinterpret_cast<const sockaddr_in*>(a.raw());
|
||||
const sockaddr_in* b4 = reinterpret_cast<const sockaddr_in*>(b.raw());
|
||||
return memcmp(&a4->sin_addr, &b4->sin_addr, 4) == 0;
|
||||
}
|
||||
case AF_INET6: {
|
||||
const sockaddr_in6* a6 = reinterpret_cast<const sockaddr_in6*>(a.raw());
|
||||
const sockaddr_in6* b6 = reinterpret_cast<const sockaddr_in6*>(b.raw());
|
||||
return memcmp(&a6->sin6_addr, &b6->sin6_addr, 16) == 0;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
SocketAddress SocketAddress::FromSockName(const uv_tcp_t& handle) {
|
||||
return FromUVHandle(uv_tcp_getsockname, handle);
|
||||
}
|
||||
@@ -119,21 +146,15 @@ SocketAddress SocketAddress::FromPeerName(const uv_udp_t& handle) {
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t mask[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
|
||||
constexpr uint8_t mask[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff};
|
||||
|
||||
bool is_match_ipv4(
|
||||
const SocketAddress& one,
|
||||
const SocketAddress& two) {
|
||||
const sockaddr_in* one_in =
|
||||
reinterpret_cast<const sockaddr_in*>(one.data());
|
||||
const sockaddr_in* two_in =
|
||||
reinterpret_cast<const sockaddr_in*>(two.data());
|
||||
bool is_match_ipv4(const SocketAddress& one, const SocketAddress& two) {
|
||||
const sockaddr_in* one_in = reinterpret_cast<const sockaddr_in*>(one.data());
|
||||
const sockaddr_in* two_in = reinterpret_cast<const sockaddr_in*>(two.data());
|
||||
return memcmp(&one_in->sin_addr, &two_in->sin_addr, sizeof(uint32_t)) == 0;
|
||||
}
|
||||
|
||||
bool is_match_ipv6(
|
||||
const SocketAddress& one,
|
||||
const SocketAddress& two) {
|
||||
bool is_match_ipv6(const SocketAddress& one, const SocketAddress& two) {
|
||||
const sockaddr_in6* one_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(one.data());
|
||||
const sockaddr_in6* two_in =
|
||||
@@ -141,29 +162,23 @@ bool is_match_ipv6(
|
||||
return memcmp(&one_in->sin6_addr, &two_in->sin6_addr, 16) == 0;
|
||||
}
|
||||
|
||||
bool is_match_ipv4_ipv6(
|
||||
const SocketAddress& ipv4,
|
||||
const SocketAddress& ipv6) {
|
||||
bool is_match_ipv4_ipv6(const SocketAddress& ipv4, const SocketAddress& ipv6) {
|
||||
const sockaddr_in* check_ipv4 =
|
||||
reinterpret_cast<const sockaddr_in*>(ipv4.data());
|
||||
const sockaddr_in6* check_ipv6 =
|
||||
reinterpret_cast<const sockaddr_in6*>(ipv6.data());
|
||||
|
||||
const uint8_t* ptr =
|
||||
reinterpret_cast<const uint8_t*>(&check_ipv6->sin6_addr);
|
||||
const uint8_t* ptr = reinterpret_cast<const uint8_t*>(&check_ipv6->sin6_addr);
|
||||
|
||||
return memcmp(ptr, mask, sizeof(mask)) == 0 &&
|
||||
memcmp(ptr + sizeof(mask),
|
||||
&check_ipv4->sin_addr,
|
||||
sizeof(uint32_t)) == 0;
|
||||
memcmp(ptr + sizeof(mask), &check_ipv4->sin_addr, sizeof(uint32_t)) ==
|
||||
0;
|
||||
}
|
||||
|
||||
std::partial_ordering compare_ipv4(const SocketAddress& one,
|
||||
const SocketAddress& two) {
|
||||
const sockaddr_in* one_in =
|
||||
reinterpret_cast<const sockaddr_in*>(one.data());
|
||||
const sockaddr_in* two_in =
|
||||
reinterpret_cast<const sockaddr_in*>(two.data());
|
||||
const sockaddr_in* one_in = reinterpret_cast<const sockaddr_in*>(one.data());
|
||||
const sockaddr_in* two_in = reinterpret_cast<const sockaddr_in*>(two.data());
|
||||
const uint32_t s_addr_one = ntohl(one_in->sin_addr.s_addr);
|
||||
const uint32_t s_addr_two = ntohl(two_in->sin_addr.s_addr);
|
||||
|
||||
@@ -193,19 +208,15 @@ std::partial_ordering compare_ipv4_ipv6(const SocketAddress& ipv4,
|
||||
const SocketAddress& ipv6) {
|
||||
const sockaddr_in* ipv4_in =
|
||||
reinterpret_cast<const sockaddr_in*>(ipv4.data());
|
||||
const sockaddr_in6 * ipv6_in =
|
||||
const sockaddr_in6* ipv6_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(ipv6.data());
|
||||
|
||||
const uint8_t* ptr =
|
||||
reinterpret_cast<const uint8_t*>(&ipv6_in->sin6_addr);
|
||||
const uint8_t* ptr = reinterpret_cast<const uint8_t*>(&ipv6_in->sin6_addr);
|
||||
|
||||
if (memcmp(ptr, mask, sizeof(mask)) != 0)
|
||||
return std::partial_ordering::unordered;
|
||||
|
||||
int ret = memcmp(
|
||||
&ipv4_in->sin_addr,
|
||||
ptr + sizeof(mask),
|
||||
sizeof(uint32_t));
|
||||
int ret = memcmp(&ipv4_in->sin_addr, ptr + sizeof(mask), sizeof(uint32_t));
|
||||
|
||||
if (ret < 0)
|
||||
return std::partial_ordering::less;
|
||||
@@ -214,25 +225,21 @@ std::partial_ordering compare_ipv4_ipv6(const SocketAddress& ipv4,
|
||||
return std::partial_ordering::equivalent;
|
||||
}
|
||||
|
||||
bool in_network_ipv4(
|
||||
const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
bool in_network_ipv4(const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
uint32_t mask = ((1ull << prefix) - 1) << (32 - prefix);
|
||||
|
||||
const sockaddr_in* ip_in =
|
||||
reinterpret_cast<const sockaddr_in*>(ip.data());
|
||||
const sockaddr_in* net_in =
|
||||
reinterpret_cast<const sockaddr_in*>(net.data());
|
||||
const sockaddr_in* ip_in = reinterpret_cast<const sockaddr_in*>(ip.data());
|
||||
const sockaddr_in* net_in = reinterpret_cast<const sockaddr_in*>(net.data());
|
||||
|
||||
return (htonl(ip_in->sin_addr.s_addr) & mask) ==
|
||||
(htonl(net_in->sin_addr.s_addr) & mask);
|
||||
}
|
||||
|
||||
bool in_network_ipv6(
|
||||
const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
bool in_network_ipv6(const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
// Special case, if prefix == 128, then just do a
|
||||
// straight comparison.
|
||||
if (prefix == 128)
|
||||
@@ -242,27 +249,23 @@ bool in_network_ipv6(
|
||||
int len = (prefix - r) / 8;
|
||||
uint8_t mask = ((1 << r) - 1) << (8 - r);
|
||||
|
||||
const sockaddr_in6* ip_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(ip.data());
|
||||
const sockaddr_in6* ip_in = reinterpret_cast<const sockaddr_in6*>(ip.data());
|
||||
const sockaddr_in6* net_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(net.data());
|
||||
|
||||
if (memcmp(&ip_in->sin6_addr, &net_in->sin6_addr, len) != 0)
|
||||
return false;
|
||||
if (memcmp(&ip_in->sin6_addr, &net_in->sin6_addr, len) != 0) return false;
|
||||
|
||||
const uint8_t* p1 = reinterpret_cast<const uint8_t*>(
|
||||
ip_in->sin6_addr.s6_addr);
|
||||
const uint8_t* p2 = reinterpret_cast<const uint8_t*>(
|
||||
net_in->sin6_addr.s6_addr);
|
||||
const uint8_t* p1 =
|
||||
reinterpret_cast<const uint8_t*>(ip_in->sin6_addr.s6_addr);
|
||||
const uint8_t* p2 =
|
||||
reinterpret_cast<const uint8_t*>(net_in->sin6_addr.s6_addr);
|
||||
|
||||
return (p1[len] & mask) == (p2[len] & mask);
|
||||
}
|
||||
|
||||
bool in_network_ipv4_ipv6(
|
||||
const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
|
||||
bool in_network_ipv4_ipv6(const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
if (prefix == 128)
|
||||
return compare_ipv4_ipv6(ip, net) == std::partial_ordering::equivalent;
|
||||
|
||||
@@ -270,8 +273,7 @@ bool in_network_ipv4_ipv6(
|
||||
int len = (prefix - r) / 8;
|
||||
uint8_t mask = ((1 << r) - 1) << (8 - r);
|
||||
|
||||
const sockaddr_in* ip_in =
|
||||
reinterpret_cast<const sockaddr_in*>(ip.data());
|
||||
const sockaddr_in* ip_in = reinterpret_cast<const sockaddr_in*>(ip.data());
|
||||
const sockaddr_in6* net_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(net.data());
|
||||
|
||||
@@ -279,35 +281,29 @@ bool in_network_ipv4_ipv6(
|
||||
uint8_t* ptr = ip_mask;
|
||||
memcpy(ptr + 12, &ip_in->sin_addr, 4);
|
||||
|
||||
if (memcmp(ptr, &net_in->sin6_addr, len) != 0)
|
||||
return false;
|
||||
if (memcmp(ptr, &net_in->sin6_addr, len) != 0) return false;
|
||||
|
||||
ptr += len;
|
||||
const uint8_t* p2 = reinterpret_cast<const uint8_t*>(
|
||||
net_in->sin6_addr.s6_addr);
|
||||
const uint8_t* p2 =
|
||||
reinterpret_cast<const uint8_t*>(net_in->sin6_addr.s6_addr);
|
||||
|
||||
return (ptr[0] & mask) == (p2[len] & mask);
|
||||
}
|
||||
|
||||
bool in_network_ipv6_ipv4(
|
||||
const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
bool in_network_ipv6_ipv4(const SocketAddress& ip,
|
||||
const SocketAddress& net,
|
||||
int prefix) {
|
||||
if (prefix == 32)
|
||||
return compare_ipv4_ipv6(net, ip) == std::partial_ordering::equivalent;
|
||||
|
||||
uint32_t m = ((1ull << prefix) - 1) << (32 - prefix);
|
||||
|
||||
const sockaddr_in6* ip_in =
|
||||
reinterpret_cast<const sockaddr_in6*>(ip.data());
|
||||
const sockaddr_in* net_in =
|
||||
reinterpret_cast<const sockaddr_in*>(net.data());
|
||||
const sockaddr_in6* ip_in = reinterpret_cast<const sockaddr_in6*>(ip.data());
|
||||
const sockaddr_in* net_in = reinterpret_cast<const sockaddr_in*>(net.data());
|
||||
|
||||
const uint8_t* ptr =
|
||||
reinterpret_cast<const uint8_t*>(&ip_in->sin6_addr);
|
||||
const uint8_t* ptr = reinterpret_cast<const uint8_t*>(&ip_in->sin6_addr);
|
||||
|
||||
if (memcmp(ptr, mask, sizeof(mask)) != 0)
|
||||
return false;
|
||||
if (memcmp(ptr, mask, sizeof(mask)) != 0) return false;
|
||||
|
||||
ptr += sizeof(mask);
|
||||
uint32_t check = nbytes::ReadUint32BE(ptr);
|
||||
@@ -324,14 +320,18 @@ bool SocketAddress::is_match(const SocketAddress& other) const {
|
||||
switch (family()) {
|
||||
case AF_INET:
|
||||
switch (other.family()) {
|
||||
case AF_INET: return is_match_ipv4(*this, other);
|
||||
case AF_INET6: return is_match_ipv4_ipv6(*this, other);
|
||||
case AF_INET:
|
||||
return is_match_ipv4(*this, other);
|
||||
case AF_INET6:
|
||||
return is_match_ipv4_ipv6(*this, other);
|
||||
}
|
||||
break;
|
||||
case AF_INET6:
|
||||
switch (other.family()) {
|
||||
case AF_INET: return is_match_ipv4_ipv6(other, *this);
|
||||
case AF_INET6: return is_match_ipv6(*this, other);
|
||||
case AF_INET:
|
||||
return is_match_ipv4_ipv6(other, *this);
|
||||
case AF_INET6:
|
||||
return is_match_ipv6(*this, other);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -342,8 +342,10 @@ std::partial_ordering SocketAddress::compare(const SocketAddress& other) const {
|
||||
switch (family()) {
|
||||
case AF_INET:
|
||||
switch (other.family()) {
|
||||
case AF_INET: return compare_ipv4(*this, other);
|
||||
case AF_INET6: return compare_ipv4_ipv6(*this, other);
|
||||
case AF_INET:
|
||||
return compare_ipv4(*this, other);
|
||||
case AF_INET6:
|
||||
return compare_ipv4_ipv6(*this, other);
|
||||
}
|
||||
break;
|
||||
case AF_INET6:
|
||||
@@ -361,28 +363,31 @@ std::partial_ordering SocketAddress::compare(const SocketAddress& other) const {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AF_INET6: return compare_ipv6(*this, other);
|
||||
case AF_INET6:
|
||||
return compare_ipv6(*this, other);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return std::partial_ordering::unordered;
|
||||
}
|
||||
|
||||
bool SocketAddress::is_in_network(
|
||||
const SocketAddress& other,
|
||||
int prefix) const {
|
||||
|
||||
bool SocketAddress::is_in_network(const SocketAddress& other,
|
||||
int prefix) const {
|
||||
switch (family()) {
|
||||
case AF_INET:
|
||||
switch (other.family()) {
|
||||
case AF_INET: return in_network_ipv4(*this, other, prefix);
|
||||
case AF_INET6: return in_network_ipv4_ipv6(*this, other, prefix);
|
||||
case AF_INET:
|
||||
return in_network_ipv4(*this, other, prefix);
|
||||
case AF_INET6:
|
||||
return in_network_ipv4_ipv6(*this, other, prefix);
|
||||
}
|
||||
break;
|
||||
case AF_INET6:
|
||||
switch (other.family()) {
|
||||
case AF_INET: return in_network_ipv6_ipv4(*this, other, prefix);
|
||||
case AF_INET6: return in_network_ipv6(*this, other, prefix);
|
||||
case AF_INET:
|
||||
return in_network_ipv6_ipv4(*this, other, prefix);
|
||||
case AF_INET6:
|
||||
return in_network_ipv6(*this, other, prefix);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -397,8 +402,7 @@ SocketAddressBlockList::SocketAddressBlockList(
|
||||
void SocketAddressBlockList::AddSocketAddress(
|
||||
const std::shared_ptr<SocketAddress>& address) {
|
||||
Mutex::ScopedLock lock(mutex_);
|
||||
std::unique_ptr<Rule> rule =
|
||||
std::make_unique<SocketAddressRule>(address);
|
||||
std::unique_ptr<Rule> rule = std::make_unique<SocketAddressRule>(address);
|
||||
rules_.emplace_front(std::move(rule));
|
||||
address_rules_[*address.get()] = rules_.begin();
|
||||
}
|
||||
@@ -423,8 +427,7 @@ void SocketAddressBlockList::AddSocketAddressRange(
|
||||
}
|
||||
|
||||
void SocketAddressBlockList::AddSocketAddressMask(
|
||||
const std::shared_ptr<SocketAddress>& network,
|
||||
int prefix) {
|
||||
const std::shared_ptr<SocketAddress>& network, int prefix) {
|
||||
Mutex::ScopedLock lock(mutex_);
|
||||
std::unique_ptr<Rule> rule =
|
||||
std::make_unique<SocketAddressMaskRule>(network, prefix);
|
||||
@@ -435,8 +438,7 @@ bool SocketAddressBlockList::Apply(
|
||||
const std::shared_ptr<SocketAddress>& address) {
|
||||
Mutex::ScopedLock lock(mutex_);
|
||||
for (const auto& rule : rules_) {
|
||||
if (rule->Apply(address))
|
||||
return true;
|
||||
if (rule->Apply(address)) return true;
|
||||
}
|
||||
return parent_ ? parent_->Apply(address) : false;
|
||||
}
|
||||
@@ -448,14 +450,11 @@ SocketAddressBlockList::SocketAddressRule::SocketAddressRule(
|
||||
SocketAddressBlockList::SocketAddressRangeRule::SocketAddressRangeRule(
|
||||
const std::shared_ptr<SocketAddress>& start_,
|
||||
const std::shared_ptr<SocketAddress>& end_)
|
||||
: start(start_),
|
||||
end(end_) {}
|
||||
: start(start_), end(end_) {}
|
||||
|
||||
SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule(
|
||||
const std::shared_ptr<SocketAddress>& network_,
|
||||
int prefix_)
|
||||
: network(network_),
|
||||
prefix(prefix_) {}
|
||||
const std::shared_ptr<SocketAddress>& network_, int prefix_)
|
||||
: network(network_), prefix(prefix_) {}
|
||||
|
||||
bool SocketAddressBlockList::SocketAddressRule::Apply(
|
||||
const std::shared_ptr<SocketAddress>& address) {
|
||||
@@ -472,8 +471,7 @@ std::string SocketAddressBlockList::SocketAddressRule::ToString() {
|
||||
|
||||
bool SocketAddressBlockList::SocketAddressRangeRule::Apply(
|
||||
const std::shared_ptr<SocketAddress>& address) {
|
||||
return *address.get() >= *start.get() &&
|
||||
*address.get() <= *end.get();
|
||||
return *address.get() >= *start.get() && *address.get() <= *end.get();
|
||||
}
|
||||
|
||||
std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() {
|
||||
@@ -503,19 +501,16 @@ std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() {
|
||||
MaybeLocal<Array> SocketAddressBlockList::ListRules(Environment* env) {
|
||||
Mutex::ScopedLock lock(mutex_);
|
||||
LocalVector<Value> rules(env->isolate());
|
||||
if (!ListRules(env, &rules))
|
||||
return MaybeLocal<Array>();
|
||||
if (!ListRules(env, &rules)) return MaybeLocal<Array>();
|
||||
return Array::New(env->isolate(), rules.data(), rules.size());
|
||||
}
|
||||
|
||||
bool SocketAddressBlockList::ListRules(Environment* env,
|
||||
LocalVector<Value>* rules) {
|
||||
if (parent_ && !parent_->ListRules(env, rules))
|
||||
return false;
|
||||
if (parent_ && !parent_->ListRules(env, rules)) return false;
|
||||
for (const auto& rule : rules_) {
|
||||
Local<Value> str;
|
||||
if (!rule->ToV8String(env).ToLocal(&str))
|
||||
return false;
|
||||
if (!rule->ToV8String(env).ToLocal(&str)) return false;
|
||||
rules->push_back(str);
|
||||
}
|
||||
return true;
|
||||
@@ -545,8 +540,7 @@ SocketAddressBlockListWrap::SocketAddressBlockListWrap(
|
||||
Environment* env,
|
||||
Local<Object> wrap,
|
||||
std::shared_ptr<SocketAddressBlockList> blocklist)
|
||||
: BaseObject(env, wrap),
|
||||
blocklist_(std::move(blocklist)) {
|
||||
: BaseObject(env, wrap), blocklist_(std::move(blocklist)) {
|
||||
MakeWeak();
|
||||
}
|
||||
|
||||
@@ -554,8 +548,9 @@ BaseObjectPtr<SocketAddressBlockListWrap> SocketAddressBlockListWrap::New(
|
||||
Environment* env) {
|
||||
Local<Object> obj;
|
||||
if (!env->blocklist_constructor_template()
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context()).ToLocal(&obj)) {
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context())
|
||||
.ToLocal(&obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
BaseObjectPtr<SocketAddressBlockListWrap> wrap =
|
||||
@@ -565,25 +560,22 @@ BaseObjectPtr<SocketAddressBlockListWrap> SocketAddressBlockListWrap::New(
|
||||
}
|
||||
|
||||
BaseObjectPtr<SocketAddressBlockListWrap> SocketAddressBlockListWrap::New(
|
||||
Environment* env,
|
||||
std::shared_ptr<SocketAddressBlockList> blocklist) {
|
||||
Environment* env, std::shared_ptr<SocketAddressBlockList> blocklist) {
|
||||
Local<Object> obj;
|
||||
if (!env->blocklist_constructor_template()
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context()).ToLocal(&obj)) {
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context())
|
||||
.ToLocal(&obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
BaseObjectPtr<SocketAddressBlockListWrap> wrap =
|
||||
MakeBaseObject<SocketAddressBlockListWrap>(
|
||||
env,
|
||||
obj,
|
||||
std::move(blocklist));
|
||||
env, obj, std::move(blocklist));
|
||||
CHECK(wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
void SocketAddressBlockListWrap::New(
|
||||
const FunctionCallbackInfo<Value>& args) {
|
||||
void SocketAddressBlockListWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK(args.IsConstructCall());
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
new SocketAddressBlockListWrap(env, args.This());
|
||||
@@ -622,9 +614,8 @@ void SocketAddressBlockListWrap::AddRange(
|
||||
if (*start_addr->address().get() > *end_addr->address().get())
|
||||
return args.GetReturnValue().Set(false);
|
||||
|
||||
wrap->blocklist_->AddSocketAddressRange(
|
||||
start_addr->address(),
|
||||
end_addr->address());
|
||||
wrap->blocklist_->AddSocketAddressRange(start_addr->address(),
|
||||
end_addr->address());
|
||||
|
||||
args.GetReturnValue().Set(true);
|
||||
}
|
||||
@@ -687,9 +678,8 @@ SocketAddressBlockListWrap::CloneForMessaging() const {
|
||||
return std::make_unique<TransferData>(this);
|
||||
}
|
||||
|
||||
bool SocketAddressBlockListWrap::HasInstance(
|
||||
Environment* env,
|
||||
Local<Value> value) {
|
||||
bool SocketAddressBlockListWrap::HasInstance(Environment* env,
|
||||
Local<Value> value) {
|
||||
return GetConstructorTemplate(env)->HasInstance(value);
|
||||
}
|
||||
|
||||
@@ -711,11 +701,10 @@ Local<FunctionTemplate> SocketAddressBlockListWrap::GetConstructorTemplate(
|
||||
return tmpl;
|
||||
}
|
||||
|
||||
void SocketAddressBlockListWrap::Initialize(
|
||||
Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
void* priv) {
|
||||
void SocketAddressBlockListWrap::Initialize(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
void* priv) {
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
|
||||
SetConstructorFunction(context,
|
||||
@@ -772,12 +761,12 @@ void SocketAddressBase::Initialize(Environment* env, Local<Object> target) {
|
||||
}
|
||||
|
||||
BaseObjectPtr<SocketAddressBase> SocketAddressBase::Create(
|
||||
Environment* env,
|
||||
std::shared_ptr<SocketAddress> address) {
|
||||
Environment* env, std::shared_ptr<SocketAddress> address) {
|
||||
Local<Object> obj;
|
||||
if (!GetConstructorTemplate(env)
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context()).ToLocal(&obj)) {
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context())
|
||||
.ToLocal(&obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -788,8 +777,8 @@ 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[1]->IsInt32()); // port
|
||||
CHECK(args[2]->IsInt32()); // family
|
||||
CHECK(args[3]->IsUint32()); // flow label
|
||||
|
||||
Utf8Value address(env->isolate(), args[0]);
|
||||
@@ -820,19 +809,21 @@ void SocketAddressBase::Detail(const FunctionCallbackInfo<Value>& args) {
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -852,12 +843,10 @@ void SocketAddressBase::LegacyDetail(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(address);
|
||||
}
|
||||
|
||||
SocketAddressBase::SocketAddressBase(
|
||||
Environment* env,
|
||||
Local<Object> wrap,
|
||||
std::shared_ptr<SocketAddress> address)
|
||||
: BaseObject(env, wrap),
|
||||
address_(std::move(address)) {
|
||||
SocketAddressBase::SocketAddressBase(Environment* env,
|
||||
Local<Object> wrap,
|
||||
std::shared_ptr<SocketAddress> address)
|
||||
: BaseObject(env, wrap), address_(std::move(address)) {
|
||||
MakeWeak();
|
||||
}
|
||||
|
||||
@@ -865,8 +854,8 @@ void SocketAddressBase::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("address", address_);
|
||||
}
|
||||
|
||||
std::unique_ptr<worker::TransferData>
|
||||
SocketAddressBase::CloneForMessaging() const {
|
||||
std::unique_ptr<worker::TransferData> SocketAddressBase::CloneForMessaging()
|
||||
const {
|
||||
return std::make_unique<TransferData>(this);
|
||||
}
|
||||
|
||||
|
||||
+40
-40
@@ -3,9 +3,9 @@
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include "base_object.h"
|
||||
#include "env.h"
|
||||
#include "memory_tracker.h"
|
||||
#include "base_object.h"
|
||||
#include "node.h"
|
||||
#include "node_worker.h"
|
||||
#include "uv.h"
|
||||
@@ -27,6 +27,16 @@ class SocketAddress : public MemoryRetainer {
|
||||
size_t operator()(const SocketAddress& addr) const;
|
||||
};
|
||||
|
||||
// Hashes and compares only the IP address, ignoring the port.
|
||||
// Useful for per-host connection counting where clients from
|
||||
// the same IP but different ports should be treated as one host.
|
||||
struct IpHash {
|
||||
size_t operator()(const SocketAddress& addr) const;
|
||||
};
|
||||
struct IpEqual {
|
||||
bool operator()(const SocketAddress& a, const SocketAddress& b) const;
|
||||
};
|
||||
|
||||
inline bool operator==(const SocketAddress& other) const;
|
||||
inline bool operator!=(const SocketAddress& other) const;
|
||||
|
||||
@@ -36,23 +46,18 @@ class SocketAddress : public MemoryRetainer {
|
||||
inline static bool is_numeric_host(const char* hostname, int family);
|
||||
|
||||
// Returns true if converting {family, host, port} to *addr succeeded.
|
||||
static bool ToSockAddr(
|
||||
int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
sockaddr_storage* addr);
|
||||
static bool ToSockAddr(int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
sockaddr_storage* addr);
|
||||
|
||||
// Returns true if converting {family, host, port} to *addr succeeded.
|
||||
static bool New(
|
||||
int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr);
|
||||
static bool New(int32_t family,
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr);
|
||||
|
||||
static bool New(
|
||||
const char* host,
|
||||
uint32_t port,
|
||||
SocketAddress* addr);
|
||||
static bool New(const char* host, uint32_t port, SocketAddress* addr);
|
||||
|
||||
// Returns the port for an IPv4 or IPv6 address.
|
||||
inline static int GetPort(const sockaddr* addr);
|
||||
@@ -135,6 +140,9 @@ class SocketAddress : public MemoryRetainer {
|
||||
template <typename T>
|
||||
using Map = std::unordered_map<SocketAddress, T, Hash>;
|
||||
|
||||
template <typename T>
|
||||
using IpMap = std::unordered_map<SocketAddress, T, IpHash, IpEqual>;
|
||||
|
||||
private:
|
||||
sockaddr_storage address_;
|
||||
};
|
||||
@@ -146,18 +154,16 @@ class SocketAddressBase : public BaseObject {
|
||||
Environment* env);
|
||||
static void Initialize(Environment* env, v8::Local<v8::Object> target);
|
||||
static BaseObjectPtr<SocketAddressBase> Create(
|
||||
Environment* env,
|
||||
std::shared_ptr<SocketAddress> address);
|
||||
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);
|
||||
SocketAddressBase(Environment* env,
|
||||
v8::Local<v8::Object> wrap,
|
||||
std::shared_ptr<SocketAddress> address);
|
||||
|
||||
inline const std::shared_ptr<SocketAddress>& address() const {
|
||||
return address_;
|
||||
@@ -245,13 +251,11 @@ class SocketAddressBlockList : public MemoryRetainer {
|
||||
|
||||
void RemoveSocketAddress(const std::shared_ptr<SocketAddress>& address);
|
||||
|
||||
void AddSocketAddressRange(
|
||||
const std::shared_ptr<SocketAddress>& start,
|
||||
const std::shared_ptr<SocketAddress>& end);
|
||||
void AddSocketAddressRange(const std::shared_ptr<SocketAddress>& start,
|
||||
const std::shared_ptr<SocketAddress>& end);
|
||||
|
||||
void AddSocketAddressMask(
|
||||
const std::shared_ptr<SocketAddress>& address,
|
||||
int prefix);
|
||||
void AddSocketAddressMask(const std::shared_ptr<SocketAddress>& address,
|
||||
int prefix);
|
||||
|
||||
bool Apply(const std::shared_ptr<SocketAddress>& address);
|
||||
|
||||
@@ -282,9 +286,8 @@ class SocketAddressBlockList : public MemoryRetainer {
|
||||
std::shared_ptr<SocketAddress> start;
|
||||
std::shared_ptr<SocketAddress> end;
|
||||
|
||||
SocketAddressRangeRule(
|
||||
const std::shared_ptr<SocketAddress>& start,
|
||||
const std::shared_ptr<SocketAddress>& end);
|
||||
SocketAddressRangeRule(const std::shared_ptr<SocketAddress>& start,
|
||||
const std::shared_ptr<SocketAddress>& end);
|
||||
|
||||
bool Apply(const std::shared_ptr<SocketAddress>& address) override;
|
||||
std::string ToString() override;
|
||||
@@ -298,9 +301,8 @@ class SocketAddressBlockList : public MemoryRetainer {
|
||||
std::shared_ptr<SocketAddress> network;
|
||||
int prefix;
|
||||
|
||||
SocketAddressMaskRule(
|
||||
const std::shared_ptr<SocketAddress>& address,
|
||||
int prefix);
|
||||
SocketAddressMaskRule(const std::shared_ptr<SocketAddress>& address,
|
||||
int prefix);
|
||||
|
||||
bool Apply(const std::shared_ptr<SocketAddress>& address) override;
|
||||
std::string ToString() override;
|
||||
@@ -336,8 +338,7 @@ class SocketAddressBlockListWrap : public BaseObject {
|
||||
|
||||
static BaseObjectPtr<SocketAddressBlockListWrap> New(Environment* env);
|
||||
static BaseObjectPtr<SocketAddressBlockListWrap> New(
|
||||
Environment* env,
|
||||
std::shared_ptr<SocketAddressBlockList> blocklist);
|
||||
Environment* env, std::shared_ptr<SocketAddressBlockList> blocklist);
|
||||
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void AddAddress(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
@@ -346,11 +347,10 @@ class SocketAddressBlockListWrap : public BaseObject {
|
||||
static void Check(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void GetRules(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
SocketAddressBlockListWrap(
|
||||
Environment* env,
|
||||
v8::Local<v8::Object> wrap,
|
||||
std::shared_ptr<SocketAddressBlockList> blocklist =
|
||||
std::make_shared<SocketAddressBlockList>());
|
||||
SocketAddressBlockListWrap(Environment* env,
|
||||
v8::Local<v8::Object> wrap,
|
||||
std::shared_ptr<SocketAddressBlockList> blocklist =
|
||||
std::make_shared<SocketAddressBlockList>());
|
||||
|
||||
void MemoryInfo(node::MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap)
|
||||
|
||||
@@ -460,6 +460,15 @@ void ConstructSharedArrayBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(sab);
|
||||
}
|
||||
|
||||
// Marks a promise as handled and silent to prevent unhandled rejection
|
||||
// tracking from triggering.
|
||||
void MarkPromiseAsHandled(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK(args[0]->IsPromise());
|
||||
Local<Promise> promise = args[0].As<Promise>();
|
||||
promise->MarkAsHandled();
|
||||
promise->MarkAsSilent();
|
||||
}
|
||||
|
||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(GetPromiseDetails);
|
||||
registry->Register(GetProxyDetails);
|
||||
@@ -478,6 +487,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(DefineLazyProperties);
|
||||
registry->Register(DefineLazyPropertiesGetter);
|
||||
registry->Register(ConstructSharedArrayBuffer);
|
||||
registry->Register(MarkPromiseAsHandled);
|
||||
}
|
||||
|
||||
void Initialize(Local<Object> target,
|
||||
@@ -583,6 +593,7 @@ void Initialize(Local<Object> target,
|
||||
target,
|
||||
"constructSharedArrayBuffer",
|
||||
ConstructSharedArrayBuffer);
|
||||
SetMethod(context, target, "markPromiseAsHandled", MarkPromiseAsHandled);
|
||||
|
||||
Local<String> should_abort_on_uncaught_toggle =
|
||||
FIXED_ONE_BYTE_STRING(env->isolate(), "shouldAbortOnUncaughtToggle");
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
# Node.js QUIC Implementation (`src/quic/`)
|
||||
|
||||
This directory contains the C++ implementation of the Node.js experimental QUIC
|
||||
support (`--experimental-quic`). The implementation builds on three external
|
||||
libraries: **ngtcp2** (QUIC transport), **nghttp3** (HTTP/3 framing), and
|
||||
**OpenSSL** (TLS 1.3).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The stack is layered as:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ JavaScript API (lib/internal/quic/) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Endpoint — UDP socket, packet I/O │
|
||||
│ Session — QUIC connection (ngtcp2) │
|
||||
│ Application — ALPN protocol logic │
|
||||
│ Stream — Bidirectional data flow │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ngtcp2 / nghttp3 / OpenSSL │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ libuv — UDP, timers, thread pool │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
An **Endpoint** binds a UDP socket and dispatches incoming packets to
|
||||
**Sessions**. Each Session wraps an `ngtcp2_conn` and delegates
|
||||
protocol-specific behavior to an **Application** (selected by ALPN
|
||||
negotiation). Sessions contain **Streams** — bidirectional or unidirectional
|
||||
data channels that carry application data.
|
||||
|
||||
## File Map
|
||||
|
||||
### Foundation
|
||||
|
||||
| File | Purpose |
|
||||
| ------------- | ------------------------------------------------------------------ |
|
||||
| `guard.h` | OpenSSL QUIC guard macro |
|
||||
| `defs.h` | Core enums, typedefs, constants, macros |
|
||||
| `arena.h` | Block-based arena allocator (header-only template) |
|
||||
| `data.h/cc` | `Path`, `PathStorage`, `Store`, `QuicError` |
|
||||
| `cid.h/cc` | `CID` — Connection ID with hash, factory, map alias |
|
||||
| `tokens.h/cc` | `TokenSecret`, `StatelessResetToken`, `RetryToken`, `RegularToken` |
|
||||
|
||||
### Security
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------- |
|
||||
| `tlscontext.h/cc` | `TLSContext`, `TLSSession` — OpenSSL integration, SNI, ALPN |
|
||||
| `sessionticket.h/cc` | `SessionTicket` — TLS 1.3 session resumption and 0-RTT |
|
||||
|
||||
### Core
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| `endpoint.h/cc` | `Endpoint` — UDP binding, packet dispatch, retry/validation |
|
||||
| `session.h/cc` | `Session` — QUIC connection state machine (\~3,500 lines) |
|
||||
| `streams.h/cc` | `Stream`, `Outbound`, `PendingStream` — data flow |
|
||||
| `application.h/cc` | `Session::Application` base + `DefaultApplication` |
|
||||
| `http3.h/cc` | `Http3ApplicationImpl` — nghttp3 integration (\~1,400 lines) |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| `bindingdata.h/cc` | `BindingData` — JS binding state, callback scopes, allocators |
|
||||
| `session_manager.h/cc` | `SessionManager` — per-Realm CID→Session routing |
|
||||
| `transportparams.h/cc` | `TransportParams` — QUIC transport parameter encoding |
|
||||
| `packet.h/cc` | `Packet` — arena-allocated outbound packets |
|
||||
| `preferredaddress.h/cc` | `PreferredAddress` — server preferred address helper |
|
||||
| `quic.cc` | Module entry point (binding registration) |
|
||||
|
||||
## Key Design Patterns
|
||||
|
||||
### SendPendingDataScope (RAII Send Coalescing)
|
||||
|
||||
Every entry point that may generate outbound data creates a
|
||||
`SendPendingDataScope`. Scopes nest — an internal depth counter ensures
|
||||
`Application::SendPendingData()` is called exactly once, when the outermost
|
||||
scope exits:
|
||||
|
||||
```cpp
|
||||
{
|
||||
SendPendingDataScope outer(session); // depth 1
|
||||
{
|
||||
SendPendingDataScope inner(session); // depth 2
|
||||
// ... generate data ...
|
||||
} // depth 1 — no send yet
|
||||
} // depth 0 — SendPendingData() fires
|
||||
```
|
||||
|
||||
This is used in `Session::Receive`, `Endpoint::Connect`, `Session::Close`,
|
||||
`Session::ResumeStream`, and all stream write operations.
|
||||
|
||||
### NgTcp2CallbackScope / NgHttp3CallbackScope
|
||||
|
||||
Per-session RAII guards that prevent re-entrant calls into ngtcp2/nghttp3.
|
||||
While active, `can_send_packets()` returns false, blocking the send loop.
|
||||
If `Destroy()` is called during a callback (e.g., via JS `MakeCallback`),
|
||||
destruction is deferred until the scope exits, preventing use-after-free.
|
||||
|
||||
### Bob Protocol (Pull-Based Streaming)
|
||||
|
||||
Data flows through the stack using the **bob** (Bytes-Over-Buffers) pull
|
||||
protocol defined in `src/node_bob.h`. The consumer calls `Pull()` on a
|
||||
source, which responds with one of four status codes:
|
||||
|
||||
| Status | Meaning |
|
||||
| --------------------- | ---------------------------------------------------------------- |
|
||||
| `STATUS_EOS` (0) | End of stream — no more data |
|
||||
| `STATUS_CONTINUE` (1) | Data delivered; pull again |
|
||||
| `STATUS_BLOCK` (2) | No data now; try later |
|
||||
| `STATUS_WAIT` (3) | Async — source will invoke the `next` callback when data arrives |
|
||||
|
||||
The `Done` callback passed with each pull signals that the consumer is
|
||||
finished with the buffer memory, enabling zero-copy transfer.
|
||||
|
||||
### Three-Phase Buffer Lifecycle
|
||||
|
||||
Data in `Stream::Outbound` moves through three states:
|
||||
|
||||
```text
|
||||
Pulled (uncommitted) → Committed (in-flight) → Acknowledged (freed)
|
||||
```
|
||||
|
||||
* **Uncommitted**: Read from the DataQueue but not yet accepted by ngtcp2
|
||||
* **Committed**: Accepted into a QUIC packet by `ngtcp2_conn_writev_stream`
|
||||
* **Acknowledged**: Peer ACKed the data; buffer memory is released
|
||||
|
||||
Separate cursors on each buffer entry track progression. This allows ngtcp2
|
||||
to retry uncommitted data (e.g., after pacing/congestion clears) without
|
||||
re-reading from the source.
|
||||
|
||||
### Application Abstraction
|
||||
|
||||
`Session::Application` is a virtual interface that the Session delegates
|
||||
ALPN-specific behavior to. Two implementations exist:
|
||||
|
||||
* **`DefaultApplication`** (`application.cc`): Used for non-HTTP/3 ALPN
|
||||
protocols. Maintains its own stream scheduling queue. Streams are scheduled
|
||||
via an intrusive linked list.
|
||||
|
||||
* **`Http3ApplicationImpl`** (`http3.cc`): Used when ALPN negotiates `h3`.
|
||||
Wraps `nghttp3_conn` for HTTP/3 framing, header compression (QPACK),
|
||||
server push, and stream prioritization. Manages unidirectional control
|
||||
streams internally.
|
||||
|
||||
The Application is selected during ALPN negotiation — immediately for
|
||||
clients (ALPN known upfront), during the `OnSelectAlpn` TLS callback for
|
||||
servers.
|
||||
|
||||
### Thread-Local Allocator
|
||||
|
||||
Both ngtcp2 and nghttp3 require custom allocators (`ngtcp2_mem`,
|
||||
`nghttp3_mem`). These allocator structs must outlive every object they
|
||||
create. Some nghttp3 objects (notably `rcbuf`s backing V8 external strings)
|
||||
can survive past `BindingData` destruction during isolate teardown.
|
||||
|
||||
The solution uses `thread_local` storage:
|
||||
|
||||
```cpp
|
||||
struct QuicAllocState {
|
||||
BindingData* binding = nullptr; // Nulled in ~BindingData
|
||||
ngtcp2_mem ngtcp2;
|
||||
nghttp3_mem nghttp3;
|
||||
};
|
||||
thread_local QuicAllocState quic_alloc_state;
|
||||
```
|
||||
|
||||
Each allocation prepends its size before the returned pointer. This allows
|
||||
`free` and `realloc` to report correct sizes for memory tracking. When
|
||||
`binding` is null (after `BindingData` destruction), allocations still
|
||||
succeed but memory tracking is silently skipped.
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
**Client**: `Endpoint::Connect()` builds a `Session::Config` with
|
||||
`Side::CLIENT`, creates a `TLSContext`, and calls `Session::Create()` →
|
||||
`ngtcp2_conn_client_new()`. The Application is selected immediately.
|
||||
|
||||
**Server**: `Endpoint::Receive()` processes an Initial packet through
|
||||
address validation (retry tokens, LRU cache), then calls `Session::Create()`
|
||||
→ `ngtcp2_conn_server_new()`. The Application is selected later, during ALPN
|
||||
negotiation in the TLS handshake.
|
||||
|
||||
### The Receive Path
|
||||
|
||||
```text
|
||||
uv_udp_recv_cb
|
||||
→ Endpoint::Receive()
|
||||
→ FindSession(dcid) // CID lookup across endpoints
|
||||
├── Found → Session::Receive()
|
||||
└── Not found:
|
||||
├── Stateless reset? → process
|
||||
├── Short header? → SendStatelessReset()
|
||||
└── Long header? → acceptInitialPacket()
|
||||
├── ngtcp2_accept()
|
||||
├── Address validation (retry tokens, LRU)
|
||||
└── Session::Create()
|
||||
|
||||
Session::Receive()
|
||||
→ SendPendingDataScope // will send after processing
|
||||
→ NgTcp2CallbackScope // re-entrancy guard
|
||||
→ ngtcp2_conn_read_pkt() // decrypt, process frames
|
||||
triggers callbacks:
|
||||
├── recv_stream_data → Application::ReceiveStreamData()
|
||||
├── stream_open → Application::ReceiveStreamOpen()
|
||||
├── acked_stream_data → Application::AcknowledgeStreamData()
|
||||
├── handshake_completed → Session::HandshakeCompleted()
|
||||
└── ... others
|
||||
→ Application::PostReceive() // deferred operations (e.g., GOAWAY)
|
||||
```
|
||||
|
||||
### The Send Loop
|
||||
|
||||
```text
|
||||
SendPendingDataScope::~SendPendingDataScope()
|
||||
→ Application::SendPendingData()
|
||||
Loop (up to max_packet_count):
|
||||
├── GetStreamData() // pull data from next stream
|
||||
│ └── stream->Pull() // bob pull from Outbound→DataQueue
|
||||
├── WriteVStream() // ngtcp2_conn_writev_stream()
|
||||
│ encrypts, frames, paces
|
||||
├── if ndatalen > 0: StreamCommit()
|
||||
│ stream->Commit(datalen, fin)
|
||||
├── if nwrite > 0: Send() // uv_udp_send()
|
||||
├── if WRITE_MORE: continue // room for more in this packet
|
||||
├── if STREAM_DATA_BLOCKED: // flow control
|
||||
│ StreamDataBlocked(), continue
|
||||
└── if nwrite == 0: // pacing/congestion
|
||||
ResumeStream() if data pending, return
|
||||
On exit: UpdateTimer(), UpdateDataStats()
|
||||
```
|
||||
|
||||
When `nwrite == 0` and the stream had unsent data (payload or FIN), the
|
||||
stream is re-scheduled via `Application::ResumeStream()` so the next
|
||||
timer-triggered `SendPendingData` retries it.
|
||||
|
||||
### Close Methods
|
||||
|
||||
| Method | Behavior |
|
||||
| ------------ | ----------------------------------------------------------- |
|
||||
| **DEFAULT** | Destroys all streams, sends CONNECTION\_CLOSE, emits to JS |
|
||||
| **SILENT** | Same but skips CONNECTION\_CLOSE (errors, stateless resets) |
|
||||
| **GRACEFUL** | Sends GOAWAY (H3), waits for streams to close naturally |
|
||||
|
||||
### Timer
|
||||
|
||||
`Session::UpdateTimer()` queries `ngtcp2_conn_get_expiry()` and sets a libuv
|
||||
timer. When it fires, `OnTimeout()` calls `ngtcp2_conn_handle_expiry()` then
|
||||
`SendPendingData()` to retransmit lost packets, send PINGs, or retry
|
||||
pacing-blocked sends.
|
||||
|
||||
## Stream Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
**Local streams**: `Session::OpenStream()` calls
|
||||
`ngtcp2_conn_open_bidi_stream()` or `ngtcp2_conn_open_uni_stream()`. If the
|
||||
handshake is incomplete or the concurrency limit is reached, the stream is
|
||||
created in **pending** state and queued. When the peer grants capacity
|
||||
(`ExtendMaxStreams`), pending streams are fulfilled with real stream IDs.
|
||||
|
||||
**Remote streams**: ngtcp2 notifies via callbacks. The Application creates a
|
||||
`Stream` object and emits it to JavaScript.
|
||||
|
||||
### Outbound Data Flow
|
||||
|
||||
The `Stream::Outbound` class bridges a `DataQueue` (the data source) to
|
||||
ngtcp2's packet-writing loop. A `DataQueue::Reader` provides the bob
|
||||
pull interface.
|
||||
|
||||
Supported body source types (via `GetDataQueueFromSource`):
|
||||
|
||||
| Source | Strategy |
|
||||
| ------------------- | -------------------------------------------- |
|
||||
| `ArrayBuffer` | Zero-copy detach, or copy if non-detachable |
|
||||
| `SharedArrayBuffer` | Always copy |
|
||||
| `ArrayBufferView` | Zero-copy detach of underlying buffer |
|
||||
| `Blob` | Slice of Blob's existing DataQueue |
|
||||
| `String` | UTF-8 encode into BackingStore |
|
||||
| `FileHandle` | `FdEntry` — async file reads via thread pool |
|
||||
|
||||
For `FileHandle` bodies, the `FdEntry::ReaderImpl` dispatches `uv_fs_read`
|
||||
to the libuv thread pool and returns `STATUS_WAIT`. When the read completes,
|
||||
the callback appends data to the Outbound buffer and calls
|
||||
`session().ResumeStream(id)` to re-enter the send loop.
|
||||
|
||||
### Inbound Data Flow
|
||||
|
||||
Received stream data is delivered by ngtcp2 via
|
||||
`Application::ReceiveStreamData()`, which calls `stream->ReceiveData()`.
|
||||
Data is appended to the stream's inbound `DataQueue`. The JavaScript side
|
||||
consumes this via an async iterator (the `stream/iter` `bytes()` helper).
|
||||
The stream implements `DataQueue::BackpressureListener` to extend the
|
||||
QUIC flow control window as data is consumed.
|
||||
|
||||
### Streaming Mode (Writer API)
|
||||
|
||||
When no body is provided at stream creation, the JavaScript `stream.writer`
|
||||
API uses streaming mode. The Outbound creates a non-idempotent DataQueue.
|
||||
Each `writeSync()` / `write()` call appends an in-memory entry. The
|
||||
`endSync()` / `end()` call caps the queue, signaling EOS to the send loop.
|
||||
|
||||
## SessionManager
|
||||
|
||||
The `SessionManager` is a per-Realm singleton that owns the authoritative
|
||||
CID→Session mapping. It enables:
|
||||
|
||||
* **Cross-endpoint routing**: A session's CIDs are registered globally so
|
||||
packets arriving on any endpoint find the right session.
|
||||
* **Connection migration**: When a session migrates to a new path, the
|
||||
SessionManager updates the routing without requiring endpoint-specific
|
||||
knowledge.
|
||||
* **Stateless reset token mapping**: Maps reset tokens to sessions for
|
||||
detecting stateless resets on any endpoint.
|
||||
|
||||
CID lookup uses a three-tier strategy:
|
||||
|
||||
1. Direct SCID match in `SessionManager::sessions_`
|
||||
2. Cross-endpoint DCID→SCID in `SessionManager::dcid_to_scid_`
|
||||
3. Per-endpoint DCID→SCID in `Endpoint::dcid_to_scid_` (peer-chosen CIDs)
|
||||
|
||||
## Address Validation
|
||||
|
||||
The endpoint uses an LRU cache to track validated remote addresses. For
|
||||
unvalidated addresses:
|
||||
|
||||
1. If no token is present, a **Retry** packet is sent with a cryptographic
|
||||
token (HKDF-derived, time-limited).
|
||||
2. The client retransmits the Initial with the retry token.
|
||||
3. The token is validated; the session is created with the original DCID
|
||||
preserved for transport parameter verification.
|
||||
|
||||
Regular tokens (from `NEW_TOKEN` frames) follow the same validation path
|
||||
but without the retry handshake. The LRU cache allows subsequent
|
||||
connections from the same address to skip validation entirely.
|
||||
|
||||
## HTTP/3 Application (`http3.cc`)
|
||||
|
||||
The `Http3ApplicationImpl` wraps `nghttp3_conn` and handles:
|
||||
|
||||
* **Header compression**: QPACK encoding/decoding via nghttp3's internal
|
||||
encoder/decoder streams (unidirectional).
|
||||
* **Stream management**: Only bidirectional streams are exposed to
|
||||
JavaScript. Unidirectional control, encoder, and decoder streams are
|
||||
managed internally.
|
||||
* **FIN management**: `stream_fin_managed_by_application()` returns true.
|
||||
nghttp3 controls when FIN is sent based on HTTP/3 framing (DATA frames,
|
||||
trailing HEADERS). The `EndWriting()` notification from JavaScript is
|
||||
forwarded to `nghttp3_conn_shutdown_stream_write()`.
|
||||
* **Data read callback**: `on_read_data_callback` pulls data from the
|
||||
stream's Outbound during `nghttp3_conn_writev_stream`. Bytes must be
|
||||
committed inside the callback (before `StreamCommit`) because QPACK can
|
||||
cause re-entrant `read_data` calls.
|
||||
* **GOAWAY**: `BeginShutdown()` sends a GOAWAY frame. The goaway ID is
|
||||
deferred to `PostReceive()` (outside callback scopes) so it can safely
|
||||
invoke JavaScript.
|
||||
* **Settings**: HTTP/3 SETTINGS (max field section size, QPACK capacities,
|
||||
CONNECT protocol, datagrams) are negotiated and enforced. Datagram
|
||||
support follows RFC 9297 — when the peer's SETTINGS disable datagrams,
|
||||
`sendDatagram()` is blocked.
|
||||
* **0-RTT**: Early data settings are validated during ticket extraction
|
||||
(`ValidateTicketData` in `ExtractSessionTicketAppData`). If the server's
|
||||
settings changed incompatibly, the ticket is rejected before TLS accepts
|
||||
it.
|
||||
|
||||
## Error Handling
|
||||
|
||||
`QuicError` (`data.h`) encapsulates QUIC error codes with a type namespace
|
||||
(transport, application, version negotiation, idle close). Factory methods
|
||||
wrap ngtcp2 error codes, TLS alerts, and application errors.
|
||||
|
||||
On the JavaScript side, `convertQuicError()` transforms the C++ error
|
||||
representation into `ERR_QUIC_TRANSPORT_ERROR` or
|
||||
`ERR_QUIC_APPLICATION_ERROR` objects. Clean closes (transport NO\_ERROR,
|
||||
H3 NO\_ERROR, or idle close) resolve `stream.closed`; all other errors
|
||||
reject it.
|
||||
|
||||
## Packet Allocation
|
||||
|
||||
Outbound packets are allocated from an `ArenaPool<Packet>` owned by the
|
||||
Endpoint. The arena provides O(1) allocation from contiguous memory blocks
|
||||
(128 slots per block), avoiding per-packet heap allocation and V8 object
|
||||
overhead. Packets are returned to the pool when the UDP send completes
|
||||
(via the `Packet::Listener::PacketDone` callback).
|
||||
|
||||
## Debug Logging
|
||||
|
||||
Use the `NODE_DEBUG_NATIVE` environment variable to enable detailed debug
|
||||
logging:
|
||||
|
||||
* `QUIC` - general QUIC events (sessions, streams, packets)
|
||||
* `NGTCP2` - ngtcp2 callback events and error codes
|
||||
* `NGHTTP3` - nghttp3 callback events and error codes
|
||||
|
||||
```bash
|
||||
NODE_DEBUG_NATIVE=QUIC,NGTCP2,NGHTTP3 node --experimental-quic ...
|
||||
```
|
||||
|
||||
The debug output will be printed to stderr and can be extremely verbose.
|
||||
|
||||
Used in combination with `qlog` and `keylog` options when creating a
|
||||
`QuicSession`, this can help significantly with debugging and understanding
|
||||
QUIC behavior and identifying bugs / performance issues in the implementation.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
| Library | Role | Location |
|
||||
| ------- | --------------------------------------- | ------------------------- |
|
||||
| ngtcp2 | QUIC transport protocol | `deps/ngtcp2/ngtcp2/` |
|
||||
| nghttp3 | HTTP/3 framing, QPACK | `deps/ngtcp2/nghttp3/` |
|
||||
| OpenSSL | TLS 1.3 handshake, encryption | system or `deps/openssl/` |
|
||||
| libuv | UDP sockets, timers, thread pool | `deps/uv/` |
|
||||
| V8 | JavaScript engine, GC, external strings | `deps/v8/` |
|
||||
+295
-115
@@ -1,3 +1,4 @@
|
||||
#include "util.h"
|
||||
#if HAVE_OPENSSL && HAVE_QUIC
|
||||
#include "guard.h"
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
@@ -34,6 +35,8 @@ const Session::Application_Options Session::Application_Options::kDefault = {};
|
||||
Session::Application_Options::operator const nghttp3_settings() const {
|
||||
// In theory, Application::Options might contain options for more than just
|
||||
// HTTP/3. Here we extract only the properties that are relevant to HTTP/3.
|
||||
// Later if we add more application types we can add more properties or
|
||||
// divide this up into multiple option structs.
|
||||
return nghttp3_settings{
|
||||
.max_field_section_size = max_field_section_size,
|
||||
.qpack_max_dtable_capacity =
|
||||
@@ -43,11 +46,13 @@ Session::Application_Options::operator const nghttp3_settings() const {
|
||||
.qpack_blocked_streams = static_cast<size_t>(qpack_blocked_streams),
|
||||
.enable_connect_protocol = enable_connect_protocol,
|
||||
.h3_datagram = enable_datagrams,
|
||||
// TODO(@jasnell): Support origin frames?
|
||||
// origin_list is nullptr here because it is set directly on the
|
||||
// nghttp3_settings in Http3ApplicationImpl::InitializeConnection()
|
||||
// from the SNI configuration.
|
||||
.origin_list = nullptr,
|
||||
.glitch_ratelim_burst = 1000,
|
||||
.glitch_ratelim_rate = 33,
|
||||
.qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_NONE,
|
||||
.qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_EAGER,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,53 +144,21 @@ bool Session::Application::Start() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Session::Application::AcknowledgeStreamData(int64_t stream_id,
|
||||
size_t datalen) {
|
||||
if (auto stream = session().FindStream(stream_id)) [[likely]] {
|
||||
bool Session::Application::AcknowledgeStreamData(stream_id id, size_t datalen) {
|
||||
if (auto stream = session().FindStream(id)) [[likely]] {
|
||||
stream->Acknowledge(datalen);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Session::Application::BlockStream(int64_t id) {
|
||||
// By default do nothing.
|
||||
}
|
||||
|
||||
bool Session::Application::CanAddHeader(size_t current_count,
|
||||
size_t current_headers_length,
|
||||
size_t this_header_length) {
|
||||
// By default headers are not supported.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Session::Application::SendHeaders(const Stream& stream,
|
||||
HeadersKind kind,
|
||||
const Local<v8::Array>& headers,
|
||||
HeadersFlags flags) {
|
||||
// By default do nothing.
|
||||
return false;
|
||||
}
|
||||
|
||||
void Session::Application::ResumeStream(int64_t id) {
|
||||
// By default do nothing.
|
||||
}
|
||||
|
||||
void Session::Application::ExtendMaxStreams(EndpointLabel label,
|
||||
Direction direction,
|
||||
uint64_t max_streams) {
|
||||
// By default do nothing.
|
||||
}
|
||||
|
||||
void Session::Application::ExtendMaxStreamData(Stream* stream,
|
||||
uint64_t max_data) {
|
||||
Debug(session_, "Application extending max stream data");
|
||||
// By default do nothing.
|
||||
// Returning true even when the stream is not found is intentional.
|
||||
// After a stream is destroyed, the peer can still ACK data that was
|
||||
// previously sent. This is benign and should not be treated as an error.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Session::Application::CollectSessionTicketAppData(
|
||||
SessionTicket::AppData* app_data) const {
|
||||
// By default do nothing.
|
||||
// By default, write just the application type byte.
|
||||
uint8_t buf[1] = {static_cast<uint8_t>(type())};
|
||||
app_data->Set(uv_buf_init(reinterpret_cast<char*>(buf), 1));
|
||||
}
|
||||
|
||||
SessionTicket::AppData::Status
|
||||
@@ -197,14 +170,39 @@ Session::Application::ExtractSessionTicketAppData(
|
||||
: SessionTicket::AppData::Status::TICKET_USE;
|
||||
}
|
||||
|
||||
void Session::Application::SetStreamPriority(const Stream& stream,
|
||||
StreamPriority priority,
|
||||
StreamPriorityFlags flags) {
|
||||
// By default do nothing.
|
||||
std::optional<PendingTicketAppData> Session::Application::ParseTicketData(
|
||||
const uv_buf_t& data) {
|
||||
if (data.len == 0 || data.base == nullptr) return std::nullopt;
|
||||
auto app_type =
|
||||
static_cast<Type>(reinterpret_cast<const uint8_t*>(data.base)[0]);
|
||||
switch (app_type) {
|
||||
case Type::DEFAULT:
|
||||
return DefaultTicketData{};
|
||||
case Type::HTTP3:
|
||||
return ParseHttp3TicketData(data);
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
StreamPriority Session::Application::GetStreamPriority(const Stream& stream) {
|
||||
return StreamPriority::DEFAULT;
|
||||
bool Session::Application::ValidateTicketData(
|
||||
const PendingTicketAppData& data, const Application_Options& options) {
|
||||
if (std::holds_alternative<Http3TicketData>(data)) {
|
||||
// TODO(@jasnell): This validation probably belongs in http3.cc but keeping
|
||||
// it here for now.
|
||||
const auto& ticket = std::get<Http3TicketData>(data);
|
||||
return options.max_field_section_size >= ticket.max_field_section_size &&
|
||||
options.qpack_max_dtable_capacity >=
|
||||
ticket.qpack_max_dtable_capacity &&
|
||||
options.qpack_encoder_max_dtable_capacity >=
|
||||
ticket.qpack_encoder_max_dtable_capacity &&
|
||||
options.qpack_blocked_streams >= ticket.qpack_blocked_streams &&
|
||||
(!ticket.enable_connect_protocol ||
|
||||
options.enable_connect_protocol) &&
|
||||
(!ticket.enable_datagrams || options.enable_datagrams);
|
||||
}
|
||||
// DefaultTicketData always validates.
|
||||
return true;
|
||||
}
|
||||
|
||||
Packet::Ptr Session::Application::CreateStreamDataPacket() {
|
||||
@@ -212,23 +210,120 @@ Packet::Ptr Session::Application::CreateStreamDataPacket() {
|
||||
session_->remote_address(), session_->max_packet_size(), "stream data");
|
||||
}
|
||||
|
||||
void Session::Application::StreamClose(Stream* stream, QuicError&& error) {
|
||||
void Session::Application::ReceiveStreamClose(Stream* stream,
|
||||
QuicError&& error) {
|
||||
DCHECK_NOT_NULL(stream);
|
||||
stream->Destroy(std::move(error));
|
||||
}
|
||||
|
||||
void Session::Application::StreamStopSending(Stream* stream,
|
||||
QuicError&& error) {
|
||||
void Session::Application::ReceiveStreamStopSending(Stream* stream,
|
||||
QuicError&& error) {
|
||||
DCHECK_NOT_NULL(stream);
|
||||
stream->ReceiveStopSending(std::move(error));
|
||||
}
|
||||
|
||||
void Session::Application::StreamReset(Stream* stream,
|
||||
uint64_t final_size,
|
||||
QuicError&& error) {
|
||||
void Session::Application::ReceiveStreamReset(Stream* stream,
|
||||
uint64_t final_size,
|
||||
QuicError&& error) {
|
||||
stream->ReceiveStreamReset(final_size, std::move(error));
|
||||
}
|
||||
|
||||
// Attempts to pack a pending datagram into the current packet.
|
||||
// Returns the nwrite value from ngtcp2_conn_writev_datagram.
|
||||
// On fatal error, closes the session and returns the error code.
|
||||
// The caller should check:
|
||||
// > 0: packet is complete, send it (pos was NOT advanced — caller
|
||||
// must add nwrite to pos and send)
|
||||
// NGTCP2_ERR_WRITE_MORE: datagram packed, room for more
|
||||
// 0: congestion controlled or doesn't fit, datagram stays in queue
|
||||
// < 0 (other): fatal error, session already closed
|
||||
ssize_t Session::Application::TryWritePendingDatagram(PathStorage* path,
|
||||
uint8_t* dest,
|
||||
size_t destlen) {
|
||||
CHECK(session_->HasPendingDatagrams());
|
||||
auto max_attempts = session_->config().options.max_datagram_send_attempts;
|
||||
|
||||
// Skip datagrams that have already exceeded the send attempt limit
|
||||
// from a previous SendPendingData cycle.
|
||||
while (session_->HasPendingDatagrams()) {
|
||||
auto& front = session_->PeekPendingDatagram();
|
||||
if (front.send_attempts < max_attempts) break;
|
||||
Debug(session_,
|
||||
"Datagram %" PRIu64 " abandoned after %u attempts",
|
||||
front.id,
|
||||
front.send_attempts);
|
||||
session_->DatagramStatus(front.id, DatagramStatus::ABANDONED);
|
||||
session_->PopPendingDatagram();
|
||||
}
|
||||
|
||||
if (!session_->HasPendingDatagrams()) return 0;
|
||||
auto& dg = session_->PeekPendingDatagram();
|
||||
ngtcp2_vec dgvec = dg.data;
|
||||
int accepted = 0;
|
||||
int dg_flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE;
|
||||
|
||||
ssize_t dg_nwrite = ngtcp2_conn_writev_datagram(*session_,
|
||||
&path->path,
|
||||
nullptr,
|
||||
dest,
|
||||
destlen,
|
||||
&accepted,
|
||||
dg_flags,
|
||||
dg.id,
|
||||
&dgvec,
|
||||
1,
|
||||
uv_hrtime());
|
||||
|
||||
if (accepted) {
|
||||
// Nice, the datagram was accepted!
|
||||
Debug(session_, "Datagram %" PRIu64 " accepted into packet", dg.id);
|
||||
session_->DatagramSent(dg.id);
|
||||
session_->PopPendingDatagram();
|
||||
} else {
|
||||
Debug(session_, "Datagram %" PRIu64 " not accepted into packet", dg.id);
|
||||
}
|
||||
|
||||
switch (dg_nwrite) {
|
||||
case 0: {
|
||||
// If dg_nwrite is 0, we are either congestion controlled or
|
||||
// there wasn't enough room in the packet for the datagram or
|
||||
// we aren't in a state where we can send.
|
||||
// We'll skip this attempt and return 0.
|
||||
CHECK(!accepted);
|
||||
dg.send_attempts++;
|
||||
return 0;
|
||||
}
|
||||
case NGTCP2_ERR_WRITE_MORE: {
|
||||
// There's still room left in the packet!
|
||||
return NGTCP2_ERR_WRITE_MORE;
|
||||
}
|
||||
case NGTCP2_ERR_INVALID_STATE:
|
||||
case NGTCP2_ERR_INVALID_ARGUMENT: {
|
||||
// Non-fatal error cases. Peer either does not support datagrams
|
||||
// or the datagram is too large for the peer's max.
|
||||
// Abandon the datagram and signal skip by returning std::nullopt.
|
||||
session_->DatagramStatus(dg.id, DatagramStatus::ABANDONED);
|
||||
session_->PopPendingDatagram();
|
||||
return 0;
|
||||
}
|
||||
default: {
|
||||
// Fatal errors: PKT_NUM_EXHAUSTED, CALLBACK_FAILURE, NOMEM, etc.
|
||||
Debug(session_, "Fatal datagram error: %zd", dg_nwrite);
|
||||
session_->SetLastError(QuicError::ForNgtcp2Error(dg_nwrite));
|
||||
session_->Close(CloseMethod::SILENT);
|
||||
return dg_nwrite;
|
||||
}
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
// the SendPendingData method is the primary driver for sending data from the
|
||||
// application layer. It loops through available stream data and pending
|
||||
// datagrams and generates packets to send until there is either no more
|
||||
// data to send or we hit the maximum number of packets to send in one go.
|
||||
// This method is extremely delicate. A bug in this method can break the
|
||||
// entire QUIC implementation; so be very careful when making changes here
|
||||
// and make sure to test thoroughly. When in doubt... don't change it.
|
||||
void Session::Application::SendPendingData() {
|
||||
DCHECK(!session().is_destroyed());
|
||||
if (!session().can_send_packets()) [[unlikely]] {
|
||||
@@ -262,19 +357,14 @@ void Session::Application::SendPendingData() {
|
||||
size_t packet_send_count = 0;
|
||||
|
||||
Packet::Ptr packet;
|
||||
uint8_t* pos = nullptr;
|
||||
uint8_t* begin = nullptr;
|
||||
|
||||
auto ensure_packet = [&] {
|
||||
if (!packet) {
|
||||
packet = CreateStreamDataPacket();
|
||||
if (!packet) [[unlikely]]
|
||||
return false;
|
||||
pos = begin = packet->data();
|
||||
}
|
||||
DCHECK(packet);
|
||||
DCHECK_NOT_NULL(pos);
|
||||
DCHECK_NOT_NULL(begin);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -302,21 +392,38 @@ void Session::Application::SendPendingData() {
|
||||
}
|
||||
|
||||
// If we got here, we were at least successful in checking for stream data.
|
||||
// There might not be any stream data to send.
|
||||
// There might not be any stream data to send. If there is no stream data,
|
||||
// that's perfectly fine, we still need to serialize any frames we do have
|
||||
// (pings, acks, datagrams, etc) so we'll just keep going.
|
||||
if (stream_data.id >= 0) {
|
||||
Debug(session_, "Application using stream data: %s", stream_data);
|
||||
} else {
|
||||
Debug(session_, "No stream data to send");
|
||||
}
|
||||
if (session_->HasPendingDatagrams()) {
|
||||
Debug(session_, "There are pending datagrams to send");
|
||||
}
|
||||
|
||||
// Awesome, let's write our packet!
|
||||
ssize_t nwrite =
|
||||
WriteVStream(&path, pos, &ndatalen, max_packet_size, stream_data);
|
||||
ssize_t nwrite = WriteVStream(
|
||||
&path, packet->data(), &ndatalen, packet->length(), stream_data);
|
||||
|
||||
// When ndatalen is > 0, that's our indication that stream data was accepted
|
||||
// in to the packet. Yay!
|
||||
if (ndatalen > 0) {
|
||||
Debug(session_,
|
||||
"Application accepted %zu bytes from stream %" PRIi64
|
||||
" into packet",
|
||||
ndatalen,
|
||||
stream_data.id);
|
||||
if (!StreamCommit(&stream_data, ndatalen)) {
|
||||
// Data was accepted into the packet, but for some reason adjusting
|
||||
// the stream's committed data failed. Treat as fatal.
|
||||
Debug(session_, "Failed to commit accepted bytes in stream");
|
||||
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
|
||||
closed = true;
|
||||
return session_->Close(CloseMethod::SILENT);
|
||||
}
|
||||
} else if (stream_data.id >= 0) {
|
||||
Debug(session_,
|
||||
"Application did not accept any bytes from stream %" PRIi64
|
||||
@@ -324,6 +431,23 @@ void Session::Application::SendPendingData() {
|
||||
stream_data.id);
|
||||
}
|
||||
|
||||
// When nwrite is zero, it means we are congestion limited or it is
|
||||
// just not our turn to send something. Re-schedule the stream if it
|
||||
// had unsent data (payload or FIN) so the next timer-triggered
|
||||
// SendPendingData retries it. Without this, a FIN-only send that
|
||||
// hits nwrite=0 is lost forever — the stream already returned EOS
|
||||
// from Pull and won't be re-scheduled by anyone else.
|
||||
// We call Application::ResumeStream directly (not Session::ResumeStream)
|
||||
// to avoid creating a SendPendingDataScope — we're already inside
|
||||
// SendPendingData and re-entering would just hit nwrite=0 again.
|
||||
if (nwrite == 0) {
|
||||
Debug(session_, "Congestion or not our turn to send");
|
||||
if (stream_data.id >= 0 && (stream_data.count > 0 || stream_data.fin)) {
|
||||
ResumeStream(stream_data.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// A negative nwrite value indicates either an error or that there is more
|
||||
// data to write into the packet.
|
||||
if (nwrite < 0) {
|
||||
@@ -344,7 +468,7 @@ void Session::Application::SendPendingData() {
|
||||
case NGTCP2_ERR_STREAM_SHUT_WR: {
|
||||
// Indicates that the writable side of the stream should be closed
|
||||
// locally or the stream is being reset. In either case, we can't send
|
||||
// any stream data!
|
||||
// data for this stream!
|
||||
Debug(session_,
|
||||
"Closing stream %" PRIi64 " for writing",
|
||||
stream_data.id);
|
||||
@@ -357,16 +481,36 @@ void Session::Application::SendPendingData() {
|
||||
if (stream_data.stream) [[likely]] {
|
||||
stream_data.stream->EndWritable();
|
||||
}
|
||||
// Notify the application that the stream's write side is shut
|
||||
// so it stops queuing data. Without this, GetStreamData would
|
||||
// keep returning the same stream and we'd loop forever.
|
||||
StreamWriteShut(stream_data.id);
|
||||
continue;
|
||||
}
|
||||
case NGTCP2_ERR_WRITE_MORE: {
|
||||
if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) {
|
||||
Debug(session_,
|
||||
"Failed to commit stream data while writing packets");
|
||||
session_->SetLastError(
|
||||
QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
|
||||
closed = true;
|
||||
return session_->Close(CloseMethod::SILENT);
|
||||
Debug(session_, "Packet buffer not full, coalesce more data into it");
|
||||
// Room for more in this packet. Try to pack a pending datagram
|
||||
// if there is one. Otherwise just loop around and keep going.
|
||||
if (session_->HasPendingDatagrams()) {
|
||||
auto result = TryWritePendingDatagram(
|
||||
&path, packet->data(), packet->length());
|
||||
// When result is 0, either the datagram was congestion controlled,
|
||||
// didn't fit in the packet, or was abandoned. Skip and continue.
|
||||
|
||||
// When result is > 0, the packet is done and the result is the
|
||||
// completed size of the packet we're sending.
|
||||
if (result > 0) {
|
||||
size_t len = result;
|
||||
Debug(session_, "Sending packet with %zu bytes", len);
|
||||
packet->Truncate(len);
|
||||
session_->Send(std::move(packet), path);
|
||||
if (++packet_send_count == max_packet_count) return;
|
||||
} else if (result < 0) {
|
||||
// Any negative result other than NGTCP2_ERR_WRITE_MORE
|
||||
// at this point is fatal. The session will have been
|
||||
// closed.
|
||||
if (result != NGTCP2_ERR_WRITE_MORE) return;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -390,46 +534,42 @@ void Session::Application::SendPendingData() {
|
||||
session_->SetLastError(QuicError::ForNgtcp2Error(nwrite));
|
||||
closed = true;
|
||||
return session_->Close(CloseMethod::SILENT);
|
||||
} else if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) {
|
||||
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
|
||||
closed = true;
|
||||
return session_->Close(CloseMethod::SILENT);
|
||||
}
|
||||
|
||||
// When nwrite is zero, it means we are congestion limited or it is
|
||||
// just not our turn now to send something. Stop sending packets.
|
||||
if (nwrite == 0) {
|
||||
// If there was stream data selected, we should reschedule it to try
|
||||
// sending again.
|
||||
if (stream_data.id >= 0) ResumeStream(stream_data.id);
|
||||
|
||||
// There might be a partial packet already prepared. If so, send it.
|
||||
size_t datalen = pos - begin;
|
||||
if (datalen) {
|
||||
Debug(session_, "Sending packet with %zu bytes", datalen);
|
||||
packet->Truncate(datalen);
|
||||
session_->Send(std::move(packet), path);
|
||||
}
|
||||
// If no data, Ptr destructor releases the packet.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we have a packet prepared to send.
|
||||
pos += nwrite;
|
||||
size_t datalen = pos - begin;
|
||||
Debug(session_, "Sending packet with %zu bytes", datalen);
|
||||
packet->Truncate(datalen);
|
||||
// At this point we have a packet prepared to send. The nwrite
|
||||
// is the size of the packet we are sending.
|
||||
size_t len = nwrite;
|
||||
Debug(session_, "Sending packet with %zu bytes", len);
|
||||
packet->Truncate(len);
|
||||
session_->Send(std::move(packet), path);
|
||||
if (++packet_send_count == max_packet_count) return;
|
||||
|
||||
// If we have sent the maximum number of packets, we're done.
|
||||
if (++packet_send_count == max_packet_count) {
|
||||
return;
|
||||
// If there are pending datagrams, try sending them in a fresh packet.
|
||||
// This is necessary because ngtcp2_conn_writev_stream only returns
|
||||
// NGTCP2_ERR_WRITE_MORE when there is actual stream data — when no
|
||||
// streams are active, the coalescing path above is never reached and
|
||||
// datagrams would never be sent.
|
||||
if (session_->HasPendingDatagrams()) {
|
||||
if (!ensure_packet()) [[unlikely]] {
|
||||
Debug(session_, "Failed to create packet for datagram");
|
||||
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
|
||||
closed = true;
|
||||
return session_->Close(CloseMethod::SILENT);
|
||||
}
|
||||
auto result =
|
||||
TryWritePendingDatagram(&path, packet->data(), packet->length());
|
||||
if (result > 0) {
|
||||
Debug(session_, "Sending datagram packet with %zd bytes", result);
|
||||
packet->Truncate(static_cast<size_t>(result));
|
||||
session_->Send(std::move(packet), path);
|
||||
if (++packet_send_count == max_packet_count) return;
|
||||
} else if (result < 0 && result != NGTCP2_ERR_WRITE_MORE) {
|
||||
// Fatal error — session already closed by TryWritePendingDatagram.
|
||||
return;
|
||||
}
|
||||
// If result == 0 (congestion) or NGTCP2_ERR_WRITE_MORE (datagram
|
||||
// packed but room for more), the loop continues normally.
|
||||
}
|
||||
|
||||
// Prepare to loop back around to prepare a new packet.
|
||||
// packet is already empty from the std::move above.
|
||||
pos = begin = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +595,7 @@ ssize_t Session::Application::WriteVStream(PathStorage* path,
|
||||
uv_hrtime());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// The DefaultApplication is the default implementation of Session::Application
|
||||
// that is used for all unrecognized ALPN identifiers.
|
||||
class DefaultApplication final : public Session::Application {
|
||||
@@ -470,7 +611,35 @@ class DefaultApplication final : public Session::Application {
|
||||
|
||||
error_code GetNoErrorCode() const override { return 0; }
|
||||
|
||||
bool ReceiveStreamData(int64_t stream_id,
|
||||
// Raw QUIC has no application-defined "general failure" code, so
|
||||
// fall back to the QUIC transport-level INTERNAL_ERROR (0x1) used
|
||||
// by ngtcp2 for unspecified failures.
|
||||
error_code GetInternalErrorCode() const override {
|
||||
return NGTCP2_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
void EarlyDataRejected() override {
|
||||
// Destroy all open streams — ngtcp2 has already discarded their
|
||||
// internal state when it rejected the early data.
|
||||
session().DestroyAllStreams(QuicError::ForApplication(0));
|
||||
if (!session().is_destroyed()) {
|
||||
session().EmitEarlyDataRejected();
|
||||
}
|
||||
}
|
||||
|
||||
bool ApplySessionTicketData(const PendingTicketAppData& data) override {
|
||||
return std::holds_alternative<DefaultTicketData>(data);
|
||||
}
|
||||
|
||||
bool ReceiveStreamOpen(stream_id id) override {
|
||||
auto stream = session().CreateStream(id);
|
||||
if (!stream || session().is_destroyed()) [[unlikely]] {
|
||||
return !session().is_destroyed();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReceiveStreamData(stream_id id,
|
||||
const uint8_t* data,
|
||||
size_t datalen,
|
||||
const Stream::ReceiveDataFlags& flags,
|
||||
@@ -478,10 +647,10 @@ class DefaultApplication final : public Session::Application {
|
||||
BaseObjectPtr<Stream> stream;
|
||||
if (stream_user_data == nullptr) {
|
||||
// This is the first time we're seeing this stream. Implicitly create it.
|
||||
stream = session().CreateStream(stream_id);
|
||||
if (!stream) [[unlikely]] {
|
||||
// We couldn't actually create the stream for whatever reason.
|
||||
Debug(&session(), "Default application failed to create new stream");
|
||||
stream = session().CreateStream(id);
|
||||
if (!stream || session().is_destroyed()) [[unlikely]] {
|
||||
// We couldn't create the stream, or the session was destroyed
|
||||
// during the onstream callback (via MakeCallback re-entrancy).
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -546,7 +715,6 @@ class DefaultApplication final : public Session::Application {
|
||||
|
||||
if (count > 0) {
|
||||
stream->Schedule(&stream_queue_);
|
||||
} else {
|
||||
}
|
||||
|
||||
// Not calling done here because we defer committing
|
||||
@@ -569,14 +737,26 @@ class DefaultApplication final : public Session::Application {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResumeStream(int64_t id) override { ScheduleStream(id); }
|
||||
void ResumeStream(stream_id id) override { ScheduleStream(id); }
|
||||
|
||||
void BlockStream(int64_t id) override {
|
||||
void BlockStream(stream_id id) override {
|
||||
if (auto stream = session().FindStream(id)) [[likely]] {
|
||||
// Remove the stream from the send queue. It will be re-scheduled
|
||||
// via ExtendMaxStreamData when the peer grants more flow control.
|
||||
// Without this, SendPendingData would repeatedly pop and retry
|
||||
// the same blocked stream in an infinite loop.
|
||||
stream->Unschedule();
|
||||
stream->EmitBlocked();
|
||||
}
|
||||
}
|
||||
|
||||
void ExtendMaxStreamData(Stream* stream, uint64_t max_data) override {
|
||||
// The peer granted more flow control for this stream. Re-schedule
|
||||
// it so SendPendingData will resume writing.
|
||||
DCHECK_NOT_NULL(stream);
|
||||
stream->Schedule(&stream_queue_);
|
||||
}
|
||||
|
||||
bool StreamCommit(StreamData* stream_data, size_t datalen) override {
|
||||
DCHECK_NOT_NULL(stream_data);
|
||||
CHECK(stream_data->stream);
|
||||
@@ -589,7 +769,7 @@ class DefaultApplication final : public Session::Application {
|
||||
SET_NO_MEMORY_INFO()
|
||||
|
||||
private:
|
||||
void ScheduleStream(int64_t id) {
|
||||
void ScheduleStream(stream_id id) {
|
||||
if (auto stream = session().FindStream(id)) [[likely]] {
|
||||
stream->Schedule(&stream_queue_);
|
||||
}
|
||||
|
||||
+147
-22
@@ -2,6 +2,9 @@
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
#include "base_object.h"
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
@@ -11,34 +14,82 @@
|
||||
|
||||
namespace node::quic {
|
||||
|
||||
// Parsed session ticket application data, produced by
|
||||
// Application::ParseTicketData() before ALPN negotiation and consumed
|
||||
// by Application::ApplySessionTicketData() after.
|
||||
struct DefaultTicketData {};
|
||||
struct Http3TicketData {
|
||||
uint64_t max_field_section_size;
|
||||
uint64_t qpack_max_dtable_capacity;
|
||||
uint64_t qpack_encoder_max_dtable_capacity;
|
||||
uint64_t qpack_blocked_streams;
|
||||
bool enable_connect_protocol;
|
||||
bool enable_datagrams;
|
||||
};
|
||||
using PendingTicketAppData =
|
||||
std::variant<std::monostate, DefaultTicketData, Http3TicketData>;
|
||||
|
||||
// An Application implements the ALPN-protocol specific semantics on behalf
|
||||
// of a QUIC Session.
|
||||
class Session::Application : public MemoryRetainer {
|
||||
public:
|
||||
using Options = Session::Application_Options;
|
||||
|
||||
Application(Session* session, const Options& options);
|
||||
DISALLOW_COPY_AND_MOVE(Application)
|
||||
|
||||
// The type of Application, exposed via the session state so JS
|
||||
// can observe which Application was selected after ALPN negotiation.
|
||||
// This is used primarily for testing/debugging.
|
||||
enum class Type : uint8_t {
|
||||
NONE = 0, // Not yet selected (server pre-negotiation)
|
||||
DEFAULT = 1, // DefaultApplication (non-h3 ALPN)
|
||||
HTTP3 = 2, // Http3ApplicationImpl (h3 / h3-XX ALPN)
|
||||
};
|
||||
|
||||
Application(Session* session, const Options& options);
|
||||
DISALLOW_COPY_AND_MOVE(Application)
|
||||
|
||||
virtual Type type() const = 0;
|
||||
|
||||
virtual bool Start();
|
||||
|
||||
// Returns true if Start() has been called successfully.
|
||||
virtual bool is_started() const { return false; }
|
||||
|
||||
// Called when the server rejects 0-RTT early data. The application
|
||||
// must destroy all streams that were opened during the 0-RTT phase
|
||||
// since ngtcp2 has already discarded their internal state.
|
||||
virtual void EarlyDataRejected() = 0;
|
||||
|
||||
// The "no error code" is the application-level error code that signals
|
||||
// "no error". Per the QUIC spec, this can vary by application protocol
|
||||
// and is not necessarily 0.
|
||||
virtual error_code GetNoErrorCode() const = 0;
|
||||
|
||||
// The "internal error code" is the application-level error code used
|
||||
// to signal a non-specific failure when no more specific code has
|
||||
// been provided by the caller. For example, `writer.fail(reason)` on
|
||||
// the JS side uses this code when `reason` is not a `QuicError`
|
||||
// carrying an explicit code. For HTTP/3 this is
|
||||
// `NGHTTP3_H3_INTERNAL_ERROR` (0x102); for raw QUIC applications
|
||||
// there is no defined application code so we fall back to
|
||||
// NGTCP2_INTERNAL_ERROR (0x1).
|
||||
virtual error_code GetInternalErrorCode() const = 0;
|
||||
|
||||
// Called after Session::Receive processes a packet, outside all callback
|
||||
// scopes. Applications can use this to handle deferred operations that
|
||||
// require calling into JS (e.g., HTTP/3 GOAWAY processing).
|
||||
virtual void PostReceive() {}
|
||||
|
||||
// Called when ngtcp2 notifies us that a new remote stream has been
|
||||
// opened. The Application decides whether to create a Stream object
|
||||
// (and fire the JS onstream callback) based on the stream type. For
|
||||
// example, HTTP/3 only creates Stream objects for bidi streams since
|
||||
// uni streams are managed internally by nghttp3.
|
||||
virtual bool ReceiveStreamOpen(stream_id id) = 0;
|
||||
|
||||
// Session will forward all received stream data immediately on to the
|
||||
// Application. The only additional processing the Session does is to
|
||||
// automatically adjust the session-level flow control window. It is up to
|
||||
// the Application to do the same for the Stream-level flow control.
|
||||
virtual bool ReceiveStreamData(int64_t stream_id,
|
||||
virtual bool ReceiveStreamData(stream_id id,
|
||||
const uint8_t* data,
|
||||
size_t datalen,
|
||||
const Stream::ReceiveDataFlags& flags,
|
||||
@@ -46,22 +97,30 @@ class Session::Application : public MemoryRetainer {
|
||||
|
||||
// Session will forward all data acknowledgements for a stream to the
|
||||
// Application.
|
||||
virtual bool AcknowledgeStreamData(int64_t stream_id, size_t datalen);
|
||||
virtual bool AcknowledgeStreamData(stream_id id, size_t datalen);
|
||||
|
||||
// Called to determine if a Header can be added to this application.
|
||||
// Applications that do not support headers will always return false.
|
||||
virtual bool CanAddHeader(size_t current_count,
|
||||
size_t current_headers_length,
|
||||
size_t this_header_length);
|
||||
size_t this_header_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called when ngtcp2 reports NGTCP2_ERR_STREAM_SHUT_WR for a stream.
|
||||
// Applications that manage their own framing (e.g., HTTP/3) must inform
|
||||
// their protocol layer that the stream's write side is shut so it stops
|
||||
// queuing data for that stream. The default is a no-op.
|
||||
virtual void StreamWriteShut(stream_id id) {}
|
||||
|
||||
// Called to mark the identified stream as being blocked. Not all
|
||||
// Application types will support blocked streams, and those that do will do
|
||||
// so differently.
|
||||
virtual void BlockStream(int64_t id);
|
||||
virtual void BlockStream(stream_id id) {}
|
||||
|
||||
// Called when the session determines that there is outbound data available
|
||||
// to send for the given stream.
|
||||
virtual void ResumeStream(int64_t id);
|
||||
virtual void ResumeStream(stream_id id) {}
|
||||
|
||||
// Called when the Session determines that the maximum number of
|
||||
// remotely-initiated unidirectional streams has been extended. Not all
|
||||
@@ -69,16 +128,27 @@ class Session::Application : public MemoryRetainer {
|
||||
// nothing.
|
||||
virtual void ExtendMaxStreams(EndpointLabel label,
|
||||
Direction direction,
|
||||
uint64_t max_streams);
|
||||
uint64_t max_streams) {}
|
||||
|
||||
// Returns true if the application manages stream FIN internally (e.g.,
|
||||
// HTTP/3 uses nghttp3 which sends FIN via the fin flag in writev_stream).
|
||||
// When true, the stream infrastructure must NOT call
|
||||
// ngtcp2_conn_shutdown_stream_write when the JS write side ends —
|
||||
// the application protocol layer handles it.
|
||||
virtual bool stream_fin_managed_by_application() const { return false; }
|
||||
|
||||
// Called when the Session determines that the flow control window for the
|
||||
// given stream has been expanded. Not all Application types will require
|
||||
// this notification so the default is to do nothing.
|
||||
virtual void ExtendMaxStreamData(Stream* stream, uint64_t max_data);
|
||||
virtual void ExtendMaxStreamData(Stream* stream, uint64_t max_data) {
|
||||
Debug(session_, "Application extending max stream data");
|
||||
// By default do nothing.
|
||||
}
|
||||
|
||||
// Different Applications may wish to set some application data in the
|
||||
// session ticket (e.g. http/3 would set server settings in the application
|
||||
// data). By default, there's nothing to set.
|
||||
// data). The first byte written MUST be the Application::Type enum value.
|
||||
// By default, writes just the type byte.
|
||||
virtual void CollectSessionTicketAppData(
|
||||
SessionTicket::AppData* app_data) const;
|
||||
|
||||
@@ -89,17 +159,37 @@ class Session::Application : public MemoryRetainer {
|
||||
const SessionTicket::AppData& app_data,
|
||||
SessionTicket::AppData::Source::Flag flag);
|
||||
|
||||
// Validates parsed ticket data against current application options.
|
||||
// Returns false if the stored settings are more permissive than the
|
||||
// current config (e.g., a feature was enabled when the ticket was
|
||||
// issued but is now disabled).
|
||||
static bool ValidateTicketData(const PendingTicketAppData& data,
|
||||
const Application_Options& options);
|
||||
|
||||
// Parse session ticket app data before ALPN negotiation. Reads the
|
||||
// type byte and dispatches to the appropriate application-specific
|
||||
// parser. Returns std::nullopt if parsing fails.
|
||||
static std::optional<PendingTicketAppData> ParseTicketData(
|
||||
const uv_buf_t& data);
|
||||
|
||||
// Called after ALPN negotiation to validate and apply previously
|
||||
// parsed session ticket app data. Returns false if the data is
|
||||
// incompatible (e.g., type mismatch or settings downgrade), which
|
||||
// causes the handshake to fail.
|
||||
virtual bool ApplySessionTicketData(const PendingTicketAppData& data) = 0;
|
||||
|
||||
// Notifies the Application that the identified stream has been closed.
|
||||
virtual void StreamClose(Stream* stream, QuicError&& error = QuicError());
|
||||
virtual void ReceiveStreamClose(Stream* stream,
|
||||
QuicError&& error = QuicError());
|
||||
|
||||
// Notifies the Application that the identified stream has been reset.
|
||||
virtual void StreamReset(Stream* stream,
|
||||
uint64_t final_size,
|
||||
QuicError&& error = QuicError());
|
||||
virtual void ReceiveStreamReset(Stream* stream,
|
||||
uint64_t final_size,
|
||||
QuicError&& error = QuicError());
|
||||
|
||||
// Notifies the Application that the identified stream should stop sending.
|
||||
virtual void StreamStopSending(Stream* stream,
|
||||
QuicError&& error = QuicError());
|
||||
virtual void ReceiveStreamStopSending(Stream* stream,
|
||||
QuicError&& error = QuicError());
|
||||
|
||||
// Submits an outbound block of headers for the given stream. Not all
|
||||
// Application types will support headers, in which case this function
|
||||
@@ -107,31 +197,57 @@ class Session::Application : public MemoryRetainer {
|
||||
virtual bool SendHeaders(const Stream& stream,
|
||||
HeadersKind kind,
|
||||
const v8::Local<v8::Array>& headers,
|
||||
HeadersFlags flags = HeadersFlags::NONE);
|
||||
HeadersFlags flags = HeadersFlags::NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Signals to the Application that it should serialize and transmit any
|
||||
// pending session and stream packets it has accumulated.
|
||||
void SendPendingData();
|
||||
|
||||
// Returns true if the application protocol supports sending and
|
||||
// receiving headers on streams (e.g. HTTP/3). Applications that
|
||||
// do not support headers should return false (the default).
|
||||
virtual bool SupportsHeaders() const { return false; }
|
||||
|
||||
// Initiates application-level graceful shutdown signaling (e.g.,
|
||||
// HTTP/3 GOAWAY). Called when Session::Close(GRACEFUL) is invoked.
|
||||
virtual void BeginShutdown() {}
|
||||
|
||||
// Completes the application-level graceful shutdown. Called from
|
||||
// FinishClose() before CONNECTION_CLOSE is sent. For HTTP/3, this
|
||||
// sends the final GOAWAY with the actual last accepted stream ID.
|
||||
virtual void CompleteShutdown() {}
|
||||
|
||||
// Set the priority level of the stream if supported by the application. Not
|
||||
// all applications support priorities, in which case this function is a
|
||||
// non-op.
|
||||
virtual void SetStreamPriority(
|
||||
const Stream& stream,
|
||||
StreamPriority priority = StreamPriority::DEFAULT,
|
||||
StreamPriorityFlags flags = StreamPriorityFlags::NONE);
|
||||
StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL) {}
|
||||
|
||||
struct StreamPriorityResult {
|
||||
StreamPriority priority;
|
||||
StreamPriorityFlags flags;
|
||||
};
|
||||
|
||||
// Get the priority level of the stream if supported by the application. Not
|
||||
// all applications support priorities, in which case this function returns
|
||||
// the default stream priority.
|
||||
virtual StreamPriority GetStreamPriority(const Stream& stream);
|
||||
virtual StreamPriorityResult GetStreamPriority(const Stream& stream) {
|
||||
return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL};
|
||||
}
|
||||
|
||||
// The StreamData struct is used by the application to pass pending stream
|
||||
// data to the session for transmission.
|
||||
struct StreamData;
|
||||
|
||||
virtual int GetStreamData(StreamData* data) = 0;
|
||||
virtual bool StreamCommit(StreamData* data, size_t datalen) = 0;
|
||||
|
||||
inline Environment* env() const { return session().env(); }
|
||||
|
||||
inline Session& session() {
|
||||
CHECK_NOT_NULL(session_);
|
||||
return *session_;
|
||||
@@ -144,6 +260,15 @@ class Session::Application : public MemoryRetainer {
|
||||
private:
|
||||
Packet::Ptr CreateStreamDataPacket();
|
||||
|
||||
// Tries to pack a pending datagram into the current packet buffer.
|
||||
// If < 0 is returned, either NGTCP2_ERR_WRITE_MORE or a fatal error is
|
||||
// returned; the caller must check. If > 0 is returned, the packet is done
|
||||
// and the value is the size of the finalized packet. If 0 is returned,
|
||||
// the datagram is either congestion limited or was abandoned
|
||||
ssize_t TryWritePendingDatagram(PathStorage* path,
|
||||
uint8_t* dest,
|
||||
size_t destlen);
|
||||
|
||||
// Write the given stream_data into the buffer.
|
||||
ssize_t WriteVStream(PathStorage* path,
|
||||
uint8_t* buf,
|
||||
@@ -159,7 +284,7 @@ struct Session::Application::StreamData final {
|
||||
size_t count = 0;
|
||||
// The stream identifier. If this is a negative value then no stream is
|
||||
// identified.
|
||||
int64_t id = -1;
|
||||
stream_id id = -1;
|
||||
int fin = 0;
|
||||
ngtcp2_vec data[kMaxVectorCount]{};
|
||||
BaseObjectPtr<Stream> stream;
|
||||
|
||||
+183
-33
@@ -11,11 +11,15 @@
|
||||
#include <node_external_reference.h>
|
||||
#include <node_mem-inl.h>
|
||||
#include <node_realm-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <v8.h>
|
||||
#include "bindingdata.h"
|
||||
#include "session.h"
|
||||
#include "session_manager.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
using mem::kReserveSizeAndAlign;
|
||||
using v8::Function;
|
||||
using v8::FunctionTemplate;
|
||||
using v8::Local;
|
||||
@@ -25,24 +29,155 @@ using v8::Value;
|
||||
|
||||
namespace quic {
|
||||
|
||||
// ============================================================================
|
||||
// Thread-local QUIC allocator.
|
||||
//
|
||||
// Both ngtcp2 and nghttp3 take an allocator struct (ngtcp2_mem /
|
||||
// nghttp3_mem) whose pointer is stored inside every object they
|
||||
// allocate. Some of those objects — notably nghttp3 rcbufs backing
|
||||
// V8 external strings — can outlive the BindingData that created them
|
||||
// (freed during V8 isolate teardown, after Environment cleanup).
|
||||
//
|
||||
// To handle this safely, both allocators live in a thread-local static
|
||||
// struct that is never destroyed. Memory tracking goes through the
|
||||
// BindingData pointer when it is alive and is silently skipped during
|
||||
// teardown (after ~BindingData nulls the pointer).
|
||||
//
|
||||
// The allocation functions use the same prepended-size-header scheme as
|
||||
// NgLibMemoryManager (node_mem-inl.h) so that frees always know the
|
||||
// allocation size regardless of whether BindingData is still around.
|
||||
|
||||
namespace {
|
||||
struct QuicAllocState {
|
||||
BindingData* binding = nullptr;
|
||||
ngtcp2_mem ngtcp2 = {};
|
||||
nghttp3_mem nghttp3 = {};
|
||||
};
|
||||
thread_local QuicAllocState quic_alloc_state;
|
||||
|
||||
// Core allocation functions shared by both ngtcp2 and nghttp3.
|
||||
// user_data always points to the thread-local QuicAllocState.
|
||||
|
||||
void* QuicRealloc(void* ptr, size_t size, void* user_data) {
|
||||
auto* state = static_cast<QuicAllocState*>(user_data);
|
||||
|
||||
size_t previous_size = 0;
|
||||
char* original_ptr = nullptr;
|
||||
|
||||
if (size > 0) size += kReserveSizeAndAlign;
|
||||
|
||||
if (ptr != nullptr) {
|
||||
original_ptr = static_cast<char*>(ptr) - kReserveSizeAndAlign;
|
||||
previous_size = *reinterpret_cast<size_t*>(original_ptr);
|
||||
if (previous_size == 0) {
|
||||
char* ret = UncheckedRealloc(original_ptr, size);
|
||||
if (ret != nullptr) ret += kReserveSizeAndAlign;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (state->binding) {
|
||||
state->binding->CheckAllocatedSize(previous_size);
|
||||
}
|
||||
|
||||
char* mem = UncheckedRealloc(original_ptr, size);
|
||||
|
||||
if (mem != nullptr) {
|
||||
const int64_t new_size = size - previous_size;
|
||||
if (state->binding) {
|
||||
state->binding->IncreaseAllocatedSize(new_size);
|
||||
state->binding->env()->external_memory_accounter()->Update(
|
||||
state->binding->env()->isolate(), new_size);
|
||||
}
|
||||
*reinterpret_cast<size_t*>(mem) = size;
|
||||
mem += kReserveSizeAndAlign;
|
||||
} else if (size == 0) {
|
||||
if (state->binding) {
|
||||
state->binding->DecreaseAllocatedSize(previous_size);
|
||||
state->binding->env()->external_memory_accounter()->Decrease(
|
||||
state->binding->env()->isolate(), previous_size);
|
||||
}
|
||||
}
|
||||
return mem;
|
||||
}
|
||||
|
||||
void* QuicMalloc(size_t size, void* user_data) {
|
||||
return QuicRealloc(nullptr, size, user_data);
|
||||
}
|
||||
|
||||
void QuicFree(void* ptr, void* user_data) {
|
||||
if (ptr == nullptr) return;
|
||||
CHECK_NULL(QuicRealloc(ptr, 0, user_data));
|
||||
}
|
||||
|
||||
void* QuicCalloc(size_t nmemb, size_t size, void* user_data) {
|
||||
size_t real_size = MultiplyWithOverflowCheck(nmemb, size);
|
||||
void* mem = QuicMalloc(real_size, user_data);
|
||||
if (mem != nullptr) memset(mem, 0, real_size);
|
||||
return mem;
|
||||
}
|
||||
|
||||
// Thin wrappers with the correct function-pointer types for each
|
||||
// library. The signatures happen to be identical today, but keeping
|
||||
// them separate avoids ABI coupling between ngtcp2 and nghttp3.
|
||||
|
||||
void* Ngtcp2Malloc(size_t size, void* ud) {
|
||||
return QuicMalloc(size, ud);
|
||||
}
|
||||
void Ngtcp2Free(void* ptr, void* ud) {
|
||||
QuicFree(ptr, ud);
|
||||
}
|
||||
void* Ngtcp2Calloc(size_t n, size_t s, void* ud) {
|
||||
return QuicCalloc(n, s, ud);
|
||||
}
|
||||
void* Ngtcp2Realloc(void* ptr, size_t size, void* ud) {
|
||||
return QuicRealloc(ptr, size, ud);
|
||||
}
|
||||
|
||||
void* Nghttp3Malloc(size_t size, void* ud) {
|
||||
return QuicMalloc(size, ud);
|
||||
}
|
||||
void Nghttp3Free(void* ptr, void* ud) {
|
||||
QuicFree(ptr, ud);
|
||||
}
|
||||
void* Nghttp3Calloc(size_t n, size_t s, void* ud) {
|
||||
return QuicCalloc(n, s, ud);
|
||||
}
|
||||
void* Nghttp3Realloc(void* ptr, size_t size, void* ud) {
|
||||
return QuicRealloc(ptr, size, ud);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
BindingData& BindingData::Get(Environment* env) {
|
||||
return *(env->principal_realm()->GetBindingData<BindingData>());
|
||||
}
|
||||
|
||||
BindingData::operator ngtcp2_mem() {
|
||||
return MakeAllocator();
|
||||
BindingData::~BindingData() {
|
||||
quic_alloc_state.binding = nullptr;
|
||||
}
|
||||
|
||||
BindingData::operator nghttp3_mem() {
|
||||
ngtcp2_mem allocator = *this;
|
||||
nghttp3_mem http3_allocator = {
|
||||
allocator.user_data,
|
||||
allocator.malloc,
|
||||
allocator.free,
|
||||
allocator.calloc,
|
||||
allocator.realloc,
|
||||
ngtcp2_mem* BindingData::ngtcp2_allocator() {
|
||||
quic_alloc_state.binding = this;
|
||||
quic_alloc_state.ngtcp2 = {
|
||||
&quic_alloc_state,
|
||||
Ngtcp2Malloc,
|
||||
Ngtcp2Free,
|
||||
Ngtcp2Calloc,
|
||||
Ngtcp2Realloc,
|
||||
};
|
||||
return http3_allocator;
|
||||
return &quic_alloc_state.ngtcp2;
|
||||
}
|
||||
|
||||
nghttp3_mem* BindingData::nghttp3_allocator() {
|
||||
quic_alloc_state.binding = this;
|
||||
quic_alloc_state.nghttp3 = {
|
||||
&quic_alloc_state,
|
||||
Nghttp3Malloc,
|
||||
Nghttp3Free,
|
||||
Nghttp3Calloc,
|
||||
Nghttp3Realloc,
|
||||
};
|
||||
return &quic_alloc_state.nghttp3;
|
||||
}
|
||||
|
||||
void BindingData::CheckAllocatedSize(size_t previous_size) const {
|
||||
@@ -59,7 +194,20 @@ void BindingData::DecreaseAllocatedSize(size_t size) {
|
||||
current_ngtcp2_memory_ -= size;
|
||||
}
|
||||
|
||||
// Forwards detailed(verbose) debugging information from nghttp3. Enabled using
|
||||
// the NODE_DEBUG_NATIVE=NGHTTP3 category.
|
||||
void nghttp3_debug_log(const char* fmt, va_list args) {
|
||||
auto isolate = v8::Isolate::GetCurrent();
|
||||
if (isolate == nullptr) return;
|
||||
auto env = Environment::GetCurrent(isolate);
|
||||
if (env->enabled_debug_list()->enabled(DebugCategory::NGHTTP3)) {
|
||||
fprintf(stderr, "nghttp3 ");
|
||||
vfprintf(stderr, fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
void BindingData::InitPerContext(Realm* realm, Local<Object> target) {
|
||||
nghttp3_set_debug_vprintf_callback(nghttp3_debug_log);
|
||||
SetMethod(realm->context(), target, "setCallbacks", SetCallbacks);
|
||||
Realm::GetCurrent(realm->context())->AddBindingData<BindingData>(target);
|
||||
}
|
||||
@@ -75,6 +223,13 @@ BindingData::BindingData(Realm* realm, Local<Object> object)
|
||||
MakeWeak();
|
||||
}
|
||||
|
||||
SessionManager& BindingData::session_manager() {
|
||||
if (!session_manager_) {
|
||||
session_manager_ = std::make_unique<SessionManager>(env());
|
||||
}
|
||||
return *session_manager_;
|
||||
}
|
||||
|
||||
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
|
||||
#define V(name, _) tracker->TrackField(#name, name##_callback());
|
||||
|
||||
@@ -162,36 +317,31 @@ JS_METHOD_IMPL(BindingData::SetCallbacks) {
|
||||
#undef V
|
||||
}
|
||||
|
||||
NgTcp2CallbackScope::NgTcp2CallbackScope(Environment* env) : env(env) {
|
||||
auto& binding = BindingData::Get(env);
|
||||
CHECK(!binding.in_ngtcp2_callback_scope);
|
||||
binding.in_ngtcp2_callback_scope = true;
|
||||
NgTcp2CallbackScope::NgTcp2CallbackScope(Session* session) : session(session) {
|
||||
CHECK(!session->in_ngtcp2_callback_scope_);
|
||||
session->in_ngtcp2_callback_scope_ = true;
|
||||
}
|
||||
|
||||
NgTcp2CallbackScope::~NgTcp2CallbackScope() {
|
||||
auto& binding = BindingData::Get(env);
|
||||
binding.in_ngtcp2_callback_scope = false;
|
||||
session->in_ngtcp2_callback_scope_ = false;
|
||||
if (session->destroy_deferred_) {
|
||||
session->destroy_deferred_ = false;
|
||||
session->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
bool NgTcp2CallbackScope::in_ngtcp2_callback(Environment* env) {
|
||||
auto& binding = BindingData::Get(env);
|
||||
return binding.in_ngtcp2_callback_scope;
|
||||
}
|
||||
|
||||
NgHttp3CallbackScope::NgHttp3CallbackScope(Environment* env) : env(env) {
|
||||
auto& binding = BindingData::Get(env);
|
||||
CHECK(!binding.in_nghttp3_callback_scope);
|
||||
binding.in_nghttp3_callback_scope = true;
|
||||
NgHttp3CallbackScope::NgHttp3CallbackScope(Session* session)
|
||||
: session(session) {
|
||||
CHECK(!session->in_nghttp3_callback_scope_);
|
||||
session->in_nghttp3_callback_scope_ = true;
|
||||
}
|
||||
|
||||
NgHttp3CallbackScope::~NgHttp3CallbackScope() {
|
||||
auto& binding = BindingData::Get(env);
|
||||
binding.in_nghttp3_callback_scope = false;
|
||||
}
|
||||
|
||||
bool NgHttp3CallbackScope::in_nghttp3_callback(Environment* env) {
|
||||
auto& binding = BindingData::Get(env);
|
||||
return binding.in_nghttp3_callback_scope;
|
||||
session->in_nghttp3_callback_scope_ = false;
|
||||
if (session->destroy_deferred_) {
|
||||
session->destroy_deferred_ = false;
|
||||
session->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
CallbackScopeBase::CallbackScopeBase(Environment* env)
|
||||
|
||||
+48
-13
@@ -11,6 +11,7 @@
|
||||
#include <node.h>
|
||||
#include <node_mem.h>
|
||||
#include <v8.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include "defs.h"
|
||||
|
||||
@@ -18,13 +19,14 @@ namespace node::quic {
|
||||
|
||||
class Endpoint;
|
||||
class Packet;
|
||||
class Session;
|
||||
class SessionManager;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// The FunctionTemplates the BindingData will store for us.
|
||||
#define QUIC_CONSTRUCTORS(V) \
|
||||
V(endpoint) \
|
||||
V(logstream) \
|
||||
V(session) \
|
||||
V(stream) \
|
||||
V(udp)
|
||||
@@ -36,37 +38,48 @@ class Packet;
|
||||
#define QUIC_JS_CALLBACKS(V) \
|
||||
V(endpoint_close, EndpointClose) \
|
||||
V(session_close, SessionClose) \
|
||||
V(session_early_data_rejected, SessionEarlyDataRejected) \
|
||||
V(session_goaway, SessionGoaway) \
|
||||
V(session_datagram, SessionDatagram) \
|
||||
V(session_datagram_status, SessionDatagramStatus) \
|
||||
V(session_handshake, SessionHandshake) \
|
||||
V(session_keylog, SessionKeyLog) \
|
||||
V(session_qlog, SessionQlog) \
|
||||
V(session_new, SessionNew) \
|
||||
V(session_new_token, SessionNewToken) \
|
||||
V(session_origin, SessionOrigin) \
|
||||
V(session_path_validation, SessionPathValidation) \
|
||||
V(session_ticket, SessionTicket) \
|
||||
V(session_version_negotiation, SessionVersionNegotiation) \
|
||||
V(stream_blocked, StreamBlocked) \
|
||||
V(stream_close, StreamClose) \
|
||||
V(stream_created, StreamCreated) \
|
||||
V(stream_drain, StreamDrain) \
|
||||
V(stream_headers, StreamHeaders) \
|
||||
V(stream_reset, StreamReset) \
|
||||
V(stream_trailers, StreamTrailers)
|
||||
|
||||
// The various JS strings the implementation uses.
|
||||
#define QUIC_STRINGS(V) \
|
||||
V(abandoned, "abandoned") \
|
||||
V(aborted, "aborted") \
|
||||
V(acknowledged, "acknowledged") \
|
||||
V(ack_delay_exponent, "ackDelayExponent") \
|
||||
V(active_connection_id_limit, "activeConnectionIDLimit") \
|
||||
V(address_lru_size, "addressLRUSize") \
|
||||
V(application, "application") \
|
||||
V(authoritative, "authoritative") \
|
||||
V(bbr, "bbr") \
|
||||
V(ca, "ca") \
|
||||
V(cc_algorithm, "cc") \
|
||||
V(certs, "certs") \
|
||||
V(code, "code") \
|
||||
V(ciphers, "ciphers") \
|
||||
V(crl, "crl") \
|
||||
V(cubic, "cubic") \
|
||||
V(datagram_drop_policy, "datagramDropPolicy") \
|
||||
V(disable_stateless_reset, "disableStatelessReset") \
|
||||
V(draining_period_multiplier, "drainingPeriodMultiplier") \
|
||||
V(enable_connect_protocol, "enableConnectProtocol") \
|
||||
V(enable_early_data, "enableEarlyData") \
|
||||
V(enable_datagrams, "enableDatagrams") \
|
||||
@@ -77,6 +90,7 @@ class Packet;
|
||||
V(groups, "groups") \
|
||||
V(handshake_timeout, "handshakeTimeout") \
|
||||
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
|
||||
V(keep_alive_timeout, "keepAlive") \
|
||||
V(initial_max_data, "initialMaxData") \
|
||||
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
|
||||
V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \
|
||||
@@ -86,15 +100,16 @@ class Packet;
|
||||
V(ipv6_only, "ipv6Only") \
|
||||
V(keylog, "keylog") \
|
||||
V(keys, "keys") \
|
||||
V(logstream, "LogStream") \
|
||||
V(lost, "lost") \
|
||||
V(max_ack_delay, "maxAckDelay") \
|
||||
V(max_connections_per_host, "maxConnectionsPerHost") \
|
||||
V(max_connections_total, "maxConnectionsTotal") \
|
||||
V(max_datagram_frame_size, "maxDatagramFrameSize") \
|
||||
V(max_datagram_send_attempts, "maxDatagramSendAttempts") \
|
||||
V(max_field_section_size, "maxFieldSectionSize") \
|
||||
V(max_header_length, "maxHeaderLength") \
|
||||
V(max_header_pairs, "maxHeaderPairs") \
|
||||
V(idle_timeout, "idleTimeout") \
|
||||
V(max_idle_timeout, "maxIdleTimeout") \
|
||||
V(max_payload_size, "maxPayloadSize") \
|
||||
V(max_retries, "maxRetries") \
|
||||
@@ -102,12 +117,16 @@ class Packet;
|
||||
V(max_stream_window, "maxStreamWindow") \
|
||||
V(max_window, "maxWindow") \
|
||||
V(min_version, "minVersion") \
|
||||
V(port, "port") \
|
||||
V(preferred_address_ipv4, "preferredAddressIpv4") \
|
||||
V(preferred_address_ipv6, "preferredAddressIpv6") \
|
||||
V(preferred_address_strategy, "preferredAddressPolicy") \
|
||||
V(alpn, "alpn") \
|
||||
V(qlog, "qlog") \
|
||||
V(qpack_blocked_streams, "qpackBlockedStreams") \
|
||||
V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \
|
||||
V(qpack_max_dtable_capacity, "qpackMaxDTableCapacity") \
|
||||
V(reason, "reason") \
|
||||
V(reject_unauthorized, "rejectUnauthorized") \
|
||||
V(reno, "reno") \
|
||||
V(reset_token_secret, "resetTokenSecret") \
|
||||
@@ -122,7 +141,9 @@ class Packet;
|
||||
V(token, "token") \
|
||||
V(token_expiration, "tokenExpiration") \
|
||||
V(token_secret, "tokenSecret") \
|
||||
V(transport, "transport") \
|
||||
V(transport_params, "transportParams") \
|
||||
V(type, "type") \
|
||||
V(tx_loss, "txDiagnosticLoss") \
|
||||
V(udp_receive_buffer_size, "udpReceiveBufferSize") \
|
||||
V(udp_send_buffer_size, "udpSendBufferSize") \
|
||||
@@ -151,27 +172,37 @@ class BindingData final
|
||||
static inline BindingData& Get(Realm* realm) { return Get(realm->env()); }
|
||||
|
||||
BindingData(Realm* realm, v8::Local<v8::Object> object);
|
||||
~BindingData() override;
|
||||
DISALLOW_COPY_AND_MOVE(BindingData)
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(BindingData)
|
||||
SET_SELF_SIZE(BindingData)
|
||||
|
||||
// NgLibMemoryManager
|
||||
operator ngtcp2_mem();
|
||||
operator nghttp3_mem();
|
||||
// NgLibMemoryManager — the base class provides CheckAllocatedSize,
|
||||
// IncreaseAllocatedSize, DecreaseAllocatedSize, and StopTrackingMemory.
|
||||
// Actual allocations go through the thread-local allocators below.
|
||||
void CheckAllocatedSize(size_t previous_size) const;
|
||||
void IncreaseAllocatedSize(size_t size);
|
||||
void DecreaseAllocatedSize(size_t size);
|
||||
|
||||
// Thread-local allocators that outlive BindingData destruction.
|
||||
// Both ngtcp2 and nghttp3 store the allocator pointer inside every
|
||||
// object they allocate; some of those objects (e.g., nghttp3 rcbufs
|
||||
// backing V8 external strings) can be freed after BindingData is gone.
|
||||
ngtcp2_mem* ngtcp2_allocator();
|
||||
nghttp3_mem* nghttp3_allocator();
|
||||
|
||||
// Installs the set of JavaScript callback functions that are used to
|
||||
// bridge out to the JS API.
|
||||
JS_METHOD(SetCallbacks);
|
||||
|
||||
// Lazily-created per-Realm SessionManager. Centralizes CID -> Session
|
||||
// routing so that any endpoint can route packets to any session.
|
||||
SessionManager& session_manager();
|
||||
|
||||
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
|
||||
|
||||
bool in_ngtcp2_callback_scope = false;
|
||||
bool in_nghttp3_callback_scope = false;
|
||||
size_t current_ngtcp2_memory_ = 0;
|
||||
|
||||
// The following set up various storage and accessors for common strings,
|
||||
@@ -214,6 +245,8 @@ class BindingData final
|
||||
#define V(name, _) mutable v8::Eternal<v8::String> on_##name##_string_;
|
||||
QUIC_JS_CALLBACKS(V)
|
||||
#undef V
|
||||
|
||||
std::unique_ptr<SessionManager> session_manager_;
|
||||
};
|
||||
|
||||
JS_METHOD_IMPL(IllegalConstructor);
|
||||
@@ -221,20 +254,22 @@ JS_METHOD_IMPL(IllegalConstructor);
|
||||
// The ngtcp2 and nghttp3 callbacks have certain restrictions
|
||||
// that forbid re-entry. We provide the following scopes for
|
||||
// use in those to help protect against it.
|
||||
// These callback scopes are per-session, not per-environment. This ensures
|
||||
// that one session's ngtcp2/nghttp3 callback does not block an unrelated
|
||||
// session from sending packets. A BaseObjectPtr prevents the Session from
|
||||
// being prematurely freed while the scope is alive on the stack.
|
||||
struct NgTcp2CallbackScope final {
|
||||
Environment* env;
|
||||
explicit NgTcp2CallbackScope(Environment* env);
|
||||
BaseObjectPtr<Session> session;
|
||||
explicit NgTcp2CallbackScope(Session* session);
|
||||
DISALLOW_COPY_AND_MOVE(NgTcp2CallbackScope)
|
||||
~NgTcp2CallbackScope();
|
||||
static bool in_ngtcp2_callback(Environment* env);
|
||||
};
|
||||
|
||||
struct NgHttp3CallbackScope final {
|
||||
Environment* env;
|
||||
explicit NgHttp3CallbackScope(Environment* env);
|
||||
BaseObjectPtr<Session> session;
|
||||
explicit NgHttp3CallbackScope(Session* session);
|
||||
DISALLOW_COPY_AND_MOVE(NgHttp3CallbackScope)
|
||||
~NgHttp3CallbackScope();
|
||||
static bool in_nghttp3_callback(Environment* env);
|
||||
};
|
||||
|
||||
struct CallbackScopeBase {
|
||||
|
||||
+73
-19
@@ -88,31 +88,45 @@ Store::Store(std::unique_ptr<BackingStore> store, size_t length, size_t offset)
|
||||
CHECK_LE(length_, store_->ByteLength() - offset_);
|
||||
}
|
||||
|
||||
Maybe<Store> Store::From(Local<ArrayBuffer> buffer, Local<Value> detach_key) {
|
||||
if (!buffer->IsDetachable()) {
|
||||
return Nothing<Store>();
|
||||
}
|
||||
bool res;
|
||||
auto backing = buffer->GetBackingStore();
|
||||
Maybe<Store> Store::From(Local<ArrayBuffer> buffer) {
|
||||
v8::Isolate* isolate = v8::Isolate::GetCurrent();
|
||||
Environment* env = Environment::GetCurrent(isolate->GetCurrentContext());
|
||||
auto length = buffer->ByteLength();
|
||||
if (!buffer->Detach(detach_key).To(&res) || !res) {
|
||||
auto dest = ArrayBuffer::NewBackingStore(
|
||||
isolate,
|
||||
length,
|
||||
v8::BackingStoreInitializationMode::kUninitialized,
|
||||
v8::BackingStoreOnFailureMode::kReturnNull);
|
||||
if (!dest) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return Nothing<Store>();
|
||||
}
|
||||
return Just(Store(std::move(backing), length, 0));
|
||||
if (length > 0) {
|
||||
memcpy(dest->Data(), buffer->Data(), length);
|
||||
}
|
||||
return Just(Store(std::move(dest), length, 0));
|
||||
}
|
||||
|
||||
Maybe<Store> Store::From(Local<ArrayBufferView> view, Local<Value> detach_key) {
|
||||
if (!view->Buffer()->IsDetachable()) {
|
||||
return Nothing<Store>();
|
||||
}
|
||||
bool res;
|
||||
auto backing = view->Buffer()->GetBackingStore();
|
||||
Maybe<Store> Store::From(Local<ArrayBufferView> view) {
|
||||
v8::Isolate* isolate = v8::Isolate::GetCurrent();
|
||||
Environment* env = Environment::GetCurrent(isolate->GetCurrentContext());
|
||||
auto length = view->ByteLength();
|
||||
auto offset = view->ByteOffset();
|
||||
if (!view->Buffer()->Detach(detach_key).To(&res) || !res) {
|
||||
auto dest = ArrayBuffer::NewBackingStore(
|
||||
isolate,
|
||||
length,
|
||||
v8::BackingStoreInitializationMode::kUninitialized,
|
||||
v8::BackingStoreOnFailureMode::kReturnNull);
|
||||
if (!dest) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return Nothing<Store>();
|
||||
}
|
||||
return Just(Store(std::move(backing), length, offset));
|
||||
if (length > 0) {
|
||||
memcpy(dest->Data(),
|
||||
static_cast<const uint8_t*>(view->Buffer()->Data()) + offset,
|
||||
length);
|
||||
}
|
||||
return Just(Store(std::move(dest), length, 0));
|
||||
}
|
||||
|
||||
Store Store::CopyFrom(Local<ArrayBuffer> buffer) {
|
||||
@@ -161,8 +175,7 @@ T Store::convert() const {
|
||||
// We can only safely convert to T if we have a valid store.
|
||||
CHECK(store_);
|
||||
T buf;
|
||||
buf.base =
|
||||
store_ != nullptr ? static_cast<N*>(store_->Data()) + offset_ : nullptr;
|
||||
buf.base = static_cast<N*>(store_->Data()) + offset_;
|
||||
buf.len = length_;
|
||||
return buf;
|
||||
}
|
||||
@@ -223,6 +236,45 @@ QuicError::QuicError(const ngtcp2_ccerr& error)
|
||||
error_(error),
|
||||
ptr_(&error_) {}
|
||||
|
||||
QuicError::QuicError(QuicError&& other) noexcept
|
||||
: reason_(std::move(other.reason_)),
|
||||
error_(other.error_),
|
||||
ptr_(other.ptr_ == &other.error_ ? &error_ : other.ptr_) {
|
||||
// Fix up the internal reason pointer after moving.
|
||||
error_.reason = reason_c_str();
|
||||
error_.reasonlen = reason_.length();
|
||||
}
|
||||
|
||||
QuicError& QuicError::operator=(QuicError&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reason_ = std::move(other.reason_);
|
||||
error_ = other.error_;
|
||||
ptr_ = (other.ptr_ == &other.error_) ? &error_ : other.ptr_;
|
||||
error_.reason = reason_c_str();
|
||||
error_.reasonlen = reason_.length();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
QuicError::QuicError(const QuicError& other)
|
||||
: reason_(other.reason_),
|
||||
error_(other.error_),
|
||||
ptr_(other.ptr_ == &other.error_ ? &error_ : other.ptr_) {
|
||||
error_.reason = reason_c_str();
|
||||
error_.reasonlen = reason_.length();
|
||||
}
|
||||
|
||||
QuicError& QuicError::operator=(const QuicError& other) {
|
||||
if (this != &other) {
|
||||
reason_ = other.reason_;
|
||||
error_ = other.error_;
|
||||
ptr_ = (other.ptr_ == &other.error_) ? &error_ : other.ptr_;
|
||||
error_.reason = reason_c_str();
|
||||
error_.reasonlen = reason_.length();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
const uint8_t* QuicError::reason_c_str() const {
|
||||
return reinterpret_cast<const uint8_t*>(reason_.c_str());
|
||||
}
|
||||
@@ -296,11 +348,13 @@ std::optional<int> QuicError::get_crypto_error() const {
|
||||
|
||||
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
|
||||
if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
|
||||
(type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR)) {
|
||||
(type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR) ||
|
||||
type() == Type::IDLE_CLOSE) {
|
||||
// Note that we only return undefined for *known* no-error application
|
||||
// codes. It is possible that other application types use other specific
|
||||
// no-error codes, but since we don't know which application is being used,
|
||||
// we'll just return the error code value for those below.
|
||||
// Idle close is always clean — the session timed out normally.
|
||||
return Undefined(env->isolate());
|
||||
}
|
||||
|
||||
|
||||
+27
-17
@@ -54,28 +54,28 @@ class Store final : public MemoryRetainer {
|
||||
size_t length,
|
||||
size_t offset = 0);
|
||||
|
||||
// Creates a Store from the contents of an ArrayBuffer, always detaching
|
||||
// it in the process. An empty Maybe will be returned if the ArrayBuffer
|
||||
// is not detachable or detaching failed (likely due to a detach key
|
||||
// mismatch).
|
||||
static v8::Maybe<Store> From(
|
||||
v8::Local<v8::ArrayBuffer> buffer,
|
||||
v8::Local<v8::Value> detach_key = v8::Local<v8::Value>());
|
||||
// Creates a Store by copying the contents of an ArrayBuffer into a fresh
|
||||
// BackingStore. The caller's buffer is not modified, so callers can safely
|
||||
// reuse or mutate it after the call returns. Returns an empty Maybe on
|
||||
// allocation failure, in which case an `ERR_MEMORY_ALLOCATION_FAILED`
|
||||
// exception will have been scheduled on the isolate.
|
||||
static v8::Maybe<Store> From(v8::Local<v8::ArrayBuffer> buffer);
|
||||
|
||||
// Creates a Store from the contents of an ArrayBufferView, always detaching
|
||||
// it in the process. An empty Maybe will be returned if the ArrayBuffer
|
||||
// is not detachable or detaching failed (likely due to a detach key
|
||||
// mismatch).
|
||||
static v8::Maybe<Store> From(
|
||||
v8::Local<v8::ArrayBufferView> view,
|
||||
v8::Local<v8::Value> detach_key = v8::Local<v8::Value>());
|
||||
// Creates a Store by copying the contents of an ArrayBufferView into a
|
||||
// fresh BackingStore. The caller's view (and its underlying ArrayBuffer)
|
||||
// is not modified. Returns an empty Maybe on allocation failure, in which
|
||||
// case an `ERR_MEMORY_ALLOCATION_FAILED` exception will have been
|
||||
// scheduled on the isolate.
|
||||
static v8::Maybe<Store> From(v8::Local<v8::ArrayBufferView> view);
|
||||
|
||||
// Creates a Store from the contents of an ArrayBuffer, always copying the
|
||||
// content.
|
||||
// content. Equivalent to `From()` but returns a Store directly without
|
||||
// surfacing allocation failure.
|
||||
static Store CopyFrom(v8::Local<v8::ArrayBuffer> buffer);
|
||||
|
||||
// Creates a Store from the contents of an ArrayBufferView, always copying the
|
||||
// content.
|
||||
// Creates a Store from the contents of an ArrayBufferView, always copying
|
||||
// the content. Equivalent to `From()` but returns a Store directly without
|
||||
// surfacing allocation failure.
|
||||
static Store CopyFrom(v8::Local<v8::ArrayBufferView> view);
|
||||
|
||||
v8::Local<v8::Uint8Array> ToUint8Array(Environment* env) const;
|
||||
@@ -208,6 +208,16 @@ class QuicError final : public MemoryRetainer {
|
||||
explicit QuicError(const ngtcp2_ccerr* ptr);
|
||||
explicit QuicError(const ngtcp2_ccerr& error);
|
||||
|
||||
// Move constructor and assignment must fix up ptr_ when it points
|
||||
// to the internal error_ member (as set by the default constructor
|
||||
// and the ForTransport/ForApplication factory methods).
|
||||
QuicError(QuicError&& other) noexcept;
|
||||
QuicError& operator=(QuicError&& other) noexcept;
|
||||
|
||||
// Copy constructor and assignment must also fix up ptr_.
|
||||
QuicError(const QuicError& other);
|
||||
QuicError& operator=(const QuicError& other);
|
||||
|
||||
Type type() const;
|
||||
error_code code() const;
|
||||
const std::string_view reason() const;
|
||||
|
||||
+45
-5
@@ -12,8 +12,8 @@
|
||||
namespace node::quic {
|
||||
|
||||
#define NGTCP2_SUCCESS 0
|
||||
#define NGTCP2_ERR(V) (V != NGTCP2_SUCCESS)
|
||||
#define NGTCP2_OK(V) (V == NGTCP2_SUCCESS)
|
||||
#define NGTCP2_ERR(V) ((V) != NGTCP2_SUCCESS)
|
||||
#define NGTCP2_OK(V) ((V) == NGTCP2_SUCCESS)
|
||||
|
||||
#define IF_QUIC_DEBUG(env) \
|
||||
if (env->enabled_debug_list()->enabled(DebugCategory::QUIC)) [[unlikely]]
|
||||
@@ -83,6 +83,39 @@ bool SetOption(Environment* env,
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Opt, uint16_t Opt::*member>
|
||||
bool SetOption(Environment* env,
|
||||
Opt* options,
|
||||
const v8::Local<v8::Object>& object,
|
||||
const v8::Local<v8::String>& name) {
|
||||
v8::Local<v8::Value> value;
|
||||
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
|
||||
if (!value->IsUndefined()) {
|
||||
if (!value->IsUint32()) {
|
||||
Utf8Value nameStr(env->isolate(), name);
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "The %s option must be an uint16", nameStr);
|
||||
return false;
|
||||
}
|
||||
v8::Local<v8::Uint32> num;
|
||||
if (!value->ToUint32(env->context()).ToLocal(&num)) {
|
||||
Utf8Value nameStr(env->isolate(), name);
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "The %s option must be an uint16", nameStr);
|
||||
return false;
|
||||
}
|
||||
uint32_t val = num->Value();
|
||||
if (val > 0xFFFF) {
|
||||
Utf8Value nameStr(env->isolate(), name);
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "The %s option must fit in a uint16", nameStr);
|
||||
return false;
|
||||
}
|
||||
options->*member = static_cast<uint16_t>(val);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Opt, uint64_t Opt::*member>
|
||||
bool SetOption(Environment* env,
|
||||
Opt* options,
|
||||
@@ -205,7 +238,7 @@ uint64_t GetStat(Stats* stats) {
|
||||
if (!GetConstructorTemplate(env) \
|
||||
->InstanceTemplate() \
|
||||
->NewInstance(env->context()) \
|
||||
.ToLocal(&obj)) { \
|
||||
.ToLocal(&name)) { \
|
||||
return ret; \
|
||||
}
|
||||
|
||||
@@ -214,7 +247,7 @@ uint64_t GetStat(Stats* stats) {
|
||||
if (!GetConstructorTemplate(env) \
|
||||
->InstanceTemplate() \
|
||||
->NewInstance(env->context()) \
|
||||
.ToLocal(&obj)) { \
|
||||
.ToLocal(&name)) { \
|
||||
return; \
|
||||
}
|
||||
|
||||
@@ -285,8 +318,14 @@ enum class StreamPriority : uint8_t {
|
||||
};
|
||||
|
||||
enum class StreamPriorityFlags : uint8_t {
|
||||
NONE,
|
||||
NON_INCREMENTAL,
|
||||
INCREMENTAL,
|
||||
};
|
||||
|
||||
enum class HeadersSupportState : uint8_t {
|
||||
UNKNOWN,
|
||||
SUPPORTED,
|
||||
UNSUPPORTED,
|
||||
};
|
||||
|
||||
enum class PathValidationResult : uint8_t {
|
||||
@@ -298,6 +337,7 @@ enum class PathValidationResult : uint8_t {
|
||||
enum class DatagramStatus : uint8_t {
|
||||
ACKNOWLEDGED,
|
||||
LOST,
|
||||
ABANDONED,
|
||||
};
|
||||
|
||||
#define CC_ALGOS(V) \
|
||||
|
||||
+258
-210
@@ -11,6 +11,7 @@
|
||||
#include <node_external_reference.h>
|
||||
#include <node_process-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <timer_wrap-inl.h>
|
||||
#include <util-inl.h>
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
@@ -21,6 +22,7 @@
|
||||
#include "endpoint.h"
|
||||
#include "http3.h"
|
||||
#include "ncrypto.h"
|
||||
#include "session_manager.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
@@ -53,6 +55,10 @@ namespace quic {
|
||||
V(CLOSING, closing, uint8_t) \
|
||||
/* Temporarily paused serving new initial requests */ \
|
||||
V(BUSY, busy, uint8_t) \
|
||||
/* Max concurrent connections per IP (0 = unlimited) */ \
|
||||
V(MAX_CONNECTIONS_PER_HOST, max_connections_per_host, uint16_t) \
|
||||
/* Max total concurrent connections (0 = unlimited) */ \
|
||||
V(MAX_CONNECTIONS_TOTAL, max_connections_total, uint16_t) \
|
||||
/* The number of pending send callbacks */ \
|
||||
V(PENDING_CALLBACKS, pending_callbacks, uint64_t)
|
||||
|
||||
@@ -189,7 +195,6 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
|
||||
env, &options, params, state.name##_string())
|
||||
|
||||
if (!SET(retry_token_expiration) || !SET(token_expiration) ||
|
||||
!SET(max_connections_per_host) || !SET(max_connections_total) ||
|
||||
!SET(max_stateless_resets) || !SET(address_lru_size) ||
|
||||
!SET(max_retries) || !SET(validate_address) ||
|
||||
!SET(disable_stateless_reset) || !SET(ipv6_only) ||
|
||||
@@ -197,7 +202,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
|
||||
!SET(rx_loss) || !SET(tx_loss) ||
|
||||
#endif
|
||||
!SET(udp_receive_buffer_size) || !SET(udp_send_buffer_size) ||
|
||||
!SET(udp_ttl) || !SET(reset_token_secret) || !SET(token_secret)) {
|
||||
!SET(udp_ttl) || !SET(idle_timeout) || !SET(reset_token_secret) ||
|
||||
!SET(token_secret)) {
|
||||
return Nothing<Options>();
|
||||
}
|
||||
|
||||
@@ -245,10 +251,6 @@ std::string Endpoint::Options::ToString() const {
|
||||
" seconds";
|
||||
res += prefix + "token expiration: " + std::to_string(token_expiration) +
|
||||
" seconds";
|
||||
res += prefix + "max connections per host: " +
|
||||
std::to_string(max_connections_per_host);
|
||||
res += prefix +
|
||||
"max connections total: " + std::to_string(max_connections_total);
|
||||
res +=
|
||||
prefix + "max stateless resets: " + std::to_string(max_stateless_resets);
|
||||
res += prefix + "address lru size: " + std::to_string(address_lru_size);
|
||||
@@ -268,6 +270,7 @@ std::string Endpoint::Options::ToString() const {
|
||||
res +=
|
||||
prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size);
|
||||
res += prefix + "udp ttl: " + std::to_string(udp_ttl);
|
||||
res += prefix + "idle timeout: " + std::to_string(idle_timeout) + " seconds";
|
||||
|
||||
res += indent.Close();
|
||||
return res;
|
||||
@@ -471,7 +474,6 @@ int Endpoint::UDP::Send(Packet::Ptr packet) {
|
||||
// Detach from the Ptr — libuv takes ownership until the callback fires.
|
||||
Packet* raw = packet.release();
|
||||
uv_buf_t buf = *raw;
|
||||
|
||||
int err = uv_udp_send(raw->req(),
|
||||
&impl_->handle_,
|
||||
&buf,
|
||||
@@ -532,8 +534,6 @@ void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
|
||||
ENDPOINT_STATE(V)
|
||||
#undef V
|
||||
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS);
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS_PER_HOST);
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE);
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS);
|
||||
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT);
|
||||
@@ -593,9 +593,15 @@ Endpoint::Endpoint(Environment* env,
|
||||
packet_pool_(kDefaultMaxPacketLength,
|
||||
ArenaPool<Packet>::kDefaultSlotsPerBlock),
|
||||
udp_(this),
|
||||
addrLRU_(options_.address_lru_size) {
|
||||
idle_timer_(env,
|
||||
[this] {
|
||||
HandleScope scope(this->env()->isolate());
|
||||
Destroy();
|
||||
}),
|
||||
addr_validation_lru_(options_.address_lru_size) {
|
||||
MakeWeak();
|
||||
udp_.Unref();
|
||||
idle_timer_.Unref();
|
||||
STAT_RECORD_TIMESTAMP(Stats, created_at);
|
||||
IF_QUIC_DEBUG(env) {
|
||||
Debug(this, "Endpoint created. Options %s", options.ToString());
|
||||
@@ -640,11 +646,12 @@ RegularToken Endpoint::GenerateNewToken(uint32_t version,
|
||||
return RegularToken(version, remote_address, options_.token_secret);
|
||||
}
|
||||
|
||||
SessionManager& Endpoint::session_manager() const {
|
||||
return BindingData::Get(env()).session_manager();
|
||||
}
|
||||
|
||||
StatelessResetToken Endpoint::GenerateNewStatelessResetToken(
|
||||
uint8_t* token, const CID& cid) const {
|
||||
Debug(const_cast<Endpoint*>(this),
|
||||
"Generating new stateless reset token for CID %s",
|
||||
cid);
|
||||
DCHECK(!is_closed() && !is_closing());
|
||||
return StatelessResetToken(token, options_.reset_token_secret, cid);
|
||||
}
|
||||
@@ -652,9 +659,38 @@ StatelessResetToken Endpoint::GenerateNewStatelessResetToken(
|
||||
void Endpoint::AddSession(const CID& cid, BaseObjectPtr<Session> session) {
|
||||
DCHECK(!is_closed() && !is_closing());
|
||||
Debug(this, "Adding session for CID %s", cid);
|
||||
IncrementSocketAddressCounter(session->remote_address());
|
||||
if (state_->max_connections_per_host > 0) {
|
||||
conn_counts_per_host_[session->remote_address()]++;
|
||||
}
|
||||
auto& mgr = session_manager();
|
||||
// Associate peer-chosen CIDs in the local dcid_to_scid_ map.
|
||||
AssociateCID(session->config().dcid, session->config().scid);
|
||||
sessions_[cid] = session;
|
||||
mgr.AddSession(cid, session);
|
||||
mgr.SetPrimaryEndpoint(session.get(), this);
|
||||
// For server sessions, associate the client's original DCID (ocid) so
|
||||
// that 0-RTT packets arriving in a separate UDP datagram can be routed
|
||||
// to this session. This must happen after the session is added (so
|
||||
// FindSession can resolve the mapping) but before EmitNewSession (which
|
||||
// runs JS and may yield to libuv, allowing the 0-RTT packet to arrive).
|
||||
if (session->is_server() && session->config().ocid) {
|
||||
AssociateCID(session->config().ocid, session->config().scid);
|
||||
}
|
||||
// After Retry, the client continues to use the Retry SCID as its DCID
|
||||
// until the handshake completes. Register it so retransmitted Initials
|
||||
// and subsequent handshake packets can be routed to this session.
|
||||
if (session->is_server() && session->config().retry_scid) {
|
||||
AssociateCID(session->config().retry_scid, session->config().scid);
|
||||
}
|
||||
// Increment the primary session count and ref the handle BEFORE
|
||||
// EmitNewSession. EmitNewSession calls into JS, which may close/destroy
|
||||
// the session synchronously. The session's ~Impl calls RemoveSession
|
||||
// which decrements the count. If we increment after EmitNewSession,
|
||||
// RemoveSession would see count=0 and the count would be permanently
|
||||
// off by one.
|
||||
if (primary_session_count_++ == 0) {
|
||||
idle_timer_.Stop();
|
||||
udp_.Ref();
|
||||
}
|
||||
if (session->is_server()) {
|
||||
STAT_INCREMENT(Stats, server_sessions);
|
||||
// We only emit the new session event for server sessions.
|
||||
@@ -664,46 +700,58 @@ void Endpoint::AddSession(const CID& cid, BaseObjectPtr<Session> session) {
|
||||
} else {
|
||||
STAT_INCREMENT(Stats, client_sessions);
|
||||
}
|
||||
udp_.Ref();
|
||||
}
|
||||
|
||||
void Endpoint::RemoveSession(const CID& cid,
|
||||
const SocketAddress& remote_address) {
|
||||
if (is_closed()) return;
|
||||
Debug(this, "Removing session for CID %s", cid);
|
||||
if (sessions_.erase(cid)) {
|
||||
DecrementSocketAddressCounter(remote_address);
|
||||
auto it = conn_counts_per_host_.find(remote_address);
|
||||
if (it != conn_counts_per_host_.end()) {
|
||||
if (--it->second == 0) {
|
||||
conn_counts_per_host_.erase(it);
|
||||
}
|
||||
}
|
||||
if (sessions_.empty()) {
|
||||
if (primary_session_count_ > 0 && --primary_session_count_ == 0) {
|
||||
udp_.Unref();
|
||||
session_manager().RemoveSession(cid);
|
||||
// The endpoint may be idle (no sessions, not listening). MaybeDestroy
|
||||
// handles both closing (immediate destroy) and idle timeout (start
|
||||
// timer or destroy based on idle_timeout setting).
|
||||
MaybeDestroy();
|
||||
return;
|
||||
}
|
||||
session_manager().RemoveSession(cid);
|
||||
if (state_->closing == 1) MaybeDestroy();
|
||||
}
|
||||
|
||||
BaseObjectPtr<Session> Endpoint::FindSession(const CID& cid) {
|
||||
auto session_it = sessions_.find(cid);
|
||||
if (session_it == std::end(sessions_)) {
|
||||
// If our given cid is not a match that doesn't mean we
|
||||
// give up. A session might be identified by multiple
|
||||
// CIDs. Let's see if our secondary map has a match!
|
||||
auto scid_it = dcid_to_scid_.find(cid);
|
||||
if (scid_it != std::end(dcid_to_scid_)) {
|
||||
session_it = sessions_.find(scid_it->second);
|
||||
CHECK_NE(session_it, std::end(sessions_));
|
||||
return session_it->second;
|
||||
}
|
||||
// No match found.
|
||||
return {};
|
||||
// First, try the SessionManager's primary sessions_ map directly.
|
||||
// This handles the common case where the CID is a locally-generated SCID.
|
||||
auto session = session_manager().FindSession(cid);
|
||||
if (session) return session;
|
||||
|
||||
// If not found, check this endpoint's local dcid_to_scid_ map for a
|
||||
// secondary CID mapping. This map contains peer-chosen CID values that
|
||||
// are only meaningful in the context of this endpoint's sessions.
|
||||
auto scid_it = dcid_to_scid_.find(cid);
|
||||
if (scid_it != dcid_to_scid_.end()) {
|
||||
session = session_manager().FindSession(scid_it->second);
|
||||
if (session) return session;
|
||||
// Stale mapping — clean up.
|
||||
dcid_to_scid_.erase(scid_it);
|
||||
}
|
||||
// Match found!
|
||||
return session_it->second;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void Endpoint::AssociateCID(const CID& cid, const CID& scid) {
|
||||
if (!is_closed() && !is_closing() && cid && scid && cid != scid &&
|
||||
dcid_to_scid_[cid] != scid) {
|
||||
Debug(this, "Associating CID %s with SCID %s", cid, scid);
|
||||
dcid_to_scid_.emplace(cid, scid);
|
||||
if (!is_closed() && !is_closing() && cid && scid && cid != scid) {
|
||||
auto it = dcid_to_scid_.find(cid);
|
||||
if (it == dcid_to_scid_.end() || it->second != scid) {
|
||||
Debug(this, "Associating CID %s with SCID %s", cid, scid);
|
||||
dcid_to_scid_[cid] = scid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,14 +766,14 @@ void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token,
|
||||
Session* session) {
|
||||
if (is_closed() || is_closing()) return;
|
||||
Debug(this, "Associating stateless reset token %s with session", token);
|
||||
token_map_[token] = session;
|
||||
session_manager().AssociateStatelessResetToken(token, session);
|
||||
}
|
||||
|
||||
void Endpoint::DisassociateStatelessResetToken(
|
||||
const StatelessResetToken& token) {
|
||||
if (!is_closed()) {
|
||||
Debug(this, "Disassociating stateless reset token %s", token);
|
||||
token_map_.erase(token);
|
||||
session_manager().DisassociateStatelessResetToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,7 +822,7 @@ void Endpoint::SendRetry(const PathDescriptor& options) {
|
||||
// its own. What this count does not give is the rate of retry, so it is still
|
||||
// somewhat limited.
|
||||
Debug(this, "Sending retry on path %s", options);
|
||||
auto info = addrLRU_.Upsert(options.remote_address);
|
||||
auto info = addr_validation_lru_.Upsert(options.remote_address);
|
||||
if (++(info->retry_count) <= options_.max_retries) {
|
||||
auto packet =
|
||||
Packet::CreateRetryPacket(*this, options, options_.token_secret);
|
||||
@@ -821,24 +869,29 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options,
|
||||
|
||||
const auto exceeds_limits = [&] {
|
||||
SocketAddressInfoTraits::Type* counts =
|
||||
addrLRU_.Peek(options.remote_address);
|
||||
addr_validation_lru_.Peek(options.remote_address);
|
||||
auto count = counts != nullptr ? counts->reset_count : 0;
|
||||
return count >= options_.max_stateless_resets;
|
||||
};
|
||||
|
||||
// Per the QUIC spec, we need to protect against sending too many stateless
|
||||
// reset tokens to an endpoint to prevent endless looping.
|
||||
if (exceeds_limits()) return false;
|
||||
if (exceeds_limits()) {
|
||||
Debug(this, "Stateless reset rate limit exceeded");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto packet = Packet::CreateStatelessResetPacket(
|
||||
*this, options, options_.reset_token_secret, source_len);
|
||||
|
||||
if (packet) {
|
||||
addrLRU_.Upsert(options.remote_address)->reset_count++;
|
||||
Debug(this, "Sending stateless reset packet (%zu bytes)", packet->length());
|
||||
addr_validation_lru_.Upsert(options.remote_address)->reset_count++;
|
||||
STAT_INCREMENT(Stats, stateless_reset_count);
|
||||
Send(std::move(packet));
|
||||
return true;
|
||||
}
|
||||
Debug(this, "Failed to create stateless reset packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -886,8 +939,9 @@ bool Endpoint::Start() {
|
||||
return false;
|
||||
}
|
||||
|
||||
BindingData::Get(env()).listening_endpoints[this] =
|
||||
BaseObjectPtr<Endpoint>(this);
|
||||
auto& binding = BindingData::Get(env());
|
||||
binding.listening_endpoints[this] = BaseObjectPtr<Endpoint>(this);
|
||||
binding.session_manager().RegisterEndpoint(this, udp_.local_address());
|
||||
state_->receiving = 1;
|
||||
return true;
|
||||
}
|
||||
@@ -929,6 +983,7 @@ void Endpoint::Listen(const Session::Options& options) {
|
||||
};
|
||||
if (Start()) {
|
||||
Debug(this, "Listening with options %s", server_state_->options);
|
||||
idle_timer_.Stop();
|
||||
state_->listening = 1;
|
||||
}
|
||||
}
|
||||
@@ -978,13 +1033,25 @@ BaseObjectPtr<Session> Endpoint::Connect(
|
||||
}
|
||||
|
||||
void Endpoint::MaybeDestroy() {
|
||||
if (!is_closed() && sessions_.empty() && state_->pending_callbacks == 0 &&
|
||||
state_->listening == 0) {
|
||||
// Destroy potentially creates v8 handles so let's make sure
|
||||
// we have a HandleScope on the stack.
|
||||
HandleScope scope(env()->isolate());
|
||||
Destroy();
|
||||
if (is_closed() || primary_session_count_ > 0 ||
|
||||
state_->pending_callbacks > 0 || state_->listening == 1) {
|
||||
return;
|
||||
}
|
||||
if (options_.idle_timeout > 0) {
|
||||
// Start the idle timer. If it fires before a new session or listen
|
||||
// call reactivates this endpoint, the endpoint will be destroyed.
|
||||
idle_timer_.Update(options_.idle_timeout * 1000);
|
||||
return;
|
||||
}
|
||||
// With idle_timeout == 0, only destroy if the endpoint is actively
|
||||
// closing (via close() or CloseGracefully). An idle endpoint that
|
||||
// is not closing stays alive with an unref'd handle so the process
|
||||
// can still exit.
|
||||
if (state_->closing != 1) return;
|
||||
// Destroy potentially creates v8 handles so let's make sure
|
||||
// we have a HandleScope on the stack.
|
||||
HandleScope scope(env()->isolate());
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void Endpoint::Destroy(CloseContext context, int status) {
|
||||
@@ -1020,14 +1087,9 @@ void Endpoint::Destroy(CloseContext context, int status) {
|
||||
// If there are open sessions still, shut them down. As those clean themselves
|
||||
// up, they will remove themselves. The cleanup here will be synchronous and
|
||||
// no attempt will be made to communicate further with the peer.
|
||||
// Intentionally copy the sessions map so that we can safely iterate over it
|
||||
// while those clean themselves up.
|
||||
auto sessions = sessions_;
|
||||
for (auto& session : sessions)
|
||||
session.second->Close(Session::CloseMethod::SILENT);
|
||||
sessions.clear();
|
||||
DCHECK(sessions_.empty());
|
||||
token_map_.clear();
|
||||
idle_timer_.Close();
|
||||
session_manager().CloseAllSessionsFor(this);
|
||||
DCHECK_EQ(primary_session_count_, 0);
|
||||
dcid_to_scid_.clear();
|
||||
server_state_.reset();
|
||||
|
||||
@@ -1035,7 +1097,9 @@ void Endpoint::Destroy(CloseContext context, int status) {
|
||||
state_->closing = 0;
|
||||
state_->bound = 0;
|
||||
state_->receiving = 0;
|
||||
BindingData::Get(env()).listening_endpoints.erase(this);
|
||||
auto& binding = BindingData::Get(env());
|
||||
binding.listening_endpoints.erase(this);
|
||||
binding.session_manager().UnregisterEndpoint(this);
|
||||
STAT_RECORD_TIMESTAMP(Stats, destroyed_at);
|
||||
|
||||
EmitClose(close_context_, close_status_);
|
||||
@@ -1045,7 +1109,6 @@ void Endpoint::CloseGracefully() {
|
||||
if (is_closed() || is_closing()) return;
|
||||
|
||||
Debug(this, "Closing gracefully");
|
||||
|
||||
state_->listening = 0;
|
||||
state_->closing = 1;
|
||||
|
||||
@@ -1062,7 +1125,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
const CID& dcid,
|
||||
const CID& scid) {
|
||||
DCHECK_NOT_NULL(session);
|
||||
DCHECK(!session->is_destroyed());
|
||||
if (session->is_destroyed()) return;
|
||||
size_t len = store.length();
|
||||
if (session->Receive(std::move(store), local_address, remote_address)) {
|
||||
STAT_INCREMENT_N(Stats, bytes_received, len);
|
||||
@@ -1129,32 +1192,28 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
return;
|
||||
}
|
||||
|
||||
// If ngtcp2_is_supported_version returns a non-zero value, the version is
|
||||
// recognized and supported. If it returns 0, we'll go ahead and send a
|
||||
// version negotiation packet in response.
|
||||
if (ngtcp2_is_supported_version(hd.version) == 0) {
|
||||
Debug(this,
|
||||
"Packet not acceptable because the version (%d) is not supported. "
|
||||
"Will attempt to send version negotiation",
|
||||
hd.version);
|
||||
SendVersionNegotiation(
|
||||
PathDescriptor{version, dcid, scid, local_address, remote_address});
|
||||
// The packet was successfully processed, even if we did refuse the
|
||||
// connection.
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
// Unsupported versions are handled earlier in Receive() via the
|
||||
// NGTCP2_ERR_VERSION_NEGOTIATION return from ngtcp2_pkt_decode_version_cid.
|
||||
// If we reach here, the version must be supported.
|
||||
CHECK_NE(ngtcp2_is_supported_version(hd.version), 0);
|
||||
|
||||
// This is the next important condition check... If the server has been
|
||||
// marked busy or the remote peer has exceeded their maximum number of
|
||||
// concurrent connections, any new connections will be shut down
|
||||
// immediately.
|
||||
const auto limits_exceeded = ([&] {
|
||||
if (sessions_.size() >= options_.max_connections_total) return true;
|
||||
|
||||
SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address);
|
||||
auto count = counts != nullptr ? counts->active_connections : 0;
|
||||
return count >= options_.max_connections_per_host;
|
||||
if (state_->max_connections_total > 0 &&
|
||||
primary_session_count_ >= state_->max_connections_total) {
|
||||
return true;
|
||||
}
|
||||
if (state_->max_connections_per_host > 0) {
|
||||
auto it = conn_counts_per_host_.find(remote_address);
|
||||
if (it != conn_counts_per_host_.end() &&
|
||||
it->second >= state_->max_connections_per_host) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (state_->busy || limits_exceeded) {
|
||||
@@ -1168,7 +1227,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
// the same.
|
||||
if (state_->busy) STAT_INCREMENT(Stats, server_busy_count);
|
||||
SendImmediateConnectionClose(
|
||||
PathDescriptor{version, scid, dcid, local_address, remote_address},
|
||||
PathDescriptor{version, dcid, scid, local_address, remote_address},
|
||||
QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED));
|
||||
// The packet was successfully processed, even if we did refuse the
|
||||
// connection.
|
||||
@@ -1179,6 +1238,12 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
Debug(
|
||||
this, "Accepting initial packet for %s from %s", dcid, remote_address);
|
||||
|
||||
// Generate a fresh server SCID rather than reusing the client's original
|
||||
// DCID. The client's original DCID is typically short (8 bytes) and we
|
||||
// need a 20-byte SCID to properly match short_dcidlen passed to
|
||||
// ngtcp2_pkt_decode_version_cid.
|
||||
auto server_scid = server_state_->options.cid_factory->Generate();
|
||||
|
||||
// At this point, we start to set up the configuration for our local
|
||||
// session. We pass the received scid here as the dcid argument value
|
||||
// because that is the value *this* session will use as the outbound dcid.
|
||||
@@ -1189,55 +1254,86 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
local_address,
|
||||
remote_address,
|
||||
scid,
|
||||
dcid,
|
||||
server_scid,
|
||||
dcid);
|
||||
|
||||
Debug(this, "Using session config %s", config);
|
||||
|
||||
// The this point, the config.scid and config.dcid represent *our* views of
|
||||
// the CIDs. Specifically, config.dcid identifies the peer and config.scid
|
||||
// identifies us. config.dcid should equal scid, and config.scid should
|
||||
// equal dcid.
|
||||
// identifies us. config.dcid should equal scid (peer's SCID is our DCID),
|
||||
// and config.ocid should equal dcid (peer's original DCID).
|
||||
DCHECK(config.dcid == scid);
|
||||
DCHECK(config.scid == dcid);
|
||||
DCHECK(config.ocid == dcid);
|
||||
|
||||
const auto is_remote_address_validated = ([&] {
|
||||
auto info = addrLRU_.Peek(remote_address);
|
||||
auto info = addr_validation_lru_.Peek(remote_address);
|
||||
return info != nullptr ? info->validated : false;
|
||||
})();
|
||||
|
||||
// QUIC has address validation built in to the handshake but allows for
|
||||
// an additional explicit validation request using RETRY frames. If we
|
||||
// are using explicit validation, we check for the existence of a valid
|
||||
// token in the packet. If one does not exist, we send a retry with
|
||||
// a new token. If it does exist, and if it is valid, we grab the original
|
||||
// cid and continue.
|
||||
if (!is_remote_address_validated) {
|
||||
// Retry token processing and address validation are two separate
|
||||
// concerns. A retry token MUST always be parsed when present because
|
||||
// it carries the original_destination_connection_id (ODCID) that the
|
||||
// server must echo in its transport parameters. Without it, the peer
|
||||
// will reject the connection with PROTOCOL_VIOLATION.
|
||||
//
|
||||
// The address validation LRU cache determines whether we need to
|
||||
// *send* a Retry, but must NOT skip *processing* an incoming retry
|
||||
// token — a concurrent connection may have already validated the
|
||||
// address (populating the LRU) while this connection's Retry was
|
||||
// still in flight.
|
||||
|
||||
// Step 1: Always process a retry token if present, to extract the
|
||||
// ODCID regardless of address validation state.
|
||||
if (hd.type == NGTCP2_PKT_INITIAL && hd.tokenlen > 0 &&
|
||||
hd.token[0] == RetryToken::kTokenMagic) {
|
||||
RetryToken token(hd.token, hd.tokenlen);
|
||||
Debug(this,
|
||||
"Initial packet from %s has retry token %s",
|
||||
remote_address,
|
||||
token);
|
||||
auto ocid =
|
||||
token.Validate(version,
|
||||
remote_address,
|
||||
dcid,
|
||||
options_.token_secret,
|
||||
options_.retry_token_expiration * NGTCP2_SECONDS);
|
||||
if (!ocid.has_value()) {
|
||||
Debug(this, "Retry token from %s is invalid.", remote_address);
|
||||
SendImmediateConnectionClose(
|
||||
PathDescriptor{version, scid, dcid, local_address, remote_address},
|
||||
QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED));
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug(this,
|
||||
"Retry token from %s is valid. Original dcid %s",
|
||||
remote_address,
|
||||
ocid.value());
|
||||
config.ocid = ocid.value();
|
||||
config.retry_scid = dcid;
|
||||
config.set_token(token);
|
||||
|
||||
// Mark the address as validated since the retry round-trip proves
|
||||
// reachability.
|
||||
Debug(this, "Remote address %s is validated", remote_address);
|
||||
addr_validation_lru_.Upsert(remote_address)->validated = true;
|
||||
}
|
||||
|
||||
// Step 2: Address validation — decide whether to send a Retry or
|
||||
// accept the packet. This only applies when the address has not
|
||||
// been validated yet (no LRU hit and no retry token above).
|
||||
if (!is_remote_address_validated && !config.retry_scid) {
|
||||
Debug(this, "Remote address %s is not validated", remote_address);
|
||||
switch (hd.type) {
|
||||
case NGTCP2_PKT_INITIAL:
|
||||
// First, let's see if we need to do anything here.
|
||||
|
||||
if (options_.validate_address) {
|
||||
// If there is no token, generate and send one.
|
||||
if (hd.tokenlen == 0) {
|
||||
Debug(this,
|
||||
"Initial packet has no token. Sending retry to %s to start "
|
||||
"validation",
|
||||
remote_address);
|
||||
// In this case we sent a retry to the remote peer and return
|
||||
// without creating a session. What we expect to happen next is
|
||||
// that the remote peer will try again with a new initial packet
|
||||
// that includes the retry token we are sending them. It's
|
||||
// possible, however, that they just give up and go away or send
|
||||
// us another initial packet that does not have the token. In that
|
||||
// case we'll end up right back here asking them to validate
|
||||
// again.
|
||||
//
|
||||
// It is possible that the SendRetry(...) won't actually send a
|
||||
// retry if the remote address has exceeded the maximum number of
|
||||
// retry attempts it is allowed as tracked by the addressLRU
|
||||
// cache. In that case, we'll just drop the packet on the floor.
|
||||
SendRetry(PathDescriptor{
|
||||
version,
|
||||
dcid,
|
||||
@@ -1245,53 +1341,12 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
local_address,
|
||||
remote_address,
|
||||
});
|
||||
// We still consider this a successfully handled packet even
|
||||
// if we send a retry.
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have two kinds of tokens, each prefixed with a different
|
||||
// magic byte.
|
||||
// Non-retry tokens (regular tokens).
|
||||
switch (hd.token[0]) {
|
||||
case RetryToken::kTokenMagic: {
|
||||
RetryToken token(hd.token, hd.tokenlen);
|
||||
Debug(this,
|
||||
"Initial packet from %s has retry token %s",
|
||||
remote_address,
|
||||
token);
|
||||
auto ocid = token.Validate(
|
||||
version,
|
||||
remote_address,
|
||||
dcid,
|
||||
options_.token_secret,
|
||||
options_.retry_token_expiration * NGTCP2_SECONDS);
|
||||
if (!ocid.has_value()) {
|
||||
Debug(
|
||||
this, "Retry token from %s is invalid.", remote_address);
|
||||
// Invalid retry token was detected. Close the connection.
|
||||
SendImmediateConnectionClose(
|
||||
PathDescriptor{
|
||||
version, scid, dcid, local_address, remote_address},
|
||||
QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED));
|
||||
// We still consider this a successfully handled packet even
|
||||
// if we send a connection close.
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
|
||||
// The ocid is the original dcid that was encoded into the
|
||||
// original retry packet sent to the client. We use it for
|
||||
// validation.
|
||||
Debug(this,
|
||||
"Retry token from %s is valid. Original dcid %s",
|
||||
remote_address,
|
||||
ocid.value());
|
||||
config.ocid = ocid.value();
|
||||
config.retry_scid = dcid;
|
||||
config.set_token(token);
|
||||
break;
|
||||
}
|
||||
case RegularToken::kTokenMagic: {
|
||||
RegularToken token(hd.token, hd.tokenlen);
|
||||
Debug(this,
|
||||
@@ -1306,10 +1361,6 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
Debug(this,
|
||||
"Regular token from %s is invalid.",
|
||||
remote_address);
|
||||
// If the regular token is invalid, let's send a retry to be
|
||||
// lenient. There's a small risk that a malicious peer is
|
||||
// trying to make us do some work but the risk is fairly low
|
||||
// here.
|
||||
SendRetry(PathDescriptor{
|
||||
version,
|
||||
dcid,
|
||||
@@ -1317,8 +1368,6 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
local_address,
|
||||
remote_address,
|
||||
});
|
||||
// We still consider this to be a successfully handled packet
|
||||
// if a retry is sent.
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
@@ -1330,13 +1379,6 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
Debug(this,
|
||||
"Initial packet from %s has unknown token type",
|
||||
remote_address);
|
||||
// If our prefix bit does not match anything we know about,
|
||||
// let's send a retry to be lenient. There's a small risk that a
|
||||
// malicious peer is trying to make us do some work but the risk
|
||||
// is fairly low here. The SendRetry will avoid sending a retry
|
||||
// if the remote address has exceeded the maximum number of
|
||||
// retry attempts it is allowed as tracked by the addressLRU
|
||||
// cache.
|
||||
SendRetry(PathDescriptor{
|
||||
version,
|
||||
dcid,
|
||||
@@ -1349,33 +1391,16 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
}
|
||||
}
|
||||
|
||||
// Ok! If we've got this far, our token is valid! Which means our
|
||||
// path to the remote address is valid (for now). Let's record that
|
||||
// so we don't have to do this dance again for this endpoint
|
||||
// instance.
|
||||
Debug(this, "Remote address %s is validated", remote_address);
|
||||
addrLRU_.Upsert(remote_address)->validated = true;
|
||||
addr_validation_lru_.Upsert(remote_address)->validated = true;
|
||||
} else if (hd.tokenlen > 0) {
|
||||
Debug(this,
|
||||
"Ignoring initial packet from %s with unexpected token",
|
||||
remote_address);
|
||||
// If validation is turned off and there is a token, that's weird.
|
||||
// The peer should only have a token if we sent it to them and we
|
||||
// wouldn't have sent it unless validation was turned on. Let's
|
||||
// assume the peer is buggy or malicious and drop the packet on the
|
||||
// floor.
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case NGTCP2_PKT_0RTT:
|
||||
// 0-RTT packets are inherently replayable and could be sent
|
||||
// from a spoofed source address to trigger amplification.
|
||||
// When address validation is enabled, we send a Retry to
|
||||
// force the client to prove it can receive at its claimed
|
||||
// address. This adds a round trip but prevents amplification
|
||||
// attacks. When address validation is disabled (e.g., on
|
||||
// trusted networks), we skip the Retry and allow 0-RTT to
|
||||
// proceed without additional validation.
|
||||
if (options_.validate_address) {
|
||||
Debug(
|
||||
this, "Sending retry to %s due to 0RTT packet", remote_address);
|
||||
@@ -1434,12 +1459,13 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
// If a Session has been associated with the token, then it is a valid
|
||||
// stateless reset token. We need to dispatch it to the session to be
|
||||
// processed.
|
||||
auto it = token_map_.find(StatelessResetToken(vec.base));
|
||||
if (it != token_map_.end()) {
|
||||
auto* session = session_manager().FindSessionByStatelessResetToken(
|
||||
StatelessResetToken(vec.base));
|
||||
if (session != nullptr) {
|
||||
// If the session happens to have been destroyed already, we'll
|
||||
// just ignore the packet.
|
||||
if (!it->second->is_destroyed()) [[likely]] {
|
||||
receive(it->second,
|
||||
if (!session->is_destroyed()) [[likely]] {
|
||||
receive(session,
|
||||
std::move(store),
|
||||
local_address,
|
||||
remote_address,
|
||||
@@ -1491,10 +1517,29 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
// cannot be processed; all we can do is ignore it. If it succeeds, we have a
|
||||
// valid QUIC header but there is still no guarantee that the packet can be
|
||||
// successfully processed.
|
||||
if (ngtcp2_pkt_decode_version_cid(
|
||||
&pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN) < 0) {
|
||||
Debug(this, "Failed to decode packet header, ignoring");
|
||||
return; // Ignore the packet!
|
||||
switch (ngtcp2_pkt_decode_version_cid(
|
||||
&pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN)) {
|
||||
case 0:
|
||||
break; // Supported version, continue processing.
|
||||
case NGTCP2_ERR_VERSION_NEGOTIATION: {
|
||||
// The packet has an unsupported version but the CIDs were
|
||||
// successfully decoded. Send a Version Negotiation response
|
||||
// per RFC 9000 Section 6. The VN packet's DCID is the client's
|
||||
// SCID and vice versa (mirrored back to the client).
|
||||
Debug(this,
|
||||
"Packet version %d is not supported, sending version negotiation",
|
||||
pversion_cid.version);
|
||||
CID dcid(pversion_cid.dcid, pversion_cid.dcidlen);
|
||||
CID scid(pversion_cid.scid, pversion_cid.scidlen);
|
||||
SendVersionNegotiation(PathDescriptor{
|
||||
pversion_cid.version, dcid, scid, local_address(), remote_address});
|
||||
STAT_INCREMENT(Stats, packets_received);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
// Truly invalid packet — cannot be decoded at all.
|
||||
Debug(this, "Failed to decode packet header, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
// QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any
|
||||
@@ -1550,11 +1595,26 @@ void Endpoint::Receive(const uv_buf_t& buf,
|
||||
// stateless reset, the packet will be handled with no additional action
|
||||
// necessary here. We want to return immediately without committing any
|
||||
// further resources.
|
||||
if (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) {
|
||||
if (pversion_cid.version == 0 &&
|
||||
maybeStatelessReset(dcid, scid, store, addr, remote_address)) {
|
||||
Debug(this, "Packet was a stateless reset");
|
||||
return; // Stateless reset! Don't do any further processing.
|
||||
}
|
||||
|
||||
// If this is a short header packet for an unknown DCID, send a
|
||||
// stateless reset so the peer knows the session is gone. Short header
|
||||
// packets are identified by version == 0 (set by ngtcp2_pkt_decode_
|
||||
// version_cid). We must NOT use !scid here because long header Initial
|
||||
// packets can have a 0-length SCID (valid per RFC 9000 Section 7.2).
|
||||
if (pversion_cid.version == 0) {
|
||||
Debug(this, "Sending stateless reset for unknown short header packet");
|
||||
SendStatelessReset(
|
||||
PathDescriptor{
|
||||
pversion_cid.version, dcid, scid, addr, remote_address},
|
||||
store.length());
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the packet as an initial packet...
|
||||
return acceptInitialPacket(pversion_cid.version,
|
||||
dcid,
|
||||
@@ -1587,18 +1647,9 @@ void Endpoint::PacketDone(int status) {
|
||||
DCHECK_GE(state_->pending_callbacks, 1);
|
||||
state_->pending_callbacks--;
|
||||
env()->DecreaseWaitingRequestCounter();
|
||||
// Can we go ahead and close now?
|
||||
if (state_->closing == 1) MaybeDestroy();
|
||||
}
|
||||
|
||||
void Endpoint::IncrementSocketAddressCounter(const SocketAddress& addr) {
|
||||
addrLRU_.Upsert(addr)->active_connections++;
|
||||
}
|
||||
|
||||
void Endpoint::DecrementSocketAddressCounter(const SocketAddress& addr) {
|
||||
auto* counts = addrLRU_.Peek(addr);
|
||||
if (counts != nullptr && counts->active_connections > 0)
|
||||
counts->active_connections--;
|
||||
// Check if we can close or start the idle timer now that this
|
||||
// pending callback has completed.
|
||||
if (state_->closing == 1 || primary_session_count_ == 0) MaybeDestroy();
|
||||
}
|
||||
|
||||
bool Endpoint::is_closed() const {
|
||||
@@ -1619,10 +1670,7 @@ void Endpoint::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackField("server_options", server_state_->options);
|
||||
tracker->TrackField("server_tls_context", server_state_->tls_context);
|
||||
}
|
||||
tracker->TrackField("token_map", token_map_);
|
||||
tracker->TrackField("sessions", sessions_);
|
||||
tracker->TrackField("cid_map", dcid_to_scid_);
|
||||
tracker->TrackField("address LRU", addrLRU_);
|
||||
tracker->TrackField("address LRU", addr_validation_lru_);
|
||||
}
|
||||
|
||||
// ======================================================================================
|
||||
|
||||
+61
-25
@@ -6,6 +6,7 @@
|
||||
#include <async_wrap.h>
|
||||
#include <env.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include <timer_wrap.h>
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
#include <algorithm>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include "bindingdata.h"
|
||||
#include "packet.h"
|
||||
#include "session.h"
|
||||
#include "session_manager.h"
|
||||
#include "sessionticket.h"
|
||||
#include "tokens.h"
|
||||
|
||||
@@ -24,12 +26,25 @@ namespace node::quic {
|
||||
// client and server simultaneously.
|
||||
class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
public:
|
||||
static constexpr uint64_t DEFAULT_MAX_CONNECTIONS =
|
||||
std::min<uint64_t>(kMaxSizeT, kMaxSafeJsInteger);
|
||||
static constexpr uint64_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100;
|
||||
static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE =
|
||||
(DEFAULT_MAX_CONNECTIONS_PER_HOST * 10);
|
||||
// The socket address LRU is used for tracking validated remote addresses.
|
||||
static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = 1024;
|
||||
|
||||
// The max stateless resets is the maximum number of stateless reset packets
|
||||
// that the Endpoint will generate for a given remote host within a window of
|
||||
// time (while tracking that host in the socket address LRU). This is not
|
||||
// mandated by QUIC, and the limit is arbitrary. We can set it to whatever
|
||||
// we'd like. The purpose is to prevent a malicious peer from intentionally
|
||||
// triggering generation of a large number of stateless resets. Once the
|
||||
// limit is reached, packets that would have otherwise triggered generation
|
||||
// of a stateless reset will simply be dropped instead.
|
||||
static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
|
||||
|
||||
// Similar to stateless resets, the max retry limit is the maximum number of
|
||||
// retry packets that the Endpoint will generate for a given remote host
|
||||
// within a window of time (while tracking that host in the socket address
|
||||
// LRU). This is not mandated by QUIC, and the limit is arbitrary. We can set
|
||||
// it to whatever we'd like. The purpose is to prevent a malicious peer from
|
||||
// intentionally triggering generation of a large number of retries.
|
||||
static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
|
||||
|
||||
// Endpoint configuration options
|
||||
@@ -50,17 +65,10 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS;
|
||||
|
||||
// Tokens issued using NEW_TOKEN are time-limited. By default, tokens expire
|
||||
// after DEFAULT_TOKEN_EXPIRATION *seconds*.
|
||||
// after QUIC_DEFAULT_REGULARTOKEN_EXPIRATION *seconds*.
|
||||
uint64_t token_expiration =
|
||||
RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS;
|
||||
|
||||
// Each Endpoint places limits on the number of concurrent connections from
|
||||
// a single host, and the total number of concurrent connections allowed as
|
||||
// a whole. These are set to fairly modest, and arbitrary defaults. We can
|
||||
// set these to whatever we'd like.
|
||||
uint64_t max_connections_per_host = DEFAULT_MAX_CONNECTIONS_PER_HOST;
|
||||
uint64_t max_connections_total = DEFAULT_MAX_CONNECTIONS;
|
||||
|
||||
// A stateless reset in QUIC is a discrete mechanism that one endpoint can
|
||||
// use to communicate to a peer that it has lost whatever state it
|
||||
// previously held about a session. Because generating a stateless reset
|
||||
@@ -134,6 +142,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
// Setting to 0 uses the default.
|
||||
uint8_t udp_ttl = 0;
|
||||
|
||||
// When an endpoint becomes idle (not listening and no primary sessions),
|
||||
// it will be destroyed after this many seconds. A value of 0 means
|
||||
// destroy immediately when idle (default, preserves pre-SessionManager
|
||||
// behavior). A positive value keeps the endpoint alive for potential
|
||||
// reuse by future connect() or listen() calls.
|
||||
static constexpr uint64_t DEFAULT_IDLE_TIMEOUT = 0;
|
||||
uint64_t idle_timeout = DEFAULT_IDLE_TIMEOUT;
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(Endpoint::Config)
|
||||
SET_SELF_SIZE(Options)
|
||||
@@ -324,9 +340,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
void EmitNewSession(const BaseObjectPtr<Session>& session);
|
||||
void EmitClose(CloseContext context, int status);
|
||||
|
||||
void IncrementSocketAddressCounter(const SocketAddress& address);
|
||||
void DecrementSocketAddressCounter(const SocketAddress& address);
|
||||
|
||||
// JavaScript API
|
||||
|
||||
// Create a new Endpoint.
|
||||
@@ -376,6 +389,11 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
ArenaPool<Packet> packet_pool_;
|
||||
UDP udp_;
|
||||
|
||||
// Idle timer: started when the endpoint becomes idle (not listening,
|
||||
// no primary sessions). When it fires, the endpoint is destroyed.
|
||||
// Stopped when a new session is added or listening begins.
|
||||
TimerWrapHandle idle_timer_;
|
||||
|
||||
struct ServerState {
|
||||
Session::Options options;
|
||||
std::shared_ptr<TLSContext> tls_context;
|
||||
@@ -383,18 +401,29 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
// Set if/when the endpoint is configured to listen.
|
||||
std::optional<ServerState> server_state_ = std::nullopt;
|
||||
|
||||
// A Session is generally identified by one or more CIDs. We use two
|
||||
// maps for this rather than one to avoid creating a whole bunch of
|
||||
// BaseObjectPtr references. The primary map (sessions_) just maps
|
||||
// the original CID to the Session, the second map (dcid_to_scid_)
|
||||
// maps the additional CIDs to the primary.
|
||||
CID::Map<BaseObjectPtr<Session>> sessions_;
|
||||
// Count of sessions for which this endpoint is the primary endpoint.
|
||||
// Drives ref/unref and idle timer logic. The actual session-to-endpoint
|
||||
// mapping is maintained by the SessionManager.
|
||||
size_t primary_session_count_ = 0;
|
||||
|
||||
// Per-endpoint CID -> SCID mapping for peer-chosen CIDs from connection
|
||||
// establishment (config.dcid, config.ocid). These are kept per-endpoint
|
||||
// because peer-chosen values can collide across endpoints (e.g., a
|
||||
// client's random outgoing DCID matching an incoming DCID on the server
|
||||
// endpoint). Locally-generated CIDs that need cross-endpoint routing
|
||||
// (preferred address, multipath) go in SessionManager::dcid_to_scid_.
|
||||
//
|
||||
// Endpoint::FindSession does a three-tier lookup:
|
||||
// 1. SessionManager::sessions_[cid] (direct SCID match)
|
||||
// 2. SessionManager::dcid_to_scid_[cid] (cross-endpoint CID)
|
||||
// 3. Endpoint::dcid_to_scid_[cid] (peer-chosen CID)
|
||||
// Each tier resolves to an SCID and looks up SessionManager::sessions_.
|
||||
CID::Map<CID> dcid_to_scid_;
|
||||
StatelessResetToken::Map<Session*> token_map_;
|
||||
|
||||
SessionManager& session_manager() const;
|
||||
|
||||
struct SocketAddressInfoTraits final {
|
||||
struct Type final {
|
||||
size_t active_connections;
|
||||
size_t reset_count;
|
||||
size_t retry_count;
|
||||
uint64_t timestamp;
|
||||
@@ -405,7 +434,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
|
||||
static void Touch(const SocketAddress& address, Type* type);
|
||||
};
|
||||
|
||||
SocketAddressLRU<SocketAddressInfoTraits> addrLRU_;
|
||||
SocketAddressLRU<SocketAddressInfoTraits> addr_validation_lru_;
|
||||
|
||||
// Per-IP connection counts for maxConnectionsPerHost enforcement.
|
||||
// Only populated when max_connections_per_host > 0. Entries are
|
||||
// added in AddSession and removed when the count reaches 0 in
|
||||
// RemoveSession. The map size is bounded by the number of active
|
||||
// sessions (each entry has count >= 1).
|
||||
SocketAddress::IpMap<uint16_t> conn_counts_per_host_;
|
||||
|
||||
CloseContext close_context_ = CloseContext::CLOSE;
|
||||
int close_status_ = 0;
|
||||
|
||||
+600
-187
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,8 @@
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include "application.h"
|
||||
#include "session.h"
|
||||
|
||||
namespace node::quic {
|
||||
@@ -13,6 +15,11 @@ namespace node::quic {
|
||||
std::unique_ptr<Session::Application> CreateHttp3Application(
|
||||
Session* session, const Session::Application_Options& options);
|
||||
|
||||
// Parse HTTP/3 specific session ticket app data. Called from
|
||||
// Application::ParseTicketData() when the type byte is HTTP3.
|
||||
// The data includes the type byte prefix.
|
||||
std::optional<PendingTicketAppData> ParseHttp3TicketData(const uv_buf_t& data);
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
@@ -1,140 +0,0 @@
|
||||
#if HAVE_OPENSSL && HAVE_QUIC
|
||||
#include "guard.h"
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
#include "logstream.h"
|
||||
#include <async_wrap-inl.h>
|
||||
#include <base_object-inl.h>
|
||||
#include <env-inl.h>
|
||||
#include <memory_tracker-inl.h>
|
||||
#include <node_external_reference.h>
|
||||
#include <stream_base-inl.h>
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
#include "bindingdata.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
using v8::FunctionTemplate;
|
||||
using v8::Local;
|
||||
using v8::Object;
|
||||
|
||||
namespace quic {
|
||||
|
||||
JS_CONSTRUCTOR_IMPL(LogStream, logstream_constructor_template, {
|
||||
tmpl = FunctionTemplate::New(env->isolate());
|
||||
JS_INHERIT(AsyncWrap);
|
||||
JS_CLASS_FIELDS(logstream, StreamBase::kInternalFieldCount);
|
||||
StreamBase::AddMethods(env, tmpl);
|
||||
})
|
||||
|
||||
BaseObjectPtr<LogStream> LogStream::Create(Environment* env) {
|
||||
JS_NEW_INSTANCE_OR_RETURN(env, obj, nullptr);
|
||||
return MakeDetachedBaseObject<LogStream>(env, obj);
|
||||
}
|
||||
|
||||
LogStream::LogStream(Environment* env, Local<Object> obj)
|
||||
: AsyncWrap(env, obj, PROVIDER_QUIC_LOGSTREAM), StreamBase(env) {
|
||||
MakeWeak();
|
||||
AttachToObject(GetObject());
|
||||
}
|
||||
|
||||
void LogStream::Emit(const uint8_t* data, size_t len, EmitOption option) {
|
||||
if (fin_seen_) return;
|
||||
fin_seen_ = option == EmitOption::FIN;
|
||||
|
||||
size_t remaining = len;
|
||||
// If the len is greater than the size of the buffer returned by
|
||||
// EmitAlloc then EmitRead will be called multiple times.
|
||||
while (remaining != 0) {
|
||||
uv_buf_t buf = EmitAlloc(remaining);
|
||||
size_t chunk_len = std::min<size_t>(remaining, buf.len);
|
||||
memcpy(buf.base, data, chunk_len);
|
||||
remaining -= chunk_len;
|
||||
data += chunk_len;
|
||||
// If we are actively reading from the stream, we'll call emit
|
||||
// read immediately. Otherwise we buffer the chunk and will push
|
||||
// the chunks out the next time ReadStart() is called.
|
||||
if (reading_) {
|
||||
EmitRead(chunk_len, buf);
|
||||
} else {
|
||||
// The total measures the total memory used so we always
|
||||
// increment but buf.len and not chunk len.
|
||||
ensure_space(buf.len);
|
||||
total_ += buf.len;
|
||||
buffer_.push_back(Chunk{chunk_len, buf});
|
||||
}
|
||||
}
|
||||
|
||||
if (ended_ && reading_) {
|
||||
EmitRead(UV_EOF);
|
||||
}
|
||||
}
|
||||
|
||||
void LogStream::Emit(const std::string_view line, EmitOption option) {
|
||||
Emit(reinterpret_cast<const uint8_t*>(line.data()), line.length(), option);
|
||||
}
|
||||
|
||||
void LogStream::End() {
|
||||
ended_ = true;
|
||||
}
|
||||
|
||||
int LogStream::ReadStart() {
|
||||
if (reading_) return 0;
|
||||
// Flush any chunks that have already been buffered.
|
||||
for (const auto& chunk : buffer_) EmitRead(chunk.len, chunk.buf);
|
||||
total_ = 0;
|
||||
buffer_.clear();
|
||||
if (fin_seen_) {
|
||||
// If we've already received the fin, there's nothing else to wait for.
|
||||
EmitRead(UV_EOF);
|
||||
return ReadStop();
|
||||
}
|
||||
// Otherwise, we're going to wait for more chunks to be written.
|
||||
reading_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LogStream::ReadStop() {
|
||||
reading_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We do not use either of these.
|
||||
int LogStream::DoShutdown(ShutdownWrap* req_wrap) {
|
||||
UNREACHABLE();
|
||||
}
|
||||
int LogStream::DoWrite(WriteWrap* w,
|
||||
uv_buf_t* bufs,
|
||||
size_t count,
|
||||
uv_stream_t* send_handle) {
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
bool LogStream::IsAlive() {
|
||||
return !ended_;
|
||||
}
|
||||
|
||||
bool LogStream::IsClosing() {
|
||||
return ended_;
|
||||
}
|
||||
|
||||
AsyncWrap* LogStream::GetAsyncWrap() {
|
||||
return this;
|
||||
}
|
||||
|
||||
void LogStream::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackFieldWithSize("buffer", total_);
|
||||
}
|
||||
|
||||
// The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer.
|
||||
void LogStream::ensure_space(size_t amt) {
|
||||
while (total_ + amt > kMaxLogStreamBuffer) {
|
||||
total_ -= buffer_.front().buf.len;
|
||||
buffer_.pop_front();
|
||||
}
|
||||
}
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
#endif // OPENSSL_NO_QUIC
|
||||
#endif // HAVE_OPENSSL && HAVE_QUIC
|
||||
@@ -1,84 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <async_wrap.h>
|
||||
#include <base_object.h>
|
||||
#include <env.h>
|
||||
#include <stream_base.h>
|
||||
#include <list>
|
||||
#include "defs.h"
|
||||
|
||||
namespace node::quic {
|
||||
|
||||
// The LogStream is a utility that the QUIC impl uses to publish both QLog
|
||||
// and Keylog diagnostic data (one instance for each).
|
||||
class LogStream final : public AsyncWrap, public StreamBase {
|
||||
public:
|
||||
enum InternalFields {
|
||||
kInternalFieldCount = std::max<uint32_t>(AsyncWrap::kInternalFieldCount,
|
||||
StreamBase::kInternalFieldCount),
|
||||
};
|
||||
|
||||
JS_CONSTRUCTOR(LogStream);
|
||||
|
||||
static BaseObjectPtr<LogStream> Create(Environment* env);
|
||||
|
||||
LogStream(Environment* env, v8::Local<v8::Object> obj);
|
||||
|
||||
enum class EmitOption : uint8_t {
|
||||
NONE,
|
||||
FIN,
|
||||
};
|
||||
|
||||
void Emit(const uint8_t* data,
|
||||
size_t len,
|
||||
EmitOption option = EmitOption::NONE);
|
||||
|
||||
void Emit(const std::string_view line, EmitOption option = EmitOption::NONE);
|
||||
|
||||
void End();
|
||||
|
||||
int ReadStart() override;
|
||||
|
||||
int ReadStop() override;
|
||||
|
||||
// We do not use either of these.
|
||||
int DoShutdown(ShutdownWrap* req_wrap) override;
|
||||
int DoWrite(WriteWrap* w,
|
||||
uv_buf_t* bufs,
|
||||
size_t count,
|
||||
uv_stream_t* send_handle) override;
|
||||
|
||||
bool IsAlive() override;
|
||||
bool IsClosing() override;
|
||||
AsyncWrap* GetAsyncWrap() override;
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(LogStream)
|
||||
SET_SELF_SIZE(LogStream)
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
// len will be <= buf.len
|
||||
size_t len;
|
||||
uv_buf_t buf;
|
||||
};
|
||||
size_t total_ = 0;
|
||||
std::list<Chunk> buffer_;
|
||||
bool fin_seen_ = false;
|
||||
bool ended_ = false;
|
||||
bool reading_ = false;
|
||||
|
||||
// The value here is fairly arbitrary. Once we get everything
|
||||
// fully implemented and start working with this, we might
|
||||
// tune this number further.
|
||||
static constexpr size_t kMaxLogStreamBuffer = 1024 * 10;
|
||||
|
||||
// The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer.
|
||||
void ensure_space(size_t amt);
|
||||
};
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
+15
-5
@@ -127,11 +127,15 @@ Packet::Ptr Packet::CreateImmediateConnectionClosePacket(
|
||||
"immediate connection close (endpoint)");
|
||||
if (!packet) return packet;
|
||||
ngtcp2_vec vec = *packet;
|
||||
// ngtcp2_crypto_write_connection_close expects dcid to be the
|
||||
// client's SCID and scid to be the client's DCID (mirrored).
|
||||
// PathDescriptor carries the incoming packet's CIDs as-is, so
|
||||
// we swap here.
|
||||
ssize_t nwrite = ngtcp2_crypto_write_connection_close(vec.base,
|
||||
vec.len,
|
||||
path_descriptor.version,
|
||||
path_descriptor.dcid,
|
||||
path_descriptor.scid,
|
||||
path_descriptor.dcid,
|
||||
reason.code(),
|
||||
nullptr,
|
||||
0);
|
||||
@@ -160,9 +164,11 @@ Packet::Ptr Packet::CreateStatelessResetPacket(
|
||||
if (!packet) return packet;
|
||||
ngtcp2_vec vec = *packet;
|
||||
|
||||
ssize_t nwrite = ngtcp2_pkt_write_stateless_reset(
|
||||
auto nwrite = ngtcp2_pkt_write_stateless_reset2(
|
||||
vec.base, pktlen, token, random, kRandlen);
|
||||
if (nwrite <= static_cast<ssize_t>(kMinStatelessResetLen)) return Ptr();
|
||||
if (nwrite < static_cast<ssize_t>(kMinStatelessResetLen)) {
|
||||
return Ptr();
|
||||
}
|
||||
|
||||
packet->Truncate(static_cast<size_t>(nwrite));
|
||||
return packet;
|
||||
@@ -203,14 +209,18 @@ Packet::Ptr Packet::CreateVersionNegotiationPacket(
|
||||
if (!packet) return packet;
|
||||
ngtcp2_vec vec = *packet;
|
||||
|
||||
// ngtcp2_pkt_write_version_negotiation expects dcid to be the
|
||||
// client's SCID and scid to be the client's DCID (mirrored).
|
||||
// PathDescriptor carries the incoming packet's CIDs as-is, so
|
||||
// we swap here.
|
||||
ssize_t nwrite =
|
||||
ngtcp2_pkt_write_version_negotiation(vec.base,
|
||||
pktlen,
|
||||
0,
|
||||
path_descriptor.dcid,
|
||||
path_descriptor.dcid.length(),
|
||||
path_descriptor.scid,
|
||||
path_descriptor.scid.length(),
|
||||
path_descriptor.dcid,
|
||||
path_descriptor.dcid.length(),
|
||||
sv,
|
||||
arraysize(sv));
|
||||
if (nwrite <= 0) return Ptr();
|
||||
|
||||
@@ -69,6 +69,15 @@ class Packet final {
|
||||
size_t capacity() const { return capacity_; }
|
||||
const SocketAddress& destination() const { return destination_; }
|
||||
Listener* listener() const { return listener_; }
|
||||
|
||||
// Redirect the packet to a different endpoint for cross-endpoint sends
|
||||
// (e.g., PATH_RESPONSE on a preferred address path). Updates the
|
||||
// listener (for pending_callbacks accounting) and the destination
|
||||
// (for uv_udp_send targeting). The packet data is unchanged.
|
||||
void Redirect(Listener* listener, const SocketAddress& destination) {
|
||||
listener_ = listener;
|
||||
destination_ = destination;
|
||||
}
|
||||
uv_udp_send_t* req() { return &req_; }
|
||||
|
||||
operator uv_buf_t() const {
|
||||
|
||||
+978
-371
File diff suppressed because it is too large.
Load diff
+76
-9
@@ -16,7 +16,6 @@
|
||||
#include "cid.h"
|
||||
#include "data.h"
|
||||
#include "defs.h"
|
||||
#include "logstream.h"
|
||||
#include "packet.h"
|
||||
#include "preferredaddress.h"
|
||||
#include "sessionticket.h"
|
||||
@@ -74,9 +73,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
|
||||
// HTTP/3 specific options.
|
||||
uint64_t max_field_section_size = 0;
|
||||
uint64_t qpack_max_dtable_capacity = 0;
|
||||
uint64_t qpack_encoder_max_dtable_capacity = 0;
|
||||
uint64_t qpack_blocked_streams = 0;
|
||||
uint64_t qpack_max_dtable_capacity = 4096;
|
||||
uint64_t qpack_encoder_max_dtable_capacity = 4096;
|
||||
uint64_t qpack_blocked_streams = 100;
|
||||
|
||||
bool enable_connect_protocol = true;
|
||||
bool enable_datagrams = true;
|
||||
@@ -112,6 +111,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
// (ALPN negotiated during handshake). Must be called before any
|
||||
// application data is received.
|
||||
void SetApplication(std::unique_ptr<Application> app);
|
||||
// Controls which datagram to drop when the pending datagram queue is full.
|
||||
enum class DatagramDropPolicy : uint8_t {
|
||||
DROP_OLDEST = 0, // Drop the oldest queued datagram (default).
|
||||
DROP_NEWEST = 1, // Drop the incoming datagram.
|
||||
};
|
||||
|
||||
// The options used to configure a session. Most of these deal directly with
|
||||
// the transport parameters that are exchanged with the remote peer during
|
||||
// handshake.
|
||||
@@ -151,6 +156,11 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
// completion of the tls handshake.
|
||||
uint64_t handshake_timeout = UINT64_MAX;
|
||||
|
||||
// The keep-alive timeout in milliseconds. When set to a non-zero value,
|
||||
// ngtcp2 will automatically send PING frames to keep the connection alive
|
||||
// before the idle timeout fires. Set to 0 to disable (default).
|
||||
uint64_t keep_alive_timeout = 0;
|
||||
|
||||
// Maximum initial flow control window size for a stream.
|
||||
uint64_t max_stream_window = 0;
|
||||
|
||||
@@ -180,6 +190,21 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
// is the better of the two for our needs.
|
||||
ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC;
|
||||
|
||||
// Controls which datagram to drop when the pending queue is full.
|
||||
DatagramDropPolicy datagram_drop_policy = DatagramDropPolicy::DROP_OLDEST;
|
||||
|
||||
// Maximum number of SendPendingData attempts before a datagram is
|
||||
// abandoned. When a datagram cannot be sent due to congestion control
|
||||
// or packet size constraints, it remains in the queue and the counter
|
||||
// is incremented. Once the limit is reached, the datagram is dropped
|
||||
// and reported as abandoned. Range: 1-255. Default: 5.
|
||||
uint8_t max_datagram_send_attempts = 5;
|
||||
|
||||
// Multiplier for the Probe Timeout (PTO) used to compute the draining
|
||||
// period duration after receiving CONNECTION_CLOSE. RFC 9000 Section
|
||||
// 10.2 requires at least 3x PTO. Range: 3-255. Default: 3.
|
||||
uint8_t draining_period_multiplier = 3;
|
||||
|
||||
// An optional NEW_TOKEN from a previous connection to the same
|
||||
// server. When set, the token is included in the Initial packet
|
||||
// to skip address validation. Client-side only.
|
||||
@@ -312,6 +337,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
struct Stats;
|
||||
|
||||
void HandleQlog(uint32_t flags, const void* data, size_t len);
|
||||
void EmitQlog(uint32_t flags, std::string_view data);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
@@ -335,6 +361,18 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
void Send(Packet::Ptr packet, const PathStorage& path);
|
||||
datagram_id SendDatagram(Store&& data);
|
||||
|
||||
// Pending datagram accessors for use by SendPendingData.
|
||||
struct PendingDatagram {
|
||||
datagram_id id;
|
||||
Store data;
|
||||
uint8_t send_attempts = 0;
|
||||
};
|
||||
bool HasPendingDatagrams() const;
|
||||
PendingDatagram& PeekPendingDatagram();
|
||||
void PopPendingDatagram();
|
||||
size_t PendingDatagramCount() const;
|
||||
void DatagramSent(datagram_id id);
|
||||
|
||||
// A non-const variation to allow certain modifications.
|
||||
Config& config();
|
||||
|
||||
@@ -343,6 +381,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
DO_NOT_NOTIFY,
|
||||
};
|
||||
BaseObjectPtr<Stream> FindStream(stream_id id) const;
|
||||
// Returns a copy of the streams map (safe for iteration while streams
|
||||
// are being destroyed).
|
||||
StreamsMap streams() const;
|
||||
BaseObjectPtr<Stream> CreateStream(
|
||||
stream_id id,
|
||||
CreateStreamOption option = CreateStreamOption::NOTIFY,
|
||||
@@ -422,6 +463,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
bool wants_session_ticket() const;
|
||||
void SetStreamOpenAllowed();
|
||||
|
||||
// Populate state buffer fields from the 0-RTT transport params.
|
||||
// Called after ngtcp2_conn_decode_and_set_0rtt_transport_params
|
||||
// succeeds, so that values like maxDatagramSize are available
|
||||
// before the handshake completes.
|
||||
void PopulateEarlyTransportParamsState();
|
||||
|
||||
// It's a terrible name but "wrapped" here means that the Session has been
|
||||
// passed out to JavaScript and should be "wrapped" by whatever handler is
|
||||
// defined there to manage it.
|
||||
@@ -471,10 +518,17 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
// JavaScript callouts
|
||||
|
||||
void EmitClose(const QuicError& error = QuicError());
|
||||
void EmitGoaway(stream_id last_stream_id);
|
||||
|
||||
// Sets the max datagram payload size in the shared state. Used by
|
||||
// Http3ApplicationImpl to block datagram sends when the peer's
|
||||
// SETTINGS_H3_DATAGRAM=0 (RFC 9297 §3).
|
||||
void set_max_datagram_size(uint16_t size);
|
||||
void EmitDatagram(Store&& datagram, DatagramReceivedFlags flag);
|
||||
void EmitDatagramStatus(datagram_id id, DatagramStatus status);
|
||||
void EmitHandshakeComplete();
|
||||
void EmitKeylog(const char* line);
|
||||
void EmitOrigins(std::vector<std::string>&& origins);
|
||||
|
||||
struct ValidatedPath {
|
||||
std::shared_ptr<SocketAddress> local;
|
||||
@@ -487,6 +541,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
const std::optional<ValidatedPath>& oldPath);
|
||||
void EmitSessionTicket(Store&& ticket);
|
||||
void EmitNewToken(const uint8_t* token, size_t len);
|
||||
void EmitEarlyDataRejected();
|
||||
void DestroyAllStreams(const QuicError& error);
|
||||
void EmitStream(const BaseObjectWeakPtr<Stream>& stream);
|
||||
void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
|
||||
const uint32_t* sv,
|
||||
@@ -495,7 +551,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
void DatagramReceived(const uint8_t* data,
|
||||
size_t datalen,
|
||||
DatagramReceivedFlags flag);
|
||||
void GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token);
|
||||
void GenerateNewConnectionId(ngtcp2_cid* cid,
|
||||
size_t len,
|
||||
ngtcp2_stateless_reset_token* token);
|
||||
bool HandshakeCompleted();
|
||||
void HandshakeConfirmed();
|
||||
void SelectPreferredAddress(PreferredAddress* preferredAddress);
|
||||
@@ -503,17 +561,26 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
|
||||
QuicConnectionPointer InitConnection();
|
||||
|
||||
Side side_;
|
||||
ngtcp2_mem allocator_;
|
||||
const ngtcp2_mem* allocator_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
// These flags live on Session (not Impl) so that the NgTcp2CallbackScope
|
||||
// and NgHttp3CallbackScope destructors can safely clear them even after
|
||||
// Impl has been destroyed via MakeCallback re-entrancy during a callback.
|
||||
// The scope is placed at the ngtcp2/nghttp3 entry point (e.g. Receive,
|
||||
// OnTimeout) rather than on individual callbacks, so the deferred destroy
|
||||
// only fires after all callbacks for that entry point have completed.
|
||||
bool in_ngtcp2_callback_scope_ = false;
|
||||
bool in_nghttp3_callback_scope_ = false;
|
||||
bool destroy_deferred_ = false;
|
||||
QuicConnectionPointer connection_;
|
||||
std::unique_ptr<TLSSession> tls_session_;
|
||||
BaseObjectPtr<LogStream> qlog_stream_;
|
||||
BaseObjectPtr<LogStream> keylog_stream_;
|
||||
|
||||
friend struct NgTcp2CallbackScope;
|
||||
friend struct NgHttp3CallbackScope;
|
||||
friend class Application;
|
||||
friend class DefaultApplication;
|
||||
friend class Http3ApplicationImpl;
|
||||
friend class Endpoint;
|
||||
friend class SessionManager;
|
||||
friend class Stream;
|
||||
friend class PendingStream;
|
||||
friend class TLSContext;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#if HAVE_OPENSSL && HAVE_QUIC
|
||||
#include "guard.h"
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
#include <base_object-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <util-inl.h>
|
||||
#include "endpoint.h"
|
||||
#include "session.h"
|
||||
#include "session_manager.h"
|
||||
|
||||
namespace node::quic {
|
||||
|
||||
SessionManager::SessionManager(Environment* env) : env_(env) {}
|
||||
|
||||
SessionManager::~SessionManager() = default;
|
||||
|
||||
BaseObjectPtr<Session> SessionManager::FindSession(const CID& cid) {
|
||||
// Direct SCID match.
|
||||
auto it = sessions_.find(cid);
|
||||
if (it != sessions_.end()) return it->second;
|
||||
|
||||
// Cross-endpoint CID mapping (locally-generated CIDs for preferred
|
||||
// address, multipath, etc.).
|
||||
auto scid_it = dcid_to_scid_.find(cid);
|
||||
if (scid_it != dcid_to_scid_.end()) {
|
||||
it = sessions_.find(scid_it->second);
|
||||
if (it != sessions_.end()) return it->second;
|
||||
// Stale mapping — clean up.
|
||||
dcid_to_scid_.erase(scid_it);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void SessionManager::AddSession(const CID& scid,
|
||||
BaseObjectPtr<Session> session) {
|
||||
sessions_[scid] = std::move(session);
|
||||
}
|
||||
|
||||
void SessionManager::AssociateCID(const CID& cid, const CID& scid) {
|
||||
if (cid && scid && cid != scid) {
|
||||
dcid_to_scid_[cid] = scid;
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::DisassociateCID(const CID& cid) {
|
||||
if (cid) {
|
||||
dcid_to_scid_.erase(cid);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::RemoveSession(const CID& scid) {
|
||||
auto it = sessions_.find(scid);
|
||||
if (it != sessions_.end()) {
|
||||
primary_map_.erase(it->second.get());
|
||||
sessions_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::AssociateStatelessResetToken(
|
||||
const StatelessResetToken& token, Session* session) {
|
||||
token_map_[token] = session;
|
||||
}
|
||||
|
||||
void SessionManager::DisassociateStatelessResetToken(
|
||||
const StatelessResetToken& token) {
|
||||
token_map_.erase(token);
|
||||
}
|
||||
|
||||
Session* SessionManager::FindSessionByStatelessResetToken(
|
||||
const StatelessResetToken& token) const {
|
||||
auto it = token_map_.find(token);
|
||||
if (it != token_map_.end()) return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SessionManager::RegisterEndpoint(Endpoint* endpoint,
|
||||
const SocketAddress& local_address) {
|
||||
endpoints_.insert(endpoint);
|
||||
endpoint_addrs_[endpoint] = local_address;
|
||||
}
|
||||
|
||||
void SessionManager::UnregisterEndpoint(Endpoint* endpoint) {
|
||||
endpoints_.erase(endpoint);
|
||||
endpoint_addrs_.erase(endpoint);
|
||||
// If no endpoints remain, destroy all sessions.
|
||||
if (endpoints_.empty()) {
|
||||
DestroyAllSessions();
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionManager::HasEndpoints() const {
|
||||
return !endpoints_.empty();
|
||||
}
|
||||
|
||||
size_t SessionManager::endpoint_count() const {
|
||||
return endpoints_.size();
|
||||
}
|
||||
|
||||
Endpoint* SessionManager::FindEndpointForAddress(
|
||||
const SocketAddress& local_addr) const {
|
||||
// First pass: exact match.
|
||||
for (const auto& [endpoint, addr] : endpoint_addrs_) {
|
||||
if (addr == local_addr) return endpoint;
|
||||
}
|
||||
// Second pass: wildcard fallback. An endpoint bound to 0.0.0.0:port
|
||||
// or [::]:port can serve any address on that port.
|
||||
int port = SocketAddress::GetPort(local_addr.data());
|
||||
for (const auto& [endpoint, addr] : endpoint_addrs_) {
|
||||
if (SocketAddress::GetPort(addr.data()) == port) {
|
||||
auto host = addr.address();
|
||||
if (host == "0.0.0.0" || host == "::") {
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SessionManager::SetPrimaryEndpoint(Session* session, Endpoint* endpoint) {
|
||||
primary_map_[session] = endpoint;
|
||||
}
|
||||
|
||||
Endpoint* SessionManager::GetPrimaryEndpoint(Session* session) const {
|
||||
auto it = primary_map_.find(session);
|
||||
if (it != primary_map_.end()) return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SessionManager::CloseAllSessionsFor(Endpoint* endpoint) {
|
||||
// Collect sessions whose primary is this endpoint, then close them.
|
||||
// We collect first because closing a session modifies primary_map_.
|
||||
std::vector<BaseObjectPtr<Session>> to_close;
|
||||
for (const auto& [session, ep] : primary_map_) {
|
||||
if (ep == endpoint) {
|
||||
// Look up the owning reference from sessions_ so the session
|
||||
// stays alive during close.
|
||||
for (const auto& [cid, sess_ptr] : sessions_) {
|
||||
if (sess_ptr.get() == session) {
|
||||
to_close.push_back(sess_ptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& session : to_close) {
|
||||
session->Close(Session::CloseMethod::SILENT);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::DestroyAllSessions() {
|
||||
// Copy the map since closing sessions will modify it.
|
||||
auto sessions = sessions_;
|
||||
for (auto& [cid, session] : sessions) {
|
||||
session->Close(Session::CloseMethod::SILENT);
|
||||
}
|
||||
sessions.clear();
|
||||
token_map_.clear();
|
||||
dcid_to_scid_.clear();
|
||||
primary_map_.clear();
|
||||
}
|
||||
|
||||
bool SessionManager::is_empty() const {
|
||||
return sessions_.empty();
|
||||
}
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
#endif // OPENSSL_NO_QUIC
|
||||
#endif // HAVE_OPENSSL && HAVE_QUIC
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <env.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include "cid.h"
|
||||
#include "tokens.h"
|
||||
|
||||
namespace node::quic {
|
||||
|
||||
class Endpoint;
|
||||
class Session;
|
||||
|
||||
// SessionManager is a per-Realm singleton that centralizes QUIC session
|
||||
// routing. It holds the authoritative CID -> Session mapping, enabling
|
||||
// any Endpoint to route packets to any session. This decouples session
|
||||
// lifetime from individual endpoints, which is required for preferred
|
||||
// address, connection migration, and multi-path QUIC.
|
||||
//
|
||||
// SessionManager is held by BindingData and lazily created on first access.
|
||||
// It is not exposed to JavaScript.
|
||||
class SessionManager final {
|
||||
public:
|
||||
explicit SessionManager(Environment* env);
|
||||
~SessionManager();
|
||||
|
||||
// Session routing. The sessions_ map holds BaseObjectPtr<Session> (owning
|
||||
// references). SessionManager is the single authority for session ownership.
|
||||
BaseObjectPtr<Session> FindSession(const CID& dcid);
|
||||
void AddSession(const CID& scid, BaseObjectPtr<Session> session);
|
||||
void RemoveSession(const CID& scid);
|
||||
|
||||
// Cross-endpoint CID association. This map holds locally-generated CIDs
|
||||
// that need to be routable from any endpoint (e.g., preferred address CID,
|
||||
// multipath NEW_CONNECTION_ID CIDs). Peer-chosen CIDs from connection
|
||||
// establishment (config.dcid, config.ocid) go in Endpoint::dcid_to_scid_
|
||||
// instead, because those values can collide across endpoints.
|
||||
void AssociateCID(const CID& cid, const CID& scid);
|
||||
void DisassociateCID(const CID& cid);
|
||||
|
||||
// Stateless reset token association. The token_map_ holds raw (non-owning)
|
||||
// pointers. Entries are valid only while the corresponding session exists
|
||||
// in sessions_. Sessions clean up their tokens during teardown.
|
||||
void AssociateStatelessResetToken(const StatelessResetToken& token,
|
||||
Session* session);
|
||||
void DisassociateStatelessResetToken(const StatelessResetToken& token);
|
||||
Session* FindSessionByStatelessResetToken(
|
||||
const StatelessResetToken& token) const;
|
||||
|
||||
// Endpoint registry. Endpoints register themselves when they start
|
||||
// receiving and unregister when they close.
|
||||
void RegisterEndpoint(Endpoint* endpoint, const SocketAddress& local_address);
|
||||
void UnregisterEndpoint(Endpoint* endpoint);
|
||||
bool HasEndpoints() const;
|
||||
size_t endpoint_count() const;
|
||||
|
||||
// Find the endpoint bound to a given local address. Used by the session
|
||||
// send path to route packets through the correct endpoint based on the
|
||||
// ngtcp2 packet path. Tries exact match first, then wildcard fallback
|
||||
// (0.0.0.0 or [::] on the same port).
|
||||
Endpoint* FindEndpointForAddress(const SocketAddress& local_addr) const;
|
||||
|
||||
// Primary endpoint tracking. Each session has one primary endpoint
|
||||
// responsible for its lifecycle.
|
||||
void SetPrimaryEndpoint(Session* session, Endpoint* endpoint);
|
||||
Endpoint* GetPrimaryEndpoint(Session* session) const;
|
||||
|
||||
// Close all sessions whose primary endpoint is the given endpoint.
|
||||
// Used by Endpoint::Destroy().
|
||||
void CloseAllSessionsFor(Endpoint* endpoint);
|
||||
|
||||
// Destroy all sessions. Used when the last endpoint is removed.
|
||||
void DestroyAllSessions();
|
||||
|
||||
bool is_empty() const;
|
||||
|
||||
private:
|
||||
Environment* env_;
|
||||
|
||||
// The sessions_ map holds strong owning references keyed by locally-
|
||||
// generated SCIDs. This is the single source of truth for session
|
||||
// ownership.
|
||||
CID::Map<BaseObjectPtr<Session>> sessions_;
|
||||
|
||||
// Cross-endpoint CID -> primary SCID mapping. Contains locally-generated
|
||||
// CIDs that need to be routable from any endpoint. Peer-chosen CIDs
|
||||
// from connection establishment are in Endpoint::dcid_to_scid_ instead.
|
||||
CID::Map<CID> dcid_to_scid_;
|
||||
|
||||
// Stateless reset token -> Session (non-owning).
|
||||
StatelessResetToken::Map<Session*> token_map_;
|
||||
|
||||
// All registered endpoints.
|
||||
std::unordered_set<Endpoint*> endpoints_;
|
||||
|
||||
// Endpoint -> bound local address, for FindEndpointForAddress lookups.
|
||||
std::unordered_map<Endpoint*, SocketAddress> endpoint_addrs_;
|
||||
|
||||
// Session -> primary Endpoint mapping (non-owning both directions;
|
||||
// sessions are owned by sessions_, endpoints are externally owned).
|
||||
std::unordered_map<Session*, Endpoint*> primary_map_;
|
||||
};
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
@@ -136,25 +136,33 @@ SSL_TICKET_RETURN SessionTicket::DecryptedCallback(SSL* ssl,
|
||||
case SSL_TICKET_NO_DECRYPT:
|
||||
return SSL_TICKET_RETURN_IGNORE_RENEW;
|
||||
case SSL_TICKET_SUCCESS_RENEW:
|
||||
[[fallthrough]];
|
||||
return static_cast<SSL_TICKET_RETURN>(
|
||||
AppData::Extract(ssl, session, AppData::Source::Flag::STATUS_RENEW));
|
||||
case SSL_TICKET_SUCCESS:
|
||||
return static_cast<SSL_TICKET_RETURN>(AppData::Extract(ssl));
|
||||
return static_cast<SSL_TICKET_RETURN>(AppData::Extract(ssl, session));
|
||||
}
|
||||
}
|
||||
|
||||
SessionTicket::AppData::AppData(SSL* ssl) : ssl_(ssl) {}
|
||||
SessionTicket::AppData::AppData(SSL* ssl, SSL_SESSION* session)
|
||||
: ssl_(ssl), session_(session) {}
|
||||
|
||||
SSL_SESSION* SessionTicket::AppData::GetSession() const {
|
||||
return session_ != nullptr ? session_ : SSL_get0_session(ssl_);
|
||||
}
|
||||
|
||||
bool SessionTicket::AppData::Set(const uv_buf_t& data) {
|
||||
if (set_ || data.base == nullptr || data.len == 0) return false;
|
||||
set_ = true;
|
||||
SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl_), data.base, data.len);
|
||||
SSL_SESSION_set1_ticket_appdata(GetSession(), data.base, data.len);
|
||||
return set_;
|
||||
}
|
||||
|
||||
std::optional<const uv_buf_t> SessionTicket::AppData::Get() const {
|
||||
auto* sess = GetSession();
|
||||
if (sess == nullptr) return std::nullopt;
|
||||
uv_buf_t buf;
|
||||
int ret =
|
||||
SSL_SESSION_get0_ticket_appdata(SSL_get0_session(ssl_),
|
||||
SSL_SESSION_get0_ticket_appdata(sess,
|
||||
reinterpret_cast<void**>(&buf.base),
|
||||
reinterpret_cast<size_t*>(&buf.len));
|
||||
if (ret != 1) return std::nullopt;
|
||||
@@ -168,11 +176,12 @@ void SessionTicket::AppData::Collect(SSL* ssl) {
|
||||
}
|
||||
}
|
||||
|
||||
SessionTicket::AppData::Status SessionTicket::AppData::Extract(SSL* ssl) {
|
||||
SessionTicket::AppData::Status SessionTicket::AppData::Extract(
|
||||
SSL* ssl, SSL_SESSION* session, Source::Flag flag) {
|
||||
auto source = GetAppDataSource(ssl);
|
||||
if (source != nullptr) {
|
||||
AppData app_data(ssl);
|
||||
return source->ExtractSessionTicketAppData(app_data);
|
||||
AppData app_data(ssl, session);
|
||||
return source->ExtractSessionTicketAppData(app_data, flag);
|
||||
}
|
||||
return Status::TICKET_IGNORE;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class SessionTicket::AppData final {
|
||||
TICKET_USE_RENEW = SSL_TICKET_RETURN_USE_RENEW,
|
||||
};
|
||||
|
||||
explicit AppData(SSL* session);
|
||||
explicit AppData(SSL* ssl, SSL_SESSION* session = nullptr);
|
||||
DISALLOW_COPY_AND_MOVE(AppData)
|
||||
|
||||
bool Set(const uv_buf_t& data);
|
||||
@@ -94,11 +94,15 @@ class SessionTicket::AppData final {
|
||||
};
|
||||
|
||||
static void Collect(SSL* ssl);
|
||||
static Status Extract(SSL* ssl);
|
||||
static Status Extract(SSL* ssl,
|
||||
SSL_SESSION* session,
|
||||
Source::Flag flag = Source::Flag::STATUS_NONE);
|
||||
|
||||
private:
|
||||
SSL_SESSION* GetSession() const;
|
||||
bool set_ = false;
|
||||
SSL* ssl_;
|
||||
SSL_SESSION* session_;
|
||||
};
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
+288
-83
@@ -1,7 +1,7 @@
|
||||
#include "ngtcp2/ngtcp2.h"
|
||||
#if HAVE_OPENSSL && HAVE_QUIC
|
||||
#include "guard.h"
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
#include "streams.h"
|
||||
#include <aliased_struct-inl.h>
|
||||
#include <async_wrap-inl.h>
|
||||
#include <base_object-inl.h>
|
||||
@@ -9,18 +9,22 @@
|
||||
#include <memory_tracker-inl.h>
|
||||
#include <node_blob.h>
|
||||
#include <node_bob-inl.h>
|
||||
#include <node_file.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include "application.h"
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
#include "session.h"
|
||||
#include "streams.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
using v8::Array;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::ArrayBufferView;
|
||||
using v8::BackingStore;
|
||||
using v8::BigInt;
|
||||
using v8::FunctionCallbackInfo;
|
||||
using v8::Global;
|
||||
using v8::Integer;
|
||||
using v8::Just;
|
||||
@@ -30,6 +34,7 @@ using v8::Nothing;
|
||||
using v8::Object;
|
||||
using v8::ObjectTemplate;
|
||||
using v8::SharedArrayBuffer;
|
||||
using v8::Uint32;
|
||||
using v8::Uint8Array;
|
||||
using v8::Value;
|
||||
|
||||
@@ -43,6 +48,7 @@ namespace quic {
|
||||
V(READ_ENDED, read_ended, uint8_t) \
|
||||
V(WRITE_ENDED, write_ended, uint8_t) \
|
||||
V(RESET, reset, uint8_t) \
|
||||
V(RESET_CODE, reset_code, uint64_t) \
|
||||
V(HAS_OUTBOUND, has_outbound, uint8_t) \
|
||||
V(HAS_READER, has_reader, uint8_t) \
|
||||
/* Set when the stream has a block event handler */ \
|
||||
@@ -52,7 +58,11 @@ namespace quic {
|
||||
/* Set when the stream has a reset event handler */ \
|
||||
V(WANTS_RESET, wants_reset, uint8_t) \
|
||||
/* Set when the stream has a trailers event handler */ \
|
||||
V(WANTS_TRAILERS, wants_trailers, uint8_t)
|
||||
V(WANTS_TRAILERS, wants_trailers, uint8_t) \
|
||||
/* True when 0-RTT early data was received */ \
|
||||
V(RECEIVED_EARLY_DATA, received_early_data, uint8_t) \
|
||||
V(WRITE_DESIRED_SIZE, write_desired_size, uint32_t) \
|
||||
V(HIGH_WATER_MARK, high_water_mark, uint32_t)
|
||||
|
||||
#define STREAM_STATS(V) \
|
||||
/* Marks the timestamp when the stream object was created. */ \
|
||||
@@ -144,31 +154,23 @@ STAT_STRUCT(Stream, STREAM)
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
// Creates an in-memory DataQueue entry from an ArrayBuffer by either
|
||||
// detaching it (zero-copy) or copying its contents if detach is not
|
||||
// possible (e.g., SharedArrayBuffer-backed or non-detachable).
|
||||
// Returns nullptr on failure (error already thrown if allocation failed).
|
||||
// Creates an in-memory DataQueue entry by copying the requested range of
|
||||
// the given ArrayBuffer into a fresh BackingStore. The caller's buffer is
|
||||
// not detached or otherwise modified, so callers can safely reuse or
|
||||
// mutate it after the call returns. Callers that want to ensure their
|
||||
// buffer cannot be mutated after handing it off can call
|
||||
// `ArrayBuffer.prototype.transfer()` themselves before calling into the
|
||||
// QUIC API.
|
||||
// Returns nullptr on zero length or allocation failure.
|
||||
std::unique_ptr<DataQueue::Entry> CreateEntryFromBuffer(
|
||||
Environment* env, Local<ArrayBuffer> buffer, size_t offset, size_t length) {
|
||||
if (length == 0) return nullptr;
|
||||
std::shared_ptr<v8::BackingStore> backing;
|
||||
if (buffer->IsDetachable()) {
|
||||
backing = buffer->GetBackingStore();
|
||||
if (buffer->Detach(Local<Value>()).IsNothing()) {
|
||||
backing.reset();
|
||||
}
|
||||
}
|
||||
if (!backing) {
|
||||
// Buffer is not detachable or detach failed. Copy the data.
|
||||
JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr);
|
||||
memcpy(copy->Data(),
|
||||
static_cast<const uint8_t*>(buffer->Data()) + offset,
|
||||
length);
|
||||
offset = 0;
|
||||
backing = std::move(copy);
|
||||
}
|
||||
JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, copy, length, nullptr);
|
||||
memcpy(copy->Data(),
|
||||
static_cast<const uint8_t*>(buffer->Data()) + offset,
|
||||
length);
|
||||
return DataQueue::CreateInMemoryEntryFromBackingStore(
|
||||
std::move(backing), offset, length);
|
||||
std::move(copy), 0, length);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -226,10 +228,43 @@ Maybe<std::shared_ptr<DataQueue>> Stream::GetDataQueueFromSource(
|
||||
JS_TRY_ALLOCATE_BACKING_OR_RETURN(
|
||||
env, backing, str.length(), Nothing<std::shared_ptr<DataQueue>>());
|
||||
memcpy(backing->Data(), *str, str.length());
|
||||
auto len = backing->ByteLength();
|
||||
entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore(
|
||||
std::move(backing), 0, backing->ByteLength()));
|
||||
std::move(backing), 0, len));
|
||||
return Just(DataQueue::CreateIdempotent(std::move(entries)));
|
||||
}
|
||||
// FileHandle — create an fd-backed DataQueue from the file path.
|
||||
// The JS side validates and locks the FileHandle before passing
|
||||
// the C++ handle here. We detect FileHandle by checking if the
|
||||
// object's constructor name is "FileHandle".
|
||||
if (value->IsObject()) {
|
||||
auto obj = value.As<Object>();
|
||||
Local<v8::String> ctor_name;
|
||||
auto maybe_name = obj->GetConstructorName();
|
||||
if (!maybe_name.IsEmpty()) {
|
||||
ctor_name = maybe_name;
|
||||
Utf8Value name(env->isolate(), ctor_name);
|
||||
if (strcmp(*name, "FileHandle") == 0) {
|
||||
fs::FileHandle* file_handle;
|
||||
ASSIGN_OR_RETURN_UNWRAP(
|
||||
&file_handle, value, Nothing<std::shared_ptr<DataQueue>>());
|
||||
Local<Value> path;
|
||||
if (!v8::String::NewFromUtf8(env->isolate(),
|
||||
file_handle->original_name().c_str())
|
||||
.ToLocal(&path)) {
|
||||
return Nothing<std::shared_ptr<DataQueue>>();
|
||||
}
|
||||
auto entry = DataQueue::CreateFdEntry(env, path);
|
||||
if (!entry) return Nothing<std::shared_ptr<DataQueue>>();
|
||||
size_t size = entry->size().value_or(0);
|
||||
auto queue = DataQueue::Create();
|
||||
if (!queue) return Nothing<std::shared_ptr<DataQueue>>();
|
||||
queue->append(std::move(entry));
|
||||
queue->cap(size);
|
||||
return Just(std::move(queue));
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(jasnell): Add streaming sources...
|
||||
THROW_ERR_INVALID_ARG_TYPE(env, "Invalid data source type");
|
||||
return Nothing<std::shared_ptr<DataQueue>>();
|
||||
@@ -247,6 +282,14 @@ struct Stream::Impl {
|
||||
std::shared_ptr<DataQueue> dataqueue;
|
||||
if (GetDataQueueFromSource(env, args[0]).To(&dataqueue)) {
|
||||
stream->set_outbound(std::move(dataqueue));
|
||||
// set_outbound does not call ResumeStream because during
|
||||
// construction the stream is not yet registered with the session.
|
||||
// When attaching a source after creation (via setBody), the
|
||||
// stream is already registered and must be resumed to enter the
|
||||
// send queue.
|
||||
if (!stream->is_pending()) {
|
||||
stream->session().ResumeStream(stream->id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +297,7 @@ struct Stream::Impl {
|
||||
JS_METHOD(Destroy) {
|
||||
Stream* stream;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
|
||||
if (args.Length() > 1) {
|
||||
if (args.Length() >= 1) {
|
||||
CHECK(args[0]->IsBigInt());
|
||||
bool lossless = false;
|
||||
uint64_t code = args[0].As<BigInt>()->Uint64Value(&lossless);
|
||||
@@ -272,8 +315,7 @@ struct Stream::Impl {
|
||||
|
||||
// Sends a block of headers to the peer. If the stream is not yet open,
|
||||
// the headers will be queued and sent immediately when the stream is
|
||||
// opened. If the application does not support sending headers on streams,
|
||||
// they will be ignored and dropped on the floor.
|
||||
// opened. Returns false if the application does not support headers.
|
||||
JS_METHOD(SendHeaders) {
|
||||
Stream* stream;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
|
||||
@@ -287,8 +329,13 @@ struct Stream::Impl {
|
||||
|
||||
// If the stream is pending, the headers will be queued until the
|
||||
// stream is opened, at which time the queued header block will be
|
||||
// immediately sent when the stream is opened.
|
||||
// immediately sent when the stream is opened. If we already know
|
||||
// that the application does not support headers, return false
|
||||
// immediately so the JS side can throw an appropriate error.
|
||||
if (stream->is_pending()) {
|
||||
if (!stream->session().application().SupportsHeaders()) {
|
||||
return args.GetReturnValue().Set(false);
|
||||
}
|
||||
stream->EnqueuePendingHeaders(kind, headers, flags);
|
||||
return args.GetReturnValue().Set(true);
|
||||
}
|
||||
@@ -355,18 +402,22 @@ struct Stream::Impl {
|
||||
JS_METHOD(SetPriority) {
|
||||
Stream* stream;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
|
||||
CHECK(args[0]->IsUint32()); // Priority
|
||||
CHECK(args[1]->IsUint32()); // Priority flag
|
||||
CHECK(args[0]->IsUint32()); // Packed: (urgency << 1) | incremental
|
||||
|
||||
StreamPriority priority = FromV8Value<StreamPriority>(args[0]);
|
||||
StreamPriorityFlags flags = FromV8Value<StreamPriorityFlags>(args[1]);
|
||||
uint32_t packed = args[0].As<Uint32>()->Value();
|
||||
StreamPriority priority = static_cast<StreamPriority>(packed >> 1);
|
||||
StreamPriorityFlags flags = (packed & 1)
|
||||
? StreamPriorityFlags::INCREMENTAL
|
||||
: StreamPriorityFlags::NON_INCREMENTAL;
|
||||
|
||||
if (stream->is_pending()) {
|
||||
stream->pending_priority_ = PendingPriority{
|
||||
.priority = priority,
|
||||
.flags = flags,
|
||||
};
|
||||
} else {
|
||||
// Always update the stored priority on the stream.
|
||||
stream->priority_ = StoredPriority{
|
||||
.priority = priority,
|
||||
.flags = flags,
|
||||
.pending = stream->is_pending(),
|
||||
};
|
||||
|
||||
if (!stream->is_pending()) {
|
||||
stream->session().application().SetStreamPriority(
|
||||
*stream, priority, flags);
|
||||
}
|
||||
@@ -376,13 +427,23 @@ struct Stream::Impl {
|
||||
Stream* stream;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
|
||||
|
||||
if (stream->is_pending()) {
|
||||
return args.GetReturnValue().Set(
|
||||
static_cast<uint32_t>(StreamPriority::DEFAULT));
|
||||
// On the client side, priority is always read from the stream's
|
||||
// stored value since the client is the one setting it. On the
|
||||
// server side, we delegate to the application which can read
|
||||
// the peer's requested priority (e.g., from PRIORITY_UPDATE
|
||||
// frames in HTTP/3).
|
||||
if (!stream->session().is_server()) {
|
||||
auto& pri = stream->priority_;
|
||||
uint32_t packed = (static_cast<uint32_t>(pri.priority) << 1) |
|
||||
(pri.flags == StreamPriorityFlags::INCREMENTAL ? 1 : 0);
|
||||
return args.GetReturnValue().Set(packed);
|
||||
}
|
||||
|
||||
auto priority = stream->session().application().GetStreamPriority(*stream);
|
||||
args.GetReturnValue().Set(static_cast<uint32_t>(priority));
|
||||
auto result = stream->session().application().GetStreamPriority(*stream);
|
||||
uint32_t packed =
|
||||
(static_cast<uint32_t>(result.priority) << 1) |
|
||||
(result.flags == StreamPriorityFlags::INCREMENTAL ? 1 : 0);
|
||||
args.GetReturnValue().Set(packed);
|
||||
}
|
||||
|
||||
// Returns a Blob::Reader that can be used to read data that has been
|
||||
@@ -391,9 +452,9 @@ struct Stream::Impl {
|
||||
Stream* stream;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
|
||||
BaseObjectPtr<Blob::Reader> reader = stream->get_reader();
|
||||
if (reader) return args.GetReturnValue().Set(reader->object());
|
||||
THROW_ERR_INVALID_STATE(Environment::GetCurrent(args),
|
||||
"Unable to get a reader for the stream");
|
||||
if (reader) args.GetReturnValue().Set(reader->object());
|
||||
// Returns undefined when the stream is not readable (e.g. a local
|
||||
// unidirectional stream). The JS side checks for this.
|
||||
}
|
||||
|
||||
JS_METHOD(InitStreamingSource) {
|
||||
@@ -505,13 +566,24 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
|
||||
bool is_streaming() const { return streaming_; }
|
||||
size_t total() const { return total_; }
|
||||
size_t uncommitted() const { return uncommitted_; }
|
||||
|
||||
// Total bytes in the pipeline: data appended to the DataQueue that
|
||||
// hasn't been pulled yet, plus data pulled but not yet acknowledged.
|
||||
// This is the number to compare against highWaterMark for backpressure.
|
||||
size_t queued_bytes() const { return queued_ + total_; }
|
||||
|
||||
// Appends an entry to the underlying DataQueue. Only valid when
|
||||
// the Outbound was created in streaming mode.
|
||||
bool AppendEntry(std::unique_ptr<DataQueue::Entry> entry) {
|
||||
if (!streaming_ || !queue_) return false;
|
||||
auto size = entry->size();
|
||||
auto result = queue_->append(std::move(entry));
|
||||
return result.has_value() && result.value();
|
||||
if (result.has_value() && result.value()) {
|
||||
if (size.has_value()) queued_ += size.value();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int Pull(bob::Next<ngtcp2_vec> next,
|
||||
@@ -520,6 +592,14 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
size_t count,
|
||||
size_t max_count_hint) {
|
||||
if (next_pending_) {
|
||||
// An async read is in flight, but there may be uncommitted bytes
|
||||
// from a previous read that ngtcp2 didn't accept (nwrite=0 due
|
||||
// to pacing/congestion). Return those bytes so the send loop can
|
||||
// retry rather than blocking until the async read completes.
|
||||
if (uncommitted_ > 0) {
|
||||
PullUncommitted(std::move(next));
|
||||
return bob::Status::STATUS_CONTINUE;
|
||||
}
|
||||
std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {});
|
||||
return bob::Status::STATUS_BLOCK;
|
||||
}
|
||||
@@ -557,9 +637,6 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
// that the pull is sync but allow for it to be async.
|
||||
int ret = reader_->Pull(
|
||||
[this](auto status, auto vecs, auto count, auto done) {
|
||||
// Always make sure next_pending_ is false when we're done.
|
||||
auto on_exit = OnScopeLeave([this] { next_pending_ = false; });
|
||||
|
||||
// The status should never be wait here.
|
||||
DCHECK_NE(status, bob::Status::STATUS_WAIT);
|
||||
|
||||
@@ -568,6 +645,7 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
// being asynchronous, our stream is blocking waiting for the data,
|
||||
// but we have an error! oh no! We need to error the stream.
|
||||
if (next_pending_) {
|
||||
next_pending_ = false;
|
||||
stream_->Destroy(
|
||||
QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR));
|
||||
// We do not need to worry about calling MarkErrored in this case
|
||||
@@ -586,7 +664,10 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
// Here, there is no more data to read, but we will might have data
|
||||
// in the uncommitted queue. We'll resume the stream so that the
|
||||
// session will try to read from it again.
|
||||
// We must clear next_pending_ before calling ResumeStream because
|
||||
// ResumeStream can synchronously re-enter Outbound::Pull.
|
||||
if (next_pending_) {
|
||||
next_pending_ = false;
|
||||
stream_->session().ResumeStream(stream_->id());
|
||||
}
|
||||
return;
|
||||
@@ -610,7 +691,10 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
// being asynchronous, our stream is blocking waiting for the data.
|
||||
// Now that we have data, let's resume the stream so the session will
|
||||
// pull from it again.
|
||||
// We must clear next_pending_ before calling ResumeStream because
|
||||
// ResumeStream can synchronously re-enter Outbound::Pull.
|
||||
if (next_pending_) {
|
||||
next_pending_ = false;
|
||||
stream_->session().ResumeStream(stream_->id());
|
||||
}
|
||||
},
|
||||
@@ -667,9 +751,17 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
|
||||
// Reads here are generally expected to be synchronous. If we have a reader
|
||||
// that insists on providing data asynchronously, then we'll have to block
|
||||
// until the data is actually available.
|
||||
// until the data is actually available. However, if there are uncommitted
|
||||
// bytes already buffered (from a previous async read), return those now
|
||||
// rather than blocking — the async callback will resume the stream when
|
||||
// more data arrives.
|
||||
if (ret == bob::Status::STATUS_WAIT) {
|
||||
next_pending_ = true;
|
||||
if (uncommitted_ > 0) {
|
||||
PullUncommitted(std::move(next));
|
||||
return bob::Status::STATUS_CONTINUE;
|
||||
}
|
||||
|
||||
std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {});
|
||||
return bob::Status::STATUS_BLOCK;
|
||||
}
|
||||
@@ -751,6 +843,11 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
count_++;
|
||||
total_ += vectors[n].len;
|
||||
uncommitted_ += vectors[n].len;
|
||||
if (queued_ >= vectors[n].len) {
|
||||
queued_ -= vectors[n].len;
|
||||
} else {
|
||||
queued_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +894,10 @@ class Stream::Outbound final : public MemoryRetainer {
|
||||
// waiting to be acknowledged. When we receive acknowledgement, we will
|
||||
// automatically free held bytes from the buffer.
|
||||
size_t uncommitted_ = 0;
|
||||
|
||||
// Bytes appended to the DataQueue that haven't been pulled yet.
|
||||
// Decremented in Pull() when data moves from the queue to the buffer.
|
||||
size_t queued_ = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -865,7 +966,6 @@ void Stream::InitPerContext(Realm* realm, Local<Object> target) {
|
||||
}
|
||||
|
||||
Stream* Stream::From(void* stream_user_data) {
|
||||
DCHECK_NOT_NULL(stream_user_data);
|
||||
return static_cast<Stream*>(stream_user_data);
|
||||
}
|
||||
|
||||
@@ -913,6 +1013,7 @@ Stream::Stream(BaseObjectWeakPtr<Session> session,
|
||||
|
||||
set_outbound(std::move(source));
|
||||
|
||||
STAT_RECORD_TIMESTAMP(Stats, created_at);
|
||||
auto params = ngtcp2_conn_get_local_transport_params(this->session());
|
||||
STAT_SET(Stats, max_offset, params->initial_max_data);
|
||||
STAT_SET(Stats, opened_at, stats_->created_at);
|
||||
@@ -945,6 +1046,7 @@ Stream::Stream(BaseObjectWeakPtr<Session> session,
|
||||
|
||||
set_outbound(std::move(source));
|
||||
|
||||
STAT_RECORD_TIMESTAMP(Stats, created_at);
|
||||
auto params = ngtcp2_conn_get_local_transport_params(this->session());
|
||||
STAT_SET(Stats, max_offset, params->initial_max_data);
|
||||
}
|
||||
@@ -969,27 +1071,36 @@ void Stream::NotifyStreamOpened(stream_id id) {
|
||||
CHECK_EQ(ngtcp2_conn_set_stream_user_data(this->session(), id, this), 0);
|
||||
maybe_pending_stream_.reset();
|
||||
|
||||
if (pending_priority_) {
|
||||
auto& priority = pending_priority_.value();
|
||||
if (priority_.pending) {
|
||||
session().application().SetStreamPriority(
|
||||
*this, priority.priority, priority.flags);
|
||||
pending_priority_ = std::nullopt;
|
||||
*this, priority_.priority, priority_.flags);
|
||||
priority_.pending = false;
|
||||
}
|
||||
decltype(pending_headers_queue_) queue;
|
||||
pending_headers_queue_.swap(queue);
|
||||
for (auto& headers : queue) {
|
||||
// TODO(@jasnell): What if the application does not support headers?
|
||||
session().application().SendHeaders(*this,
|
||||
headers->kind,
|
||||
headers->headers.Get(env()->isolate()),
|
||||
headers->flags);
|
||||
if (!pending_headers_queue_.empty()) {
|
||||
if (!session().application().SupportsHeaders()) {
|
||||
// Headers were enqueued while the application was not yet known
|
||||
// (headers_supported == 0), and the negotiated application does
|
||||
// not support headers. This is a fatal mismatch.
|
||||
Destroy(QuicError::ForApplication(0));
|
||||
return;
|
||||
}
|
||||
decltype(pending_headers_queue_) queue;
|
||||
pending_headers_queue_.swap(queue);
|
||||
for (auto& headers : queue) {
|
||||
session().application().SendHeaders(
|
||||
*this,
|
||||
headers->kind,
|
||||
headers->headers.Get(env()->isolate()),
|
||||
headers->flags);
|
||||
}
|
||||
}
|
||||
// If the stream is not a local undirectional stream and is_readable is
|
||||
// false, then we should shutdown the streams readable side now.
|
||||
if (!is_local_unidirectional() && !is_readable()) {
|
||||
NotifyReadableEnded(pending_close_read_code_);
|
||||
}
|
||||
if (!is_remote_unidirectional() && !is_writable()) {
|
||||
if (!is_remote_unidirectional() && !is_writable() &&
|
||||
!session_->application().stream_fin_managed_by_application()) {
|
||||
NotifyWritableEnded(pending_close_write_code_);
|
||||
}
|
||||
|
||||
@@ -1003,7 +1114,7 @@ void Stream::NotifyStreamOpened(stream_id id) {
|
||||
void Stream::NotifyReadableEnded(error_code code) {
|
||||
CHECK(!is_pending());
|
||||
Session::SendPendingDataScope send_scope(&session());
|
||||
ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code);
|
||||
CHECK_EQ(ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code), 0);
|
||||
}
|
||||
|
||||
void Stream::NotifyWritableEnded(error_code code) {
|
||||
@@ -1061,6 +1172,14 @@ bool Stream::is_eos() const {
|
||||
return state_->fin_sent;
|
||||
}
|
||||
|
||||
bool Stream::wants_trailers() const {
|
||||
return state_->wants_trailers;
|
||||
}
|
||||
|
||||
void Stream::set_early() {
|
||||
state_->received_early_data = 1;
|
||||
}
|
||||
|
||||
bool Stream::is_writable() const {
|
||||
// Remote unidirectional streams are never writable, and remote streams can
|
||||
// never be pending.
|
||||
@@ -1071,6 +1190,18 @@ bool Stream::is_writable() const {
|
||||
return state_->write_ended == 0;
|
||||
}
|
||||
|
||||
bool Stream::has_outbound() const {
|
||||
return outbound_ != nullptr;
|
||||
}
|
||||
|
||||
bool Stream::has_reader() const {
|
||||
return reader_ != nullptr;
|
||||
}
|
||||
|
||||
Blob::Reader* Stream::reader() const {
|
||||
return reader_.get();
|
||||
}
|
||||
|
||||
bool Stream::is_readable() const {
|
||||
// Local unidirectional streams are never readable, and remote streams can
|
||||
// never be pending.
|
||||
@@ -1102,7 +1233,11 @@ void Stream::set_outbound(std::shared_ptr<DataQueue> source) {
|
||||
DCHECK_NULL(outbound_);
|
||||
outbound_ = std::make_unique<Outbound>(this, std::move(source));
|
||||
state_->has_outbound = 1;
|
||||
if (!is_pending()) session_->ResumeStream(id());
|
||||
// Note: We intentionally do NOT call ResumeStream here. During
|
||||
// construction, the stream has not yet been added to the session's
|
||||
// streams map, so FindStream would fail. The caller (CreateStream /
|
||||
// AddStream) is responsible for calling ResumeStream after the
|
||||
// stream is registered.
|
||||
}
|
||||
|
||||
void Stream::InitStreaming() {
|
||||
@@ -1120,7 +1255,7 @@ void Stream::InitStreaming() {
|
||||
if (!is_pending()) session_->ResumeStream(id());
|
||||
}
|
||||
|
||||
void Stream::WriteStreamData(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
||||
void Stream::WriteStreamData(const FunctionCallbackInfo<Value>& args) {
|
||||
auto env = this->env();
|
||||
if (outbound_ == nullptr || !outbound_->is_streaming()) {
|
||||
return THROW_ERR_INVALID_STATE(env, "Streaming source is not initialized");
|
||||
@@ -1161,6 +1296,7 @@ void Stream::WriteStreamData(const v8::FunctionCallbackInfo<v8::Value>& args) {
|
||||
|
||||
if (!is_pending()) session_->ResumeStream(id());
|
||||
|
||||
UpdateWriteDesiredSize();
|
||||
args.GetReturnValue().Set(static_cast<double>(outbound_->total()));
|
||||
}
|
||||
|
||||
@@ -1179,9 +1315,8 @@ void Stream::EndWriting() {
|
||||
}
|
||||
|
||||
void Stream::EntryRead(size_t amount) {
|
||||
// Tells us that amount bytes we're reading from inbound_
|
||||
// We use this as a signal to extend the flow control
|
||||
// window to receive more bytes.
|
||||
// Called when the JS consumer reads data from the inbound DataQueue.
|
||||
// Extend the flow control window so the sender can transmit more.
|
||||
session().ExtendStreamOffset(id(), amount);
|
||||
session().ExtendOffset(amount);
|
||||
}
|
||||
@@ -1250,12 +1385,12 @@ void Stream::Acknowledge(size_t datalen) {
|
||||
|
||||
// ngtcp2 guarantees that offset must always be greater than the previously
|
||||
// received offset.
|
||||
DCHECK_GE(datalen, STAT_GET(Stats, max_offset_ack));
|
||||
STAT_SET(Stats, max_offset_ack, datalen);
|
||||
STAT_INCREMENT_N(Stats, max_offset_ack, datalen);
|
||||
|
||||
// Consumes the given number of bytes in the buffer.
|
||||
outbound_->Acknowledge(datalen);
|
||||
STAT_RECORD_TIMESTAMP(Stats, acked_at);
|
||||
UpdateWriteDesiredSize();
|
||||
}
|
||||
|
||||
void Stream::Commit(size_t datalen, bool fin) {
|
||||
@@ -1280,8 +1415,10 @@ void Stream::EndReadable(std::optional<uint64_t> maybe_final_size) {
|
||||
state_->read_ended = 1;
|
||||
set_final_size(maybe_final_size.value_or(STAT_GET(Stats, bytes_received)));
|
||||
inbound_->cap(STAT_GET(Stats, final_size));
|
||||
// Notify the JS reader so it can see EOS.
|
||||
if (reader_) reader_->NotifyPull();
|
||||
// Notify the JS reader so it can see EOS. Pass fin=true so the
|
||||
// wakeup promise resolves with a value the iterator can check to
|
||||
// avoid waiting for another wakeup that will never come.
|
||||
if (reader_) reader_->NotifyPull(true);
|
||||
}
|
||||
|
||||
void Stream::Destroy(QuicError error) {
|
||||
@@ -1327,7 +1464,9 @@ void Stream::Destroy(QuicError error) {
|
||||
|
||||
auto session = session_;
|
||||
session_.reset();
|
||||
session->RemoveStream(id());
|
||||
// EmitClose above triggers MakeCallback which can destroy the session
|
||||
// via JS re-entrancy. The weak pointer may now be null.
|
||||
if (session) session->RemoveStream(id());
|
||||
|
||||
// Critically, make sure that the RemoveStream call is the last thing
|
||||
// trying to use this stream object. Once that call is made, the stream
|
||||
@@ -1342,14 +1481,13 @@ void Stream::ReceiveData(const uint8_t* data,
|
||||
ReceiveDataFlags flags) {
|
||||
// If reading has ended, or there is no data, there's nothing to do but maybe
|
||||
// end the readable side if this is the last bit of data we've received.
|
||||
|
||||
Debug(this, "Receiving %zu bytes of data", len);
|
||||
|
||||
if (state_->read_ended == 1 || len == 0) {
|
||||
if (flags.fin) EndReadable();
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.early) state_->received_early_data = 1;
|
||||
STAT_INCREMENT_N(Stats, bytes_received, len);
|
||||
STAT_SET(Stats, max_offset_received, STAT_GET(Stats, bytes_received));
|
||||
STAT_RECORD_TIMESTAMP(Stats, received_at);
|
||||
@@ -1365,12 +1503,19 @@ void Stream::ReceiveData(const uint8_t* data,
|
||||
}
|
||||
|
||||
void Stream::ReceiveStopSending(QuicError error) {
|
||||
// Note that this comes from *this* endpoint, not the other side. We handle it
|
||||
// if we haven't already shutdown our *receiving* side of the stream.
|
||||
if (state_->read_ended) return;
|
||||
// STOP_SENDING from the peer asks us to stop sending. Per RFC 9000
|
||||
// §3.5 the receiver SHOULD respond with RESET_STREAM, which is what
|
||||
// ngtcp2_conn_shutdown_stream_write below schedules. If our
|
||||
// writable side has already been shut down (e.g. we already sent
|
||||
// RESET_STREAM ourselves or finished sending with FIN) there is
|
||||
// nothing more to do here. The previous guard checked
|
||||
// `state_->read_ended` which is unrelated to the writable side and
|
||||
// suppressed STOP_SENDING handling whenever a sibling RESET_STREAM
|
||||
// frame had been processed first within the same packet.
|
||||
if (state_->write_ended) return;
|
||||
Debug(this, "Received stop sending with error %s", error);
|
||||
ngtcp2_conn_shutdown_stream_read(session(), 0, id(), error.code());
|
||||
EndReadable();
|
||||
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), error.code());
|
||||
EndWritable();
|
||||
}
|
||||
|
||||
void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
|
||||
@@ -1383,6 +1528,7 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
|
||||
"Received stream reset with final size %" PRIu64 " and error %s",
|
||||
final_size,
|
||||
error);
|
||||
state_->reset_code = error.code();
|
||||
EndReadable(final_size);
|
||||
EmitReset(error);
|
||||
}
|
||||
@@ -1400,6 +1546,58 @@ void Stream::EmitBlocked() {
|
||||
MakeCallback(BindingData::Get(env()).stream_blocked_callback(), 0, nullptr);
|
||||
}
|
||||
|
||||
void Stream::EmitDrain() {
|
||||
if (!env()->can_call_into_js()) return;
|
||||
CallbackScope<Stream> cb_scope(this);
|
||||
MakeCallback(BindingData::Get(env()).stream_drain_callback(), 0, nullptr);
|
||||
}
|
||||
|
||||
void Stream::UpdateWriteDesiredSize() {
|
||||
if (!outbound_ || !outbound_->is_streaming()) return;
|
||||
|
||||
uint64_t available;
|
||||
uint64_t hwm = state_->high_water_mark;
|
||||
|
||||
if (is_pending()) {
|
||||
// Pending streams don't have a stream ID yet, so ngtcp2 can't
|
||||
// report their flow control window. Use the high water mark as
|
||||
// the available capacity so writes can proceed while pending.
|
||||
available = hwm > 0 ? hwm : std::numeric_limits<uint32_t>::max();
|
||||
} else {
|
||||
// Calculate available capacity based on QUIC flow control.
|
||||
// The effective limit is the minimum of stream-level and
|
||||
// connection-level flow control remaining.
|
||||
ngtcp2_conn* conn = session();
|
||||
uint64_t stream_left = ngtcp2_conn_get_max_stream_data_left(conn, id());
|
||||
uint64_t conn_left = ngtcp2_conn_get_max_data_left(conn);
|
||||
available = std::min(stream_left, conn_left);
|
||||
|
||||
// Apply the high water mark as an additional ceiling.
|
||||
if (hwm > 0) {
|
||||
available = std::min(available, hwm);
|
||||
}
|
||||
}
|
||||
|
||||
// Total bytes in the pipeline: data in the DataQueue (not yet pulled by
|
||||
// ngtcp2) plus data pulled but not yet acknowledged. Using queued_bytes()
|
||||
// ensures that data appended via writeSync is accounted for in
|
||||
// backpressure even before ngtcp2 pulls it.
|
||||
uint64_t buffered = outbound_->queued_bytes();
|
||||
uint64_t desired = (available > buffered) ? (available - buffered) : 0;
|
||||
|
||||
// Clamp to uint32 range since write_desired_size is uint32_t.
|
||||
uint32_t clamped = static_cast<uint32_t>(
|
||||
std::min<uint64_t>(desired, std::numeric_limits<uint32_t>::max()));
|
||||
|
||||
uint32_t old_size = state_->write_desired_size;
|
||||
state_->write_desired_size = clamped;
|
||||
|
||||
// Fire drain when transitioning from 0 to non-zero
|
||||
if (old_size == 0 && desired > 0) {
|
||||
EmitDrain();
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::EmitClose(const QuicError& error) {
|
||||
if (!env()->can_call_into_js()) return;
|
||||
CallbackScope<Stream> cb_scope(this);
|
||||
@@ -1458,6 +1656,13 @@ void Stream::Schedule(Queue* queue) {
|
||||
if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this);
|
||||
}
|
||||
|
||||
void Stream::Unschedule() {
|
||||
// Remove this stream from the send queue. Used when the stream becomes
|
||||
// flow-control blocked so that SendPendingData does not spin retrying it.
|
||||
Debug(this, "Unscheduled");
|
||||
stream_queue_.Remove();
|
||||
}
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
|
||||
+31
-4
@@ -218,12 +218,27 @@ class Stream final : public AsyncWrap,
|
||||
// data to be acknowledged by the remote peer.
|
||||
bool is_eos() const;
|
||||
|
||||
// True if the stream wants to send trailing headers after the body.
|
||||
bool wants_trailers() const;
|
||||
|
||||
// Marks this stream as having received 0-RTT early data.
|
||||
void set_early();
|
||||
|
||||
// True if this stream is still in a readable state.
|
||||
bool is_readable() const;
|
||||
|
||||
// True if this stream is still in a writable state.
|
||||
bool is_writable() const;
|
||||
|
||||
// True if an outbound data source has been configured.
|
||||
bool has_outbound() const;
|
||||
|
||||
// True if a Blob::Reader has been created for the inbound data.
|
||||
bool has_reader() const;
|
||||
|
||||
// Returns the Blob::Reader for the inbound data, or nullptr.
|
||||
Blob::Reader* reader() const;
|
||||
|
||||
// Called by the session/application to indicate that the specified number
|
||||
// of bytes have been acknowledged by the peer.
|
||||
void Acknowledge(size_t datalen);
|
||||
@@ -326,6 +341,14 @@ class Stream final : public AsyncWrap,
|
||||
// blocked because of flow control restriction.
|
||||
void EmitBlocked();
|
||||
|
||||
// Notifies the JavaScript side that the outbound buffer has capacity
|
||||
// for more data. Fires when write_desired_size transitions from 0 to > 0.
|
||||
void EmitDrain();
|
||||
|
||||
// Updates the write_desired_size state field based on current flow control
|
||||
// and outbound buffer state. Emits drain if transitioning from 0 to > 0.
|
||||
void UpdateWriteDesiredSize();
|
||||
|
||||
// Delivers the set of inbound headers that have been collected.
|
||||
void EmitHeaders();
|
||||
|
||||
@@ -355,11 +378,14 @@ class Stream final : public AsyncWrap,
|
||||
error_code pending_close_read_code_ = 0;
|
||||
error_code pending_close_write_code_ = 0;
|
||||
|
||||
struct PendingPriority {
|
||||
StreamPriority priority;
|
||||
StreamPriorityFlags flags;
|
||||
struct StoredPriority {
|
||||
StreamPriority priority = StreamPriority::DEFAULT;
|
||||
StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL;
|
||||
bool pending = false;
|
||||
};
|
||||
std::optional<PendingPriority> pending_priority_ = std::nullopt;
|
||||
StoredPriority priority_;
|
||||
|
||||
const StoredPriority& stored_priority() const { return priority_; }
|
||||
|
||||
// The headers_ field holds a block of headers that have been received and
|
||||
// are being buffered for delivery to the JavaScript side.
|
||||
@@ -393,6 +419,7 @@ class Stream final : public AsyncWrap,
|
||||
using Queue = ListHead<Stream, &Stream::stream_queue_>;
|
||||
|
||||
void Schedule(Queue* queue);
|
||||
void Unschedule();
|
||||
};
|
||||
|
||||
} // namespace node::quic
|
||||
|
||||
+28
-11
@@ -631,8 +631,16 @@ int TLSContext::OnSNI(SSL* ssl, int* ad, void* arg) {
|
||||
auto it = default_ctx->sni_contexts_.find(servername);
|
||||
if (it != default_ctx->sni_contexts_.end()) {
|
||||
SSL_set_SSL_CTX(ssl, it->second->ctx_.get());
|
||||
return SSL_TLSEXT_ERR_OK;
|
||||
}
|
||||
}
|
||||
// No matching hostname found. If the default context has a certificate
|
||||
// (from the sni['*'] wildcard identity), fall through to use it.
|
||||
// Otherwise, reject the connection with an unrecognized_name alert.
|
||||
if (SSL_CTX_get0_certificate(default_ctx->ctx_.get()) == nullptr) {
|
||||
*ad = SSL_AD_UNRECOGNIZED_NAME;
|
||||
return SSL_TLSEXT_ERR_ALERT_FATAL;
|
||||
}
|
||||
return SSL_TLSEXT_ERR_OK;
|
||||
}
|
||||
|
||||
@@ -697,9 +705,10 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
|
||||
if (!SET(verify_client) || !SET(reject_unauthorized) ||
|
||||
!SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) ||
|
||||
!SET(servername) || !SET(ciphers) || !SET(groups) ||
|
||||
!SET(verify_private_key) || !SET(keylog) ||
|
||||
!SET_VECTOR(crypto::KeyObjectData, keys) || !SET_VECTOR(Store, certs) ||
|
||||
!SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) {
|
||||
!SET(verify_private_key) || !SET(keylog) || !SET(port) ||
|
||||
!SET(authoritative) || !SET_VECTOR(crypto::KeyObjectData, keys) ||
|
||||
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
|
||||
!SET_VECTOR(Store, crl)) {
|
||||
return Nothing<Options>();
|
||||
}
|
||||
|
||||
@@ -840,15 +849,23 @@ void TLSSession::Initialize(
|
||||
|
||||
// The early data will just be ignored if it's invalid.
|
||||
if (ossl_context_.set_session_ticket(ticket)) {
|
||||
ngtcp2_vec rtp = sessionTicket.transport_params();
|
||||
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
|
||||
*session_, rtp.base, rtp.len) == 0) {
|
||||
if (!ossl_context_.set_early_data_enabled()) {
|
||||
validation_error_ = "Failed to enable early data";
|
||||
ossl_context_.reset();
|
||||
return;
|
||||
// Only enable 0-RTT if the option allows it. The session
|
||||
// ticket is still used for TLS resumption (1-RTT) either way.
|
||||
if (options.enable_early_data) {
|
||||
ngtcp2_vec rtp = sessionTicket.transport_params();
|
||||
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
|
||||
*session_, rtp.base, rtp.len) == 0) {
|
||||
if (!ossl_context_.set_early_data_enabled()) {
|
||||
validation_error_ = "Failed to enable early data";
|
||||
ossl_context_.reset();
|
||||
return;
|
||||
}
|
||||
session_->SetStreamOpenAllowed();
|
||||
// Populate the state buffer from the 0-RTT transport
|
||||
// params so that maxDatagramSize and other values are
|
||||
// available before the handshake completes.
|
||||
session_->PopulateEarlyTransportParamsState();
|
||||
}
|
||||
session_->SetStreamOpenAllowed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +241,15 @@ class TLSContext final : public MemoryRetainer,
|
||||
// JavaScript option name "crl"
|
||||
std::vector<Store> crl;
|
||||
|
||||
// The port to advertise in ORIGIN frames for this hostname.
|
||||
// Defaults to 443 (the standard HTTPS port). Only relevant for
|
||||
// server-side SNI entries used with HTTP/3.
|
||||
uint16_t port = 443;
|
||||
|
||||
// Whether this hostname should be included in ORIGIN frames.
|
||||
// Only relevant for server-side SNI entries.
|
||||
bool authoritative = true;
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
SET_MEMORY_INFO_NAME(TLSContext::Options)
|
||||
SET_SELF_SIZE(Options)
|
||||
|
||||
+30
-13
@@ -61,42 +61,59 @@ std::string TokenSecret::ToString() const {
|
||||
// ============================================================================
|
||||
// StatelessResetToken
|
||||
|
||||
StatelessResetToken::StatelessResetToken() : ptr_(nullptr), buf_() {}
|
||||
StatelessResetToken::StatelessResetToken()
|
||||
: ngtcp2_stateless_reset_token(), ptr_(nullptr) {}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const uint8_t* token) : ptr_(token) {}
|
||||
StatelessResetToken::StatelessResetToken(const uint8_t* token)
|
||||
: ptr_(reinterpret_cast<const ngtcp2_stateless_reset_token*>(token)) {}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(
|
||||
const ngtcp2_stateless_reset_token* token)
|
||||
: ptr_(token) {}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const TokenSecret& secret,
|
||||
const CID& cid)
|
||||
: ptr_(buf_) {
|
||||
: ptr_(this) {
|
||||
CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token(
|
||||
buf_, secret, kStatelessTokenLen, cid),
|
||||
data, secret, kStatelessTokenLen, cid),
|
||||
0);
|
||||
}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(uint8_t* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid)
|
||||
: ptr_(token) {
|
||||
: ptr_(reinterpret_cast<const ngtcp2_stateless_reset_token*>(token)) {
|
||||
CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token(
|
||||
token, secret, kStatelessTokenLen, cid),
|
||||
0);
|
||||
}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(ngtcp2_stateless_reset_token* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid)
|
||||
: ptr_(token) {
|
||||
CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token(
|
||||
token->data, secret, kStatelessTokenLen, cid),
|
||||
0);
|
||||
}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const StatelessResetToken& other)
|
||||
: ptr_(buf_) {
|
||||
: ngtcp2_stateless_reset_token(), ptr_(other ? this : nullptr) {
|
||||
if (other) {
|
||||
memcpy(buf_, other.ptr_, kStatelessTokenLen);
|
||||
} else {
|
||||
ptr_ = nullptr;
|
||||
memcpy(data, other.ptr_->data, kStatelessTokenLen);
|
||||
}
|
||||
}
|
||||
|
||||
StatelessResetToken::operator const uint8_t*() const {
|
||||
return ptr_ != nullptr ? ptr_ : buf_;
|
||||
return ptr_ != nullptr ? ptr_->data : data;
|
||||
}
|
||||
|
||||
StatelessResetToken::operator const ngtcp2_stateless_reset_token*() const {
|
||||
return ptr_;
|
||||
}
|
||||
|
||||
StatelessResetToken::operator const char*() const {
|
||||
return reinterpret_cast<const char*>(ptr_ != nullptr ? ptr_ : buf_);
|
||||
return reinterpret_cast<const char*>(ptr_ != nullptr ? ptr_->data : data);
|
||||
}
|
||||
|
||||
StatelessResetToken::operator bool() const {
|
||||
@@ -109,7 +126,7 @@ bool StatelessResetToken::operator==(const StatelessResetToken& other) const {
|
||||
(ptr_ != nullptr && other.ptr_ == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
return CRYPTO_memcmp(ptr_, other.ptr_, kStatelessTokenLen) == 0;
|
||||
return CRYPTO_memcmp(ptr_->data, other.ptr_->data, kStatelessTokenLen) == 0;
|
||||
}
|
||||
|
||||
bool StatelessResetToken::operator!=(const StatelessResetToken& other) const {
|
||||
@@ -128,7 +145,7 @@ std::string StatelessResetToken::ToString() const {
|
||||
size_t StatelessResetToken::Hash::operator()(
|
||||
const StatelessResetToken& token) const {
|
||||
if (token.ptr_ == nullptr) return 0;
|
||||
return HashBytes(token.ptr_, kStatelessTokenLen);
|
||||
return HashBytes(token.ptr_->data, kStatelessTokenLen);
|
||||
}
|
||||
|
||||
StatelessResetToken StatelessResetToken::kInvalid;
|
||||
|
||||
+16
-11
@@ -70,7 +70,8 @@ class TokenSecret final : public MemoryRetainer {
|
||||
//
|
||||
// StatlessResetTokens are always kStatelessTokenLen bytes,
|
||||
// as are the secrets used to generate the token.
|
||||
class StatelessResetToken final : public MemoryRetainer {
|
||||
class StatelessResetToken final : public ngtcp2_stateless_reset_token,
|
||||
public MemoryRetainer {
|
||||
public:
|
||||
static constexpr int kStatelessTokenLen = NGTCP2_STATELESS_RESET_TOKENLEN;
|
||||
|
||||
@@ -78,30 +79,35 @@ class StatelessResetToken final : public MemoryRetainer {
|
||||
|
||||
// Generates a stateless reset token using HKDF with the cid and token secret
|
||||
// as input. The token secret is either provided by user code when an Endpoint
|
||||
// is created or is generated randomly.
|
||||
// is created or is generated randomly. The token is stored in the inherited
|
||||
// ngtcp2_stateless_reset_token::data and ptr_ is set to this.
|
||||
StatelessResetToken(const TokenSecret& secret, const CID& cid);
|
||||
|
||||
// Generates a stateless reset token using the given token storage.
|
||||
// Generates a stateless reset token into the given external storage.
|
||||
// The StatelessResetToken wraps the token and does not take ownership.
|
||||
// The token storage must be at least kStatelessTokenLen bytes in length.
|
||||
// The length is not verified so care must be taken when using this
|
||||
// constructor.
|
||||
StatelessResetToken(uint8_t* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid);
|
||||
|
||||
// Generates a stateless reset token into the given external storage.
|
||||
// The StatelessResetToken wraps the token and does not take ownership.
|
||||
StatelessResetToken(ngtcp2_stateless_reset_token* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid);
|
||||
|
||||
// Wraps the given token. Does not take over ownership of the token storage.
|
||||
// The token must be at least kStatelessTokenLen bytes in length.
|
||||
// The length is not verified so care must be taken when using this
|
||||
// constructor.
|
||||
explicit StatelessResetToken(const uint8_t* token);
|
||||
|
||||
// Wraps the given token. Does not take over ownership of the token storage.
|
||||
explicit StatelessResetToken(const ngtcp2_stateless_reset_token* token);
|
||||
|
||||
StatelessResetToken(const StatelessResetToken& other);
|
||||
DISALLOW_MOVE(StatelessResetToken)
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
operator const uint8_t*() const;
|
||||
operator const ngtcp2_stateless_reset_token*() const;
|
||||
operator bool() const;
|
||||
|
||||
bool operator==(const StatelessResetToken& other) const;
|
||||
@@ -124,8 +130,7 @@ class StatelessResetToken final : public MemoryRetainer {
|
||||
private:
|
||||
operator const char*() const;
|
||||
|
||||
const uint8_t* ptr_;
|
||||
uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN];
|
||||
const ngtcp2_stateless_reset_token* ptr_;
|
||||
};
|
||||
|
||||
// A RETRY packet communicates a retry token to the client. Retry tokens are
|
||||
|
||||
+58
-10
@@ -1,6 +1,8 @@
|
||||
#if HAVE_OPENSSL && HAVE_QUIC
|
||||
#include "guard.h"
|
||||
#ifndef OPENSSL_NO_QUIC
|
||||
#include <async_wrap-inl.h>
|
||||
#include <base_object-inl.h>
|
||||
#include <env-inl.h>
|
||||
#include <memory_tracker-inl.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
@@ -10,6 +12,7 @@
|
||||
#include "defs.h"
|
||||
#include "endpoint.h"
|
||||
#include "session.h"
|
||||
#include "session_manager.h"
|
||||
#include "tokens.h"
|
||||
#include "transportparams.h"
|
||||
|
||||
@@ -69,10 +72,49 @@ Maybe<TransportParams::Options> TransportParams::Options::From(
|
||||
|
||||
#undef SET
|
||||
|
||||
// TODO(@jasnell): We are not yet exposing the ability to set the preferred
|
||||
// adddress via the options, tho the underlying support is here in the class.
|
||||
options.preferred_address_ipv4 = std::nullopt;
|
||||
options.preferred_address_ipv6 = std::nullopt;
|
||||
// Parse the preferred address options. These are SocketAddress objects
|
||||
// (or undefined to skip). Only meaningful for server sessions.
|
||||
Local<Value> preferred_ipv4;
|
||||
if (!params->Get(env->context(), state.preferred_address_ipv4_string())
|
||||
.ToLocal(&preferred_ipv4)) {
|
||||
return Nothing<Options>();
|
||||
}
|
||||
if (!preferred_ipv4->IsUndefined()) {
|
||||
if (!SocketAddressBase::HasInstance(env, preferred_ipv4)) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
env, "transportParams.preferredAddressIpv4 must be a SocketAddress");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
auto* addr = BaseObject::FromJSObject<SocketAddressBase>(
|
||||
preferred_ipv4.As<Object>());
|
||||
if (addr->address()->family() != AF_INET) {
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "transportParams.preferredAddressIpv4 must be an IPv4 address");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
options.preferred_address_ipv4 = *addr->address();
|
||||
}
|
||||
|
||||
Local<Value> preferred_ipv6;
|
||||
if (!params->Get(env->context(), state.preferred_address_ipv6_string())
|
||||
.ToLocal(&preferred_ipv6)) {
|
||||
return Nothing<Options>();
|
||||
}
|
||||
if (!preferred_ipv6->IsUndefined()) {
|
||||
if (!SocketAddressBase::HasInstance(env, preferred_ipv6)) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
env, "transportParams.preferredAddressIpv6 must be a SocketAddress");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
auto* addr = BaseObject::FromJSObject<SocketAddressBase>(
|
||||
preferred_ipv6.As<Object>());
|
||||
if (addr->address()->family() != AF_INET6) {
|
||||
THROW_ERR_INVALID_ARG_VALUE(
|
||||
env, "transportParams.preferredAddressIpv6 must be an IPv6 address");
|
||||
return Nothing<Options>();
|
||||
}
|
||||
options.preferred_address_ipv6 = *addr->address();
|
||||
}
|
||||
|
||||
return Just<Options>(options);
|
||||
}
|
||||
@@ -113,8 +155,6 @@ std::string TransportParams::Options::ToString() const {
|
||||
res += prefix + "max ack delay: " + std::to_string(max_ack_delay);
|
||||
res += prefix +
|
||||
"max datagram frame size: " + std::to_string(max_datagram_frame_size);
|
||||
res += prefix + "disable active migration: " +
|
||||
(disable_active_migration ? std::string("yes") : std::string("no"));
|
||||
res += indent.Close();
|
||||
return res;
|
||||
}
|
||||
@@ -151,8 +191,8 @@ TransportParams::TransportParams(const Config& config, const Options& options)
|
||||
SET_PARAM(ack_delay_exponent);
|
||||
SET_PARAM(max_datagram_frame_size);
|
||||
SET_PARAM_V(max_idle_timeout, options.max_idle_timeout * NGTCP2_SECONDS);
|
||||
SET_PARAM_V(disable_active_migration,
|
||||
options.disable_active_migration ? 1 : 0);
|
||||
SET_PARAM_V(disable_active_migration, 0);
|
||||
SET_PARAM_V(grease_quic_bit, 1);
|
||||
SET_PARAM_V(preferred_addr_present, 0);
|
||||
SET_PARAM_V(stateless_reset_token_present, 0);
|
||||
SET_PARAM_V(retry_scid_present, 0);
|
||||
@@ -172,11 +212,13 @@ TransportParams::TransportParams(const Config& config, const Options& options)
|
||||
#undef SET_PARAM
|
||||
#undef SET_PARAM_V
|
||||
|
||||
if (options.preferred_address_ipv4.has_value())
|
||||
if (options.preferred_address_ipv4.has_value()) {
|
||||
SetPreferredAddress(options.preferred_address_ipv4.value());
|
||||
}
|
||||
|
||||
if (options.preferred_address_ipv6.has_value())
|
||||
if (options.preferred_address_ipv6.has_value()) {
|
||||
SetPreferredAddress(options.preferred_address_ipv6.value());
|
||||
}
|
||||
}
|
||||
|
||||
TransportParams::TransportParams(const ngtcp2_vec& vec, Version version)
|
||||
@@ -288,6 +330,12 @@ void TransportParams::GeneratePreferredAddressToken(Session* session) {
|
||||
params_.preferred_addr.stateless_reset_token,
|
||||
config.preferred_address_cid),
|
||||
session);
|
||||
// Register the preferred address CID with SessionManager for
|
||||
// cross-endpoint routing. This is a locally-generated CID that needs
|
||||
// to be routable from the preferred address endpoint (which may be
|
||||
// different from the primary endpoint).
|
||||
auto& mgr = BindingData::Get(session->env()).session_manager();
|
||||
mgr.AssociateCID(config.preferred_address_cid, config.scid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,16 +114,9 @@ class TransportParams final {
|
||||
// The maximum size of DATAGRAM frames that the endpoint will accept.
|
||||
// Setting the value to 0 will disable DATAGRAM support.
|
||||
// https://datatracker.ietf.org/doc/html/rfc9221#section-3
|
||||
uint64_t max_datagram_frame_size = kDefaultMaxPacketLength;
|
||||
uint16_t max_datagram_frame_size = kDefaultMaxPacketLength;
|
||||
|
||||
// When true, communicates that the Session does not support active
|
||||
// connection migration. See the QUIC specification for more details on
|
||||
// connection migration.
|
||||
// https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.30.1
|
||||
// TODO(@jasnell): Active connection migration is not yet implemented.
|
||||
// This will be revisited in a future update.
|
||||
bool disable_active_migration = true;
|
||||
|
||||
static const Options kDefault;
|
||||
|
||||
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||
|
||||
@@ -495,25 +495,10 @@ TEST(DataQueue, NonIdempotentDataQueue) {
|
||||
CHECK(!waitingForPull);
|
||||
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
|
||||
|
||||
// We can read the expected data from reader1. Because the entries are
|
||||
// InMemoryEntry instances, reads will be fully synchronous here.
|
||||
// The next read produces buffer2. When the first entry's reader returns
|
||||
// EOS, the NonIdempotentDataQueueReader immediately pulls from the next
|
||||
// entry (recursive Pull), so the transition is seamless.
|
||||
waitingForPull = true;
|
||||
|
||||
status = reader->Pull(
|
||||
[&](int status, const DataQueue::Vec* vecs, size_t count, auto done) {
|
||||
waitingForPull = false;
|
||||
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
|
||||
CHECK_EQ(count, 0);
|
||||
},
|
||||
node::bob::OPTIONS_SYNC,
|
||||
nullptr,
|
||||
0,
|
||||
node::bob::kMaxCountHint);
|
||||
|
||||
CHECK(!waitingForPull);
|
||||
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
|
||||
|
||||
// The next read produces buffer2, and should be the end.
|
||||
status = reader->Pull(
|
||||
[&](int status, const DataQueue::Vec* vecs, size_t count, auto done) {
|
||||
waitingForPull = false;
|
||||
@@ -628,6 +613,9 @@ TEST(DataQueue, DataQueueEntry) {
|
||||
CHECK(!pullIsPending);
|
||||
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
|
||||
|
||||
// Cap the queue so the reader can reach EOS after draining all entries.
|
||||
data_queue2->cap();
|
||||
|
||||
// Read to completion...
|
||||
while (status != node::bob::STATUS_EOS) {
|
||||
status = reader->Pull(
|
||||
|
||||
@@ -56,7 +56,7 @@ TEST(StatelessResetToken, Basic) {
|
||||
|
||||
CHECK_EQ(token, token2);
|
||||
|
||||
// Let's pretend out secret is also a token just for the sake
|
||||
// Let's pretend our secret is also a token just for the sake
|
||||
// of the test. That's ok because they're the same length.
|
||||
StatelessResetToken token3(secret);
|
||||
|
||||
@@ -85,6 +85,83 @@ TEST(StatelessResetToken, Basic) {
|
||||
CHECK_EQ(found->second, token);
|
||||
}
|
||||
|
||||
TEST(StatelessResetToken, Ngtcp2StructIntegration) {
|
||||
uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6};
|
||||
uint8_t cid_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
|
||||
ngtcp2_cid cid_;
|
||||
ngtcp2_cid_init(&cid_, cid_data, 10);
|
||||
TokenSecret fixed_secret(secret);
|
||||
CID cid(cid_);
|
||||
|
||||
// Owning token — generated into the inherited ngtcp2_stateless_reset_token
|
||||
StatelessResetToken owning(fixed_secret, cid);
|
||||
CHECK(owning);
|
||||
|
||||
// The ngtcp2_stateless_reset_token* conversion operator should return
|
||||
// a valid pointer to the token data.
|
||||
const ngtcp2_stateless_reset_token* as_struct = owning;
|
||||
CHECK_NE(as_struct, nullptr);
|
||||
// The struct's data should match the uint8_t* conversion.
|
||||
const uint8_t* as_bytes = owning;
|
||||
CHECK_EQ(
|
||||
memcmp(
|
||||
as_struct->data, as_bytes, StatelessResetToken::kStatelessTokenLen),
|
||||
0);
|
||||
|
||||
// Non-owning from const ngtcp2_stateless_reset_token* — wraps an
|
||||
// existing struct without copying.
|
||||
StatelessResetToken from_struct(as_struct);
|
||||
CHECK(from_struct);
|
||||
CHECK_EQ(from_struct, owning);
|
||||
// The pointer should be the same (non-owning wraps, doesn't copy).
|
||||
const ngtcp2_stateless_reset_token* from_struct_ptr = from_struct;
|
||||
CHECK_EQ(from_struct_ptr, as_struct);
|
||||
|
||||
// Owning into external ngtcp2_stateless_reset_token — generates the
|
||||
// token into a caller-provided struct.
|
||||
ngtcp2_stateless_reset_token external_struct{};
|
||||
StatelessResetToken into_struct(&external_struct, fixed_secret, cid);
|
||||
CHECK(into_struct);
|
||||
CHECK_EQ(into_struct, owning);
|
||||
// The external struct should now contain the generated token.
|
||||
CHECK_EQ(memcmp(external_struct.data,
|
||||
as_bytes,
|
||||
StatelessResetToken::kStatelessTokenLen),
|
||||
0);
|
||||
// The conversion operator should return a pointer to the external struct.
|
||||
const ngtcp2_stateless_reset_token* into_struct_ptr = into_struct;
|
||||
CHECK_EQ(into_struct_ptr, &external_struct);
|
||||
|
||||
// Copy of an owning token should itself be owning (independent copy).
|
||||
StatelessResetToken copy_of_owning = owning;
|
||||
CHECK_EQ(copy_of_owning, owning);
|
||||
const ngtcp2_stateless_reset_token* copy_ptr = copy_of_owning;
|
||||
// Should NOT point to the same memory as the original.
|
||||
CHECK_NE(copy_ptr, as_struct);
|
||||
// But data should match.
|
||||
CHECK_EQ(
|
||||
memcmp(copy_ptr->data, as_bytes, StatelessResetToken::kStatelessTokenLen),
|
||||
0);
|
||||
|
||||
// Copy of a non-owning token should become owning (copies data).
|
||||
StatelessResetToken copy_of_non_owning = from_struct;
|
||||
CHECK_EQ(copy_of_non_owning, from_struct);
|
||||
const ngtcp2_stateless_reset_token* copy_no_ptr = copy_of_non_owning;
|
||||
// Should NOT point to the original non-owning source.
|
||||
CHECK_NE(copy_no_ptr, from_struct_ptr);
|
||||
|
||||
// kInvalid conversions.
|
||||
const ngtcp2_stateless_reset_token* invalid_ptr =
|
||||
StatelessResetToken::kInvalid;
|
||||
CHECK_EQ(invalid_ptr, nullptr);
|
||||
const uint8_t* invalid_bytes = StatelessResetToken::kInvalid;
|
||||
// When ptr_ is null, falls back to inherited data (zeroed).
|
||||
uint8_t zeroed[StatelessResetToken::kStatelessTokenLen]{};
|
||||
CHECK_EQ(
|
||||
memcmp(invalid_bytes, zeroed, StatelessResetToken::kStatelessTokenLen),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST(RetryToken, Basic) {
|
||||
auto& random = CID::Factory::random();
|
||||
TokenSecret secret;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "node_sockaddr-inl.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "node_sockaddr-inl.h"
|
||||
|
||||
using node::SocketAddress;
|
||||
using node::SocketAddressBlockList;
|
||||
@@ -43,6 +43,85 @@ TEST(SocketAddress, SocketAddress) {
|
||||
CHECK_EQ(map[addr], 2);
|
||||
}
|
||||
|
||||
TEST(SocketAddress, IpHashAndIpEqual) {
|
||||
sockaddr_storage s1, s2, s3, s4;
|
||||
// Same IP, different ports.
|
||||
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 443, &s1);
|
||||
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 8080, &s2);
|
||||
// Different IP.
|
||||
SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 443, &s3);
|
||||
|
||||
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
|
||||
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
|
||||
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&s3));
|
||||
|
||||
SocketAddress::IpHash ip_hash;
|
||||
SocketAddress::IpEqual ip_equal;
|
||||
|
||||
// Same IP, different port: should hash equal and compare equal.
|
||||
CHECK_EQ(ip_hash(addr1), ip_hash(addr2));
|
||||
CHECK(ip_equal(addr1, addr2));
|
||||
|
||||
// Different IP: should not compare equal.
|
||||
CHECK(!ip_equal(addr1, addr3));
|
||||
|
||||
// Full Hash (includes port) should differ for same IP, different port.
|
||||
CHECK_NE(SocketAddress::Hash()(addr1), SocketAddress::Hash()(addr2));
|
||||
|
||||
// IpMap should treat same-IP-different-port as the same key.
|
||||
SocketAddress::IpMap<uint16_t> map;
|
||||
map[addr1] = 1;
|
||||
map[addr2]++; // Same IP as addr1, should increment the same entry.
|
||||
CHECK_EQ(map[addr1], 2);
|
||||
CHECK_EQ(map.size(), 1);
|
||||
|
||||
map[addr3] = 10;
|
||||
CHECK_EQ(map.size(), 2);
|
||||
CHECK_EQ(map[addr3], 10);
|
||||
}
|
||||
|
||||
TEST(SocketAddress, IpHashIPv6) {
|
||||
sockaddr_storage s1, s2, s3;
|
||||
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s1);
|
||||
SocketAddress::ToSockAddr(AF_INET6, "::1", 8080, &s2);
|
||||
SocketAddress::ToSockAddr(AF_INET6, "::2", 443, &s3);
|
||||
|
||||
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
|
||||
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
|
||||
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&s3));
|
||||
|
||||
SocketAddress::IpHash ip_hash;
|
||||
SocketAddress::IpEqual ip_equal;
|
||||
|
||||
// Same IPv6, different port: equal.
|
||||
CHECK_EQ(ip_hash(addr1), ip_hash(addr2));
|
||||
CHECK(ip_equal(addr1, addr2));
|
||||
|
||||
// Different IPv6: not equal.
|
||||
CHECK(!ip_equal(addr1, addr3));
|
||||
|
||||
// IpMap with IPv6 keys.
|
||||
SocketAddress::IpMap<uint16_t> map;
|
||||
map[addr1] = 5;
|
||||
map[addr2]++;
|
||||
CHECK_EQ(map[addr1], 6);
|
||||
CHECK_EQ(map.size(), 1);
|
||||
}
|
||||
|
||||
TEST(SocketAddress, IpEqualCrossFamily) {
|
||||
sockaddr_storage s1, s2;
|
||||
SocketAddress::ToSockAddr(AF_INET, "127.0.0.1", 443, &s1);
|
||||
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s2);
|
||||
|
||||
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
|
||||
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
|
||||
|
||||
SocketAddress::IpEqual ip_equal;
|
||||
|
||||
// Different address families should never be equal.
|
||||
CHECK(!ip_equal(addr1, addr2));
|
||||
}
|
||||
|
||||
TEST(SocketAddress, SocketAddressIPv6) {
|
||||
sockaddr_storage storage;
|
||||
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &storage);
|
||||
@@ -85,7 +164,6 @@ TEST(SocketAddressLRU, SocketAddressLRU) {
|
||||
SocketAddress::ToSockAddr(AF_INET, "123.123.123.125", 443, &storage[2]);
|
||||
SocketAddress::ToSockAddr(AF_INET, "123.123.123.123", 443, &storage[3]);
|
||||
|
||||
|
||||
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&storage[0]));
|
||||
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&storage[1]));
|
||||
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&storage[2]));
|
||||
@@ -197,12 +275,10 @@ TEST(SocketAddressBlockList, Simple) {
|
||||
sockaddr_storage storage[2];
|
||||
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 0, &storage[0]);
|
||||
SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 0, &storage[1]);
|
||||
std::shared_ptr<SocketAddress> addr1 =
|
||||
std::make_shared<SocketAddress>(
|
||||
reinterpret_cast<const sockaddr*>(&storage[0]));
|
||||
std::shared_ptr<SocketAddress> addr2 =
|
||||
std::make_shared<SocketAddress>(
|
||||
reinterpret_cast<const sockaddr*>(&storage[1]));
|
||||
std::shared_ptr<SocketAddress> addr1 = std::make_shared<SocketAddress>(
|
||||
reinterpret_cast<const sockaddr*>(&storage[0]));
|
||||
std::shared_ptr<SocketAddress> addr2 = std::make_shared<SocketAddress>(
|
||||
reinterpret_cast<const sockaddr*>(&storage[1]));
|
||||
|
||||
bl.AddSocketAddress(addr1);
|
||||
bl.AddSocketAddress(addr2);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Shared helpers for QUIC tests.
|
||||
//
|
||||
// Usage:
|
||||
// import { key, cert, listen, connect } from '../common/quic.mjs';
|
||||
//
|
||||
// Provides pre-loaded TLS credentials and thin wrappers around node:quic
|
||||
// listen/connect that apply default options suitable for most tests.
|
||||
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
const quic = await import('node:quic');
|
||||
|
||||
// Pre-loaded TLS credentials from the standard agent1 fixture pair.
|
||||
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
|
||||
const cert = fixtures.readKey('agent1-cert.pem');
|
||||
|
||||
/**
|
||||
* Start a QUIC server with sensible test defaults.
|
||||
* @param {Function} callback The session callback (receives QuicSession).
|
||||
* @param {object} [options] Options forwarded to quic.listen(). The
|
||||
* following defaults are applied when not specified:
|
||||
* - sni: { '*': { keys: [key], certs: [cert] } }
|
||||
* - alpn: ['quic-test']
|
||||
* @returns {Promise<QuicEndpoint>}
|
||||
*/
|
||||
async function listen(callback, options = {}) {
|
||||
const {
|
||||
sni = { '*': { keys: [key], certs: [cert] } },
|
||||
alpn = ['quic-test'],
|
||||
...rest
|
||||
} = options;
|
||||
return quic.listen(callback, { sni, alpn, ...rest });
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a QUIC client with sensible test defaults.
|
||||
* @param {SocketAddress|string} address The server address.
|
||||
* @param {object} [options] Options forwarded to quic.connect(). The
|
||||
* following defaults are applied when not specified:
|
||||
* - alpn: 'quic-test'
|
||||
* @returns {Promise<QuicSession>}
|
||||
*/
|
||||
async function connect(address, options = {}) {
|
||||
const {
|
||||
alpn = 'quic-test',
|
||||
...rest
|
||||
} = options;
|
||||
return quic.connect(address, { alpn, ...rest });
|
||||
}
|
||||
|
||||
export {
|
||||
key,
|
||||
cert,
|
||||
listen,
|
||||
connect,
|
||||
};
|
||||
Vendored
+24
-1
@@ -2,6 +2,7 @@ all: \
|
||||
ca1-cert.pem \
|
||||
ca2-cert.pem \
|
||||
ca2-crl.pem \
|
||||
ca2-crl-agent3.pem \
|
||||
ca3-cert.pem \
|
||||
ca4-cert.pem \
|
||||
ca5-cert.pem \
|
||||
@@ -529,6 +530,28 @@ ca2-crl.pem: ca2-key.pem ca2-cert.pem ca2.cnf agent4-cert.pem
|
||||
-out ca2-crl.pem \
|
||||
-passin 'pass:password'
|
||||
|
||||
#
|
||||
# Make CRL with agent3 being rejected
|
||||
# Uses a separate temporary database so the ca2-crl.pem revocation of agent4
|
||||
# does not contaminate this CRL.
|
||||
#
|
||||
ca2-crl-agent3.pem: ca2-key.pem ca2-cert.pem ca2.cnf agent3-cert.pem
|
||||
@> ca2-crl-agent3-database.txt
|
||||
@sed 's/ca2-database/ca2-crl-agent3-database/' ca2.cnf > ca2-crl-agent3.cnf
|
||||
openssl ca -revoke agent3-cert.pem \
|
||||
-keyfile ca2-key.pem \
|
||||
-cert ca2-cert.pem \
|
||||
-config ca2-crl-agent3.cnf \
|
||||
-passin 'pass:password'
|
||||
openssl ca \
|
||||
-keyfile ca2-key.pem \
|
||||
-cert ca2-cert.pem \
|
||||
-config ca2-crl-agent3.cnf \
|
||||
-gencrl \
|
||||
-out ca2-crl-agent3.pem \
|
||||
-passin 'pass:password'
|
||||
@rm -f ca2-crl-agent3.cnf ca2-crl-agent3-database.txt*
|
||||
|
||||
#
|
||||
# agent5 is signed by ca2 (client cert)
|
||||
#
|
||||
@@ -1161,7 +1184,7 @@ irrelevant_san_correct_subject-key.pem:
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem
|
||||
|
||||
clean:
|
||||
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
|
||||
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-crl-agent3-database.txt* ca2-crl-agent3.cnf ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
|
||||
@> fake-startcom-root-database.txt
|
||||
|
||||
test: agent1-verify agent2-verify agent3-verify agent4-verify agent5-verify agent6-verify agent7-verify agent8-verify agent10-verify ec10-verify
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-----BEGIN X509 CRL-----
|
||||
MIIB/jCB5wIBATANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQGEwJVUzELMAkGA1UE
|
||||
CAwCQ0ExCzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05v
|
||||
ZGUuanMxDDAKBgNVBAMMA2NhMjEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vk
|
||||
cy5vcmcXDTI2MDQxNjA0MDM1MloYDzIwNTMwOTAxMDQwMzUyWjAnMCUCFHtnB1Iw
|
||||
05rTKjL+Xc+x+pXi6jGdFw0yNjA0MTYwNDAzNTJaoA4wDDAKBgNVHRQEAwIBATAN
|
||||
BgkqhkiG9w0BAQ0FAAOCAQEAS7PnQxPHv+VXvmCOcTQOYWns16+G5cmaY8/fYjwM
|
||||
6zOQPTItJTH+S2EJ3JvqES3Xm3KH+2Qh/8gAiiGNL9zdBpuNcJyUlJpIPuvWPd0P
|
||||
Bup7u2YEvc9NjuP8thslf267A8tieFf4mF+AO1lvFp+CGoyRSwtNGOWCMkFDGgGn
|
||||
ZOVXw5Q782PhUwThozGjR40zDkNjW/uFPJjMkz/RZFEmWshGf9t3VzahRs8PUApr
|
||||
XTdatufBUPrWiTWyQAuME50ajzq/tfuj2kokqfOvy1mkoNwtySVxKSlwGjejd5Xj
|
||||
yV/v4a5FDjXw4AwqEe+Cul9J2eyBb1jHkc+R9rutHTKEZA==
|
||||
-----END X509 CRL-----
|
||||
@@ -0,0 +1,48 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: validateAddress triggers Retry flow.
|
||||
// When the server endpoint has validateAddress: true, it should send
|
||||
// a Retry packet before accepting the connection. The handshake still
|
||||
// completes successfully.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect, QuicEndpoint } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const endpoint = new QuicEndpoint({ validateAddress: true });
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
const info = await serverSession.opened;
|
||||
// The handshake should complete despite the Retry flow.
|
||||
strictEqual(info.protocol, 'quic-test');
|
||||
serverSession.close();
|
||||
}), {
|
||||
endpoint,
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
servername: 'localhost',
|
||||
});
|
||||
|
||||
const info = await clientSession.opened;
|
||||
strictEqual(info.protocol, 'quic-test');
|
||||
|
||||
// The serverEndpoint must be closed after we wait for the clientSession to close.
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
@@ -4,6 +4,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { strictEqual, notStrictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
@@ -11,34 +14,33 @@ if (!hasQuic) {
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
|
||||
const cert = fixtures.readKey('agent1-cert.pem');
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
// Test h3 ALPN negotiation with Http3ApplicationImpl.
|
||||
// Both server and client use the default ALPN (h3).
|
||||
|
||||
const serverOpened = Promise.withResolvers();
|
||||
const clientOpened = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.opened.then(mustCall((info) => {
|
||||
assert.strictEqual(info.protocol, 'h3');
|
||||
serverOpened.resolve();
|
||||
serverSession.close();
|
||||
}));
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
const info = await serverSession.opened;
|
||||
strictEqual(info.protocol, 'h3');
|
||||
serverOpened.resolve();
|
||||
serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
});
|
||||
|
||||
assert.ok(serverEndpoint.address !== undefined);
|
||||
notStrictEqual(serverEndpoint.address, undefined);
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
servername: 'localhost',
|
||||
});
|
||||
clientSession.opened.then(mustCall((info) => {
|
||||
assert.strictEqual(info.protocol, 'h3');
|
||||
clientOpened.resolve();
|
||||
}));
|
||||
|
||||
await Promise.all([serverOpened.promise, clientOpened.promise]);
|
||||
async function checkClient() {
|
||||
const info = await clientSession.opened;
|
||||
strictEqual(info.protocol, 'h3');
|
||||
}
|
||||
|
||||
await Promise.all([serverOpened.promise, checkClient()]);
|
||||
clientSession.close();
|
||||
@@ -0,0 +1,50 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: ALPN mismatch causes connection failure.
|
||||
// The server offers 'quic-test' but the client requests 'nonexistent'.
|
||||
// The handshake should fail.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const onerror = mustCall((err) => {
|
||||
strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR');
|
||||
}, 2);
|
||||
const transportParams = { maxIdleTimeout: 1 };
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await rejects(serverSession.opened, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
});
|
||||
await rejects(serverSession.closed, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
});
|
||||
}), {
|
||||
transportParams,
|
||||
onerror,
|
||||
});
|
||||
|
||||
// Client requests an ALPN the server doesn't offer.
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'nonexistent-protocol',
|
||||
transportParams,
|
||||
onerror,
|
||||
});
|
||||
|
||||
await rejects(clientSession.opened, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
});
|
||||
|
||||
// The handshake should fail — opened may reject or never resolve.
|
||||
// The session should close with an error.
|
||||
await rejects(clientSession.closed, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
});
|
||||
@@ -4,6 +4,9 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { notStrictEqual, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
@@ -11,37 +14,34 @@ if (!hasQuic) {
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
|
||||
const cert = fixtures.readKey('agent1-cert.pem');
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
// Server offers multiple ALPNs. Client requests one that the server supports.
|
||||
// Verify the negotiated protocol matches on both sides.
|
||||
|
||||
const serverOpened = Promise.withResolvers();
|
||||
const clientOpened = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.opened.then(mustCall((info) => {
|
||||
// The server should negotiate proto-b (client's choice from server's list)
|
||||
assert.strictEqual(info.protocol, 'proto-b');
|
||||
serverOpened.resolve();
|
||||
serverSession.close();
|
||||
}));
|
||||
async function checkSession(session) {
|
||||
const info = await session.opened;
|
||||
// The client should negotiate proto-b (the only protocol it requested)
|
||||
strictEqual(info.protocol, 'proto-b');
|
||||
}
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await checkSession(serverSession);
|
||||
serverOpened.resolve();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['proto-a', 'proto-b', 'proto-c'],
|
||||
});
|
||||
|
||||
assert.ok(serverEndpoint.address !== undefined);
|
||||
notStrictEqual(serverEndpoint.address, undefined);
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'proto-b',
|
||||
servername: 'localhost',
|
||||
});
|
||||
clientSession.opened.then(mustCall((info) => {
|
||||
assert.strictEqual(info.protocol, 'proto-b');
|
||||
clientOpened.resolve();
|
||||
}));
|
||||
|
||||
await Promise.all([serverOpened.promise, clientOpened.promise]);
|
||||
clientSession.close();
|
||||
await Promise.all([serverOpened.promise, checkSession(clientSession)]);
|
||||
await clientSession.close();
|
||||
@@ -0,0 +1,45 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onblocked callback error handling.
|
||||
// A sync throw in stream.onblocked destroys the stream via
|
||||
// safeCallbackInvoke. The stream.closed promise rejects with the error.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('onblocked throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
// Small stream window to trigger flow control blocking.
|
||||
transportParams: {
|
||||
maxIdleTimeout: 1,
|
||||
initialMaxStreamDataBidiRemote: 256,
|
||||
},
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const stream = await clientSession.createBidirectionalStream();
|
||||
|
||||
stream.onblocked = mustCall(() => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
// Body larger than the 256-byte flow control window triggers onblocked.
|
||||
stream.setBody(new Uint8Array(4096));
|
||||
|
||||
// The stream's closed promise should reject with the error from the throw.
|
||||
await rejects(stream.closed, testError);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: async rejection in ondatagram destroys session.
|
||||
// safeCallbackInvoke detects the returned promise and attaches a
|
||||
// rejection handler that calls session.destroy(err). The error is
|
||||
// delivered to the onerror callback.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('async ondatagram rejection');
|
||||
const serverDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await assert.rejects(serverSession.closed, testError);
|
||||
serverDone.resolve();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall(async () => {
|
||||
throw testError;
|
||||
}),
|
||||
onerror: mustCall((err) => {
|
||||
assert.strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
await serverDone.promise;
|
||||
// The server session was destroyed abruptly (no CONNECTION_CLOSE sent).
|
||||
// The client may receive a stateless reset if it sends any packet
|
||||
// before its idle timeout fires, so closed may reject.
|
||||
await assert.rejects(clientSession.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR' });
|
||||
serverEndpoint.close();
|
||||
await serverEndpoint.closed;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: ondatagram callback error handling.
|
||||
// A sync throw in ondatagram destroys the session via safeCallbackInvoke.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('ondatagram throw');
|
||||
const serverDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
// The session is destroyed by the ondatagram throw. The closed promise
|
||||
// rejects with testError. Verify that and signal completion.
|
||||
await rejects(serverSession.closed, testError);
|
||||
serverDone.resolve();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send a datagram to trigger the server's ondatagram callback.
|
||||
await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
await serverDone.promise;
|
||||
// The server session was destroyed abruptly (no CONNECTION_CLOSE sent).
|
||||
// The client may receive a stateless reset if it sends any packet
|
||||
// before its idle timeout fires, so closed may reject.
|
||||
await rejects(clientSession.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR' });
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,40 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: ondatagramstatus callback error handling.
|
||||
// A sync throw in ondatagramstatus destroys the session via safeCallbackInvoke.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('ondatagramstatus throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
ondatagramstatus() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send a datagram. The status callback fires when the peer ACKs it.
|
||||
await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
// The session's closed should reject with the error from the throw.
|
||||
await rejects(clientSession.closed, testError);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onerror set via connect() options.
|
||||
// The onerror callback can be provided in the options object at
|
||||
// session creation time to avoid race conditions with errors that
|
||||
// occur during or immediately after the handshake.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const transportParams = { maxIdleTimeout: 1 };
|
||||
const testError = new Error('destroy with error');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams,
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
clientSession.destroy(testError);
|
||||
|
||||
await rejects(clientSession.closed, testError);
|
||||
@@ -0,0 +1,62 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onerror setter validation.
|
||||
// Setting onerror to a non-function (including null) throws.
|
||||
// Setting to undefined clears it. Setting to a function works.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { strictEqual, throws } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
const errorCheck = {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
};
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.opened;
|
||||
|
||||
// Session onerror validation: non-functions throw.
|
||||
throws(() => { serverSession.onerror = 'not a function'; }, errorCheck);
|
||||
throws(() => { serverSession.onerror = 42; }, errorCheck);
|
||||
throws(() => { serverSession.onerror = null; }, errorCheck);
|
||||
|
||||
// Setting to a function works.
|
||||
const fn = () => {};
|
||||
serverSession.onerror = fn;
|
||||
// The getter returns the bound version, not the original.
|
||||
strictEqual(typeof serverSession.onerror, 'function');
|
||||
|
||||
// Setting to undefined clears it.
|
||||
serverSession.onerror = undefined;
|
||||
strictEqual(serverSession.onerror, undefined);
|
||||
|
||||
serverSession.close();
|
||||
}));
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address);
|
||||
await clientSession.opened;
|
||||
|
||||
// Client-side stream onerror validation.
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: new TextEncoder().encode('x'),
|
||||
});
|
||||
|
||||
throws(() => { stream.onerror = 'not a function'; }, errorCheck);
|
||||
throws(() => { stream.onerror = 42; }, errorCheck);
|
||||
throws(() => { stream.onerror = null; }, errorCheck);
|
||||
|
||||
// Setting to a function works.
|
||||
stream.onerror = () => {};
|
||||
strictEqual(typeof stream.onerror, 'function');
|
||||
|
||||
// Setting to undefined clears it.
|
||||
stream.onerror = undefined;
|
||||
strictEqual(stream.onerror, undefined);
|
||||
|
||||
await clientSession.closed;
|
||||
@@ -0,0 +1,76 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onerror callback behavior
|
||||
// session.onerror fires when session is destroyed with error.
|
||||
// session.onerror receives the original error as argument.
|
||||
// session.closed rejects with the original error after onerror.
|
||||
// session.onerror not called when destroy() has no error.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import dc from 'node:diagnostics_channel';
|
||||
|
||||
const { ok, rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
// quic.session.error fires when a session is destroyed with an error.
|
||||
// It should fire once for the first client session (destroyed with error)
|
||||
// and not for the second (destroyed without error).
|
||||
dc.subscribe('quic.session.error', mustCall((msg) => {
|
||||
ok(msg.session, 'session.error should include session');
|
||||
ok(msg.error, 'session.error should include error');
|
||||
}));
|
||||
|
||||
const transportParams = { maxIdleTimeout: 1 };
|
||||
|
||||
// All tested using a single endpoint with two client sessions.
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}, 2), { transportParams });
|
||||
|
||||
// First client: destroy WITH error — onerror fires.
|
||||
{
|
||||
const testError = new Error('destroy with error');
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams,
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
let onerrorCalled = false;
|
||||
clientSession.onerror = mustCall((err) => {
|
||||
// Receives the original error.
|
||||
strictEqual(err, testError);
|
||||
onerrorCalled = true;
|
||||
});
|
||||
|
||||
clientSession.destroy(testError);
|
||||
|
||||
// Onerror was called synchronously during destroy.
|
||||
strictEqual(onerrorCalled, true);
|
||||
|
||||
// Closed rejects with the original error.
|
||||
await rejects(clientSession.closed, testError);
|
||||
}
|
||||
|
||||
// Second client: destroy WITHOUT error — onerror should NOT fire.
|
||||
{
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams,
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
clientSession.onerror = mustNotCall('onerror should not be called');
|
||||
|
||||
clientSession.destroy();
|
||||
|
||||
// Closed resolves (no error).
|
||||
await clientSession.closed;
|
||||
}
|
||||
|
||||
serverEndpoint.close();
|
||||
await serverEndpoint.closed;
|
||||
@@ -0,0 +1,36 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onhandshake callback error handling.
|
||||
// A sync throw in onhandshake destroys the session via safeCallbackInvoke.
|
||||
// The error is delivered to the onerror callback and the session's
|
||||
// closed promise rejects with the error.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('onhandshake throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams: { maxIdleTimeout: 1 } });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
onhandshake() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
assert.strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
|
||||
// The session's closed should reject with the error from the throw.
|
||||
await assert.rejects(clientSession.closed, testError);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,42 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onnewtoken callback error handling.
|
||||
// A sync throw in onnewtoken destroys the session via safeCallbackInvoke.
|
||||
// The server submits a NEW_TOKEN after handshake completes; the client
|
||||
// receives it via the onnewtoken callback. Since the session ticket and
|
||||
// NEW_TOKEN both arrive after the handshake, session.opened is already
|
||||
// resolved and there is no unhandled rejection / uncaught exception.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('onnewtoken throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams: { maxIdleTimeout: 1 } });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
onnewtoken() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
// The session's closed should reject with the error from the throw.
|
||||
await rejects(clientSession.closed, testError);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,53 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onpathvalidation callback error handling.
|
||||
// A sync throw in onpathvalidation destroys the session via
|
||||
// safeCallbackInvoke. The error is delivered to the onerror
|
||||
// callback and the session's closed promise rejects.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('onpathvalidation throw');
|
||||
|
||||
// The preferred endpoint never receives a new session — it only
|
||||
// routes PATH_CHALLENGE packets via SessionManager.
|
||||
const preferredEndpoint = await listen(mustNotCall(), {});
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
// The server session closes with a transport error when the
|
||||
// client is destroyed by the throw. That's expected.
|
||||
await rejects(serverSession.closed, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
});
|
||||
}), {
|
||||
transportParams: {
|
||||
preferredAddressIpv4: preferredEndpoint.address,
|
||||
},
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
reuseEndpoint: false,
|
||||
onpathvalidation() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
// The error from the throw should be delivered here.
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// The session's closed should reject with the thrown error.
|
||||
await rejects(clientSession.closed, testError);
|
||||
|
||||
await serverEndpoint.close();
|
||||
await preferredEndpoint.close();
|
||||
@@ -0,0 +1,66 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onreset callback error handling.
|
||||
// A sync throw in stream.onreset destroys the STREAM (not the session)
|
||||
// via safeCallbackInvoke. The stream.onerror fires with the original
|
||||
// error, and stream.closed rejects. The session remains alive.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const testError = new Error('onreset throw');
|
||||
|
||||
const serverReady = Promise.withResolvers();
|
||||
const serverStreamDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.onstream = mustCall(async (stream) => {
|
||||
// The stream's onerror should fire with the throw from onreset.
|
||||
stream.onerror = mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
});
|
||||
|
||||
stream.onreset = () => {
|
||||
throw testError;
|
||||
};
|
||||
|
||||
serverReady.resolve();
|
||||
|
||||
// Stream closed rejects because the onreset throw destroyed it.
|
||||
await rejects(stream.closed, testError);
|
||||
serverStreamDone.resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address);
|
||||
await clientSession.opened;
|
||||
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: encoder.encode('trigger onstream'),
|
||||
});
|
||||
|
||||
// Wait for the server to have the stream before resetting.
|
||||
await serverReady.promise;
|
||||
stream.resetStream(1n);
|
||||
|
||||
// Wait for the server stream to be destroyed by the onreset throw.
|
||||
await serverStreamDone.promise;
|
||||
|
||||
// The client stream was reset. Destroy it explicitly to clean up
|
||||
// (resetStream only shuts the write side; the read side is still open
|
||||
// waiting for the server which won't send anything now).
|
||||
stream.destroy();
|
||||
await stream.closed;
|
||||
|
||||
// Close both sides.
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,41 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: onsessionticket callback error handling.
|
||||
// A sync throw in onsessionticket destroys the session via safeCallbackInvoke.
|
||||
// Unlike onhandshake throws, no uncaughtException is produced because the
|
||||
// session ticket arrives after the handshake completes (session.opened is
|
||||
// already resolved so there is no unhandled rejection).
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('onsessionticket throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams: { maxIdleTimeout: 1 } });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
onsessionticket() {
|
||||
throw testError;
|
||||
},
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
}),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
// The session's closed should reject with the error from the throw.
|
||||
await rejects(clientSession.closed, testError);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,46 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: async rejection in onstream destroys session.
|
||||
// safeCallbackInvoke detects the returned promise and attaches a
|
||||
// rejection handler that calls session.destroy(err). The error is
|
||||
// delivered to the onerror callback.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { strictEqual, rejects } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const testError = new Error('async onstream rejection');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
serverSession.onerror = mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
});
|
||||
|
||||
serverSession.onstream = async () => {
|
||||
throw testError;
|
||||
};
|
||||
|
||||
// Session closed rejects with the error from the async rejection.
|
||||
await rejects(serverSession.closed, testError);
|
||||
}), { transportParams: { maxIdleTimeout: 1 } });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: new TextEncoder().encode('trigger onstream'),
|
||||
});
|
||||
|
||||
// The client session closes via CONNECTION_CLOSE or idle timeout
|
||||
// after the server session is destroyed by the async rejection.
|
||||
await Promise.all([stream.closed, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,49 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: callback error handling for onstream.
|
||||
// Sync throw in onstream destroys the session.
|
||||
// safeCallbackInvoke catches the throw and calls session.destroy(error).
|
||||
// The error is delivered to the onerror callback.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const testError = new Error('sync onstream throw');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
serverSession.onerror = mustCall((err) => {
|
||||
strictEqual(err, testError);
|
||||
});
|
||||
|
||||
serverSession.onstream = () => {
|
||||
throw testError;
|
||||
};
|
||||
|
||||
// The session's closed rejects with the error from destroy().
|
||||
await rejects(serverSession.closed, testError);
|
||||
}), { transportParams: { maxIdleTimeout: 1 } });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send data to trigger onstream on the server.
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: encoder.encode('trigger onstream'),
|
||||
});
|
||||
|
||||
// The client session will close via CONNECTION_CLOSE or idle timeout
|
||||
// after the server session is destroyed.
|
||||
await Promise.all([stream.closed, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,83 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: stream.onerror callback behavior.
|
||||
// * stream.onerror fires when stream is destroyed with error.
|
||||
// * stream.onerror receives the original error as argument.
|
||||
// * stream.closed rejects with the original error after onerror.
|
||||
// * stream.onerror not called when destroy() has no error.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const serverDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.onstream = mustCall(async (stream) => {
|
||||
stream.writer.endSync();
|
||||
await stream.closed;
|
||||
serverSession.close();
|
||||
serverDone.resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address);
|
||||
await clientSession.opened;
|
||||
|
||||
{
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: encoder.encode('will error'),
|
||||
});
|
||||
|
||||
const testError = new Error('stream destroy error');
|
||||
|
||||
let onerrorCalled = false;
|
||||
stream.onerror = mustCall((err) => {
|
||||
// Receives the original error.
|
||||
strictEqual(err, testError);
|
||||
onerrorCalled = true;
|
||||
});
|
||||
|
||||
stream.destroy(testError);
|
||||
|
||||
// The onerror was called synchronously during destroy.
|
||||
strictEqual(onerrorCalled, true);
|
||||
|
||||
// The stream.closed rejects with the original error.
|
||||
await rejects(stream.closed, testError);
|
||||
}
|
||||
|
||||
// The stream.onerror not called when destroy() has no error.
|
||||
// Create a stream with no body — use the writer API so the server sees
|
||||
// it and can close cleanly.
|
||||
{
|
||||
const stream = await clientSession.createBidirectionalStream();
|
||||
const w = stream.writer;
|
||||
|
||||
stream.onerror = mustNotCall('stream.onerror should not be called');
|
||||
|
||||
// Send data so the server's onstream fires, then end.
|
||||
w.writeSync('no error');
|
||||
w.endSync();
|
||||
|
||||
// Wait for the server to process and close its side.
|
||||
await serverDone.promise;
|
||||
|
||||
// Now destroy without error.
|
||||
stream.destroy();
|
||||
|
||||
// Closed should resolve (not reject).
|
||||
await stream.closed;
|
||||
}
|
||||
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,53 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: SuppressedError when async onerror rejects.
|
||||
// When session.onerror returns a Promise that rejects, a SuppressedError
|
||||
// wrapping both the rejection reason and the original error is thrown
|
||||
// via process.nextTick as an uncaught exception.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok, rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const originalError = new Error('original destroy error');
|
||||
const onerrorRejection = new Error('async onerror rejected');
|
||||
|
||||
const transportParams = { maxIdleTimeout: 1 };
|
||||
|
||||
// The SuppressedError is thrown via process.nextTick after the
|
||||
// onerror promise rejects, so it appears as an uncaught exception.
|
||||
process.on('uncaughtException', mustCall((err) => {
|
||||
ok(err instanceof SuppressedError);
|
||||
// .error is the onerror rejection reason
|
||||
strictEqual(err.error, onerrorRejection);
|
||||
// .suppressed is the original error that triggered destroy
|
||||
strictEqual(err.suppressed, originalError);
|
||||
}));
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams,
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Async onerror: returns a promise that rejects.
|
||||
clientSession.onerror = mustCall(async () => {
|
||||
throw onerrorRejection;
|
||||
});
|
||||
|
||||
clientSession.destroy(originalError);
|
||||
|
||||
// Closed rejects with the original error (not the SuppressedError).
|
||||
await rejects(clientSession.closed, originalError);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,52 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: SuppressedError when onerror throws.
|
||||
// If session.onerror throws synchronously, a SuppressedError
|
||||
// wrapping both the onerror error and the original error is
|
||||
// thrown via process.nextTick as an uncaught exception.
|
||||
// The SuppressedError's .error is the onerror failure and
|
||||
// .suppressed is the original error.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok, rejects, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const originalError = new Error('original destroy error');
|
||||
const onerrorError = new Error('onerror itself threw');
|
||||
|
||||
const transportParams = { maxIdleTimeout: 1 };
|
||||
|
||||
// The SuppressedError is thrown via process.nextTick, so it appears
|
||||
// as an uncaught exception.
|
||||
process.on('uncaughtException', mustCall((err) => {
|
||||
ok(err instanceof SuppressedError);
|
||||
// .error is the onerror failure
|
||||
strictEqual(err.error, onerrorError);
|
||||
// .suppressed is the original error that triggered destroy
|
||||
strictEqual(err.suppressed, originalError);
|
||||
}));
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), { transportParams });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams,
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
clientSession.onerror = mustCall(() => {
|
||||
throw onerrorError;
|
||||
});
|
||||
|
||||
clientSession.destroy(originalError);
|
||||
|
||||
// Closed rejects with the original error (not the SuppressedError).
|
||||
await rejects(clientSession.closed, originalError);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: congestion control algorithm selection.
|
||||
// Verify that each CC algorithm (reno, cubic, bbr) can be selected
|
||||
// and that a session completes a data transfer successfully with each.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
const { bytes } = await import('stream/iter');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const payload = encoder.encode('congestion control test');
|
||||
const payloadLength = payload.byteLength;
|
||||
|
||||
for (const cc of ['reno', 'cubic', 'bbr']) {
|
||||
const serverDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.onstream = mustCall(async (stream) => {
|
||||
const data = await bytes(stream);
|
||||
strictEqual(data.byteLength, payloadLength);
|
||||
stream.writer.endSync();
|
||||
await stream.closed;
|
||||
serverSession.close();
|
||||
serverDone.resolve();
|
||||
});
|
||||
}), { cc });
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, { cc });
|
||||
await clientSession.opened;
|
||||
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: encoder.encode('congestion control test'),
|
||||
});
|
||||
|
||||
for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars
|
||||
await Promise.all([stream.closed, serverDone.promise]);
|
||||
|
||||
// Verify the session stats show congestion control was active.
|
||||
ok(clientSession.stats.cwnd > 0n, `${cc}: cwnd should be > 0`);
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: concurrent connections from multiple clients.
|
||||
// Multiple clients connect to the same server simultaneously and each
|
||||
// exchanges data successfully.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
const { bytes } = await import('stream/iter');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const numClients = 5;
|
||||
let serverStreamCount = 0;
|
||||
const allDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall((serverSession) => {
|
||||
serverSession.onstream = mustCall(async (stream) => {
|
||||
// Echo back the data.
|
||||
const w = stream.writer;
|
||||
w.writeSync(await bytes(stream));
|
||||
w.endSync();
|
||||
await stream.closed;
|
||||
if (++serverStreamCount === numClients) {
|
||||
allDone.resolve();
|
||||
}
|
||||
});
|
||||
}, numClients));
|
||||
|
||||
// Connect all clients concurrently.
|
||||
const clientPromises = [];
|
||||
for (let i = 0; i < numClients; i++) {
|
||||
clientPromises.push((async () => {
|
||||
const cs = await connect(serverEndpoint.address, { reuseEndpoint: false });
|
||||
await cs.opened;
|
||||
const message = `client ${i}`;
|
||||
const stream = await cs.createBidirectionalStream({
|
||||
body: encoder.encode(message),
|
||||
});
|
||||
const received = await bytes(stream);
|
||||
strictEqual(new TextDecoder().decode(received), message);
|
||||
await stream.closed;
|
||||
cs.close();
|
||||
await cs.closed;
|
||||
})());
|
||||
}
|
||||
|
||||
await Promise.all([...clientPromises, allDone.promise]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,76 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: connection total limit enforcement.
|
||||
// maxConnectionsTotal limits total concurrent connections.
|
||||
// When the limit is exceeded, the server sends CONNECTION_REFUSED
|
||||
// and the client's session is destroyed with ERR_QUIC_TRANSPORT_ERROR.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { rejects, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect, QuicEndpoint } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
// Create endpoint with maxConnectionsTotal = 1.
|
||||
const endpoint = new QuicEndpoint({ maxConnectionsTotal: 1 });
|
||||
|
||||
// Verify the limits are readable and mutable.
|
||||
strictEqual(endpoint.maxConnectionsTotal, 1);
|
||||
strictEqual(endpoint.maxConnectionsPerHost, 0);
|
||||
endpoint.maxConnectionsPerHost = 100;
|
||||
strictEqual(endpoint.maxConnectionsPerHost, 100);
|
||||
endpoint.maxConnectionsPerHost = 0;
|
||||
|
||||
let sessionCount = 0;
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
sessionCount++;
|
||||
await Promise.all([serverSession.opened, serverSession.closed]);
|
||||
}), {
|
||||
endpoint,
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxIdleTimeout: 2 },
|
||||
});
|
||||
|
||||
// First connection should succeed.
|
||||
const cs1 = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxIdleTimeout: 2 },
|
||||
});
|
||||
await cs1.opened;
|
||||
|
||||
// Second connection — server rejects with CONNECTION_REFUSED.
|
||||
const cs2 = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxIdleTimeout: 1 },
|
||||
onerror: mustCall((err) => {
|
||||
strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR');
|
||||
}),
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
rejects(cs2.opened, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
}),
|
||||
rejects(cs2.closed, {
|
||||
code: 'ERR_QUIC_TRANSPORT_ERROR',
|
||||
}),
|
||||
]);
|
||||
|
||||
// Only 1 session should have been accepted by the server.
|
||||
strictEqual(sessionCount, 1);
|
||||
|
||||
await cs1.close();
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,64 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: datagram abandoned status for queue overflow.
|
||||
// When the datagram pending queue is full and a new datagram is sent,
|
||||
// the drop policy causes a datagram to be dropped. The dropped datagram
|
||||
// should be reported with status 'abandoned' (not 'lost'), indicating
|
||||
// it was never actually sent on the wire.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { notStrictEqual, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
let serverSession;
|
||||
|
||||
const serverEndpoint = await listen(mustCall((session) => {
|
||||
serverSession = session;
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
const ids = [0n, 0n, 0n];
|
||||
let abandoned = false;
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagramstatus: mustCallAtLeast((id, status) => {
|
||||
if (status === 'abandoned') {
|
||||
strictEqual(id, ids[0]);
|
||||
abandoned = true;
|
||||
}
|
||||
// We'll likely only get status for one other datagram.
|
||||
}),
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Set a very small queue so overflow happens immediately.
|
||||
clientSession.maxPendingDatagrams = 2;
|
||||
|
||||
// Send 3 datagrams with a queue size of 2. The first datagram should
|
||||
// be abandoned when the third is sent (drop-oldest policy is default).
|
||||
ids[0] = await clientSession.sendDatagram(new Uint8Array([1]));
|
||||
ids[1] = await clientSession.sendDatagram(new Uint8Array([2]));
|
||||
ids[2] = await clientSession.sendDatagram(new Uint8Array([3]));
|
||||
|
||||
notStrictEqual(ids[0], 0n);
|
||||
notStrictEqual(ids[1], 0n);
|
||||
notStrictEqual(ids[2], 0n);
|
||||
|
||||
// The abandoned status fires synchronously during sendDatagram when the
|
||||
// queue overflows. It should already be set
|
||||
strictEqual(abandoned, true);
|
||||
|
||||
await Promise.all([
|
||||
serverSession.close(),
|
||||
clientSession.close(),
|
||||
]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,82 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: datagram drop-newest policy.
|
||||
// With maxPendingDatagrams=2 and drop-newest, sending 5 datagrams
|
||||
// rapidly should reject the newest when the queue is full. The
|
||||
// server should receive the oldest datagrams.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const allReceived = Promise.withResolvers();
|
||||
const allStatusReceived = Promise.withResolvers();
|
||||
|
||||
let serverCounter = 0;
|
||||
let clientAbandonCounter = 0;
|
||||
let clientAckCounter = 0;
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await Promise.all([serverSession.opened, allStatusReceived.promise]);
|
||||
serverSession.close();
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall(function(data, early) {
|
||||
// We whould only receive datagrams 1 and 2
|
||||
strictEqual(data.length, 1);
|
||||
ok(data[0] === 0 || data[0] === 1);
|
||||
ok(!early);
|
||||
if (++serverCounter === 2) allReceived.resolve();
|
||||
}, 2),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
datagramDropPolicy: 'drop-newest',
|
||||
ondatagramstatus: mustCall((_, status) => {
|
||||
if (status === 'abandoned') {
|
||||
clientAbandonCounter++;
|
||||
} else if (status === 'acknowledged') {
|
||||
clientAckCounter++;
|
||||
}
|
||||
if (clientAbandonCounter + clientAckCounter === 5) {
|
||||
allStatusReceived.resolve();
|
||||
}
|
||||
}, 5),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
clientSession.maxPendingDatagrams = 2;
|
||||
|
||||
// Send 5 datagrams. With drop-newest, the 3rd/4th/5th are rejected
|
||||
// (the queue holds the 1st and 2nd).
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await clientSession.sendDatagram(new Uint8Array([i]));
|
||||
}
|
||||
|
||||
await Promise.all([allReceived.promise, allStatusReceived.promise]);
|
||||
|
||||
// 3 abandoned (datagrams 1, 2, 3) and 2 acknowledged (datagrams 4, 5).
|
||||
strictEqual(clientAbandonCounter, 3);
|
||||
strictEqual(clientAckCounter, 2);
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,83 @@
|
||||
// Flags: --experimental-quic
|
||||
|
||||
// Test: datagram drop-oldest policy.
|
||||
// With maxPendingDatagrams=2 and drop-oldest, sending 5 datagrams
|
||||
// rapidly should drop the oldest when the queue overflows. The
|
||||
// server should receive the most recent datagrams (4th and 5th).
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const allReceived = Promise.withResolvers();
|
||||
const allStatusReceived = Promise.withResolvers();
|
||||
|
||||
let serverCounter = 0;
|
||||
let clientAbandonCounter = 0;
|
||||
let clientAckCounter = 0;
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await Promise.all([serverSession.opened, allStatusReceived.promise]);
|
||||
await serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall(function(data, early) {
|
||||
// With drop-oldest, the queue keeps the newest. After 5 sends with
|
||||
// queue size 2, only datagrams 4 and 5 (values 3 and 4) remain.
|
||||
strictEqual(data.length, 1);
|
||||
ok(data[0] === 3 || data[0] === 4);
|
||||
ok(!early);
|
||||
if (++serverCounter === 2) allReceived.resolve();
|
||||
}, 2),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
datagramDropPolicy: 'drop-oldest',
|
||||
ondatagramstatus: mustCall((_, status) => {
|
||||
if (status === 'abandoned') {
|
||||
clientAbandonCounter++;
|
||||
} else if (status === 'acknowledged') {
|
||||
clientAckCounter++;
|
||||
}
|
||||
if (clientAbandonCounter + clientAckCounter === 5) {
|
||||
allStatusReceived.resolve();
|
||||
}
|
||||
}, 5),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
clientSession.maxPendingDatagrams = 2;
|
||||
|
||||
// Send 5 datagrams. With drop-oldest and queue size 2:
|
||||
// 1 queued, 2 queued, 3 arrives → 1 dropped, 4 arrives → 2 dropped,
|
||||
// 5 arrives → 3 dropped. Queue ends with [4, 5].
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await clientSession.sendDatagram(new Uint8Array([i]));
|
||||
}
|
||||
|
||||
await Promise.all([allReceived.promise, allStatusReceived.promise]);
|
||||
|
||||
// 3 abandoned (datagrams 1, 2, 3) and 2 acknowledged (datagrams 4, 5).
|
||||
strictEqual(clientAbandonCounter, 3);
|
||||
strictEqual(clientAckCounter, 2);
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,70 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: datagram server-to-client and echo round-trip
|
||||
// Server sends datagram, client receives via ondatagram.
|
||||
// Datagram echo — client sends, server echoes back in
|
||||
// its ondatagram callback (queued, flushed via SendPendingData).
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
const { setTimeout } = await import('node:timers/promises');
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
const clientGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await Promise.all([serverSession.opened, serverGot.promise]);
|
||||
// Give time for the echo to be sent before closing.
|
||||
await setTimeout(100);
|
||||
await serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 10 },
|
||||
// Server echoes received datagram data back to client.
|
||||
// The sendDatagram call happens inside ondatagram (ngtcp2 callback
|
||||
// scope). The datagram is queued and flushed by SendPendingData.
|
||||
ondatagram: mustCall((data, early, session) => {
|
||||
ok(data instanceof Uint8Array);
|
||||
ok(!early);
|
||||
session.sendDatagram(data);
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 10 },
|
||||
// Client receives datagram from server.
|
||||
ondatagram: mustCall(function(data) {
|
||||
ok(data instanceof Uint8Array);
|
||||
strictEqual(data.byteLength, 3);
|
||||
strictEqual(data[0], 10);
|
||||
strictEqual(data[1], 20);
|
||||
strictEqual(data[2], 30);
|
||||
clientGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
// Client sends datagram to trigger the echo.
|
||||
await clientSession.sendDatagram(new Uint8Array([10, 20, 30]));
|
||||
|
||||
await Promise.all([serverGot.promise, clientGot.promise, clientSession.closed]);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,93 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: datagram edge cases.
|
||||
// DGRAM-08 / DGIMP-08: Zero-length datagram returns 0n (not sent).
|
||||
// DGC-01 / DGIMP-09: maxDatagramFrameSize: 0 disables datagrams entirely.
|
||||
// Datagram arrives with no ondatagram callback — no crash.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
const { strictEqual, notStrictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
// --- DGRAM-08 / DGIMP-08: Zero-length datagram returns 0n ---
|
||||
{
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Zero-length ArrayBufferView
|
||||
const zeroId = await clientSession.sendDatagram(new Uint8Array(0));
|
||||
strictEqual(zeroId, 0n);
|
||||
|
||||
// Zero-length string
|
||||
const emptyStringId = await clientSession.sendDatagram('');
|
||||
strictEqual(emptyStringId, 0n);
|
||||
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGC-01 / DGIMP-09: maxDatagramFrameSize: 0 disables datagrams ---
|
||||
{
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
// Server advertises 0 — no datagrams accepted.
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 0 },
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 10 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// maxDatagramSize reflects the peer's (server's) transport param.
|
||||
strictEqual(clientSession.maxDatagramSize, 0);
|
||||
|
||||
// Sending returns 0n immediately — datagram not sent.
|
||||
const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
strictEqual(id, 0n);
|
||||
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGRAM-11: No ondatagram callback — no crash ---
|
||||
{
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
// No ondatagram set — datagrams arrive but are silently discarded.
|
||||
await serverSession.opened;
|
||||
// Give time for the datagram to arrive and be processed without crash.
|
||||
await setTimeout(200);
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send a datagram even though the server has no ondatagram handler.
|
||||
const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
notStrictEqual(id, 0n);
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: maxDatagramFrameSize transport param validation.
|
||||
// The maxDatagramFrameSize transport parameter must be a uint16
|
||||
// (0-65535). Values outside this range or of the wrong type should
|
||||
// be rejected.
|
||||
|
||||
import { hasQuic, skip, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { rejects } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
const sni = { '*': { keys: [key], certs: [cert] } };
|
||||
const alpn = ['quic-test'];
|
||||
|
||||
// Invalid values for maxDatagramFrameSize — must be rejected.
|
||||
const invalid = [
|
||||
-1,
|
||||
65536,
|
||||
1.5,
|
||||
'a',
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
{},
|
||||
[],
|
||||
() => {},
|
||||
];
|
||||
|
||||
for (const maxDatagramFrameSize of invalid) {
|
||||
const transportParams = { maxDatagramFrameSize };
|
||||
await rejects(
|
||||
listen(mustNotCall(), { sni, alpn, transportParams }),
|
||||
{ code: 'ERR_INVALID_ARG_VALUE' },
|
||||
`listen should reject maxDatagramFrameSize: ${maxDatagramFrameSize}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Valid values — should not throw.
|
||||
const valid = [0, 1, 100, 1200, 65535];
|
||||
|
||||
for (const maxDatagramFrameSize of valid) {
|
||||
const transportParams = { maxDatagramFrameSize };
|
||||
const ep = await listen(mustNotCall(), { sni, alpn, transportParams });
|
||||
ep.close();
|
||||
await ep.closed;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
|
||||
|
||||
// Test: multiple datagrams and datagrams alongside streams
|
||||
// Client sends multiple datagrams.
|
||||
// Datagrams sent alongside an active bidi stream.
|
||||
// Datagrams are unreliable — we verify at least some arrive.
|
||||
// The stream is opened first to establish bidirectional traffic,
|
||||
// keeping the congestion window healthy for datagram sends.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustCallAtLeast } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { ok } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
const { bytes } = await import('stream/iter');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const numDatagrams = 5;
|
||||
let serverDatagramCount = 0;
|
||||
const gotSomeDg = Promise.withResolvers();
|
||||
const streamDone = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.opened;
|
||||
|
||||
// Server receives stream data alongside datagrams.
|
||||
serverSession.onstream = mustCall(async (stream) => {
|
||||
stream.writer.endSync();
|
||||
await bytes(stream);
|
||||
await stream.closed;
|
||||
streamDone.resolve();
|
||||
});
|
||||
|
||||
await Promise.all([gotSomeDg.promise, streamDone.promise]);
|
||||
await serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCallAtLeast((data) => {
|
||||
ok(data instanceof Uint8Array);
|
||||
serverDatagramCount++;
|
||||
gotSomeDg.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
// Open a stream FIRST to establish bidirectional traffic.
|
||||
// This ensures ACKs flow back from the server, keeping the
|
||||
// congestion window open for subsequent datagram sends.
|
||||
const stream = await clientSession.createBidirectionalStream({
|
||||
body: new TextEncoder().encode('hello'),
|
||||
});
|
||||
|
||||
// Send multiple datagrams alongside the active stream.
|
||||
for (let i = 0; i < numDatagrams; i++) {
|
||||
await clientSession.sendDatagram(new Uint8Array([i]));
|
||||
}
|
||||
|
||||
// Complete the stream.
|
||||
for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars
|
||||
await stream.closed;
|
||||
|
||||
// At least some datagrams should have arrived.
|
||||
ok(serverDatagramCount > 0, 'Server should have received at least one datagram');
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,72 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// session.sendDatagram() must not detach the caller's underlying
|
||||
// ArrayBuffer. The bytes are copied into an internal buffer on every
|
||||
// send, so the caller's source ArrayBuffer remains live and may be
|
||||
// reused for further sends, mutated, or sliced into additional views.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { strictEqual, deepStrictEqual, notStrictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const received = [];
|
||||
const allReceived = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await allReceived.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
received.push(Buffer.from(data));
|
||||
if (received.length === 3) allReceived.resolve();
|
||||
}, 3),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// A full-buffer Uint8Array. The same source must be reusable across
|
||||
// multiple sendDatagram calls.
|
||||
const source = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
|
||||
const firstId = await clientSession.sendDatagram(source);
|
||||
notStrictEqual(firstId, 0n);
|
||||
strictEqual(source.buffer.detached, false,
|
||||
'source ArrayBuffer must not be detached after sendDatagram');
|
||||
|
||||
const secondId = await clientSession.sendDatagram(source);
|
||||
notStrictEqual(secondId, 0n);
|
||||
strictEqual(source.buffer.detached, false,
|
||||
'source ArrayBuffer must remain live after second sendDatagram');
|
||||
|
||||
// Mutating the source after the previous sendDatagram returned must
|
||||
// not affect what the peer ultimately receives — the bytes have
|
||||
// already been copied into the QUIC layer's internal buffer.
|
||||
source[0] = 99;
|
||||
const thirdId = await clientSession.sendDatagram(source);
|
||||
notStrictEqual(thirdId, 0n);
|
||||
|
||||
await allReceived.promise;
|
||||
|
||||
// Datagrams are unreliable and may be reordered, but the test fixture
|
||||
// does not exercise loss or reordering — assert by sorting.
|
||||
const sorted = received.map((b) => b.toString('hex')).sort();
|
||||
const expected = [
|
||||
Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]).toString('hex'),
|
||||
Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]).toString('hex'),
|
||||
Buffer.from([99, 2, 3, 4, 5, 6, 7, 8]).toString('hex'),
|
||||
].sort();
|
||||
deepStrictEqual(sorted, expected);
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,64 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: datagram size limit enforcement.
|
||||
// Datagram larger than maxDatagramSize returns 0n (not sent).
|
||||
// Datagram at exactly maxDatagramSize is accepted and delivered.
|
||||
// Same as DGRAM-03 via sendDatagram return value.
|
||||
// maxDatagramSize reflects the maximum datagram payload the peer can
|
||||
// receive, accounting for DATAGRAM frame overhead (type byte + varint
|
||||
// length encoding). It is derived from the peer's maxDatagramFrameSize
|
||||
// transport parameter minus the frame overhead.
|
||||
// We use maxDatagramFrameSize: 200 so that the exact-max datagram fits
|
||||
// comfortably within a QUIC packet (which has its own header + AEAD
|
||||
// overhead on top of the DATAGRAM frame).
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok, strictEqual, notStrictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
serverSession.close();
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
ok(data instanceof Uint8Array);
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const maxSize = clientSession.maxDatagramSize;
|
||||
|
||||
// maxDatagramSize should be less than maxDatagramFrameSize due to
|
||||
// the DATAGRAM frame overhead (1 byte type + varint length encoding).
|
||||
ok(maxSize > 0);
|
||||
ok(maxSize < 200);
|
||||
|
||||
// DGRAM-03 / DGIMP-10: Datagram too large — returns 0n.
|
||||
const oversized = new Uint8Array(maxSize + 1);
|
||||
const tooLargeId = await clientSession.sendDatagram(oversized);
|
||||
strictEqual(tooLargeId, 0n);
|
||||
|
||||
// Datagram at exactly maxDatagramSize — accepted and delivered.
|
||||
const exactMax = new Uint8Array(maxSize);
|
||||
exactMax[0] = 42;
|
||||
const exactId = await clientSession.sendDatagram(exactMax);
|
||||
notStrictEqual(exactId, 0n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,220 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: sendDatagram with various input source types.
|
||||
// String with custom encoding (e.g., 'hex').
|
||||
// Promise input — resolves then sends.
|
||||
// Promise input — session closes during await, returns 0n.
|
||||
// SharedArrayBuffer copies instead of transfers.
|
||||
// Pooled Buffer (partial view) copies correctly.
|
||||
// DataView input.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { deepStrictEqual, notStrictEqual, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
// --- DGIMP-01: String with custom encoding ---
|
||||
{
|
||||
const received = [];
|
||||
const allReceived = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await allReceived.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
received.push(Buffer.from(data));
|
||||
if (received.length === 2) allReceived.resolve();
|
||||
}, 2),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send hex-encoded string — '48656c6c6f' is 'Hello' in hex.
|
||||
const hexId = await clientSession.sendDatagram('48656c6c6f', 'hex');
|
||||
notStrictEqual(hexId, 0n);
|
||||
|
||||
// Send base64-encoded string — 'V29ybGQ=' is 'World' in base64.
|
||||
const b64Id = await clientSession.sendDatagram('V29ybGQ=', 'base64');
|
||||
notStrictEqual(b64Id, 0n);
|
||||
|
||||
await allReceived.promise;
|
||||
|
||||
deepStrictEqual(received[0], Buffer.from('Hello'));
|
||||
deepStrictEqual(received[1], Buffer.from('World'));
|
||||
|
||||
await clientSession.closed;
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGIMP-02: Promise input ---
|
||||
{
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
deepStrictEqual(Buffer.from(data), Buffer.from([42]));
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Send a Promise that resolves to a Uint8Array.
|
||||
const promiseId = await clientSession.sendDatagram(
|
||||
Promise.resolve(new Uint8Array([42])),
|
||||
);
|
||||
notStrictEqual(promiseId, 0n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGIMP-03: Promise input, session closes during await ---
|
||||
{
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}), {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
onerror() {},
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxIdleTimeout: 1, maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Create a promise that resolves after the session starts closing.
|
||||
// sendDatagram passes the initial checks, then awaits the promise.
|
||||
// While awaiting, the session closes. When the promise resolves,
|
||||
// sendDatagram finds the session closed and returns 0n.
|
||||
const slowPromise = new Promise((resolve) => {
|
||||
setImmediate(mustCall(async () => {
|
||||
await clientSession.close();
|
||||
resolve(new Uint8Array([1]));
|
||||
}));
|
||||
});
|
||||
|
||||
const id = await clientSession.sendDatagram(slowPromise);
|
||||
strictEqual(id, 0n);
|
||||
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGIMP-04: SharedArrayBuffer ---
|
||||
{
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
deepStrictEqual(Buffer.from(data), Buffer.from([10, 20, 30]));
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Create a SharedArrayBuffer-backed view.
|
||||
const sab = new SharedArrayBuffer(3);
|
||||
const view = new Uint8Array(sab);
|
||||
view[0] = 10;
|
||||
view[1] = 20;
|
||||
view[2] = 30;
|
||||
|
||||
const id = await clientSession.sendDatagram(view);
|
||||
notStrictEqual(id, 0n);
|
||||
|
||||
// The SharedArrayBuffer should still be usable (copied, not transferred).
|
||||
strictEqual(view[0], 10);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGIMP-05: Pooled Buffer (partial view) ---
|
||||
{
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
// The received data should match the slice content.
|
||||
deepStrictEqual(Buffer.from(data), Buffer.from('hello'));
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
// Buffer.from('hello') creates a pooled buffer — its backing
|
||||
// ArrayBuffer is larger and the view has a non-zero offset.
|
||||
const pooledBuf = Buffer.from('hello');
|
||||
const id = await clientSession.sendDatagram(pooledBuf);
|
||||
notStrictEqual(id, 0n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
|
||||
// --- DGIMP-06: DataView ---
|
||||
{
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
deepStrictEqual(Buffer.from(data), Buffer.from([0xCA, 0xFE]));
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const ab = new ArrayBuffer(4);
|
||||
const fullView = new Uint8Array(ab);
|
||||
fullView.set([0xDE, 0xAD, 0xCA, 0xFE]);
|
||||
|
||||
// DataView over bytes [2, 3] of the buffer.
|
||||
const dv = new DataView(ab, 2, 2);
|
||||
const id = await clientSession.sendDatagram(dv);
|
||||
notStrictEqual(id, 0n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: ondatagramstatus callback.
|
||||
// After sending a datagram, the ondatagramstatus callback fires
|
||||
// with the datagram ID and either 'acknowledged' or 'lost'.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
const { setTimeout } = await import('node:timers/promises');
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
const statusReceived = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await Promise.all([serverSession.opened, serverGot.promise]);
|
||||
// Give a moment for the ACK to propagate to the client so the
|
||||
// ondatagramstatus callback fires before the session closes.
|
||||
await setTimeout(100);
|
||||
await serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall(function() {
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
let statusId;
|
||||
let statusValue;
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagramstatus: mustCall((id, status) => {
|
||||
strictEqual(typeof id, 'bigint');
|
||||
strictEqual(typeof status, 'string');
|
||||
ok(
|
||||
status === 'acknowledged' || status === 'lost' || status === 'abandoned',
|
||||
`status should be 'acknowledged', 'lost', or 'abandoned', got '${status}'`,
|
||||
);
|
||||
|
||||
statusId = id;
|
||||
statusValue = status;
|
||||
statusReceived.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
// Wait for the server to receive and the status callback to fire.
|
||||
await Promise.all([serverGot.promise, statusReceived.promise]);
|
||||
|
||||
// The status callback should have been called with the same ID.
|
||||
strictEqual(statusId, id);
|
||||
// On localhost the datagram should be acknowledged, not lost.
|
||||
strictEqual(statusValue, 'acknowledged');
|
||||
|
||||
await clientSession.closed;
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,46 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: string datagram with multi-byte UTF-8 characters.
|
||||
// Verifies that sendDatagram with a string containing multi-byte UTF-8
|
||||
// characters (CJK, emoji, etc.) encodes correctly and the receiver
|
||||
// gets the exact bytes.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok, deepStrictEqual, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const message = '\u4f60\u597d\u4e16\u754c'; // "Hello World" in Chinese
|
||||
const expected = Buffer.from(message, 'utf8');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
ok(data instanceof Uint8Array);
|
||||
// Verify the received bytes match the UTF-8 encoding.
|
||||
deepStrictEqual(Buffer.from(data), expected);
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
const id = await clientSession.sendDatagram(message);
|
||||
strictEqual(id, 1n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,62 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: basic datagram send and receive.
|
||||
// Client sends datagram, server receives via ondatagram.
|
||||
// maxDatagramSize reflects peer's transport param.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
await serverSession.opened;
|
||||
// maxDatagramSize reflects peer's max payload (frame size
|
||||
// minus DATAGRAM frame overhead of type byte + varint length).
|
||||
ok(serverSession.maxDatagramSize > 0);
|
||||
ok(serverSession.maxDatagramSize < 1200);
|
||||
// Wait for the datagram before closing.
|
||||
await serverGot.promise;
|
||||
await serverSession.close();
|
||||
}), {
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram: mustCall((data) => {
|
||||
ok(data instanceof Uint8Array);
|
||||
strictEqual(data.byteLength, 3);
|
||||
serverGot.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
alpn: 'quic-test',
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
|
||||
await clientSession.opened;
|
||||
|
||||
// Client maxDatagramSize reflects actual payload max.
|
||||
ok(clientSession.maxDatagramSize > 0);
|
||||
ok(clientSession.maxDatagramSize < 1200);
|
||||
|
||||
// Client sends datagram.
|
||||
const id = await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
assert.strictEqual(id, 1n);
|
||||
|
||||
await Promise.all([serverGot.promise, clientSession.closed]);
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,55 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: default transport parameter limits are reasonable.
|
||||
// Verify that the default transport parameters have sane values:
|
||||
// not zero (which would prevent streams), and not excessively large
|
||||
// (which could waste resources).
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const { ok } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const serverEndpoint = await listen(mustCall(async (serverSession) => {
|
||||
const info = await serverSession.opened;
|
||||
|
||||
// The handshake info should be available.
|
||||
ok(info, 'handshake info should be available');
|
||||
|
||||
await serverSession.closed;
|
||||
}));
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
reuseEndpoint: false,
|
||||
});
|
||||
const info = await clientSession.opened;
|
||||
|
||||
// Verify the handshake completed and we can inspect the session.
|
||||
ok(info, 'handshake info should be available');
|
||||
|
||||
// Check that the session has reasonable default stream limits by
|
||||
// verifying we can create at least one bidirectional and one
|
||||
// unidirectional stream.
|
||||
const bidiStream = await clientSession.createBidirectionalStream();
|
||||
ok(bidiStream, 'should be able to create a bidi stream');
|
||||
bidiStream.destroy();
|
||||
|
||||
const uniStream = await clientSession.createUnidirectionalStream();
|
||||
ok(uniStream, 'should be able to create a uni stream');
|
||||
uniStream.destroy();
|
||||
|
||||
// Check the endpoint's maxConnectionsPerHost and maxConnectionsTotal
|
||||
// defaults are either 0 (unlimited) or a reasonable positive number.
|
||||
const maxPerHost = serverEndpoint.maxConnectionsPerHost;
|
||||
const maxTotal = serverEndpoint.maxConnectionsTotal;
|
||||
ok(maxPerHost >= 0, 'maxConnectionsPerHost should be non-negative');
|
||||
ok(maxTotal >= 0, 'maxConnectionsTotal should be non-negative');
|
||||
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,44 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: diagnostics_channel endpoint busy change.
|
||||
// quic.endpoint.busy.change fires on endpoint.busy toggle.
|
||||
|
||||
import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import dc from 'node:diagnostics_channel';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
const { readKey } = fixtures;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, QuicEndpoint } = await import('node:quic');
|
||||
const { createPrivateKey } = await import('node:crypto');
|
||||
|
||||
const key = createPrivateKey(readKey('agent1-key.pem'));
|
||||
const cert = readKey('agent1-cert.pem');
|
||||
|
||||
let busyChangeCount = 0;
|
||||
dc.subscribe('quic.endpoint.busy.change', mustCall((msg) => {
|
||||
busyChangeCount++;
|
||||
ok(msg.endpoint);
|
||||
strictEqual(typeof msg.busy, 'boolean');
|
||||
}, 2));
|
||||
|
||||
const endpoint = new QuicEndpoint();
|
||||
const serverEndpoint = await listen(mustNotCall(), {
|
||||
endpoint,
|
||||
sni: { '*': { keys: [key], certs: [cert] } },
|
||||
alpn: ['quic-test'],
|
||||
});
|
||||
|
||||
// Toggle busy on and off.
|
||||
endpoint.busy = true;
|
||||
endpoint.busy = false;
|
||||
|
||||
strictEqual(busyChangeCount, 2);
|
||||
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,48 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: diagnostics_channel datagram status event.
|
||||
// quic.session.receive.datagram.status fires with ack/lost status.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import dc from 'node:diagnostics_channel';
|
||||
|
||||
const { ok, strictEqual } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const statusDone = Promise.withResolvers();
|
||||
|
||||
// quic.session.receive.datagram.status fires with status.
|
||||
dc.subscribe('quic.session.receive.datagram.status', mustCall((msg) => {
|
||||
ok(msg.session);
|
||||
ok(msg.id);
|
||||
strictEqual(msg?.status, 'acknowledged');
|
||||
statusDone.resolve();
|
||||
}));
|
||||
|
||||
const serverEndpoint = await listen(async (serverSession) => {
|
||||
// Server stays alive until the client closes so the ACK
|
||||
// has time to propagate back to the client.
|
||||
await serverSession.closed;
|
||||
}, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram() {},
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagramstatus() {},
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
// Wait for the status event before closing.
|
||||
await statusDone.promise;
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
@@ -0,0 +1,52 @@
|
||||
// Flags: --experimental-quic --no-warnings
|
||||
|
||||
// Test: diagnostics_channel datagram events.
|
||||
// quic.session.receive.datagram fires on datagram receipt.
|
||||
// quic.session.send.datagram fires on datagram send.
|
||||
|
||||
import { hasQuic, skip, mustCall } from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import dc from 'node:diagnostics_channel';
|
||||
|
||||
const { ok } = assert;
|
||||
|
||||
if (!hasQuic) {
|
||||
skip('QUIC is not enabled');
|
||||
}
|
||||
|
||||
const { listen, connect } = await import('../common/quic.mjs');
|
||||
|
||||
const serverGot = Promise.withResolvers();
|
||||
|
||||
// quic.session.send.datagram fires on send.
|
||||
dc.subscribe('quic.session.send.datagram', mustCall((msg) => {
|
||||
ok(msg.session);
|
||||
ok(msg.id);
|
||||
ok(msg.length > 0);
|
||||
}));
|
||||
|
||||
// quic.session.receive.datagram fires on receipt.
|
||||
dc.subscribe('quic.session.receive.datagram', mustCall((msg) => {
|
||||
ok(msg.session);
|
||||
ok(msg.length > 0);
|
||||
}));
|
||||
|
||||
const serverEndpoint = await listen(async (serverSession) => {
|
||||
await serverSession.closed;
|
||||
}, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
ondatagram() {
|
||||
serverGot.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
const clientSession = await connect(serverEndpoint.address, {
|
||||
transportParams: { maxDatagramFrameSize: 1200 },
|
||||
});
|
||||
await clientSession.opened;
|
||||
|
||||
await clientSession.sendDatagram(new Uint8Array([1, 2, 3]));
|
||||
|
||||
await serverGot.promise;
|
||||
await clientSession.close();
|
||||
await serverEndpoint.close();
|
||||
Loaded 100 of 287 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user