mirror of https://github.com/nodejs/node.git
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:
parent
72537f5631
commit
062ae6f3cb
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
.
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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().
|
||||
{
|
||||
|
|
|
@ -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
|
@ -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)}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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
|
||||
|
|
3489
src/quic/session.cc
3489
src/quic/session.cc
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, ¶ms_);
|
||||
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, ¶ms_);
|
||||
|
||||
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_ == ¶ms_);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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_{};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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; }, {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = /(?:\[])+$/;
|
||||
|
|
Loading…
Reference in New Issue