src, quic: refine more of the quic implementation

Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/56328
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
James M Snell 2024-11-24 14:57:24 -08:00
parent 72537f5631
commit 062ae6f3cb
51 changed files with 6723 additions and 3515 deletions

View File

@ -966,6 +966,14 @@ If the ES module being `require()`'d contains top-level `await`, this flag
allows Node.js to evaluate the module, try to locate the
top-level awaits, and print their location to help users find them.
### `--experimental-quic`
<!--
added: REPLACEME
-->
Enables the experimental `node:quic` built-in module.
### `--experimental-require-module`
<!-- YAML
@ -3089,6 +3097,7 @@ one is included in the list below.
* `--experimental-loader`
* `--experimental-modules`
* `--experimental-print-required-tla`
* `--experimental-quic`
* `--experimental-require-module`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`

View File

@ -50,6 +50,7 @@
* [Process](process.md)
* [Punycode](punycode.md)
* [Query strings](querystring.md)
* [QUIC](quic.md)
* [Readline](readline.md)
* [REPL](repl.md)
* [Report](report.md)

1713
doc/api/quic.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -217,6 +217,9 @@ flag is no longer required as WASI is enabled by default.
.It Fl -experimental-wasm-modules
Enable experimental WebAssembly module support.
.
.It Fl -experimental-quic
Enable the experimental QUIC support.
.
.It Fl -force-context-aware
Disable loading native addons that are not context-aware.
.

View File

@ -71,8 +71,8 @@ const {
} = require('internal/validators');
const {
CountQueuingStrategy,
} = require('internal/webstreams/queuingstrategies');
setImmediate,
} = require('timers');
const { queueMicrotask } = require('internal/process/task_queues');
@ -315,80 +315,7 @@ class Blob {
stream() {
if (!isBlob(this))
throw new ERR_INVALID_THIS('Blob');
const reader = this[kHandle].getReader();
return new lazyReadableStream({
type: 'bytes',
start(c) {
// There really should only be one read at a time so using an
// array here is purely defensive.
this.pendingPulls = [];
},
pull(c) {
const { promise, resolve, reject } = PromiseWithResolvers();
this.pendingPulls.push({ resolve, reject });
const readNext = () => {
reader.pull((status, buffer) => {
// If pendingPulls is empty here, the stream had to have
// been canceled, and we don't really care about the result.
// We can simply exit.
if (this.pendingPulls.length === 0) {
return;
}
if (status === 0) {
// EOS
c.close();
// This is to signal the end for byob readers
// see https://streams.spec.whatwg.org/#example-rbs-pull
c.byobRequest?.respond(0);
const pending = this.pendingPulls.shift();
pending.resolve();
return;
} else if (status < 0) {
// The read could fail for many different reasons when reading
// from a non-memory resident blob part (e.g. file-backed blob).
// The error details the system error code.
const error = lazyDOMException('The blob could not be read', 'NotReadableError');
const pending = this.pendingPulls.shift();
c.error(error);
pending.reject(error);
return;
}
// ReadableByteStreamController.enqueue errors if we submit a 0-length
// buffer. We need to check for that here.
if (buffer !== undefined && buffer.byteLength !== 0) {
c.enqueue(new Uint8Array(buffer));
}
// We keep reading until we either reach EOS, some error, or we
// hit the flow rate of the stream (c.desiredSize).
queueMicrotask(() => {
if (c.desiredSize < 0) {
// A manual backpressure check.
if (this.pendingPulls.length !== 0) {
// A case of waiting pull finished (= not yet canceled)
const pending = this.pendingPulls.shift();
pending.resolve();
}
return;
}
readNext();
});
});
};
readNext();
return promise;
},
cancel(reason) {
// Reject any currently pending pulls here.
for (const pending of this.pendingPulls) {
pending.reject(reason);
}
this.pendingPulls = [];
},
// We set the highWaterMark to 0 because we do not want the stream to
// start reading immediately on creation. We want it to wait until read
// is called.
}, new CountQueuingStrategy({ highWaterMark: 0 }));
return createBlobReaderStream(this[kHandle].getReader());
}
}
@ -505,6 +432,84 @@ function arrayBuffer(blob) {
return promise;
}
function createBlobReaderStream(reader) {
return new lazyReadableStream({
type: 'bytes',
start(c) {
// There really should only be one read at a time so using an
// array here is purely defensive.
this.pendingPulls = [];
},
pull(c) {
const { promise, resolve, reject } = PromiseWithResolvers();
this.pendingPulls.push({ resolve, reject });
const readNext = () => {
reader.pull((status, buffer) => {
// If pendingPulls is empty here, the stream had to have
// been canceled, and we don't really care about the result.
// We can simply exit.
if (this.pendingPulls.length === 0) {
return;
}
if (status === 0) {
// EOS
c.close();
// This is to signal the end for byob readers
// see https://streams.spec.whatwg.org/#example-rbs-pull
c.byobRequest?.respond(0);
const pending = this.pendingPulls.shift();
pending.resolve();
return;
} else if (status < 0) {
// The read could fail for many different reasons when reading
// from a non-memory resident blob part (e.g. file-backed blob).
// The error details the system error code.
const error = lazyDOMException('The blob could not be read', 'NotReadableError');
const pending = this.pendingPulls.shift();
c.error(error);
pending.reject(error);
return;
}
// ReadableByteStreamController.enqueue errors if we submit a 0-length
// buffer. We need to check for that here.
if (buffer !== undefined && buffer.byteLength !== 0) {
c.enqueue(new Uint8Array(buffer));
}
// We keep reading until we either reach EOS, some error, or we
// hit the flow rate of the stream (c.desiredSize).
// We use set immediate here because we have to allow the event
// loop to turn in order to proecss any pending i/o. Using
// queueMicrotask won't allow the event loop to turn.
setImmediate(() => {
if (c.desiredSize < 0) {
// A manual backpressure check.
if (this.pendingPulls.length !== 0) {
// A case of waiting pull finished (= not yet canceled)
const pending = this.pendingPulls.shift();
pending.resolve();
}
return;
}
readNext();
});
});
};
readNext();
return promise;
},
cancel(reason) {
// Reject any currently pending pulls here.
for (const pending of this.pendingPulls) {
pending.reject(reason);
}
this.pendingPulls = [];
},
// We set the highWaterMark to 0 because we do not want the stream to
// start reading immediately on creation. We want it to wait until read
// is called.
}, { highWaterMark: 0 });
}
module.exports = {
Blob,
createBlob,
@ -513,4 +518,5 @@ module.exports = {
kHandle,
resolveObjectURL,
TransferableBlob,
createBlobReaderStream,
};

View File

@ -131,11 +131,12 @@ const legacyWrapperList = new SafeSet([
const schemelessBlockList = new SafeSet([
'sea',
'sqlite',
'quic',
'test',
'test/reporters',
]);
// Modules that will only be enabled at run time.
const experimentalModuleList = new SafeSet(['sqlite']);
const experimentalModuleList = new SafeSet(['sqlite', 'quic']);
// Set up process.binding() and process._linkedBinding().
{

View File

@ -101,6 +101,7 @@ function prepareExecution(options) {
setupNavigator();
setupWarningHandler();
setupSQLite();
setupQuic();
setupWebStorage();
setupWebsocket();
setupEventsource();
@ -311,6 +312,15 @@ function setupSQLite() {
BuiltinModule.allowRequireByUsers('sqlite');
}
function setupQuic() {
if (!getOptionValue('--experimental-quic')) {
return;
}
const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('quic');
}
function setupWebStorage() {
if (getEmbedderOptions().noBrowserGlobals ||
!getOptionValue('--experimental-webstorage')) {

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,6 @@ const {
codes: {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
@ -23,11 +22,14 @@ const {
} = require('util/types');
const { inspect } = require('internal/util/inspect');
const assert = require('internal/assert');
const {
kFinishClose,
kInspect,
kPrivateConstructor,
kWantsHeaders,
kWantsTrailers,
} = require('internal/quic/symbols');
// This file defines the helper objects for accessing state for
@ -47,7 +49,6 @@ const {
IDX_STATE_SESSION_GRACEFUL_CLOSE,
IDX_STATE_SESSION_SILENT_CLOSE,
IDX_STATE_SESSION_STATELESS_RESET,
IDX_STATE_SESSION_DESTROYED,
IDX_STATE_SESSION_HANDSHAKE_COMPLETED,
IDX_STATE_SESSION_HANDSHAKE_CONFIRMED,
IDX_STATE_SESSION_STREAM_OPEN_ALLOWED,
@ -63,13 +64,14 @@ const {
IDX_STATE_ENDPOINT_PENDING_CALLBACKS,
IDX_STATE_STREAM_ID,
IDX_STATE_STREAM_PENDING,
IDX_STATE_STREAM_FIN_SENT,
IDX_STATE_STREAM_FIN_RECEIVED,
IDX_STATE_STREAM_READ_ENDED,
IDX_STATE_STREAM_WRITE_ENDED,
IDX_STATE_STREAM_DESTROYED,
IDX_STATE_STREAM_PAUSED,
IDX_STATE_STREAM_RESET,
IDX_STATE_STREAM_HAS_OUTBOUND,
IDX_STATE_STREAM_HAS_READER,
IDX_STATE_STREAM_WANTS_BLOCK,
IDX_STATE_STREAM_WANTS_HEADERS,
@ -77,6 +79,41 @@ const {
IDX_STATE_STREAM_WANTS_TRAILERS,
} = 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_CLOSING !== undefined);
assert(IDX_STATE_SESSION_GRACEFUL_CLOSE !== undefined);
assert(IDX_STATE_SESSION_SILENT_CLOSE !== undefined);
assert(IDX_STATE_SESSION_STATELESS_RESET !== undefined);
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_WRAPPED !== undefined);
assert(IDX_STATE_SESSION_LAST_DATAGRAM_ID !== undefined);
assert(IDX_STATE_ENDPOINT_BOUND !== undefined);
assert(IDX_STATE_ENDPOINT_RECEIVING !== undefined);
assert(IDX_STATE_ENDPOINT_LISTENING !== undefined);
assert(IDX_STATE_ENDPOINT_CLOSING !== undefined);
assert(IDX_STATE_ENDPOINT_BUSY !== undefined);
assert(IDX_STATE_ENDPOINT_PENDING_CALLBACKS !== undefined);
assert(IDX_STATE_STREAM_ID !== undefined);
assert(IDX_STATE_STREAM_PENDING !== undefined);
assert(IDX_STATE_STREAM_FIN_SENT !== undefined);
assert(IDX_STATE_STREAM_FIN_RECEIVED !== undefined);
assert(IDX_STATE_STREAM_READ_ENDED !== undefined);
assert(IDX_STATE_STREAM_WRITE_ENDED !== undefined);
assert(IDX_STATE_STREAM_PAUSED !== undefined);
assert(IDX_STATE_STREAM_RESET !== undefined);
assert(IDX_STATE_STREAM_HAS_OUTBOUND !== undefined);
assert(IDX_STATE_STREAM_HAS_READER !== undefined);
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);
class QuicEndpointState {
/** @type {DataView} */
#handle;
@ -95,39 +132,33 @@ class QuicEndpointState {
this.#handle = new DataView(buffer);
}
#assertNotClosed() {
if (this.#handle.byteLength === 0) {
throw new ERR_INVALID_STATE('Endpoint is closed');
}
}
/** @type {boolean} */
get isBound() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BOUND);
}
/** @type {boolean} */
get isReceiving() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_RECEIVING);
}
/** @type {boolean} */
get isListening() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_LISTENING);
}
/** @type {boolean} */
get isClosing() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_CLOSING);
}
/** @type {boolean} */
get isBusy() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BUSY);
}
@ -138,7 +169,7 @@ class QuicEndpointState {
* @type {bigint}
*/
get pendingCallbacks() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS);
}
@ -208,123 +239,111 @@ class QuicSessionState {
this.#handle = new DataView(buffer);
}
#assertNotClosed() {
if (this.#handle.byteLength === 0) {
throw new ERR_INVALID_STATE('Session is closed');
}
}
/** @type {boolean} */
get hasPathValidationListener() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION);
}
/** @type {boolean} */
set hasPathValidationListener(val) {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0);
}
/** @type {boolean} */
get hasVersionNegotiationListener() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION);
}
/** @type {boolean} */
set hasVersionNegotiationListener(val) {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0);
}
/** @type {boolean} */
get hasDatagramListener() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM);
}
/** @type {boolean} */
set hasDatagramListener(val) {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0);
}
/** @type {boolean} */
get hasSessionTicketListener() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET);
}
/** @type {boolean} */
set hasSessionTicketListener(val) {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0);
}
/** @type {boolean} */
get isClosing() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_CLOSING);
}
/** @type {boolean} */
get isGracefulClose() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_GRACEFUL_CLOSE);
}
/** @type {boolean} */
get isSilentClose() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SILENT_CLOSE);
}
/** @type {boolean} */
get isStatelessReset() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET);
}
/** @type {boolean} */
get isDestroyed() {
this.#assertNotClosed();
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DESTROYED);
}
/** @type {boolean} */
get isHandshakeCompleted() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_COMPLETED);
}
/** @type {boolean} */
get isHandshakeConfirmed() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_CONFIRMED);
}
/** @type {boolean} */
get isStreamOpenAllowed() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED);
}
/** @type {boolean} */
get isPrioritySupported() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PRIORITY_SUPPORTED);
}
/** @type {boolean} */
get isWrapped() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED);
}
/** @type {bigint} */
get lastDatagramId() {
this.#assertNotClosed();
if (this.#handle.byteLength === 0) return undefined;
return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID);
}
@ -414,86 +433,109 @@ class QuicStreamState {
/** @type {bigint} */
get id() {
if (this.#handle.byteLength === 0) return undefined;
return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID);
}
/** @type {boolean} */
get pending() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PENDING);
}
/** @type {boolean} */
get finSent() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_SENT);
}
/** @type {boolean} */
get finReceived() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_RECEIVED);
}
/** @type {boolean} */
get readEnded() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_READ_ENDED);
}
/** @type {boolean} */
get writeEnded() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WRITE_ENDED);
}
/** @type {boolean} */
get destroyed() {
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_DESTROYED);
}
/** @type {boolean} */
get paused() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PAUSED);
}
/** @type {boolean} */
get reset() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RESET);
}
/** @type {boolean} */
get hasOutbound() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_OUTBOUND);
}
/** @type {boolean} */
get hasReader() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_READER);
}
/** @type {boolean} */
get wantsBlock() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK);
}
/** @type {boolean} */
set wantsBlock(val) {
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0);
}
/** @type {boolean} */
get wantsHeaders() {
get [kWantsHeaders]() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS);
}
/** @type {boolean} */
set wantsHeaders(val) {
set [kWantsHeaders](val) {
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0);
}
/** @type {boolean} */
get wantsReset() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET);
}
/** @type {boolean} */
set wantsReset(val) {
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0);
}
/** @type {boolean} */
get wantsTrailers() {
get [kWantsTrailers]() {
if (this.#handle.byteLength === 0) return undefined;
return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS);
}
/** @type {boolean} */
set wantsTrailers(val) {
set [kWantsTrailers](val) {
if (this.#handle.byteLength === 0) return;
DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0);
}
@ -506,18 +548,17 @@ class QuicStreamState {
return {
__proto__: null,
id: `${this.id}`,
pending: this.pending,
finSent: this.finSent,
finReceived: this.finReceived,
readEnded: this.readEnded,
writeEnded: this.writeEnded,
destroyed: this.destroyed,
paused: this.paused,
reset: this.reset,
hasOutbound: this.hasOutbound,
hasReader: this.hasReader,
wantsBlock: this.wantsBlock,
wantsHeaders: this.wantsHeaders,
wantsReset: this.wantsReset,
wantsTrailers: this.wantsTrailers,
};
}
@ -536,18 +577,17 @@ class QuicStreamState {
return `QuicStreamState ${inspect({
id: this.id,
pending: this.pending,
finSent: this.finSent,
finReceived: this.finReceived,
readEnded: this.readEnded,
writeEnded: this.writeEnded,
destroyed: this.destroyed,
paused: this.paused,
reset: this.reset,
hasOutbound: this.hasOutbound,
hasReader: this.hasReader,
wantsBlock: this.wantsBlock,
wantsHeaders: this.wantsHeaders,
wantsReset: this.wantsReset,
wantsTrailers: this.wantsTrailers,
}, opts)}`;
}

View File

@ -17,6 +17,7 @@ const {
} = require('internal/errors');
const { inspect } = require('internal/util/inspect');
const assert = require('internal/assert');
const {
kFinishClose,
@ -50,17 +51,14 @@ const {
IDX_STATS_SESSION_CREATED_AT,
IDX_STATS_SESSION_CLOSING_AT,
IDX_STATS_SESSION_DESTROYED_AT,
IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
IDX_STATS_SESSION_GRACEFUL_CLOSING_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,
IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT,
IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT,
IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT,
IDX_STATS_SESSION_BYTES_IN_FLIGHT,
IDX_STATS_SESSION_BLOCK_COUNT,
@ -76,9 +74,9 @@ const {
IDX_STATS_SESSION_DATAGRAMS_LOST,
IDX_STATS_STREAM_CREATED_AT,
IDX_STATS_STREAM_OPENED_AT,
IDX_STATS_STREAM_RECEIVED_AT,
IDX_STATS_STREAM_ACKED_AT,
IDX_STATS_STREAM_CLOSING_AT,
IDX_STATS_STREAM_DESTROYED_AT,
IDX_STATS_STREAM_BYTES_RECEIVED,
IDX_STATS_STREAM_BYTES_SENT,
@ -88,6 +86,54 @@ const {
IDX_STATS_STREAM_FINAL_SIZE,
} = internalBinding('quic');
assert(IDX_STATS_ENDPOINT_CREATED_AT !== undefined);
assert(IDX_STATS_ENDPOINT_DESTROYED_AT !== undefined);
assert(IDX_STATS_ENDPOINT_BYTES_RECEIVED !== undefined);
assert(IDX_STATS_ENDPOINT_BYTES_SENT !== undefined);
assert(IDX_STATS_ENDPOINT_PACKETS_RECEIVED !== undefined);
assert(IDX_STATS_ENDPOINT_PACKETS_SENT !== undefined);
assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS !== undefined);
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT !== undefined);
assert(IDX_STATS_ENDPOINT_RETRY_COUNT !== undefined);
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_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);
assert(IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT !== undefined);
assert(IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT !== undefined);
assert(IDX_STATS_SESSION_BYTES_IN_FLIGHT !== undefined);
assert(IDX_STATS_SESSION_BLOCK_COUNT !== undefined);
assert(IDX_STATS_SESSION_CWND !== undefined);
assert(IDX_STATS_SESSION_LATEST_RTT !== undefined);
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_DATAGRAMS_RECEIVED !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined);
assert(IDX_STATS_STREAM_CREATED_AT !== undefined);
assert(IDX_STATS_STREAM_OPENED_AT !== undefined);
assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined);
assert(IDX_STATS_STREAM_ACKED_AT !== undefined);
assert(IDX_STATS_STREAM_DESTROYED_AT !== undefined);
assert(IDX_STATS_STREAM_BYTES_RECEIVED !== undefined);
assert(IDX_STATS_STREAM_BYTES_SENT !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET_ACK !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET_RECV !== undefined);
assert(IDX_STATS_STREAM_FINAL_SIZE !== undefined);
class QuicEndpointStats {
/** @type {BigUint64Array} */
#handle;
@ -278,11 +324,6 @@ class QuicSessionStats {
return this.#handle[IDX_STATS_SESSION_CLOSING_AT];
}
/** @type {bigint} */
get destroyedAt() {
return this.#handle[IDX_STATS_SESSION_DESTROYED_AT];
}
/** @type {bigint} */
get handshakeCompletedAt() {
return this.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
@ -293,11 +334,6 @@ class QuicSessionStats {
return this.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
}
/** @type {bigint} */
get gracefulClosingAt() {
return this.#handle[IDX_STATS_SESSION_GRACEFUL_CLOSING_AT];
}
/** @type {bigint} */
get bytesReceived() {
return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED];
@ -328,11 +364,6 @@ class QuicSessionStats {
return this.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
}
/** @type {bigint} */
get lossRetransmitCount() {
return this.#handle[IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT];
}
/** @type {bigint} */
get maxBytesInFlights() {
return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
@ -420,7 +451,6 @@ class QuicSessionStats {
bidiOutStreamCount: `${this.bidiOutStreamCount}`,
uniInStreamCount: `${this.uniInStreamCount}`,
uniOutStreamCount: `${this.uniOutStreamCount}`,
lossRetransmitCount: `${this.lossRetransmitCount}`,
maxBytesInFlights: `${this.maxBytesInFlights}`,
bytesInFlight: `${this.bytesInFlight}`,
blockCount: `${this.blockCount}`,
@ -460,7 +490,6 @@ class QuicSessionStats {
bidiOutStreamCount: this.bidiOutStreamCount,
uniInStreamCount: this.uniInStreamCount,
uniOutStreamCount: this.uniOutStreamCount,
lossRetransmitCount: this.lossRetransmitCount,
maxBytesInFlights: this.maxBytesInFlights,
bytesInFlight: this.bytesInFlight,
blockCount: this.blockCount,
@ -522,6 +551,11 @@ class QuicStreamStats {
return this.#handle[IDX_STATS_STREAM_CREATED_AT];
}
/** @type {bigint} */
get openedAt() {
return this.#handle[IDX_STATS_STREAM_OPENED_AT];
}
/** @type {bigint} */
get receivedAt() {
return this.#handle[IDX_STATS_STREAM_RECEIVED_AT];
@ -532,11 +566,6 @@ class QuicStreamStats {
return this.#handle[IDX_STATS_STREAM_ACKED_AT];
}
/** @type {bigint} */
get closingAt() {
return this.#handle[IDX_STATS_STREAM_CLOSING_AT];
}
/** @type {bigint} */
get destroyedAt() {
return this.#handle[IDX_STATS_STREAM_DESTROYED_AT];
@ -583,9 +612,9 @@ class QuicStreamStats {
// We need to convert the values to strings because JSON does not
// support BigInts.
createdAt: `${this.createdAt}`,
openedAt: `${this.openedAt}`,
receivedAt: `${this.receivedAt}`,
ackedAt: `${this.ackedAt}`,
closingAt: `${this.closingAt}`,
destroyedAt: `${this.destroyedAt}`,
bytesReceived: `${this.bytesReceived}`,
bytesSent: `${this.bytesSent}`,
@ -608,9 +637,9 @@ class QuicStreamStats {
return `StreamStats ${inspect({
connected: this.isConnected,
createdAt: this.createdAt,
openedAt: this.openedAt,
receivedAt: this.receivedAt,
ackedAt: this.ackedAt,
closingAt: this.closingAt,
destroyedAt: this.destroyedAt,
bytesReceived: this.bytesReceived,
bytesSent: this.bytesSent,

View File

@ -16,45 +16,61 @@ const {
// Symbols used to hide various private properties and methods from the
// public API.
const kApplicationProvider = Symbol('kApplicationProvider');
const kBlocked = Symbol('kBlocked');
const kConnect = Symbol('kConnect');
const kDatagram = Symbol('kDatagram');
const kDatagramStatus = Symbol('kDatagramStatus');
const kError = Symbol('kError');
const kFinishClose = Symbol('kFinishClose');
const kHandshake = Symbol('kHandshake');
const kHeaders = Symbol('kHeaders');
const kOwner = Symbol('kOwner');
const kRemoveSession = Symbol('kRemoveSession');
const kListen = Symbol('kListen');
const kNewSession = Symbol('kNewSession');
const kRemoveStream = Symbol('kRemoveStream');
const kNewStream = Symbol('kNewStream');
const kOnHeaders = Symbol('kOnHeaders');
const kOnTrailers = Symbol('kOwnTrailers');
const kOwner = Symbol('kOwner');
const kPathValidation = Symbol('kPathValidation');
const kPrivateConstructor = Symbol('kPrivateConstructor');
const kRemoveSession = Symbol('kRemoveSession');
const kRemoveStream = Symbol('kRemoveStream');
const kReset = Symbol('kReset');
const kSendHeaders = Symbol('kSendHeaders');
const kSessionTicket = Symbol('kSessionTicket');
const kState = Symbol('kState');
const kTrailers = Symbol('kTrailers');
const kVersionNegotiation = Symbol('kVersionNegotiation');
const kPrivateConstructor = Symbol('kPrivateConstructor');
const kWantsHeaders = Symbol('kWantsHeaders');
const kWantsTrailers = Symbol('kWantsTrailers');
module.exports = {
kApplicationProvider,
kBlocked,
kConnect,
kDatagram,
kDatagramStatus,
kError,
kFinishClose,
kHandshake,
kHeaders,
kOwner,
kRemoveSession,
kNewSession,
kRemoveStream,
kNewStream,
kPathValidation,
kReset,
kSessionTicket,
kTrailers,
kVersionNegotiation,
kInspect,
kKeyObjectHandle,
kKeyObjectInner,
kListen,
kNewSession,
kNewStream,
kOnHeaders,
kOnTrailers,
kOwner,
kPathValidation,
kPrivateConstructor,
kRemoveSession,
kRemoveStream,
kReset,
kSendHeaders,
kSessionTicket,
kState,
kTrailers,
kVersionNegotiation,
kWantsHeaders,
kWantsTrailers,
};

32
lib/quic.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
const {
emitExperimentalWarning,
} = require('internal/util');
emitExperimentalWarning('quic');
const {
connect,
listen,
QuicEndpoint,
QuicSession,
QuicStream,
CC_ALGO_RENO,
CC_ALGO_CUBIC,
CC_ALGO_BBR,
DEFAULT_CIPHERS,
DEFAULT_GROUPS,
} = require('internal/quic/quic');
module.exports = {
connect,
listen,
QuicEndpoint,
QuicSession,
QuicStream,
CC_ALGO_RENO,
CC_ALGO_CUBIC,
CC_ALGO_BBR,
DEFAULT_CIPHERS,
DEFAULT_GROUPS,
};

View File

@ -138,6 +138,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
"internal/quic/quic", "internal/quic/symbols", "internal/quic/stats",
"internal/quic/state",
#endif // !NODE_OPENSSL_HAS_QUIC
"quic", // Experimental.
"sqlite", // Experimental.
"sys", // Deprecated.
"wasi", // Experimental.

View File

@ -93,17 +93,13 @@ bool NgHeader<T>::IsZeroLength(
}
template <typename T>
bool NgHeader<T>::IsZeroLength(
int32_t token,
NgHeader<T>::rcbuf_t* name,
NgHeader<T>::rcbuf_t* value) {
bool NgHeader<T>::IsZeroLength(int32_t token,
NgHeader<T>::rcbuf_t* name,
NgHeader<T>::rcbuf_t* value) {
if (NgHeader<T>::rcbufferpointer_t::IsZeroLength(value))
return true;
const char* header_name = T::ToHttpHeaderName(token);
return header_name != nullptr ||
NgHeader<T>::rcbufferpointer_t::IsZeroLength(name);
return NgHeader<T>::rcbufferpointer_t::IsZeroLength(name);
}
template <typename T>

View File

@ -436,6 +436,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::experimental_sqlite,
kAllowedInEnvvar,
true);
AddOption("--experimental-quic",
"experimental QUIC API",
&EnvironmentOptions::experimental_quic,
kAllowedInEnvvar);
AddOption("--experimental-webstorage",
"experimental Web Storage API",
&EnvironmentOptions::experimental_webstorage,

View File

@ -126,6 +126,7 @@ class EnvironmentOptions : public Options {
bool experimental_websocket = true;
bool experimental_sqlite = true;
bool experimental_webstorage = false;
bool experimental_quic = false;
std::string localstorage_file;
bool experimental_global_navigator = true;
bool experimental_global_web_crypto = true;

View File

@ -3,6 +3,7 @@
#include "application.h"
#include <async_wrap-inl.h>
#include <debug_utils-inl.h>
#include <nghttp3/nghttp3.h>
#include <ngtcp2/ngtcp2.h>
#include <node_bob.h>
#include <node_sockaddr-inl.h>
@ -30,15 +31,17 @@ namespace quic {
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
// 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.
return nghttp3_settings{
max_field_section_size,
static_cast<size_t>(qpack_max_dtable_capacity),
static_cast<size_t>(qpack_encoder_max_dtable_capacity),
static_cast<size_t>(qpack_blocked_streams),
enable_connect_protocol,
enable_datagrams,
.max_field_section_size = max_field_section_size,
.qpack_max_dtable_capacity =
static_cast<size_t>(qpack_max_dtable_capacity),
.qpack_encoder_max_dtable_capacity =
static_cast<size_t>(qpack_encoder_max_dtable_capacity),
.qpack_blocked_streams = static_cast<size_t>(qpack_blocked_streams),
.enable_connect_protocol = enable_connect_protocol,
.h3_datagram = enable_datagrams,
};
}
@ -66,29 +69,33 @@ std::string Session::Application_Options::ToString() const {
Maybe<Session::Application_Options> Session::Application_Options::From(
Environment* env, Local<Value> value) {
if (value.IsEmpty() || (!value->IsUndefined() && !value->IsObject())) {
if (value.IsEmpty()) [[unlikely]] {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Application_Options>();
}
Application_Options options;
auto& state = BindingData::Get(env);
if (value->IsUndefined()) {
return Just<Application_Options>(options);
}
auto params = value.As<Object>();
#define SET(name) \
SetOption<Session::Application_Options, \
&Session::Application_Options::name>( \
env, &options, params, state.name##_string())
if (!SET(max_header_pairs) || !SET(max_header_length) ||
!SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) ||
!SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams) ||
!SET(enable_connect_protocol) || !SET(enable_datagrams)) {
return Nothing<Application_Options>();
if (!value->IsUndefined()) {
if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Application_Options>();
}
auto params = value.As<Object>();
if (!SET(max_header_pairs) || !SET(max_header_length) ||
!SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) ||
!SET(qpack_encoder_max_dtable_capacity) ||
!SET(qpack_blocked_streams) || !SET(enable_connect_protocol) ||
!SET(enable_datagrams)) {
// The call to SetOption should have scheduled an exception to be thrown.
return Nothing<Application_Options>();
}
}
#undef SET
@ -100,12 +107,18 @@ Maybe<Session::Application_Options> Session::Application_Options::From(
std::string Session::Application::StreamData::ToString() const {
DebugIndentScope indent;
size_t total_bytes = 0;
for (size_t n = 0; n < count; n++) {
total_bytes += data[n].len;
}
auto prefix = indent.Prefix();
std::string res("{");
res += prefix + "count: " + std::to_string(count);
res += prefix + "remaining: " + std::to_string(remaining);
res += prefix + "id: " + std::to_string(id);
res += prefix + "fin: " + std::to_string(fin);
res += prefix + "total: " + std::to_string(total_bytes);
res += indent.Close();
return res;
}
@ -120,27 +133,23 @@ bool Session::Application::Start() {
return true;
}
void Session::Application::AcknowledgeStreamData(Stream* stream,
bool Session::Application::AcknowledgeStreamData(int64_t stream_id,
size_t datalen) {
Debug(session_,
"Application acknowledging stream %" PRIi64 " data: %zu",
stream->id(),
datalen);
DCHECK_NOT_NULL(stream);
stream->Acknowledge(datalen);
if (auto stream = session().FindStream(stream_id)) [[likely]] {
stream->Acknowledge(datalen);
return true;
}
return false;
}
void Session::Application::BlockStream(int64_t id) {
Debug(session_, "Application blocking stream %" PRIi64, id);
auto stream = session().FindStream(id);
if (stream) stream->EmitBlocked();
// 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.
Debug(session_, "Application cannot add header");
return false;
}
@ -149,19 +158,16 @@ bool Session::Application::SendHeaders(const Stream& stream,
const v8::Local<v8::Array>& headers,
HeadersFlags flags) {
// By default do nothing.
Debug(session_, "Application cannot send headers");
return false;
}
void Session::Application::ResumeStream(int64_t id) {
Debug(session_, "Application resuming stream %" PRIi64, id);
// By default do nothing.
}
void Session::Application::ExtendMaxStreams(EndpointLabel label,
Direction direction,
uint64_t max_streams) {
Debug(session_, "Application extending max streams");
// By default do nothing.
}
@ -173,7 +179,6 @@ void Session::Application::ExtendMaxStreamData(Stream* stream,
void Session::Application::CollectSessionTicketAppData(
SessionTicket::AppData* app_data) const {
Debug(session_, "Application collecting session ticket app data");
// By default do nothing.
}
@ -181,7 +186,6 @@ SessionTicket::AppData::Status
Session::Application::ExtractSessionTicketAppData(
const SessionTicket::AppData& app_data,
SessionTicket::AppData::Source::Flag flag) {
Debug(session_, "Application extracting session ticket app data");
// By default we do not have any application data to retrieve.
return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW
? SessionTicket::AppData::Status::TICKET_USE_RENEW
@ -191,8 +195,6 @@ Session::Application::ExtractSessionTicketAppData(
void Session::Application::SetStreamPriority(const Stream& stream,
StreamPriority priority,
StreamPriorityFlags flags) {
Debug(
session_, "Application setting stream %" PRIi64 " priority", stream.id());
// By default do nothing.
}
@ -200,68 +202,73 @@ StreamPriority Session::Application::GetStreamPriority(const Stream& stream) {
return StreamPriority::DEFAULT;
}
Packet* Session::Application::CreateStreamDataPacket() {
BaseObjectPtr<Packet> Session::Application::CreateStreamDataPacket() {
return Packet::Create(env(),
session_->endpoint_.get(),
session_->remote_address_,
session_->endpoint(),
session_->remote_address(),
session_->max_packet_size(),
"stream data");
}
void Session::Application::StreamClose(Stream* stream, QuicError error) {
Debug(session_,
"Application closing stream %" PRIi64 " with error %s",
stream->id(),
error);
stream->Destroy(error);
void Session::Application::StreamClose(Stream* stream, QuicError&& error) {
DCHECK_NOT_NULL(stream);
stream->Destroy(std::move(error));
}
void Session::Application::StreamStopSending(Stream* stream, QuicError error) {
Debug(session_,
"Application stopping sending on stream %" PRIi64 " with error %s",
stream->id(),
error);
void Session::Application::StreamStopSending(Stream* stream,
QuicError&& error) {
DCHECK_NOT_NULL(stream);
stream->ReceiveStopSending(error);
stream->ReceiveStopSending(std::move(error));
}
void Session::Application::StreamReset(Stream* stream,
uint64_t final_size,
QuicError error) {
Debug(session_,
"Application resetting stream %" PRIi64 " with error %s",
stream->id(),
error);
stream->ReceiveStreamReset(final_size, error);
QuicError&& error) {
stream->ReceiveStreamReset(final_size, std::move(error));
}
void Session::Application::SendPendingData() {
DCHECK(!session().is_destroyed());
if (!session().can_send_packets()) [[unlikely]] {
return;
}
static constexpr size_t kMaxPackets = 32;
Debug(session_, "Application sending pending data");
PathStorage path;
StreamData stream_data;
auto update_stats = OnScopeLeave([&] {
auto& s = session();
if (!s.is_destroyed()) [[likely]] {
s.UpdatePacketTxTime();
s.UpdateTimer();
s.UpdateDataStats();
}
});
// The maximum size of packet to create.
const size_t max_packet_size = session_->max_packet_size();
// The maximum number of packets to send in this call to SendPendingData.
const size_t max_packet_count = std::min(
kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size);
if (max_packet_count == 0) return;
// The number of packets that have been sent in this call to SendPendingData.
size_t packet_send_count = 0;
Packet* packet = nullptr;
BaseObjectPtr<Packet> packet;
uint8_t* pos = nullptr;
uint8_t* begin = nullptr;
auto ensure_packet = [&] {
if (packet == nullptr) {
if (!packet) {
packet = CreateStreamDataPacket();
if (packet == nullptr) return false;
if (!packet) [[unlikely]]
return false;
pos = begin = ngtcp2_vec(*packet).base;
}
DCHECK_NOT_NULL(packet);
DCHECK(packet);
DCHECK_NOT_NULL(pos);
DCHECK_NOT_NULL(begin);
return true;
@ -274,29 +281,43 @@ void Session::Application::SendPendingData() {
ssize_t ndatalen = 0;
// Make sure we have a packet to write data into.
if (!ensure_packet()) {
if (!ensure_packet()) [[unlikely]] {
Debug(session_, "Failed to create packet for stream data");
// Doh! Could not create a packet. Time to bail.
session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
return session_->Close(Session::CloseMethod::SILENT);
}
// The stream_data is the next block of data from the application stream.
if (GetStreamData(&stream_data) < 0) {
Debug(session_, "Application failed to get stream data");
session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
packet->Done(UV_ECANCELED);
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
return session_->Close(Session::CloseMethod::SILENT);
}
// If we got here, we were at least successful in checking for stream data.
// There might not be any stream data to send.
Debug(session_, "Application using stream data: %s", stream_data);
if (stream_data.id >= 0) {
Debug(session_, "Application using stream data: %s", stream_data);
}
// Awesome, let's write our packet!
ssize_t nwrite =
WriteVStream(&path, pos, &ndatalen, max_packet_size, stream_data);
Debug(session_, "Application accepted %zu bytes into packet", ndatalen);
if (ndatalen > 0) {
Debug(session_,
"Application accepted %zu bytes from stream %" PRIi64
" into packet",
ndatalen,
stream_data.id);
} else if (stream_data.id >= 0) {
Debug(session_,
"Application did not accept any bytes from stream %" PRIi64
" into packet",
stream_data.id);
}
// A negative nwrite value indicates either an error or that there is more
// data to write into the packet.
@ -309,7 +330,9 @@ void Session::Application::SendPendingData() {
// ndatalen = -1 means that no stream data was accepted into the
// packet, which is what we want here.
DCHECK_EQ(ndatalen, -1);
DCHECK(stream_data.stream);
// We should only have received this error if there was an actual
// stream identified in the stream data, but let's double check.
DCHECK_GE(stream_data.id, 0);
session_->StreamDataBlocked(stream_data.id);
continue;
}
@ -318,22 +341,26 @@ void Session::Application::SendPendingData() {
// locally or the stream is being reset. In either case, we can't send
// any stream data!
Debug(session_,
"Stream %" PRIi64 " should be closed for writing",
"Closing stream %" PRIi64 " for writing",
stream_data.id);
// ndatalen = -1 means that no stream data was accepted into the
// packet, which is what we want here.
DCHECK_EQ(ndatalen, -1);
DCHECK(stream_data.stream);
stream_data.stream->EndWritable();
// We should only have received this error if there was an actual
// stream identified in the stream data, but let's double check.
DCHECK_GE(stream_data.id, 0);
if (stream_data.stream) [[likely]] {
stream_data.stream->EndWritable();
}
continue;
}
case NGTCP2_ERR_WRITE_MORE: {
// This return value indicates that we should call into WriteVStream
// again to write more data into the same packet.
Debug(session_, "Application should write more to packet");
DCHECK_GE(ndatalen, 0);
if (!StreamCommit(&stream_data, ndatalen)) {
if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) {
Debug(session_,
"Failed to commit stream data while writing packets");
packet->Done(UV_ECANCELED);
session_->SetLastError(
QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
return session_->Close(CloseMethod::SILENT);
}
continue;
@ -345,39 +372,33 @@ void Session::Application::SendPendingData() {
Debug(session_,
"Application encountered error while writing packet: %s",
ngtcp2_strerror(nwrite));
session_->SetLastError(QuicError::ForNgtcp2Error(nwrite));
packet->Done(UV_ECANCELED);
session_->SetLastError(QuicError::ForNgtcp2Error(nwrite));
return session_->Close(Session::CloseMethod::SILENT);
} else if (ndatalen >= 0) {
// We wrote some data into the packet. We need to update the flow control
// by committing the data.
if (!StreamCommit(&stream_data, ndatalen)) {
packet->Done(UV_ECANCELED);
return session_->Close(CloseMethod::SILENT);
}
} else if (ndatalen >= 0 && !StreamCommit(&stream_data, ndatalen)) {
packet->Done(UV_ECANCELED);
session_->SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL));
return session_->Close(CloseMethod::SILENT);
}
// When nwrite is zero, it means we are congestion limited.
// We should stop trying to send additional packets.
// 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) {
Debug(session_, "Congestion limited.");
// 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_, "Packet has %zu bytes to send", datalen);
// At least some data had been written into the packet. We should send
// it.
Debug(session_, "Sending packet with %zu bytes", datalen);
packet->Truncate(datalen);
session_->Send(packet, path);
} else {
packet->Done(UV_ECANCELED);
}
// If there was stream data selected, we should reschedule it to try
// sending again.
if (stream_data.id >= 0) ResumeStream(stream_data.id);
return session_->UpdatePacketTxTime();
return;
}
// At this point we have a packet prepared to send.
@ -389,11 +410,11 @@ void Session::Application::SendPendingData() {
// If we have sent the maximum number of packets, we're done.
if (++packet_send_count == max_packet_count) {
return session_->UpdatePacketTxTime();
return;
}
// Prepare to loop back around to prepare a new packet.
packet = nullptr;
packet.reset();
pos = begin = nullptr;
}
}
@ -406,16 +427,15 @@ ssize_t Session::Application::WriteVStream(PathStorage* path,
DCHECK_LE(stream_data.count, kMaxVectorCount);
uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
ngtcp2_pkt_info pi;
return ngtcp2_conn_writev_stream(*session_,
&path->path,
&pi,
nullptr,
dest,
max_packet_size,
ndatalen,
flags,
stream_data.id,
stream_data.buf,
stream_data,
stream_data.count,
uv_hrtime());
}
@ -429,17 +449,44 @@ class DefaultApplication final : public Session::Application {
// of the namespace.
using Application::Application; // NOLINT
bool ReceiveStreamData(Stream* stream,
bool ReceiveStreamData(int64_t stream_id,
const uint8_t* data,
size_t datalen,
Stream::ReceiveDataFlags flags) override {
Debug(&session(), "Default application receiving stream data");
DCHECK_NOT_NULL(stream);
if (!stream->is_destroyed()) stream->ReceiveData(data, datalen, flags);
const Stream::ReceiveDataFlags& flags,
void* stream_user_data) override {
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");
return false;
}
} else {
stream = BaseObjectPtr<Stream>(Stream::From(stream_user_data));
if (!stream) {
Debug(&session(),
"Default application failed to get existing stream "
"from user data");
return false;
}
}
CHECK(stream);
// Now we can actually receive the data! Woo!
stream->ReceiveData(data, datalen, flags);
return true;
}
int GetStreamData(StreamData* stream_data) override {
// Reset the state of stream_data before proceeding...
stream_data->id = -1;
stream_data->count = 0;
stream_data->fin = 0;
stream_data->stream.reset();
stream_data->remaining = 0;
Debug(&session(), "Default application getting stream data");
DCHECK_NOT_NULL(stream_data);
// If the queue is empty, there aren't any streams with data yet
@ -467,6 +514,17 @@ class DefaultApplication final : public Session::Application {
stream_data->fin = 1;
}
// It is possible that the data pointers returned are not actually
// the data pointers in the stream_data. If that's the case, we need
// to copy over the pointers.
count = std::min(count, kMaxVectorCount);
ngtcp2_vec* dest = *stream_data;
if (dest != data) {
for (size_t n = 0; n < count; n++) {
dest[n] = data[n];
}
}
stream_data->count = count;
if (count > 0) {
@ -496,45 +554,28 @@ class DefaultApplication final : public Session::Application {
return 0;
}
void ResumeStream(int64_t id) override {
Debug(&session(), "Default application resuming stream %" PRIi64, id);
ScheduleStream(id);
}
void ResumeStream(int64_t id) override { ScheduleStream(id); }
bool ShouldSetFin(const StreamData& stream_data) override {
auto const is_empty = [](auto vec, size_t cnt) {
size_t i;
for (i = 0; i < cnt && vec[i].len == 0; ++i) {
}
return i == cnt;
auto const is_empty = [](const ngtcp2_vec* vec, size_t cnt) {
size_t i = 0;
for (size_t n = 0; n < cnt; n++) i += vec[n].len;
return i > 0;
};
return stream_data.stream && is_empty(stream_data.buf, stream_data.count);
return stream_data.stream && is_empty(stream_data, stream_data.count);
}
void BlockStream(int64_t id) override {
if (auto stream = session().FindStream(id)) [[likely]] {
stream->EmitBlocked();
}
}
bool StreamCommit(StreamData* stream_data, size_t datalen) override {
Debug(&session(), "Default application committing stream data");
if (datalen == 0) return true;
DCHECK_NOT_NULL(stream_data);
const auto consume = [](ngtcp2_vec** pvec, size_t* pcnt, size_t len) {
ngtcp2_vec* v = *pvec;
size_t cnt = *pcnt;
for (; cnt > 0; --cnt, ++v) {
if (v->len > len) {
v->len -= len;
v->base += len;
break;
}
len -= v->len;
}
*pvec = v;
*pcnt = cnt;
};
CHECK(stream_data->stream);
stream_data->remaining -= datalen;
consume(&stream_data->buf, &stream_data->count, datalen);
stream_data->stream->Commit(datalen);
return true;
}
@ -545,34 +586,28 @@ class DefaultApplication final : public Session::Application {
private:
void ScheduleStream(int64_t id) {
Debug(&session(), "Default application scheduling stream %" PRIi64, id);
auto stream = session().FindStream(id);
if (stream && !stream->is_destroyed()) {
if (auto stream = session().FindStream(id)) [[likely]] {
stream->Schedule(&stream_queue_);
}
}
void UnscheduleStream(int64_t id) {
Debug(&session(), "Default application unscheduling stream %" PRIi64, id);
auto stream = session().FindStream(id);
if (stream && !stream->is_destroyed()) stream->Unschedule();
if (auto stream = session().FindStream(id)) [[likely]] {
stream->Unschedule();
}
}
Stream::Queue stream_queue_;
};
std::unique_ptr<Session::Application> Session::select_application() {
// In the future, we may end up supporting additional QUIC protocols. As they
// are added, extend the cases here to create and return them.
if (config_.options.tls_options.alpn == NGHTTP3_ALPN_H3) {
Debug(this, "Selecting HTTP/3 application");
return createHttp3Application(this, config_.options.application_options);
std::unique_ptr<Session::Application> Session::SelectApplication(
Session* session, const Session::Config& config) {
if (config.options.application_provider) {
return config.options.application_provider->Create(session);
}
Debug(this, "Selecting default application");
return std::make_unique<DefaultApplication>(
this, config_.options.application_options);
session, Session::Application_Options::kDefault);
}
} // namespace quic

View File

@ -1,9 +1,9 @@
#pragma once
#include "quic/defs.h"
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "base_object.h"
#include "bindingdata.h"
#include "defs.h"
#include "session.h"
@ -27,14 +27,15 @@ class Session::Application : public MemoryRetainer {
// 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(Stream* stream,
virtual bool ReceiveStreamData(int64_t stream_id,
const uint8_t* data,
size_t datalen,
Stream::ReceiveDataFlags flags) = 0;
const Stream::ReceiveDataFlags& flags,
void* stream_user_data) = 0;
// Session will forward all data acknowledgements for a stream to the
// Application.
virtual void AcknowledgeStreamData(Stream* stream, size_t datalen);
virtual bool AcknowledgeStreamData(int64_t stream_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.
@ -78,15 +79,16 @@ class Session::Application : public MemoryRetainer {
SessionTicket::AppData::Source::Flag flag);
// Notifies the Application that the identified stream has been closed.
virtual void StreamClose(Stream* stream, QuicError error = QuicError());
virtual void StreamClose(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&& error = QuicError());
// Notifies the Application that the identified stream should stop sending.
virtual void StreamStopSending(Stream* stream, QuicError error);
virtual void StreamStopSending(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
@ -124,7 +126,7 @@ class Session::Application : public MemoryRetainer {
inline const Session& session() const { return *session_; }
private:
Packet* CreateStreamDataPacket();
BaseObjectPtr<Packet> CreateStreamDataPacket();
// Write the given stream_data into the buffer.
ssize_t WriteVStream(PathStorage* path,
@ -145,10 +147,14 @@ struct Session::Application::StreamData final {
int64_t id = -1;
int fin = 0;
ngtcp2_vec data[kMaxVectorCount]{};
ngtcp2_vec* buf = data;
BaseObjectPtr<Stream> stream;
inline operator nghttp3_vec() const { return {data[0].base, data[0].len}; }
inline operator nghttp3_vec*() {
return reinterpret_cast<nghttp3_vec*>(data);
}
inline operator const ngtcp2_vec*() const { return data; }
inline operator ngtcp2_vec*() { return data; }
std::string ToString() const;
};

View File

@ -30,7 +30,8 @@ class Packet;
V(packet) \
V(session) \
V(stream) \
V(udp)
V(udp) \
V(http3application)
// The callbacks are persistent v8::Function references that are set in the
// quic::BindingState used to communicate data and events back out to the JS
@ -60,8 +61,7 @@ class Packet;
V(ack_delay_exponent, "ackDelayExponent") \
V(active_connection_id_limit, "activeConnectionIDLimit") \
V(address_lru_size, "addressLRUSize") \
V(alpn, "alpn") \
V(application_options, "application") \
V(application_provider, "provider") \
V(bbr, "bbr") \
V(ca, "ca") \
V(certs, "certs") \
@ -69,7 +69,6 @@ class Packet;
V(crl, "crl") \
V(ciphers, "ciphers") \
V(cubic, "cubic") \
V(disable_active_migration, "disableActiveMigration") \
V(disable_stateless_reset, "disableStatelessReset") \
V(enable_connect_protocol, "enableConnectProtocol") \
V(enable_datagrams, "enableDatagrams") \
@ -80,6 +79,7 @@ class Packet;
V(groups, "groups") \
V(handshake_timeout, "handshakeTimeout") \
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
V(http3application, "Http3Application") \
V(initial_max_data, "initialMaxData") \
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \
@ -105,9 +105,9 @@ class Packet;
V(max_stream_window, "maxStreamWindow") \
V(max_window, "maxWindow") \
V(min_version, "minVersion") \
V(no_udp_payload_size_shaping, "noUdpPayloadSizeShaping") \
V(packetwrap, "PacketWrap") \
V(preferred_address_strategy, "preferredAddressPolicy") \
V(protocol, "protocol") \
V(qlog, "qlog") \
V(qpack_blocked_streams, "qpackBlockedStreams") \
V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \
@ -117,8 +117,8 @@ class Packet;
V(retry_token_expiration, "retryTokenExpiration") \
V(reset_token_secret, "resetTokenSecret") \
V(rx_loss, "rxDiagnosticLoss") \
V(servername, "servername") \
V(session, "Session") \
V(sni, "sni") \
V(stream, "Stream") \
V(success, "success") \
V(tls_options, "tls") \
@ -169,7 +169,7 @@ class BindingData final
// bridge out to the JS API.
static void SetCallbacks(const v8::FunctionCallbackInfo<v8::Value>& args);
std::vector<Packet*> packet_freelist;
std::vector<BaseObjectPtr<BaseObject>> packet_freelist;
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;

View File

@ -20,14 +20,12 @@ CID::CID() : ptr_(&cid_) {
CID::CID(const ngtcp2_cid& cid) : CID(cid.data, cid.datalen) {}
CID::CID(const uint8_t* data, size_t len) : CID() {
DCHECK_GE(len, kMinLength);
DCHECK_LE(len, kMaxLength);
ngtcp2_cid_init(&cid_, data, len);
}
CID::CID(const ngtcp2_cid* cid) : ptr_(cid) {
CHECK_NOT_NULL(cid);
DCHECK_GE(cid->datalen, kMinLength);
DCHECK_LE(cid->datalen, kMaxLength);
}

View File

@ -257,6 +257,14 @@ std::optional<int> QuicError::crypto_error() const {
}
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
if ((type() == QuicError::Type::TRANSPORT && code() == NGTCP2_NO_ERROR) ||
(type() == QuicError::Type::APPLICATION &&
code() == NGTCP2_APP_NOERROR) ||
(type() == QuicError::Type::APPLICATION &&
code() == NGHTTP3_H3_NO_ERROR)) {
return Undefined(env->isolate());
}
Local<Value> argv[] = {
Integer::New(env->isolate(), static_cast<int>(type())),
BigInt::NewFromUnsigned(env->isolate(), code()),

View File

@ -212,6 +212,15 @@ enum class DatagramStatus : uint8_t {
LOST,
};
#define CC_ALGOS(V) \
V(RENO, reno) \
V(CUBIC, cubic) \
V(BBR, bbr)
#define V(name, _) static constexpr auto CC_ALGO_##name = NGTCP2_CC_ALGO_##name;
CC_ALGOS(V)
#undef V
constexpr uint64_t NGTCP2_APP_NOERROR = 65280;
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();

View File

@ -19,6 +19,7 @@
#include "application.h"
#include "bindingdata.h"
#include "defs.h"
#include "http3.h"
#include "ncrypto.h"
namespace node {
@ -28,7 +29,6 @@ using v8::BackingStore;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Int32;
using v8::Integer;
using v8::Just;
using v8::Local;
@ -93,65 +93,7 @@ bool is_diagnostic_packet_loss(double probability) {
CHECK(ncrypto::CSPRNG(&c, 1));
return (static_cast<double>(c) / 255) < probability;
}
#endif // DEBUG
Maybe<ngtcp2_cc_algo> getAlgoFromString(Environment* env, Local<String> input) {
auto& state = BindingData::Get(env);
#define V(name, str) \
if (input->StringEquals(state.str##_string())) { \
return Just(NGTCP2_CC_ALGO_##name); \
}
ENDPOINT_CC(V)
#undef V
return Nothing<ngtcp2_cc_algo>();
}
template <typename Opt, ngtcp2_cc_algo Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
ngtcp2_cc_algo algo;
if (value->IsString()) {
if (!getAlgoFromString(env, value.As<String>()).To(&algo)) {
THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid");
return false;
}
} else {
if (!value->IsInt32()) {
THROW_ERR_INVALID_ARG_VALUE(
env, "The cc_algorithm option must be a string or an integer");
return false;
}
Local<Int32> num;
if (!value->ToInt32(env->context()).ToLocal(&num)) {
THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid");
return false;
}
switch (num->Value()) {
#define V(name, _) \
case NGTCP2_CC_ALGO_##name: \
break;
ENDPOINT_CC(V)
#undef V
default:
THROW_ERR_INVALID_ARG_VALUE(env,
"The cc_algorithm option is invalid");
return false;
}
algo = static_cast<ngtcp2_cc_algo>(num->Value());
}
options->*member = algo;
}
return true;
}
#if DEBUG
template <typename Opt, double Opt::*member>
bool SetOption(Environment* env,
Opt* options,
@ -251,17 +193,13 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
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(max_payload_size) ||
!SET(unacknowledged_packet_threshold) || !SET(validate_address) ||
!SET(max_retries) || !SET(validate_address) ||
!SET(disable_stateless_reset) || !SET(ipv6_only) ||
!SET(handshake_timeout) || !SET(max_stream_window) || !SET(max_window) ||
!SET(no_udp_payload_size_shaping) ||
#ifdef DEBUG
!SET(rx_loss) || !SET(tx_loss) ||
#endif
!SET(cc_algorithm) || !SET(udp_receive_buffer_size) ||
!SET(udp_send_buffer_size) || !SET(udp_ttl) || !SET(reset_token_secret) ||
!SET(token_secret)) {
!SET(udp_receive_buffer_size) || !SET(udp_send_buffer_size) ||
!SET(udp_ttl) || !SET(reset_token_secret) || !SET(token_secret)) {
return Nothing<Options>();
}
@ -317,19 +255,6 @@ std::string Endpoint::Options::ToString() const {
prefix + "max stateless resets: " + std::to_string(max_stateless_resets);
res += prefix + "address lru size: " + std::to_string(address_lru_size);
res += prefix + "max retries: " + std::to_string(max_retries);
res += prefix + "max payload size: " + std::to_string(max_payload_size);
res += prefix + "unacknowledged packet threshold: " +
std::to_string(unacknowledged_packet_threshold);
if (handshake_timeout == UINT64_MAX) {
res += prefix + "handshake timeout: <none>";
} else {
res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) +
" nanoseconds";
}
res += prefix + "max stream window: " + std::to_string(max_stream_window);
res += prefix + "max window: " + std::to_string(max_window);
res += prefix + "no udp payload size shaping: " +
boolToString(no_udp_payload_size_shaping);
res += prefix + "validate address: " + boolToString(validate_address);
res += prefix +
"disable stateless reset: " + boolToString(disable_stateless_reset);
@ -337,18 +262,6 @@ std::string Endpoint::Options::ToString() const {
res += prefix + "rx loss: " + std::to_string(rx_loss);
res += prefix + "tx loss: " + std::to_string(tx_loss);
#endif
auto ccalg = ([&] {
switch (cc_algorithm) {
#define V(name, label) \
case NGTCP2_CC_ALGO_##name: \
return #label;
ENDPOINT_CC(V)
#undef V
}
return "<unknown>";
})();
res += prefix + "cc algorithm: " + std::string(ccalg);
res += prefix + "reset token secret: " + reset_token_secret.ToString();
res += prefix + "token secret: " + token_secret.ToString();
res += prefix + "ipv6 only: " + boolToString(ipv6_only);
@ -453,6 +366,10 @@ class Endpoint::UDP::Impl final : public HandleWrap {
Endpoint::UDP::UDP(Endpoint* endpoint) : impl_(Impl::Create(endpoint)) {
DCHECK(impl_);
// The endpoint starts in an inactive, unref'd state. It will be ref'd when
// the endpoint is either configured to listen as a server or when then are
// active client sessions.
Unref();
}
Endpoint::UDP::~UDP() {
@ -553,31 +470,33 @@ SocketAddress Endpoint::UDP::local_address() const {
return SocketAddress::FromSockName(impl_->handle_);
}
int Endpoint::UDP::Send(Packet* packet) {
int Endpoint::UDP::Send(const BaseObjectPtr<Packet>& packet) {
DCHECK(packet);
DCHECK(!packet->IsDispatched());
if (is_closed_or_closing()) return UV_EBADF;
DCHECK_NOT_NULL(packet);
uv_buf_t buf = *packet;
// We don't use the default implementation of Dispatch because the packet
// itself is going to be reset and added to a freelist to be reused. The
// default implementation of Dispatch will cause the packet to be deleted,
// which we don't want. We call ClearWeak here just to be doubly sure.
// which we don't want.
packet->ClearWeak();
packet->Dispatched();
int err = uv_udp_send(
packet->req(),
&impl_->handle_,
&buf,
1,
packet->destination().data(),
uv_udp_send_cb{[](uv_udp_send_t* req, int status) {
auto ptr = static_cast<Packet*>(ReqWrap<uv_udp_send_t>::from_req(req));
ptr->env()->DecreaseWaitingRequestCounter();
ptr->Done(status);
}});
int err = uv_udp_send(packet->req(),
&impl_->handle_,
&buf,
1,
packet->destination().data(),
uv_udp_send_cb{[](uv_udp_send_t* req, int status) {
auto ptr = BaseObjectPtr<Packet>(static_cast<Packet*>(
ReqWrap<uv_udp_send_t>::from_req(req)));
ptr->env()->DecreaseWaitingRequestCounter();
ptr->Done(status);
}});
if (err < 0) {
// The packet failed.
packet->Done(err);
packet->MakeWeak();
} else {
packet->env()->IncreaseWaitingRequestCounter();
}
@ -617,15 +536,10 @@ Local<FunctionTemplate> Endpoint::GetConstructorTemplate(Environment* env) {
void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
Http3Application::InitPerIsolate(data, target);
}
void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
#define V(name, str) \
NODE_DEFINE_CONSTANT(target, CC_ALGO_##name); \
NODE_DEFINE_STRING_CONSTANT(target, "CC_ALGO_" #name "_STR", #str);
ENDPOINT_CC(V)
#undef V
#define V(name, _) IDX_STATS_ENDPOINT_##name,
enum IDX_STATS_ENDPOINT { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT };
NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT);
@ -678,6 +592,8 @@ void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE);
NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE);
Http3Application::InitPerContext(realm, target);
SetConstructorFunction(realm->context(),
target,
"Endpoint",
@ -704,6 +620,7 @@ Endpoint::Endpoint(Environment* env,
udp_(this),
addrLRU_(options_.address_lru_size) {
MakeWeak();
udp_.Unref();
STAT_RECORD_TIMESTAMP(Stats, created_at);
IF_QUIC_DEBUG(env) {
Debug(this, "Endpoint created. Options %s", options.ToString());
@ -733,64 +650,71 @@ void Endpoint::MarkAsBusy(bool on) {
RegularToken Endpoint::GenerateNewToken(uint32_t version,
const SocketAddress& remote_address) {
IF_QUIC_DEBUG(env()) {
Debug(this,
"Generating new regular token for version %u and remote address %s",
version,
remote_address);
}
Debug(this,
"Generating new regular token for version %u and remote address %s",
version,
remote_address);
DCHECK(!is_closed() && !is_closing());
return RegularToken(version, remote_address, options_.token_secret);
}
StatelessResetToken Endpoint::GenerateNewStatelessResetToken(
uint8_t* token, const CID& cid) const {
IF_QUIC_DEBUG(env()) {
Debug(const_cast<Endpoint*>(this),
"Generating new stateless reset token for CID %s",
cid);
}
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);
}
void Endpoint::AddSession(const CID& cid, BaseObjectPtr<Session> session) {
if (is_closed() || is_closing()) return;
DCHECK(!is_closed() && !is_closing());
Debug(this, "Adding session for CID %s", cid);
sessions_[cid] = session;
IncrementSocketAddressCounter(session->remote_address());
AssociateCID(session->config().dcid, session->config().scid);
sessions_[cid] = session;
if (session->is_server()) {
STAT_INCREMENT(Stats, server_sessions);
// We only emit the new session event for server sessions.
EmitNewSession(session);
// It is important to note that the session may be closed/destroyed
// when it is emitted here.
} else {
STAT_INCREMENT(Stats, client_sessions);
}
udp_.Ref();
}
void Endpoint::RemoveSession(const CID& cid) {
void Endpoint::RemoveSession(const CID& cid,
const SocketAddress& remote_address) {
if (is_closed()) return;
Debug(this, "Removing session for CID %s", cid);
auto session = FindSession(cid);
if (!session) return;
DecrementSocketAddressCounter(session->remote_address());
sessions_.erase(cid);
if (sessions_.erase(cid)) {
DecrementSocketAddressCounter(remote_address);
}
if (sessions_.empty()) {
udp_.Unref();
}
if (state_->closing == 1) MaybeDestroy();
}
BaseObjectPtr<Session> Endpoint::FindSession(const CID& cid) {
BaseObjectPtr<Session> session;
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_));
session = session_it->second;
return session_it->second;
}
} else {
session = session_it->second;
// No match found.
return {};
}
return session;
// Match found!
return session_it->second;
}
void Endpoint::AssociateCID(const CID& cid, const CID& scid) {
@ -823,8 +747,7 @@ void Endpoint::DisassociateStatelessResetToken(
}
}
void Endpoint::Send(Packet* packet) {
CHECK_NOT_NULL(packet);
void Endpoint::Send(const BaseObjectPtr<Packet>& packet) {
#ifdef DEBUG
// When diagnostic packet loss is enabled, the packet will be randomly
// dropped. This can happen to any type of packet. We use this only in
@ -836,11 +759,13 @@ void Endpoint::Send(Packet* packet) {
}
#endif // DEBUG
if (is_closed() || is_closing() || packet->length() == 0) return;
if (is_closed() || is_closing() || packet->length() == 0) {
packet->Done(UV_ECANCELED);
return;
}
Debug(this, "Sending %s", packet->ToString());
state_->pending_callbacks++;
int err = udp_.Send(packet);
if (err != 0) {
Debug(this, "Sending packet failed with error %d", err);
packet->Done(err);
@ -868,6 +793,7 @@ void Endpoint::SendRetry(const PathDescriptor& options) {
if (packet) {
STAT_INCREMENT(Stats, retry_count);
Send(std::move(packet));
packet.reset();
}
// If creating the retry is unsuccessful, we just drop things on the floor.
@ -889,6 +815,7 @@ void Endpoint::SendVersionNegotiation(const PathDescriptor& options) {
if (packet) {
STAT_INCREMENT(Stats, version_negotiation_count);
Send(std::move(packet));
packet.reset();
}
// If creating the packet is unsuccessful, we just drop things on the floor.
@ -924,6 +851,7 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options,
addrLRU_.Upsert(options.remote_address)->reset_count++;
STAT_INCREMENT(Stats, stateless_reset_count);
Send(std::move(packet));
packet.reset();
return true;
}
return false;
@ -942,6 +870,7 @@ void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options,
if (packet) {
STAT_INCREMENT(Stats, immediate_close_count);
Send(std::move(packet));
packet.reset();
}
}
@ -965,6 +894,7 @@ bool Endpoint::Start() {
}
err = udp_.Start();
udp_.Ref();
if (err != 0) {
// If we failed to start listening, destroy the endpoint. There's nothing we
// can do.
@ -1015,41 +945,42 @@ BaseObjectPtr<Session> Endpoint::Connect(
const Session::Options& options,
std::optional<SessionTicket> session_ticket) {
// If starting fails, the endpoint will be destroyed.
if (!Start()) return BaseObjectPtr<Session>();
if (!Start()) return {};
Session::Config config(*this, options, local_address(), remote_address);
Session::Config config(env(), options, local_address(), remote_address);
IF_QUIC_DEBUG(env()) {
Debug(
this,
Debug(this,
"Connecting to %s with options %s and config %s [has 0rtt ticket? %s]",
remote_address,
options,
config,
session_ticket.has_value() ? "yes" : "no");
}
auto tls_context = TLSContext::CreateClient(options.tls_options);
if (!*tls_context) {
THROW_ERR_INVALID_STATE(env(),
"Failed to create TLS context: %s",
tls_context->validation_error());
return BaseObjectPtr<Session>();
return {};
}
auto session =
Session::Create(this, config, tls_context.get(), session_ticket);
if (!session) {
THROW_ERR_INVALID_STATE(env(), "Failed to create session");
return {};
}
if (!session->tls_session()) {
THROW_ERR_INVALID_STATE(env(),
"Failed to create TLS session: %s",
session->tls_session().validation_error());
return BaseObjectPtr<Session>();
return {};
}
if (!session) return BaseObjectPtr<Session>();
session->set_wrapped();
// Calling SendPendingData here triggers the session to send the initial
// handshake packets starting the connection.
session->application().SendPendingData();
// Marking a session as "wrapped" means that the reference has been
// (or will be) passed out to JavaScript.
Session::SendPendingDataScope send_scope(session);
session->set_wrapped();
AddSession(config.scid, session);
return session;
}
@ -1139,8 +1070,8 @@ void Endpoint::Receive(const uv_buf_t& buf,
const CID& dcid,
const CID& scid) {
DCHECK_NOT_NULL(session);
DCHECK(!session->is_destroyed());
size_t len = store.length();
Debug(this, "Passing received packet to session for processing");
if (session->Receive(std::move(store), local_address, remote_address)) {
STAT_INCREMENT_N(Stats, bytes_received, len);
STAT_INCREMENT(Stats, packets_received);
@ -1157,21 +1088,31 @@ void Endpoint::Receive(const uv_buf_t& buf,
std::optional<SessionTicket> no_ticket = std::nullopt;
auto session = Session::Create(
this, config, server_state_->tls_context.get(), no_ticket);
if (session) {
if (!session->tls_session()) {
Debug(this,
"Failed to create TLS session for %s: %s",
config.dcid,
session->tls_session().validation_error());
return;
}
receive(session.get(),
std::move(store),
config.local_address,
config.remote_address,
config.dcid,
config.scid);
if (!session) {
Debug(this, "Failed to create session for %s", config.dcid);
return;
}
if (!session->tls_session()) {
Debug(this,
"Failed to create TLS session for %s: %s",
config.dcid,
session->tls_session().validation_error());
return;
}
AddSession(config.scid, session);
// It is possible that the session was created then immediately destroyed
// during the call to AddSession. If that's the case, we'll just return
// early.
if (session->is_destroyed()) [[unlikely]]
return;
receive(session.get(),
std::move(store),
config.local_address,
config.remote_address,
config.dcid,
config.scid);
};
const auto acceptInitialPacket = [&](const uint32_t version,
@ -1180,26 +1121,19 @@ void Endpoint::Receive(const uv_buf_t& buf,
Store&& store,
const SocketAddress& local_address,
const SocketAddress& remote_address) {
// Conditionally accept an initial packet to create a new session.
Debug(this,
"Trying to accept initial packet for %s from %s",
dcid,
remote_address);
// If we're not listening as a server, do not accept an initial packet.
if (state_->listening == 0) return;
if (!is_listening()) return;
ngtcp2_pkt_hd hd;
// This is our first condition check... A minimal check to see if ngtcp2 can
// even recognize this packet as a quic packet with the correct version.
// even recognize this packet as a quic packet.
ngtcp2_vec vec = store;
if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) {
// Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was
// successful, or an error code if it was not. Currently there's only one
// documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle
// any error here the same -- by ignoring the packet entirely.
Debug(this, "Failed to accept initial packet from %s", remote_address);
return;
}
@ -1208,10 +1142,13 @@ void Endpoint::Receive(const uv_buf_t& buf,
// version negotiation packet in response.
if (ngtcp2_is_supported_version(hd.version) == 0) {
Debug(this,
"Packet was not accepted because the version (%d) is not supported",
"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;
}
@ -1247,23 +1184,27 @@ void Endpoint::Receive(const uv_buf_t& buf,
return;
}
Debug(
this, "Accepting initial packet for %s from %s", dcid, remote_address);
// 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.
Session::Config config(Side::SERVER,
*this,
Session::Config config(env(),
Side::SERVER,
server_state_->options,
version,
local_address,
remote_address,
scid,
dcid,
dcid);
Debug(this, "Using session config for initial packet %s", config);
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. config.scid should *not*
// identifies us. config.dcid should equal scid, and config.scid should
// equal dcid.
DCHECK(config.dcid == scid);
DCHECK(config.scid == dcid);
@ -1292,6 +1233,19 @@ void Endpoint::Receive(const uv_buf_t& buf,
"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,
@ -1305,8 +1259,8 @@ void Endpoint::Receive(const uv_buf_t& buf,
return;
}
// We have two kinds of tokens, each prefixed with a different magic
// byte.
// We have two kinds of tokens, each prefixed with a different
// magic byte.
switch (hd.token[0]) {
case RetryToken::kTokenMagic: {
RetryToken token(hd.token, hd.tokenlen);
@ -1387,7 +1341,10 @@ void Endpoint::Receive(const uv_buf_t& buf,
// 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.
// 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,
@ -1484,12 +1441,16 @@ void Endpoint::Receive(const uv_buf_t& buf,
// processed.
auto it = token_map_.find(StatelessResetToken(vec.base));
if (it != token_map_.end()) {
receive(it->second,
std::move(store),
local_address,
remote_address,
dcid,
scid);
// If the session happens to have been destroyed already, we'll
// just ignore the packet.
if (!it->second->is_destroyed()) [[likely]] {
receive(it->second,
std::move(store),
local_address,
remote_address,
dcid,
scid);
}
return true;
}
@ -1512,10 +1473,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
// return;
// }
Debug(this,
"Received packet with length %" PRIu64 " from %s",
buf.len,
remote_address);
Debug(this, "Received %zu-byte packet from %s", buf.len, remote_address);
// The managed buffer here contains the received packet. We do not yet know
// at this point if it is a valid QUIC packet. We need to do some basic
@ -1528,7 +1486,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM);
}
Store store(backing, buf.len, 0);
Store store(std::move(backing), buf.len, 0);
ngtcp2_vec vec = store;
ngtcp2_version_cid pversion_cid;
@ -1547,7 +1505,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
// QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any
// packet with a non-standard CID length.
if (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN ||
pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) [[unlikely]] {
pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) {
Debug(this, "Packet had incorrectly sized CIDs, ignoring");
return; // Ignore the packet!
}
@ -1582,7 +1540,6 @@ void Endpoint::Receive(const uv_buf_t& buf,
auto session = FindSession(dcid);
auto addr = local_address();
HandleScope handle_scope(env()->isolate());
// If a session is not found, there are four possible reasons:
@ -1612,16 +1569,26 @@ void Endpoint::Receive(const uv_buf_t& buf,
remote_address);
}
if (session->is_destroyed()) [[unlikely]] {
// The session has been destroyed. Well that's not good.
Debug(this, "Session for dcid %s has been destroyed", dcid);
return;
}
// If we got here, the dcid matched the scid of a known local session. Yay!
// The session will take over any further processing of the packet.
Debug(this, "Dispatching packet to known session");
receive(session.get(), std::move(store), addr, remote_address, dcid, scid);
// It is important to note that the session may have been destroyed during
// the call to receive(...). If that's the case, the session object still
// exists but it is in a destroyed state. Care should be taken accessing
// session after this point.
}
void Endpoint::PacketDone(int status) {
if (is_closed()) return;
// At this point we should be waiting on at least one packet.
Debug(this, "Packet was sent with status %d", status);
DCHECK_GE(state_->pending_callbacks, 1);
state_->pending_callbacks--;
// Can we go ahead and close now?
@ -1685,6 +1652,11 @@ void Endpoint::EmitNewSession(const BaseObjectPtr<Session>& session) {
Debug(this, "Notifying JavaScript about new session");
MakeCallback(BindingData::Get(env()).session_new_callback(), 1, &arg);
// It is important to note that the session may have been destroyed during
// the call to MakeCallback. If that's the case, the session object still
// exists but it is in a destroyed state. Care should be taken accessing
// session after this point.
}
void Endpoint::EmitClose(CloseContext context, int status) {
@ -1735,7 +1707,7 @@ void Endpoint::DoConnect(const FunctionCallbackInfo<Value>& args) {
return;
}
BaseObjectPtr<Session> session;
BaseObjectWeakPtr<Session> session;
if (!args[2]->IsUndefined()) {
SessionTicket ticket;

View File

@ -19,11 +19,6 @@
namespace node::quic {
#define ENDPOINT_CC(V) \
V(RENO, reno) \
V(CUBIC, cubic) \
V(BBR, bbr)
// An Endpoint encapsulates the UDP local port binding and is responsible for
// sending and receiving QUIC packets. A single endpoint can act as both a QUIC
// client and server simultaneously.
@ -37,10 +32,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
#define V(name, _) static constexpr auto CC_ALGO_##name = NGTCP2_CC_ALGO_##name;
ENDPOINT_CC(V)
#undef V
// Endpoint configuration options
struct Options final : public MemoryRetainer {
// The local socket address to which the UDP port will be bound. The port
@ -95,30 +86,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
// retries, so limiting them helps prevent a DOS vector.
uint64_t max_retries = DEFAULT_MAX_RETRY_LIMIT;
// The max_payload_size is the maximum size of a serialized QUIC packet. It
// should always be set small enough to fit within a single MTU without
// fragmentation. The default is set by the QUIC specification at 1200. This
// value should not be changed unless you know for sure that the entire path
// supports a given MTU without fragmenting at any point in the path.
uint64_t max_payload_size = kDefaultMaxPacketLength;
// The unacknowledged_packet_threshold is the maximum number of
// unacknowledged packets that an ngtcp2 session will accumulate before
// sending an acknowledgement. Setting this to 0 uses the ngtcp2 defaults,
// which is what most will want. The value can be changed to fine tune some
// of the performance characteristics of the session. This should only be
// changed if you have a really good reason for doing so.
uint64_t unacknowledged_packet_threshold = 0;
// The amount of time (in milliseconds) that the endpoint will wait for the
// completion of the tls handshake.
uint64_t handshake_timeout = UINT64_MAX;
uint64_t max_stream_window = 0;
uint64_t max_window = 0;
bool no_udp_payload_size_shaping = true;
// The validate_address parameter instructs the Endpoint to perform explicit
// address validation using retry tokens. This is strongly recommended and
// should only be disabled in trusted, closed environments as a performance
@ -142,14 +109,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
double tx_loss = 0.0;
#endif // DEBUG
// There are several common congestion control algorithms that ngtcp2 uses
// to determine how it manages the flow control window: RENO, CUBIC, and
// BBR. The details of how each works is not relevant here. The choice of
// which to use by default is arbitrary and we can choose whichever we'd
// like. Additional performance profiling will be needed to determine which
// is the better of the two for our needs.
ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC;
// By default, when the endpoint is created, it will generate a
// reset_token_secret at random. This is a secret used in generating
// stateless reset tokens. In order for stateless reset to be effective,
@ -197,6 +156,10 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
v8::Local<v8::Object> object,
const Endpoint::Options& options);
inline operator Packet::Listener*() {
return this;
}
inline const Options& options() const {
return options_;
}
@ -216,7 +179,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
const CID& cid) const;
void AddSession(const CID& cid, BaseObjectPtr<Session> session);
void RemoveSession(const CID& cid);
void RemoveSession(const CID& cid, const SocketAddress& remote_address);
BaseObjectPtr<Session> FindSession(const CID& cid);
// A single session may be associated with multiple CIDs.
@ -232,7 +195,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
Session* session);
void DisassociateStatelessResetToken(const StatelessResetToken& token);
void Send(Packet* packet);
void Send(const BaseObjectPtr<Packet>& packet);
// Generates and sends a retry packet. This is terminal for the connection.
// Retry packets are used to force explicit path validation by issuing a token
@ -298,7 +261,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
int Start();
void Stop();
void Close();
int Send(Packet* packet);
int Send(const BaseObjectPtr<Packet>& packet);
// Returns the local UDP socket address to which we are bound,
// or fail with an assert if we are not bound.

View File

@ -17,16 +17,107 @@
#include "session.h"
#include "sessionticket.h"
namespace node::quic {
namespace {
namespace node {
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Local;
using v8::Object;
using v8::ObjectTemplate;
using v8::Value;
namespace quic {
// ============================================================================
bool Http3Application::HasInstance(Environment* env, Local<Value> value) {
return GetConstructorTemplate(env)->HasInstance(value);
}
Local<FunctionTemplate> Http3Application::GetConstructorTemplate(
Environment* env) {
auto& state = BindingData::Get(env);
auto tmpl = state.http3application_constructor_template();
if (tmpl.IsEmpty()) {
auto isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, New);
tmpl->SetClassName(state.http3application_string());
tmpl->InstanceTemplate()->SetInternalFieldCount(
Http3Application::kInternalFieldCount);
state.set_http3application_constructor_template(tmpl);
}
return tmpl;
}
void Http3Application::InitPerIsolate(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
}
void Http3Application::InitPerContext(Realm* realm, Local<Object> target) {
SetConstructorFunction(realm->context(),
target,
"Http3Application",
GetConstructorTemplate(realm->env()));
}
void Http3Application::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(New);
}
Http3Application::Http3Application(Environment* env,
Local<Object> object,
const Session::Application::Options& options)
: ApplicationProvider(env, object), options_(options) {
MakeWeak();
}
void Http3Application::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return;
}
Session::Application::Options options;
if (!args[0]->IsUndefined() &&
!Session::Application::Options::From(env, args[0]).To(&options)) {
return;
}
if (auto app = MakeBaseObject<Http3Application>(env, obj, options)) {
args.GetReturnValue().Set(app->object());
}
}
void Http3Application::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_);
}
std::string Http3Application::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
std::string res("{");
res += prefix + "options: " + options_.ToString();
res += indent.Close();
return res;
}
// ============================================================================
struct Http3HeadersTraits {
typedef nghttp3_nv nv_t;
using nv_t = nghttp3_nv;
};
struct Http3RcBufferPointerTraits {
typedef nghttp3_rcbuf rcbuf_t;
typedef nghttp3_vec vector_t;
using rcbuf_t = nghttp3_rcbuf;
using vector_t = nghttp3_vec;
static void inc(rcbuf_t* buf) {
CHECK_NOT_NULL(buf);
@ -76,10 +167,10 @@ struct Http3HeaderTraits {
using Http3Header = NgHeader<Http3HeaderTraits>;
// Implements the low-level HTTP/3 Application semantics.
class Http3Application final : public Session::Application {
class Http3ApplicationImpl final : public Session::Application {
public:
Http3Application(Session* session,
const Session::Application_Options& options)
Http3ApplicationImpl(Session* session,
const Session::Application::Options& options)
: Application(session, options),
allocator_(BindingData::Get(env())),
options_(options),
@ -91,8 +182,9 @@ class Http3Application final : public Session::Application {
CHECK(!started_);
started_ = true;
Debug(&session(), "Starting HTTP/3 application.");
auto params = ngtcp2_conn_get_remote_transport_params(session());
if (params == nullptr) {
if (params == nullptr) [[unlikely]] {
// The params are not available yet. Cannot start.
Debug(&session(),
"Cannot start HTTP/3 application yet. No remote transport params");
@ -100,29 +192,67 @@ class Http3Application final : public Session::Application {
}
if (params->initial_max_streams_uni < 3) {
// If the initial max unidirectional stream limit is not at least three,
// we cannot actually use it since we need to create the control streams.
// HTTP3 requires 3 unidirectional control streams to be opened in each
// direction in additional to the bidirectional streams that are used to
// actually carry request and response payload back and forth.
// See:
// https://nghttp2.org/nghttp3/programmers-guide.html#binding-control-streams
Debug(&session(),
"Cannot start HTTP/3 application. Initial max "
"unidirectional streams is too low");
"unidirectional streams [%zu] is too low. Must be at least 3",
params->initial_max_streams_uni);
return false;
}
// If this is a server session, then set the maximum number of
// bidirectional streams that can be created. This determines the number
// of requests that the client can actually created.
if (session().is_server()) {
nghttp3_conn_set_max_client_streams_bidi(
*this, params->initial_max_streams_bidi);
}
return CreateAndBindControlStreams();
Debug(&session(), "Creating and binding HTTP/3 control streams");
bool ret =
ngtcp2_conn_open_uni_stream(session(), &control_stream_id_, nullptr) ==
0 &&
ngtcp2_conn_open_uni_stream(
session(), &qpack_enc_stream_id_, nullptr) == 0 &&
ngtcp2_conn_open_uni_stream(
session(), &qpack_dec_stream_id_, nullptr) == 0 &&
nghttp3_conn_bind_control_stream(*this, control_stream_id_) == 0 &&
nghttp3_conn_bind_qpack_streams(
*this, qpack_enc_stream_id_, qpack_dec_stream_id_) == 0;
if (env()->enabled_debug_list()->enabled(DebugCategory::QUIC) && ret) {
Debug(&session(),
"Created and bound control stream %" PRIi64,
control_stream_id_);
Debug(&session(),
"Created and bound qpack enc stream %" PRIi64,
qpack_enc_stream_id_);
Debug(&session(),
"Created and bound qpack dec streams %" PRIi64,
qpack_dec_stream_id_);
}
return ret;
}
bool ReceiveStreamData(Stream* stream,
bool ReceiveStreamData(int64_t stream_id,
const uint8_t* data,
size_t datalen,
Stream::ReceiveDataFlags flags) override {
Debug(&session(), "HTTP/3 application received %zu bytes of data", datalen);
const Stream::ReceiveDataFlags& flags,
void* unused) override {
Debug(&session(),
"HTTP/3 application received %zu bytes of data "
"on stream %" PRIi64 ". Is final? %d",
datalen,
stream_id,
flags.fin);
ssize_t nread = nghttp3_conn_read_stream(
*this, stream->id(), data, datalen, flags.fin ? 1 : 0);
*this, stream_id, data, datalen, flags.fin ? 1 : 0);
if (nread < 0) {
Debug(&session(),
@ -131,20 +261,24 @@ class Http3Application final : public Session::Application {
return false;
}
Debug(&session(),
"Extending stream and connection offset by %zd bytes",
nread);
session().ExtendStreamOffset(stream->id(), nread);
session().ExtendOffset(nread);
if (nread > 0) {
Debug(&session(),
"Extending stream and connection offset by %zd bytes",
nread);
session().ExtendStreamOffset(stream_id, nread);
session().ExtendOffset(nread);
}
return true;
}
void AcknowledgeStreamData(Stream* stream, size_t datalen) override {
bool AcknowledgeStreamData(int64_t stream_id, size_t datalen) override {
Debug(&session(),
"HTTP/3 application received acknowledgement for %zu bytes of data",
datalen);
CHECK_EQ(nghttp3_conn_add_ack_offset(*this, stream->id(), datalen), 0);
"HTTP/3 application received acknowledgement for %zu bytes of data "
"on stream %" PRIi64,
datalen,
stream_id);
return nghttp3_conn_add_ack_offset(*this, stream_id, datalen) == 0;
}
bool CanAddHeader(size_t current_count,
@ -153,17 +287,9 @@ class Http3Application final : public Session::Application {
// We cannot add the header if we've either reached
// * the max number of header pairs or
// * the max number of header bytes
bool answer = (current_count < options_.max_header_pairs) &&
(current_headers_length + this_header_length) <=
options_.max_header_length;
IF_QUIC_DEBUG(env()) {
if (answer) {
Debug(&session(), "HTTP/3 application can add header");
} else {
Debug(&session(), "HTTP/3 application cannot add header");
}
}
return answer;
return (current_count < options_.max_header_pairs) &&
(current_headers_length + this_header_length) <=
options_.max_header_length;
}
void BlockStream(int64_t id) override {
@ -186,7 +312,7 @@ class Http3Application final : public Session::Application {
switch (direction) {
case Direction::BIDIRECTIONAL: {
Debug(&session(),
"HTTP/3 application extending max bidi streams to %" PRIu64,
"HTTP/3 application extending max bidi streams by %" PRIu64,
max_streams);
ngtcp2_conn_extend_max_streams_bidi(
session(), static_cast<size_t>(max_streams));
@ -194,7 +320,7 @@ class Http3Application final : public Session::Application {
}
case Direction::UNIDIRECTIONAL: {
Debug(&session(),
"HTTP/3 application extending max uni streams to %" PRIu64,
"HTTP/3 application extending max uni streams by %" PRIu64,
max_streams);
ngtcp2_conn_extend_max_streams_uni(
session(), static_cast<size_t>(max_streams));
@ -227,7 +353,7 @@ class Http3Application final : public Session::Application {
: SessionTicket::AppData::Status::TICKET_USE;
}
void StreamClose(Stream* stream, QuicError error = QuicError()) override {
void StreamClose(Stream* stream, QuicError&& error = QuicError()) override {
Debug(
&session(), "HTTP/3 application closing stream %" PRIi64, stream->id());
uint64_t code = NGHTTP3_H3_NO_ERROR;
@ -254,14 +380,14 @@ class Http3Application final : public Session::Application {
void StreamReset(Stream* stream,
uint64_t final_size,
QuicError error) override {
QuicError&& error = QuicError()) override {
// We are shutting down the readable side of the local stream here.
Debug(&session(),
"HTTP/3 application resetting stream %" PRIi64,
stream->id());
int rv = nghttp3_conn_shutdown_stream_read(*this, stream->id());
if (rv == 0) {
stream->ReceiveStreamReset(final_size, error);
stream->ReceiveStreamReset(final_size, std::move(error));
return;
}
@ -270,8 +396,9 @@ class Http3Application final : public Session::Application {
session().Close();
}
void StreamStopSending(Stream* stream, QuicError error) override {
Application::StreamStopSending(stream, error);
void StreamStopSending(Stream* stream,
QuicError&& error = QuicError()) override {
Application::StreamStopSending(stream, std::move(error));
}
bool SendHeaders(const Stream& stream,
@ -288,7 +415,7 @@ class Http3Application final : public Session::Application {
return false;
}
Debug(&session(),
"Submitting early hints for stream " PRIi64,
"Submitting %" PRIu64 " early hints for stream %" PRIu64,
stream.id());
return nghttp3_conn_submit_info(
*this, stream.id(), nva.data(), nva.length()) == 0;
@ -301,19 +428,23 @@ class Http3Application final : public Session::Application {
// If the terminal flag is set, that means that we know we're only
// sending headers and no body and the stream writable side should be
// closed immediately because there is no nghttp3_data_reader provided.
if (flags != HeadersFlags::TERMINAL) reader_ptr = &reader;
if (flags != HeadersFlags::TERMINAL) {
reader_ptr = &reader;
}
if (session().is_server()) {
// If this is a server, we're submitting a response...
Debug(&session(),
"Submitting response headers for stream " PRIi64,
"Submitting %" PRIu64 " response headers for stream %" PRIu64,
nva.length(),
stream.id());
return nghttp3_conn_submit_response(
*this, stream.id(), nva.data(), nva.length(), reader_ptr);
} else {
// Otherwise we're submitting a request...
Debug(&session(),
"Submitting request headers for stream " PRIi64,
"Submitting %" PRIu64 " request headers for stream %" PRIu64,
nva.length(),
stream.id());
return nghttp3_conn_submit_request(*this,
stream.id(),
@ -325,6 +456,10 @@ class Http3Application final : public Session::Application {
break;
}
case HeadersKind::TRAILING: {
Debug(&session(),
"Submitting %" PRIu64 " trailing headers for stream %" PRIu64,
nva.length(),
stream.id());
return nghttp3_conn_submit_trailers(
*this, stream.id(), nva.data(), nva.length()) == 0;
break;
@ -351,22 +486,25 @@ class Http3Application final : public Session::Application {
}
int GetStreamData(StreamData* data) override {
data->count = kMaxVectorCount;
ssize_t ret = 0;
Debug(&session(), "HTTP/3 application getting stream data");
if (conn_ && session().max_data_left()) {
nghttp3_vec vec = *data;
ret = nghttp3_conn_writev_stream(
*this, &data->id, &data->fin, &vec, data->count);
*this, &data->id, &data->fin, *data, data->count);
// A negative return value indicates an error.
if (ret < 0) {
return static_cast<int>(ret);
} else {
data->remaining = data->count = static_cast<size_t>(ret);
if (data->id > 0) {
data->stream = session().FindStream(data->id);
}
}
data->count = static_cast<size_t>(ret);
if (data->id > 0 && data->id != control_stream_id_ &&
data->id != qpack_dec_stream_id_ &&
data->id != qpack_enc_stream_id_) {
data->stream = session().FindStream(data->id);
}
}
DCHECK_NOT_NULL(data->buf);
return 0;
}
@ -389,8 +527,8 @@ class Http3Application final : public Session::Application {
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(Http3Application)
SET_SELF_SIZE(Http3Application)
SET_MEMORY_INFO_NAME(Http3ApplicationImpl)
SET_SELF_SIZE(Http3ApplicationImpl)
private:
inline operator nghttp3_conn*() const {
@ -398,35 +536,11 @@ class Http3Application final : public Session::Application {
return conn_.get();
}
bool CreateAndBindControlStreams() {
Debug(&session(), "Creating and binding HTTP/3 control streams");
auto stream = session().OpenStream(Direction::UNIDIRECTIONAL);
if (!stream) return false;
if (nghttp3_conn_bind_control_stream(*this, stream->id()) != 0) {
return false;
}
auto enc_stream = session().OpenStream(Direction::UNIDIRECTIONAL);
if (!enc_stream) return false;
auto dec_stream = session().OpenStream(Direction::UNIDIRECTIONAL);
if (!dec_stream) return false;
bool bound = nghttp3_conn_bind_qpack_streams(
*this, enc_stream->id(), dec_stream->id()) == 0;
control_stream_id_ = stream->id();
qpack_enc_stream_id_ = enc_stream->id();
qpack_dec_stream_id_ = dec_stream->id();
return bound;
}
inline bool is_control_stream(int64_t id) const {
return id == control_stream_id_ || id == qpack_dec_stream_id_ ||
id == qpack_enc_stream_id_;
}
bool is_destroyed() const { return session().is_destroyed(); }
Http3ConnectionPointer InitializeConnection() {
nghttp3_conn* conn = nullptr;
nghttp3_settings settings = options_;
@ -443,118 +557,141 @@ class Http3Application final : public Session::Application {
}
void OnStreamClose(Stream* stream, uint64_t app_error_code) {
if (stream->is_destroyed()) return;
Debug(&session(),
"HTTP/3 application received stream close for stream %" PRIi64,
stream->id());
if (app_error_code != NGHTTP3_H3_NO_ERROR) {
Debug(&session(),
"HTTP/3 application received stream close for stream %" PRIi64
" with code %" PRIu64,
stream->id(),
app_error_code);
}
auto direction = stream->direction();
stream->Destroy(QuicError::ForApplication(app_error_code));
ExtendMaxStreams(EndpointLabel::REMOTE, direction, 1);
}
void OnReceiveData(Stream* stream, const nghttp3_vec& vec) {
if (stream->is_destroyed()) return;
Debug(&session(), "HTTP/3 application received %zu bytes of data", vec.len);
stream->ReceiveData(vec.base, vec.len, Stream::ReceiveDataFlags{});
}
void OnDeferredConsume(Stream* stream, size_t consumed) {
auto& sess = session();
Debug(
&session(), "HTTP/3 application deferred consume %zu bytes", consumed);
if (!stream->is_destroyed()) {
sess.ExtendStreamOffset(stream->id(), consumed);
}
sess.ExtendOffset(consumed);
}
void OnBeginHeaders(Stream* stream) {
if (stream->is_destroyed()) return;
void OnBeginHeaders(int64_t stream_id) {
auto stream = session().FindStream(stream_id);
// If the stream does not exist or is destroyed, ignore!
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application beginning initial block of headers for stream "
"%" PRIi64,
stream->id());
stream_id);
stream->BeginHeaders(HeadersKind::INITIAL);
}
void OnReceiveHeader(Stream* stream, Http3Header&& header) {
if (stream->is_destroyed()) return;
if (header.name() == ":status") {
if (header.value()[0] == '1') {
Debug(
&session(),
void OnReceiveHeader(int64_t stream_id, Http3Header&& header) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
if (header.name() == ":status" && header.value()[0] == '1') {
Debug(&session(),
"HTTP/3 application switching to hints headers for stream %" PRIi64,
stream->id());
stream->set_headers_kind(HeadersKind::HINTS);
}
stream->set_headers_kind(HeadersKind::HINTS);
}
IF_QUIC_DEBUG(env()) {
Debug(&session(),
"Received header \"%s: %s\"",
header.name(),
header.value());
}
stream->AddHeader(std::move(header));
}
void OnEndHeaders(Stream* stream, int fin) {
void OnEndHeaders(int64_t stream_id, int fin) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application received end of headers for stream %" PRIi64,
stream->id());
stream_id);
stream->EmitHeaders();
if (fin != 0) {
if (fin) {
// The stream is done. There's no more data to receive!
Debug(&session(), "Headers are final for stream %" PRIi64, stream->id());
OnEndStream(stream);
Debug(&session(), "Headers are final for stream %" PRIi64, stream_id);
Stream::ReceiveDataFlags flags{
.fin = true,
.early = false,
};
stream->ReceiveData(nullptr, 0, flags);
}
}
void OnBeginTrailers(Stream* stream) {
if (stream->is_destroyed()) return;
void OnBeginTrailers(int64_t stream_id) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application beginning block of trailers for stream %" PRIi64,
stream->id());
stream_id);
stream->BeginHeaders(HeadersKind::TRAILING);
}
void OnReceiveTrailer(Stream* stream, Http3Header&& header) {
void OnReceiveTrailer(int64_t stream_id, Http3Header&& header) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
IF_QUIC_DEBUG(env()) {
Debug(&session(),
"Received header \"%s: %s\"",
header.name(),
header.value());
}
stream->AddHeader(header);
}
void OnEndTrailers(Stream* stream, int fin) {
if (stream->is_destroyed()) return;
void OnEndTrailers(int64_t stream_id, int fin) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application received end of trailers for stream %" PRIi64,
stream->id());
stream_id);
stream->EmitHeaders();
if (fin != 0) {
Debug(&session(), "Trailers are final for stream %" PRIi64, stream->id());
// The stream is done. There's no more data to receive!
stream->ReceiveData(nullptr,
0,
Stream::ReceiveDataFlags{/* .fin = */ true,
/* .early = */ false});
if (fin) {
Debug(&session(), "Trailers are final for stream %" PRIi64, stream_id);
Stream::ReceiveDataFlags flags{
.fin = true,
.early = false,
};
stream->ReceiveData(nullptr, 0, flags);
}
}
void OnEndStream(Stream* stream) {
if (stream->is_destroyed()) return;
void OnEndStream(int64_t stream_id) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application received end of stream for stream %" PRIi64,
stream->id());
stream->ReceiveData(nullptr,
0,
Stream::ReceiveDataFlags{/* .fin = */ true,
/* .early = */ false});
stream_id);
Stream::ReceiveDataFlags flags{
.fin = true,
.early = false,
};
stream->ReceiveData(nullptr, 0, flags);
}
void OnStopSending(Stream* stream, uint64_t app_error_code) {
if (stream->is_destroyed()) return;
void OnStopSending(int64_t stream_id, uint64_t app_error_code) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application received stop sending for stream %" PRIi64,
stream->id());
stream_id);
stream->ReceiveStopSending(QuicError::ForApplication(app_error_code));
}
void OnResetStream(Stream* stream, uint64_t app_error_code) {
if (stream->is_destroyed()) return;
void OnResetStream(int64_t stream_id, uint64_t app_error_code) {
auto stream = session().FindStream(stream_id);
if (!stream) [[unlikely]]
return;
Debug(&session(),
"HTTP/3 application received reset stream for stream %" PRIi64,
stream->id());
stream_id);
stream->ReceiveStreamReset(0, QuicError::ForApplication(app_error_code));
}
@ -584,13 +721,14 @@ class Http3Application final : public Session::Application {
options_.qpack_encoder_max_dtable_capacity =
settings->qpack_encoder_max_dtable_capacity;
options_.qpack_max_dtable_capacity = settings->qpack_max_dtable_capacity;
Debug(
&session(), "HTTP/3 application received updated settings ", options_);
Debug(&session(),
"HTTP/3 application received updated settings: %s",
options_);
}
bool started_ = false;
nghttp3_mem allocator_;
Session::Application_Options options_;
Session::Application::Options options_;
Http3ConnectionPointer conn_;
int64_t control_stream_id_ = -1;
int64_t qpack_dec_stream_id_ = -1;
@ -599,26 +737,30 @@ class Http3Application final : public Session::Application {
// ==========================================================================
// Static callbacks
static Http3Application* From(nghttp3_conn* conn, void* user_data) {
static Http3ApplicationImpl* From(nghttp3_conn* conn, void* user_data) {
DCHECK_NOT_NULL(user_data);
auto app = static_cast<Http3Application*>(user_data);
auto app = static_cast<Http3ApplicationImpl*>(user_data);
DCHECK_EQ(conn, app->conn_.get());
return app;
}
static Stream* From(int64_t stream_id, void* stream_user_data) {
DCHECK_NOT_NULL(stream_user_data);
auto stream = static_cast<Stream*>(stream_user_data);
DCHECK_EQ(stream_id, stream->id());
return stream;
static BaseObjectWeakPtr<Stream> FindOrCreateStream(nghttp3_conn* conn,
Session* session,
int64_t stream_id) {
if (auto stream = session->FindStream(stream_id)) {
return stream;
}
if (auto stream = session->CreateStream(stream_id)) {
return stream;
}
return {};
}
#define NGHTTP3_CALLBACK_SCOPE(name) \
auto name = From(conn, conn_user_data); \
if (name->is_destroyed()) [[unlikely]] { \
return NGHTTP3_ERR_CALLBACK_FAILURE; \
} \
NgHttp3CallbackScope scope(name->env());
auto ptr = From(conn, conn_user_data); \
CHECK_NOT_NULL(ptr); \
auto& name = *ptr; \
NgHttp3CallbackScope scope(name.env());
static nghttp3_ssize on_read_data_callback(nghttp3_conn* conn,
int64_t stream_id,
@ -627,7 +769,7 @@ class Http3Application final : public Session::Application {
uint32_t* pflags,
void* conn_user_data,
void* stream_user_data) {
return 0;
return NGTCP2_SUCCESS;
}
static int on_acked_stream_data(nghttp3_conn* conn,
@ -636,10 +778,9 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->AcknowledgeStreamData(stream, static_cast<size_t>(datalen));
return NGTCP2_SUCCESS;
return app.AcknowledgeStreamData(stream_id, static_cast<size_t>(datalen))
? NGTCP2_SUCCESS
: NGHTTP3_ERR_CALLBACK_FAILURE;
}
static int on_stream_close(nghttp3_conn* conn,
@ -648,9 +789,9 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnStreamClose(stream, app_error_code);
if (auto stream = app.session().FindStream(stream_id)) {
app.OnStreamClose(stream.get(), app_error_code);
}
return NGTCP2_SUCCESS;
}
@ -661,11 +802,19 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnReceiveData(stream,
nghttp3_vec{const_cast<uint8_t*>(data), datalen});
return NGTCP2_SUCCESS;
// The on_receive_data callback will never be called for control streams,
// so we know that if we get here, the data received is for a stream that
// we know is for an HTTP payload.
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
auto& session = app.session();
if (auto stream = FindOrCreateStream(conn, &session, stream_id))
[[likely]] {
stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{});
return NGTCP2_SUCCESS;
}
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
static int on_deferred_consume(nghttp3_conn* conn,
@ -674,9 +823,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnDeferredConsume(stream, consumed);
auto& session = app.session();
Debug(&session, "HTTP/3 application deferred consume %zu bytes", consumed);
session.ExtendStreamOffset(stream_id, consumed);
session.ExtendOffset(consumed);
return NGTCP2_SUCCESS;
}
@ -685,9 +835,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnBeginHeaders(stream);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnBeginHeaders(stream_id);
return NGTCP2_SUCCESS;
}
@ -700,11 +851,12 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS;
app->OnReceiveHeader(stream,
Http3Header(app->env(), token, name, value, flags));
app.OnReceiveHeader(stream_id,
Http3Header(app.env(), token, name, value, flags));
return NGTCP2_SUCCESS;
}
@ -714,9 +866,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnEndHeaders(stream, fin);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnEndHeaders(stream_id, fin);
return NGTCP2_SUCCESS;
}
@ -725,9 +878,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnBeginTrailers(stream);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnBeginTrailers(stream_id);
return NGTCP2_SUCCESS;
}
@ -740,11 +894,12 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS;
app->OnReceiveTrailer(stream,
Http3Header(app->env(), token, name, value, flags));
app.OnReceiveTrailer(stream_id,
Http3Header(app.env(), token, name, value, flags));
return NGTCP2_SUCCESS;
}
@ -754,9 +909,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnEndTrailers(stream, fin);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnEndTrailers(stream_id, fin);
return NGTCP2_SUCCESS;
}
@ -765,9 +921,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnEndStream(stream);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnEndStream(stream_id);
return NGTCP2_SUCCESS;
}
@ -777,9 +934,10 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnStopSending(stream, app_error_code);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnStopSending(stream_id, app_error_code);
return NGTCP2_SUCCESS;
}
@ -789,15 +947,16 @@ class Http3Application final : public Session::Application {
void* conn_user_data,
void* stream_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
auto stream = From(stream_id, stream_user_data);
if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE;
app->OnResetStream(stream, app_error_code);
if (app.is_control_stream(stream_id)) [[unlikely]] {
return NGHTTP3_ERR_CALLBACK_FAILURE;
}
app.OnResetStream(stream_id, app_error_code);
return NGTCP2_SUCCESS;
}
static int on_shutdown(nghttp3_conn* conn, int64_t id, void* conn_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
app->OnShutdown();
app.OnShutdown();
return NGTCP2_SUCCESS;
}
@ -805,7 +964,7 @@ class Http3Application final : public Session::Application {
const nghttp3_settings* settings,
void* conn_user_data) {
NGHTTP3_CALLBACK_SCOPE(app);
app->OnReceiveSettings(settings);
app.OnReceiveSettings(settings);
return NGTCP2_SUCCESS;
}
@ -825,13 +984,14 @@ class Http3Application final : public Session::Application {
on_shutdown,
on_receive_settings};
};
} // namespace
std::unique_ptr<Session::Application> createHttp3Application(
Session* session, const Session::Application_Options& options) {
return std::make_unique<Http3Application>(session, options);
std::unique_ptr<Session::Application> Http3Application::Create(
Session* session) {
Debug(session, "Selecting HTTP/3 application");
return std::make_unique<Http3ApplicationImpl>(session, options_);
}
} // namespace node::quic
} // namespace quic
} // namespace node
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

View File

@ -3,11 +3,40 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include <base_object.h>
#include <env.h>
#include <memory_tracker.h>
#include "session.h"
namespace node::quic {
std::unique_ptr<Session::Application> createHttp3Application(
Session* session, const Session::Application_Options& options);
// Provides an implementation of the HTTP/3 Application implementation
class Http3Application final : public Session::ApplicationProvider {
public:
static bool HasInstance(Environment* env, v8::Local<v8::Value> value);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static void InitPerIsolate(IsolateData* isolate_data,
v8::Local<v8::ObjectTemplate> target);
static void InitPerContext(Realm* realm, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
Http3Application(Environment* env,
v8::Local<v8::Object> object,
const Session::Application_Options& options);
std::unique_ptr<Session::Application> Create(Session* session) override;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_SELF_SIZE(Http3Application)
SET_MEMORY_INFO_NAME(Http3Application)
std::string ToString() const;
private:
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
Session::Application_Options options_;
};
} // namespace node::quic

View File

@ -40,7 +40,7 @@ BaseObjectPtr<LogStream> LogStream::Create(Environment* env) {
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return BaseObjectPtr<LogStream>();
return {};
}
return MakeDetachedBaseObject<LogStream>(env, obj);
}

View File

@ -110,21 +110,21 @@ Local<FunctionTemplate> Packet::GetConstructorTemplate(Environment* env) {
return tmpl;
}
Packet* Packet::Create(Environment* env,
Listener* listener,
const SocketAddress& destination,
size_t length,
const char* diagnostic_label) {
BaseObjectPtr<Packet> Packet::Create(Environment* env,
Listener* listener,
const SocketAddress& destination,
size_t length,
const char* diagnostic_label) {
if (BindingData::Get(env).packet_freelist.empty()) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) [[unlikely]] {
return nullptr;
return {};
}
return new Packet(
return MakeBaseObject<Packet>(
env, listener, obj, destination, length, diagnostic_label);
}
@ -134,7 +134,7 @@ Packet* Packet::Create(Environment* env,
destination);
}
Packet* Packet::Clone() const {
BaseObjectPtr<Packet> Packet::Clone() const {
auto& binding = BindingData::Get(env());
if (binding.packet_freelist.empty()) {
Local<Object> obj;
@ -142,26 +142,27 @@ Packet* Packet::Clone() const {
->InstanceTemplate()
->NewInstance(env()->context())
.ToLocal(&obj)) [[unlikely]] {
return nullptr;
return {};
}
return new Packet(env(), listener_, obj, destination_, data_);
return MakeBaseObject<Packet>(env(), listener_, obj, destination_, data_);
}
return FromFreeList(env(), data_, listener_, destination_);
}
Packet* Packet::FromFreeList(Environment* env,
std::shared_ptr<Data> data,
Listener* listener,
const SocketAddress& destination) {
BaseObjectPtr<Packet> Packet::FromFreeList(Environment* env,
std::shared_ptr<Data> data,
Listener* listener,
const SocketAddress& destination) {
auto& binding = BindingData::Get(env);
if (binding.packet_freelist.empty()) return nullptr;
Packet* packet = binding.packet_freelist.back();
if (binding.packet_freelist.empty()) return {};
auto obj = binding.packet_freelist.back();
binding.packet_freelist.pop_back();
CHECK_NOT_NULL(packet);
CHECK_EQ(env, packet->env());
Debug(packet, "Reusing packet from freelist");
CHECK(obj);
CHECK_EQ(env, obj->env());
auto packet = BaseObjectPtr<Packet>(static_cast<Packet*>(obj.get()));
Debug(packet.get(), "Reusing packet from freelist");
packet->data_ = std::move(data);
packet->destination_ = destination;
packet->listener_ = listener;
@ -195,23 +196,25 @@ Packet::Packet(Environment* env,
void Packet::Done(int status) {
Debug(this, "Packet is done with status %d", status);
if (listener_ != nullptr) {
BaseObjectPtr<Packet> self(this);
self->MakeWeak();
if (listener_ != nullptr && IsDispatched()) {
listener_->PacketDone(status);
}
// As a performance optimization, we add this packet to a freelist
// rather than deleting it but only if the freelist isn't too
// big, we don't want to accumulate these things forever.
auto& binding = BindingData::Get(env());
if (binding.packet_freelist.size() < kMaxFreeList) {
Debug(this, "Returning packet to freelist");
listener_ = nullptr;
data_.reset();
Reset();
binding.packet_freelist.push_back(this);
} else {
delete this;
if (binding.packet_freelist.size() >= kMaxFreeList) {
return;
}
Debug(this, "Returning packet to freelist");
listener_ = nullptr;
data_.reset();
Reset();
binding.packet_freelist.push_back(std::move(self));
}
std::string Packet::ToString() const {
@ -224,10 +227,11 @@ void Packet::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("data", data_);
}
Packet* Packet::CreateRetryPacket(Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const TokenSecret& token_secret) {
BaseObjectPtr<Packet> Packet::CreateRetryPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const TokenSecret& token_secret) {
auto& random = CID::Factory::random();
CID cid = random.Generate();
RetryToken token(path_descriptor.version,
@ -235,7 +239,7 @@ Packet* Packet::CreateRetryPacket(Environment* env,
cid,
path_descriptor.dcid,
token_secret);
if (!token) return nullptr;
if (!token) return {};
const ngtcp2_vec& vec = token;
@ -244,7 +248,7 @@ Packet* Packet::CreateRetryPacket(Environment* env,
auto packet =
Create(env, listener, path_descriptor.remote_address, pktlen, "retry");
if (packet == nullptr) return nullptr;
if (!packet) return packet;
ngtcp2_vec dest = *packet;
@ -258,33 +262,34 @@ Packet* Packet::CreateRetryPacket(Environment* env,
vec.len);
if (nwrite <= 0) {
packet->Done(UV_ECANCELED);
return nullptr;
return {};
}
packet->Truncate(static_cast<size_t>(nwrite));
return packet;
}
Packet* Packet::CreateConnectionClosePacket(Environment* env,
Listener* listener,
const SocketAddress& destination,
ngtcp2_conn* conn,
const QuicError& error) {
BaseObjectPtr<Packet> Packet::CreateConnectionClosePacket(
Environment* env,
Listener* listener,
const SocketAddress& destination,
ngtcp2_conn* conn,
const QuicError& error) {
auto packet = Create(
env, listener, destination, kDefaultMaxPacketLength, "connection close");
if (packet == nullptr) return nullptr;
if (!packet) return packet;
ngtcp2_vec vec = *packet;
ssize_t nwrite = ngtcp2_conn_write_connection_close(
conn, nullptr, nullptr, vec.base, vec.len, error, uv_hrtime());
if (nwrite < 0) {
packet->Done(UV_ECANCELED);
return nullptr;
return {};
}
packet->Truncate(static_cast<size_t>(nwrite));
return packet;
}
Packet* Packet::CreateImmediateConnectionClosePacket(
BaseObjectPtr<Packet> Packet::CreateImmediateConnectionClosePacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
@ -294,7 +299,7 @@ Packet* Packet::CreateImmediateConnectionClosePacket(
path_descriptor.remote_address,
kDefaultMaxPacketLength,
"immediate connection close (endpoint)");
if (packet == nullptr) return nullptr;
if (!packet) return packet;
ngtcp2_vec vec = *packet;
ssize_t nwrite = ngtcp2_crypto_write_connection_close(
vec.base,
@ -309,13 +314,13 @@ Packet* Packet::CreateImmediateConnectionClosePacket(
0);
if (nwrite <= 0) {
packet->Done(UV_ECANCELED);
return nullptr;
return {};
}
packet->Truncate(static_cast<size_t>(nwrite));
return packet;
}
Packet* Packet::CreateStatelessResetPacket(
BaseObjectPtr<Packet> Packet::CreateStatelessResetPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
@ -328,7 +333,7 @@ Packet* Packet::CreateStatelessResetPacket(
// QUIC spec. The reason is that packets less than 41 bytes may allow an
// observer to reliably determine that it's a stateless reset.
size_t pktlen = source_len - 1;
if (pktlen < kMinStatelessResetLen) return nullptr;
if (pktlen < kMinStatelessResetLen) return {};
StatelessResetToken token(token_secret, path_descriptor.dcid);
uint8_t random[kRandlen];
@ -339,21 +344,21 @@ Packet* Packet::CreateStatelessResetPacket(
path_descriptor.remote_address,
kDefaultMaxPacketLength,
"stateless reset");
if (packet == nullptr) return nullptr;
if (!packet) return packet;
ngtcp2_vec vec = *packet;
ssize_t nwrite = ngtcp2_pkt_write_stateless_reset(
vec.base, pktlen, token, random, kRandlen);
if (nwrite <= static_cast<ssize_t>(kMinStatelessResetLen)) {
packet->Done(UV_ECANCELED);
return nullptr;
return {};
}
packet->Truncate(static_cast<size_t>(nwrite));
return packet;
}
Packet* Packet::CreateVersionNegotiationPacket(
BaseObjectPtr<Packet> Packet::CreateVersionNegotiationPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor) {
@ -389,7 +394,7 @@ Packet* Packet::CreateVersionNegotiationPacket(
path_descriptor.remote_address,
kDefaultMaxPacketLength,
"version negotiation");
if (packet == nullptr) return nullptr;
if (!packet) return packet;
ngtcp2_vec vec = *packet;
ssize_t nwrite =
@ -404,7 +409,7 @@ Packet* Packet::CreateVersionNegotiationPacket(
arraysize(sv));
if (nwrite <= 0) {
packet->Done(UV_ECANCELED);
return nullptr;
return {};
}
packet->Truncate(static_cast<size_t>(nwrite));
return packet;

View File

@ -89,13 +89,14 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
// tells us how many of the packets bytes were used.
void Truncate(size_t len);
static Packet* Create(Environment* env,
Listener* listener,
const SocketAddress& destination,
size_t length = kDefaultMaxPacketLength,
const char* diagnostic_label = "<unknown>");
static BaseObjectPtr<Packet> Create(
Environment* env,
Listener* listener,
const SocketAddress& destination,
size_t length = kDefaultMaxPacketLength,
const char* diagnostic_label = "<unknown>");
Packet* Clone() const;
BaseObjectPtr<Packet> Clone() const;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(Packet)
@ -103,31 +104,33 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
std::string ToString() const;
static Packet* CreateRetryPacket(Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const TokenSecret& token_secret);
static BaseObjectPtr<Packet> CreateRetryPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const TokenSecret& token_secret);
static Packet* CreateConnectionClosePacket(Environment* env,
Listener* listener,
const SocketAddress& destination,
ngtcp2_conn* conn,
const QuicError& error);
static BaseObjectPtr<Packet> CreateConnectionClosePacket(
Environment* env,
Listener* listener,
const SocketAddress& destination,
ngtcp2_conn* conn,
const QuicError& error);
static Packet* CreateImmediateConnectionClosePacket(
static BaseObjectPtr<Packet> CreateImmediateConnectionClosePacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const QuicError& reason);
static Packet* CreateStatelessResetPacket(
static BaseObjectPtr<Packet> CreateStatelessResetPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor,
const TokenSecret& token_secret,
size_t source_len);
static Packet* CreateVersionNegotiationPacket(
static BaseObjectPtr<Packet> CreateVersionNegotiationPacket(
Environment* env,
Listener* listener,
const PathDescriptor& path_descriptor);
@ -136,10 +139,10 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
void Done(int status);
private:
static Packet* FromFreeList(Environment* env,
std::shared_ptr<Data> data,
Listener* listener,
const SocketAddress& destination);
static BaseObjectPtr<Packet> FromFreeList(Environment* env,
std::shared_ptr<Data> data,
Listener* listener,
const SocketAddress& destination);
Listener* listener_;
SocketAddress destination_;

View File

@ -26,6 +26,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Endpoint::InitPerIsolate(isolate_data, target);
Session::InitPerIsolate(isolate_data, target);
Stream::InitPerIsolate(isolate_data, target);
}
void CreatePerContextProperties(Local<Object> target,
@ -36,12 +37,14 @@ void CreatePerContextProperties(Local<Object> target,
BindingData::InitPerContext(realm, target);
Endpoint::InitPerContext(realm, target);
Session::InitPerContext(realm, target);
Stream::InitPerContext(realm, target);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
BindingData::RegisterExternalReferences(registry);
Endpoint::RegisterExternalReferences(registry);
Session::RegisterExternalReferences(registry);
Stream::RegisterExternalReferences(registry);
}
} // namespace quic

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,13 @@ class Endpoint;
// secure the communication. Once those keys are established, the Session can be
// used to open Streams. Based on how the Session is configured, any number of
// Streams can exist concurrently on a single Session.
//
// The Session wraps an ngtcp2_conn that is initialized when the session object
// is created. This ngtcp2_conn is destroyed when the session object is freed.
// However, the session can be in a closed/destroyed state and still have a
// valid ngtcp2_conn pointer. This is important because the ngtcp2 still might
// be processsing data within the scope of an ngtcp2_conn after the session
// object itself is closed/destroyed by user code.
class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
public:
// For simplicity, we use the same Application::Options struct for all
@ -92,6 +99,17 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
// of a QUIC Session.
class Application;
// The ApplicationProvider optionally supplies the underlying application
// protocol handler used by a session. The ApplicationProvider is supplied
// in the *internal* options (that is, it is not exposed as a public, user
// facing API. If the ApplicationProvider is not specified, then the
// DefaultApplication is used (see application.cc).
class ApplicationProvider : public BaseObject {
public:
using BaseObject::BaseObject;
virtual std::unique_ptr<Application> Create(Session* session) = 0;
};
// 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.
@ -102,26 +120,63 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
// Te minimum QUIC protocol version supported by this session.
uint32_t min_version = NGTCP2_PROTO_VER_MIN;
// By default a client session will use the preferred address advertised by
// the the server. This option is only relevant for client sessions.
// By default a client session will ignore the preferred address
// advertised by the the server. This option is only relevant for
// client sessions.
PreferredAddress::Policy preferred_address_strategy =
PreferredAddress::Policy::USE_PREFERRED;
PreferredAddress::Policy::IGNORE_PREFERRED;
TransportParams::Options transport_params =
TransportParams::Options::kDefault;
TLSContext::Options tls_options = TLSContext::Options::kDefault;
Application_Options application_options = Application_Options::kDefault;
// A reference to the CID::Factory used to generate CID instances
// for this session.
const CID::Factory* cid_factory = &CID::Factory::random();
// If the CID::Factory is a base object, we keep a reference to it
// so that it cannot be garbage collected.
BaseObjectPtr<BaseObject> cid_factory_ref = BaseObjectPtr<BaseObject>();
BaseObjectPtr<BaseObject> cid_factory_ref = {};
// If the application provider is specified, it will be used to create
// the underlying Application instance for the session.
BaseObjectPtr<ApplicationProvider> application_provider = {};
// When true, QLog output will be enabled for the session.
bool qlog = false;
// The amount of time (in milliseconds) that the endpoint will wait for the
// completion of the tls handshake.
uint64_t handshake_timeout = UINT64_MAX;
// Maximum initial flow control window size for a stream.
uint64_t max_stream_window = 0;
// Maximum initial flow control window size for the connection.
uint64_t max_window = 0;
// The max_payload_size is the maximum size of a serialized QUIC packet. It
// should always be set small enough to fit within a single MTU without
// fragmentation. The default is set by the QUIC specification at 1200. This
// value should not be changed unless you know for sure that the entire path
// supports a given MTU without fragmenting at any point in the path.
uint64_t max_payload_size = kDefaultMaxPacketLength;
// The unacknowledged_packet_threshold is the maximum number of
// unacknowledged packets that an ngtcp2 session will accumulate before
// sending an acknowledgement. Setting this to 0 uses the ngtcp2 defaults,
// which is what most will want. The value can be changed to fine tune some
// of the performance characteristics of the session. This should only be
// changed if you have a really good reason for doing so.
uint64_t unacknowledged_packet_threshold = 0;
// There are several common congestion control algorithms that ngtcp2 uses
// to determine how it manages the flow control window: RENO, CUBIC, and
// BBR. The details of how each works is not relevant here. The choice of
// which to use by default is arbitrary and we can choose whichever we'd
// like. Additional performance profiling will be needed to determine which
// is the better of the two for our needs.
ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(Session::Options)
SET_SELF_SIZE(Options)
@ -167,8 +222,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
operator ngtcp2_settings*() { return &settings; }
operator const ngtcp2_settings*() const { return &settings; }
Config(Side side,
const Endpoint& endpoint,
Config(Environment* env,
Side side,
const Options& options,
uint32_t version,
const SocketAddress& local_address,
@ -177,7 +232,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
const CID& scid,
const CID& ocid = CID::kInvalid);
Config(const Endpoint& endpoint,
Config(Environment* env,
const Options& options,
const SocketAddress& local_address,
const SocketAddress& remote_address,
@ -216,115 +271,113 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
const Config& config,
TLSContext* tls_context,
const std::optional<SessionTicket>& ticket);
DISALLOW_COPY_AND_MOVE(Session)
~Session() override;
bool is_destroyed() const;
bool is_server() const;
uint32_t version() const;
Endpoint& endpoint() const;
TLSSession& tls_session();
Application& application();
TLSSession& tls_session() const;
Application& application() const;
const Config& config() const;
const Options& options() const;
const SocketAddress& remote_address() const;
const SocketAddress& local_address() const;
bool is_closing() const;
bool is_graceful_closing() const;
bool is_silent_closing() const;
bool is_destroyed() const;
bool is_server() const;
size_t max_packet_size() const;
void set_priority_supported(bool on = true);
std::string diagnostic_name() const override;
// Use the configured CID::Factory to generate a new CID.
CID new_cid(size_t len = CID::kMaxLength) const;
void HandleQlog(uint32_t flags, const void* data, size_t len);
TransportParams GetLocalTransportParams() const;
TransportParams GetRemoteTransportParams() const;
void UpdatePacketTxTime();
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(Session)
SET_SELF_SIZE(Session)
struct State;
struct Stats;
operator ngtcp2_conn*() const;
BaseObjectPtr<Stream> FindStream(int64_t id) const;
BaseObjectPtr<Stream> CreateStream(int64_t id);
BaseObjectPtr<Stream> OpenStream(Direction direction);
void ExtendStreamOffset(int64_t id, size_t amount);
void ExtendOffset(size_t amount);
void SetLastError(QuicError&& error);
uint64_t max_data_left() const;
enum class CloseMethod {
// Roundtrip through JavaScript, causing all currently opened streams
// to be closed. An attempt will be made to send a CONNECTION_CLOSE
// frame to the peer. If closing while within the ngtcp2 callback scope,
// sending the CONNECTION_CLOSE will be deferred until the scope exits.
DEFAULT,
// The connected peer will not be notified.
SILENT,
// Closing gracefully disables the ability to open or accept new streams for
// this Session. Existing streams are allowed to close naturally on their
// own.
// Once called, the Session will be immediately closed once there are no
// remaining streams. No notification is given to the connected peer that we
// are in a graceful closing state. A CONNECTION_CLOSE will be sent only
// once
// Close() is called.
GRACEFUL
};
void Close(CloseMethod method = CloseMethod::DEFAULT);
struct SendPendingDataScope {
// Ensures that the session/application sends pending data when the scope
// exits. Scopes can be nested. When nested, pending data will be sent
// only when the outermost scope is exited.
struct SendPendingDataScope final {
Session* session;
explicit SendPendingDataScope(Session* session);
explicit SendPendingDataScope(const BaseObjectPtr<Session>& session);
DISALLOW_COPY_AND_MOVE(SendPendingDataScope)
~SendPendingDataScope();
DISALLOW_COPY_AND_MOVE(SendPendingDataScope)
};
struct State;
struct Stats;
void HandleQlog(uint32_t flags, const void* data, size_t len);
private:
struct Impl;
struct MaybeCloseConnectionScope;
using StreamsMap = std::unordered_map<int64_t, BaseObjectPtr<Stream>>;
using QuicConnectionPointer = DeleteFnPtr<ngtcp2_conn, ngtcp2_conn_del>;
struct PathValidationFlags {
struct PathValidationFlags final {
bool preferredAddress = false;
};
struct DatagramReceivedFlags {
struct DatagramReceivedFlags final {
bool early = false;
};
void Destroy();
bool Receive(Store&& store,
const SocketAddress& local_address,
const SocketAddress& remote_address);
void Send(Packet* packet);
void Send(Packet* packet, const PathStorage& path);
void Send(const BaseObjectPtr<Packet>& packet);
void Send(const BaseObjectPtr<Packet>& packet, const PathStorage& path);
uint64_t SendDatagram(Store&& data);
void AddStream(const BaseObjectPtr<Stream>& stream);
// A non-const variation to allow certain modifications.
Config& config();
enum class CreateStreamOption {
NOTIFY,
DO_NOT_NOTIFY,
};
BaseObjectPtr<Stream> FindStream(int64_t id) const;
BaseObjectPtr<Stream> CreateStream(
int64_t id,
CreateStreamOption option = CreateStreamOption::NOTIFY,
std::shared_ptr<DataQueue> data_source = nullptr);
void AddStream(BaseObjectPtr<Stream> stream,
CreateStreamOption option = CreateStreamOption::NOTIFY);
void RemoveStream(int64_t id);
void ResumeStream(int64_t id);
void ShutdownStream(int64_t id, QuicError error);
void StreamDataBlocked(int64_t id);
void ShutdownStream(int64_t id, QuicError error = QuicError());
void ShutdownStreamWrite(int64_t id, QuicError code = QuicError());
// Use the configured CID::Factory to generate a new CID.
CID new_cid(size_t len = CID::kMaxLength) const;
const TransportParams local_transport_params() const;
const TransportParams remote_transport_params() const;
bool is_destroyed_or_closing() const;
size_t max_packet_size() const;
void set_priority_supported(bool on = true);
// Open a new locally-initialized stream with the specified directionality.
// If the session is not yet in a state where the stream can be openen --
// such as when the handshake is not yet sufficiently far along and ORTT
// session resumption is not being used -- then the stream will be created
// in a pending state where actually opening the stream will be deferred.
v8::MaybeLocal<v8::Object> OpenStream(
Direction direction, std::shared_ptr<DataQueue> data_source = nullptr);
void ExtendStreamOffset(int64_t id, size_t amount);
void ExtendOffset(size_t amount);
void SetLastError(QuicError&& error);
uint64_t max_data_left() const;
PendingStream::PendingStreamQueue& pending_bidi_stream_queue() const;
PendingStream::PendingStreamQueue& pending_uni_stream_queue() const;
// Implementation of SessionTicket::AppData::Source
void CollectSessionTicketAppData(
SessionTicket::AppData* app_data) const override;
@ -349,8 +402,17 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
bool can_send_packets() const;
// Returns false if the Session is currently in a state where it cannot create
// new streams.
// new streams. Specifically, a stream is not in a state to create streams if
// it has been destroyed or is closing.
bool can_create_streams() const;
// Returns false if the Session is currently in a state where it cannot open
// a new locally-initiated stream. When using 0RTT session resumption, this
// will become true immediately after the session ticket and transport params
// have been configured. Otherwise, it becomes true after the remote transport
// params and tx keys have been installed.
bool can_open_streams() const;
uint64_t max_local_streams_uni() const;
uint64_t max_local_streams_bidi() const;
@ -362,12 +424,46 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
// defined there to manage it.
void set_wrapped();
void DoClose(bool silent = false);
void UpdateDataStats();
enum class CloseMethod {
// Immediate close with a roundtrip through JavaScript, causing all
// currently opened streams to be closed. An attempt will be made to
// send a CONNECTION_CLOSE frame to the peer. If closing while within
// the ngtcp2 callback scope, sending the CONNECTION_CLOSE will be
// deferred until the scope exits.
DEFAULT,
// Same as DEFAULT except that no attempt to notify the peer will be
// made.
SILENT,
// Closing gracefully disables the ability to open or accept new streams
// for this Session. Existing streams are allowed to close naturally on
// their own.
// Once called, the Session will be immediately closed once there are no
// remaining streams. No notification is given to the connected peer that
// we are in a graceful closing state. A CONNECTION_CLOSE will be sent
// only once FinishClose() is called.
GRACEFUL
};
// Initiate closing of the session.
void Close(CloseMethod method = CloseMethod::DEFAULT);
void FinishClose();
void Destroy();
// Close the session and send a connection close packet to the peer.
// If creating the packet fails the session will be silently closed.
// The connection close packet will use the value of last_error_ as
// the error code transmitted to the peer.
void SendConnectionClose();
void OnTimeout();
void UpdateTimer();
bool StartClosingPeriod();
// Has to be called after certain operations that generate packets.
void UpdatePacketTxTime();
void UpdateDataStats();
void UpdatePath(const PathStorage& path);
void ProcessPendingBidiStreams();
void ProcessPendingUniStreams();
// JavaScript callouts
@ -387,54 +483,43 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
const ValidatedPath& newPath,
const std::optional<ValidatedPath>& oldPath);
void EmitSessionTicket(Store&& ticket);
void EmitStream(BaseObjectPtr<Stream> stream);
void EmitStream(const BaseObjectWeakPtr<Stream>& stream);
void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
const uint32_t* sv,
size_t nsv);
void DatagramStatus(uint64_t datagramId, DatagramStatus status);
void DatagramReceived(const uint8_t* data,
size_t datalen,
DatagramReceivedFlags flag);
bool GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token);
void GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token);
bool HandshakeCompleted();
void HandshakeConfirmed();
void SelectPreferredAddress(PreferredAddress* preferredAddress);
void UpdatePath(const PathStorage& path);
static std::unique_ptr<Application> SelectApplication(Session* session,
const Config& config);
QuicConnectionPointer InitConnection();
std::unique_ptr<Application> select_application();
AliasedStruct<Stats> stats_;
AliasedStruct<State> state_;
Side side_;
ngtcp2_mem allocator_;
BaseObjectWeakPtr<Endpoint> endpoint_;
Config config_;
SocketAddress local_address_;
SocketAddress remote_address_;
std::unique_ptr<Impl> impl_;
QuicConnectionPointer connection_;
std::unique_ptr<TLSSession> tls_session_;
std::unique_ptr<Application> application_;
StreamsMap streams_;
TimerWrapHandle timer_;
size_t send_scope_depth_ = 0;
size_t connection_close_depth_ = 0;
QuicError last_error_;
Packet* conn_closebuf_;
BaseObjectPtr<LogStream> qlog_stream_;
BaseObjectPtr<LogStream> keylog_stream_;
friend class Application;
friend class DefaultApplication;
friend class Http3ApplicationImpl;
friend class Endpoint;
friend struct Impl;
friend struct MaybeCloseConnectionScope;
friend struct SendPendingDataScope;
friend class Stream;
friend class PendingStream;
friend class TLSContext;
friend class TLSSession;
friend class TransportParams;
friend struct Impl;
friend struct SendPendingDataScope;
};
} // namespace node::quic

View File

@ -155,9 +155,8 @@ std::optional<const uv_buf_t> SessionTicket::AppData::Get() const {
}
void SessionTicket::AppData::Collect(SSL* ssl) {
auto source = GetAppDataSource(ssl);
if (source != nullptr) {
SessionTicket::AppData app_data(ssl);
SessionTicket::AppData app_data(ssl);
if (auto source = GetAppDataSource(ssl)) {
source->CollectSessionTicketAppData(&app_data);
}
}

View File

@ -21,12 +21,14 @@ using v8::ArrayBufferView;
using v8::BigInt;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Integer;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::Nothing;
using v8::Object;
using v8::ObjectTemplate;
using v8::PropertyAttribute;
using v8::SharedArrayBuffer;
using v8::Uint32;
@ -36,13 +38,14 @@ namespace quic {
#define STREAM_STATE(V) \
V(ID, id, int64_t) \
V(PENDING, pending, uint8_t) \
V(FIN_SENT, fin_sent, uint8_t) \
V(FIN_RECEIVED, fin_received, uint8_t) \
V(READ_ENDED, read_ended, uint8_t) \
V(WRITE_ENDED, write_ended, uint8_t) \
V(DESTROYED, destroyed, uint8_t) \
V(PAUSED, paused, uint8_t) \
V(RESET, reset, uint8_t) \
V(HAS_OUTBOUND, has_outbound, uint8_t) \
V(HAS_READER, has_reader, uint8_t) \
/* Set when the stream has a block event handler */ \
V(WANTS_BLOCK, wants_block, uint8_t) \
@ -54,12 +57,20 @@ namespace quic {
V(WANTS_TRAILERS, wants_trailers, uint8_t)
#define STREAM_STATS(V) \
/* Marks the timestamp when the stream object was created. */ \
V(CREATED_AT, created_at) \
/* Marks the timestamp when the stream was opened. This can be different */ \
/* from the created_at timestamp if the stream was created in as pending */ \
V(OPENED_AT, opened_at) \
/* Marks the timestamp when the stream last received data */ \
V(RECEIVED_AT, received_at) \
/* Marks the timestamp when the stream last received an acknowledgement */ \
V(ACKED_AT, acked_at) \
V(CLOSING_AT, closing_at) \
/* Marks the timestamp when the stream was destroyed */ \
V(DESTROYED_AT, destroyed_at) \
/* Records the total number of bytes receied by the stream */ \
V(BYTES_RECEIVED, bytes_received) \
/* Records the total number of bytes sent by the stream */ \
V(BYTES_SENT, bytes_sent) \
V(MAX_OFFSET, max_offset) \
V(MAX_OFFSET_ACK, max_offset_ack) \
@ -76,6 +87,53 @@ namespace quic {
V(GetPriority, getPriority, true) \
V(GetReader, getReader, false)
// ============================================================================
PendingStream::PendingStream(Direction direction,
Stream* stream,
BaseObjectWeakPtr<Session> session)
: direction_(direction), stream_(stream), session_(session) {
if (session_) {
if (direction == Direction::BIDIRECTIONAL) {
session_->pending_bidi_stream_queue().PushBack(this);
} else {
session_->pending_uni_stream_queue().PushBack(this);
}
}
}
PendingStream::~PendingStream() {
pending_stream_queue_.Remove();
if (waiting_) {
Debug(stream_, "A pending stream was canceled");
}
}
void PendingStream::fulfill(int64_t id) {
CHECK(waiting_);
waiting_ = false;
stream_->NotifyStreamOpened(id);
}
void PendingStream::reject(QuicError error) {
CHECK(waiting_);
waiting_ = false;
stream_->Destroy(error);
}
struct Stream::PendingHeaders {
HeadersKind kind;
v8::Global<v8::Array> headers;
HeadersFlags flags;
PendingHeaders(HeadersKind kind_,
v8::Global<v8::Array> headers_,
HeadersFlags flags_)
: kind(kind_), headers(std::move(headers_)), flags(flags_) {}
DISALLOW_COPY_AND_MOVE(PendingHeaders)
};
// ============================================================================
struct Stream::State {
#define V(_, name, type) type name;
STREAM_STATE(V)
@ -86,28 +144,30 @@ STAT_STRUCT(Stream, STREAM)
// ============================================================================
namespace {
Maybe<std::shared_ptr<DataQueue>> GetDataQueueFromSource(Environment* env,
Local<Value> value) {
Maybe<std::shared_ptr<DataQueue>> Stream::GetDataQueueFromSource(
Environment* env, Local<Value> value) {
DCHECK_IMPLIES(!value->IsUndefined(), value->IsObject());
std::vector<std::unique_ptr<DataQueue::Entry>> entries;
if (value->IsUndefined()) {
return Just(std::shared_ptr<DataQueue>());
} else if (value->IsArrayBuffer()) {
auto buffer = value.As<ArrayBuffer>();
std::vector<std::unique_ptr<DataQueue::Entry>> entries(1);
entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore(
buffer->GetBackingStore(), 0, buffer->ByteLength()));
return Just(DataQueue::CreateIdempotent(std::move(entries)));
} else if (value->IsSharedArrayBuffer()) {
auto buffer = value.As<SharedArrayBuffer>();
std::vector<std::unique_ptr<DataQueue::Entry>> entries(1);
entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore(
buffer->GetBackingStore(), 0, buffer->ByteLength()));
return Just(DataQueue::CreateIdempotent(std::move(entries)));
} else if (value->IsArrayBufferView()) {
std::vector<std::unique_ptr<DataQueue::Entry>> entries(1);
entries.push_back(
DataQueue::CreateInMemoryEntryFromView(value.As<ArrayBufferView>()));
auto entry =
DataQueue::CreateInMemoryEntryFromView(value.As<ArrayBufferView>());
if (!entry) {
THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable");
return Nothing<std::shared_ptr<DataQueue>>();
}
entries.push_back(std::move(entry));
return Just(DataQueue::CreateIdempotent(std::move(entries)));
} else if (Blob::HasInstance(env, value)) {
Blob* blob;
@ -119,9 +179,11 @@ Maybe<std::shared_ptr<DataQueue>> GetDataQueueFromSource(Environment* env,
THROW_ERR_INVALID_ARG_TYPE(env, "Invalid data source type");
return Nothing<std::shared_ptr<DataQueue>>();
}
} // namespace
// Provides the implementation of the various JavaScript APIs for the
// Stream object.
struct Stream::Impl {
// Attaches an outbound data source to the stream.
static void AttachSource(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@ -158,7 +220,13 @@ struct Stream::Impl {
HeadersFlags flags =
static_cast<HeadersFlags>(args[2].As<Uint32>()->Value());
if (stream->is_destroyed()) return args.GetReturnValue().Set(false);
// 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.
if (stream->is_pending()) {
stream->EnqueuePendingHeaders(kind, headers, flags);
return args.GetReturnValue().Set(true);
}
args.GetReturnValue().Set(stream->session().application().SendHeaders(
*stream, kind, headers, flags));
@ -173,14 +241,19 @@ struct Stream::Impl {
uint64_t code = NGTCP2_APP_NOERROR;
CHECK_IMPLIES(!args[0]->IsUndefined(), args[0]->IsBigInt());
if (!args[0]->IsUndefined()) {
bool lossless = false; // not used but still necessary.
code = args[0].As<BigInt>()->Uint64Value(&lossless);
bool unused = false; // not used but still necessary.
code = args[0].As<BigInt>()->Uint64Value(&unused);
}
if (stream->is_destroyed()) return;
stream->EndReadable();
Session::SendPendingDataScope send_scope(&stream->session());
ngtcp2_conn_shutdown_stream_read(stream->session(), 0, stream->id(), code);
if (!stream->is_pending()) {
// If the stream is a local unidirectional there's nothing to do here.
if (stream->is_local_unidirectional()) return;
stream->NotifyReadableEnded(code);
} else {
stream->pending_close_read_code_ = code;
}
}
// Sends a reset stream to the peer to tell it we will not be sending any
@ -197,15 +270,21 @@ struct Stream::Impl {
code = args[0].As<BigInt>()->Uint64Value(&lossless);
}
if (stream->is_destroyed() || stream->state_->reset == 1) return;
if (stream->state_->reset == 1) return;
stream->EndWritable();
// We can release our outbound here now. Since the stream is being reset
// on the ngtcp2 side, we do not need to keep any of the data around
// waiting for acknowledgement that will never come.
stream->outbound_.reset();
stream->state_->reset = 1;
Session::SendPendingDataScope send_scope(&stream->session());
ngtcp2_conn_shutdown_stream_write(stream->session(), 0, stream->id(), code);
if (!stream->is_pending()) {
if (stream->is_remote_unidirectional()) return;
stream->NotifyWritableEnded(code);
} else {
stream->pending_close_write_code_ = code;
}
}
static void SetPriority(const FunctionCallbackInfo<Value>& args) {
@ -219,12 +298,26 @@ struct Stream::Impl {
StreamPriorityFlags flags =
static_cast<StreamPriorityFlags>(args[1].As<Uint32>()->Value());
stream->session().application().SetStreamPriority(*stream, priority, flags);
if (stream->is_pending()) {
stream->pending_priority_ = Stream::PendingPriority{
.priority = priority,
.flags = flags,
};
} else {
stream->session().application().SetStreamPriority(
*stream, priority, flags);
}
}
static void GetPriority(const FunctionCallbackInfo<Value>& args) {
Stream* stream;
ASSIGN_OR_RETURN_UNWRAP(&stream, args.This());
if (stream->is_pending()) {
return args.GetReturnValue().Set(
static_cast<uint32_t>(StreamPriority::DEFAULT));
}
auto priority = stream->session().application().GetStreamPriority(*stream);
args.GetReturnValue().Set(static_cast<uint32_t>(priority));
}
@ -316,7 +409,7 @@ class Stream::Outbound final : public MemoryRetainer {
// Calling cap without a value halts the ability to add any
// new data to the queue if it is not idempotent. If it is
// idempotent, it's a non-op.
queue_->cap();
if (queue_) queue_->cap();
}
int Pull(bob::Next<ngtcp2_vec> next,
@ -391,7 +484,7 @@ 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.
if (next_pending_ && !stream_->is_destroyed()) {
if (next_pending_) {
stream_->session().ResumeStream(stream_->id());
}
return;
@ -415,7 +508,7 @@ 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.
if (next_pending_ && !stream_->is_destroyed()) {
if (next_pending_) {
stream_->session().ResumeStream(stream_->id());
}
},
@ -638,8 +731,12 @@ void Stream::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
#undef V
}
void Stream::Initialize(Environment* env, Local<Object> target) {
USE(GetConstructorTemplate(env));
void Stream::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
}
void Stream::InitPerContext(Realm* realm, Local<Object> target) {
USE(GetConstructorTemplate(realm->env()));
#define V(name, _) IDX_STATS_STREAM_##name,
enum StreamStatsIdx { STREAM_STATS(V) IDX_STATS_STREAM_COUNT };
@ -692,13 +789,29 @@ BaseObjectPtr<Stream> Stream::Create(Session* session,
->InstanceTemplate()
->NewInstance(session->env()->context())
.ToLocal(&obj)) {
return BaseObjectPtr<Stream>();
return {};
}
return MakeDetachedBaseObject<Stream>(
BaseObjectWeakPtr<Session>(session), obj, id, std::move(source));
}
BaseObjectPtr<Stream> Stream::Create(Session* session,
Direction direction,
std::shared_ptr<DataQueue> source) {
DCHECK_NOT_NULL(session);
Local<Object> obj;
if (!GetConstructorTemplate(session->env())
->InstanceTemplate()
->NewInstance(session->env()->context())
.ToLocal(&obj)) {
return {};
}
return MakeBaseObject<Stream>(
BaseObjectWeakPtr<Session>(session), obj, direction, std::move(source));
}
Stream::Stream(BaseObjectWeakPtr<Session> session,
v8::Local<v8::Object> object,
int64_t id,
@ -707,12 +820,45 @@ Stream::Stream(BaseObjectWeakPtr<Session> session,
stats_(env()->isolate()),
state_(env()->isolate()),
session_(std::move(session)),
origin_(id & 0b01 ? Side::SERVER : Side::CLIENT),
direction_(id & 0b10 ? Direction::UNIDIRECTIONAL
: Direction::BIDIRECTIONAL),
inbound_(DataQueue::Create()) {
MakeWeak();
state_->id = id;
state_->pending = 0;
// Allows us to be notified when data is actually read from the
// inbound queue so that we can update the stream flow control.
inbound_->addBackpressureListener(this);
const auto defineProperty = [&](auto name, auto value) {
object
->DefineOwnProperty(
env()->context(), name, value, PropertyAttribute::ReadOnly)
.Check();
};
defineProperty(env()->state_string(), state_.GetArrayBuffer());
defineProperty(env()->stats_string(), stats_.GetArrayBuffer());
set_outbound(std::move(source));
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);
}
Stream::Stream(BaseObjectWeakPtr<Session> session,
v8::Local<v8::Object> object,
Direction direction,
std::shared_ptr<DataQueue> source)
: AsyncWrap(session->env(), object, AsyncWrap::PROVIDER_QUIC_STREAM),
stats_(env()->isolate()),
state_(env()->isolate()),
session_(std::move(session)),
inbound_(DataQueue::Create()),
maybe_pending_stream_(
std::make_unique<PendingStream>(direction, this, session_)) {
MakeWeak();
state_->id = -1;
state_->pending = 1;
// Allows us to be notified when data is actually read from the
// inbound queue so that we can update the stream flow control.
@ -735,8 +881,77 @@ Stream::Stream(BaseObjectWeakPtr<Session> session,
}
Stream::~Stream() {
// Make sure that Destroy() was called before Stream is destructed.
DCHECK(is_destroyed());
// Make sure that Destroy() was called before Stream is actually destructed.
DCHECK_NE(stats_->destroyed_at, 0);
}
void Stream::NotifyStreamOpened(int64_t id) {
CHECK(is_pending());
Debug(this, "Pending stream opened with id %" PRIi64, id);
state_->pending = 0;
state_->id = id;
STAT_RECORD_TIMESTAMP(Stats, opened_at);
// Now that the stream is actually opened, add it to the sessions
// list of known open streams.
session().AddStream(BaseObjectPtr<Stream>(this),
Session::CreateStreamOption::DO_NOT_NOTIFY);
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();
session().application().SetStreamPriority(
*this, priority.priority, priority.flags);
pending_priority_ = std::nullopt;
}
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 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()) {
NotifyWritableEnded(pending_close_write_code_);
}
// Finally, if we have an outbound data source attached already, make
// sure our stream is scheduled. This is likely a bit superfluous
// since the stream likely hasn't had any opporunity to get blocked
// yet, but just for completeness, let's make sure.
if (outbound_) session().ResumeStream(id);
}
void Stream::NotifyReadableEnded(uint64_t code) {
CHECK(!is_pending());
Session::SendPendingDataScope send_scope(&session());
ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code);
}
void Stream::NotifyWritableEnded(uint64_t code) {
CHECK(!is_pending());
Session::SendPendingDataScope send_scope(&session());
ngtcp2_conn_shutdown_stream_write(session(), 0, id(), code);
}
void Stream::EnqueuePendingHeaders(HeadersKind kind,
Local<Array> headers,
HeadersFlags flags) {
Debug(this, "Enqueuing headers for pending stream");
pending_headers_queue_.push_back(std::make_unique<PendingHeaders>(
kind, Global<Array>(env()->isolate(), headers), flags));
}
bool Stream::is_pending() const {
return state_->pending;
}
int64_t Stream::id() const {
@ -744,19 +959,32 @@ int64_t Stream::id() const {
}
Side Stream::origin() const {
return origin_;
CHECK(!is_pending());
return (state_->id & 0b01) ? Side::SERVER : Side::CLIENT;
}
Direction Stream::direction() const {
return direction_;
if (state_->pending) {
CHECK(maybe_pending_stream_.has_value());
auto& val = maybe_pending_stream_.value();
return val->direction();
}
return (state_->id & 0b10) ? Direction::UNIDIRECTIONAL
: Direction::BIDIRECTIONAL;
}
Session& Stream::session() const {
return *session_;
}
bool Stream::is_destroyed() const {
return state_->destroyed;
bool Stream::is_local_unidirectional() const {
return direction() == Direction::UNIDIRECTIONAL &&
ngtcp2_conn_is_local_stream(*session_, id());
}
bool Stream::is_remote_unidirectional() const {
return direction() == Direction::UNIDIRECTIONAL &&
!ngtcp2_conn_is_local_stream(*session_, id());
}
bool Stream::is_eos() const {
@ -764,40 +992,27 @@ bool Stream::is_eos() const {
}
bool Stream::is_writable() const {
if (direction() == Direction::UNIDIRECTIONAL) {
switch (origin()) {
case Side::CLIENT: {
if (session_->is_server()) return false;
break;
}
case Side::SERVER: {
if (!session_->is_server()) return false;
break;
}
}
// Remote unidirectional streams are never writable, and remote streams can
// never be pending.
if (!is_pending() && direction() == Direction::UNIDIRECTIONAL &&
!ngtcp2_conn_is_local_stream(session(), id())) {
return false;
}
return state_->write_ended == 0;
}
bool Stream::is_readable() const {
if (direction() == Direction::UNIDIRECTIONAL) {
switch (origin()) {
case Side::CLIENT: {
if (!session_->is_server()) return false;
break;
}
case Side::SERVER: {
if (session_->is_server()) return false;
break;
}
}
// Local unidirectional streams are never readable, and remote streams can
// never be pending.
if (!is_pending() && direction() == Direction::UNIDIRECTIONAL &&
ngtcp2_conn_is_local_stream(session(), id())) {
return false;
}
return state_->read_ended == 0;
}
BaseObjectPtr<Blob::Reader> Stream::get_reader() {
if (!is_readable() || state_->has_reader)
return BaseObjectPtr<Blob::Reader>();
if (!is_readable() || state_->has_reader) return {};
state_->has_reader = 1;
return Blob::Reader::Create(env(), Blob::Create(env(), inbound_));
}
@ -810,17 +1025,19 @@ void Stream::set_final_size(uint64_t final_size) {
}
void Stream::set_outbound(std::shared_ptr<DataQueue> source) {
if (!source || is_destroyed() || !is_writable()) return;
if (!source || !is_writable()) return;
Debug(this, "Setting the outbound data source");
DCHECK_NULL(outbound_);
outbound_ = std::make_unique<Outbound>(this, std::move(source));
session_->ResumeStream(id());
state_->has_outbound = 1;
if (!is_pending()) session_->ResumeStream(id());
}
void Stream::EntryRead(size_t amount) {
// Tells us that amount bytes were read from inbound_
// 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.
if (!is_destroyed() && session_) session_->ExtendStreamOffset(id(), amount);
session().ExtendStreamOffset(id(), amount);
}
int Stream::DoPull(bob::Next<ngtcp2_vec> next,
@ -828,7 +1045,7 @@ int Stream::DoPull(bob::Next<ngtcp2_vec> next,
ngtcp2_vec* data,
size_t count,
size_t max_count_hint) {
if (is_destroyed() || is_eos()) {
if (is_eos()) {
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](int) {});
return bob::Status::STATUS_EOS;
}
@ -848,7 +1065,6 @@ int Stream::DoPull(bob::Next<ngtcp2_vec> next,
}
void Stream::BeginHeaders(HeadersKind kind) {
if (is_destroyed()) return;
headers_length_ = 0;
headers_.clear();
set_headers_kind(kind);
@ -860,8 +1076,8 @@ void Stream::set_headers_kind(HeadersKind kind) {
bool Stream::AddHeader(const Header& header) {
size_t len = header.length();
if (is_destroyed() || !session_->application().CanAddHeader(
headers_.size(), headers_length_, len)) {
if (!session_->application().CanAddHeader(
headers_.size(), headers_length_, len)) {
return false;
}
@ -882,42 +1098,59 @@ bool Stream::AddHeader(const Header& header) {
}
void Stream::Acknowledge(size_t datalen) {
if (is_destroyed() || outbound_ == nullptr) return;
if (outbound_ == nullptr) return;
Debug(this, "Acknowledging %zu bytes", 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);
// // Consumes the given number of bytes in the buffer.
// Consumes the given number of bytes in the buffer.
outbound_->Acknowledge(datalen);
}
void Stream::Commit(size_t datalen) {
if (!is_destroyed() && outbound_) outbound_->Commit(datalen);
Debug(this, "Commiting %zu bytes", datalen);
STAT_RECORD_TIMESTAMP(Stats, acked_at);
if (outbound_) outbound_->Commit(datalen);
}
void Stream::EndWritable() {
if (is_destroyed() || !is_writable()) return;
if (!is_writable()) return;
// If an outbound_ has been attached, we want to mark it as being ended.
// If the outbound_ is wrapping an idempotent DataQueue, then capping
// will be a non-op since we're not going to be writing any more data
// into it anyway.
if (outbound_ != nullptr) outbound_->Cap();
if (outbound_) outbound_->Cap();
state_->write_ended = 1;
}
void Stream::EndReadable(std::optional<uint64_t> maybe_final_size) {
if (is_destroyed() || !is_readable()) return;
if (!is_readable()) return;
state_->read_ended = 1;
set_final_size(maybe_final_size.value_or(STAT_GET(Stats, bytes_received)));
inbound_->cap(STAT_GET(Stats, final_size));
}
void Stream::Destroy(QuicError error) {
if (is_destroyed()) return;
if (stats_->destroyed_at != 0) return;
// Record the destroyed at timestamp before notifying the JavaScript side
// that the stream is being destroyed.
STAT_RECORD_TIMESTAMP(Stats, destroyed_at);
DCHECK_NOT_NULL(session_.get());
Debug(this, "Stream %" PRIi64 " being destroyed with error %s", id(), error);
if (!state_->pending) {
Debug(
this, "Stream %" PRIi64 " being destroyed with error %s", id(), error);
} else {
Debug(this, "Pending stream being destroyed with error %s", error);
}
state_->pending = 0;
maybe_pending_stream_.reset();
// End the writable before marking as destroyed.
EndWritable();
@ -925,10 +1158,6 @@ void Stream::Destroy(QuicError error) {
// Also end the readable side if it isn't already.
EndReadable();
state_->destroyed = 1;
EmitClose(error);
// We are going to release our reference to the outbound_ queue here.
outbound_.reset();
@ -936,40 +1165,55 @@ void Stream::Destroy(QuicError error) {
// the JavaScript side could still have a reader on the inbound DataQueue,
// which may keep that data alive a bit longer.
inbound_->removeBackpressureListener(this);
inbound_.reset();
CHECK_NOT_NULL(session_.get());
// Notify the JavaScript side that our handle is being destroyed. The
// JavaScript side should clean up any state that it needs to and should
// detach itself from the handle. After this is called, it should no
// longer be considered safe for the JavaScript side to access the
// handle.
EmitClose(error);
// Finally, remove the stream from the session and clear our reference
// to the session.
session_->RemoveStream(id());
auto session = session_;
session_.reset();
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
// object is no longer valid and should not be accessed.
// Specifically, the session object's streams map holds the its
// BaseObjectPtr<Stream> instances in a detached state, meaning that
// once that BaseObjectPtr is deleted the Stream will be freed as well.
}
void Stream::ReceiveData(const uint8_t* data,
size_t len,
ReceiveDataFlags flags) {
if (is_destroyed()) return;
// 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;
}
STAT_INCREMENT_N(Stats, bytes_received, len);
STAT_RECORD_TIMESTAMP(Stats, received_at);
auto backing = ArrayBuffer::NewBackingStore(env()->isolate(), len);
memcpy(backing->Data(), data, len);
inbound_->append(DataQueue::CreateInMemoryEntryFromBackingStore(
std::move(backing), 0, len));
if (flags.fin) EndReadable();
}
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 (is_destroyed() || state_->read_ended) return;
if (state_->read_ended) return;
Debug(this, "Received stop sending with error %s", error);
ngtcp2_conn_shutdown_stream_read(session(), 0, id(), error.code());
EndReadable();
}
@ -980,6 +1224,10 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
// has abruptly terminated the writable end of their stream with an error.
// Any data we have received up to this point remains in the queue waiting to
// be read.
Debug(this,
"Received stream reset with final size %" PRIu64 " and error %s",
final_size,
error);
EndReadable(final_size);
EmitReset(error);
}
@ -989,8 +1237,8 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) {
void Stream::EmitBlocked() {
// state_->wants_block will be set from the javascript side if the
// stream object has a handler for the blocked event.
if (is_destroyed() || !env()->can_call_into_js() ||
state_->wants_block == 0) {
Debug(this, "Blocked");
if (!env()->can_call_into_js() || !state_->wants_block) {
return;
}
CallbackScope<Stream> cb_scope(this);
@ -998,17 +1246,17 @@ void Stream::EmitBlocked() {
}
void Stream::EmitClose(const QuicError& error) {
if (is_destroyed() || !env()->can_call_into_js()) return;
if (!env()->can_call_into_js()) return;
CallbackScope<Stream> cb_scope(this);
Local<Value> err;
if (!error.ToV8Value(env()).ToLocal(&err)) return;
MakeCallback(BindingData::Get(env()).stream_close_callback(), 1, &err);
}
void Stream::EmitHeaders() {
if (is_destroyed() || !env()->can_call_into_js() ||
state_->wants_headers == 0) {
// state_->wants_headers will be set from the javascript side if the
// stream object has a handler for the headers event.
if (!env()->can_call_into_js() || !state_->wants_headers) {
return;
}
CallbackScope<Stream> cb_scope(this);
@ -1025,8 +1273,9 @@ void Stream::EmitHeaders() {
}
void Stream::EmitReset(const QuicError& error) {
if (is_destroyed() || !env()->can_call_into_js() ||
state_->wants_reset == 0) {
// state_->wants_reset will be set from the javascript side if the
// stream object has a handler for the reset event.
if (!env()->can_call_into_js() || !state_->wants_reset) {
return;
}
CallbackScope<Stream> cb_scope(this);
@ -1037,8 +1286,9 @@ void Stream::EmitReset(const QuicError& error) {
}
void Stream::EmitWantTrailers() {
if (is_destroyed() || !env()->can_call_into_js() ||
state_->wants_trailers == 0) {
// state_->wants_trailers will be set from the javascript side if the
// stream object has a handler for the trailers event.
if (!env()->can_call_into_js() || !state_->wants_trailers) {
return;
}
CallbackScope<Stream> cb_scope(this);
@ -1049,11 +1299,12 @@ void Stream::EmitWantTrailers() {
void Stream::Schedule(Stream::Queue* queue) {
// If this stream is not already in the queue to send data, add it.
if (!is_destroyed() && outbound_ && stream_queue_.IsEmpty())
queue->PushBack(this);
Debug(this, "Scheduled");
if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this);
}
void Stream::Unschedule() {
Debug(this, "Unscheduled");
stream_queue_.Remove();
}

View File

@ -12,15 +12,61 @@
#include <node_blob.h>
#include <node_bob.h>
#include <node_http_common.h>
#include <util.h>
#include "bindingdata.h"
#include "data.h"
namespace node::quic {
class Session;
class Stream;
using Ngtcp2Source = bob::SourceImpl<ngtcp2_vec>;
// When a request to open a stream is made before a Session is able to actually
// open a stream (either because the handshake is not yet sufficiently complete
// or concurrency limits are temporarily reached) then the request to open the
// stream is represented as a queued PendingStream.
//
// The PendingStream instance itself is held by the stream but sits in a linked
// list in the session.
//
// The PendingStream request can be canceled by dropping the PendingStream
// instance before it can be fulfilled, at which point it is removed from the
// pending stream queue.
//
// Note that only locally initiated streams can be created in a pending state.
class PendingStream final {
public:
PendingStream(Direction direction,
Stream* stream,
BaseObjectWeakPtr<Session> session);
DISALLOW_COPY_AND_MOVE(PendingStream)
~PendingStream();
// Called when the stream has been opened. Transitions the stream from a
// pending state to an opened state.
void fulfill(int64_t id);
// Called when opening the stream fails or is canceled. Transitions the
// stream into a closed/destroyed state.
void reject(QuicError error = QuicError());
inline Direction direction() const { return direction_; }
private:
Direction direction_;
Stream* stream_;
BaseObjectWeakPtr<Session> session_;
bool waiting_ = true;
ListNode<PendingStream> pending_stream_queue_;
public:
using PendingStreamQueue =
ListHead<PendingStream, &PendingStream::pending_stream_queue_>;
};
// QUIC Stream's are simple data flows that may be:
//
// * Bidirectional (both sides can send) or Unidirectional (one side can send)
@ -63,7 +109,7 @@ using Ngtcp2Source = bob::SourceImpl<ngtcp2_vec>;
// the right thing.
//
// A Stream may be in a fully closed state (No longer readable nor writable)
// state but still have unacknowledged data in it's inbound and outbound
// state but still have unacknowledged data in both the inbound and outbound
// queues.
//
// A Stream is gracefully closed when (a) both read and write states are closed,
@ -78,50 +124,98 @@ using Ngtcp2Source = bob::SourceImpl<ngtcp2_vec>;
//
// QUIC streams in general do not have headers. Some QUIC applications, however,
// may associate headers with the stream (HTTP/3 for instance).
class Stream : public AsyncWrap,
public Ngtcp2Source,
public DataQueue::BackpressureListener {
//
// Streams may be created in a pending state. This means that while the Stream
// object is created, it has not yet been opened in ngtcp2 and therefore has
// no official status yet. Certain operations can still be performed on the
// stream object such as providing data and headers, and destroying the stream.
//
// When a stream is created the data source for the stream must be given.
// If no data source is given, then the stream is assumed to not have any
// outbound data. The data source can be fixed length or may support
// streaming. What this means practically is, when a stream is opened,
// you must already have a sense of whether that will provide data or
// not. When in doubt, specify a streaming data source, which can produce
// zero-length output.
class Stream final : public AsyncWrap,
public Ngtcp2Source,
public DataQueue::BackpressureListener {
public:
using Header = NgHeaderBase<BindingData>;
static v8::Maybe<std::shared_ptr<DataQueue>> GetDataQueueFromSource(
Environment* env, v8::Local<v8::Value> value);
static Stream* From(void* stream_user_data);
static bool HasInstance(Environment* env, v8::Local<v8::Value> value);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static void Initialize(Environment* env, v8::Local<v8::Object> target);
static void InitPerIsolate(IsolateData* data,
v8::Local<v8::ObjectTemplate> target);
static void InitPerContext(Realm* realm, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
// Creates a new non-pending stream.
static BaseObjectPtr<Stream> Create(
Session* session,
int64_t id,
std::shared_ptr<DataQueue> source = nullptr);
// Creates a new pending stream.
static BaseObjectPtr<Stream> Create(
Session* session,
Direction direction,
std::shared_ptr<DataQueue> source = nullptr);
// The constructor is only public to be visible by MakeDetachedBaseObject.
// Call Create to create new instances of Stream.
Stream(BaseObjectWeakPtr<Session> session,
v8::Local<v8::Object> obj,
int64_t id,
std::shared_ptr<DataQueue> source);
// Creates the stream in a pending state. The constructor is only public
// to be visible to MakeDetachedBaseObject. Call Create to create new
// instances of Stream.
Stream(BaseObjectWeakPtr<Session> session,
v8::Local<v8::Object> obj,
Direction direction,
std::shared_ptr<DataQueue> source);
DISALLOW_COPY_AND_MOVE(Stream)
~Stream() override;
// While the stream is still pending, the id will be -1.
int64_t id() const;
// While the stream is still pending, the origin will be invalid.
Side origin() const;
Direction direction() const;
Session& session() const;
bool is_destroyed() const;
// True if this stream was created in a pending state and is still waiting
// to be created.
bool is_pending() const;
// True if we've completely sent all outbound data for this stream.
// Importantly, this does not necessarily mean that we are completely
// done with the outbound data. We may still be waiting on outbound
// data to be acknowledged by the remote peer.
bool is_eos() const;
// 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;
// Called by the session/application to indicate that the specified number
// of bytes have been acknowledged by the peer.
void Acknowledge(size_t datalen);
void Commit(size_t datalen);
void EndWritable();
void EndReadable(std::optional<uint64_t> maybe_final_size = std::nullopt);
void EntryRead(size_t amount) override;
@ -133,7 +227,8 @@ class Stream : public AsyncWrap,
size_t count,
size_t max_count_hint) override;
// Forcefully close the stream immediately. All queued data and pending
// Forcefully close the stream immediately. Data already queued in the
// inbound is preserved but new data will not be accepted. All pending
// writes are abandoned, and the stream is immediately closed at the ngtcp2
// level without waiting for any outstanding acknowledgements.
void Destroy(QuicError error = QuicError());
@ -152,12 +247,15 @@ class Stream : public AsyncWrap,
void ReceiveStopSending(QuicError error);
void ReceiveStreamReset(uint64_t final_size, QuicError error);
// Currently, only HTTP/3 streams support headers. These methods are here
// to support that. They are not used when using any other QUIC application.
void BeginHeaders(HeadersKind kind);
void set_headers_kind(HeadersKind kind);
// Returns false if the header cannot be added. This will typically happen
// if the application does not support headers, a maximum number of headers
// have already been added, or the maximum total header length is reached.
bool AddHeader(const Header& header);
void set_headers_kind(HeadersKind kind);
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(Stream)
@ -166,15 +264,10 @@ class Stream : public AsyncWrap,
struct State;
struct Stats;
// Notifies the JavaScript side that sending data on the stream has been
// blocked because of flow control restriction.
void EmitBlocked();
// Delivers the set of inbound headers that have been collected.
void EmitHeaders();
private:
struct Impl;
struct PendingHeaders;
class Outbound;
// Gets a reader for the data received for this stream from the peer,
@ -183,6 +276,9 @@ class Stream : public AsyncWrap,
void set_final_size(uint64_t amount);
void set_outbound(std::shared_ptr<DataQueue> source);
bool is_local_unidirectional() const;
bool is_remote_unidirectional() const;
// JavaScript callouts
// Notifies the JavaScript side that the stream has been destroyed.
@ -195,19 +291,61 @@ class Stream : public AsyncWrap,
// trailing headers.
void EmitWantTrailers();
// Notifies the JavaScript side that sending data on the stream has been
// blocked because of flow control restriction.
void EmitBlocked();
// Delivers the set of inbound headers that have been collected.
void EmitHeaders();
void NotifyReadableEnded(uint64_t code);
void NotifyWritableEnded(uint64_t code);
// When a pending stream is finally opened, the NotifyStreamOpened method
// will be called and the id will be assigned.
void NotifyStreamOpened(int64_t id);
void EnqueuePendingHeaders(HeadersKind kind,
v8::Local<v8::Array> headers,
HeadersFlags flags);
AliasedStruct<Stats> stats_;
AliasedStruct<State> state_;
BaseObjectWeakPtr<Session> session_;
const Side origin_;
const Direction direction_;
std::unique_ptr<Outbound> outbound_;
std::shared_ptr<DataQueue> inbound_;
// If the stream cannot be opened yet, it will be created in a pending state.
// Once the owning session is able to, it will complete opening of the stream
// and the stream id will be assigned.
std::optional<std::unique_ptr<PendingStream>> maybe_pending_stream_ =
std::nullopt;
std::vector<std::unique_ptr<PendingHeaders>> pending_headers_queue_;
uint64_t pending_close_read_code_ = NGTCP2_APP_NOERROR;
uint64_t pending_close_write_code_ = NGTCP2_APP_NOERROR;
struct PendingPriority {
StreamPriority priority;
StreamPriorityFlags flags;
};
std::optional<PendingPriority> pending_priority_ = std::nullopt;
// The headers_ field holds a block of headers that have been received and
// are being buffered for delivery to the JavaScript side.
// TODO(@jasnell): Use v8::Global instead of v8::Local here.
std::vector<v8::Local<v8::Value>> headers_;
// The headers_kind_ field indicates the kind of headers that are being
// buffered.
HeadersKind headers_kind_ = HeadersKind::INITIAL;
// The headers_length_ field holds the total length of the headers that have
// been buffered.
size_t headers_length_ = 0;
friend struct Impl;
friend class PendingStream;
friend class Http3ApplicationImpl;
friend class DefaultApplication;
public:
// The Queue/Schedule/Unschedule here are part of the mechanism used to

View File

@ -170,7 +170,7 @@ int TLSContext::OnSelectAlpn(SSL* ssl,
static constexpr size_t kMaxAlpnLen = 255;
auto& session = TLSSession::From(ssl);
const auto& requested = session.context().options().alpn;
const auto& requested = session.context().options().protocol;
if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK;
// The Session supports exactly one ALPN identifier. If that does not match
@ -266,11 +266,13 @@ crypto::SSLCtxPointer TLSContext::Initialize() {
OnVerifyClientCertificate);
}
CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
SessionTicket::GenerateCallback,
SessionTicket::DecryptedCallback,
nullptr),
1);
// TODO(@jasnell): There's a bug int the GenerateCallback flow somewhere.
// Need to update in order to support session tickets.
// CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
// SessionTicket::GenerateCallback,
// SessionTicket::DecryptedCallback,
// nullptr),
// 1);
break;
}
case Side::CLIENT: {
@ -434,11 +436,11 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
SetOption<TLSContext::Options, &TLSContext::Options::name>( \
env, &options, params, state.name##_string())
if (!SET(verify_client) || !SET(enable_tls_trace) || !SET(alpn) ||
!SET(sni) || !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)) {
if (!SET(verify_client) || !SET(enable_tls_trace) || !SET(protocol) ||
!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)) {
return Nothing<Options>();
}
@ -449,8 +451,8 @@ std::string TLSContext::Options::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
std::string res("{");
res += prefix + "alpn: " + alpn;
res += prefix + "sni: " + sni;
res += prefix + "protocol: " + protocol;
res += prefix + "servername: " + servername;
res +=
prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no"));
res += prefix + "verify client: " +
@ -496,6 +498,12 @@ TLSSession::TLSSession(Session* session,
Debug(session_, "Created new TLS session for %s", session->config().dcid);
}
TLSSession::~TLSSession() {
if (ssl_) {
SSL_set_app_data(ssl_.get(), nullptr);
}
}
TLSSession::operator SSL*() const {
CHECK(ssl_);
return ssl_.get();
@ -530,14 +538,14 @@ crypto::SSLPointer TLSSession::Initialize(
SSL_set_connect_state(ssl.get());
if (SSL_set_alpn_protos(
ssl.get(),
reinterpret_cast<const unsigned char*>(options.alpn.data()),
options.alpn.size()) != 0) {
reinterpret_cast<const unsigned char*>(options.protocol.data()),
options.protocol.size()) != 0) {
validation_error_ = "Invalid ALPN";
return crypto::SSLPointer();
}
if (!options.sni.empty()) {
SSL_set_tlsext_host_name(ssl.get(), options.sni.data());
if (!options.servername.empty()) {
SSL_set_tlsext_host_name(ssl.get(), options.servername.data());
} else {
SSL_set_tlsext_host_name(ssl.get(), "localhost");
}
@ -619,7 +627,7 @@ const std::string_view TLSSession::servername() const {
: std::string_view();
}
const std::string_view TLSSession::alpn() const {
const std::string_view TLSSession::protocol() const {
const unsigned char* alpn_buf = nullptr;
unsigned int alpnlen;
SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
@ -629,7 +637,7 @@ const std::string_view TLSSession::alpn() const {
}
bool TLSSession::InitiateKeyUpdate() {
if (session_->is_destroyed() || in_key_update_) return false;
if (in_key_update_) return false;
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
in_key_update_ = true;

View File

@ -34,6 +34,7 @@ class TLSSession final : public MemoryRetainer {
std::shared_ptr<TLSContext> context,
const std::optional<SessionTicket>& maybeSessionTicket);
DISALLOW_COPY_AND_MOVE(TLSSession)
~TLSSession();
inline operator bool() const { return ssl_ != nullptr; }
inline Session& session() const { return *session_; }
@ -54,7 +55,7 @@ class TLSSession final : public MemoryRetainer {
const std::string_view servername() const;
// The ALPN (protocol name) negotiated for the session
const std::string_view alpn() const;
const std::string_view protocol() const;
// Triggers key update to begin. This will fail and return false if either a
// previous key update is in progress or if the initial handshake has not yet
@ -113,11 +114,11 @@ class TLSContext final : public MemoryRetainer,
struct Options final : public MemoryRetainer {
// The SNI servername to use for this session. This option is only used by
// the client.
std::string sni = "localhost";
std::string servername = "localhost";
// The ALPN (protocol name) to use for this session. This option is only
// used by the client.
std::string alpn = NGHTTP3_ALPN_H3;
std::string protocol = NGHTTP3_ALPN_H3;
// The list of TLS ciphers to use for this session.
std::string ciphers = DEFAULT_CIPHERS;

View File

@ -62,7 +62,7 @@ Maybe<TransportParams::Options> TransportParams::Options::From(
!SET(initial_max_streams_bidi) || !SET(initial_max_streams_uni) ||
!SET(max_idle_timeout) || !SET(active_connection_id_limit) ||
!SET(ack_delay_exponent) || !SET(max_ack_delay) ||
!SET(max_datagram_frame_size) || !SET(disable_active_migration)) {
!SET(max_datagram_frame_size)) {
return Nothing<Options>();
}
@ -153,6 +153,7 @@ TransportParams::TransportParams(const Config& config, const Options& options)
// For the server side, the original dcid is always set.
CHECK(config.ocid);
params_.original_dcid = config.ocid;
params_.original_dcid_present = 1;
// The retry_scid is only set if the server validated a retry token.
if (config.retry_scid) {
@ -179,25 +180,25 @@ TransportParams::TransportParams(const ngtcp2_vec& vec, int version)
}
}
Store TransportParams::Encode(Environment* env, int version) {
Store TransportParams::Encode(Environment* env, int version) const {
if (ptr_ == nullptr) {
error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR);
return Store();
}
// Preflight to see how much storage we'll need.
ssize_t size =
ngtcp2_transport_params_encode_versioned(nullptr, 0, version, &params_);
if (size == 0) {
return Store();
}
DCHECK_GT(size, 0);
auto result = ArrayBuffer::NewBackingStore(env->isolate(), size);
auto result = ArrayBuffer::NewBackingStore(
env->isolate(), size, v8::BackingStoreInitializationMode::kUninitialized);
auto ret = ngtcp2_transport_params_encode_versioned(
static_cast<uint8_t*>(result->Data()), size, version, &params_);
if (ret != 0) {
error_ = QuicError::ForNgtcp2Error(ret);
return Store();
}
@ -232,7 +233,7 @@ void TransportParams::SetPreferredAddress(const SocketAddress& address) {
void TransportParams::GenerateSessionTokens(Session* session) {
if (session->is_server()) {
GenerateStatelessResetToken(session->endpoint(), session->config_.scid);
GenerateStatelessResetToken(session->endpoint(), session->config().scid);
GeneratePreferredAddressToken(session);
}
}
@ -247,14 +248,15 @@ void TransportParams::GenerateStatelessResetToken(const Endpoint& endpoint,
void TransportParams::GeneratePreferredAddressToken(Session* session) {
DCHECK(ptr_ == &params_);
Session::Config& config = session->config();
if (params_.preferred_addr_present) {
session->config_.preferred_address_cid = session->new_cid();
params_.preferred_addr.cid = session->config_.preferred_address_cid;
config.preferred_address_cid = session->new_cid();
params_.preferred_addr.cid = config.preferred_address_cid;
auto& endpoint = session->endpoint();
endpoint.AssociateStatelessResetToken(
endpoint.GenerateNewStatelessResetToken(
params_.preferred_addr.stateless_reset_token,
session->config_.preferred_address_cid),
config.preferred_address_cid),
session);
}
}

View File

@ -107,7 +107,8 @@ class TransportParams final {
// When true, communicates that the Session does not support active
// connection migration. See the QUIC specification for more details on
// connection migration.
bool disable_active_migration = false;
// TODO(@jasnell): We currently do not implementation active migration.
bool disable_active_migration = true;
static const Options kDefault;
@ -151,7 +152,7 @@ class TransportParams final {
// Returns an ArrayBuffer containing the encoded transport parameters.
// If an error occurs during encoding, an empty shared_ptr will be returned
// and the error() property will be set to an appropriate QuicError.
Store Encode(Environment* env, int version = QUIC_TRANSPORT_PARAMS_V1);
Store Encode(Environment* env, int version = QUIC_TRANSPORT_PARAMS_V1) const;
private:
ngtcp2_transport_params params_{};

View File

@ -49,6 +49,11 @@ void ReqWrap<T>::Cancel() {
uv_cancel(reinterpret_cast<uv_req_t*>(&req_));
}
template <typename T>
bool ReqWrap<T>::IsDispatched() {
return req_.data != nullptr;
}
template <typename T>
AsyncWrap* ReqWrap<T>::GetAsyncWrap() {
return this;

View File

@ -48,6 +48,8 @@ class ReqWrap : public AsyncWrap, public ReqWrapBase {
template <typename LibuvFunction, typename... Args>
inline int Dispatch(LibuvFunction fn, Args... args);
inline bool IsDispatched();
private:
friend int GenDebugSymbols();

View File

@ -61,6 +61,8 @@ class TimerWrapHandle : public MemoryRetainer {
void Update(uint64_t interval, uint64_t repeat = 0);
inline operator bool() const { return timer_ != nullptr; }
void Ref();
void Unref();

View File

@ -339,7 +339,7 @@ assert.throws(() => new Blob({}), {
setTimeout(() => {
// The blob stream is now a byte stream hence after the first read,
// it should pull in the next 'hello' which is 5 bytes hence -5.
assert.strictEqual(stream[kState].controller.desiredSize, -5);
assert.strictEqual(stream[kState].controller.desiredSize, 0);
}, 0);
})().then(common.mustCall());
@ -366,7 +366,7 @@ assert.throws(() => new Blob({}), {
assert.strictEqual(value.byteLength, 5);
assert(!done);
setTimeout(() => {
assert.strictEqual(stream[kState].controller.desiredSize, -5);
assert.strictEqual(stream[kState].controller.desiredSize, 0);
}, 0);
})().then(common.mustCall());

View File

@ -87,8 +87,6 @@ expected.beforePreExec = new Set([
'NativeModule internal/process/signal',
'Internal Binding fs',
'NativeModule internal/encoding',
'NativeModule internal/webstreams/util',
'NativeModule internal/webstreams/queuingstrategies',
'NativeModule internal/blob',
'NativeModule internal/fs/utils',
'NativeModule fs',

View File

@ -35,6 +35,8 @@ if (!hasIntl) {
publicBuiltins.delete('inspector');
publicBuiltins.delete('trace_events');
}
// TODO(@jasnell): Remove this once node:quic graduates from unflagged.
publicBuiltins.delete('node:quic');
for (const id of publicBuiltins) {
assert.strictEqual(process.getBuiltinModule(id), require(id));

View File

@ -0,0 +1,82 @@
// Flags: --experimental-quic --no-warnings
'use strict';
const { hasQuic } = require('../common');
const { Buffer } = require('node:buffer');
const {
describe,
it,
} = require('node:test');
// TODO(@jasnell): Temporarily skip the test on mac until we can figure
// out while it is failing on macs in CI but running locally on macs ok.
const isMac = process.platform === 'darwin';
const skip = isMac || !hasQuic;
async function readAll(readable, resolve) {
const chunks = [];
for await (const chunk of readable) {
chunks.push(chunk);
}
resolve(Buffer.concat(chunks));
}
describe('quic basic server/client handshake works', { skip }, async () => {
const { createPrivateKey } = require('node:crypto');
const fixtures = require('../common/fixtures');
const keys = createPrivateKey(fixtures.readKey('agent1-key.pem'));
const certs = fixtures.readKey('agent1-cert.pem');
const {
listen,
connect,
} = require('node:quic');
const {
strictEqual,
ok,
} = require('node:assert');
it('a quic client can connect to a quic server in the same process', async () => {
const p1 = Promise.withResolvers();
const p2 = Promise.withResolvers();
const p3 = Promise.withResolvers();
const serverEndpoint = await listen((serverSession) => {
serverSession.opened.then((info) => {
strictEqual(info.servername, 'localhost');
strictEqual(info.protocol, 'h3');
strictEqual(info.cipher, 'TLS_AES_128_GCM_SHA256');
p1.resolve();
});
serverSession.onstream = (stream) => {
readAll(stream.readable, p3.resolve).then(() => {
serverSession.close();
});
};
}, { keys, certs });
ok(serverEndpoint.address !== undefined);
const clientSession = await connect(serverEndpoint.address);
clientSession.opened.then((info) => {
strictEqual(info.servername, 'localhost');
strictEqual(info.protocol, 'h3');
strictEqual(info.cipher, 'TLS_AES_128_GCM_SHA256');
p2.resolve();
});
const body = new Blob(['hello']);
const stream = await clientSession.createUnidirectionalStream({
body,
});
ok(stream);
const { 2: data } = await Promise.all([p1.promise, p2.promise, p3.promise]);
clientSession.close();
strictEqual(Buffer.from(data).toString(), 'hello');
});
});

View File

@ -11,41 +11,54 @@ const {
describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async () => {
const {
ok,
rejects,
strictEqual,
throws,
} = require('node:assert');
const {
kState,
} = require('internal/quic/symbols');
const { createPrivateKey } = require('node:crypto');
const fixtures = require('../common/fixtures');
const keys = createPrivateKey(fixtures.readKey('agent1-key.pem'));
const certs = fixtures.readKey('agent1-cert.pem');
const {
SocketAddress,
} = require('net');
const {
QuicEndpoint,
listen,
} = require('internal/quic/quic');
it('are reasonable and work as expected', async () => {
const endpoint = new QuicEndpoint({
onsession() {},
});
const endpoint = new QuicEndpoint();
ok(!endpoint.state.isBound);
ok(!endpoint.state.isReceiving);
ok(!endpoint.state.isListening);
ok(!endpoint[kState].isBound);
ok(!endpoint[kState].isReceiving);
ok(!endpoint[kState].isListening);
strictEqual(endpoint.address, undefined);
throws(() => endpoint.listen(123), {
await rejects(listen(123, { keys, certs, endpoint }), {
code: 'ERR_INVALID_ARG_TYPE',
});
endpoint.listen();
throws(() => endpoint.listen(), {
await rejects(listen(() => {}, 123), {
code: 'ERR_INVALID_ARG_TYPE',
});
await listen(() => {}, { keys, certs, endpoint });
await rejects(listen(() => {}, { keys, certs, endpoint }), {
code: 'ERR_INVALID_STATE',
});
ok(endpoint.state.isBound);
ok(endpoint.state.isReceiving);
ok(endpoint.state.isListening);
ok(endpoint[kState].isBound);
ok(endpoint[kState].isReceiving);
ok(endpoint[kState].isListening);
const address = endpoint.address;
ok(address instanceof SocketAddress);
@ -61,7 +74,7 @@ describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async ()
await endpoint.closed;
ok(endpoint.destroyed);
throws(() => endpoint.listen(), {
await rejects(listen(() => {}, { keys, certs, endpoint }), {
code: 'ERR_INVALID_STATE',
});
throws(() => { endpoint.busy = true; }, {

View File

@ -1,4 +1,4 @@
// Flags: --expose-internals
// Flags: --experimental-quic --no-warnings
'use strict';
const { hasQuic } = require('../common');
@ -16,7 +16,7 @@ describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
const {
QuicEndpoint,
} = require('internal/quic/quic');
} = require('node:quic');
const {
inspect,
@ -86,20 +86,6 @@ describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'maxPayloadSize',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'unacknowledgedPacketThreshold',
valid: [
1, 10, 100, 1000, 10000, 10000n,
],
invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}]
},
{
key: 'validateAddress',
valid: [true, false, 0, 1, 'a'],
@ -115,18 +101,6 @@ describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
valid: [true, false, 0, 1, 'a'],
invalid: [],
},
{
key: 'cc',
valid: [
QuicEndpoint.CC_ALGO_RENO,
QuicEndpoint.CC_ALGO_CUBIC,
QuicEndpoint.CC_ALGO_BBR,
QuicEndpoint.CC_ALGO_RENO_STR,
QuicEndpoint.CC_ALGO_CUBIC_STR,
QuicEndpoint.CC_ALGO_BBR_STR,
],
invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}],
},
{
key: 'udpReceiveBufferSize',
valid: [0, 1, 2, 3, 4, 1000],
@ -189,20 +163,12 @@ describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
const options = {};
options[key] = value;
throws(() => new QuicEndpoint(options), {
code: 'ERR_INVALID_ARG_VALUE',
});
message: new RegExp(`${key}`),
}, value);
}
}
});
it('endpoint can be ref/unrefed without error', async () => {
const endpoint = new QuicEndpoint();
endpoint.unref();
endpoint.ref();
endpoint.close();
await endpoint.closed;
});
it('endpoint can be inspected', async () => {
const endpoint = new QuicEndpoint({});
strictEqual(typeof inspect(endpoint), 'string');
@ -214,7 +180,10 @@ describe('quic internal endpoint options', { skip: !hasQuic }, async () => {
new QuicEndpoint({
address: { host: '127.0.0.1:0' },
});
throws(() => new QuicEndpoint({ address: '127.0.0.1:0' }), {
new QuicEndpoint({
address: '127.0.0.1:0',
});
throws(() => new QuicEndpoint({ address: 123 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
});

View File

@ -11,15 +11,22 @@ const {
describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
const {
QuicEndpoint,
QuicStreamState,
QuicStreamStats,
QuicSessionState,
QuicSessionStats,
} = require('internal/quic/quic');
const {
QuicSessionState,
QuicStreamState,
} = require('internal/quic/state');
const {
QuicSessionStats,
QuicStreamStats,
} = require('internal/quic/stats');
const {
kFinishClose,
kPrivateConstructor,
kState,
} = require('internal/quic/symbols');
const {
@ -35,14 +42,14 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
it('endpoint state', () => {
const endpoint = new QuicEndpoint();
strictEqual(endpoint.state.isBound, false);
strictEqual(endpoint.state.isReceiving, false);
strictEqual(endpoint.state.isListening, false);
strictEqual(endpoint.state.isClosing, false);
strictEqual(endpoint.state.isBusy, false);
strictEqual(endpoint.state.pendingCallbacks, 0n);
strictEqual(endpoint[kState].isBound, false);
strictEqual(endpoint[kState].isReceiving, false);
strictEqual(endpoint[kState].isListening, false);
strictEqual(endpoint[kState].isClosing, false);
strictEqual(endpoint[kState].isBusy, false);
strictEqual(endpoint[kState].pendingCallbacks, 0n);
deepStrictEqual(JSON.parse(JSON.stringify(endpoint.state)), {
deepStrictEqual(JSON.parse(JSON.stringify(endpoint[kState])), {
isBound: false,
isReceiving: false,
isListening: false,
@ -52,26 +59,24 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
});
endpoint.busy = true;
strictEqual(endpoint.state.isBusy, true);
strictEqual(endpoint[kState].isBusy, true);
endpoint.busy = false;
strictEqual(endpoint.state.isBusy, false);
strictEqual(endpoint[kState].isBusy, false);
it('state can be inspected without errors', () => {
strictEqual(typeof inspect(endpoint.state), 'string');
strictEqual(typeof inspect(endpoint[kState]), 'string');
});
});
it('state is not readable after close', () => {
const endpoint = new QuicEndpoint();
endpoint.state[kFinishClose]();
throws(() => endpoint.state.isBound, {
name: 'Error',
});
endpoint[kState][kFinishClose]();
strictEqual(endpoint[kState].isBound, undefined);
});
it('state constructor argument is ArrayBuffer', () => {
const endpoint = new QuicEndpoint();
const Cons = endpoint.state.constructor;
const Cons = endpoint[kState].constructor;
throws(() => new Cons(kPrivateConstructor, 1), {
code: 'ERR_INVALID_ARG_TYPE'
});
@ -142,18 +147,16 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
const streamState = new QuicStreamState(kPrivateConstructor, new ArrayBuffer(1024));
const sessionState = new QuicSessionState(kPrivateConstructor, new ArrayBuffer(1024));
strictEqual(streamState.pending, false);
strictEqual(streamState.finSent, false);
strictEqual(streamState.finReceived, false);
strictEqual(streamState.readEnded, false);
strictEqual(streamState.writeEnded, false);
strictEqual(streamState.destroyed, false);
strictEqual(streamState.paused, false);
strictEqual(streamState.reset, false);
strictEqual(streamState.hasReader, false);
strictEqual(streamState.wantsBlock, false);
strictEqual(streamState.wantsHeaders, false);
strictEqual(streamState.wantsReset, false);
strictEqual(streamState.wantsTrailers, false);
strictEqual(sessionState.hasPathValidationListener, false);
strictEqual(sessionState.hasVersionNegotiationListener, false);
@ -163,7 +166,6 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
strictEqual(sessionState.isGracefulClose, false);
strictEqual(sessionState.isSilentClose, false);
strictEqual(sessionState.isStatelessReset, false);
strictEqual(sessionState.isDestroyed, false);
strictEqual(sessionState.isHandshakeCompleted, false);
strictEqual(sessionState.isHandshakeConfirmed, false);
strictEqual(sessionState.isStreamOpenAllowed, false);
@ -180,34 +182,31 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => {
it('stream and session stats', () => {
const streamStats = new QuicStreamStats(kPrivateConstructor, new ArrayBuffer(1024));
const sessionStats = new QuicSessionStats(kPrivateConstructor, new ArrayBuffer(1024));
strictEqual(streamStats.createdAt, undefined);
strictEqual(streamStats.receivedAt, undefined);
strictEqual(streamStats.ackedAt, undefined);
strictEqual(streamStats.closingAt, undefined);
strictEqual(streamStats.destroyedAt, undefined);
strictEqual(streamStats.bytesReceived, undefined);
strictEqual(streamStats.bytesSent, undefined);
strictEqual(streamStats.maxOffset, undefined);
strictEqual(streamStats.maxOffsetAcknowledged, undefined);
strictEqual(streamStats.maxOffsetReceived, undefined);
strictEqual(streamStats.finalSize, undefined);
strictEqual(streamStats.createdAt, 0n);
strictEqual(streamStats.openedAt, 0n);
strictEqual(streamStats.receivedAt, 0n);
strictEqual(streamStats.ackedAt, 0n);
strictEqual(streamStats.destroyedAt, 0n);
strictEqual(streamStats.bytesReceived, 0n);
strictEqual(streamStats.bytesSent, 0n);
strictEqual(streamStats.maxOffset, 0n);
strictEqual(streamStats.maxOffsetAcknowledged, 0n);
strictEqual(streamStats.maxOffsetReceived, 0n);
strictEqual(streamStats.finalSize, 0n);
strictEqual(typeof streamStats.toJSON(), 'object');
strictEqual(typeof inspect(streamStats), 'string');
streamStats[kFinishClose]();
strictEqual(typeof sessionStats.createdAt, 'bigint');
strictEqual(typeof sessionStats.closingAt, 'bigint');
strictEqual(typeof sessionStats.destroyedAt, 'bigint');
strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint');
strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint');
strictEqual(typeof sessionStats.gracefulClosingAt, 'bigint');
strictEqual(typeof sessionStats.bytesReceived, 'bigint');
strictEqual(typeof sessionStats.bytesSent, 'bigint');
strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint');
strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint');
strictEqual(typeof sessionStats.uniInStreamCount, 'bigint');
strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint');
strictEqual(typeof sessionStats.lossRetransmitCount, 'bigint');
strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint');
strictEqual(typeof sessionStats.bytesInFlight, 'bigint');
strictEqual(typeof sessionStats.blockCount, 'bigint');

View File

@ -60,6 +60,8 @@ require(fixtures.path('resolve-paths', 'default', 'verify-paths.js'));
{
// builtinModules.
builtinModules.forEach((mod) => {
// TODO(@jasnell): Remove once node:quic is no longer flagged
if (mod === 'node:quic') return;
assert.strictEqual(require.resolve.paths(mod), null);
if (!mod.startsWith('node:')) {
assert.strictEqual(require.resolve.paths(`node:${mod}`), null);

View File

@ -283,6 +283,31 @@ const customTypesMap = {
'Response': 'https://developer.mozilla.org/en-US/docs/Web/API/Response',
'Request': 'https://developer.mozilla.org/en-US/docs/Web/API/Request',
'Disposable': 'https://tc39.es/proposal-explicit-resource-management/#sec-disposable-interface',
'quic.QuicEndpoint': 'quic.html#class-quicendpoint',
'quic.QuicEndpoint.Stats': 'quic.html#class-quicendpointstats',
'quic.QuicSession': 'quic.html#class-quicsession',
'quic.QuicSession.Stats': 'quic.html#class-quicsessionstats',
'quic.QuicStream': 'quic.html#class-quicstream',
'quic.QuicStream.Stats': 'quic.html#class-quicstreamstats',
'quic.EndpointOptions': 'quic.html#type-endpointoptions',
'quic.SessionOptions': 'quic.html#type-sessionoptions',
'quic.ApplicationOptions': 'quic.html#type-applicationoptions',
'quic.TlsOptions': 'quic.html#type-tlsoptions',
'quic.TransportParams': 'quic.html#type-transportparams',
'quic.OnSessionCallback': 'quic.html#callback-onsessioncallback',
'quic.OnStreamCallback': 'quic.html#callback-onstreamcallback',
'quic.OnDatagramCallback': 'quic.html#callback-ondatagramcallback',
'quic.OnDatagramStatusCallback': 'quic.html#callback-ondatagramstatuscallback',
'quic.OnPathValidationCallback': 'quic.html#callback-onpathvalidationcallback',
'quic.OnSessionTicketCallback': 'quic.html#callback-onsessionticketcallback',
'quic.OnVersionNegotiationCallback': 'quic.html#callback-onversionnegotiationcallback',
'quic.OnHandshakeCallback': 'quic.html#callback-onhandshakecallback',
'quic.OnBlockedCallback': 'quic.html#callback-onblockedcallback',
'quic.OnStreamErrorCallback': 'quic.html#callback-onstreamerrorcallback',
'quic.OnHeadersCallback': 'quic.html#callback-onheaderscallback',
'quic.OnTrailersCallback': 'quic.html#callback-ontrailerscallback',
'quic.OnPullCallback': 'quic.html#callback-onpullcallback',
};
const arrayPart = /(?:\[])+$/;