diff --git a/doc/api/cli.md b/doc/api/cli.md index 87eda0deb66b46..4290f5461c6538 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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` + + + +Enables the experimental `node:quic` built-in module. + ### `--experimental-require-module` + +* `address` {string|net.SocketAddress} +* `options` {quic.SessionOptions} +* Returns: {Promise} a promise for a {quic.QuicSession} + +Initiate a new client-side session. + +```mjs +import { connect } from 'node:quic'; +import { Buffer } from 'node:buffer'; + +const enc = new TextEncoder(); +const alpn = 'foo'; +const client = await connect('123.123.123.123:8888', { alpn }); +await client.createUnidirectionalStream({ + body: enc.encode('hello world'), +}); +``` + +By default, every call to `connect(...)` will create a new local +`QuicEndpoint` instance bound to a new random local IP port. To +specify the exact local address to use, or to multiplex multiple +QUIC sessions over a single local port, pass the `endpoint` option +with either a `QuicEndpoint` or `EndpointOptions` as the argument. ```mjs -import { QuicEndpoint } from 'node:quic'; +import { QuicEndpoint, connect } from 'node:quic'; + +const endpoint = new QuicEndpoint({ + address: '127.0.0.1:1234', +}); + +const client = await connect('123.123.123.123:8888', { endpoint }); +``` + +## `quic.listen(onsession,[options])` + + -const endpoint = new QuicEndpoint(); +* `onsession` {quic.OnSessionCallback} +* `options` {quic.SessionOptions} +* Returns: {Promise} a promise for a {quic.QuicEndpoint} + +Configures the endpoint to listen as a server. When a new session is initiated by +a remote peer, the given `onsession` callback will be invoked with the created +session. + +```mjs +import { listen } from 'node:quic'; -// Server... -endpoint.listen((session) => { - session.onstream = (stream) => { - // Handle the stream.... - }; +const endpoint = await listen((session) => { + // ... handle the session }); -// Client... -const client = endpoint.connect('123.123.123.123:8888'); -const stream = client.openBidirectionalStream(); +// Closing the endpoint allows any sessions open when close is called +// to complete naturally while preventing new sessions from being +// initiated. Once all existing sessions have finished, the endpoint +// will be destroyed. The call returns a promise that is resolved once +// the endpoint is destroyed. +await endpoint.close(); ``` +By default, every call to `listen(...)` will create a new local +`QuicEndpoint` instance bound to a new random local IP port. To +specify the exact local address to use, or to multiplex multiple +QUIC sessions over a single local port, pass the `endpoint` option +with either a `QuicEndpoint` or `EndpointOptions` as the argument. + +At most, any single `QuicEndpoint` can only be configured to listen as +a server once. + +## Class: `QuicEndpoint` + +A `QuicEndpoint` encapsulates the local UDP-port binding for QUIC. It can be +used as both a client and a server. + ### `new QuicEndpoint([options])` -* `address` {string|net.SocketAddress} -* `options` {quic.SessionOptions} -* Returns: {quic.QuicSession} +* {boolean} -Initiate a new client-side session using this endpoint. +True if `endpoint.close()` has been called and closing the endpoint has not yet completed. +Read only. ### `endpoint.destroy([error])` @@ -130,10 +190,9 @@ added: REPLACEME --> * `error` {any} -* Returns: {Promise} Forcefully closes the endpoint by forcing all open sessions to be immediately -closed. Returns `endpoint.closed`. +closed. ### `endpoint.destroyed` @@ -145,56 +204,13 @@ added: REPLACEME True if `endpoint.destroy()` has been called. Read only. -### `endpoint.listen([onsession,][options])` - - - -* `onsession` {quic.OnSessionCallback} -* `options` {quic.SessionOptions} - -Configures the endpoint to listen as a server. When a new session is initiated by -a remote peer, the given `onsession` callback will be invoked with the created -session. - -The `onsession` callback must be specified either here or by setting the `onsession` -property or an error will be thrown. - -### `endpoint.onsession` - -* {quic.OnSessionCallback} - -The callback function that is invoked when a new session is initiated by a remote peer. -Read/write. - -### `endpoint.sessions` - - - -* {Iterator} of {quic.QuicSession}. - -An iterator over all sessions associated with this endpoint. Read only. - -### `endpoint.state` - - - -* {quic.QuicEndpointState} - -The state associated with an active session. Read only. - ### `endpoint.stats` -* {quic.QuicEndpointStats} +* {quic.QuicEndpoint.Stats} The statistics collected for an active session. Read only. @@ -207,64 +223,7 @@ added: REPLACEME Calls `endpoint.close()` and returns a promise that fulfills when the endpoint has closed. -## Class: `QuicEndpointState` - - - -A view of the internal state of an endpoint. - -### `endpointState.isBound` - - - -* {boolean} True if the endpoint is bound to a local UDP port. - -### `endpointState.isReceiving` - - - -* {boolean} True if the endpoint is bound to a local UDP port and actively listening - for packets. - -### `endpointState.isListening` - - - -* {boolean} True if the endpoint is listening as a server. - -### `endpointState.isClosing` - - - -* {boolean} True if the endpoint is in the process of closing down. - -### `endpointState.isBusy` - - - -* {boolean} True if the endpoint has been marked busy. - -### `endpointState.pendingCallbacks` - - - -* {bigint} The total number of pending callbacks the endpoint is waiting on currently. - -## Class: `QuicEndpointStats` +## Class: `QuicEndpoint.Stats` +A `QuicSession` represents the local side of a QUIC connection. + ### `session.close()` * `error` {any} -* Returns: {Promise} + +Immediately destroy the session. All streams will be destroys and the +session will be closed. ### `session.destroyed` @@ -416,6 +386,8 @@ added: REPLACEME * {boolean} +True if `session.destroy()` has been called. Read only. + ### `session.endpoint` * `options` {Object} - * `headers` {Object} -* Returns: {quic.QuicStream} + * `body` {ArrayBuffer | ArrayBufferView | Blob} + * `sendOrder` {number} +* Returns: {Promise} for a {quic.QuicStream} -### `session.openUnidirectionalStream([options])` +Open a new bidirectional stream. If the `body` option is not specified, +the outgoing stream will be half-closed. + +### `session.createUnidirectionalStream([options])` * `options` {Object} - * `headers` {Object -* Returns: {quic.QuicStream} + * `body` {ArrayBuffer | ArrayBufferView | Blob} + * `sendOrder` {number} +* Returns: {Promise} for a {quic.QuicStream} + +Open a new unidirectional stream. If the `body` option is not specified, +the outgoing stream will be closed. ### `session.path` @@ -510,22 +506,20 @@ added: REPLACEME * `local` {net.SocketAddress} * `remote` {net.SocketAddress} +The local and remote socket addresses associated with the session. Read only. + ### `session.sendDatagram(datagram)` -* `datagram` {Uint8Array} +* `datagram` {string|ArrayBufferView} * Returns: {bigint} -### `session.state` - - - -* {quic.QuicSessionState} +Sends an unreliable datagram to the remote peer, returning the datagram ID. +If the datagram payload is specified as an `ArrayBufferView`, then ownership of +that view will be transfered to the underlying stream. ### `session.stats` @@ -533,7 +527,9 @@ added: REPLACEME added: REPLACEME --> -* {quic.QuicSessionStats} +* {quic.QuicSession.Stats} + +Return the current statistics for the session. Read only. ### `session.updateKey()` @@ -541,131 +537,136 @@ added: REPLACEME added: REPLACEME --> +Initiate a key update for the session. + ### `session[Symbol.asyncDispose]()` -## Class: `QuicSessionState` +Calls `session.close()` and returns a promise that fulfills when the +session has closed. + +## Class: `QuicSession.Stats` -### `sessionState.hasPathValidationListener` +### `sessionStats.createdAt` -* {boolean} +* {bigint} -### `sessionState.hasVersionNegotiationListener` +### `sessionStats.closingAt` -* {boolean} +* {bigint} -### `sessionState.hasDatagramListener` +### `sessionStats.handshakeCompletedAt` -* {boolean} +* {bigint} -### `sessionState.hasSessionTicketListener` +### `sessionStats.handshakeConfirmedAt` -* {boolean} +* {bigint} -### `sessionState.isClosing` +### `sessionStats.bytesReceived` -* {boolean} +* {bigint} -### `sessionState.isGracefulClose` +### `sessionStats.bytesSent` -* {boolean} +* {bigint} -### `sessionState.isSilentClose` +### `sessionStats.bidiInStreamCount` -* {boolean} +* {bigint} -### `sessionState.isStatelessReset` +### `sessionStats.bidiOutStreamCount` -* {boolean} +* {bigint} -### `sessionState.isDestroyed` +### `sessionStats.uniInStreamCount` -* {boolean} +* {bigint} -### `sessionState.isHandshakeCompleted` +### `sessionStats.uniOutStreamCount` -* {boolean} +* {bigint} -### `sessionState.isHandshakeConfirmed` +### `sessionStats.maxBytesInFlights` -* {boolean} +* {bigint} -### `sessionState.isStreamOpenAllowed` +### `sessionStats.bytesInFlight` -* {boolean} +* {bigint} -### `sessionState.isPrioritySupported` +### `sessionStats.blockCount` -* {boolean} +* {bigint} -### `sessionState.isWrapped` +### `sessionStats.cwnd` -* {boolean} +* {bigint} -### `sessionState.lastDatagramId` +### `sessionStats.latestRtt` -### `sessionStats.createdAt` +* {bigint} + +### `sessionStats.rttVar` -* {bigint} - -### `sessionStats.bidiInStreamCount` +### `stream.closed` -* {bigint} +* {Promise} -### `sessionStats.bidiOutStreamCount` +A promise that is fulfilled when the stream is fully closed. + +### `stream.destroy([error])` -* {bigint} +* `error` {any} -### `sessionStats.uniInStreamCount` +Immediately and abruptly destroys the stream. + +### `stream.destroyed` -* {bigint} +* {boolean} -### `sessionStats.uniOutStreamCount` +True if `stream.destroy()` has been called. + +### `stream.direction` -* {bigint} +* {string} One of either `'bidi'` or `'uni'`. + +The directionality of the stream. Read only. -### `sessionStats.lossRetransmitCount` +### `stream.id` -* {bigint} +* {quic.OnBlockedCallback} -### `sessionStats.bytesInFlight` +The callback to invoke when the stream is blocked. Read/write. + +### `stream.onreset` -* {bigint} +* {quic.OnStreamErrorCallback} -### `sessionStats.blockCount` +The callback to invoke when the stream is reset. Read/write. + +### `stream.readable` -* {bigint} +* {ReadableStream} -### `sessionStats.cwnd` +### `stream.session` -* {bigint} +* {quic.QuicSession} -### `sessionStats.latestRtt` +The session that created this stream. Read only. + +### `stream.stats` -* {bigint} +* {quic.QuicStream.Stats} -### `sessionStats.minRtt` +The current statistics for the stream. Read only. + +## Class: `QuicStream.Stats` -* {bigint} - -### `sessionStats.rttVar` +### `streamStats.ackedAt` - -* {bigint} - -### `sessionStats.datagramsReceived` - - - -* {bigint} - -### `sessionStats.datagramsSent` - - - -* {bigint} - -### `sessionStats.datagramsAcknowledged` - - - -* {bigint} - -### `sessionStats.datagramsLost` - - - -* {bigint} - -## Class: `QuicStream` - - - -### `stream.closed` - - - -* {Promise} - -### `stream.destroy([error])` - - - -* `error` {any} -* Returns: {Promise} - -### `stream.destroyed` - - - -* {boolean} - -### `stream.direction` - - - -* {string} One of either `'bidi'` or `'uni'`. - -### `stream.id` - - - -* {bigint} - -### `stream.onblocked` - - - -* {quic.OnBlockedCallback} - -### `stream.onheaders` - - - -* {quic.OnHeadersCallback} - -### `stream.onreset` - - - -* {quic.OnStreamErrorCallback} - -### `stream.ontrailers` - - - -* {quic.OnTrailersCallback} - -### `stream.pull(callback)` - - - -* `callback` {quic.OnPullCallback} - -### `stream.sendHeaders(headers)` - - - -* `headers` {Object} - -### `stream.session` - - - -* {quic.QuicSession} - -### `stream.state` - - - -* {quic.QuicStreamState} - -### `stream.stats` - - - -* {quic.QuicStreamStats} - -## Class: `QuicStreamState` - - - -### `streamState.destroyed` - - - -* {boolean} - -### `streamState.finReceived` - - - -* {boolean} - -### `streamState.finSent` - - - -* {boolean} - -### `streamState.hasReader` - - - -* {boolean} - -### `streamState.id` - - - -* {bigint} - -### `streamState.paused` - - - -* {boolean} - -### `streamState.pending` - - - -* {boolean} - -### `streamState.readEnded` - - - -* {boolean} - -### `streamState.reset` - - - -* {boolean} - -### `streamState.wantsBlock` - - - -* {boolean} - -### `streamState.wantsHeaders` - - - -* {boolean} - -### `streamState.wantsReset` - - - -* {boolean} - -### `streamState.wantsTrailers` - - - -* {boolean} - -### `streamState.writeEnded` - - - -* {boolean} - -## Class: `QuicStreamStats` - - - -### `streamStats.ackedAt` - - - -* {bigint} - -### `streamStats.bytesReceived` - - - -* {bigint} - -### `streamStats.bytesSent` +### `streamStats.bytesSent` - -* {bigint} - -### `streamStats.finalSize` - - - -* {bigint} - -### `streamStats.isConnected` - - - -* {bigint} - -### `streamStats.maxOffset` - - - -* {bigint} - -### `streamStats.maxOffsetAcknowledged` - - - -* {bigint} - -### `streamStats.maxOffsetReceived` +### `streamStats.destroyedAt` - -#### `applicationOptions.maxHeaderPairs` - - - -* {bigint|number} The maximum number of header pairs that can be received. - -#### `applicationOptions.maxHeaderLength` - - - -* {bigint|number} The maximum number of header bytes that can be received. - -#### `applicationOptions.maxFieldSectionSize` - - - -* {bigint|number} The maximum header field section size. - -#### `applicationOptions.qpackMaxDTableCapacity` +### `streamStats.maxOffset` -* {bigint|number} The QPack maximum dynamic table capacity. +* {bigint} -#### `applicationOptions.qpackEncoderMaxDTableCapacity` +### `streamStats.maxOffsetAcknowledged` -* {bigint|number} The QPack encoder maximum dynamic table capacity. +* {bigint} -#### `applicationOptions.qpackBlockedStreams` +### `streamStats.maxOffsetReceived` -* {bigint|number} The maximum number of QPack blocked streams. +* {bigint} -#### `applicationOptions.enableConnectProtocol` +### `streamStats.openedAt` -* {boolean} - -True to allow use of the `CONNECT` method when using HTTP/3. +* {bigint} -#### `applicationOptions.enableDatagrams` +### `streamStats.receivedAt` -* {boolean} +* {bigint} -True to allow use of unreliable datagrams. +## Types ### Type: `EndpointOptions` @@ -1310,6 +954,8 @@ added: REPLACEME * {Object} +The endpoint configuration options passed when constructing a new `QuicEndpoint` instance. + #### `endpointOptions.address` - -* {string|number} - -Specifies the congestion control algorithm that will be used by all sessions -using this endpoint. Must be set to one of: - -* `QuicEndpoint.CC_ALGO_RENO` -* `QuicEndpoint.CC_ALGP_RENO_STR` -* `QuicEndpoint.CC_ALGO_CUBIC` -* `QuicEndpoint.CC_ALGO_CUBIC_STR` -* `QuicEndpoint.CC_ALGO_BBR` -* `QuicEndpoint.CC_ALGO_BBR_STR` - -This is an advanced option that users typically won't have need to specify -unless. - -#### `endpointOptions.disableActiveMigration` - - - -* {boolean} - -When `true`, this option disables the ability for a session to migrate to a different -socket address. - -\*\* THIS OPTION IS AT RISK OF BEING DROPPED \*\* - -#### `endpointOptions.handshakeTimeout` - - - -* {bigint|number} - -Specifies the maximum number of milliseconds a TLS handshake is permitted to take -to complete before timing out. - #### `endpointOptions.ipv6Only` - -* {bigint|number} - -Specifies the maximum UDP packet payload size. - #### `endpointOptions.maxRetries` - -* {bigint|number} - -Specifies the maximum stream flow-control window size. - -#### `endpointOptions.maxWindow` - - - -* {bigint|number} - -Specifies the maxumum session flow-control window size. - -#### `endpointOptions.noUdpPayloadSizeShaping` - - - -* {boolean} - #### `endpointOptions.retryTokenExpiration` -* {bigint|number} +* {boolean} -Specifies the maximum number of unacknowledged packets a session should allow. +When `true`, requires that the endpoint validate peer addresses using retry packets +while establishing a new connection. -#### `endpointOptions.validateAddress` +### Type: `SessionOptions` -* {boolean} +#### `sessionOptions.alpn` -When `true`, requires that the endpoint validate peer addresses while establishing -a connection. + -### Type: `SessionOptions` +* {string} + +The ALPN protocol identifier. + +#### `sessionOptions.ca` -#### `sessionOptions.application` +* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} + +The CA certificates to use for sessions. + +#### `sessionOptions.cc` -* {quic.ApplicationOptions} +* {string} + +Specifies the congestion control algorithm that will be used +. Must be set to one of either `'reno'`, `'cubic'`, or `'bbr'`. -The application-level options to use for the session. +This is an advanced option that users typically won't have need to specify. -#### `sessionOptions.minVersion` +#### `sessionOptions.certs` -* {number} +* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} -The minimum QUIC version number to allow. This is an advanced option that users -typically won't have need to specify. +The TLS certificates to use for sessions. -#### `sessionOptions.preferredAddressPolicy` +#### `sessionOptions.ciphers` -* {string} One of `'use'`, `'ignore'`, or `'default'`. +* {string} -When the remote peer advertises a preferred address, this option specifies whether -to use it or ignore it. +The list of supported TLS 1.3 cipher algorithms. -#### `sessionOptions.qlog` +#### `sessionOptions.crl` -* {boolean} +* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} -True if qlog output should be enabled. +The CRL to use for sessions. -#### `sessionOptions.sessionTicket` +#### `sessionOptions.groups` -* {ArrayBufferView} A session ticket to use for 0RTT session resumption. +* {string} + +The list of support TLS 1.3 cipher groups. -#### `sessionOptions.tls` +#### `sessionOptions.keylog` -* {quic.TlsOptions} +* {boolean} -The TLS options to use for the session. +True to enable TLS keylogging output. -#### `sessionOptions.transportParams` +#### `sessionOptions.keys` -* {quic.TransportParams} +* {KeyObject|CryptoKey|KeyObject\[]|CryptoKey\[]} -The QUIC transport parameters to use for the session. +The TLS crypto keys to use for sessions. -#### `sessionOptions.version` +#### `sessionOptions.maxPayloadSize` -* {number} +* {bigint|number} -The QUIC version number to use. This is an advanced option that users typically -won't have need to specify. +Specifies the maximum UDP packet payload size. -### Type: `TlsOptions` +#### `sessionOptions.maxStreamWindow` -#### `tlsOptions.sni` +* {bigint|number} + +Specifies the maximum stream flow-control window size. + +#### `sessionOptions.maxWindow` -* {string} +* {bigint|number} -The peer server name to target. +Specifies the maxumum session flow-control window size. -#### `tlsOptions.alpn` +#### `sessionOptions.minVersion` -* {string} +* {number} -The ALPN protocol identifier. +The minimum QUIC version number to allow. This is an advanced option that users +typically won't have need to specify. -#### `tlsOptions.ciphers` +#### `sessionOptions.preferredAddressPolicy` -* {string} +* {string} One of `'use'`, `'ignore'`, or `'default'`. -The list of supported TLS 1.3 cipher algorithms. +When the remote peer advertises a preferred address, this option specifies whether +to use it or ignore it. -#### `tlsOptions.groups` +#### `sessionOptions.qlog` -* {string} +* {boolean} -The list of support TLS 1.3 cipher groups. +True if qlog output should be enabled. -#### `tlsOptions.keylog` +#### `sessionOptions.sessionTicket` -* {boolean} +* {ArrayBufferView} A session ticket to use for 0RTT session resumption. -True to enable TLS keylogging output. +#### `sessionOptions.handshakeTimeout` + + + +* {bigint|number} + +Specifies the maximum number of milliseconds a TLS handshake is permitted to take +to complete before timing out. -#### `tlsOptions.verifyClient` +#### `sessionOptions.sni` -* {boolean} +* {string} -True to require verification of TLS client certificate. +The peer server name to target. -#### `tlsOptions.tlsTrace` +#### `sessionOptions.tlsTrace` -* {boolean} +* {quic.TransportParams} -True to require private key verification. +The QUIC transport parameters to use for the session. -#### `tlsOptions.keys` +#### `sessionOptions.unacknowledgedPacketThreshold` -* {KeyObject|CryptoKey|KeyObject\[]|CryptoKey\[]} +* {bigint|number} -The TLS crypto keys to use for sessions. +Specifies the maximum number of unacknowledged packets a session should allow. -#### `tlsOptions.certs` +#### `sessionOptions.verifyClient` -* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} +* {boolean} -The TLS certificates to use for sessions. +True to require verification of TLS client certificate. -#### `tlsOptions.ca` +#### `sessionOptions.verifyPrivateKey` -* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} +* {boolean} -The CA certificates to use for sessions. +True to require private key verification. -#### `tlsOptions.crl` +#### `sessionOptions.version` -* {ArrayBuffer|ArrayBufferView|ArrayBuffer\[]|ArrayBufferView\[]} +* {number} -The CRL to use for sessions. +The QUIC version number to use. This is an advanced option that users typically +won't have need to specify. ### Type: `TransportParams` @@ -1874,14 +1465,6 @@ added: REPLACEME * {bigint|number} -#### `transportParams.disableActiveMigration` - - - -* {boolean} - ## Callbacks ### Callback: `OnSessionCallback` @@ -1990,32 +1573,6 @@ added: REPLACEME * `this` {quic.QuicStream} * `error` {any} -### Callback: `OnHeadersCallback` - - - -* `this` {quic.QuicStream} -* `headers` {Object} -* `kind` {string} - -### Callback: `OnTrailersCallback` - - - -* `this` {quic.QuicStream} - -### Callback: `OnPullCallback` - - - -* `chunks` {Uint8Array\[]} - ## Diagnostic Channels ### Channel: `quic.endpoint.created` diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 43a7ae5ac34d9c..6a526de7bfeb73 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -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, }; diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 0a047f4e2cf034..afe057de5bd951 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -8,10 +8,10 @@ const { ArrayBufferPrototypeTransfer, ArrayIsArray, ArrayPrototypePush, + BigInt, ObjectDefineProperties, SafeSet, SymbolAsyncDispose, - SymbolIterator, Uint8Array, } = primordials; @@ -29,12 +29,10 @@ let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { const { Endpoint: Endpoint_, + Http3Application: Http3, setCallbacks, // The constants to be exposed to end users for various options. - CC_ALGO_RENO, - CC_ALGO_CUBIC, - CC_ALGO_BBR, CC_ALGO_RENO_STR, CC_ALGO_CUBIC_STR, CC_ALGO_BBR_STR, @@ -71,6 +69,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, + ERR_MISSING_ARGS, ERR_QUIC_APPLICATION_ERROR, ERR_QUIC_CONNECTION_FAILED, ERR_QUIC_ENDPOINT_CLOSED, @@ -87,7 +86,9 @@ const { } = require('internal/socketaddress'); const { + createBlobReaderStream, isBlob, + kHandle: kBlobHandle, } = require('internal/blob'); const { @@ -96,10 +97,11 @@ const { } = require('internal/crypto/keys'); const { + validateBoolean, validateFunction, + validateNumber, validateObject, validateString, - validateBoolean, } = require('internal/validators'); const { @@ -109,7 +111,9 @@ const { const kEmptyObject = { __proto__: null }; const { + kApplicationProvider, kBlocked, + kConnect, kDatagram, kDatagramStatus, kFinishClose, @@ -117,18 +121,25 @@ const { kHeaders, kOwner, kRemoveSession, + kListen, kNewSession, kRemoveStream, kNewStream, + kOnHeaders, + kOnTrailers, kPathValidation, + kPrivateConstructor, kReset, + kSendHeaders, kSessionTicket, + kState, kTrailers, kVersionNegotiation, kInspect, kKeyObjectHandle, kKeyObjectInner, - kPrivateConstructor, + kWantsHeaders, + kWantsTrailers, } = require('internal/quic/symbols'); const { @@ -173,49 +184,29 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @typedef {import('../crypto/keys.js').CryptoKey} CryptoKey */ +/** + * @typedef {object} OpenStreamOptions + * @property {ArrayBuffer|ArrayBufferView|Blob} [body] The outbound payload + * @property {number} [sendOrder] The ordering of this stream relative to others in the same session. + */ + /** * @typedef {object} EndpointOptions - * @property {SocketAddress} [address] The local address to bind to - * @property {bigint|number} [retryTokenExpiration] The retry token expiration - * @property {bigint|number} [tokenExpiration] The token expiration + * @property {string|SocketAddress} [address] The local address to bind to + * @property {bigint|number} [addressLRUSize] The size of the address LRU cache + * @property {boolean} [ipv6Only] Use IPv6 only * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections - * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host - * @property {bigint|number} [addressLRUSize] The size of the address LRU cache * @property {bigint|number} [maxRetries] The maximum number of retries - * @property {bigint|number} [maxPayloadSize] The maximum payload size - * @property {bigint|number} [unacknowledgedPacketThreshold] The unacknowledged packet threshold - * @property {bigint|number} [handshakeTimeout] The handshake timeout - * @property {bigint|number} [maxStreamWindow] The maximum stream window - * @property {bigint|number} [maxWindow] The maximum window - * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) - * @property {number} [txDiagnosticLoss] The transmit diagnostic loss probability (range 0.0-1.0) + * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host + * @property {ArrayBufferView} [resetTokenSecret] The reset token secret + * @property {bigint|number} [retryTokenExpiration] The retry token expiration + * @property {bigint|number} [tokenExpiration] The token expiration + * @property {ArrayBufferView} [tokenSecret] The token secret * @property {number} [udpReceiveBufferSize] The UDP receive buffer size * @property {number} [udpSendBufferSize] The UDP send buffer size * @property {number} [udpTTL] The UDP TTL - * @property {boolean} [noUdpPayloadSizeShaping] Disable UDP payload size shaping - * @property {boolean} [validateAddress] Validate the address - * @property {boolean} [disableActiveMigration] Disable active migration - * @property {boolean} [ipv6Only] Use IPv6 only - * @property {'reno'|'cubic'|'bbr'|number} [cc] The congestion control algorithm - * @property {ArrayBufferView} [resetTokenSecret] The reset token secret - * @property {ArrayBufferView} [tokenSecret] The token secret - */ - -/** - * @typedef {object} TlsOptions - * @property {string} [sni] The server name indication - * @property {string} [alpn] The application layer protocol negotiation - * @property {string} [ciphers] The ciphers - * @property {string} [groups] The groups - * @property {boolean} [keylog] Enable key logging - * @property {boolean} [verifyClient] Verify the client - * @property {boolean} [tlsTrace] Enable TLS tracing - * @property {boolean} [verifyPrivateKey] Verify the private key - * @property {KeyObject|CryptoKey|Array} [keys] The keys - * @property {ArrayBuffer|ArrayBufferView|Array} [certs] The certificates - * @property {ArrayBuffer|ArrayBufferView|Array} [ca] The certificate authority - * @property {ArrayBuffer|ArrayBufferView|Array} [crl] The certificate revocation list + * @property {boolean} [validateAddress] Validate the address using retry packets */ /** @@ -233,7 +224,6 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @property {bigint|number} [ackDelayExponent] The acknowledgment delay exponent * @property {bigint|number} [maxAckDelay] The maximum acknowledgment delay * @property {bigint|number} [maxDatagramFrameSize] The maximum datagram frame size - * @property {boolean} [disableActiveMigration] Disable active migration */ /** @@ -250,14 +240,32 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); /** * @typedef {object} SessionOptions + * @property {EndpointOptions|QuicEndpoint} [endpoint] An endpoint to use. * @property {number} [version] The version * @property {number} [minVersion] The minimum version * @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy * @property {ApplicationOptions} [application] The application options * @property {TransportParams} [transportParams] The transport parameters - * @property {TlsOptions} [tls] The TLS options + * @property {string} [servername] The server name identifier + * @property {string} [protocol] The application layer protocol negotiation + * @property {string} [ciphers] The ciphers + * @property {string} [groups] The groups + * @property {boolean} [keylog] Enable key logging + * @property {boolean} [verifyClient] Verify the client + * @property {boolean} [tlsTrace] Enable TLS tracing + * @property {boolean} [verifyPrivateKey] Verify the private key + * @property {KeyObject|CryptoKey|Array} [keys] The keys + * @property {ArrayBuffer|ArrayBufferView|Array} [certs] The certificates + * @property {ArrayBuffer|ArrayBufferView|Array} [ca] The certificate authority + * @property {ArrayBuffer|ArrayBufferView|Array} [crl] The certificate revocation list * @property {boolean} [qlog] Enable qlog * @property {ArrayBufferView} [sessionTicket] The session ticket + * @property {bigint|number} [handshakeTimeout] The handshake timeout + * @property {bigint|number} [maxStreamWindow] The maximum stream window + * @property {bigint|number} [maxWindow] The maximum window + * @property {bigint|number} [maxPayloadSize] The maximum payload size + * @property {bigint|number} [unacknowledgedPacketThreshold] The unacknowledged packet threshold + * @property {'reno'|'cubic'|'bbr'} [cc] The congestion control algorithm */ /** @@ -295,26 +303,6 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @returns {void} */ -/** - * @callback OnDatagramStatusCallback - * @this {QuicSession} - * @param {bigint} id - * @param {'lost'|'acknowledged'} status - * @returns {void} - */ - -/** - * @callback OnPathValidationCallback - * @this {QuicSession} - * @param {'aborted'|'failure'|'success'} result - * @param {SocketAddress} newLocalAddress - * @param {SocketAddress} newRemoteAddress - * @param {SocketAddress} oldLocalAddress - * @param {SocketAddress} oldRemoteAddress - * @param {boolean} preferredAddress - * @returns {void} - */ - /** * @callback OnSessionTicketCallback * @this {QuicSession} @@ -322,28 +310,6 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @returns {void} */ -/** - * @callback OnVersionNegotiationCallback - * @this {QuicSession} - * @param {number} version - * @param {number[]} requestedVersions - * @param {number[]} supportedVersions - * @returns {void} - */ - -/** - * @callback OnHandshakeCallback - * @this {QuicSession} - * @param {string} sni - * @param {string} alpn - * @param {string} cipher - * @param {string} cipherVersion - * @param {string} validationErrorReason - * @param {number} validationErrorCode - * @param {boolean} earlyDataAccepted - * @returns {void} - */ - /** * @callback OnBlockedCallback * @this {QuicStream} stream @@ -381,26 +347,30 @@ const onSessionHandshakeChannel = dc.channel('quic.session.handshake'); * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host * @property {bigint|number} [addressLRUSize] The size of the address LRU cache - * @property {bigint|number} [maxRetries] The maximum number of retries - * @property {bigint|number} [maxPayloadSize] The maximum payload size - * @property {bigint|number} [unacknowledgedPacketThreshold] The unacknowledged packet threshold - * @property {bigint|number} [handshakeTimeout] The handshake timeout - * @property {bigint|number} [maxStreamWindow] The maximum stream window - * @property {bigint|number} [maxWindow] The maximum window + * @property {bigint|number} [maxRetries] The maximum number of retriesw * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) * @property {number} [txDiagnosticLoss] The transmit diagnostic loss probability (range 0.0-1.0) * @property {number} [udpReceiveBufferSize] The UDP receive buffer size * @property {number} [udpSendBufferSize] The UDP send buffer size * @property {number} [udpTTL] The UDP TTL - * @property {boolean} [noUdpPayloadSizeShaping] Disable UDP payload size shaping * @property {boolean} [validateAddress] Validate the address - * @property {boolean} [disableActiveMigration] Disable active migration * @property {boolean} [ipv6Only] Use IPv6 only - * @property {'reno'|'cubic'|'bbr'|number} [cc] The congestion control algorithm * @property {ArrayBufferView} [resetTokenSecret] The reset token secret * @property {ArrayBufferView} [tokenSecret] The token secret */ +/** + * @typedef {object} QuicSessionInfo + * @property {SocketAddress} local The local address + * @property {SocketAddress} remote The remote address + * @property {string} protocol The alpn protocol identifier negotiated for this session + * @property {string} servername The servername identifier for this session + * @property {string} cipher The cipher suite negotiated for this session + * @property {string} cipherVersion The version of the cipher suite negotiated for this session + * @property {string} [validationErrorReason] The reason the session failed validation (if any) + * @property {string} [validationErrorCode] The error code for the validation failure (if any) + */ + setCallbacks({ // QuicEndpoint callbacks @@ -459,23 +429,20 @@ setCallbacks({ /** * Called when the session handshake completes. - * @param {string} sni - * @param {string} alpn + * @param {string} servername + * @param {string} protocol * @param {string} cipher * @param {string} cipherVersion * @param {string} validationErrorReason * @param {number} validationErrorCode - * @param {boolean} earlyDataAccepted */ - onSessionHandshake(sni, alpn, cipher, cipherVersion, + onSessionHandshake(servername, protocol, cipher, cipherVersion, validationErrorReason, - validationErrorCode, - earlyDataAccepted) { - debug('session handshake callback', sni, alpn, cipher, cipherVersion, - validationErrorReason, validationErrorCode, earlyDataAccepted); - this[kOwner][kHandshake](sni, alpn, cipher, cipherVersion, - validationErrorReason, validationErrorCode, - earlyDataAccepted); + validationErrorCode) { + debug('session handshake callback', servername, protocol, cipher, cipherVersion, + validationErrorReason, validationErrorCode); + this[kOwner][kHandshake](servername, protocol, cipher, cipherVersion, + validationErrorReason, validationErrorCode); }, /** @@ -490,8 +457,11 @@ setCallbacks({ onSessionPathValidation(result, newLocalAddress, newRemoteAddress, oldLocalAddress, oldRemoteAddress, preferredAddress) { debug('session path validation callback', this[kOwner]); - this[kOwner][kPathValidation](result, newLocalAddress, newRemoteAddress, - oldLocalAddress, oldRemoteAddress, + this[kOwner][kPathValidation](result, + new InternalSocketAddress(newLocalAddress), + new InternalSocketAddress(newRemoteAddress), + new InternalSocketAddress(oldLocalAddress), + new InternalSocketAddress(oldRemoteAddress), preferredAddress); }, @@ -568,6 +538,24 @@ setCallbacks({ }, }); +function validateBody(body) { + // TODO(@jasnell): Support streaming sources + if (body === undefined) return body; + if (isArrayBuffer(body)) return ArrayBufferPrototypeTransfer(body); + if (isArrayBufferView(body)) { + const size = body.byteLength; + const offset = body.byteOffset; + return new Uint8Array(ArrayBufferPrototypeTransfer(body.buffer), offset, size); + } + if (isBlob(body)) return body[kBlobHandle]; + + throw new ERR_INVALID_ARG_TYPE('options.body', [ + 'ArrayBuffer', + 'ArrayBufferView', + 'Blob', + ], body); +} + class QuicStream { /** @type {object} */ #handle; @@ -590,6 +578,7 @@ class QuicStream { /** @type {Promise} */ #pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials #reader; + #readable; /** * @param {symbol} privateSymbol @@ -617,6 +606,14 @@ class QuicStream { } } + get readable() { + if (this.#readable === undefined) { + assert(this.#reader); + this.#readable = createBlobReaderStream(this.#reader); + } + return this.#readable; + } + /** @type {boolean} */ get pending() { return this.#state.pending; } @@ -649,30 +646,30 @@ class QuicStream { } /** @type {OnHeadersCallback} */ - get onheaders() { return this.#onheaders; } + get [kOnHeaders]() { return this.#onheaders; } - set onheaders(fn) { + set [kOnHeaders](fn) { if (fn === undefined) { this.#onheaders = undefined; - this.#state.wantsHeaders = false; + this.#state[kWantsHeaders] = false; } else { validateFunction(fn, 'onheaders'); this.#onheaders = fn.bind(this); - this.#state.wantsHeaders = true; + this.#state[kWantsHeaders] = true; } } /** @type {OnTrailersCallback} */ - get ontrailers() { return this.#ontrailers; } + get [kOnTrailers]() { return this.#ontrailers; } - set ontrailers(fn) { + set [kOnTrailers](fn) { if (fn === undefined) { this.#ontrailers = undefined; - this.#state.wantsTrailers = false; + this.#state[kWantsTrailers] = false; } else { validateFunction(fn, 'ontrailers'); this.#ontrailers = fn.bind(this); - this.#state.wantsTrailers = true; + this.#state[kWantsTrailers] = true; } } @@ -710,8 +707,58 @@ class QuicStream { return this.#pendingClose.promise; } - pull(callback) { - this.#reader.pull(callback); + /** + * @param {ArrayBuffer|ArrayBufferView|Blob} outbound + */ + setOutbound(outbound) { + if (this.destroyed) { + throw new ERR_INVALID_STATE('Stream is destroyed'); + } + if (this.#state.hasOutbound) { + throw new ERR_INVALID_STATE('Stream already has an outbound data source'); + } + this.#handle.attachSource(validateBody(outbound)); + } + + /** + * @param {bigint} code + */ + stopSending(code = 0n) { + if (this.destroyed) { + throw new ERR_INVALID_STATE('Stream is destroyed'); + } + this.#handle.stopSending(BigInt(code)); + } + + /** + * @param {bigint} code + */ + resetStream(code = 0n) { + if (this.destroyed) { + throw new ERR_INVALID_STATE('Stream is destroyed'); + } + this.#handle.resetStream(BigInt(code)); + } + + /** @type {'default' | 'low' | 'high'} */ + get priority() { + if (this.destroyed || !this.session.state.isPrioritySupported) return undefined; + switch (this.#handle.getPriority()) { + case 3: return 'default'; + case 7: return 'low'; + case 0: return 'high'; + default: return 'default'; + } + } + + set priority(val) { + if (this.destroyed || !this.session.state.isPrioritySupported) return; + switch (val) { + case 'default': this.#handle.setPriority(3, 1); break; + case 'low': this.#handle.setPriority(7, 1); break; + case 'high': this.#handle.setPriority(0, 1); break; + } + // Otherwise ignore the value as invalid. } /** @@ -719,10 +766,14 @@ class QuicStream { * of key, value pairs. The reason we don't use a Headers object * here is because this needs to be able to represent headers like * :method which the high-level Headers API does not allow. + * + * Note that QUIC in general does not support headers. This method + * is in place to support HTTP3 and is therefore not generally + * exposed except via a private symbol. * @param {object} headers * @returns {boolean} true if the headers were scheduled to be sent. */ - sendHeaders(headers) { + [kSendHeaders](headers) { validateObject(headers, 'headers'); if (this.pending) { debug('pending stream enqueing headers', headers); @@ -818,7 +869,7 @@ class QuicStream { direction: this.direction, pending: this.pending, stats: this.stats, - state: this.state, + state: this.#state, session: this.session, }, opts)}`; } @@ -833,8 +884,8 @@ class QuicSession { #handle; /** @type {PromiseWithResolvers} */ #pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials - /** @type {SocketAddress|undefined} */ - #remoteAddress = undefined; + /** @type {PromiseWithResolvers} */ + #pendingOpen = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials /** @type {QuicSessionState} */ #state; /** @type {QuicSessionStats} */ @@ -845,16 +896,8 @@ class QuicSession { #onstream = undefined; /** @type {OnDatagramCallback|undefined} */ #ondatagram = undefined; - /** @type {OnDatagramStatusCallback|undefined} */ - #ondatagramstatus = undefined; - /** @type {OnPathValidationCallback|undefined} */ - #onpathvalidation = undefined; - /** @type {OnSessionTicketCallback|undefined} */ - #onsessionticket = undefined; - /** @type {OnVersionNegotiationCallback|undefined} */ - #onversionnegotiation = undefined; - /** @type {OnHandshakeCallback} */ - #onhandshake = undefined; + /** @type {{}} */ + #sessionticket = undefined; /** * @param {symbol} privateSymbol @@ -872,6 +915,9 @@ class QuicSession { this.#handle[kOwner] = this; this.#stats = new QuicSessionStats(kPrivateConstructor, handle.stats); this.#state = new QuicSessionState(kPrivateConstructor, handle.state); + this.#state.hasVersionNegotiationListener = true; + this.#state.hasPathValidationListener = true; + this.#state.hasSessionTicketListener = true; debug('session created'); } @@ -881,6 +927,9 @@ class QuicSession { return this.#handle === undefined || this.#isPendingClose; } + /** @type {any} */ + get sessionticket() { return this.#sessionticket; } + /** @type {OnStreamCallback} */ get onstream() { return this.#onstream; } @@ -907,211 +956,85 @@ class QuicSession { } } - /** @type {OnDatagramStatusCallback} */ - get ondatagramstatus() { return this.#ondatagramstatus; } - - set ondatagramstatus(fn) { - if (fn === undefined) { - this.#ondatagramstatus = undefined; - } else { - validateFunction(fn, 'ondatagramstatus'); - this.#ondatagramstatus = fn.bind(this); - } - } - - /** @type {OnPathValidationCallback} */ - get onpathvalidation() { return this.#onpathvalidation; } - - set onpathvalidation(fn) { - if (fn === undefined) { - this.#onpathvalidation = undefined; - this.#state.hasPathValidationListener = false; - } else { - validateFunction(fn, 'onpathvalidation'); - this.#onpathvalidation = fn.bind(this); - this.#state.hasPathValidationListener = true; - } - } - - /** @type {OnSessionCallback} */ - get onsessionticket() { return this.#onsessionticket; } - - set onsessionticket(fn) { - if (fn === undefined) { - this.#onsessionticket = undefined; - this.#state.hasSessionTicketListener = false; - } else { - validateFunction(fn, 'onsessionticket'); - this.#onsessionticket = fn.bind(this); - this.#state.hasSessionTicketListener = true; - } - } - - /** @type {OnVersionNegotiationCallback} */ - get onversionnegotiation() { return this.#onversionnegotiation; } - - set onversionnegotiation(fn) { - if (fn === undefined) { - this.#onversionnegotiation = undefined; - this.#state.hasVersionNegotiationListener = false; - } else { - validateFunction(fn, 'onversionnegotiation'); - this.#onversionnegotiation = fn.bind(this); - this.#state.hasVersionNegotiationListener = true; - } - } - - /** @type {OnHandshakeCallback} */ - get onhandshake() { return this.#onhandshake; } - - set onhandshake(fn) { - if (fn === undefined) { - this.#onhandshake = undefined; - } else { - validateFunction(fn, 'onhandshake'); - this.#onhandshake = fn.bind(this); - } - } - /** @type {QuicSessionStats} */ get stats() { return this.#stats; } /** @type {QuicSessionState} */ - get state() { return this.#state; } + get [kState]() { return this.#state; } /** @type {QuicEndpoint} */ get endpoint() { return this.#endpoint; } - /** @type {boolean} */ - get pending() { return !this.#state.isStreamOpenAllowed; } - - /** - * The path is the local and remote addresses of the session. - * @type {Path} - */ - get path() { - if (this.destroyed) return undefined; - if (this.#remoteAddress === undefined) { - const addr = this.#handle.getRemoteAddress(); - if (addr !== undefined) { - this.#remoteAddress = new InternalSocketAddress(addr); - } - } - return { - local: this.#endpoint.address, - remote: this.#remoteAddress, - }; - } - /** + * @param {number} direction + * @param {OpenStreamOptions} options * @returns {QuicStream} */ - openBidirectionalStream(options = kEmptyObject) { + async #createStream(direction, options = kEmptyObject) { if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed. New streams cannot be opened.'); } - if (this.pending) { - debug('opening new pending bidirectional stream'); + const dir = direction === STREAM_DIRECTION_BIDIRECTIONAL ? 'bidi' : 'uni'; + if (this.#state.isStreamOpenAllowed) { + debug(`opening new pending ${dir} stream`); } else { - debug('opening new bidirectional stream'); + debug(`opening new ${dir} stream`); } validateObject(options, 'options'); const { body, - headers, + sendOrder = 50, + [kHeaders]: headers, } = options; if (headers !== undefined) { validateObject(headers, 'options.headers'); } - const handle = this.#handle.openStream(STREAM_DIRECTION_BIDIRECTIONAL); + validateNumber(sendOrder, 'options.sendOrder'); + // TODO(@jasnell): Make use of sendOrder to set the priority + + const validatedBody = validateBody(body); + + const handle = this.#handle.openStream(direction, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); } if (headers !== undefined) { - handle.sendHeaders(1, mapToHeaders(headers), 1); + // If headers are specified and there's no body, then we assume + // that the headers are terminal. + handle.sendHeaders(1, mapToHeaders(headers), + validatedBody === undefined ? 1 : 0); } - const stream = new QuicStream(kPrivateConstructor, handle, this, - STREAM_DIRECTION_BIDIRECTIONAL); + const stream = new QuicStream(kPrivateConstructor, handle, this, direction); this.#streams.add(stream); if (onSessionOpenStreamChannel.hasSubscribers) { onSessionOpenStreamChannel.publish({ stream, session: this, - options: { __proto__: null, ...options }, + direction: dir, }); } return stream; } - #validateBody(body) { - // TODO(@jasnell): Support streaming sources - if (body === undefined || - isArrayBuffer(body) || - isArrayBufferView(body) || - isBlob(body)) { - return body; - } - - throw new ERR_INVALID_ARG_TYPE('options.body', [ - 'ArrayBuffer', - 'ArrayBufferView', - 'Blob', - ], body); + /** + * @param {OpenStreamOptions} [options] + * @returns {Promise} + */ + async createBidirectionalStream(options = kEmptyObject) { + return await this.#createStream(STREAM_DIRECTION_BIDIRECTIONAL, options); } /** - * @returns {QuicStream} + * @param {OpenStreamOptions} [options] + * @returns {Promise} */ - openUnidirectionalStream(options = kEmptyObject) { - if (this.#isClosedOrClosing) { - throw new ERR_INVALID_STATE('Session is closed. New streams cannot be opened.'); - } - if (this.pending) { - debug('opening new pending unidirectional stream'); - } else { - debug('opening new unidirectional stream'); - } - - validateObject(options, 'options'); - const { - body, - headers, - } = options; - if (headers !== undefined) { - validateObject(headers, 'options.headers'); - } - const validatedBody = this.#validateBody(body); - - const handle = this.#handle.openStream(STREAM_DIRECTION_UNIDIRECTIONAL); - if (handle === undefined) { - throw new ERR_QUIC_OPEN_STREAM_FAILED(); - } - - if (headers !== undefined) { - handle.sendHeaders(1, mapToHeaders(headers), 1); - } - if (validatedBody !== undefined) { - headers.attachSource(validatedBody); - } - - const stream = new QuicStream(kPrivateConstructor, handle, this, - STREAM_DIRECTION_UNIDIRECTIONAL); - this.#streams.add(stream); - - if (onSessionOpenStreamChannel.hasSubscribers) { - onSessionOpenStreamChannel.publish({ - stream, - session: this, - options: { __proto__: null, ...options }, - }); - } - - return stream; + async createUnidirectionalStream(options = kEmptyObject) { + return await this.#createStream(STREAM_DIRECTION_UNIDIRECTIONAL, options); } /** @@ -1123,9 +1046,9 @@ class QuicSession { * * If an ArrayBufferView is given, the view will be copied. * @param {ArrayBufferView|string} datagram The datagram payload - * @returns {bigint} The datagram ID + * @returns {Promise} */ - sendDatagram(datagram) { + async sendDatagram(datagram) { if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Session is closed'); } @@ -1137,12 +1060,13 @@ class QuicSession { ['ArrayBufferView', 'string'], datagram); } + const length = datagram.byteLength; + const offset = datagram.byteOffset; datagram = new Uint8Array(ArrayBufferPrototypeTransfer(datagram.buffer), - datagram.byteOffset, - datagram.byteLength); + length, offset); } - debug('sending datagram', datagram.byteLength); + debug(`sending datagram with ${datagram.byteLength} bytes`); const id = this.#handle.sendDatagram(datagram); @@ -1153,8 +1077,6 @@ class QuicSession { session: this, }); } - - return id; } /** @@ -1201,6 +1123,9 @@ class QuicSession { return this.closed; } + /** @type {Promise} */ + get opened() { return this.#pendingOpen.promise; } + /** * A promise that is resolved when the session is closed, or is rejected if * the session is closed abruptly due to an error. @@ -1218,7 +1143,6 @@ class QuicSession { * the closed promise will be rejected with that error. If no error is given, * the closed promise will be resolved. * @param {any} error - * @return {Promise} Returns this.closed */ destroy(error) { if (this.destroyed) return; @@ -1250,23 +1174,22 @@ class QuicSession { // If the session is still waiting to be closed, and error // is specified, reject the closed promise. this.#pendingClose.reject?.(error); + this.#pendingOpen.reject?.(error); } else { this.#pendingClose.resolve?.(); } + this.#pendingClose.reject = undefined; this.#pendingClose.resolve = undefined; + this.#pendingOpen.reject = undefined; + this.#pendingOpen.resolve = undefined; - this.#remoteAddress = undefined; this.#state[kFinishClose](); this.#stats[kFinishClose](); this.#onstream = undefined; this.#ondatagram = undefined; - this.#ondatagramstatus = undefined; - this.#onpathvalidation = undefined; - this.#onsessionticket = undefined; - this.#onversionnegotiation = undefined; - this.#onhandshake = undefined; + this.#sessionticket = undefined; // Destroy the underlying C++ handle this.#handle.destroy(); @@ -1275,10 +1198,9 @@ class QuicSession { if (onSessionClosedChannel.hasSubscribers) { onSessionClosedChannel.publish({ session: this, + error, }); } - - return this.closed; } /** @@ -1333,11 +1255,12 @@ class QuicSession { // an ondatagram callback. The callback should always exist here. assert(this.#ondatagram, 'Unexpected datagram event'); if (this.destroyed) return; + const length = u8.byteLength; this.#ondatagram(u8, early); if (onSessionReceiveDatagramChannel.hasSubscribers) { onSessionReceiveDatagramChannel.publish({ - length: u8.byteLength, + length, early, session: this, }); @@ -1349,11 +1272,7 @@ class QuicSession { * @param {'lost'|'acknowledged'} status */ [kDatagramStatus](id, status) { - if (this.destroyed || typeof this.ondatagramstatus !== 'function') return; - // The ondatagramstatus callback may not have been specified. That's ok. - // We'll just ignore the event in that case. - this.#ondatagramstatus?.(id, status); - + if (this.destroyed) return; if (onSessionReceiveDatagramStatusChannel.hasSubscribers) { onSessionReceiveDatagramStatusChannel.publish({ id, @@ -1373,13 +1292,7 @@ class QuicSession { */ [kPathValidation](result, newLocalAddress, newRemoteAddress, oldLocalAddress, oldRemoteAddress, preferredAddress) { - // The path validation event should only be called if the session was created - // with an onpathvalidation callback. The callback should always exist here. - assert(this.#onpathvalidation, 'Unexpected path validation event'); if (this.destroyed) return; - this.#onpathvalidation(result, newLocalAddress, newRemoteAddress, - oldLocalAddress, oldRemoteAddress, preferredAddress); - if (onSessionPathValidationChannel.hasSubscribers) { onSessionPathValidationChannel.publish({ result, @@ -1397,11 +1310,8 @@ class QuicSession { * @param {object} ticket */ [kSessionTicket](ticket) { - // The session ticket event should only be called if the session was created - // with an onsessionticket callback. The callback should always exist here. - assert(this.#onsessionticket, 'Unexpected session ticket event'); if (this.destroyed) return; - this.#onsessionticket(ticket); + this.#sessionticket = ticket; if (onSessionTicketChannel.hasSubscribers) { onSessionTicketChannel.publish({ ticket, @@ -1416,14 +1326,8 @@ class QuicSession { * @param {number[]} supportedVersions */ [kVersionNegotiation](version, requestedVersions, supportedVersions) { - // The version negotiation event should only be called if the session was - // created with an onversionnegotiation callback. The callback should always - // exist here. - assert(this.#onversionnegotiation, 'Unexpected version negotiation event'); if (this.destroyed) return; - this.#onversionnegotiation(version, requestedVersions, supportedVersions); this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); - if (onSessionVersionNegotiationChannel.hasSubscribers) { onSessionVersionNegotiationChannel.publish({ version, @@ -1435,32 +1339,40 @@ class QuicSession { } /** - * @param {string} sni - * @param {string} alpn + * @param {string} servername + * @param {string} protocol * @param {string} cipher * @param {string} cipherVersion * @param {string} validationErrorReason * @param {number} validationErrorCode - * @param {boolean} earlyDataAccepted */ - [kHandshake](sni, alpn, cipher, cipherVersion, validationErrorReason, - validationErrorCode, earlyDataAccepted) { - if (this.destroyed) return; - // The onhandshake callback may not have been specified. That's ok. - // We'll just ignore the event in that case. - this.#onhandshake?.(sni, alpn, cipher, cipherVersion, validationErrorReason, - validationErrorCode, earlyDataAccepted); + [kHandshake](servername, protocol, cipher, cipherVersion, validationErrorReason, + validationErrorCode) { + if (this.destroyed || !this.#pendingOpen.resolve) return; + + const addr = this.#handle.getRemoteAddress(); + + const info = { + local: this.#endpoint.address, + remote: addr !== undefined ? + new InternalSocketAddress(addr) : + undefined, + servername, + protocol, + cipher, + cipherVersion, + validationErrorReason, + validationErrorCode, + }; + + this.#pendingOpen.resolve?.(info); + this.#pendingOpen.resolve = undefined; + this.#pendingOpen.reject = undefined; if (onSessionHandshakeChannel.hasSubscribers) { onSessionHandshakeChannel.publish({ - sni, - alpn, - cipher, - cipherVersion, - validationErrorReason, - validationErrorCode, - earlyDataAccepted, session: this, + ...info, }); } } @@ -1510,7 +1422,7 @@ class QuicSession { destroyed: this.destroyed, endpoint: this.endpoint, path: this.path, - state: this.state, + state: this.#state, stats: this.stats, streams: this.#streams, }, opts)}`; @@ -1602,19 +1514,12 @@ class QuicEndpoint { maxStatelessResetsPerHost, addressLRUSize, maxRetries, - maxPayloadSize, - unacknowledgedPacketThreshold, - handshakeTimeout, - maxStreamWindow, - maxWindow, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, udpSendBufferSize, udpTTL, - noUdpPayloadSizeShaping, validateAddress, - disableActiveMigration, ipv6Only, cc, resetTokenSecret, @@ -1642,19 +1547,12 @@ class QuicEndpoint { maxStatelessResetsPerHost, addressLRUSize, maxRetries, - maxPayloadSize, - unacknowledgedPacketThreshold, - handshakeTimeout, - maxStreamWindow, - maxWindow, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, udpSendBufferSize, udpTTL, - noUdpPayloadSizeShaping, validateAddress, - disableActiveMigration, ipv6Only, cc, resetTokenSecret, @@ -1687,20 +1585,6 @@ class QuicEndpoint { debug('endpoint created'); } - /** - * @type {OnSessionCallback|undefined} - */ - get onsession() { return this.#onsession; } - - set onsession(fn) { - if (fn === undefined) { - this.#onsession = undefined; - } else { - validateFunction(fn, 'onsession'); - this.#onsession = fn.bind(this); - } - } - /** * Statistics collected while the endpoint is operational. * @type {QuicEndpointStats} @@ -1708,10 +1592,10 @@ class QuicEndpoint { get stats() { return this.#stats; } /** @type {QuicEndpointState} */ - get state() { return this.#state; } + get [kState]() { return this.#state; } get #isClosedOrClosing() { - return this.#handle === undefined || this.#isPendingClose; + return this.destroyed || this.#isPendingClose; } /** @@ -1756,158 +1640,12 @@ class QuicEndpoint { return this.#address; } - /** - * @param {TlsOptions} tls - */ - #processTlsOptions(tls) { - validateObject(tls, 'options.tls'); - const { - sni, - alpn, - ciphers = DEFAULT_CIPHERS, - groups = DEFAULT_GROUPS, - keylog = false, - verifyClient = false, - tlsTrace = false, - verifyPrivateKey = false, - keys, - certs, - ca, - crl, - } = tls; - - if (sni !== undefined) { - validateString(sni, 'options.tls.sni'); - } - if (alpn !== undefined) { - validateString(alpn, 'options.tls.alpn'); - } - if (ciphers !== undefined) { - validateString(ciphers, 'options.tls.ciphers'); - } - if (groups !== undefined) { - validateString(groups, 'options.tls.groups'); - } - validateBoolean(keylog, 'options.tls.keylog'); - validateBoolean(verifyClient, 'options.tls.verifyClient'); - validateBoolean(tlsTrace, 'options.tls.tlsTrace'); - validateBoolean(verifyPrivateKey, 'options.tls.verifyPrivateKey'); - - if (certs !== undefined) { - const certInputs = ArrayIsArray(certs) ? certs : [certs]; - for (const cert of certInputs) { - if (!isArrayBufferView(cert) && !isArrayBuffer(cert)) { - throw new ERR_INVALID_ARG_TYPE('options.tls.certs', - ['ArrayBufferView', 'ArrayBuffer'], cert); - } - } - } - - if (ca !== undefined) { - const caInputs = ArrayIsArray(ca) ? ca : [ca]; - for (const caCert of caInputs) { - if (!isArrayBufferView(caCert) && !isArrayBuffer(caCert)) { - throw new ERR_INVALID_ARG_TYPE('options.tls.ca', - ['ArrayBufferView', 'ArrayBuffer'], caCert); - } - } - } - - if (crl !== undefined) { - const crlInputs = ArrayIsArray(crl) ? crl : [crl]; - for (const crlCert of crlInputs) { - if (!isArrayBufferView(crlCert) && !isArrayBuffer(crlCert)) { - throw new ERR_INVALID_ARG_TYPE('options.tls.crl', - ['ArrayBufferView', 'ArrayBuffer'], crlCert); - } - } - } - - const keyHandles = []; - if (keys !== undefined) { - const keyInputs = ArrayIsArray(keys) ? keys : [keys]; - for (const key of keyInputs) { - if (isKeyObject(key)) { - if (key.type !== 'private') { - throw new ERR_INVALID_ARG_VALUE('options.tls.keys', key, 'must be a private key'); - } - ArrayPrototypePush(keyHandles, key[kKeyObjectHandle]); - } else if (isCryptoKey(key)) { - if (key.type !== 'private') { - throw new ERR_INVALID_ARG_VALUE('options.tls.keys', key, 'must be a private key'); - } - ArrayPrototypePush(keyHandles, key[kKeyObjectInner][kKeyObjectHandle]); - } else { - throw new ERR_INVALID_ARG_TYPE('options.tls.keys', ['KeyObject', 'CryptoKey'], key); - } - } - } - - return { - __proto__: null, - sni, - alpn, - ciphers, - groups, - keylog, - verifyClient, - tlsTrace, - verifyPrivateKey, - keys: keyHandles, - certs, - ca, - crl, - }; - } - - /** - * @param {'use'|'ignore'|'default'} policy - * @returns {number} - */ - #getPreferredAddressPolicy(policy = 'default') { - switch (policy) { - case 'use': return PREFERRED_ADDRESS_USE; - case 'ignore': return PREFERRED_ADDRESS_IGNORE; - case 'default': return DEFAULT_PREFERRED_ADDRESS_POLICY; - } - throw new ERR_INVALID_ARG_VALUE('options.preferredAddressPolicy', policy); - } - - /** - * @param {SessionOptions} options - */ - #processSessionOptions(options) { - validateObject(options, 'options'); - const { - version, - minVersion, - preferredAddressPolicy = 'default', - application = kEmptyObject, - transportParams = kEmptyObject, - tls = kEmptyObject, - qlog = false, - sessionTicket, - } = options; - - return { - __proto__: null, - version, - minVersion, - preferredAddressPolicy: this.#getPreferredAddressPolicy(preferredAddressPolicy), - application, - transportParams, - tls: this.#processTlsOptions(tls), - qlog, - sessionTicket, - }; - } - /** * Configures the endpoint to listen for incoming connections. * @param {OnSessionCallback|SessionOptions} [onsession] * @param {SessionOptions} [options] */ - listen(onsession, options = kEmptyObject) { + [kListen](onsession, options) { if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Endpoint is closed'); } @@ -1917,83 +1655,45 @@ class QuicEndpoint { if (this.#state.isBusy) { throw new ERR_INVALID_STATE('Endpoint is busy'); } - if (onsession !== undefined && typeof onsession !== 'function') { - options = onsession; - onsession = undefined; - } - if (onsession !== undefined) { - validateFunction(onsession, 'onsession'); - this.#onsession = onsession.bind(this); - } - if (typeof this.#onsession !== 'function') { - throw new ERR_INVALID_STATE('No onsession callback is provided'); - } + validateObject(options, 'options'); + this.#onsession = onsession.bind(this); debug('endpoint listening as a server'); - this.#handle.listen(this.#processSessionOptions(options)); + this.#handle.listen(options); this.#listening = true; - - if (onEndpointListeningChannel.hasSubscribers) { - onEndpointListeningChannel.publish({ - endpoint: this, - options, - }); - } } /** * Initiates a session with a remote endpoint. - * @param {string|SocketAddress} address + * @param {{}} address * @param {SessionOptions} [options] * @returns {QuicSession} */ - connect(address, options = kEmptyObject) { + [kConnect](address, options) { if (this.#isClosedOrClosing) { throw new ERR_INVALID_STATE('Endpoint is closed'); } if (this.#state.isBusy) { throw new ERR_INVALID_STATE('Endpoint is busy'); } - if (typeof address === 'string') { - address = SocketAddress.parse(address); - } - - if (!SocketAddress.isSocketAddress(address)) { - if (address == null || typeof address !== 'object') { - throw new ERR_INVALID_ARG_TYPE('address', ['SocketAddress', 'string'], address); - } - address = new SocketAddress(address); - } - - const processedOptions = this.#processSessionOptions(options); - const { sessionTicket } = processedOptions; + validateObject(options, 'options'); + const { sessionTicket, ...rest } = options; debug('endpoint connecting as a client'); - const handle = this.#handle.connect(address[kSocketAddressHandle], - processedOptions, sessionTicket); - + const handle = this.#handle.connect(address, rest, sessionTicket); if (handle === undefined) { throw new ERR_QUIC_CONNECTION_FAILED(); } const session = this.#newSession(handle); - if (onEndpointClientSessionChannel.hasSubscribers) { - onEndpointClientSessionChannel.publish({ - endpoint: this, - session, - address, - options, - }); - } - return session; } /** * Gracefully closes the endpoint. Any existing sessions will be permitted to - * end gracefully, after which the endpoint will be closed. New sessions will - * not be accepted or created. The returned promise will be resolved when - * closing is complete, or will be rejected if the endpoint is closed abruptly + * end gracefully, after which the endpoint will be closed immediately. New + * sessions will not be accepted or created. The returned promise will be resolved + * when closing is complete, or will be rejected if the endpoint is closed abruptly * due to an error. * @returns {Promise} Returns this.closed */ @@ -2022,17 +1722,13 @@ class QuicEndpoint { */ get closed() { return this.#pendingClose.promise; } - /** @type {boolean} */ - get destroyed() { return this.#handle === undefined; } - /** - * Return an iterator over all currently active sessions associated - * with this endpoint. - * @type {SetIterator} + * @type {boolean} */ - get sessions() { - return this.#sessions[SymbolIterator](); - } + get closing() { return this.#isPendingClose; } + + /** @type {boolean} */ + get destroyed() { return this.#handle === undefined; } /** * Forcefully terminates the endpoint by immediately destroying all sessions @@ -2045,10 +1741,12 @@ class QuicEndpoint { destroy(error) { debug('destroying the endpoint'); if (!this.#isClosedOrClosing) { - // Start closing the endpoint. this.#pendingError = error; // Trigger a graceful close of the endpoint that'll ensure that the - // endpoint is closed down after all sessions are closed... + // endpoint is closed down after all sessions are closed... Because + // we force all sessions to be abruptly destroyed as the next step, + // the endpoint will be closed immediately after all the sessions + // are destroyed. this.close(); } // Now, force all sessions to be abruptly closed... @@ -2177,7 +1875,7 @@ class QuicEndpoint { listening: this.#listening, sessions: this.#sessions, stats: this.stats, - state: this.state, + state: this.#state, }, opts)}`; } }; @@ -2192,29 +1890,322 @@ function readOnlyConstant(value) { }; } +/** + * @param {EndpointOptions} endpoint + */ +function processEndpointOption(endpoint) { + if (endpoint === undefined) { + return { + endpoint: new QuicEndpoint(), + created: true, + }; + } else if (endpoint instanceof QuicEndpoint) { + return { + endpoint, + created: false, + }; + } + validateObject(endpoint, 'options.endpoint'); + return { + endpoint: new QuicEndpoint(endpoint), + created: true, + }; +} + +/** + * @param {SessionOptions} tls + */ +function processTlsOptions(tls, forServer) { + const { + servername, + protocol, + ciphers = DEFAULT_CIPHERS, + groups = DEFAULT_GROUPS, + keylog = false, + verifyClient = false, + tlsTrace = false, + verifyPrivateKey = false, + keys, + certs, + ca, + crl, + } = tls; + + if (servername !== undefined) { + validateString(servername, 'options.servername'); + } + if (protocol !== undefined) { + validateString(protocol, 'options.protocol'); + } + if (ciphers !== undefined) { + validateString(ciphers, 'options.ciphers'); + } + if (groups !== undefined) { + validateString(groups, 'options.groups'); + } + validateBoolean(keylog, 'options.keylog'); + validateBoolean(verifyClient, 'options.verifyClient'); + validateBoolean(tlsTrace, 'options.tlsTrace'); + validateBoolean(verifyPrivateKey, 'options.verifyPrivateKey'); + + if (certs !== undefined) { + const certInputs = ArrayIsArray(certs) ? certs : [certs]; + for (const cert of certInputs) { + if (!isArrayBufferView(cert) && !isArrayBuffer(cert)) { + throw new ERR_INVALID_ARG_TYPE('options.certs', + ['ArrayBufferView', 'ArrayBuffer'], cert); + } + } + } + + if (ca !== undefined) { + const caInputs = ArrayIsArray(ca) ? ca : [ca]; + for (const caCert of caInputs) { + if (!isArrayBufferView(caCert) && !isArrayBuffer(caCert)) { + throw new ERR_INVALID_ARG_TYPE('options.ca', + ['ArrayBufferView', 'ArrayBuffer'], caCert); + } + } + } + + if (crl !== undefined) { + const crlInputs = ArrayIsArray(crl) ? crl : [crl]; + for (const crlCert of crlInputs) { + if (!isArrayBufferView(crlCert) && !isArrayBuffer(crlCert)) { + throw new ERR_INVALID_ARG_TYPE('options.crl', + ['ArrayBufferView', 'ArrayBuffer'], crlCert); + } + } + } + + const keyHandles = []; + if (keys !== undefined) { + const keyInputs = ArrayIsArray(keys) ? keys : [keys]; + for (const key of keyInputs) { + if (isKeyObject(key)) { + if (key.type !== 'private') { + throw new ERR_INVALID_ARG_VALUE('options.keys', key, 'must be a private key'); + } + ArrayPrototypePush(keyHandles, key[kKeyObjectHandle]); + } else if (isCryptoKey(key)) { + if (key.type !== 'private') { + throw new ERR_INVALID_ARG_VALUE('options.keys', key, 'must be a private key'); + } + ArrayPrototypePush(keyHandles, key[kKeyObjectInner][kKeyObjectHandle]); + } else { + throw new ERR_INVALID_ARG_TYPE('options.keys', ['KeyObject', 'CryptoKey'], key); + } + } + } + + // For a server we require key and cert at least + if (forServer) { + if (keyHandles.length === 0) { + throw new ERR_MISSING_ARGS('options.keys'); + } + if (certs === undefined) { + throw new ERR_MISSING_ARGS('options.certs'); + } + } + + return { + __proto__: null, + servername, + protocol, + ciphers, + groups, + keylog, + verifyClient, + tlsTrace, + verifyPrivateKey, + keys: keyHandles, + certs, + ca, + crl, + }; +} + +/** + * @param {'use'|'ignore'|'default'} policy + * @returns {number} + */ +function getPreferredAddressPolicy(policy = 'default') { + switch (policy) { + case 'use': return PREFERRED_ADDRESS_USE; + case 'ignore': return PREFERRED_ADDRESS_IGNORE; + case 'default': return DEFAULT_PREFERRED_ADDRESS_POLICY; + } + throw new ERR_INVALID_ARG_VALUE('options.preferredAddressPolicy', policy); +} + +/** + * @param {SessionOptions} options + * @param {boolean} [forServer] + * @returns {SessionOptions} + */ +function processSessionOptions(options, forServer = false) { + validateObject(options, 'options'); + const { + endpoint, + version, + minVersion, + preferredAddressPolicy = 'default', + transportParams = kEmptyObject, + qlog = false, + sessionTicket, + maxPayloadSize, + unacknowledgedPacketThreshold = 0, + handshakeTimeout, + maxStreamWindow, + maxWindow, + cc, + [kApplicationProvider]: provider, + } = options; + + if (provider !== undefined) { + validateObject(provider, 'options[kApplicationProvider]'); + } + + if (cc !== undefined) { + validateString(cc, 'options.cc'); + if (cc !== 'reno' || cc !== 'bbr' || cc !== 'cubic') { + throw new ERR_INVALID_ARG_VALUE(cc, 'options.cc'); + } + } + + const { + endpoint: actualEndpoint, + created: endpointCreated, + } = processEndpointOption(endpoint); + + return { + __proto__: null, + endpoint: actualEndpoint, + endpointCreated, + version, + minVersion, + preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy), + transportParams, + tls: processTlsOptions(options, forServer), + qlog, + maxPayloadSize, + unacknowledgedPacketThreshold, + handshakeTimeout, + maxStreamWindow, + maxWindow, + sessionTicket, + provider, + cc, + }; +} + +// ============================================================================ + +/** + * @param {OnSessionCallback} callback + * @param {SessionOptions} [options] + * @returns {Promise} + */ +async function listen(callback, options = kEmptyObject) { + validateFunction(callback, 'callback'); + const { + endpoint, + ...sessionOptions + } = processSessionOptions(options, true /* for server */); + endpoint[kListen](callback, sessionOptions); + + if (onEndpointListeningChannel.hasSubscribers) { + onEndpointListeningChannel.publish({ + endpoint, + options, + }); + } + + return endpoint; +} + +/** + * @param {string|SocketAddress} address + * @param {SessionOptions} [options] + * @returns {Promise} + */ +async function connect(address, options = kEmptyObject) { + if (typeof address === 'string') { + address = SocketAddress.parse(address); + } + + if (!SocketAddress.isSocketAddress(address)) { + if (address == null || typeof address !== 'object') { + throw new ERR_INVALID_ARG_TYPE('address', ['SocketAddress', 'string'], address); + } + address = new SocketAddress(address); + } + + const { + endpoint, + ...rest + } = processSessionOptions(options); + + const session = endpoint[kConnect](address[kSocketAddressHandle], rest); + + if (onEndpointClientSessionChannel.hasSubscribers) { + onEndpointClientSessionChannel.publish({ + endpoint, + session, + address, + options, + }); + } + + return session; +} + ObjectDefineProperties(QuicEndpoint, { - CC_ALGO_RENO: readOnlyConstant(CC_ALGO_RENO), - CC_ALGO_CUBIC: readOnlyConstant(CC_ALGO_CUBIC), - CC_ALGO_BBR: readOnlyConstant(CC_ALGO_BBR), - CC_ALGP_RENO_STR: readOnlyConstant(CC_ALGO_RENO_STR), - CC_ALGO_CUBIC_STR: readOnlyConstant(CC_ALGO_CUBIC_STR), - CC_ALGO_BBR_STR: readOnlyConstant(CC_ALGO_BBR_STR), + Stats: { + __proto__: null, + writable: true, + configurable: true, + enumerable: true, + value: QuicEndpointStats, + }, }); ObjectDefineProperties(QuicSession, { - DEFAULT_CIPHERS: readOnlyConstant(DEFAULT_CIPHERS), - DEFAULT_GROUPS: readOnlyConstant(DEFAULT_GROUPS), + Stats: { + __proto__: null, + writable: true, + configurable: true, + enumerable: true, + value: QuicSessionStats, + }, +}); +ObjectDefineProperties(QuicStream, { + Stats: { + __proto__: null, + writable: true, + configurable: true, + enumerable: true, + value: QuicStreamStats, + }, }); +// ============================================================================ + module.exports = { + listen, + connect, QuicEndpoint, QuicSession, QuicStream, - QuicSessionState, - QuicSessionStats, - QuicStreamState, - QuicStreamStats, - QuicEndpointState, - QuicEndpointStats, + Http3, }; +ObjectDefineProperties(module.exports, { + CC_ALGO_RENO: readOnlyConstant(CC_ALGO_RENO_STR), + CC_ALGO_CUBIC: readOnlyConstant(CC_ALGO_CUBIC_STR), + CC_ALGO_BBR: readOnlyConstant(CC_ALGO_BBR_STR), + DEFAULT_CIPHERS: readOnlyConstant(DEFAULT_CIPHERS), + DEFAULT_GROUPS: readOnlyConstant(DEFAULT_GROUPS), +}); + + /* c8 ignore stop */ diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 053fd01ba922c9..da880501d8cd61 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -28,6 +28,8 @@ 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, @@ -70,6 +71,7 @@ const { IDX_STATE_STREAM_WRITE_ENDED, 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, @@ -85,7 +87,6 @@ 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_DESTROYED !== undefined); assert(IDX_STATE_SESSION_HANDSHAKE_COMPLETED !== undefined); assert(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED !== undefined); assert(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED !== undefined); @@ -106,6 +107,7 @@ 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); @@ -309,12 +311,6 @@ class QuicSessionState { return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET); } - /** @type {boolean} */ - get isDestroyed() { - if (this.#handle.byteLength === 0) return undefined; - return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DESTROYED); - } - /** @type {boolean} */ get isHandshakeCompleted() { if (this.#handle.byteLength === 0) return undefined; @@ -483,6 +479,12 @@ class QuicStreamState { 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; @@ -502,13 +504,13 @@ class QuicStreamState { } /** @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); } @@ -526,13 +528,13 @@ class QuicStreamState { } /** @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); } @@ -553,11 +555,10 @@ class QuicStreamState { writeEnded: this.writeEnded, paused: this.paused, reset: this.reset, + hasOutbound: this.hasOutbound, hasReader: this.hasReader, wantsBlock: this.wantsBlock, - wantsHeaders: this.wantsHeaders, wantsReset: this.wantsReset, - wantsTrailers: this.wantsTrailers, }; } @@ -583,11 +584,10 @@ class QuicStreamState { writeEnded: this.writeEnded, 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)}`; } diff --git a/lib/internal/quic/stats.js b/lib/internal/quic/stats.js index 425b1c62841020..d12a85745bd79a 100644 --- a/lib/internal/quic/stats.js +++ b/lib/internal/quic/stats.js @@ -51,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, @@ -104,17 +101,14 @@ 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_DESTROYED_AT !== undefined); assert(IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT !== undefined); assert(IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT !== undefined); -assert(IDX_STATS_SESSION_GRACEFUL_CLOSING_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_LOSS_RETRANSMIT_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); @@ -330,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]; @@ -345,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]; @@ -380,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]; @@ -472,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}`, @@ -512,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, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index f6ddb7af1840e2..15f2339fc95504 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -16,43 +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 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, kFinishClose, kHandshake, kHeaders, - kOwner, - kRemoveSession, + kInspect, + kKeyObjectHandle, + kKeyObjectInner, + kListen, kNewSession, - kRemoveStream, kNewStream, + kOnHeaders, + kOnTrailers, + kOwner, kPathValidation, + kPrivateConstructor, + kRemoveSession, + kRemoveStream, kReset, + kSendHeaders, kSessionTicket, + kState, kTrailers, kVersionNegotiation, - kInspect, - kKeyObjectHandle, - kKeyObjectInner, - kPrivateConstructor, + kWantsHeaders, + kWantsTrailers, }; diff --git a/lib/quic.js b/lib/quic.js index 308a06d9eefa56..a6ca37825fbe71 100644 --- a/lib/quic.js +++ b/lib/quic.js @@ -1,25 +1,32 @@ 'use strict'; const { + emitExperimentalWarning, +} = require('internal/util'); +emitExperimentalWarning('quic'); + +const { + connect, + listen, QuicEndpoint, QuicSession, QuicStream, - QuicSessionState, - QuicSessionStats, - QuicStreamState, - QuicStreamStats, - QuicEndpointState, - QuicEndpointStats, + CC_ALGO_RENO, + CC_ALGO_CUBIC, + CC_ALGO_BBR, + DEFAULT_CIPHERS, + DEFAULT_GROUPS, } = require('internal/quic/quic'); module.exports = { + connect, + listen, QuicEndpoint, QuicSession, QuicStream, - QuicSessionState, - QuicSessionStats, - QuicStreamState, - QuicStreamStats, - QuicEndpointState, - QuicEndpointStats, + CC_ALGO_RENO, + CC_ALGO_CUBIC, + CC_ALGO_BBR, + DEFAULT_CIPHERS, + DEFAULT_GROUPS, }; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 9aaf5626fcfe4a..33682cf4fc5ce2 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -136,6 +136,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. diff --git a/src/quic/application.cc b/src/quic/application.cc index 01bba5090fea07..4c126a64ed7138 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -3,6 +3,7 @@ #include "application.h" #include #include +#include #include #include #include @@ -27,10 +28,9 @@ namespace quic { // ============================================================================ // Session::Application_Options -const Session::Application::Options Session::Application::Options::kDefault = - {}; +const Session::Application_Options Session::Application_Options::kDefault = {}; -Session::Application::Options::operator const nghttp3_settings() const { +Session::Application_Options::operator const nghttp3_settings() const { // In theory, Application::Options might contain options for more than just // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. return nghttp3_settings{ @@ -45,7 +45,7 @@ Session::Application::Options::operator const nghttp3_settings() const { }; } -std::string Session::Application::Options::ToString() const { +std::string Session::Application_Options::ToString() const { DebugIndentScope indent; auto prefix = indent.Prefix(); std::string res("{"); @@ -67,25 +67,25 @@ std::string Session::Application::Options::ToString() const { return res; } -Maybe Session::Application::Options::From( +Maybe Session::Application_Options::From( Environment* env, Local value) { if (value.IsEmpty()) [[unlikely]] { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - return Nothing(); + return Nothing(); } - Application::Options options; + Application_Options options; auto& state = BindingData::Get(env); #define SET(name) \ - SetOption( \ + SetOption( \ env, &options, params, state.name##_string()) if (!value->IsUndefined()) { if (!value->IsObject()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - return Nothing(); + return Nothing(); } auto params = value.As(); if (!SET(max_header_pairs) || !SET(max_header_length) || @@ -94,13 +94,13 @@ Maybe Session::Application::Options::From( !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(); + return Nothing(); } } #undef SET - return Just(options); + return Just(options); } // ============================================================================ @@ -135,27 +135,21 @@ bool Session::Application::Start() { bool Session::Application::AcknowledgeStreamData(int64_t stream_id, size_t datalen) { - Debug(session_, - "Application acknowledging stream %" PRIi64 " data: %zu", - stream_id, - datalen); - auto stream = session().FindStream(stream_id); - if (!stream) return false; - stream->Acknowledge(datalen); - return true; + 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; } @@ -164,19 +158,16 @@ bool Session::Application::SendHeaders(const Stream& stream, const v8::Local& 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. } @@ -188,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. } @@ -196,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 @@ -206,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. } @@ -217,40 +204,34 @@ StreamPriority Session::Application::GetStreamPriority(const Stream& stream) { BaseObjectPtr 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; @@ -258,9 +239,10 @@ void Session::Application::SendPendingData() { auto update_stats = OnScopeLeave([&] { auto& s = session(); - s.UpdateDataStats(); - if (!s.is_destroyed()) { + if (!s.is_destroyed()) [[likely]] { + s.UpdatePacketTxTime(); s.UpdateTimer(); + s.UpdateDataStats(); } }); @@ -270,6 +252,7 @@ void Session::Application::SendPendingData() { // 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; @@ -281,7 +264,8 @@ void Session::Application::SendPendingData() { auto ensure_packet = [&] { if (!packet) { packet = CreateStreamDataPacket(); - if (!packet) return false; + if (!packet) [[unlikely]] + return false; pos = begin = ngtcp2_vec(*packet).base; } DCHECK(packet); @@ -297,24 +281,26 @@ 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 = @@ -322,11 +308,15 @@ void Session::Application::SendPendingData() { if (ndatalen > 0) { Debug(session_, - "Application accepted %zu bytes from stream into packet", - ndatalen); - } else { + "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 into packet"); + "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 @@ -340,6 +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); + // 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; } @@ -348,21 +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); - if (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; @@ -374,40 +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(std::move(packet), path); + session_->Send(packet, path); } else { packet->Done(UV_ECANCELED); } - packet.reset(); - - // 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. @@ -415,17 +406,15 @@ void Session::Application::SendPendingData() { size_t datalen = pos - begin; Debug(session_, "Sending packet with %zu bytes", datalen); packet->Truncate(datalen); - session_->Send(std::move(packet), path); - // TODO(@jasnell): Moving a BaseObjectPtr apparently does not fully - // invalidate it. - packet.reset(); + session_->Send(packet, path); // 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.reset(); pos = begin = nullptr; } } @@ -438,7 +427,6 @@ 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; - return ngtcp2_conn_writev_stream(*session_, &path->path, nullptr, @@ -466,9 +454,7 @@ class DefaultApplication final : public Session::Application { size_t datalen, const Stream::ReceiveDataFlags& flags, void* stream_user_data) override { - Debug(&session(), "Default application receiving stream data"); - - BaseObjectWeakPtr stream; + BaseObjectPtr stream; if (stream_user_data == nullptr) { // This is the first time we're seeing this stream. Implicitly create it. stream = session().CreateStream(stream_id); @@ -478,7 +464,7 @@ class DefaultApplication final : public Session::Application { return false; } } else { - stream = BaseObjectWeakPtr(Stream::From(stream_user_data)); + stream = BaseObjectPtr(Stream::From(stream_user_data)); if (!stream) { Debug(&session(), "Default application failed to get existing stream " @@ -495,6 +481,12 @@ class DefaultApplication final : public Session::Application { } 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 @@ -522,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) { @@ -551,10 +554,7 @@ 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 = [](const ngtcp2_vec* vec, size_t cnt) { @@ -566,10 +566,15 @@ class DefaultApplication final : public Session::Application { 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); - CHECK(stream_data->stream); stream_data->stream->Commit(datalen); return true; @@ -582,14 +587,12 @@ class DefaultApplication final : public Session::Application { private: void ScheduleStream(int64_t id) { if (auto stream = session().FindStream(id)) [[likely]] { - Debug(&session(), "Default application scheduling stream %" PRIi64, id); stream->Schedule(&stream_queue_); } } void UnscheduleStream(int64_t id) { if (auto stream = session().FindStream(id)) [[likely]] { - Debug(&session(), "Default application unscheduling stream %" PRIi64, id); stream->Unschedule(); } } @@ -597,18 +600,14 @@ class DefaultApplication final : public Session::Application { Stream::Queue stream_queue_; }; -std::unique_ptr 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::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( - this, config_.options.application_options); + session, Session::Application_Options::kDefault); } } // namespace quic diff --git a/src/quic/application.h b/src/quic/application.h index 682a8e0be9ec1b..346180229322a5 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -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" @@ -79,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 @@ -146,7 +147,7 @@ struct Session::Application::StreamData final { int64_t id = -1; int fin = 0; ngtcp2_vec data[kMaxVectorCount]{}; - BaseObjectWeakPtr stream; + BaseObjectPtr stream; inline operator nghttp3_vec*() { return reinterpret_cast(data); diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 3025800d089b28..9d8ac0c6fb1f6d 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -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") \ diff --git a/src/quic/defs.h b/src/quic/defs.h index 628b2b754a36a5..8c97d30d26f77f 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -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::max(); diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index da2441b2c4a9ab..bff3ced8a2b8ab 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -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(c) / 255) < probability; } -#endif // DEBUG - -Maybe getAlgoFromString(Environment* env, Local 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(); -} - -template -bool SetOption(Environment* env, - Opt* options, - const Local& object, - const Local& name) { - Local 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()).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 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(num->Value()); - } - options->*member = algo; - } - return true; -} - -#if DEBUG template bool SetOption(Environment* env, Opt* options, @@ -251,17 +193,13 @@ Maybe 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(); } @@ -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: "; - } 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 ""; - })(); - 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); @@ -557,7 +470,9 @@ SocketAddress Endpoint::UDP::local_address() const { return SocketAddress::FromSockName(impl_->handle_); } -int Endpoint::UDP::Send(BaseObjectPtr packet) { +int Endpoint::UDP::Send(const BaseObjectPtr& packet) { + DCHECK(packet); + DCHECK(!packet->IsDispatched()); if (is_closed_or_closing()) return UV_EBADF; uv_buf_t buf = *packet; @@ -621,15 +536,10 @@ Local Endpoint::GetConstructorTemplate(Environment* env) { void Endpoint::InitPerIsolate(IsolateData* data, Local target) { // TODO(@jasnell): Implement the per-isolate state + Http3Application::InitPerIsolate(data, target); } void Endpoint::InitPerContext(Realm* realm, Local 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); @@ -682,6 +592,8 @@ void Endpoint::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE); + Http3Application::InitPerContext(realm, target); + SetConstructorFunction(realm->context(), target, "Endpoint", @@ -708,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()); @@ -737,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(this), - "Generating new stateless reset token for CID %s", - cid); - } + Debug(const_cast(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) { - 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 Endpoint::FindSession(const CID& cid) { - BaseObjectPtr 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) { @@ -827,8 +747,7 @@ void Endpoint::DisassociateStatelessResetToken( } } -void Endpoint::Send(BaseObjectPtr&& packet) { - CHECK(packet && !packet->IsDispatched()); +void Endpoint::Send(const BaseObjectPtr& 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 @@ -840,11 +759,13 @@ void Endpoint::Send(BaseObjectPtr&& 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); @@ -1026,17 +947,14 @@ BaseObjectPtr Endpoint::Connect( // If starting fails, the endpoint will be destroyed. 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) { @@ -1047,18 +965,22 @@ BaseObjectPtr Endpoint::Connect( } 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 {}; } - if (!session) return {}; - 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; } @@ -1148,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); @@ -1166,21 +1088,31 @@ void Endpoint::Receive(const uv_buf_t& buf, std::optional 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, @@ -1189,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; } @@ -1217,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; } @@ -1256,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); @@ -1301,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, @@ -1314,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); @@ -1396,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, @@ -1493,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; } @@ -1521,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 @@ -1537,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; @@ -1556,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! } @@ -1591,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: @@ -1621,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? @@ -1694,6 +1652,11 @@ void Endpoint::EmitNewSession(const BaseObjectPtr& 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) { @@ -1744,7 +1707,7 @@ void Endpoint::DoConnect(const FunctionCallbackInfo& args) { return; } - BaseObjectPtr session; + BaseObjectWeakPtr session; if (!args[2]->IsUndefined()) { SessionTicket ticket; diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index ddc57d62d5443b..9cfd828c815f2b 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -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 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); - void RemoveSession(const CID& cid); + void RemoveSession(const CID& cid, const SocketAddress& remote_address); BaseObjectPtr 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(BaseObjectPtr&& packet); + void Send(const BaseObjectPtr& 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(BaseObjectPtr packet); + int Send(const BaseObjectPtr& packet); // Returns the local UDP socket address to which we are bound, // or fail with an assert if we are not bound. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 4ffbd26b8b1935..6160596be1867b 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -17,7 +17,99 @@ #include "session.h" #include "sessionticket.h" -namespace node::quic { +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) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local 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 target) { + // TODO(@jasnell): Implement the per-isolate state +} + +void Http3Application::InitPerContext(Realm* realm, Local target) { + SetConstructorFunction(realm->context(), + target, + "Http3Application", + GetConstructorTemplate(realm->env())); +} + +void Http3Application::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); +} + +Http3Application::Http3Application(Environment* env, + Local object, + const Session::Application::Options& options) + : ApplicationProvider(env, object), options_(options) { + MakeWeak(); +} + +void Http3Application::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + Local 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(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 { using nv_t = nghttp3_nv; @@ -75,10 +167,10 @@ struct Http3HeaderTraits { using Http3Header = NgHeader; // 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), @@ -261,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; @@ -288,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; } @@ -304,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, @@ -434,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 { @@ -448,8 +541,6 @@ class Http3Application final : public Session::Application { id == qpack_enc_stream_id_; } - bool is_destroyed() const { return session().is_destroyed(); } - Http3ConnectionPointer InitializeConnection() { nghttp3_conn* conn = nullptr; nghttp3_settings settings = options_; @@ -646,9 +737,9 @@ 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(user_data); + auto app = static_cast(user_data); DCHECK_EQ(conn, app->conn_.get()); return app; } @@ -669,9 +760,6 @@ class Http3Application final : public Session::Application { auto ptr = From(conn, conn_user_data); \ CHECK_NOT_NULL(ptr); \ auto& name = *ptr; \ - if (name.is_destroyed()) [[unlikely]] { \ - return NGHTTP3_ERR_CALLBACK_FAILURE; \ - } \ NgHttp3CallbackScope scope(name.env()); static nghttp3_ssize on_read_data_callback(nghttp3_conn* conn, @@ -897,11 +985,13 @@ class Http3Application final : public Session::Application { on_receive_settings}; }; -std::unique_ptr createHttp3Application( - Session* session, const Session::Application_Options& options) { - return std::make_unique(session, options); +std::unique_ptr Http3Application::Create( + Session* session) { + Debug(session, "Selecting HTTP/3 application"); + return std::make_unique(session, options_); } -} // namespace node::quic +} // namespace quic +} // namespace node #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/http3.h b/src/quic/http3.h index 94860c9b771830..01f682a4829a3c 100644 --- a/src/quic/http3.h +++ b/src/quic/http3.h @@ -3,11 +3,40 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include +#include +#include #include "session.h" namespace node::quic { -std::unique_ptr 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 value); + static v8::Local GetConstructorTemplate( + Environment* env); + static void InitPerIsolate(IsolateData* isolate_data, + v8::Local target); + static void InitPerContext(Realm* realm, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + Http3Application(Environment* env, + v8::Local object, + const Session::Application_Options& options); + + std::unique_ptr 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& args); + + Session::Application_Options options_; +}; } // namespace node::quic diff --git a/src/quic/session.cc b/src/quic/session.cc index b1f18f834db19c..30f91163418d16 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -23,6 +23,7 @@ #include "data.h" #include "defs.h" #include "endpoint.h" +#include "http3.h" #include "logstream.h" #include "ncrypto.h" #include "packet.h" @@ -37,18 +38,22 @@ namespace node { using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; +using v8::BackingStoreInitializationMode; using v8::BigInt; using v8::Boolean; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; +using v8::Int32; using v8::Integer; using v8::Just; using v8::Local; +using v8::LocalVector; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; +using v8::ObjectTemplate; using v8::PropertyAttribute; using v8::String; using v8::Uint32; @@ -58,41 +63,32 @@ using v8::Value; namespace quic { #define SESSION_STATE(V) \ - /* Set if the JavaScript wrapper has a path-validation event listener */ \ V(PATH_VALIDATION, path_validation, uint8_t) \ - /* Set if the JavaScript wrapper has a version-negotiation event listener */ \ V(VERSION_NEGOTIATION, version_negotiation, uint8_t) \ - /* Set if the JavaScript wrapper has a datagram event listener */ \ V(DATAGRAM, datagram, uint8_t) \ - /* Set if the JavaScript wrapper has a session-ticket event listener */ \ V(SESSION_TICKET, session_ticket, uint8_t) \ V(CLOSING, closing, uint8_t) \ V(GRACEFUL_CLOSE, graceful_close, uint8_t) \ V(SILENT_CLOSE, silent_close, uint8_t) \ V(STATELESS_RESET, stateless_reset, uint8_t) \ - V(DESTROYED, destroyed, uint8_t) \ V(HANDSHAKE_COMPLETED, handshake_completed, uint8_t) \ V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \ V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \ V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \ - /* A Session is wrapped if it has been passed out to JS */ \ V(WRAPPED, wrapped, uint8_t) \ V(LAST_DATAGRAM_ID, last_datagram_id, uint64_t) #define SESSION_STATS(V) \ V(CREATED_AT, created_at) \ V(CLOSING_AT, closing_at) \ - V(DESTROYED_AT, destroyed_at) \ V(HANDSHAKE_COMPLETED_AT, handshake_completed_at) \ V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at) \ - V(GRACEFUL_CLOSING_AT, graceful_closing_at) \ V(BYTES_RECEIVED, bytes_received) \ V(BYTES_SENT, bytes_sent) \ V(BIDI_IN_STREAM_COUNT, bidi_in_stream_count) \ V(BIDI_OUT_STREAM_COUNT, bidi_out_stream_count) \ V(UNI_IN_STREAM_COUNT, uni_in_stream_count) \ V(UNI_OUT_STREAM_COUNT, uni_out_stream_count) \ - V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count) \ V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight) \ V(BYTES_IN_FLIGHT, bytes_in_flight) \ V(BLOCK_COUNT, block_count) \ @@ -108,7 +104,7 @@ namespace quic { V(DATAGRAMS_LOST, datagrams_lost) #define SESSION_JS_METHODS(V) \ - V(DoDestroy, destroy, false) \ + V(Destroy, destroy, false) \ V(GetRemoteAddress, getRemoteAddress, true) \ V(GetCertificate, getCertificate, true) \ V(GetEphemeralKeyInfo, getEphemeralKey, true) \ @@ -117,9 +113,9 @@ namespace quic { V(SilentClose, silentClose, false) \ V(UpdateKey, updateKey, false) \ V(OpenStream, openStream, false) \ - V(DoSendDatagram, sendDatagram, false) + V(SendDatagram, sendDatagram, false) -struct Session::State { +struct Session::State final { #define V(_, name, type) type name; SESSION_STATE(V) #undef V @@ -128,61 +124,31 @@ struct Session::State { STAT_STRUCT(Session, SESSION) // ============================================================================ -// Used to conditionally trigger sending an explicit connection -// close. If there are multiple MaybeCloseConnectionScope in the -// stack, the determination of whether to send the close will be -// done once the final scope is closed. -struct Session::MaybeCloseConnectionScope final { - Session* session; - bool silent = false; - MaybeCloseConnectionScope(Session* session_, bool silent_) - : session(session_), - silent(silent_ || session->connection_close_depth_ > 0) { - Debug(session_, - "Entering maybe close connection scope. Silent? %s", - silent ? "yes" : "no"); - session->connection_close_depth_++; - } - DISALLOW_COPY_AND_MOVE(MaybeCloseConnectionScope) - ~MaybeCloseConnectionScope() { - // We only want to trigger the sending the connection close if ... - // a) Silent is not explicitly true at this scope. - // b) We're not within the scope of an ngtcp2 callback, and - // c) We are not already in a closing or draining period. - if (--session->connection_close_depth_ == 0 && !silent && - session->can_send_packets()) { - session->SendConnectionClose(); - } - } -}; -// ============================================================================ -// Used to conditionally trigger sending of any pending data the session may -// be holding onto. If there are multiple SendPendingDataScope in the stack, -// the determination of whether to send the data will be done once the final -// scope is closed. +class Http3Application; -Session::SendPendingDataScope::SendPendingDataScope(Session* session) - : session(session) { - Debug(session, "Entering send pending data scope"); - session->send_scope_depth_++; +namespace { +std::string to_string(PreferredAddress::Policy policy) { + switch (policy) { + case PreferredAddress::Policy::USE_PREFERRED: + return "use"; + case PreferredAddress::Policy::IGNORE_PREFERRED: + return "ignore"; + } + return ""; } -Session::SendPendingDataScope::SendPendingDataScope( - const BaseObjectPtr& session) - : SendPendingDataScope(session.get()) {} - -Session::SendPendingDataScope::~SendPendingDataScope() { - if (--session->send_scope_depth_ == 0 && session->can_send_packets()) { - session->application().SendPendingData(); +std::string to_string(Side side) { + switch (side) { + case Side::CLIENT: + return "client"; + case Side::SERVER: + return "server"; } + return ""; } -// ============================================================================ - -namespace { - -constexpr std::string to_string(ngtcp2_encryption_level level) { +std::string to_string(ngtcp2_encryption_level level) { switch (level) { case NGTCP2_ENCRYPTION_LEVEL_1RTT: return "1rtt"; @@ -196,6 +162,28 @@ constexpr std::string to_string(ngtcp2_encryption_level level) { return ""; } +std::string to_string(ngtcp2_cc_algo cc_algorithm) { +#define V(name, label) \ + case NGTCP2_CC_ALGO_##name: \ + return #label; + switch (cc_algorithm) { CC_ALGOS(V) } + return ""; +#undef V +} + +Maybe getAlgoFromString(Environment* env, Local input) { + auto& state = BindingData::Get(env); +#define V(name, str) \ + if (input->StringEquals(state.str##_string())) { \ + return Just(NGTCP2_CC_ALGO_##name); \ + } + + CC_ALGOS(V) + +#undef V + return Nothing(); +} + // Qlog is a JSON-based logging format that is being standardized for low-level // debug logging of QUIC connections and dataflows. The qlog output is generated // optionally by ngtcp2 for us. The on_qlog_write callback is registered with @@ -225,8 +213,8 @@ void ngtcp2_debug_log(void* user_data, const char* fmt, ...) { template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { + const Local& object, + const Local& name) { Local value; PreferredAddress::Policy policy = PreferredAddress::Policy::USE_PREFERRED; if (!object->Get(env->context(), name).ToLocal(&value) || @@ -240,8 +228,8 @@ bool SetOption(Environment* env, template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { + const Local& object, + const Local& name) { Local value; TLSContext::Options opts; if (!object->Get(env->context(), name).ToLocal(&value) || @@ -252,41 +240,96 @@ bool SetOption(Environment* env, return true; } -template +template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { + const Local& object, + const Local& name) { Local value; - Session::Application_Options opts; + TransportParams::Options opts; if (!object->Get(env->context(), name).ToLocal(&value) || - !Session::Application_Options::From(env, value).To(&opts)) { + !TransportParams::Options::From(env, value).To(&opts)) { return false; } options->*member = opts; return true; } -template +template Opt::*member> bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { + const Local& object, + const Local& name) { Local value; - TransportParams::Options opts; - if (!object->Get(env->context(), name).ToLocal(&value) || - !TransportParams::Options::From(env, value).To(&opts)) { + if (!object->Get(env->context(), name).ToLocal(&value)) { return false; } - options->*member = opts; + if (!value->IsUndefined()) { + // We currently only support Http3Application for this option. + if (!Http3Application::HasInstance(env, value)) { + THROW_ERR_INVALID_ARG_TYPE(env, + "Application must be an Http3Application"); + return false; + } + Http3Application* app; + ASSIGN_OR_RETURN_UNWRAP(&app, value.As(), false); + CHECK_NOT_NULL(app); + auto& assigned = options->*member = + BaseObjectPtr(app); + assigned->Detach(); + } + return true; +} + +template +bool SetOption(Environment* env, + Opt* options, + const Local& object, + const Local& name) { + Local 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()).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 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; + CC_ALGOS(V) +#undef V + default: + THROW_ERR_INVALID_ARG_VALUE(env, + "The cc_algorithm option is invalid"); + return false; + } + algo = static_cast(num->Value()); + } + options->*member = algo; + } return true; } } // namespace // ============================================================================ -Session::Config::Config(Side side, - const Endpoint& endpoint, +Session::Config::Config(Environment* env, + Side side, const Options& options, uint32_t version, const SocketAddress& local_address, @@ -307,6 +350,14 @@ Session::Config::Config(Side side, // We currently do not support Path MTU Discovery. Once we do, unset this. settings.no_pmtud = 1; + // Per the ngtcp2 documentation, when no_tx_udp_payload_size_shaping is set + // to a non-zero value, ngtcp2 not to limit the UDP payload size to + // NGTCP2_MAX_UDP_PAYLOAD_SIZE` and will instead "use the minimum size among + // the given buffer size, :member:`max_tx_udp_payload_size`, and the + // received max_udp_payload_size QUIC transport parameter." For now, this + // works for us, especially since we do not implement Path MTU discovery. + settings.no_tx_udp_payload_size_shaping = 1; + settings.max_tx_udp_payload_size = options.max_payload_size; settings.tokenlen = 0; settings.token = nullptr; @@ -315,31 +366,24 @@ Session::Config::Config(Side side, settings.qlog_write = on_qlog_write; } - if (endpoint.env()->enabled_debug_list()->enabled( - DebugCategory::NGTCP2_DEBUG)) { + if (env->enabled_debug_list()->enabled(DebugCategory::NGTCP2_DEBUG)) { settings.log_printf = ngtcp2_debug_log; } - // We pull parts of the settings for the session from the endpoint options. - auto& config = endpoint.options(); - settings.no_tx_udp_payload_size_shaping = config.no_udp_payload_size_shaping; - settings.handshake_timeout = config.handshake_timeout; - settings.max_stream_window = config.max_stream_window; - settings.max_window = config.max_window; - settings.cc_algo = config.cc_algorithm; - settings.max_tx_udp_payload_size = config.max_payload_size; - if (config.unacknowledged_packet_threshold > 0) { - settings.ack_thresh = config.unacknowledged_packet_threshold; - } + settings.handshake_timeout = options.handshake_timeout; + settings.max_stream_window = options.max_stream_window; + settings.max_window = options.max_window; + settings.ack_thresh = options.unacknowledged_packet_threshold; + settings.cc_algo = options.cc_algorithm; } -Session::Config::Config(const Endpoint& endpoint, +Session::Config::Config(Environment* env, const Options& options, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& ocid) - : Config(Side::CLIENT, - endpoint, + : Config(env, + Side::CLIENT, options, options.version, local_address, @@ -380,17 +424,7 @@ std::string Session::Config::ToString() const { DebugIndentScope indent; auto prefix = indent.Prefix(); std::string res("{"); - - auto sidestr = ([&] { - switch (side) { - case Side::CLIENT: - return "client"; - case Side::SERVER: - return "server"; - } - return ""; - })(); - res += prefix + "side: " + std::string(sidestr); + res += prefix + "side: " + to_string(side); res += prefix + "options: " + options.ToString(); res += prefix + "version: " + std::to_string(version); res += prefix + "local address: " + local_address.ToString(); @@ -422,8 +456,10 @@ Maybe Session::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) || - !SET(transport_params) || !SET(tls_options) || - !SET(application_options) || !SET(qlog)) { + !SET(transport_params) || !SET(tls_options) || !SET(qlog) || + !SET(application_provider) || !SET(handshake_timeout) || + !SET(max_stream_window) || !SET(max_window) || !SET(max_payload_size) || + !SET(unacknowledged_packet_threshold) || !SET(cc_algorithm)) { return Nothing(); } @@ -438,7 +474,6 @@ Maybe Session::Options::From(Environment* env, void Session::Options::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("transport_params", transport_params); tracker->TrackField("crypto_options", tls_options); - tracker->TrackField("application_options", application_options); tracker->TrackField("cid_factory_ref", cid_factory_ref); } @@ -448,1966 +483,2274 @@ std::string Session::Options::ToString() const { std::string res("{"); res += prefix + "version: " + std::to_string(version); res += prefix + "min version: " + std::to_string(min_version); - - auto policy = ([&] { - switch (preferred_address_strategy) { - case PreferredAddress::Policy::USE_PREFERRED: - return "use"; - case PreferredAddress::Policy::IGNORE_PREFERRED: - return "ignore"; - } - return ""; - })(); - res += prefix + "preferred address policy: " + std::string(policy); + res += prefix + + "preferred address policy: " + to_string(preferred_address_strategy); res += prefix + "transport params: " + transport_params.ToString(); res += prefix + "crypto options: " + tls_options.ToString(); - res += prefix + "application options: " + application_options.ToString(); - res += prefix + "qlog: " + (qlog ? std::string("yes") : std::string("no")); + if (qlog) { + res += prefix + "qlog: yes"; + } + if (handshake_timeout == UINT64_MAX) { + res += prefix + "handshake timeout: "; + } 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 + "max payload size: " + std::to_string(max_payload_size); + if (unacknowledged_packet_threshold != 0) { + res += prefix + "unacknowledged packet threshold: " + + std::to_string(unacknowledged_packet_threshold); + } else { + res += prefix + "unacknowledged packet threshold: "; + } + res += prefix + "cc algorithm: " + to_string(cc_algorithm); res += indent.Close(); return res; } // ============================================================================ +// ngtcp2 static callback functions -bool Session::HasInstance(Environment* env, Local value) { - return GetConstructorTemplate(env)->HasInstance(value); -} +// Utility used only within Session::Impl to reduce boilerplate +#define NGTCP2_CALLBACK_SCOPE(name) \ + auto name = Impl::From(conn, user_data); \ + if (name == nullptr) return NGTCP2_ERR_CALLBACK_FAILURE; \ + NgTcp2CallbackScope scope(name->env()); + +// Session::Impl maintains most of the internal state of an active Session. +struct Session::Impl final : public MemoryRetainer { + Session* session_; + AliasedStruct stats_; + AliasedStruct state_; + BaseObjectWeakPtr endpoint_; + Config config_; + SocketAddress local_address_; + SocketAddress remote_address_; + std::unique_ptr application_; + StreamsMap streams_; + TimerWrapHandle timer_; + size_t send_scope_depth_ = 0; + QuicError last_error_; + PendingStream::PendingStreamQueue pending_bidi_stream_queue_; + PendingStream::PendingStreamQueue pending_uni_stream_queue_; + + Impl(Session* session, Endpoint* endpoint, const Config& config) + : session_(session), + stats_(env()->isolate()), + state_(env()->isolate()), + endpoint_(endpoint), + config_(config), + local_address_(config.local_address), + remote_address_(config.remote_address), + application_(SelectApplication(session, config_)), + timer_(session_->env(), [this] { session_->OnTimeout(); }) { + timer_.Unref(); + } + + inline bool is_closing() const { return state_->closing; } + + /** + * @returns {boolean} Returns true if the Session can be destroyed + * immediately. + */ + bool Close() { + if (state_->closing) return true; + state_->closing = 1; + STAT_RECORD_TIMESTAMP(Stats, closing_at); + + // Iterate through all of the known streams and close them. The streams + // will remove themselves from the Session as soon as they are closed. + // Note: we create a copy because the streams will remove themselves + // while they are cleaning up which will invalidate the iterator. + StreamsMap streams = streams_; + for (auto& stream : streams) stream.second->Destroy(last_error_); + DCHECK(streams.empty()); + + // Clear the pending streams. + while (!pending_bidi_stream_queue_.IsEmpty()) { + pending_bidi_stream_queue_.PopFront()->reject(last_error_); + } + while (!pending_uni_stream_queue_.IsEmpty()) { + pending_uni_stream_queue_.PopFront()->reject(last_error_); + } -BaseObjectPtr Session::Create( - Endpoint* endpoint, - const Config& config, - TLSContext* tls_context, - const std::optional& ticket) { - Local obj; - if (!GetConstructorTemplate(endpoint->env()) - ->InstanceTemplate() - ->NewInstance(endpoint->env()->context()) - .ToLocal(&obj)) { - return {}; - } + // If we are able to send packets, we should try sending a connection + // close packet to the remote peer. + if (!state_->silent_close) { + session_->SendConnectionClose(); + } - return MakeDetachedBaseObject( - endpoint, obj, config, tls_context, ticket); -} + timer_.Close(); -Session::Session(Endpoint* endpoint, - v8::Local object, - const Config& config, - TLSContext* tls_context, - const std::optional& session_ticket) - : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION), - stats_(env()->isolate()), - state_(env()->isolate()), - allocator_(BindingData::Get(env())), - endpoint_(BaseObjectWeakPtr(endpoint)), - config_(config), - local_address_(config.local_address), - remote_address_(config.remote_address), - connection_(InitConnection()), - tls_session_(tls_context->NewSession(this, session_ticket)), - application_(select_application()), - timer_(env(), [this] { OnTimeout(); }) { - MakeWeak(); + return !state_->wrapped; + } - Debug(this, "Session created."); + ~Impl() { + // Ensure that Close() was called before dropping + DCHECK(is_closing()); + DCHECK(endpoint_); - timer_.Unref(); + // Removing the session from the endpoint may cause the endpoint to be + // destroyed if it is waiting on the last session to be destroyed. Let's + // grab a reference just to be safe for the rest of the function. + BaseObjectPtr endpoint = endpoint_; + endpoint_.reset(); - application().ExtendMaxStreams(EndpointLabel::LOCAL, - Direction::BIDIRECTIONAL, - TransportParams::DEFAULT_MAX_STREAMS_BIDI); - application().ExtendMaxStreams(EndpointLabel::LOCAL, - Direction::UNIDIRECTIONAL, - TransportParams::DEFAULT_MAX_STREAMS_UNI); + MaybeStackBuffer cids( + ngtcp2_conn_get_scid(*session_, nullptr)); + ngtcp2_conn_get_scid(*session_, cids.out()); - const auto defineProperty = [&](auto name, auto value) { - object - ->DefineOwnProperty( - env()->context(), name, value, PropertyAttribute::ReadOnly) - .Check(); - }; + MaybeStackBuffer tokens( + ngtcp2_conn_get_active_dcid(*session_, nullptr)); + ngtcp2_conn_get_active_dcid(*session_, tokens.out()); - defineProperty(env()->state_string(), state_.GetArrayBuffer()); - defineProperty(env()->stats_string(), stats_.GetArrayBuffer()); + endpoint->DisassociateCID(config_.dcid); + endpoint->DisassociateCID(config_.preferred_address_cid); - auto& state = BindingData::Get(env()); + for (size_t n = 0; n < cids.length(); n++) { + endpoint->DisassociateCID(CID(cids[n])); + } - if (config_.options.qlog) [[unlikely]] { - qlog_stream_ = LogStream::Create(env()); - if (qlog_stream_) - defineProperty(state.qlog_string(), qlog_stream_->object()); - } + for (size_t n = 0; n < tokens.length(); n++) { + if (tokens[n].token_present) { + endpoint->DisassociateStatelessResetToken( + StatelessResetToken(tokens[n].token)); + } + } - if (config_.options.tls_options.keylog) [[unlikely]] { - keylog_stream_ = LogStream::Create(env()); - if (keylog_stream_) - defineProperty(state.keylog_string(), keylog_stream_->object()); + endpoint->RemoveSession(config_.scid, remote_address_); } - // We index the Session by our local CID (the scid) and dcid (the peer's cid) - endpoint_->AddSession(config_.scid, BaseObjectPtr(this)); - endpoint_->AssociateCID(config_.dcid, config_.scid); + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackField("config", config_); + tracker->TrackField("endpoint", endpoint_); + tracker->TrackField("streams", streams_); + tracker->TrackField("local_address", local_address_); + tracker->TrackField("remote_address", remote_address_); + tracker->TrackField("application", application_); + tracker->TrackField("timer", timer_); + } + SET_SELF_SIZE(Impl) + SET_MEMORY_INFO_NAME(Session::Impl) - UpdateDataStats(); -} + Environment* env() const { return session_->env(); } -Session::~Session() { - Debug(this, "Session destroyed."); - // Double check that our timer has stopped. - CHECK(!timer_); - if (conn_closebuf_) { - conn_closebuf_->Done(0); + // Gets the Session pointer from the user_data void pointer + // provided by ngtcp2. + static Session* From(ngtcp2_conn* conn, void* user_data) { + if (user_data == nullptr) [[unlikely]] { + return nullptr; + } + auto session = static_cast(user_data); + if (session->is_destroyed()) [[unlikely]] { + return nullptr; + } + return session; } - if (qlog_stream_) { - Debug(this, "Closing the qlog stream for this session"); - env()->SetImmediate( - [ptr = std::move(qlog_stream_)](Environment*) { ptr->End(); }); + + // JavaScript APIs + + static void Destroy(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } + session->Destroy(); } - if (keylog_stream_) { - Debug(this, "Closing the keylog stream for this session"); - env()->SetImmediate( - [ptr = std::move(keylog_stream_)](Environment*) { ptr->End(); }); + + static void GetRemoteAddress(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); + + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } + + auto address = session->remote_address(); + args.GetReturnValue().Set( + SocketAddressBase::Create(env, std::make_shared(address)) + ->object()); } - DCHECK(streams_.empty()); -} -size_t Session::max_packet_size() const { - return ngtcp2_conn_get_max_tx_udp_payload_size(*this); -} + static void GetCertificate(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -Session::operator ngtcp2_conn*() const { - return connection_.get(); -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -uint32_t Session::version() const { - return config_.version; -} + Local ret; + if (session->tls_session().cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); + } -Endpoint& Session::endpoint() const { - return *endpoint_; -} + static void GetEphemeralKeyInfo(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -TLSSession& Session::tls_session() { - return *tls_session_; -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -Session::Application& Session::application() { - return *application_; -} + Local ret; + if (!session->is_server() && + session->tls_session().ephemeral_key(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); + } -const SocketAddress& Session::remote_address() const { - return remote_address_; -} + static void GetPeerCertificate(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -const SocketAddress& Session::local_address() const { - return local_address_; -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -bool Session::is_closing() const { - return state_->closing; -} + Local ret; + if (session->tls_session().peer_cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); + } -bool Session::is_graceful_closing() const { - return state_->graceful_close; -} + static void GracefulClose(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -bool Session::is_silent_closing() const { - return state_->silent_close; -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -bool Session::is_destroyed() const { - return state_->destroyed; -} + session->Close(Session::CloseMethod::GRACEFUL); + } -bool Session::is_server() const { - return config_.side == Side::SERVER; -} + static void SilentClose(const FunctionCallbackInfo& args) { + // This is exposed for testing purposes only! + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -std::string Session::diagnostic_name() const { - const auto get_type = [&] { return is_server() ? "server" : "client"; }; + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } - return std::string("Session (") + get_type() + "," + - std::to_string(env()->thread_id()) + ":" + - std::to_string(static_cast(get_async_id())) + ")"; -} + session->Close(Session::CloseMethod::SILENT); + } -const Session::Config& Session::config() const { - return config_; -} + static void UpdateKey(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -const Session::Options& Session::options() const { - return config_.options; -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { - if (qlog_stream_) { - // Fun fact... ngtcp2 does not emit the final qlog statement until the - // ngtcp2_conn object is destroyed. Ideally, destroying is explicit, but - // sometimes the Session object can be garbage collected without being - // explicitly destroyed. During those times, we cannot call out to - // JavaScript. Because we don't know for sure if we're in in a GC when this - // is called, it is safer to just defer writes to immediate, and to keep it - // consistent, let's just always defer (this is not performance sensitive so - // the deferring is fine). - std::vector buffer(len); - memcpy(buffer.data(), data, len); - Debug(this, "Emitting qlog data to the qlog stream"); - env()->SetImmediate( - [ptr = qlog_stream_, buffer = std::move(buffer), flags](Environment*) { - ptr->Emit(buffer.data(), - buffer.size(), - flags & NGTCP2_QLOG_WRITE_FLAG_FIN - ? LogStream::EmitOption::FIN - : LogStream::EmitOption::NONE); - }); + // Initiating a key update may fail if it is done too early (either + // before the TLS handshake has been confirmed or while a previous + // key update is being processed). When it fails, InitiateKeyUpdate() + // will return false. + SendPendingDataScope send_scope(session); + args.GetReturnValue().Set(session->tls_session().InitiateKeyUpdate()); } -} -const TransportParams Session::GetLocalTransportParams() const { - DCHECK(!is_destroyed()); - return TransportParams(ngtcp2_conn_get_local_transport_params(*this)); -} + static void OpenStream(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); -const TransportParams Session::GetRemoteTransportParams() const { - DCHECK(!is_destroyed()); - return TransportParams(ngtcp2_conn_get_remote_transport_params(*this)); -} + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } -void Session::SetLastError(QuicError&& error) { - Debug(this, "Setting last error to %s", error); - last_error_ = std::move(error); -} + DCHECK(args[0]->IsUint32()); -void Session::Close(Session::CloseMethod method) { - if (is_destroyed()) return; - switch (method) { - case CloseMethod::DEFAULT: { - Debug(this, "Closing session"); - DoClose(false); - break; + // GetDataQueueFromSource handles type validation. + std::shared_ptr data_source = + Stream::GetDataQueueFromSource(env, args[1]).ToChecked(); + if (data_source == nullptr) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid data source"); } - case CloseMethod::SILENT: { - Debug(this, "Closing session silently"); - DoClose(true); - break; - } - case CloseMethod::GRACEFUL: { - if (is_graceful_closing()) return; - Debug(this, "Closing session gracefully"); - // If there are no open streams, then we can close just immediately and - // not worry about waiting around for the right moment. - if (streams_.empty()) { - DoClose(false); - } else { - state_->graceful_close = 1; - STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); - } - break; + + SendPendingDataScope send_scope(session); + auto direction = static_cast(args[0].As()->Value()); + Local stream; + if (session->OpenStream(direction, std::move(data_source)).ToLocal(&stream)) + [[likely]] { + args.GetReturnValue().Set(stream); } } -} -void Session::Destroy() { - if (is_destroyed()) return; - Debug(this, "Session destroyed"); - - // The DoClose() method should have already been called. - DCHECK(state_->closing); + static void SendDatagram(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - // We create a copy of the streams because they will remove themselves - // from streams_ as they are cleaning up, causing the iterator to be - // invalidated. - auto streams = streams_; - for (auto& stream : streams) stream.second->Destroy(last_error_); + if (session->is_destroyed()) { + THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + } - while (!pending_bidi_stream_queue_.IsEmpty()) { - pending_bidi_stream_queue_.PopFront()->reject(); - } - while (!pending_uni_stream_queue_.IsEmpty()) { - pending_uni_stream_queue_.PopFront()->reject(); + DCHECK(args[0]->IsArrayBufferView()); + SendPendingDataScope send_scope(session); + args.GetReturnValue().Set(BigInt::New( + env->isolate(), + session->SendDatagram(Store(args[0].As())))); } - DCHECK(streams_.empty()); + // Internal ngtcp2 callbacks - STAT_RECORD_TIMESTAMP(Stats, destroyed_at); - state_->closing = 0; - state_->graceful_close = 0; + static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + return session->application().AcknowledgeStreamData(stream_id, datalen) + ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; + } - timer_.Close(); + static int on_acknowledge_datagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->DatagramStatus(dgram_id, quic::DatagramStatus::ACKNOWLEDGED); + return NGTCP2_SUCCESS; + } - // The Session instances are kept alive using a in the Endpoint. Removing the - // Session from the Endpoint will free that pointer, allowing the Session to - // be deconstructed once the stack unwinds and any remaining - // BaseObjectPtr instances fall out of scope. + static int on_cid_status(ngtcp2_conn* conn, + ngtcp2_connection_id_status_type type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + std::optional maybe_reset_token; + if (token != nullptr) maybe_reset_token.emplace(token); + auto& endpoint = session->endpoint(); + switch (type) { + case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: { + endpoint.AssociateCID(session->config().scid, CID(cid)); + if (token != nullptr) { + endpoint.AssociateStatelessResetToken(StatelessResetToken(token), + session); + } + break; + } + case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: { + endpoint.DisassociateCID(CID(cid)); + if (token != nullptr) { + endpoint.DisassociateStatelessResetToken(StatelessResetToken(token)); + } + break; + } + } + return NGTCP2_SUCCESS; + } - MaybeStackBuffer cids(ngtcp2_conn_get_scid(*this, nullptr)); - ngtcp2_conn_get_scid(*this, cids.out()); + static int on_extend_max_remote_streams_bidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + // TODO(@jasnell): Do anything here? + return NGTCP2_SUCCESS; + } - MaybeStackBuffer tokens( - ngtcp2_conn_get_active_dcid(*this, nullptr)); - ngtcp2_conn_get_active_dcid(*this, tokens.out()); + static int on_extend_max_remote_streams_uni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + // TODO(@jasnell): Do anything here? + return NGTCP2_SUCCESS; + } - endpoint_->DisassociateCID(config_.dcid); - endpoint_->DisassociateCID(config_.preferred_address_cid); + static int on_extend_max_streams_bidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->ProcessPendingBidiStreams(); + return NGTCP2_SUCCESS; + } - for (size_t n = 0; n < cids.length(); n++) { - endpoint_->DisassociateCID(CID(cids[n])); + static int on_extend_max_streams_uni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->ProcessPendingUniStreams(); + return NGTCP2_SUCCESS; } - for (size_t n = 0; n < tokens.length(); n++) { - if (tokens[n].token_present) { - endpoint_->DisassociateStatelessResetToken( - StatelessResetToken(tokens[n].token)); - } + static int on_extend_max_stream_data(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->application().ExtendMaxStreamData(Stream::From(stream_user_data), + max_data); + return NGTCP2_SUCCESS; } - state_->destroyed = 1; + static int on_get_new_cid(ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->GenerateNewConnectionId(cid, cidlen, token); + return NGTCP2_SUCCESS; + } - // Removing the session from the endpoint may cause the endpoint to be - // destroyed if it is waiting on the last session to be destroyed. Let's grab - // a reference just to be safe for the rest of the function. - BaseObjectPtr endpoint = std::move(endpoint_); - endpoint->RemoveSession(config_.scid); -} + static int on_handshake_completed(ngtcp2_conn* conn, void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + return session->HandshakeCompleted() ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; + } -bool Session::Receive(Store&& store, - const SocketAddress& local_address, - const SocketAddress& remote_address) { - if (is_destroyed()) return false; + static int on_handshake_confirmed(ngtcp2_conn* conn, void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->HandshakeConfirmed(); + return NGTCP2_SUCCESS; + } - const auto receivePacket = [&](ngtcp2_path* path, ngtcp2_vec vec) { - DCHECK(!is_destroyed()); + static int on_lost_datagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->DatagramStatus(dgram_id, quic::DatagramStatus::LOST); + return NGTCP2_SUCCESS; + } - uint64_t now = uv_hrtime(); - ngtcp2_pkt_info pi{}; // Not used but required. - int err = ngtcp2_conn_read_pkt(*this, path, &pi, vec.base, vec.len, now); - switch (err) { - case 0: { - // Return true so we send after receiving. - Debug(this, "Session successfully received packet"); - return true; - } - case NGTCP2_ERR_DRAINING: { - // Connection has entered the draining state, no further data should be - // sent. This happens when the remote peer has sent a CONNECTION_CLOSE. - Debug(this, "Session is draining"); - return false; - } - case NGTCP2_ERR_CLOSING: { - // Connection has entered the closing state, no further data should be - // sent. This happens when the local peer has called - // ngtcp2_conn_write_connection_close. - Debug(this, "Session is closing"); - return false; - } - case NGTCP2_ERR_CRYPTO: { - // Crypto error happened! Set the last error to the tls alert - last_error_ = QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(*this)); - Debug(this, "Crypto error while receiving packet: %s", last_error_); - Close(); - return false; - } - case NGTCP2_ERR_RETRY: { - // This should only ever happen on the server. We have to send a path - // validation challenge in the form of a RETRY packet to the peer and - // drop the connection. - DCHECK(is_server()); - Debug(this, "Server must send a retry packet"); - endpoint_->SendRetry(PathDescriptor{ - version(), - config_.dcid, - config_.scid, - local_address_, - remote_address_, - }); - Close(CloseMethod::SILENT); - return false; - } - case NGTCP2_ERR_DROP_CONN: { - // There's nothing else to do but drop the connection state. - Debug(this, "Session must drop the connection"); - Close(CloseMethod::SILENT); - return false; - } + static int on_path_validation(ngtcp2_conn* conn, + uint32_t flags, + const ngtcp2_path* path, + const ngtcp2_path* old_path, + ngtcp2_path_validation_result res, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + bool flag_preferred_address = + flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR; + ValidatedPath newValidatedPath{ + std::make_shared(path->local.addr), + std::make_shared(path->remote.addr)}; + std::optional oldValidatedPath = std::nullopt; + if (old_path != nullptr) { + oldValidatedPath = + ValidatedPath{std::make_shared(old_path->local.addr), + std::make_shared(old_path->remote.addr)}; } - // Shouldn't happen but just in case. - last_error_ = QuicError::ForNgtcp2Error(err); - Debug(this, "Error while receiving packet: %s (%d)", last_error_, err); - Close(); - return false; - }; - - remote_address_ = remote_address; - Path path(local_address, remote_address_); - size_t len = store.length(); - Debug(this, - "Session is receiving %" PRIu64 "-byte packet received along path %s", - len, - path); - STAT_INCREMENT_N(Stats, bytes_received, len); - - // After every packet we receive and successfully process, we - // want to process and send any pending data. - if (receivePacket(&path, store)) application().SendPendingData(); - - Debug(this, "Session successfully received %" PRIu64 "-byte packet", len); - - return true; -} + session->EmitPathValidation(static_cast(res), + PathValidationFlags{flag_preferred_address}, + newValidatedPath, + oldValidatedPath); + return NGTCP2_SUCCESS; + } -void Session::Send(BaseObjectPtr&& packet) { - // Sending a Packet is generally best effort. If we're not in a state - // where we can send a packet, it's ok to drop it on the floor. The - // packet loss mechanisms will cause the packet data to be resent later - // if appropriate (and possible). - DCHECK(!is_destroyed()); - DCHECK(!is_in_draining_period()); + static int on_receive_datagram(ngtcp2_conn* conn, + uint32_t flags, + const uint8_t* data, + size_t datalen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->DatagramReceived( + data, + datalen, + DatagramReceivedFlags{ + .early = (flags & NGTCP2_DATAGRAM_FLAG_0RTT) == + NGTCP2_DATAGRAM_FLAG_0RTT, + }); + return NGTCP2_SUCCESS; + } - if (can_send_packets() && packet->length() > 0) { - Debug(this, "Session is sending %s", packet->ToString()); - STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); - endpoint_->Send(std::move(packet)); - packet.reset(); - return; + static int on_receive_new_token(ngtcp2_conn* conn, + const uint8_t* token, + size_t tokenlen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + // We currently do nothing with this callback. + return NGTCP2_SUCCESS; } - Debug(this, "Session could not send %s", packet->ToString()); - packet->Done(packet->length() > 0 ? UV_ECANCELED : 0); -} + static int on_receive_rx_key(ngtcp2_conn* conn, + ngtcp2_encryption_level level, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + CHECK(!session->is_server()); -void Session::Send(BaseObjectPtr&& packet, const PathStorage& path) { - UpdatePath(path); - Send(std::move(packet)); - packet.reset(); -} + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; -void Session::UpdatePacketTxTime() { - ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); -} + Debug(session, + "Receiving RX key for level %s for dcid %s", + to_string(level), + session->config().dcid); -uint64_t Session::SendDatagram(Store&& data) { - auto tp = ngtcp2_conn_get_remote_transport_params(*this); - uint64_t max_datagram_size = tp->max_datagram_frame_size; - if (max_datagram_size == 0 || data.length() > max_datagram_size) { - // Datagram is too large. - Debug(this, "Data is too large to send as a datagram"); - return 0; + return session->application().Start() ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; } - Debug(this, "Session is sending datagram"); - BaseObjectPtr packet; - uint8_t* pos = nullptr; - int accepted = 0; - ngtcp2_vec vec = data; - PathStorage path; - int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; - uint64_t did = state_->last_datagram_id + 1; - - // Let's give it a max number of attempts to send the datagram - static const int kMaxAttempts = 16; - int attempts = 0; + static int on_receive_stateless_reset(ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->impl_->state_->stateless_reset = 1; + return NGTCP2_SUCCESS; + } - for (;;) { - // We may have to make several attempts at encoding and sending the - // datagram packet. On each iteration here we'll try to encode the - // datagram. It's entirely up to ngtcp2 whether to include the datagram - // in the packet on each call to ngtcp2_conn_writev_datagram. - if (!packet) { - packet = Packet::Create(env(), - endpoint_.get(), - remote_address_, - ngtcp2_conn_get_max_tx_udp_payload_size(*this), - "datagram"); - // Typically sending datagrams is best effort, but if we cannot create - // the packet, then we handle it as a fatal error. - if (!packet) { - last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); - Close(CloseMethod::SILENT); - return 0; - } - pos = ngtcp2_vec(*packet).base; + static int on_receive_stream_data(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + Stream::ReceiveDataFlags data_flags{ + // The fin flag indicates that this is the last chunk of data we will + // receive on this stream. + .fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) == + NGTCP2_STREAM_DATA_FLAG_FIN, + // Stream data is early if it is received before the TLS handshake is + // complete. + .early = (flags & NGTCP2_STREAM_DATA_FLAG_0RTT) == + NGTCP2_STREAM_DATA_FLAG_0RTT, + }; + + // We received data for a stream! What we don't know yet at this point + // is whether the application wants us to treat this as a control stream + // data (something the application will handle on its own) or a user stream + // data (something that we should create a Stream handle for that is passed + // out to JavaScript). HTTP3, for instance, will generally create three + // control stream in either direction and we want to make sure those are + // never exposed to users and that we don't waste time creating Stream + // handles for them. So, what we do here is pass the stream data on to the + // application for processing. If it ends up being a user stream, the + // application will handle creating the Stream handle and passing that off + // to the JavaScript side. + if (!session->application().ReceiveStreamData( + stream_id, data, datalen, data_flags, stream_user_data)) { + return NGTCP2_ERR_CALLBACK_FAILURE; } - ssize_t nwrite = ngtcp2_conn_writev_datagram(*this, - &path.path, - nullptr, - pos, - packet->length(), - &accepted, - flags, - did, - &vec, - 1, - uv_hrtime()); - ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); + return NGTCP2_SUCCESS; + } - if (nwrite <= 0) { - // Nothing was written to the packet. - switch (nwrite) { - case 0: { - // We cannot send data because of congestion control or the data will - // not fit. Since datagrams are best effort, we are going to abandon - // the attempt and just return. - CHECK_EQ(accepted, 0); - packet->Done(UV_ECANCELED); - return 0; - } - case NGTCP2_ERR_WRITE_MORE: { - // We keep on looping! Keep on sending! - continue; - } - case NGTCP2_ERR_INVALID_STATE: { - // The remote endpoint does not want to accept datagrams. That's ok, - // just return 0. - packet->Done(UV_ECANCELED); - return 0; - } - case NGTCP2_ERR_INVALID_ARGUMENT: { - // The datagram is too large. That should have been caught above but - // that's ok. We'll just abandon the attempt and return. - packet->Done(UV_ECANCELED); - return 0; - } - case NGTCP2_ERR_PKT_NUM_EXHAUSTED: { - // We've exhausted the packet number space. Sadly we have to treat it - // as a fatal condition. - break; - } - case NGTCP2_ERR_CALLBACK_FAILURE: { - // There was an internal failure. Sadly we have to treat it as a fatal - // condition. - break; - } - } - packet->Done(UV_ECANCELED); - last_error_ = QuicError::ForNgtcp2Error(nwrite); - Close(CloseMethod::SILENT); - return 0; - } + static int on_receive_tx_key(ngtcp2_conn* conn, + ngtcp2_encryption_level level, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + CHECK(session->is_server()); - // In this case, a complete packet was written and we need to send it along. - // Note that this doesn't mean that the packet actually contains the - // datagram! We'll check that next by checking the accepted value. - packet->Truncate(nwrite); - Send(std::move(packet)); - packet.reset(); + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; - if (accepted != 0) { - // Yay! The datagram was accepted into the packet we just sent and we can - // return the datagram ID. - Debug(this, "Session successfully encoded datagram"); - STAT_INCREMENT(Stats, datagrams_sent); - STAT_INCREMENT_N(Stats, bytes_sent, vec.len); - state_->last_datagram_id = did; - return did; - } + Debug(session, + "Receiving TX key for level %s for dcid %s", + to_string(level), + session->config().dcid); + return session->application().Start() ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; + } - // We sent a packet, but it wasn't the datagram packet. That can happen. - // Let's loop around and try again. - if (++attempts == kMaxAttempts) { - Debug(this, "Too many attempts to send the datagram"); - // Too many attempts to send the datagram. - break; + static int on_receive_version_negotiation(ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->EmitVersionNegotiation(*hd, sv, nsv); + return NGTCP2_SUCCESS; + } + + static int on_remove_connection_id(ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->endpoint().DisassociateCID(CID(cid)); + return NGTCP2_SUCCESS; + } + + static int on_select_preferred_address(ngtcp2_conn* conn, + ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + PreferredAddress preferred_address(dest, paddr); + session->SelectPreferredAddress(&preferred_address); + return NGTCP2_SUCCESS; + } + + static int on_stream_close(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + if (flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) { + session->application().StreamClose( + Stream::From(stream_user_data), + QuicError::ForApplication(app_error_code)); + } else { + session->application().StreamClose(Stream::From(stream_user_data)); } + return NGTCP2_SUCCESS; } - return 0; -} + static int on_stream_reset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->application().StreamReset( + Stream::From(stream_user_data), + final_size, + QuicError::ForApplication(app_error_code)); + return NGTCP2_SUCCESS; + } -void Session::UpdatePath(const PathStorage& storage) { - remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen); - local_address_.Update(storage.path.local.addr, storage.path.local.addrlen); - Debug(this, - "path updated. local %s, remote %s", - local_address_, - remote_address_); -} + static int on_stream_stop_sending(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->application().StreamStopSending( + Stream::From(stream_user_data), + QuicError::ForApplication(app_error_code)); + return NGTCP2_SUCCESS; + } -BaseObjectWeakPtr Session::FindStream(int64_t id) const { - auto it = streams_.find(id); - if (it == std::end(streams_)) return {}; - return it->second; -} + static void on_rand(uint8_t* dest, + size_t destlen, + const ngtcp2_rand_ctx* rand_ctx) { + CHECK(ncrypto::CSPRNG(dest, destlen)); + } -BaseObjectWeakPtr Session::CreateStream(int64_t id, - CreateStreamOption option) { - if (!can_create_streams()) return {}; - if (auto stream = Stream::Create(this, id)) { - AddStream(stream, option); - return stream; + static int on_early_data_rejected(ngtcp2_conn* conn, void* user_data) { + // TODO(@jasnell): Called when early data was rejected by server during the + // TLS handshake or client decided not to attempt early data. + return NGTCP2_SUCCESS; } - return {}; + + static constexpr ngtcp2_callbacks CLIENT = { + ngtcp2_crypto_client_initial_cb, + nullptr, + ngtcp2_crypto_recv_crypto_data_cb, + on_handshake_completed, + on_receive_version_negotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + on_receive_stream_data, + on_acknowledge_stream_data_offset, + nullptr, + on_stream_close, + on_receive_stateless_reset, + ngtcp2_crypto_recv_retry_cb, + on_extend_max_streams_bidi, + on_extend_max_streams_uni, + on_rand, + on_get_new_cid, + on_remove_connection_id, + ngtcp2_crypto_update_key_cb, + on_path_validation, + on_select_preferred_address, + on_stream_reset, + on_extend_max_remote_streams_bidi, + on_extend_max_remote_streams_uni, + on_extend_max_stream_data, + on_cid_status, + on_handshake_confirmed, + on_receive_new_token, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + on_receive_datagram, + on_acknowledge_datagram, + on_lost_datagram, + ngtcp2_crypto_get_path_challenge_data_cb, + on_stream_stop_sending, + ngtcp2_crypto_version_negotiation_cb, + on_receive_rx_key, + nullptr, + on_early_data_rejected}; + + static constexpr ngtcp2_callbacks SERVER = { + nullptr, + ngtcp2_crypto_recv_client_initial_cb, + ngtcp2_crypto_recv_crypto_data_cb, + on_handshake_completed, + nullptr, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + on_receive_stream_data, + on_acknowledge_stream_data_offset, + nullptr, + on_stream_close, + on_receive_stateless_reset, + nullptr, + on_extend_max_streams_bidi, + on_extend_max_streams_uni, + on_rand, + on_get_new_cid, + on_remove_connection_id, + ngtcp2_crypto_update_key_cb, + on_path_validation, + nullptr, + on_stream_reset, + on_extend_max_remote_streams_bidi, + on_extend_max_remote_streams_uni, + on_extend_max_stream_data, + on_cid_status, + nullptr, + nullptr, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + on_receive_datagram, + on_acknowledge_datagram, + on_lost_datagram, + ngtcp2_crypto_get_path_challenge_data_cb, + on_stream_stop_sending, + ngtcp2_crypto_version_negotiation_cb, + nullptr, + on_receive_tx_key, + on_early_data_rejected}; +}; + +#undef NGTCP2_CALLBACK_SCOPE + +// ============================================================================ +Session::SendPendingDataScope::SendPendingDataScope(Session* session) + : session(session) { + CHECK_NOT_NULL(session); + CHECK(!session->is_destroyed()); + ++session->impl_->send_scope_depth_; } -MaybeLocal Session::OpenStream(Direction direction) { - // If can_create_streams() returns false, we are not able to open a stream - // at all now, even in a pending state. The implication is that that session - // is destroyed or closing. - if (!can_create_streams()) return MaybeLocal(); +Session::SendPendingDataScope::SendPendingDataScope( + const BaseObjectPtr& session) + : SendPendingDataScope(session.get()) {} - // If can_open_streams() returns false, we are able to create streams but - // they will remain in a pending state. The implication is that the session - // TLS handshake is still progressing. Note that when a pending stream is - // created, it will not be listed in the streams list. - if (!can_open_streams()) { - if (auto stream = Stream::Create(this, direction)) [[likely]] { - return stream->object(); - } - return MaybeLocal(); +Session::SendPendingDataScope::~SendPendingDataScope() { + if (session->is_destroyed()) return; + DCHECK_GE(session->impl_->send_scope_depth_, 1); + if (--session->impl_->send_scope_depth_ == 0) { + session->application().SendPendingData(); } +} - int64_t id = -1; - auto open = [&] { - switch (direction) { - case Direction::BIDIRECTIONAL: { - Debug(this, "Opening bidirectional stream"); - return ngtcp2_conn_open_bidi_stream(*this, &id, nullptr); - } - case Direction::UNIDIRECTIONAL: { - Debug(this, "Opening uni-directional stream"); - return ngtcp2_conn_open_uni_stream(*this, &id, nullptr); - } - } - }; +// ============================================================================ +bool Session::HasInstance(Environment* env, Local value) { + return GetConstructorTemplate(env)->HasInstance(value); +} - switch (open()) { - case 0: { - // Woo! Our stream was created. - CHECK_GE(id, 0); - if (auto stream = CreateStream(id, CreateStreamOption::DO_NOT_NOTIFY)) { - return stream->object(); - } - return MaybeLocal(); - } - case NGTCP2_ERR_STREAM_ID_BLOCKED: { - // The stream cannot yet be opened. - // This is typically caused by the application exceeding the allowed max - // number of concurrent streams. We will allow the stream to be created - // in a pending state. - if (auto stream = Stream::Create(this, direction)) { - return stream->object(); - } - return MaybeLocal(); - } - default: { - // The stream could not be opened. Return nothing to signal error. - return MaybeLocal(); - } +BaseObjectPtr Session::Create( + Endpoint* endpoint, + const Config& config, + TLSContext* tls_context, + const std::optional& ticket) { + Local obj; + if (!GetConstructorTemplate(endpoint->env()) + ->InstanceTemplate() + ->NewInstance(endpoint->env()->context()) + .ToLocal(&obj)) { + return {}; } - UNREACHABLE(); -} -void Session::AddStream(const BaseObjectPtr& stream, - CreateStreamOption option) { - CHECK(stream); + return MakeDetachedBaseObject( + endpoint, obj, config, tls_context, ticket); +} - auto id = stream->id(); - auto direction = stream->direction(); +Session::Session(Endpoint* endpoint, + Local object, + const Config& config, + TLSContext* tls_context, + const std::optional& session_ticket) + : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION), + side_(config.side), + allocator_(BindingData::Get(env())), + impl_(std::make_unique(this, endpoint, config)), + connection_(InitConnection()), + tls_session_(tls_context->NewSession(this, session_ticket)) { + DCHECK(impl_); + MakeWeak(); + Debug(this, "Session created."); - // Let's double check that a stream with the given id does not already - // exist. If it does, that means we've got a bug somewhere. - CHECK_EQ(streams_.find(id), streams_.end()); + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env()->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; - Debug(this, "Adding stream %" PRIi64 " to session", id); + defineProperty(env()->state_string(), impl_->state_.GetArrayBuffer()); + defineProperty(env()->stats_string(), impl_->stats_.GetArrayBuffer()); - // The streams_ map becomes the sole owner of the Stream instance. - // We mark the stream detached so that when it is removed from - // the session, or is the session is destroyed, the stream will - // also be destroyed. - auto& inserted = streams_[id] = std::move(stream); - inserted->Detach(); + auto& binding = BindingData::Get(env()); - ngtcp2_conn_set_stream_user_data(*this, id, inserted.get()); + if (config.options.qlog) [[unlikely]] { + qlog_stream_ = LogStream::Create(env()); + defineProperty(binding.qlog_string(), qlog_stream_->object()); + } - if (option == CreateStreamOption::NOTIFY) { - EmitStream(inserted); + if (config.options.tls_options.keylog) [[unlikely]] { + keylog_stream_ = LogStream::Create(env()); + defineProperty(binding.keylog_string(), keylog_stream_->object()); } - // Update tracking statistics for the number of streams associated with this - // session. - if (ngtcp2_conn_is_local_stream(*this, id)) { - switch (direction) { - case Direction::BIDIRECTIONAL: { - STAT_INCREMENT(Stats, bidi_out_stream_count); - break; - } - case Direction::UNIDIRECTIONAL: { - STAT_INCREMENT(Stats, uni_out_stream_count); - break; - } + UpdateDataStats(); +} + +Session::~Session() { + // Double check that Destroy() was called first. + CHECK(is_destroyed()); +} + +Session::QuicConnectionPointer Session::InitConnection() { + ngtcp2_conn* conn; + Path path(config().local_address, config().remote_address); + TransportParams::Config tp_config(side_, config().ocid, config().retry_scid); + TransportParams transport_params(tp_config, + config().options.transport_params); + transport_params.GenerateSessionTokens(this); + + switch (side_) { + case Side::SERVER: { + // On the server side there are certain transport parameters that are + // required to be sent. Let's make sure they are set. + const ngtcp2_transport_params& params = transport_params; + CHECK_EQ(params.original_dcid_present, 1); + CHECK_EQ(ngtcp2_conn_server_new(&conn, + config().dcid, + config().scid, + path, + config().version, + &Impl::SERVER, + &config().settings, + transport_params, + &allocator_, + this), + 0); + break; } - } else { - switch (direction) { - case Direction::BIDIRECTIONAL: { - STAT_INCREMENT(Stats, bidi_in_stream_count); - break; - } - case Direction::UNIDIRECTIONAL: { - STAT_INCREMENT(Stats, uni_in_stream_count); - break; - } + case Side::CLIENT: { + CHECK_EQ(ngtcp2_conn_client_new(&conn, + config().dcid, + config().scid, + path, + config().version, + &Impl::CLIENT, + &config().settings, + transport_params, + &allocator_, + this), + 0); + break; } } + return QuicConnectionPointer(conn); } -void Session::RemoveStream(int64_t id) { - // ngtcp2 does not extend the max streams count automatically except in very - // specific conditions, none of which apply once we've gotten this far. We - // need to manually extend when a remote peer initiated stream is removed. - Debug(this, "Removing stream %" PRIi64 " from session", id); - if (!is_in_draining_period() && !is_in_closing_period() && - !state_->silent_close && - !ngtcp2_conn_is_local_stream(connection_.get(), id)) { - if (ngtcp2_is_bidi_stream(id)) - ngtcp2_conn_extend_max_streams_bidi(connection_.get(), 1); - else - ngtcp2_conn_extend_max_streams_uni(connection_.get(), 1); - } - - // Frees the persistent reference to the Stream object, allowing it to be gc'd - // any time after the JS side releases it's own reference. - streams_.erase(id); - ngtcp2_conn_set_stream_user_data(*this, id, nullptr); +Session::operator ngtcp2_conn*() const { + return connection_.get(); } -void Session::ResumeStream(int64_t id) { - Debug(this, "Resuming stream %" PRIi64, id); - SendPendingDataScope send_scope(this); - application_->ResumeStream(id); +bool Session::is_server() const { + return side_ == Side::SERVER; } -void Session::ShutdownStream(int64_t id, QuicError error) { - Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); - SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream(*this, - 0, - id, - error.type() == QuicError::Type::APPLICATION - ? error.code() - : NGTCP2_APP_NOERROR); +bool Session::is_destroyed() const { + return !impl_; } -void Session::StreamDataBlocked(int64_t id) { - Debug(this, "Stream %" PRIi64 " is blocked", id); - STAT_INCREMENT(Stats, block_count); - application_->BlockStream(id); +bool Session::is_destroyed_or_closing() const { + return !impl_ || impl_->state_->closing; } -void Session::ShutdownStreamWrite(int64_t id, QuicError code) { - Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); - SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream_write(*this, - 0, - id, - code.type() == QuicError::Type::APPLICATION - ? code.code() - : NGTCP2_APP_NOERROR); -} +void Session::Close(Session::CloseMethod method) { + if (is_destroyed()) return; + auto& stats_ = impl_->stats_; + + if (impl_->last_error_) { + Debug(this, "Closing with error: %s", impl_->last_error_); + } + + STAT_RECORD_TIMESTAMP(Stats, closing_at); + + // With both the DEFAULT and SILENT options, we will proceed to closing + // the session immediately. All open streams will be immediately destroyed + // with whatever is set as the last error. The session will then be destroyed + // with a possible roundtrip to JavaScript to emit a close event and clean up + // any JavaScript side state. Importantly, these operations are all done + // synchronously, so the session will be destroyed once FinishClose returns. + // + // With the graceful option, we will wait for all streams to close on their + // own before proceeding with the FinishClose operation. New streams will + // be rejected, however. + + switch (method) { + case CloseMethod::DEFAULT: { + Debug(this, "Immediately closing session"); + impl_->state_->silent_close = 0; + return FinishClose(); + } + case CloseMethod::SILENT: { + Debug(this, "Immediately closing session silently"); + impl_->state_->silent_close = 1; + return FinishClose(); + } + case CloseMethod::GRACEFUL: { + // If there are no open streams, then we can close just immediately and + // not worry about waiting around. + if (impl_->streams_.empty()) { + impl_->state_->silent_close = 0; + impl_->state_->graceful_close = 0; + return FinishClose(); + } -void Session::CollectSessionTicketAppData( - SessionTicket::AppData* app_data) const { - application_->CollectSessionTicketAppData(app_data); + // If we are already closing gracefully, do nothing. + if (impl_->state_->graceful_close) [[unlikely]] { + return; + } + impl_->state_->graceful_close = 1; + Debug(this, + "Gracefully closing session (waiting on %zu streams)", + impl_->streams_.size()); + return; + } + } + UNREACHABLE(); } -SessionTicket::AppData::Status Session::ExtractSessionTicketAppData( - const SessionTicket::AppData& app_data, - SessionTicket::AppData::Source::Flag flag) { - return application_->ExtractSessionTicketAppData(app_data, flag); +void Session::FinishClose() { + // FinishClose() should be called only after, and as a result of, Close() + // being called first. + DCHECK(impl_); + DCHECK(impl_->state_->closing); + + // If impl_->Close() returns true, then the session can be destroyed + // immediately without round-tripping through JavaScript. + if (impl_->Close()) { + return Destroy(); + } + + // Otherwise, we emit a close callback so that the JavaScript side can + // clean up anything it needs to clean up before destroying. + EmitClose(); } -void Session::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("config", config_); - tracker->TrackField("endpoint", endpoint_); - tracker->TrackField("streams", streams_); - tracker->TrackField("local_address", local_address_); - tracker->TrackField("remote_address", remote_address_); - tracker->TrackField("application", application_); - tracker->TrackField("tls_session", tls_session_); - tracker->TrackField("timer", timer_); - tracker->TrackField("conn_closebuf", conn_closebuf_); - tracker->TrackField("qlog_stream", qlog_stream_); - tracker->TrackField("keylog_stream", keylog_stream_); +void Session::Destroy() { + // Destroy() should be called only after, and as a result of, Close() + // being called first. + DCHECK(impl_); + DCHECK(impl_->state_->closing); + Debug(this, "Session destroyed"); + impl_.reset(); + if (qlog_stream_ || keylog_stream_) { + env()->SetImmediate( + [qlog = qlog_stream_, keylog = keylog_stream_](Environment*) { + if (qlog) qlog->End(); + if (keylog) keylog->End(); + }); + } + qlog_stream_.reset(); + keylog_stream_.reset(); } -bool Session::is_in_closing_period() const { - return ngtcp2_conn_in_closing_period(*this) != 0; +PendingStream::PendingStreamQueue& Session::pending_bidi_stream_queue() const { + CHECK(!is_destroyed()); + return impl_->pending_bidi_stream_queue_; } -bool Session::is_in_draining_period() const { - return ngtcp2_conn_in_draining_period(*this) != 0; +PendingStream::PendingStreamQueue& Session::pending_uni_stream_queue() const { + CHECK(!is_destroyed()); + return impl_->pending_uni_stream_queue_; } -bool Session::wants_session_ticket() const { - return state_->session_ticket == 1; +size_t Session::max_packet_size() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_max_tx_udp_payload_size(*this); } -void Session::SetStreamOpenAllowed() { - state_->stream_open_allowed = 1; +uint32_t Session::version() const { + CHECK(!is_destroyed()); + return impl_->config_.version; } -bool Session::can_send_packets() const { - // We can send packets if we're not in the middle of a ngtcp2 callback, - // we're not destroyed, we're not in a draining or closing period, and - // endpoint is set. - return !NgTcp2CallbackScope::in_ngtcp2_callback(env()) && !is_destroyed() && - !is_in_draining_period() && !is_in_closing_period() && endpoint_; +Endpoint& Session::endpoint() const { + CHECK(!is_destroyed()); + return *impl_->endpoint_; } -bool Session::can_create_streams() const { - return !state_->destroyed && !state_->graceful_close && !state_->closing && - !is_in_closing_period() && !is_in_draining_period(); +TLSSession& Session::tls_session() const { + CHECK(!is_destroyed()); + return *tls_session_; } -bool Session::can_open_streams() const { - return state_->stream_open_allowed; +Session::Application& Session::application() const { + CHECK(!is_destroyed()); + return *impl_->application_; } -uint64_t Session::max_data_left() const { - return ngtcp2_conn_get_max_data_left(*this); +const SocketAddress& Session::remote_address() const { + CHECK(!is_destroyed()); + return impl_->remote_address_; } -uint64_t Session::max_local_streams_uni() const { - return ngtcp2_conn_get_streams_uni_left(*this); +const SocketAddress& Session::local_address() const { + CHECK(!is_destroyed()); + return impl_->local_address_; } -uint64_t Session::max_local_streams_bidi() const { - return ngtcp2_conn_get_local_transport_params(*this) - ->initial_max_streams_bidi; +std::string Session::diagnostic_name() const { + const auto get_type = [&] { return is_server() ? "server" : "client"; }; + + return std::string("Session (") + get_type() + "," + + std::to_string(env()->thread_id()) + ":" + + std::to_string(static_cast(get_async_id())) + ")"; } -void Session::set_wrapped() { - state_->wrapped = 1; +const Session::Config& Session::config() const { + CHECK(!is_destroyed()); + return impl_->config_; } -void Session::set_priority_supported(bool on) { - state_->priority_supported = on ? 1 : 0; +Session::Config& Session::config() { + CHECK(!is_destroyed()); + return impl_->config_; } -void Session::DoClose(bool silent) { - DCHECK(!is_destroyed()); - Debug(this, "Session is closing. Silently %s", silent ? "yes" : "no"); - // Once Close has been called, we cannot re-enter - if (state_->closing == 1) return; - state_->closing = 1; - state_->silent_close = silent ? 1 : 0; - STAT_RECORD_TIMESTAMP(Stats, closing_at); +const Session::Options& Session::options() const { + CHECK(!is_destroyed()); + return impl_->config_.options; +} - // Iterate through all of the known streams and close them. The streams - // will remove themselves from the Session as soon as they are closed. - // Note: we create a copy because the streams will remove themselves - // while they are cleaning up which will invalidate the iterator. - auto streams = streams_; - for (auto& stream : streams) stream.second->Destroy(last_error_); - DCHECK(streams.empty()); - - // If the state has not been passed out to JavaScript yet, we can skip closing - // entirely and drop directly out to Destroy. - if (!state_->wrapped) return Destroy(); - - // If we're not running within a ngtcp2 callback scope, schedule a - // CONNECTION_CLOSE to be sent. If we are within a ngtcp2 callback scope, - // sending the CONNECTION_CLOSE will be deferred. - { MaybeCloseConnectionScope close_scope(this, silent); } - - // We emit a close callback so that the JavaScript side can clean up anything - // it needs to clean up before destroying. It's the JavaScript side's - // responsibility to call destroy() when ready. - EmitClose(); +void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { + DCHECK(qlog_stream_); + // Fun fact... ngtcp2 does not emit the final qlog statement until the + // ngtcp2_conn object is destroyed. + std::vector buffer(len); + memcpy(buffer.data(), data, len); + Debug(this, "Emitting qlog data to the qlog stream"); + env()->SetImmediate([ptr = qlog_stream_, buffer = std::move(buffer), flags]( + Environment*) { + ptr->Emit(buffer.data(), + buffer.size(), + flags & NGTCP2_QLOG_WRITE_FLAG_FIN ? LogStream::EmitOption::FIN + : LogStream::EmitOption::NONE); + }); } -void Session::ExtendStreamOffset(int64_t id, size_t amount) { - Debug(this, "Extending stream %" PRIi64 " offset by %zu", id, amount); - ngtcp2_conn_extend_max_stream_offset(*this, id, amount); +const TransportParams Session::local_transport_params() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_local_transport_params(*this); } -void Session::ExtendOffset(size_t amount) { - Debug(this, "Extending offset by %zu", amount); - ngtcp2_conn_extend_max_offset(*this, amount); +const TransportParams Session::remote_transport_params() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_remote_transport_params(*this); } -void Session::UpdateDataStats() { - if (state_->destroyed) return; - Debug(this, "Updating data stats"); - ngtcp2_conn_info info; - ngtcp2_conn_get_conn_info(*this, &info); - STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight); - STAT_SET(Stats, cwnd, info.cwnd); - STAT_SET(Stats, latest_rtt, info.latest_rtt); - STAT_SET(Stats, min_rtt, info.min_rtt); - STAT_SET(Stats, rttvar, info.rttvar); - STAT_SET(Stats, smoothed_rtt, info.smoothed_rtt); - STAT_SET(Stats, ssthresh, info.ssthresh); - STAT_SET( - Stats, - max_bytes_in_flight, - std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); +void Session::SetLastError(QuicError&& error) { + CHECK(!is_destroyed()); + Debug(this, "Setting last error to %s", error); + impl_->last_error_ = std::move(error); } -void Session::SendConnectionClose() { - DCHECK(!NgTcp2CallbackScope::in_ngtcp2_callback(env())); - if (is_destroyed() || is_in_draining_period() || state_->silent_close) return; +bool Session::Receive(Store&& store, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + CHECK(!is_destroyed()); + impl_->remote_address_ = remote_address; - Debug(this, "Sending connection close"); - auto on_exit = OnScopeLeave([this] { UpdateTimer(); }); + // When we are done processing thins packet, we arrange to send any + // pending data for this session. + SendPendingDataScope send_scope(this); - switch (config_.side) { - case Side::SERVER: { - if (!is_in_closing_period() && !StartClosingPeriod()) { + ngtcp2_vec vec = store; + Path path(local_address, remote_address); + + Debug(this, + "Session is receiving %zu-byte packet received along path %s", + vec.len, + path); + + // It is important to understand that reading the packet will cause + // callback functions to be invoked, any one of which could lead to + // the Session being closed/destroyed synchronously. After calling + // ngtcp2_conn_read_pkt here, we will need to double check that the + // session is not destroyed before we try doing anything with it + // (like updating stats, sending pending data, etc). + int err = ngtcp2_conn_read_pkt( + *this, path, nullptr, vec.base, vec.len, uv_hrtime()); + + switch (err) { + case 0: { + Debug(this, "Session successfully received %zu-byte packet", vec.len); + if (!is_destroyed()) [[unlikely]] { + auto& stats_ = impl_->stats_; + STAT_INCREMENT_N(Stats, bytes_received, vec.len); + } + return true; + } + case NGTCP2_ERR_REQUIRED_TRANSPORT_PARAM: { + Debug(this, + "Receiving packet failed: " + "Remote peer failed to send a required transport parameter"); + if (!is_destroyed()) [[likely]] { + SetLastError(QuicError::ForTransport(err)); + Close(); + } + return false; + } + case NGTCP2_ERR_DRAINING: { + // Connection has entered the draining state, no further data should be + // sent. This happens when the remote peer has already sent a + // CONNECTION_CLOSE. + Debug(this, "Receiving packet failed: Session is draining"); + return false; + } + case NGTCP2_ERR_CLOSING: { + // Connection has entered the closing state, no further data should be + // sent. This happens when the local peer has called + // ngtcp2_conn_write_connection_close. + Debug(this, "Receiving packet failed: Session is closing"); + return false; + } + case NGTCP2_ERR_CRYPTO: { + Debug(this, "Receiving packet failed: Crypto error"); + // Crypto error happened! Set the last error to the tls alert + if (!is_destroyed()) [[likely]] { + SetLastError(QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(*this))); + Close(); + } + return false; + } + case NGTCP2_ERR_RETRY: { + // This should only ever happen on the server. We have to send a path + // validation challenge in the form of a RETRY packet to the peer and + // drop the connection. + DCHECK(is_server()); + Debug(this, "Receiving packet failed: Server must send a retry packet"); + if (!is_destroyed()) { + endpoint().SendRetry(PathDescriptor{ + version(), + config().dcid, + config().scid, + impl_->local_address_, + impl_->remote_address_, + }); Close(CloseMethod::SILENT); - } else { - DCHECK(conn_closebuf_); - Send(conn_closebuf_->Clone()); } - return; + return false; } - case Side::CLIENT: { - Path path(local_address_, remote_address_); - auto packet = Packet::Create(env(), - endpoint_.get(), - remote_address_, - kDefaultMaxPacketLength, - "immediate connection close (client)"); - ngtcp2_vec vec = *packet; - ssize_t nwrite = ngtcp2_conn_write_connection_close( - *this, &path, nullptr, vec.base, vec.len, last_error_, uv_hrtime()); - - if (nwrite < 0) [[unlikely]] { - packet->Done(UV_ECANCELED); - last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); + case NGTCP2_ERR_DROP_CONN: { + // There's nothing else to do but drop the connection state. + Debug(this, "Receiving packet failed: Session must drop the connection"); + if (!is_destroyed()) { Close(CloseMethod::SILENT); - } else { - packet->Truncate(nwrite); - Send(std::move(packet)); - packet.reset(); } - return; + return false; } } - UNREACHABLE(); + + // Shouldn't happen but just in case... handle other unknown errors + Debug(this, + "Receiving packet failed: " + "Unexpected error %d while receiving packet", + err); + if (!is_destroyed()) { + SetLastError(QuicError::ForNgtcp2Error(err)); + Close(); + } + return false; } -void Session::OnTimeout() { - HandleScope scope(env()->isolate()); - if (is_destroyed()) return; +void Session::Send(const BaseObjectPtr& packet) { + // Sending a Packet is generally best effort. If we're not in a state + // where we can send a packet, it's ok to drop it on the floor. The + // packet loss mechanisms will cause the packet data to be resent later + // if appropriate (and possible). - int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); - if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { - Debug(this, "Sending pending data after timr expiry"); - SendPendingDataScope send_scope(this); - return; + // That said, we should never be trying to send a packet when we're in + // a draining period. + CHECK(!is_destroyed()); + DCHECK(!is_in_draining_period()); + + if (!can_send_packets()) [[unlikely]] { + return packet->Done(UV_ECANCELED); } - Debug(this, "Session timed out"); - last_error_ = QuicError::ForNgtcp2Error(ret); - Close(CloseMethod::SILENT); + Debug(this, "Session is sending %s", packet->ToString()); + auto& stats_ = impl_->stats_; + STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); + endpoint().Send(packet); } -void Session::UpdateTimer() { - // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. - uint64_t expiry = ngtcp2_conn_get_expiry(*this); - uint64_t now = uv_hrtime(); - Debug( - this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); +void Session::Send(const BaseObjectPtr& packet, + const PathStorage& path) { + UpdatePath(path); + Send(packet); +} - if (expiry <= now) { - // The timer has already expired. - return OnTimeout(); +uint64_t Session::SendDatagram(Store&& data) { + CHECK(!is_destroyed()); + if (!can_send_packets()) { + Debug(this, "Unable to send datagram"); + return 0; } - auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; - Debug(this, "Updating timeout to %zu milliseconds", timeout); + const ngtcp2_transport_params* tp = remote_transport_params(); + uint64_t max_datagram_size = tp->max_datagram_frame_size; - // If timeout is zero here, it means our timer is less than a millisecond - // off from expiry. Let's bump the timer to 1. - timer_.Update(timeout == 0 ? 1 : timeout); -} + if (max_datagram_size == 0) { + Debug(this, "Datagrams are disabled"); + return 0; + } + + if (data.length() > max_datagram_size) { + Debug(this, "Ignoring oversized datagram"); + return 0; + } + + if (data.length() == 0) { + Debug(this, "Ignoring empty datagram"); + return 0; + } -bool Session::StartClosingPeriod() { - if (is_in_closing_period()) return true; - if (is_destroyed()) return false; + BaseObjectPtr packet; + uint8_t* pos = nullptr; + int accepted = 0; + ngtcp2_vec vec = data; + PathStorage path; + int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; + uint64_t did = impl_->state_->last_datagram_id + 1; - Debug(this, "Session is entering closing period"); + Debug(this, "Sending %zu-byte datagram %" PRIu64, data.length(), did); - conn_closebuf_ = Packet::CreateConnectionClosePacket( - env(), endpoint_.get(), remote_address_, *this, last_error_); + // Let's give it a max number of attempts to send the datagram. + static const int kMaxAttempts = 16; + int attempts = 0; - // If we were unable to create a connection close packet, we're in trouble. - // Set the internal error and return false so that the session will be - // silently closed. - if (!conn_closebuf_) { - last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); - return false; - } + auto on_exit = OnScopeLeave([&] { + UpdatePacketTxTime(); + UpdateTimer(); + UpdateDataStats(); + }); - return true; -} + for (;;) { + // We may have to make several attempts at encoding and sending the + // datagram packet. On each iteration here we'll try to encode the + // datagram. It's entirely up to ngtcp2 whether to include the datagram + // in the packet on each call to ngtcp2_conn_writev_datagram. + if (!packet) { + packet = Packet::Create(env(), + endpoint(), + impl_->remote_address_, + ngtcp2_conn_get_max_tx_udp_payload_size(*this), + "datagram"); + // Typically sending datagrams is best effort, but if we cannot create + // the packet, then we handle it as a fatal error. + if (!packet) { + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); + Close(CloseMethod::SILENT); + return 0; + } + pos = ngtcp2_vec(*packet).base; + } -void Session::DatagramStatus(uint64_t datagramId, quic::DatagramStatus status) { - switch (status) { - case quic::DatagramStatus::ACKNOWLEDGED: { - Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId); - STAT_INCREMENT(Stats, datagrams_acknowledged); - break; + ssize_t nwrite = ngtcp2_conn_writev_datagram(*this, + &path.path, + nullptr, + pos, + packet->length(), + &accepted, + flags, + did, + &vec, + 1, + uv_hrtime()); + + if (nwrite <= 0) { + // Nothing was written to the packet. + switch (nwrite) { + case 0: { + // We cannot send data because of congestion control or the data will + // not fit. Since datagrams are best effort, we are going to abandon + // the attempt and just return. + CHECK_EQ(accepted, 0); + packet->Done(UV_ECANCELED); + return 0; + } + case NGTCP2_ERR_WRITE_MORE: { + // We keep on looping! Keep on sending! + continue; + } + case NGTCP2_ERR_INVALID_STATE: { + // The remote endpoint does not want to accept datagrams. That's ok, + // just return 0. + packet->Done(UV_ECANCELED); + return 0; + } + case NGTCP2_ERR_INVALID_ARGUMENT: { + // The datagram is too large. That should have been caught above but + // that's ok. We'll just abandon the attempt and return. + packet->Done(UV_ECANCELED); + return 0; + } + case NGTCP2_ERR_PKT_NUM_EXHAUSTED: { + // We've exhausted the packet number space. Sadly we have to treat it + // as a fatal condition (which we will do after the switch) + break; + } + case NGTCP2_ERR_CALLBACK_FAILURE: { + // There was an internal failure. Sadly we have to treat it as a fatal + // condition. (which we will do after the switch) + break; + } + } + packet->Done(UV_ECANCELED); + SetLastError(QuicError::ForTransport(nwrite)); + Close(CloseMethod::SILENT); + return 0; + } + + // In this case, a complete packet was written and we need to send it along. + // Note that this doesn't mean that the packet actually contains the + // datagram! We'll check that next by checking the accepted value. + packet->Truncate(nwrite); + Send(packet); + packet.reset(); + + if (accepted) { + // Yay! The datagram was accepted into the packet we just sent and we can + // return the datagram ID. + Debug(this, "Datagram %" PRIu64 " sent", did); + auto& stats_ = impl_->stats_; + STAT_INCREMENT(Stats, datagrams_sent); + STAT_INCREMENT_N(Stats, bytes_sent, vec.len); + impl_->state_->last_datagram_id = did; + return did; } - case quic::DatagramStatus::LOST: { - Debug(this, "Datagram %" PRIu64 " was lost", datagramId); - STAT_INCREMENT(Stats, datagrams_lost); + + // We sent a packet, but it wasn't the datagram packet. That can happen. + // Let's loop around and try again. + if (++attempts == kMaxAttempts) [[unlikely]] { + Debug(this, "Too many attempts to send datagram. Canceling."); + // Too many attempts to send the datagram. break; } + + // If we get here that means the datagram has not yet been sent. + // We're going to loop around to try again. } - EmitDatagramStatus(datagramId, status); -} -void Session::DatagramReceived(const uint8_t* data, - size_t datalen, - DatagramReceivedFlags flag) { - // If there is nothing watching for the datagram on the JavaScript side, - // we just drop it on the floor. - if (state_->datagram == 0 || datalen == 0) return; + return 0; +} - auto backing = ArrayBuffer::NewBackingStore(env()->isolate(), datalen); - Debug(this, "Session is receiving datagram of size %zu", datalen); - memcpy(backing->Data(), data, datalen); - STAT_INCREMENT(Stats, datagrams_received); - STAT_INCREMENT_N(Stats, bytes_received, datalen); - EmitDatagram(Store(std::move(backing), datalen), flag); +void Session::UpdatePacketTxTime() { + CHECK(!is_destroyed()); + ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); } -bool Session::GenerateNewConnectionId(ngtcp2_cid* cid, - size_t len, - uint8_t* token) { - CID cid_ = config_.options.cid_factory->GenerateInto(cid, len); - Debug(this, "Generated new connection id %s", cid_); - StatelessResetToken new_token( - token, endpoint_->options().reset_token_secret, cid_); - endpoint_->AssociateCID(cid_, config_.scid); - endpoint_->AssociateStatelessResetToken(new_token, this); - return true; +void Session::UpdatePath(const PathStorage& storage) { + CHECK(!is_destroyed()); + impl_->remote_address_.Update(storage.path.remote.addr, + storage.path.remote.addrlen); + impl_->local_address_.Update(storage.path.local.addr, + storage.path.local.addrlen); + Debug(this, + "path updated. local %s, remote %s", + impl_->local_address_, + impl_->remote_address_); } -bool Session::HandshakeCompleted() { - Debug(this, "Session handshake completed"); +BaseObjectPtr Session::FindStream(int64_t id) const { + if (is_destroyed()) return {}; + auto it = impl_->streams_.find(id); + if (it == std::end(impl_->streams_)) return {}; + return it->second; +} - if (state_->handshake_completed) return false; - state_->handshake_completed = 1; - SetStreamOpenAllowed(); +BaseObjectPtr Session::CreateStream( + int64_t id, + CreateStreamOption option, + std::shared_ptr data_source) { + if (!can_create_streams()) [[unlikely]] + return {}; + if (auto stream = Stream::Create(this, id, std::move(data_source))) + [[likely]] { + AddStream(stream, option); + return stream; + } + return {}; +} - STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); +MaybeLocal Session::OpenStream(Direction direction, + std::shared_ptr data_source) { + // If can_create_streams() returns false, we are not able to open a stream + // at all now, even in a pending state. The implication is that that session + // is destroyed or closing. + if (!can_create_streams()) [[unlikely]] + return {}; - // TODO(@jasnel): Not yet supporting early data... - // if (!tls_session().early_data_was_accepted()) - // ngtcp2_conn_tls_early_data_rejected(*this); + // If can_open_streams() returns false, we are able to create streams but + // they will remain in a pending state. The implication is that the session + // TLS handshake is still progressing. Note that when a pending stream is + // created, it will not be listed in the streams list. + if (!can_open_streams()) { + if (auto stream = Stream::Create(this, direction, std::move(data_source))) + [[likely]] { + return stream->object(); + } + return {}; + } - // When in a server session, handshake completed == handshake confirmed. - if (is_server()) { - HandshakeConfirmed(); + int64_t id = -1; + auto open = [&] { + switch (direction) { + case Direction::BIDIRECTIONAL: { + Debug(this, "Opening bidirectional stream"); + return ngtcp2_conn_open_bidi_stream(*this, &id, nullptr); + } + case Direction::UNIDIRECTIONAL: { + Debug(this, "Opening uni-directional stream"); + return ngtcp2_conn_open_uni_stream(*this, &id, nullptr); + } + } + UNREACHABLE(); + }; - if (!endpoint().is_closed() && !endpoint().is_closing()) { - auto token = endpoint().GenerateNewToken(version(), remote_address_); - ngtcp2_vec vec = token; - if (NGTCP2_ERR(ngtcp2_conn_submit_new_token(*this, vec.base, vec.len))) { - // Submitting the new token failed... In this case we're going to - // fail because submitting the new token should only fail if we - // ran out of memory or some other unrecoverable state. - return false; + switch (open()) { + case 0: { + // Woo! Our stream was created. + CHECK_GE(id, 0); + if (auto stream = CreateStream( + id, CreateStreamOption::DO_NOT_NOTIFY, std::move(data_source))) + [[likely]] { + return stream->object(); + } + break; + } + case NGTCP2_ERR_STREAM_ID_BLOCKED: { + // The stream cannot yet be opened. + // This is typically caused by the application exceeding the allowed max + // number of concurrent streams. We will allow the stream to be created + // in a pending state. + if (auto stream = Stream::Create(this, direction, std::move(data_source))) + [[likely]] { + return stream->object(); } + break; } } + return {}; +} - EmitHandshakeComplete(); +void Session::AddStream(BaseObjectPtr stream, + CreateStreamOption option) { + CHECK(!is_destroyed()); + CHECK(stream); - return true; -} + auto id = stream->id(); + auto direction = stream->direction(); -void Session::HandshakeConfirmed() { - if (state_->handshake_confirmed) return; + // Let's double check that a stream with the given id does not already + // exist. If it does, that means we've got a bug somewhere. + DCHECK_EQ(impl_->streams_.find(id), impl_->streams_.end()); - Debug(this, "Session handshake confirmed"); + Debug(this, "Adding stream %" PRIi64 " to session", id); - state_->handshake_confirmed = true; - STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at); -} + // The streams_ map becomes the sole owner of the Stream instance. + // We mark the stream detached so that when it is removed from + // the session, or is the session is destroyed, the stream will + // also be destroyed. + impl_->streams_[id] = stream; + stream->Detach(); -void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { - if (config_.options.preferred_address_strategy == - PreferredAddress::Policy::IGNORE_PREFERRED) { - Debug(this, "Ignoring preferred address"); - return; - } + ngtcp2_conn_set_stream_user_data(*this, id, stream.get()); - auto local_address = endpoint_->local_address(); - int family = local_address.family(); + if (option == CreateStreamOption::NOTIFY) { + EmitStream(stream); + } - switch (family) { - case AF_INET: { - Debug(this, "Selecting preferred address for AF_INET"); - auto ipv4 = preferredAddress->ipv4(); - if (ipv4.has_value()) { - if (ipv4->address.empty() || ipv4->port == 0) return; - CHECK(SocketAddress::New(AF_INET, - std::string(ipv4->address).c_str(), - ipv4->port, - &remote_address_)); - preferredAddress->Use(ipv4.value()); + // Update tracking statistics for the number of streams associated with this + // session. + auto& stats_ = impl_->stats_; + if (ngtcp2_conn_is_local_stream(*this, id)) { + switch (direction) { + case Direction::BIDIRECTIONAL: { + STAT_INCREMENT(Stats, bidi_out_stream_count); + break; + } + case Direction::UNIDIRECTIONAL: { + STAT_INCREMENT(Stats, uni_out_stream_count); + break; } - break; } - case AF_INET6: { - Debug(this, "Selecting preferred address for AF_INET6"); - auto ipv6 = preferredAddress->ipv6(); - if (ipv6.has_value()) { - if (ipv6->address.empty() || ipv6->port == 0) return; - CHECK(SocketAddress::New(AF_INET, - std::string(ipv6->address).c_str(), - ipv6->port, - &remote_address_)); - preferredAddress->Use(ipv6.value()); + } else { + switch (direction) { + case Direction::BIDIRECTIONAL: { + STAT_INCREMENT(Stats, bidi_in_stream_count); + break; + } + case Direction::UNIDIRECTIONAL: { + STAT_INCREMENT(Stats, uni_in_stream_count); + break; } - break; } } } -CID Session::new_cid(size_t len) const { - return config_.options.cid_factory->Generate(len); -} - -void Session::ProcessPendingBidiStreams() { - // It shouldn't be possible to get here if can_create_streams() is false. - CHECK(can_create_streams()); +void Session::RemoveStream(int64_t id) { + CHECK(!is_destroyed()); + Debug(this, "Removing stream %" PRIi64 " from session", id); + if (!is_in_draining_period() && !is_in_closing_period() && + !ngtcp2_conn_is_local_stream(*this, id)) { + if (ngtcp2_is_bidi_stream(id)) { + ngtcp2_conn_extend_max_streams_bidi(*this, 1); + } else { + ngtcp2_conn_extend_max_streams_uni(*this, 1); + } + } - int64_t id; + ngtcp2_conn_set_stream_user_data(*this, id, nullptr); - while (!pending_bidi_stream_queue_.IsEmpty()) { - if (ngtcp2_conn_get_streams_bidi_left(*this) == 0) { - return; - } + // Note that removing the stream from the streams map likely releases + // the last BaseObjectPtr holding onto the Stream instance, at which + // point it will be freed. If there are other BaseObjectPtr instances + // or other references to the Stream, however, freeing will be deferred. + // In either case, we cannot assume that the stream still exists after + // this call. + impl_->streams_.erase(id); - switch (ngtcp2_conn_open_bidi_stream(*this, &id, nullptr)) { - case 0: { - pending_bidi_stream_queue_.PopFront()->fulfill(id); - continue; - } - case NGTCP2_ERR_STREAM_ID_BLOCKED: { - // This case really should not happen since we've checked the number - // of bidi streams left above. However, if it does happen we'll treat - // it the same as if the get_streams_bidi_left call returned zero. - return; - } - default: { - // We failed to open the stream for some reason other than being - // blocked. Report the failure. - pending_bidi_stream_queue_.PopFront()->reject( - QuicError::ForTransport(NGTCP2_STREAM_LIMIT_ERROR)); - continue; - } - } + // If we are gracefully closing and there are no more streams, + // then we can proceed to finishing the close now. Note that the + // expectation is that the session will be destroyed once FinishClose + // returns. + if (impl_->state_->closing && impl_->state_->graceful_close) { + FinishClose(); + CHECK(is_destroyed()); } } -void Session::ProcessPendingUniStreams() { - // It shouldn't be possible to get here if can_create_streams() is false. - CHECK(can_create_streams()); +void Session::ResumeStream(int64_t id) { + CHECK(!is_destroyed()); + SendPendingDataScope send_scope(this); + application().ResumeStream(id); +} - int64_t id; +void Session::ShutdownStream(int64_t id, QuicError error) { + CHECK(!is_destroyed()); + Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); + SendPendingDataScope send_scope(this); + ngtcp2_conn_shutdown_stream(*this, + 0, + id, + error.type() == QuicError::Type::APPLICATION + ? error.code() + : NGTCP2_APP_NOERROR); +} - while (!pending_uni_stream_queue_.IsEmpty()) { - if (ngtcp2_conn_get_streams_uni_left(*this) == 0) { - return; - } +void Session::ShutdownStreamWrite(int64_t id, QuicError code) { + CHECK(!is_destroyed()); + Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); + SendPendingDataScope send_scope(this); + ngtcp2_conn_shutdown_stream_write(*this, + 0, + id, + code.type() == QuicError::Type::APPLICATION + ? code.code() + : NGTCP2_APP_NOERROR); +} - switch (ngtcp2_conn_open_uni_stream(*this, &id, nullptr)) { - case 0: { - pending_uni_stream_queue_.PopFront()->fulfill(id); - continue; - } - case NGTCP2_ERR_STREAM_ID_BLOCKED: { - // This case really should not happen since we've checked the number - // of bidi streams left above. However, if it does happen we'll treat - // it the same as if the get_streams_bidi_left call returned zero. - return; - } - default: { - // We failed to open the stream for some reason other than being - // blocked. Report the failure. - pending_uni_stream_queue_.PopFront()->reject( - QuicError::ForTransport(NGTCP2_STREAM_LIMIT_ERROR)); - continue; - } - } +void Session::StreamDataBlocked(int64_t id) { + CHECK(!is_destroyed()); + auto& stats_ = impl_->stats_; + STAT_INCREMENT(Stats, block_count); + application().BlockStream(id); +} + +void Session::CollectSessionTicketAppData( + SessionTicket::AppData* app_data) const { + CHECK(!is_destroyed()); + application().CollectSessionTicketAppData(app_data); +} + +SessionTicket::AppData::Status Session::ExtractSessionTicketAppData( + const SessionTicket::AppData& app_data, + SessionTicket::AppData::Source::Flag flag) { + CHECK(!is_destroyed()); + return application().ExtractSessionTicketAppData(app_data, flag); +} + +void Session::MemoryInfo(MemoryTracker* tracker) const { + if (impl_) { + tracker->TrackField("impl", impl_); + } + tracker->TrackField("tls_session", tls_session_); + if (qlog_stream_) { + tracker->TrackField("qlog_stream", qlog_stream_); + } + if (keylog_stream_) { + tracker->TrackField("keylog_stream", keylog_stream_); } } -// JavaScript callouts +bool Session::is_in_closing_period() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_in_closing_period(*this) != 0; +} -void Session::EmitClose(const QuicError& error) { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return Destroy(); +bool Session::is_in_draining_period() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_in_draining_period(*this) != 0; +} - CallbackScope cb_scope(this); - Local argv[] = { - Integer::New(env()->isolate(), static_cast(error.type())), - BigInt::NewFromUnsigned(env()->isolate(), error.code()), - Undefined(env()->isolate()), - }; - if (error.reason().length() > 0 && - !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { - return; - } - Debug(this, "Notifying JavaScript of session close"); - MakeCallback( - BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); +bool Session::wants_session_ticket() const { + return !is_destroyed() && impl_->state_->session_ticket == 1; } -void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return; +void Session::SetStreamOpenAllowed() { + CHECK(!is_destroyed()); + impl_->state_->stream_open_allowed = 1; +} - CallbackScope cbv_scope(this); +bool Session::can_send_packets() const { + // We can send packets if we're not in the middle of a ngtcp2 callback, + // we're not destroyed, we're not in a draining or closing period, and + // endpoint is set. + return !is_destroyed() && !NgTcp2CallbackScope::in_ngtcp2_callback(env()) && + !is_in_draining_period() && !is_in_closing_period(); +} - Local argv[] = {datagram.ToUint8Array(env()), - v8::Boolean::New(env()->isolate(), flag.early)}; +bool Session::can_create_streams() const { + return !is_destroyed_or_closing() && !is_in_closing_period() && + !is_in_draining_period(); +} - Debug(this, "Notifying JavaScript of datagram"); - MakeCallback(BindingData::Get(env()).session_datagram_callback(), - arraysize(argv), - argv); +bool Session::can_open_streams() const { + return !is_destroyed() && impl_->state_->stream_open_allowed; } -void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return; +uint64_t Session::max_data_left() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_max_data_left(*this); +} - CallbackScope cb_scope(this); - auto& state = BindingData::Get(env()); +uint64_t Session::max_local_streams_uni() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_streams_uni_left(*this); +} - const auto status_to_string = ([&] { - switch (status) { - case quic::DatagramStatus::ACKNOWLEDGED: - return state.acknowledged_string(); - case quic::DatagramStatus::LOST: - return state.lost_string(); - } - UNREACHABLE(); - })(); +uint64_t Session::max_local_streams_bidi() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_local_transport_params(*this) + ->initial_max_streams_bidi; +} - Local argv[] = {BigInt::NewFromUnsigned(env()->isolate(), id), - status_to_string}; - Debug(this, "Notifying JavaScript of datagram status"); - MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv); +void Session::set_wrapped() { + CHECK(!is_destroyed()); + impl_->state_->wrapped = 1; } -void Session::EmitHandshakeComplete() { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return; +void Session::set_priority_supported(bool on) { + CHECK(!is_destroyed()); + impl_->state_->priority_supported = on ? 1 : 0; +} - CallbackScope cb_scope(this); +void Session::ExtendStreamOffset(int64_t id, size_t amount) { + CHECK(!is_destroyed()); + Debug(this, "Extending stream %" PRIi64 " offset by %zu bytes", id, amount); + ngtcp2_conn_extend_max_stream_offset(*this, id, amount); +} - auto isolate = env()->isolate(); +void Session::ExtendOffset(size_t amount) { + CHECK(!is_destroyed()); + Debug(this, "Extending offset by %zu bytes", amount); + ngtcp2_conn_extend_max_offset(*this, amount); +} - static constexpr auto kServerName = 0; - static constexpr auto kSelectedAlpn = 1; - static constexpr auto kCipherName = 2; - static constexpr auto kCipherVersion = 3; - static constexpr auto kValidationErrorReason = 4; - static constexpr auto kValidationErrorCode = 5; +void Session::UpdateDataStats() { + Debug(this, "Updating data stats"); + auto& stats_ = impl_->stats_; + ngtcp2_conn_info info; + ngtcp2_conn_get_conn_info(*this, &info); + STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight); + STAT_SET(Stats, cwnd, info.cwnd); + STAT_SET(Stats, latest_rtt, info.latest_rtt); + STAT_SET(Stats, min_rtt, info.min_rtt); + STAT_SET(Stats, rttvar, info.rttvar); + STAT_SET(Stats, smoothed_rtt, info.smoothed_rtt); + STAT_SET(Stats, ssthresh, info.ssthresh); + STAT_SET( + Stats, + max_bytes_in_flight, + std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); +} - Local argv[] = { - Undefined(isolate), // The negotiated server name - Undefined(isolate), // The selected alpn - Undefined(isolate), // Cipher name - Undefined(isolate), // Cipher version - Undefined(isolate), // Validation error reason - Undefined(isolate), // Validation error code - v8::Boolean::New(isolate, tls_session().early_data_was_accepted())}; +void Session::SendConnectionClose() { + // Method is a non-op if the session is in a state where packets cannot + // be transmitted to the remote peer. + if (!can_send_packets()) return; - auto& tls = tls_session(); - auto peerVerifyError = tls.VerifyPeerIdentity(env()); - if (peerVerifyError.has_value() && - (!peerVerifyError->reason.ToLocal(&argv[kValidationErrorReason]) || - !peerVerifyError->code.ToLocal(&argv[kValidationErrorCode]))) { - return; - } + Debug(this, "Sending connection close packet to peer"); - if (!ToV8Value(env()->context(), tls.servername()) - .ToLocal(&argv[kServerName]) || - !ToV8Value(env()->context(), tls.alpn()).ToLocal(&argv[kSelectedAlpn]) || - !tls.cipher_name(env()).ToLocal(&argv[kCipherName]) || - !tls.cipher_version(env()).ToLocal(&argv[kCipherVersion])) { - return; - } + auto ErrorAndSilentClose = [&] { + Debug(this, "Failed to create connection close packet"); + SetLastError(QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR)); + Close(CloseMethod::SILENT); + }; - Debug(this, "Notifying JavaScript of handshake complete"); - MakeCallback(BindingData::Get(env()).session_handshake_callback(), - arraysize(argv), - argv); -} + if (is_server()) { + if (auto packet = Packet::CreateConnectionClosePacket( + env(), + endpoint(), + impl_->remote_address_, + *this, + impl_->last_error_)) [[likely]] { + return Send(packet); + } -void Session::EmitPathValidation(PathValidationResult result, - PathValidationFlags flags, - const ValidatedPath& newPath, - const std::optional& oldPath) { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return; - if (state_->path_validation == 0) [[likely]] { - return; + // If we are unable to create a connection close packet then + // we are in a bad state. An internal error will be set and + // the session will be silently closed. This is not ideal + // because the remote peer will not know immediately that + // the connection has terminated but there's not much else + // we can do. + return ErrorAndSilentClose(); } - auto isolate = env()->isolate(); - CallbackScope cb_scope(this); - auto& state = BindingData::Get(env()); - - const auto resultToString = ([&] { - switch (result) { - case PathValidationResult::ABORTED: - return state.aborted_string(); - case PathValidationResult::FAILURE: - return state.failure_string(); - case PathValidationResult::SUCCESS: - return state.success_string(); - } - UNREACHABLE(); - })(); + auto packet = Packet::Create(env(), + endpoint(), + impl_->remote_address_, + kDefaultMaxPacketLength, + "immediate connection close (client)"); + if (!packet) [[unlikely]] { + return ErrorAndSilentClose(); + } - Local argv[] = { - resultToString, - SocketAddressBase::Create(env(), newPath.local)->object(), - SocketAddressBase::Create(env(), newPath.remote)->object(), - Undefined(isolate), - Undefined(isolate), - Boolean::New(isolate, flags.preferredAddress)}; + ngtcp2_vec vec = *packet; + Path path(impl_->local_address_, impl_->remote_address_); + ssize_t nwrite = ngtcp2_conn_write_connection_close(*this, + &path, + nullptr, + vec.base, + vec.len, + impl_->last_error_, + uv_hrtime()); - if (oldPath.has_value()) { - argv[3] = SocketAddressBase::Create(env(), oldPath->local)->object(); - argv[4] = SocketAddressBase::Create(env(), oldPath->remote)->object(); + if (nwrite < 0) [[unlikely]] { + packet->Done(UV_ECANCELED); + return ErrorAndSilentClose(); } - Debug(this, "Notifying JavaScript of path validation"); - MakeCallback(state.session_path_validation_callback(), arraysize(argv), argv); + packet->Truncate(nwrite); + return Send(packet); } -void Session::EmitSessionTicket(Store&& ticket) { - DCHECK(!is_destroyed()); - if (!env()->can_call_into_js()) return; - - // If there is nothing listening for the session ticket, don't bother - // emitting. - if (!wants_session_ticket()) [[likely]] { - Debug(this, "Session ticket was discarded"); - return; +void Session::OnTimeout() { + CHECK(!is_destroyed()); + HandleScope scope(env()->isolate()); + int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); + if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { + return application().SendPendingData(); } - CallbackScope cb_scope(this); + Debug(this, "Session timed out"); + SetLastError(QuicError::ForNgtcp2Error(ret)); + Close(CloseMethod::SILENT); +} - auto remote_transport_params = GetRemoteTransportParams(); - Store transport_params; - if (remote_transport_params) - transport_params = remote_transport_params.Encode(env()); +void Session::UpdateTimer() { + CHECK(!is_destroyed()); + // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. + uint64_t expiry = ngtcp2_conn_get_expiry(*this); + uint64_t now = uv_hrtime(); + Debug( + this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); - SessionTicket session_ticket(std::move(ticket), std::move(transport_params)); - Local argv; - if (session_ticket.encode(env()).ToLocal(&argv)) { - Debug(this, "Notifying JavaScript of session ticket"); - MakeCallback(BindingData::Get(env()).session_ticket_callback(), 1, &argv); + if (expiry <= now) { + // The timer has already expired. + return OnTimeout(); } -} -void Session::EmitStream(const BaseObjectPtr& stream) { - if (is_destroyed()) return; - if (!env()->can_call_into_js()) return; - CallbackScope cb_scope(this); - auto isolate = env()->isolate(); - Local argv[] = { - stream->object(), - Integer::NewFromUnsigned(isolate, - static_cast(stream->direction())), - }; + auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; + Debug(this, "Updating timeout to %zu milliseconds", timeout); - Debug(this, "Notifying JavaScript of stream created"); - MakeCallback( - BindingData::Get(env()).stream_created_callback(), arraysize(argv), argv); + // If timeout is zero here, it means our timer is less than a millisecond + // off from expiry. Let's bump the timer to 1. + impl_->timer_.Update(timeout == 0 ? 1 : timeout); } -void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, - const uint32_t* sv, - size_t nsv) { +void Session::DatagramStatus(uint64_t datagramId, quic::DatagramStatus status) { DCHECK(!is_destroyed()); - DCHECK(!is_server()); - if (!env()->can_call_into_js()) return; - - auto isolate = env()->isolate(); - const auto to_integer = [&](uint32_t version) { - return Integer::NewFromUnsigned(isolate, version); - }; - - CallbackScope cb_scope(this); + auto& stats_ = impl_->stats_; + switch (status) { + case quic::DatagramStatus::ACKNOWLEDGED: { + Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId); + STAT_INCREMENT(Stats, datagrams_acknowledged); + break; + } + case quic::DatagramStatus::LOST: { + Debug(this, "Datagram %" PRIu64 " was lost", datagramId); + STAT_INCREMENT(Stats, datagrams_lost); + break; + } + } + EmitDatagramStatus(datagramId, status); +} - // version() is the version that was actually configured for this session. +void Session::DatagramReceived(const uint8_t* data, + size_t datalen, + DatagramReceivedFlags flag) { + DCHECK(!is_destroyed()); + // If there is nothing watching for the datagram on the JavaScript side, + // or if the datagram is zero-length, we just drop it on the floor. + if (impl_->state_->datagram == 0 || datalen == 0) return; - // versions are the versions requested by the peer. - MaybeStackBuffer, 5> versions; - versions.AllocateSufficientStorage(nsv); - for (size_t n = 0; n < nsv; n++) versions[n] = to_integer(sv[n]); + Debug(this, "Session is receiving datagram of size %zu", datalen); + auto& stats_ = impl_->stats_; + STAT_INCREMENT(Stats, datagrams_received); + auto backing = ArrayBuffer::NewBackingStore( + env()->isolate(), + datalen, + BackingStoreInitializationMode::kUninitialized); + memcpy(backing->Data(), data, datalen); + EmitDatagram(Store(std::move(backing), datalen), flag); +} - // supported are the versions we acutually support expressed as a range. - // The first value is the minimum version, the second is the maximum. - Local supported[] = {to_integer(config_.options.min_version), - to_integer(config_.options.version)}; +void Session::GenerateNewConnectionId(ngtcp2_cid* cid, + size_t len, + uint8_t* token) { + DCHECK(!is_destroyed()); + CID cid_ = impl_->config_.options.cid_factory->GenerateInto(cid, len); + Debug(this, "Generated new connection id %s", cid_); + StatelessResetToken new_token( + token, endpoint().options().reset_token_secret, cid_); + endpoint().AssociateCID(cid_, impl_->config_.scid); + endpoint().AssociateStatelessResetToken(new_token, this); +} - Local argv[] = {// The version configured for this session. - to_integer(version()), - // The versions requested. - Array::New(isolate, versions.out(), nsv), - // The versions we actually support. - Array::New(isolate, supported, arraysize(supported))}; +bool Session::HandshakeCompleted() { + DCHECK(!is_destroyed()); + DCHECK(!impl_->state_->handshake_completed); - Debug(this, "Notifying JavaScript of version negotiation"); - MakeCallback(BindingData::Get(env()).session_version_negotiation_callback(), - arraysize(argv), - argv); -} + Debug(this, "Session handshake completed"); + impl_->state_->handshake_completed = 1; + auto& stats_ = impl_->stats_; + STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); + SetStreamOpenAllowed(); -void Session::EmitKeylog(const char* line) { - if (!env()->can_call_into_js()) return; - if (keylog_stream_) { - Debug(this, "Emitting keylog line"); - env()->SetImmediate([ptr = keylog_stream_, data = std::string(line) + "\n"]( - Environment* env) { ptr->Emit(data); }); - } -} + // TODO(@jasnel): Not yet supporting early data... + // if (!tls_session().early_data_was_accepted()) + // ngtcp2_conn_tls_early_data_rejected(*this); -// ============================================================================ -// ngtcp2 static callback functions + // When in a server session, handshake completed == handshake confirmed. + if (is_server()) { + HandshakeConfirmed(); -#define NGTCP2_CALLBACK_SCOPE(name) \ - auto name = Impl::From(conn, user_data); \ - if (name->is_destroyed()) [[unlikely]] { \ - return NGTCP2_ERR_CALLBACK_FAILURE; \ - } \ - NgTcp2CallbackScope scope(session->env()); + auto& ep = endpoint(); -struct Session::Impl { - static Session* From(ngtcp2_conn* conn, void* user_data) { - DCHECK_NOT_NULL(user_data); - auto session = static_cast(user_data); - DCHECK_EQ(conn, session->connection_.get()); - return session; + if (!ep.is_closed() && !ep.is_closing()) { + auto token = ep.GenerateNewToken(version(), impl_->remote_address_); + ngtcp2_vec vec = token; + if (NGTCP2_ERR(ngtcp2_conn_submit_new_token(*this, vec.base, vec.len))) { + // Submitting the new token failed... In this case we're going to + // fail because submitting the new token should only fail if we + // ran out of memory or some other unrecoverable state. + return false; + } + } } - static void DoDestroy(const FunctionCallbackInfo& args) { - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - session->Destroy(); - } + EmitHandshakeComplete(); - static void GetRemoteAddress(const FunctionCallbackInfo& args) { - auto env = Environment::GetCurrent(args); - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - auto address = session->remote_address(); - args.GetReturnValue().Set( - SocketAddressBase::Create(env, std::make_shared(address)) - ->object()); - } + return true; +} - static void GetCertificate(const FunctionCallbackInfo& args) { - auto env = Environment::GetCurrent(args); - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - Local ret; - if (session->tls_session().cert(env).ToLocal(&ret)) - args.GetReturnValue().Set(ret); - } +void Session::HandshakeConfirmed() { + DCHECK(!is_destroyed()); + DCHECK(!impl_->state_->handshake_confirmed); + Debug(this, "Session handshake confirmed"); + impl_->state_->handshake_confirmed = 1; + auto& stats_ = impl_->stats_; + STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at); +} - static void GetEphemeralKeyInfo(const FunctionCallbackInfo& args) { - auto env = Environment::GetCurrent(args); - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - Local ret; - if (!session->is_server() && - session->tls_session().ephemeral_key(env).ToLocal(&ret)) - args.GetReturnValue().Set(ret); +void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { + if (options().preferred_address_strategy == + PreferredAddress::Policy::IGNORE_PREFERRED) { + Debug(this, "Ignoring preferred address"); + return; } - static void GetPeerCertificate(const FunctionCallbackInfo& args) { - auto env = Environment::GetCurrent(args); - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - Local ret; - if (session->tls_session().peer_cert(env).ToLocal(&ret)) - args.GetReturnValue().Set(ret); + switch (endpoint().local_address().family()) { + case AF_INET: { + Debug(this, "Selecting preferred address for AF_INET"); + auto ipv4 = preferredAddress->ipv4(); + if (ipv4.has_value()) { + if (ipv4->address.empty() || ipv4->port == 0) return; + CHECK(SocketAddress::New(AF_INET, + std::string(ipv4->address).c_str(), + ipv4->port, + &impl_->remote_address_)); + preferredAddress->Use(ipv4.value()); + } + break; + } + case AF_INET6: { + Debug(this, "Selecting preferred address for AF_INET6"); + auto ipv6 = preferredAddress->ipv6(); + if (ipv6.has_value()) { + if (ipv6->address.empty() || ipv6->port == 0) return; + CHECK(SocketAddress::New(AF_INET, + std::string(ipv6->address).c_str(), + ipv6->port, + &impl_->remote_address_)); + preferredAddress->Use(ipv6.value()); + } + break; + } } +} - static void GracefulClose(const FunctionCallbackInfo& args) { - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - session->Close(Session::CloseMethod::GRACEFUL); - } +CID Session::new_cid(size_t len) const { + return options().cid_factory->Generate(len); +} - static void SilentClose(const FunctionCallbackInfo& args) { - // This is exposed for testing purposes only! - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - session->Close(Session::CloseMethod::SILENT); - } +void Session::ProcessPendingBidiStreams() { + // It shouldn't be possible to get here if can_create_streams() is false. + DCHECK(can_create_streams()); - static void UpdateKey(const FunctionCallbackInfo& args) { - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - // Initiating a key update may fail if it is done too early (either - // before the TLS handshake has been confirmed or while a previous - // key update is being processed). When it fails, InitiateKeyUpdate() - // will return false. - Debug(session, "Initiating key update"); - args.GetReturnValue().Set(session->tls_session().InitiateKeyUpdate()); - } + int64_t id; - static void OpenStream(const FunctionCallbackInfo& args) { - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - DCHECK(args[0]->IsUint32()); - auto direction = static_cast(args[0].As()->Value()); - Local stream; - if (session->OpenStream(direction).ToLocal(&stream)) [[likely]] { - args.GetReturnValue().Set(stream); + while (!impl_->pending_bidi_stream_queue_.IsEmpty()) { + if (ngtcp2_conn_get_streams_bidi_left(*this) == 0) { + return; } - } - static void DoSendDatagram(const FunctionCallbackInfo& args) { - auto env = Environment::GetCurrent(args); - Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); - DCHECK(args[0]->IsArrayBufferView()); - args.GetReturnValue().Set(BigInt::New( - env->isolate(), - session->SendDatagram(Store(args[0].As())))); + switch (ngtcp2_conn_open_bidi_stream(*this, &id, nullptr)) { + case 0: { + impl_->pending_bidi_stream_queue_.PopFront()->fulfill(id); + continue; + } + case NGTCP2_ERR_STREAM_ID_BLOCKED: { + // This case really should not happen since we've checked the number + // of bidi streams left above. However, if it does happen we'll treat + // it the same as if the get_streams_bidi_left call returned zero. + return; + } + default: { + // We failed to open the stream for some reason other than being + // blocked. Report the failure. + impl_->pending_bidi_stream_queue_.PopFront()->reject( + QuicError::ForTransport(NGTCP2_STREAM_LIMIT_ERROR)); + continue; + } + } } +} - static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn, - int64_t stream_id, - uint64_t offset, - uint64_t datalen, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - return session->application().AcknowledgeStreamData(stream_id, datalen) - ? NGTCP2_SUCCESS - : NGTCP2_ERR_CALLBACK_FAILURE; - } +void Session::ProcessPendingUniStreams() { + // It shouldn't be possible to get here if can_create_streams() is false. + DCHECK(can_create_streams()); - static int on_acknowledge_datagram(ngtcp2_conn* conn, - uint64_t dgram_id, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->DatagramStatus(dgram_id, quic::DatagramStatus::ACKNOWLEDGED); - return NGTCP2_SUCCESS; - } + int64_t id; - static int on_cid_status(ngtcp2_conn* conn, - ngtcp2_connection_id_status_type type, - uint64_t seq, - const ngtcp2_cid* cid, - const uint8_t* token, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - std::optional maybe_reset_token; - if (token != nullptr) maybe_reset_token.emplace(token); - auto& endpoint = session->endpoint(); - switch (type) { - case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: { - endpoint.AssociateCID(session->config_.scid, CID(cid)); - if (token != nullptr) { - endpoint.AssociateStatelessResetToken(StatelessResetToken(token), - session); - } - break; + while (!impl_->pending_uni_stream_queue_.IsEmpty()) { + if (ngtcp2_conn_get_streams_uni_left(*this) == 0) { + return; + } + + switch (ngtcp2_conn_open_uni_stream(*this, &id, nullptr)) { + case 0: { + impl_->pending_uni_stream_queue_.PopFront()->fulfill(id); + continue; } - case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: { - endpoint.DisassociateCID(CID(cid)); - if (token != nullptr) { - endpoint.DisassociateStatelessResetToken(StatelessResetToken(token)); - } - break; + case NGTCP2_ERR_STREAM_ID_BLOCKED: { + // This case really should not happen since we've checked the number + // of bidi streams left above. However, if it does happen we'll treat + // it the same as if the get_streams_bidi_left call returned zero. + return; + } + default: { + // We failed to open the stream for some reason other than being + // blocked. Report the failure. + impl_->pending_uni_stream_queue_.PopFront()->reject( + QuicError::ForTransport(NGTCP2_STREAM_LIMIT_ERROR)); + continue; } } - return NGTCP2_SUCCESS; } +} - static int on_extend_max_remote_streams_bidi(ngtcp2_conn* conn, - uint64_t max_streams, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - // TODO(@jasnell): Do anything here? - return NGTCP2_SUCCESS; +// JavaScript callouts + +void Session::EmitClose(const QuicError& error) { + DCHECK(!is_destroyed()); + // When EmitClose is called, the expectation is that the JavaScript + // side will close the loop and call destroy on the underlying session. + // If we cannot call out into JavaScript at this point, go ahead and + // skip to calling destroy directly. + if (!env()->can_call_into_js()) return Destroy(); + + CallbackScope cb_scope(this); + + Local argv[] = { + Integer::New(env()->isolate(), static_cast(error.type())), + BigInt::NewFromUnsigned(env()->isolate(), error.code()), + Undefined(env()->isolate()), + }; + if (error.reason().length() > 0 && + !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { + return; } - static int on_extend_max_remote_streams_uni(ngtcp2_conn* conn, - uint64_t max_streams, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - // TODO(@jasnell): Do anything here? - return NGTCP2_SUCCESS; - } + MakeCallback( + BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); - static int on_extend_max_streams_bidi(ngtcp2_conn* conn, - uint64_t max_streams, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->ProcessPendingBidiStreams(); - return NGTCP2_SUCCESS; - } + // Importantly, the session instance itself should have been destroyed! + CHECK(is_destroyed()); +} - static int on_extend_max_streams_uni(ngtcp2_conn* conn, - uint64_t max_streams, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->ProcessPendingUniStreams(); - return NGTCP2_SUCCESS; - } +void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { + DCHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; - static int on_extend_max_stream_data(ngtcp2_conn* conn, - int64_t stream_id, - uint64_t max_data, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->application().ExtendMaxStreamData(Stream::From(stream_user_data), - max_data); - return NGTCP2_SUCCESS; - } + CallbackScope cbv_scope(this); - static int on_get_new_cid(ngtcp2_conn* conn, - ngtcp2_cid* cid, - uint8_t* token, - size_t cidlen, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - return session->GenerateNewConnectionId(cid, cidlen, token) - ? NGTCP2_SUCCESS - : NGTCP2_ERR_CALLBACK_FAILURE; - } + Local argv[] = {datagram.ToUint8Array(env()), + Boolean::New(env()->isolate(), flag.early)}; - static int on_handshake_completed(ngtcp2_conn* conn, void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - return session->HandshakeCompleted() ? NGTCP2_SUCCESS - : NGTCP2_ERR_CALLBACK_FAILURE; - } + MakeCallback(BindingData::Get(env()).session_datagram_callback(), + arraysize(argv), + argv); +} - static int on_handshake_confirmed(ngtcp2_conn* conn, void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->HandshakeConfirmed(); - return NGTCP2_SUCCESS; - } +void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { + DCHECK(!is_destroyed()); - static int on_lost_datagram(ngtcp2_conn* conn, - uint64_t dgram_id, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->DatagramStatus(dgram_id, quic::DatagramStatus::LOST); - return NGTCP2_SUCCESS; - } + if (!env()->can_call_into_js()) return; - static int on_path_validation(ngtcp2_conn* conn, - uint32_t flags, - const ngtcp2_path* path, - const ngtcp2_path* old_path, - ngtcp2_path_validation_result res, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - bool flag_preferred_address = - flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR; - ValidatedPath newValidatedPath{ - std::make_shared(path->local.addr), - std::make_shared(path->remote.addr)}; - std::optional oldValidatedPath = std::nullopt; - if (old_path != nullptr) { - oldValidatedPath = - ValidatedPath{std::make_shared(old_path->local.addr), - std::make_shared(old_path->remote.addr)}; + CallbackScope cb_scope(this); + + auto& state = BindingData::Get(env()); + + const auto status_to_string = ([&] { + switch (status) { + case quic::DatagramStatus::ACKNOWLEDGED: + return state.acknowledged_string(); + case quic::DatagramStatus::LOST: + return state.lost_string(); } - session->EmitPathValidation(static_cast(res), - PathValidationFlags{flag_preferred_address}, - newValidatedPath, - oldValidatedPath); - return NGTCP2_SUCCESS; - } + UNREACHABLE(); + })(); - static int on_receive_datagram(ngtcp2_conn* conn, - uint32_t flags, - const uint8_t* data, - size_t datalen, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - DatagramReceivedFlags f; - f.early = flags & NGTCP2_DATAGRAM_FLAG_0RTT; - session->DatagramReceived(data, datalen, f); - return NGTCP2_SUCCESS; - } + Local argv[] = {BigInt::NewFromUnsigned(env()->isolate(), id), + status_to_string}; - static int on_receive_new_token(ngtcp2_conn* conn, - const uint8_t* token, - size_t tokenlen, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - // We currently do nothing with this callback. - return NGTCP2_SUCCESS; - } + MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv); +} - static int on_receive_rx_key(ngtcp2_conn* conn, - ngtcp2_encryption_level level, - void* user_data) { - auto session = Impl::From(conn, user_data); - if (session->is_destroyed()) [[unlikely]] { - return NGTCP2_ERR_CALLBACK_FAILURE; - } - CHECK(!session->is_server()); +void Session::EmitHandshakeComplete() { + DCHECK(!is_destroyed()); - if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; + if (!env()->can_call_into_js()) return; - Debug(session, - "Receiving RX key for level %d for dcid %s", - to_string(level), - session->config().dcid); + CallbackScope cb_scope(this); - return session->application().Start() ? NGTCP2_SUCCESS - : NGTCP2_ERR_CALLBACK_FAILURE; + auto isolate = env()->isolate(); + + static constexpr auto kServerName = 0; + static constexpr auto kSelectedAlpn = 1; + static constexpr auto kCipherName = 2; + static constexpr auto kCipherVersion = 3; + static constexpr auto kValidationErrorReason = 4; + static constexpr auto kValidationErrorCode = 5; + + Local argv[] = { + Undefined(isolate), // The negotiated server name + Undefined(isolate), // The selected protocol + Undefined(isolate), // Cipher name + Undefined(isolate), // Cipher version + Undefined(isolate), // Validation error reason + Undefined(isolate), // Validation error code + Boolean::New(isolate, tls_session().early_data_was_accepted())}; + + auto& tls = tls_session(); + auto peerVerifyError = tls.VerifyPeerIdentity(env()); + if (peerVerifyError.has_value() && + (!peerVerifyError->reason.ToLocal(&argv[kValidationErrorReason]) || + !peerVerifyError->code.ToLocal(&argv[kValidationErrorCode]))) { + return; } - static int on_receive_stateless_reset(ngtcp2_conn* conn, - const ngtcp2_pkt_stateless_reset* sr, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->state_->stateless_reset = 1; - return NGTCP2_SUCCESS; + if (!ToV8Value(env()->context(), tls.servername()) + .ToLocal(&argv[kServerName]) || + !ToV8Value(env()->context(), tls.protocol()) + .ToLocal(&argv[kSelectedAlpn]) || + !tls.cipher_name(env()).ToLocal(&argv[kCipherName]) || + !tls.cipher_version(env()).ToLocal(&argv[kCipherVersion])) { + return; } - static int on_receive_stream_data(ngtcp2_conn* conn, - uint32_t flags, - int64_t stream_id, - uint64_t offset, - const uint8_t* data, - size_t datalen, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - Stream::ReceiveDataFlags data_flags{ - // The fin flag indicates that this is the last chunk of data we will - // receive on this stream. - .fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) == - NGTCP2_STREAM_DATA_FLAG_FIN, - // Stream data is early if it is received before the TLS handshake is - // complete. - .early = (flags & NGTCP2_STREAM_DATA_FLAG_0RTT) == - NGTCP2_STREAM_DATA_FLAG_0RTT, - }; + MakeCallback(BindingData::Get(env()).session_handshake_callback(), + arraysize(argv), + argv); +} - // We received data for a stream! What we don't know yet at this point - // is whether the application wants us to treat this as a control stream - // data (something the application will handle on its own) or a user stream - // data (something that we should create a Stream handle for that is passed - // out to JavaScript). HTTP3, for instance, will generally create three - // control stream in either direction and we want to make sure those are - // never exposed to users and that we don't waste time creating Stream - // handles for them. So, what we do here is pass the stream data on to the - // application for processing. If it ends up being a user stream, the - // application will handle creating the Stream handle and passing that off - // to the JavaScript side. - if (!session->application().ReceiveStreamData( - stream_id, data, datalen, data_flags, stream_user_data)) { - return NGTCP2_ERR_CALLBACK_FAILURE; +void Session::EmitPathValidation(PathValidationResult result, + PathValidationFlags flags, + const ValidatedPath& newPath, + const std::optional& oldPath) { + DCHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return; + + if (impl_->state_->path_validation == 0) [[likely]] { + return; + } + + auto isolate = env()->isolate(); + CallbackScope cb_scope(this); + auto& state = BindingData::Get(env()); + + const auto resultToString = ([&] { + switch (result) { + case PathValidationResult::ABORTED: + return state.aborted_string(); + case PathValidationResult::FAILURE: + return state.failure_string(); + case PathValidationResult::SUCCESS: + return state.success_string(); } + UNREACHABLE(); + })(); + + Local argv[] = { + resultToString, + SocketAddressBase::Create(env(), newPath.local)->object(), + SocketAddressBase::Create(env(), newPath.remote)->object(), + Undefined(isolate), + Undefined(isolate), + Boolean::New(isolate, flags.preferredAddress)}; + + if (oldPath.has_value()) { + argv[3] = SocketAddressBase::Create(env(), oldPath->local)->object(); + argv[4] = SocketAddressBase::Create(env(), oldPath->remote)->object(); + } + + Debug(this, "Notifying JavaScript of path validation"); + MakeCallback(state.session_path_validation_callback(), arraysize(argv), argv); +} - return NGTCP2_SUCCESS; +void Session::EmitSessionTicket(Store&& ticket) { + DCHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return; + + // If there is nothing listening for the session ticket, don't bother + // emitting. + if (impl_->state_->session_ticket == 0) [[likely]] { + Debug(this, "Session ticket was discarded"); + return; } - static int on_receive_tx_key(ngtcp2_conn* conn, - ngtcp2_encryption_level level, - void* user_data) { - auto session = Impl::From(conn, user_data); - if (session->is_destroyed()) [[unlikely]] { - return NGTCP2_ERR_CALLBACK_FAILURE; + CallbackScope cb_scope(this); + + auto& remote_params = remote_transport_params(); + Store transport_params; + if (remote_params) { + if (auto transport_params = remote_params.Encode(env())) { + SessionTicket session_ticket(std::move(ticket), + std::move(transport_params)); + Local argv; + if (session_ticket.encode(env()).ToLocal(&argv)) [[likely]] { + MakeCallback( + BindingData::Get(env()).session_ticket_callback(), 1, &argv); + } } - CHECK(session->is_server()); + } +} - if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS; +void Session::EmitStream(const BaseObjectWeakPtr& stream) { + DCHECK(!is_destroyed()); - Debug(session, - "Receiving TX key for level %d for dcid %s", - to_string(level), - session->config().dcid); - return session->application().Start() ? NGTCP2_SUCCESS - : NGTCP2_ERR_CALLBACK_FAILURE; - } + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); - static int on_receive_version_negotiation(ngtcp2_conn* conn, - const ngtcp2_pkt_hd* hd, - const uint32_t* sv, - size_t nsv, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->EmitVersionNegotiation(*hd, sv, nsv); - return NGTCP2_SUCCESS; - } + auto isolate = env()->isolate(); + Local argv[] = { + stream->object(), + Integer::NewFromUnsigned(isolate, + static_cast(stream->direction())), + }; - static int on_remove_connection_id(ngtcp2_conn* conn, - const ngtcp2_cid* cid, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->endpoint().DisassociateCID(CID(cid)); - return NGTCP2_SUCCESS; - } + MakeCallback( + BindingData::Get(env()).stream_created_callback(), arraysize(argv), argv); +} - static int on_select_preferred_address(ngtcp2_conn* conn, - ngtcp2_path* dest, - const ngtcp2_preferred_addr* paddr, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - PreferredAddress preferred_address(dest, paddr); - session->SelectPreferredAddress(&preferred_address); - return NGTCP2_SUCCESS; - } +void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, + const uint32_t* sv, + size_t nsv) { + DCHECK(!is_destroyed()); + DCHECK(!is_server()); - static int on_stream_close(ngtcp2_conn* conn, - uint32_t flags, - int64_t stream_id, - uint64_t app_error_code, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - if (flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) { - session->application().StreamClose( - Stream::From(stream_user_data), - QuicError::ForApplication(app_error_code)); - } else { - session->application().StreamClose(Stream::From(stream_user_data)); - } - return NGTCP2_SUCCESS; - } + if (!env()->can_call_into_js()) return; - static int on_stream_reset(ngtcp2_conn* conn, - int64_t stream_id, - uint64_t final_size, - uint64_t app_error_code, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->application().StreamReset( - Stream::From(stream_user_data), - final_size, - QuicError::ForApplication(app_error_code)); - return NGTCP2_SUCCESS; - } + CallbackScope cb_scope(this); + auto& opts = options(); - static int on_stream_stop_sending(ngtcp2_conn* conn, - int64_t stream_id, - uint64_t app_error_code, - void* user_data, - void* stream_user_data) { - NGTCP2_CALLBACK_SCOPE(session) - session->application().StreamStopSending( - Stream::From(stream_user_data), - QuicError::ForApplication(app_error_code)); - return NGTCP2_SUCCESS; - } + // version() is the version that was actually configured for this session. + // versions are the versions requested by the peer. + // supported are the versions supported by Node.js. - static void on_rand(uint8_t* dest, - size_t destlen, - const ngtcp2_rand_ctx* rand_ctx) { - CHECK(ncrypto::CSPRNG(dest, destlen)); + LocalVector versions(env()->isolate(), nsv); + for (size_t n = 0; n < nsv; n++) { + versions.push_back(Integer::NewFromUnsigned(env()->isolate(), sv[n])); } - static int on_early_data_rejected(ngtcp2_conn* conn, void* user_data) { - // TODO(@jasnell): Called when early data was rejected by server during the - // TLS handshake or client decided not to attempt early data. - return NGTCP2_SUCCESS; - } + // supported are the versions we acutually support expressed as a range. + // The first value is the minimum version, the second is the maximum. + Local supported[] = { + Integer::NewFromUnsigned(env()->isolate(), opts.min_version), + Integer::NewFromUnsigned(env()->isolate(), opts.version)}; - static constexpr ngtcp2_callbacks CLIENT = { - ngtcp2_crypto_client_initial_cb, - nullptr, - ngtcp2_crypto_recv_crypto_data_cb, - on_handshake_completed, - on_receive_version_negotiation, - ngtcp2_crypto_encrypt_cb, - ngtcp2_crypto_decrypt_cb, - ngtcp2_crypto_hp_mask_cb, - on_receive_stream_data, - on_acknowledge_stream_data_offset, - nullptr, - on_stream_close, - on_receive_stateless_reset, - ngtcp2_crypto_recv_retry_cb, - on_extend_max_streams_bidi, - on_extend_max_streams_uni, - on_rand, - on_get_new_cid, - on_remove_connection_id, - ngtcp2_crypto_update_key_cb, - on_path_validation, - on_select_preferred_address, - on_stream_reset, - on_extend_max_remote_streams_bidi, - on_extend_max_remote_streams_uni, - on_extend_max_stream_data, - on_cid_status, - on_handshake_confirmed, - on_receive_new_token, - ngtcp2_crypto_delete_crypto_aead_ctx_cb, - ngtcp2_crypto_delete_crypto_cipher_ctx_cb, - on_receive_datagram, - on_acknowledge_datagram, - on_lost_datagram, - ngtcp2_crypto_get_path_challenge_data_cb, - on_stream_stop_sending, - ngtcp2_crypto_version_negotiation_cb, - on_receive_rx_key, - nullptr, - on_early_data_rejected}; + Local argv[] = { + // The version configured for this session. + Integer::NewFromUnsigned(env()->isolate(), version()), + // The versions requested. + Array::New(env()->isolate(), versions.data(), nsv), + // The versions we actually support. + Array::New(env()->isolate(), supported, arraysize(supported))}; - static constexpr ngtcp2_callbacks SERVER = { - nullptr, - ngtcp2_crypto_recv_client_initial_cb, - ngtcp2_crypto_recv_crypto_data_cb, - on_handshake_completed, - nullptr, - ngtcp2_crypto_encrypt_cb, - ngtcp2_crypto_decrypt_cb, - ngtcp2_crypto_hp_mask_cb, - on_receive_stream_data, - on_acknowledge_stream_data_offset, - nullptr, - on_stream_close, - on_receive_stateless_reset, - nullptr, - on_extend_max_streams_bidi, - on_extend_max_streams_uni, - on_rand, - on_get_new_cid, - on_remove_connection_id, - ngtcp2_crypto_update_key_cb, - on_path_validation, - nullptr, - on_stream_reset, - on_extend_max_remote_streams_bidi, - on_extend_max_remote_streams_uni, - on_extend_max_stream_data, - on_cid_status, - nullptr, - nullptr, - ngtcp2_crypto_delete_crypto_aead_ctx_cb, - ngtcp2_crypto_delete_crypto_cipher_ctx_cb, - on_receive_datagram, - on_acknowledge_datagram, - on_lost_datagram, - ngtcp2_crypto_get_path_challenge_data_cb, - on_stream_stop_sending, - ngtcp2_crypto_version_negotiation_cb, - nullptr, - on_receive_tx_key, - on_early_data_rejected}; -}; + MakeCallback(BindingData::Get(env()).session_version_negotiation_callback(), + arraysize(argv), + argv); +} -#undef NGTCP2_CALLBACK_SCOPE +void Session::EmitKeylog(const char* line) { + if (!env()->can_call_into_js()) return; + if (keylog_stream_) { + Debug(this, "Emitting keylog line"); + env()->SetImmediate([ptr = keylog_stream_, data = std::string(line) + "\n"]( + Environment* env) { ptr->Emit(data); }); + } +} + +// ============================================================================ Local Session::GetConstructorTemplate(Environment* env) { auto& state = BindingData::Get(env); @@ -2439,54 +2782,16 @@ void Session::RegisterExternalReferences(ExternalReferenceRegistry* registry) { #undef V } -Session::QuicConnectionPointer Session::InitConnection() { - ngtcp2_conn* conn; - Path path(local_address_, remote_address_); - Debug(this, "Initializing session for path %s", path); - TransportParams::Config tp_config( - config_.side, config_.ocid, config_.retry_scid); - TransportParams transport_params(tp_config, config_.options.transport_params); - transport_params.GenerateSessionTokens(this); - - switch (config_.side) { - case Side::SERVER: { - CHECK_EQ(ngtcp2_conn_server_new(&conn, - config_.dcid, - config_.scid, - path, - config_.version, - &Impl::SERVER, - &config_.settings, - transport_params, - &allocator_, - this), - 0); - break; - } - case Side::CLIENT: { - CHECK_EQ(ngtcp2_conn_client_new(&conn, - config_.dcid, - config_.scid, - path, - config_.version, - &Impl::CLIENT, - &config_.settings, - transport_params, - &allocator_, - this), - 0); - break; - } - } - return QuicConnectionPointer(conn); -} - -void Session::InitPerIsolate(IsolateData* data, - v8::Local target) { +void Session::InitPerIsolate(IsolateData* data, Local target) { // TODO(@jasnell): Implement the per-isolate state } void Session::InitPerContext(Realm* realm, Local target) { +#define V(name, str) \ + NODE_DEFINE_CONSTANT(target, CC_ALGO_##name); \ + NODE_DEFINE_STRING_CONSTANT(target, "CC_ALGO_" #name "_STR", #str); + CC_ALGOS(V) +#undef V // Make sure the Session constructor template is initialized. USE(GetConstructorTemplate(realm->env())); diff --git a/src/quic/session.h b/src/quic/session.h index f84a125321c9da..be59a6ed7dec9d 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -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 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,15 +120,15 @@ 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. @@ -119,9 +137,46 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // so that it cannot be garbage collected. BaseObjectPtr cid_factory_ref = {}; + // If the application provider is specified, it will be used to create + // the underlying Application instance for the session. + BaseObjectPtr 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, @@ -219,144 +274,110 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { 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); - - const TransportParams GetLocalTransportParams() const; - 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; - // Note that we are returning a BaseObjectWeakPtr here. The Stream instance - // itself is owned strongly by the Session in a detached state. This means - // that the only reference to the Stream keeping it alive is held by the - // Session itself. That strong reference will be destroyed either when the - // stream is removed from session or the session is destroyed. All other - // references to the stream should be held weakly. - BaseObjectWeakPtr FindStream(int64_t id) const; - - enum class CreateStreamOption { - NOTIFY, - DO_NOT_NOTIFY, - }; - - // Creates the stream, adding it to the sessions stream map if successful. - // A weak reference to the stream is returned. - BaseObjectWeakPtr CreateStream( - int64_t id, CreateStreamOption option = CreateStreamOption::NOTIFY); - - // 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 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); - DISALLOW_COPY_AND_MOVE(SendPendingDataScope) ~SendPendingDataScope(); + DISALLOW_COPY_AND_MOVE(SendPendingDataScope) }; - inline PendingStream::PendingStreamQueue& pending_bidi_stream_queue() { - return pending_bidi_stream_queue_; - } + struct State; + struct Stats; - inline PendingStream::PendingStreamQueue& pending_uni_stream_queue() { - return pending_uni_stream_queue_; - } + void HandleQlog(uint32_t flags, const void* data, size_t len); private: struct Impl; - struct MaybeCloseConnectionScope; using StreamsMap = std::unordered_map>; using QuicConnectionPointer = DeleteFnPtr; - 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(BaseObjectPtr&& packet); - void Send(BaseObjectPtr&& packet, const PathStorage& path); + void Send(const BaseObjectPtr& packet); + void Send(const BaseObjectPtr& packet, const PathStorage& path); uint64_t SendDatagram(Store&& data); - void AddStream(const BaseObjectPtr& stream, - CreateStreamOption option); + // A non-const variation to allow certain modifications. + Config& config(); + + enum class CreateStreamOption { + NOTIFY, + DO_NOT_NOTIFY, + }; + BaseObjectPtr FindStream(int64_t id) const; + BaseObjectPtr CreateStream( + int64_t id, + CreateStreamOption option = CreateStreamOption::NOTIFY, + std::shared_ptr data_source = nullptr); + void AddStream(BaseObjectPtr 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 OpenStream( + Direction direction, std::shared_ptr 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; @@ -403,12 +424,43 @@ 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(); @@ -431,57 +483,43 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { const ValidatedPath& newPath, const std::optional& oldPath); void EmitSessionTicket(Store&& ticket); - void EmitStream(const BaseObjectPtr& stream); + void EmitStream(const BaseObjectWeakPtr& 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); - QuicConnectionPointer InitConnection(); + static std::unique_ptr SelectApplication(Session* session, + const Config& config); - std::unique_ptr select_application(); + QuicConnectionPointer InitConnection(); - AliasedStruct stats_; - AliasedStruct state_; + Side side_; ngtcp2_mem allocator_; - BaseObjectWeakPtr endpoint_; - Config config_; - SocketAddress local_address_; - SocketAddress remote_address_; + std::unique_ptr impl_; QuicConnectionPointer connection_; std::unique_ptr tls_session_; - std::unique_ptr application_; - StreamsMap streams_; - TimerWrapHandle timer_; - size_t send_scope_depth_ = 0; - size_t connection_close_depth_ = 0; - QuicError last_error_; - BaseObjectPtr conn_closebuf_; BaseObjectPtr qlog_stream_; BaseObjectPtr keylog_stream_; - PendingStream::PendingStreamQueue pending_bidi_stream_queue_; - PendingStream::PendingStreamQueue pending_uni_stream_queue_; friend class Application; friend class DefaultApplication; - friend class Http3Application; + 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 diff --git a/src/quic/sessionticket.cc b/src/quic/sessionticket.cc index 481409457226cb..701d6d2eb16856 100644 --- a/src/quic/sessionticket.cc +++ b/src/quic/sessionticket.cc @@ -155,9 +155,8 @@ std::optional 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); } } diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 15ee9e3101ed0e..f1ce7d2e562703 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -45,6 +45,7 @@ namespace quic { V(WRITE_ENDED, write_ended, 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) \ @@ -103,7 +104,7 @@ PendingStream::PendingStream(Direction direction, PendingStream::~PendingStream() { pending_stream_queue_.Remove(); - if (!waiting_) { + if (waiting_) { Debug(stream_, "A pending stream was canceled"); } } @@ -143,11 +144,10 @@ STAT_STRUCT(Stream, STREAM) // ============================================================================ -namespace { -Maybe> GetDataQueueFromSource(Environment* env, - Local value) { +Maybe> Stream::GetDataQueueFromSource( + Environment* env, Local value) { DCHECK_IMPLIES(!value->IsUndefined(), value->IsObject()); - std::vector> entries(1); + std::vector> entries; if (value->IsUndefined()) { return Just(std::shared_ptr()); } else if (value->IsArrayBuffer()) { @@ -161,8 +161,13 @@ Maybe> GetDataQueueFromSource(Environment* env, buffer->GetBackingStore(), 0, buffer->ByteLength())); return Just(DataQueue::CreateIdempotent(std::move(entries))); } else if (value->IsArrayBufferView()) { - entries.push_back( - DataQueue::CreateInMemoryEntryFromView(value.As())); + auto entry = + DataQueue::CreateInMemoryEntryFromView(value.As()); + if (!entry) { + THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable"); + return Nothing>(); + } + entries.push_back(std::move(entry)); return Just(DataQueue::CreateIdempotent(std::move(entries))); } else if (Blob::HasInstance(env, value)) { Blob* blob; @@ -174,7 +179,6 @@ Maybe> GetDataQueueFromSource(Environment* env, THROW_ERR_INVALID_ARG_TYPE(env, "Invalid data source type"); return Nothing>(); } -} // namespace // Provides the implementation of the various JavaScript APIs for the // Stream object. @@ -405,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 next, @@ -1022,8 +1026,10 @@ void Stream::set_final_size(uint64_t final_size) { void Stream::set_outbound(std::shared_ptr source) { if (!source || !is_writable()) return; + Debug(this, "Setting the outbound data source"); DCHECK_NULL(outbound_); outbound_ = std::make_unique(this, std::move(source)); + state_->has_outbound = 1; if (!is_pending()) session_->ResumeStream(id()); } @@ -1094,6 +1100,8 @@ bool Stream::AddHeader(const Header& header) { void Stream::Acknowledge(size_t datalen) { 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)); @@ -1104,6 +1112,7 @@ void Stream::Acknowledge(size_t datalen) { } void Stream::Commit(size_t datalen) { + Debug(this, "Commiting %zu bytes", datalen); STAT_RECORD_TIMESTAMP(Stats, acked_at); if (outbound_) outbound_->Commit(datalen); } @@ -1114,7 +1123,7 @@ void Stream::EndWritable() { // 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; } @@ -1182,6 +1191,9 @@ void Stream::ReceiveData(const uint8_t* data, ReceiveDataFlags flags) { // If reading has ended, or there is no data, there's nothing to do but maybe // end the readable side if this is the last bit of data we've received. + + Debug(this, "Receiving %zu bytes of data", len); + if (state_->read_ended == 1 || len == 0) { if (flags.fin) EndReadable(); return; @@ -1201,6 +1213,7 @@ void Stream::ReceiveStopSending(QuicError error) { // Note that this comes from *this* endpoint, not the other side. We handle it // if we haven't already shutdown our *receiving* side of the stream. if (state_->read_ended) return; + Debug(this, "Received stop sending with error %s", error); ngtcp2_conn_shutdown_stream_read(session(), 0, id(), error.code()); EndReadable(); } @@ -1211,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); } @@ -1220,6 +1237,7 @@ 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. + Debug(this, "Blocked"); if (!env()->can_call_into_js() || !state_->wants_block) { return; } @@ -1281,10 +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. + Debug(this, "Scheduled"); if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this); } void Stream::Unschedule() { + Debug(this, "Unscheduled"); stream_queue_.Remove(); } diff --git a/src/quic/streams.h b/src/quic/streams.h index 7461ef5b66f5fe..4c6f63a851cf03 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -129,12 +129,23 @@ class PendingStream final { // 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; + static v8::Maybe> GetDataQueueFromSource( + Environment* env, v8::Local value); + static Stream* From(void* stream_user_data); static bool HasInstance(Environment* env, v8::Local value); @@ -189,6 +200,9 @@ class Stream final : public AsyncWrap, 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. @@ -201,6 +215,7 @@ class Stream final : public AsyncWrap, // of bytes have been acknowledged by the peer. void Acknowledge(size_t datalen); void Commit(size_t datalen); + void EndWritable(); void EndReadable(std::optional maybe_final_size = std::nullopt); void EntryRead(size_t amount) override; @@ -232,12 +247,15 @@ class Stream final : 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) @@ -246,13 +264,6 @@ class Stream final : 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; @@ -280,6 +291,13 @@ class Stream final : 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); @@ -326,6 +344,8 @@ class Stream final : public AsyncWrap, 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 diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 358bad2ee3697f..fda49710e85938 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -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::From(Environment* env, SetOption( \ 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(); } @@ -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(options.alpn.data()), - options.alpn.size()) != 0) { + reinterpret_cast(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; diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 3f2f8aff42a8a5..77771d1a252a24 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -34,6 +34,7 @@ class TLSSession final : public MemoryRetainer { std::shared_ptr context, const std::optional& 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; diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 2e8cd26a0cef9e..0f54fe2d499060 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -62,7 +62,7 @@ Maybe 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(); } @@ -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(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); } } diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index af6af3fc0266b3..77f367deaa4d41 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -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_{}; diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js index bcf88699b2910c..df753c4b2975ba 100644 --- a/test/parallel/test-blob.js +++ b/test/parallel/test-blob.js @@ -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()); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index c0ba01d3891477..c75ee390dcd195 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -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', diff --git a/test/parallel/test-process-get-builtin.mjs b/test/parallel/test-process-get-builtin.mjs index 3cf8179f7286bb..b376e1b88f905a 100644 --- a/test/parallel/test-process-get-builtin.mjs +++ b/test/parallel/test-process-get-builtin.mjs @@ -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)); diff --git a/test/parallel/test-quic-handshake.js b/test/parallel/test-quic-handshake.js new file mode 100644 index 00000000000000..451c6bdd982e2f --- /dev/null +++ b/test/parallel/test-quic-handshake.js @@ -0,0 +1,77 @@ +// Flags: --experimental-quic --no-warnings +'use strict'; + +const { hasQuic } = require('../common'); +const { Buffer } = require('node:buffer'); + +const { + describe, + it, +} = require('node:test'); + +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: !hasQuic }, 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'); + }); +}); diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/parallel/test-quic-internal-endpoint-listen-defaults.js index 5bf4d69d1e5786..d5a96c252298f2 100644 --- a/test/parallel/test-quic-internal-endpoint-listen-defaults.js +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.js @@ -11,43 +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(); - 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), { - code: 'ERR_INVALID_STATE', + await rejects(listen(123, { keys, certs, endpoint }), { + code: 'ERR_INVALID_ARG_TYPE', }); - throws(() => endpoint.listen(() => {}, 123), { + await rejects(listen(() => {}, 123), { code: 'ERR_INVALID_ARG_TYPE', }); - endpoint.listen(() => {}); - throws(() => endpoint.listen(() => {}), { + 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); @@ -63,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; }, { diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js index 2cce05bc4c0b3e..db8b13fe4bdb10 100644 --- a/test/parallel/test-quic-internal-endpoint-options.js +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -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,8 +163,8 @@ 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); } } }); diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js index 62b42169909d64..0565eaa979a3ed 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.js +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -11,15 +11,22 @@ const { describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { const { QuicEndpoint, - QuicStreamState, - QuicStreamStats, + } = require('internal/quic/quic'); + + const { QuicSessionState, + QuicStreamState, + } = require('internal/quic/state'); + + const { QuicSessionStats, - } = require('internal/quic/quic'); + 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,24 +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](); - strictEqual(endpoint.state.isBound, undefined); + 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' }); @@ -149,9 +156,7 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { 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); @@ -161,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); @@ -195,17 +199,14 @@ describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { 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'); diff --git a/test/parallel/test-require-resolve.js b/test/parallel/test-require-resolve.js index b69192635e6d79..93896486ff5dbe 100644 --- a/test/parallel/test-require-resolve.js +++ b/test/parallel/test-require-resolve.js @@ -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); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index f4b1c8d3ce5d8c..02a0dfcbcda525 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -285,14 +285,11 @@ const customTypesMap = { 'Disposable': 'https://tc39.es/proposal-explicit-resource-management/#sec-disposable-interface', 'quic.QuicEndpoint': 'quic.html#class-quicendpoint', - 'quic.QuicEndpointState': 'quic.html#class-quicendpointstate', - 'quic.QuicEndpointStats': 'quic.html#class-quicendpointstats', + 'quic.QuicEndpoint.Stats': 'quic.html#class-quicendpointstats', 'quic.QuicSession': 'quic.html#class-quicsession', - 'quic.QuicSessionState': 'quic.html#class-quicsessionstate', - 'quic.QuicSessionStats': 'quic.html#class-quicsessionstats', + 'quic.QuicSession.Stats': 'quic.html#class-quicsessionstats', 'quic.QuicStream': 'quic.html#class-quicstream', - 'quic.QuicStreamState': 'quic.html#class-quicstreamstate', - 'quic.QuicStreamStats': 'quic.html#class-quicstreamstats', + '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',