From fd1f2d08820693f5c81b6d0534e9b450dd822539 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 1 Jun 2024 20:11:58 -0700 Subject: [PATCH 1/2] quic: start adding in the internal quic js api While the external API for QUIC is expected to be the WebTransport API primarily, this provides the internal API for QUIC that aligns with the native C++ QUIC components. --- .nycrc | 2 +- doc/api/errors.md | 36 + lib/internal/errors.js | 3 + lib/internal/quic/quic.js | 2548 +++++++++++++++++ src/node_builtins.cc | 3 + src/quic/endpoint.cc | 23 + src/quic/session.cc | 15 +- src/quic/tlscontext.cc | 2 +- ...-quic-internal-endpoint-listen-defaults.js | 126 +- .../test-quic-internal-endpoint-options.js | 415 +-- ...test-quic-internal-endpoint-stats-state.js | 312 +- .../test-quic-internal-setcallbacks.js | 75 +- 12 files changed, 3187 insertions(+), 373 deletions(-) create mode 100644 lib/internal/quic/quic.js diff --git a/.nycrc b/.nycrc index 42aad37414a2ee..1ca428df1c8ef8 100644 --- a/.nycrc +++ b/.nycrc @@ -4,7 +4,7 @@ "test/**", "tools/**", "benchmark/**", - "deps/**" + "deps/**", ], "reporter": [ "html", diff --git a/doc/api/errors.md b/doc/api/errors.md index 10b0aea8754da1..a6aaa57d924f26 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3644,6 +3644,42 @@ removed: v10.0.0 The `node:repl` module was unable to parse data from the REPL history file. + + +### `ERR_QUIC_CONNECTION_FAILED` + + + +> Stability: 1 - Experimental + +Establishing a QUIC connection failed. + + + +### `ERR_QUIC_ENDPOINT_CLOSED` + + + +> Stability: 1 - Experimental + +A QUIC Endpoint closed with an error. + + + +### `ERR_QUIC_OPEN_STREAM_FAILED` + + + +> Stability: 1 - Experimental + +Opening a QUIC stream failed. + ### `ERR_SOCKET_CANNOT_SEND` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 2869369262f18c..c7cb7715dbcaef 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1644,6 +1644,9 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => { E('ERR_PERFORMANCE_INVALID_TIMESTAMP', '%d is not a valid timestamp', TypeError); E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); +E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); +E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); +E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error); E('ERR_REQUIRE_ESM', function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) { diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js new file mode 100644 index 00000000000000..4d0077195d0629 --- /dev/null +++ b/lib/internal/quic/quic.js @@ -0,0 +1,2548 @@ +'use strict'; + +// TODO(@jasnell) Temporarily ignoring c8 covrerage for this file while tests +// are still being developed. +/* c8 ignore start */ + +const { + ArrayBuffer, + ArrayIsArray, + ArrayPrototypePush, + BigUint64Array, + DataView, + DataViewPrototypeGetBigInt64, + DataViewPrototypeGetBigUint64, + DataViewPrototypeGetUint8, + DataViewPrototypeSetUint8, + ObjectDefineProperties, + Symbol, + SymbolAsyncDispose, + Uint8Array, +} = primordials; + +const { + assertCrypto, + customInspectSymbol: kInspect, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); +assertCrypto(); + +const { + Endpoint: Endpoint_, + 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, + PREFERRED_ADDRESS_IGNORE, + PREFERRED_ADDRESS_USE, + DEFAULT_PREFERRED_ADDRESS_POLICY, + DEFAULT_CIPHERS, + DEFAULT_GROUPS, + STREAM_DIRECTION_BIDIRECTIONAL, + STREAM_DIRECTION_UNIDIRECTIONAL, + + // Internal constants for use by the implementation. + // These are not exposed to end users. + CLOSECONTEXT_CLOSE: kCloseContextClose, + CLOSECONTEXT_BIND_FAILURE: kCloseContextBindFailure, + CLOSECONTEXT_LISTEN_FAILURE: kCloseContextListenFailure, + CLOSECONTEXT_RECEIVE_FAILURE: kCloseContextReceiveFailure, + CLOSECONTEXT_SEND_FAILURE: kCloseContextSendFailure, + CLOSECONTEXT_START_FAILURE: kCloseContextStartFailure, + + // All of the IDX_STATS_* constants are the index positions of the stats + // fields in the relevant BigUint64Array's that underlie the *Stats objects. + // These are not exposed to end users. + IDX_STATS_ENDPOINT_CREATED_AT, + IDX_STATS_ENDPOINT_DESTROYED_AT, + IDX_STATS_ENDPOINT_BYTES_RECEIVED, + IDX_STATS_ENDPOINT_BYTES_SENT, + IDX_STATS_ENDPOINT_PACKETS_RECEIVED, + IDX_STATS_ENDPOINT_PACKETS_SENT, + IDX_STATS_ENDPOINT_SERVER_SESSIONS, + IDX_STATS_ENDPOINT_CLIENT_SESSIONS, + IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, + IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + + IDX_STATS_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, + IDX_STATS_SESSION_CWND, + IDX_STATS_SESSION_LATEST_RTT, + IDX_STATS_SESSION_MIN_RTT, + IDX_STATS_SESSION_RTTVAR, + IDX_STATS_SESSION_SMOOTHED_RTT, + IDX_STATS_SESSION_SSTHRESH, + IDX_STATS_SESSION_DATAGRAMS_RECEIVED, + IDX_STATS_SESSION_DATAGRAMS_SENT, + IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED, + IDX_STATS_SESSION_DATAGRAMS_LOST, + + IDX_STATS_STREAM_CREATED_AT, + IDX_STATS_STREAM_RECEIVED_AT, + IDX_STATS_STREAM_ACKED_AT, + IDX_STATS_STREAM_CLOSING_AT, + IDX_STATS_STREAM_DESTROYED_AT, + IDX_STATS_STREAM_BYTES_RECEIVED, + IDX_STATS_STREAM_BYTES_SENT, + IDX_STATS_STREAM_MAX_OFFSET, + IDX_STATS_STREAM_MAX_OFFSET_ACK, + IDX_STATS_STREAM_MAX_OFFSET_RECV, + IDX_STATS_STREAM_FINAL_SIZE, + + IDX_STATE_SESSION_PATH_VALIDATION, + IDX_STATE_SESSION_VERSION_NEGOTIATION, + IDX_STATE_SESSION_DATAGRAM, + IDX_STATE_SESSION_SESSION_TICKET, + IDX_STATE_SESSION_CLOSING, + 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, + IDX_STATE_SESSION_PRIORITY_SUPPORTED, + IDX_STATE_SESSION_WRAPPED, + IDX_STATE_SESSION_LAST_DATAGRAM_ID, + + IDX_STATE_ENDPOINT_BOUND, + IDX_STATE_ENDPOINT_RECEIVING, + IDX_STATE_ENDPOINT_LISTENING, + IDX_STATE_ENDPOINT_CLOSING, + IDX_STATE_ENDPOINT_BUSY, + IDX_STATE_ENDPOINT_PENDING_CALLBACKS, + + IDX_STATE_STREAM_ID, + IDX_STATE_STREAM_FIN_SENT, + IDX_STATE_STREAM_FIN_RECEIVED, + IDX_STATE_STREAM_READ_ENDED, + IDX_STATE_STREAM_WRITE_ENDED, + IDX_STATE_STREAM_DESTROYED, + IDX_STATE_STREAM_PAUSED, + IDX_STATE_STREAM_RESET, + IDX_STATE_STREAM_HAS_READER, + IDX_STATE_STREAM_WANTS_BLOCK, + IDX_STATE_STREAM_WANTS_HEADERS, + IDX_STATE_STREAM_WANTS_RESET, + IDX_STATE_STREAM_WANTS_TRAILERS, +} = internalBinding('quic'); + +const { + isArrayBuffer, + isArrayBufferView, +} = require('util/types'); + +const { + Buffer, +} = require('buffer'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_STATE, + ERR_QUIC_CONNECTION_FAILED, + ERR_QUIC_ENDPOINT_CLOSED, + ERR_QUIC_OPEN_STREAM_FAILED, + }, +} = require('internal/errors'); + +const { + InternalSocketAddress, + SocketAddress, + kHandle: kSocketAddressHandle, +} = require('internal/socketaddress'); + +const { + isKeyObject, + isCryptoKey, +} = require('internal/crypto/keys'); + +const { + kHandle: kKeyObjectHandle, + kKeyObject: kKeyObjectInner, +} = require('internal/crypto/util'); + +const { + validateFunction, + validateObject, +} = require('internal/validators'); + +const kEmptyObject = { __proto__: null }; +const kEnumerable = { __proto__: null, enumerable: true }; + +const kBlocked = Symbol('kBlocked'); +const kDatagram = Symbol('kDatagram'); +const kDatagramStatus = Symbol('kDatagramStatus'); +const kError = Symbol('kError'); +const kFinishClose = Symbol('kFinishClose'); +const kHandshake = Symbol('kHandshake'); +const kHeaders = Symbol('kHeaders'); +const kOwner = Symbol('kOwner'); +const kNewSession = Symbol('kNewSession'); +const kNewStream = Symbol('kNewStream'); +const kPathValidation = Symbol('kPathValidation'); +const kReset = Symbol('kReset'); +const kSessionTicket = Symbol('kSessionTicket'); +const kTrailers = Symbol('kTrailers'); +const kVersionNegotiation = Symbol('kVersionNegotiation'); + +/** + * @typedef {import('../socketaddress.js').SocketAddress} SocketAddress + * @typedef {import('../crypto/keys.js').KeyObject} KeyObject + * @typedef {import('../crypto/keys.js').CryptoKey} CryptoKey + */ + +/** + * @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 {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 (range 0.0-1.0) + * @property {number} [txDiagnosticLoss] The transmit diagnostic loss (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} 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 + */ + +/** + * @typedef {object} TransportParams + * @property {SocketAddress} [preferredAddressIpv4] The preferred IPv4 address + * @property {SocketAddress} [preferredAddressIpv6] The preferred IPv6 address + * @property {bigint|number} [initialMaxStreamDataBidiLocal] The initial maximum stream data bidirectional local + * @property {bigint|number} [initialMaxStreamDataBidiRemote] The initial maximum stream data bidirectional remote + * @property {bigint|number} [initialMaxStreamDataUni] The initial maximum stream data unidirectional + * @property {bigint|number} [initialMaxData] The initial maximum data + * @property {bigint|number} [initialMaxStreamsBidi] The initial maximum streams bidirectional + * @property {bigint|number} [initialMaxStreamsUni] The initial maximum streams unidirectional + * @property {bigint|number} [maxIdleTimeout] The maximum idle timeout + * @property {bigint|number} [activeConnectionIDLimit] The active connection ID limit + * @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 + */ + +/** + * @typedef {object} ApplicationOptions + * @property {bigint|number} [maxHeaderPairs] The maximum header pairs + * @property {bigint|number} [maxHeaderLength] The maximum header length + * @property {bigint|number} [maxFieldSectionSize] The maximum field section size + * @property {bigint|number} [qpackMaxDTableCapacity] The qpack maximum dynamic table capacity + * @property {bigint|number} [qpackEncoderMaxDTableCapacity] The qpack encoder maximum dynamic table capacity + * @property {bigint|number} [qpackBlockedStreams] The qpack blocked streams + * @property {boolean} [enableConnectProtocol] Enable the connect protocol + * @property {boolean} [enableDatagrams] Enable datagrams + */ + +/** + * @typedef {object} SessionOptions + * @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 {boolean} [qlog] Enable qlog + * @property {ArrayBufferView} [sessionTicket] The session ticket + */ + +/** + * @typedef {object} Datagrams + * @property {ReadableStream} readable The readable stream + * @property {WritableStream} writable The writable stream + */ + +/** + * @typedef {object} Path + * @property {SocketAddress} local The local address + * @property {SocketAddress} remote The remote address + */ + +/** + * Called when the Endpoint receives a new server-side Session. + * @callback OnSessionCallback + * @param {Session} session + * @param {Endpoint} endpoint + * @returns {void} + */ + +/** + * @callback OnStreamCallback + * @param {QuicStream} stream + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnDatagramCallback + * @param {Uint8Array} datagram + * @param {Session} session + * @param {boolean} early + * @returns {void} + */ + +/** + * @callback OnDatagramStatusCallback + * @param {bigint} id + * @param {'lost'|'acknowledged'} status + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnPathValidationCallback + * @param {'aborted'|'failure'|'success'} result + * @param {SocketAddress} newLocalAddress + * @param {SocketAddress} newRemoteAddress + * @param {SocketAddress} oldLocalAddress + * @param {SocketAddress} oldRemoteAddress + * @param {boolean} preferredAddress + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnSessionTicketCallback + * @param {object} ticket + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnVersionNegotiationCallback + * @param {number} version + * @param {number[]} requestedVersions + * @param {number[]} supportedVersions + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnHandshakeCallback + * @param {string} sni + * @param {string} alpn + * @param {string} cipher + * @param {string} cipherVersion + * @param {string} validationErrorReason + * @param {number} validationErrorCode + * @param {boolean} earlyDataAccepted + * @param {Session} session + * @returns {void} + */ + +/** + * @callback OnBlockedCallback + * @param {QuicStream} stream + * @returns {void} + */ + +/** + * @callback OnStreamErrorCallback + * @param {any} error + * @param {QuicStream} stream + * @returns {void} + */ + +/** + * @callback OnHeadersCallback + * @param {object} headers + * @param {string} kind + * @param {QuicStream} stream + * @returns {void} + */ + +/** + * @callback OnTrailersCallback + * @param {QuicStream} stream + * @returns {void} + */ + +/** + * Provdes the callback configuration for Sessions. + * @typedef {object} SessionCallbackConfiguration + * @property {OnStreamCallback} onstream The stream callback + * @property {OnDatagramCallback} [ondatagram] The datagram callback + * @property {OnDatagramStatusCallback} [ondatagramstatus] The datagram status callback + * @property {OnPathValidationCallback} [onpathvalidation] The path validation callback + * @property {OnSessionTicketCallback} [onsessionticket] The session ticket callback + * @property {OnVersionNegotiationCallback} [onversionnegotiation] The version negotiation callback + * @property {OnHandshakeCallback} [onhandshake] The handshake callback + */ + +/** + * @typedef {object} StreamCallbackConfiguration + * @property {OnStreamErrorCallback} onerror The error callback + * @property {OnBlockedCallback} [onblocked] The blocked callback + * @property {OnStreamErrorCallback} [onreset] The reset callback + * @property {OnHeadersCallback} [onheaders] The headers callback + * @property {OnTrailersCallback} [ontrailers] The trailers callback + */ + +/** + * Provides the callback configuration for the Endpoint. + * @typedef {object} EndpointCallbackConfiguration + * @property {OnSessionCallback} onsession The session callback + * @property {SessionCallbackConfiguration} session The session callback configuration + * @property {StreamCallbackConfiguration} stream The stream callback configuration + */ + +setCallbacks({ + onEndpointClose(context, status) { + this[kOwner][kFinishClose](context, status); + }, + onSessionNew(session) { + this[kOwner][kNewSession](session); + }, + onSessionClose(errorType, code, reason) { + this[kOwner][kFinishClose](errorType, code, reason); + }, + onSessionDatagram(uint8Array, early) { + this[kOwner][kDatagram](uint8Array, early); + }, + onSessionDatagramStatus(id, status) { + this[kOwner][kDatagramStatus](id, status); + }, + onSessionHandshake(sni, alpn, cipher, cipherVersion, + validationErrorReason, + validationErrorCode, + earlyDataAccepted) { + this[kOwner][kHandshake](sni, alpn, cipher, cipherVersion, + validationErrorReason, validationErrorCode, + earlyDataAccepted); + }, + onSessionPathValidation(...args) { + this[kOwner][kPathValidation](...args); + }, + onSessionTicket(ticket) { + this[kOwner][kSessionTicket](ticket); + }, + onSessionVersionNegotiation(version, + requestedVersions, + supportedVersions) { + this[kOwner][kVersionNegotiation](version, requestedVersions, supportedVersions); + // Note that immediately following a version negotiation event, the + // session will be destroyed. + }, + onStreamCreated(stream, direction) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) { + stream.destroy(); + return; + }; + session[kNewStream](stream, direction); + }, + onStreamBlocked() { + this[kOwner][kBlocked](); + }, + onStreamClose(error) { + this[kOwner][kError](error); + }, + onStreamReset(error) { + this[kOwner][kReset](error); + }, + onStreamHeaders(headers, kind) { + this[kOwner][kHeaders](headers, kind); + }, + onStreamTrailers() { + this[kOwner][kTrailers](); + }, +}); + +/** + * @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 {TlsOptions} tls + */ +function processTlsOptions(tls) { + validateObject(tls, 'options.tls'); + const { + sni, + alpn, + ciphers, + groups, + keylog, + verifyClient, + tlsTrace, + verifyPrivateKey, + keys, + certs, + ca, + crl, + } = tls; + + 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 { + sni, + alpn, + ciphers, + groups, + keylog, + verifyClient, + tlsTrace, + verifyPrivateKey, + keys: keyHandles, + certs, + ca, + crl, + }; +} + +class QuicStreamStats { + /** @type {BigUint64Array} */ + #handle; + + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new BigUint64Array(buffer); + } + + /** @type {bigint} */ + get createdAt() { + return this.#handle[IDX_STATS_STREAM_CREATED_AT]; + } + + /** @type {bigint} */ + get receivedAt() { + return this.#handle[IDX_STATS_STREAM_RECEIVED_AT]; + } + + /** @type {bigint} */ + get ackedAt() { + return this.#handle[IDX_STATS_STREAM_ACKED_AT]; + } + + /** @type {bigint} */ + get closingAt() { + return this.#handle[IDX_STATS_STREAM_CLOSING_AT]; + } + + /** @type {bigint} */ + get destroyedAt() { + return this.#handle[IDX_STATS_STREAM_DESTROYED_AT]; + } + + /** @type {bigint} */ + get bytesReceived() { + return this.#handle[IDX_STATS_STREAM_BYTES_RECEIVED]; + } + + /** @type {bigint} */ + get bytesSent() { + return this.#handle[IDX_STATS_STREAM_BYTES_SENT]; + } + + /** @type {bigint} */ + get maxOffset() { + return this.#handle[IDX_STATS_STREAM_MAX_OFFSET]; + } + + /** @type {bigint} */ + get maxOffsetAcknowledged() { + return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK]; + } + + /** @type {bigint} */ + get maxOffsetReceived() { + return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV]; + } + + /** @type {bigint} */ + get finalSize() { + return this.#handle[IDX_STATS_STREAM_FINAL_SIZE]; + } + + toJSON() { + return { + __proto__: null, + createdAt: `${this.createdAt}`, + receivedAt: `${this.receivedAt}`, + ackedAt: `${this.ackedAt}`, + closingAt: `${this.closingAt}`, + destroyedAt: `${this.destroyedAt}`, + bytesReceived: `${this.bytesReceived}`, + bytesSent: `${this.bytesSent}`, + maxOffset: `${this.maxOffset}`, + maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`, + maxOffsetReceived: `${this.maxOffsetReceived}`, + finalSize: `${this.finalSize}`, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `StreamStats ${inspect({ + createdAt: this.createdAt, + receivedAt: this.receivedAt, + ackedAt: this.ackedAt, + closingAt: this.closingAt, + destroyedAt: this.destroyedAt, + bytesReceived: this.bytesReceived, + bytesSent: this.bytesSent, + maxOffset: this.maxOffset, + maxOffsetAcknowledged: this.maxOffsetAcknowledged, + maxOffsetReceived: this.maxOffsetReceived, + finalSize: this.finalSize, + }, opts)}`; + } + + [kFinishClose]() { + // Snapshot the stats into a new BigUint64Array since the underlying + // buffer will be destroyed. + this.#handle = new BigUint64Array(this.#handle); + } +} + +class QuicStreamState { + /** @type {DataView} */ + #handle; + + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new DataView(buffer); + } + + /** @type {bigint} */ + get id() { + return DataViewPrototypeGetBigInt64(this.#handle, IDX_STATE_STREAM_ID); + } + + /** @type {boolean} */ + get finSent() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_SENT); + } + + /** @type {boolean} */ + get finReceived() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_FIN_RECEIVED); + } + + /** @type {boolean} */ + get readEnded() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_READ_ENDED); + } + + /** @type {boolean} */ + get writeEnded() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WRITE_ENDED); + } + + /** @type {boolean} */ + get destroyed() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_DESTROYED); + } + + /** @type {boolean} */ + get paused() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_PAUSED); + } + + /** @type {boolean} */ + get reset() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_RESET); + } + + /** @type {boolean} */ + get hasReader() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_HAS_READER); + } + + /** @type {boolean} */ + get wantsBlock() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK); + } + + /** @type {boolean} */ + set wantsBlock(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsHeaders() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS); + } + + /** @type {boolean} */ + set wantsHeaders(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsReset() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET); + } + + /** @type {boolean} */ + set wantsReset(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsTrailers() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS); + } + + /** @type {boolean} */ + set wantsTrailers(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); + } + + toJSON() { + return { + __proto__: null, + id: `${this.id}`, + finSent: this.finSent, + finReceived: this.finReceived, + readEnded: this.readEnded, + writeEnded: this.writeEnded, + destroyed: this.destroyed, + paused: this.paused, + reset: this.reset, + hasReader: this.hasReader, + wantsBlock: this.wantsBlock, + wantsHeaders: this.wantsHeaders, + wantsReset: this.wantsReset, + wantsTrailers: this.wantsTrailers, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `StreamState ${inspect({ + id: this.id, + finSent: this.finSent, + finReceived: this.finReceived, + readEnded: this.readEnded, + writeEnded: this.writeEnded, + destroyed: this.destroyed, + paused: this.paused, + reset: this.reset, + hasReader: this.hasReader, + wantsBlock: this.wantsBlock, + wantsHeaders: this.wantsHeaders, + wantsReset: this.wantsReset, + wantsTrailers: this.wantsTrailers, + }, opts)}`; + } + + [kFinishClose]() { + // When the stream is destroyed, the state is no longer available. + this.#handle = new DataView(new ArrayBuffer(0)); + } +} + +class QuicStream { + /** @type {object} */ + #handle; + /** @type {Session} */ + #session; + /** @type {QuicStreamStats} */ + #stats; + /** @type {QuicStreamState} */ + #state; + /** @type {number} */ + #direction; + /** @type {OnStreamErrorCallback} */ + #onerror; + /** @type {OnBlockedCallback|undefined} */ + #onblocked; + /** @type {OnStreamErrorCallback|undefined} */ + #onreset; + /** @type {OnHeadersCallback|undefined} */ + #onheaders; + /** @type {OnTrailersCallback|undefined} */ + #ontrailers; + + /** + * @param {StreamCallbackConfiguration} config + * @param {object} handle + * @param {Session} session + */ + constructor(config, handle, session, direction) { + validateObject(config, 'config'); + this.#stats = new QuicStreamStats(handle.stats); + this.#state = new QuicStreamState(handle.stats); + const { + onblocked, + onerror, + onreset, + onheaders, + ontrailers, + } = config; + if (onblocked !== undefined) { + validateFunction(onblocked, 'config.onblocked'); + this.#state.wantsBlock = true; + this.#onblocked = onblocked; + } + if (onreset !== undefined) { + validateFunction(onreset, 'config.onreset'); + this.#state.wantsReset = true; + this.#onreset = onreset; + } + if (onheaders !== undefined) { + validateFunction(onheaders, 'config.onheaders'); + this.#state.wantsHeaders = true; + this.#onheaders = onheaders; + } + if (ontrailers !== undefined) { + validateFunction(ontrailers, 'config.ontrailers'); + this.#state.wantsTrailers = true; + this.#ontrailers = ontrailers; + } + validateFunction(onerror, 'config.onerror'); + this.#onerror = onerror; + this.#handle = handle; + this.#session = session; + this.#direction = direction; + this.#handle[kOwner] = true; + } + + /** @type {QuicStreamStats} */ + get stats() { return this.#stats; } + + /** @type {QuicStreamState} */ + get state() { return this.#state; } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {bigint} */ + get id() { return this.#state.id; } + + /** @type {'bidi'|'uni'} */ + get direction() { + return this.#direction === 0 ? 'bidi' : 'uni'; + } + + [kBlocked]() { + this.#onblocked(this); + } + + [kError](error) { + this.#onerror(error, this); + } + + [kReset](error) { + this.#onreset(error, this); + } + + [kHeaders](headers, kind) { + this.#onheaders(headers, kind, this); + } + + [kTrailers]() { + this.#ontrailers(this); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `Stream ${inspect({ + id: this.id, + direction: this.direction, + stats: this.stats, + state: this.state, + session: this.session, + }, opts)}`; + } +} + +class SessionStats { + /** @type {BigUint64Array} */ + #handle; + + /** + * @param {BigUint64Array} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new BigUint64Array(buffer); + } + + /** @type {bigint} */ + get createdAt() { + return this.#handle[IDX_STATS_SESSION_CREATED_AT]; + } + + /** @type {bigint} */ + get closingAt() { + 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]; + } + + /** @type {bigint} */ + get handshakeConfirmedAt() { + 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]; + } + + /** @type {bigint} */ + get bytesSent() { + return this.#handle[IDX_STATS_SESSION_BYTES_SENT]; + } + + /** @type {bigint} */ + get bidiInStreamCount() { + return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; + } + + /** @type {bigint} */ + get bidiOutStreamCount() { + return this.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; + } + + /** @type {bigint} */ + get uniInStreamCount() { + return this.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; + } + + /** @type {bigint} */ + get uniOutStreamCount() { + 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]; + } + + /** @type {bigint} */ + get bytesInFlight() { + return this.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT]; + } + + /** @type {bigint} */ + get blockCount() { + return this.#handle[IDX_STATS_SESSION_BLOCK_COUNT]; + } + + /** @type {bigint} */ + get cwnd() { + return this.#handle[IDX_STATS_SESSION_CWND]; + } + + /** @type {bigint} */ + get latestRtt() { + return this.#handle[IDX_STATS_SESSION_LATEST_RTT]; + } + + /** @type {bigint} */ + get minRtt() { + return this.#handle[IDX_STATS_SESSION_MIN_RTT]; + } + + /** @type {bigint} */ + get rttVar() { + return this.#handle[IDX_STATS_SESSION_RTTVAR]; + } + + /** @type {bigint} */ + get smoothedRtt() { + return this.#handle[IDX_STATS_SESSION_SMOOTHED_RTT]; + } + + /** @type {bigint} */ + get ssthresh() { + return this.#handle[IDX_STATS_SESSION_SSTHRESH]; + } + + /** @type {bigint} */ + get datagramsReceived() { + return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; + } + + /** @type {bigint} */ + get datagramsSent() { + return this.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT]; + } + + /** @type {bigint} */ + get datagramsAcknowledged() { + return this.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; + } + + /** @type {bigint} */ + get datagramsLost() { + return this.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST]; + } + + toJSON() { + return { + __proto__: null, + createdAt: `${this.createdAt}`, + closingAt: `${this.closingAt}`, + destroyedAt: `${this.destroyedAt}`, + handshakeCompletedAt: `${this.handshakeCompletedAt}`, + handshakeConfirmedAt: `${this.handshakeConfirmedAt}`, + gracefulClosingAt: `${this.gracefulClosingAt}`, + bytesReceived: `${this.bytesReceived}`, + bytesSent: `${this.bytesSent}`, + bidiInStreamCount: `${this.bidiInStreamCount}`, + bidiOutStreamCount: `${this.bidiOutStreamCount}`, + uniInStreamCount: `${this.uniInStreamCount}`, + uniOutStreamCount: `${this.uniOutStreamCount}`, + lossRetransmitCount: `${this.lossRetransmitCount}`, + maxBytesInFlights: `${this.maxBytesInFlights}`, + bytesInFlight: `${this.bytesInFlight}`, + blockCount: `${this.blockCount}`, + cwnd: `${this.cwnd}`, + latestRtt: `${this.latestRtt}`, + minRtt: `${this.minRtt}`, + rttVar: `${this.rttVar}`, + smoothedRtt: `${this.smoothedRtt}`, + ssthresh: `${this.ssthresh}`, + datagramsReceived: `${this.datagramsReceived}`, + datagramsSent: `${this.datagramsSent}`, + datagramsAcknowledged: `${this.datagramsAcknowledged}`, + datagramsLost: `${this.datagramsLost}`, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `SessionStats ${inspect({ + createdAt: this.createdAt, + closingAt: this.closingAt, + destroyedAt: this.destroyedAt, + handshakeCompletedAt: this.handshakeCompletedAt, + handshakeConfirmedAt: this.handshakeConfirmedAt, + gracefulClosingAt: this.gracefulClosingAt, + bytesReceived: this.bytesReceived, + bytesSent: this.bytesSent, + bidiInStreamCount: this.bidiInStreamCount, + bidiOutStreamCount: this.bidiOutStreamCount, + uniInStreamCount: this.uniInStreamCount, + uniOutStreamCount: this.uniOutStreamCount, + lossRetransmitCount: this.lossRetransmitCount, + maxBytesInFlights: this.maxBytesInFlights, + bytesInFlight: this.bytesInFlight, + blockCount: this.blockCount, + cwnd: this.cwnd, + latestRtt: this.latestRtt, + minRtt: this.minRtt, + rttVar: this.rttVar, + smoothedRtt: this.smoothedRtt, + ssthresh: this.ssthresh, + datagramsReceived: this.datagramsReceived, + datagramsSent: this.datagramsSent, + datagramsAcknowledged: this.datagramsAcknowledged, + datagramsLost: this.datagramsLost, + }, opts)}`; + } + + [kFinishClose]() { + // Snapshot the stats into a new BigUint64Array since the underlying + // buffer will be destroyed. + this.#handle = new BigUint64Array(this.#handle); + } +} + +class SessionState { + /** @type {DataView} */ + #handle; + + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new DataView(buffer); + } + + /** @type {boolean} */ + get hasPathValidationListener() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION); + } + + /** @type {boolean} */ + set hasPathValidationListener(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasVersionNegotiationListener() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION); + } + + /** @type {boolean} */ + set hasVersionNegotiationListener(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasDatagramListener() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM); + } + + /** @type {boolean} */ + set hasDatagramListener(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasSessionTicketListener() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET); + } + + /** @type {boolean} */ + set hasSessionTicketListener(val) { + DataViewPrototypeSetUint8(this.#handle, IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0); + } + + /** @type {boolean} */ + get isClosing() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_CLOSING); + } + + /** @type {boolean} */ + get isGracefulClose() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_GRACEFUL_CLOSE); + } + + /** @type {boolean} */ + get isSilentClose() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_SILENT_CLOSE); + } + + /** @type {boolean} */ + get isStatelessReset() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STATELESS_RESET); + } + + /** @type {boolean} */ + get isDestroyed() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_DESTROYED); + } + + /** @type {boolean} */ + get isHandshakeCompleted() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_COMPLETED); + } + + /** @type {boolean} */ + get isHandshakeConfirmed() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_HANDSHAKE_CONFIRMED); + } + + /** @type {boolean} */ + get isStreamOpenAllowed() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_STREAM_OPEN_ALLOWED); + } + + /** @type {boolean} */ + get isPrioritySupported() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_PRIORITY_SUPPORTED); + } + + /** @type {boolean} */ + get isWrapped() { + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_SESSION_WRAPPED); + } + + /** @type {bigint} */ + get lastDatagramId() { + return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_SESSION_LAST_DATAGRAM_ID); + } + + toJSON() { + return { + __proto__: null, + hasPathValidationListener: this.hasPathValidationListener, + hasVersionNegotiationListener: this.hasVersionNegotiationListener, + hasDatagramListener: this.hasDatagramListener, + hasSessionTicketListener: this.hasSessionTicketListener, + isClosing: this.isClosing, + isGracefulClose: this.isGracefulClose, + isSilentClose: this.isSilentClose, + isStatelessReset: this.isStatelessReset, + isDestroyed: this.isDestroyed, + isHandshakeCompleted: this.isHandshakeCompleted, + isHandshakeConfirmed: this.isHandshakeConfirmed, + isStreamOpenAllowed: this.isStreamOpenAllowed, + isPrioritySupported: this.isPrioritySupported, + isWrapped: this.isWrapped, + lastDatagramId: `${this.lastDatagramId}`, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `SessionState ${inspect({ + hasPathValidationListener: this.hasPathValidationListener, + hasVersionNegotiationListener: this.hasVersionNegotiationListener, + hasDatagramListener: this.hasDatagramListener, + hasSessionTicketListener: this.hasSessionTicketListener, + isClosing: this.isClosing, + isGracefulClose: this.isGracefulClose, + isSilentClose: this.isSilentClose, + isStatelessReset: this.isStatelessReset, + isDestroyed: this.isDestroyed, + isHandshakeCompleted: this.isHandshakeCompleted, + isHandshakeConfirmed: this.isHandshakeConfirmed, + isStreamOpenAllowed: this.isStreamOpenAllowed, + isPrioritySupported: this.isPrioritySupported, + isWrapped: this.isWrapped, + lastDatagramId: this.lastDatagramId, + }, opts)}`; + } + + [kFinishClose]() { + // Snapshot the state into a new DataView since the underlying + // buffer will be destroyed. + this.#handle = new DataView(new ArrayBuffer(0)); + } +} + +class Session { + /** @type {Endpoint} */ + #endpoint = undefined; + /** @type {boolean} */ + #isPendingClose = false; + /** @type {object|undefined} */ + #handle; + /** @type {PromiseWithResolvers} */ + #pendingClose; + /** @type {SocketAddress|undefined} */ + #remoteAddress = undefined; + /** @type {SessionState} */ + #state; + /** @type {SessionStats} */ + #stats; + /** @type {QuicStream[]} */ + #streams = []; + /** @type {OnStreamCallback} */ + #onstream; + /** @type {OnDatagramCallback|undefined} */ + #ondatagram; + /** @type {OnDatagramStatusCallback|undefined} */ + #ondatagramstatus; + /** @type {OnPathValidationCallback|undefined} */ + #onpathvalidation; + /** @type {OnSessionTicketCallback|undefined} */ + #onsessionticket; + /** @type {OnVersionNegotiationCallback|undefined} */ + #onversionnegotiation; + /** @type {OnHandshakeCallback} */ + #onhandshake; + /** @type {StreamCallbackConfiguration} */ + #streamConfig; + + /** + * @param {SessionCallbackConfiguration} config + * @param {StreamCallbackConfiguration} streamConfig + * @param {object} [handle] + * @param {Endpoint} [endpoint] + */ + constructor(config, streamConfig, handle, endpoint) { + validateObject(config, 'config'); + this.#stats = new SessionStats(handle.stats); + this.#state = new SessionState(handle.state); + const { + ondatagram, + ondatagramstatus, + onhandshake, + onpathvalidation, + onsessionticket, + onstream, + onversionnegotiation, + } = config; + if (ondatagram !== undefined) { + validateFunction(ondatagram, 'config.ondatagram'); + validateFunction(ondatagramstatus, 'config.ondatagramstatus'); + this.#state.hasDatagramListener = true; + this.#ondatagram = ondatagram; + this.#ondatagramstatus = ondatagramstatus; + } + validateFunction(onhandshake, 'config.onhandshake'); + if (onpathvalidation !== undefined) { + validateFunction(onpathvalidation, 'config.onpathvalidation'); + this.#state.hasPathValidationListener = true; + this.#onpathvalidation = onpathvalidation; + } + if (onsessionticket !== undefined) { + validateFunction(onsessionticket, 'config.onsessionticket'); + this.#state.hasSessionTicketListener = true; + this.#onsessionticket = onsessionticket; + } + validateFunction(onstream, 'config.onstream'); + if (onversionnegotiation !== undefined) { + validateFunction(onversionnegotiation, 'config.onversionnegotiation'); + this.#state.hasVersionNegotiationListener = true; + this.#onversionnegotiation = onversionnegotiation; + } + this.#onhandshake = onhandshake; + this.#onstream = onstream; + this.#handle = handle; + this.#endpoint = endpoint; + this.#pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials + this.#handle[kOwner] = this; + } + + /** @type {boolean} */ + get #isClosedOrClosing() { + return this.#handle === undefined || this.#isPendingClose; + } + + /** @type {SessionStats} */ + get stats() { return this.#stats; } + + /** @type {SessionState} */ + get state() { return this.#state; } + + /** @type {Endpoint} */ + get endpoint() { return this.#endpoint; } + + /** @type {Path} */ + get path() { + if (this.#isClosedOrClosing) 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, + }; + } + + /** + * @returns {QuicStream} + */ + openBidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (!this.state.isStreamOpenAllowed) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const handle = this.#handle.openStream(STREAM_DIRECTION_BIDIRECTIONAL); + if (handle === undefined) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const stream = new QuicStream(this.#streamConfig, handle, this, 0); + ArrayPrototypePush(this.#streams, stream); + return stream; + } + + /** + * @returns {QuicStream} + */ + openUnidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (!this.state.isStreamOpenAllowed) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const handle = this.#handle.openStream(STREAM_DIRECTION_UNIDIRECTIONAL); + if (handle === undefined) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const stream = new QuicStream(this.#streamConfig, handle, this, 1); + ArrayPrototypePush(this.#streams, stream); + return stream; + } + + /** + * Initiate a key update. + */ + updateKey() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + this.#handle.updateKey(); + } + + /** + * Gracefully closes the session. Any streams created on the session will be + * allowed to complete gracefully and any datagrams that have already been + * queued for sending will be allowed to complete. Once all streams have been + * completed and all datagrams have been sent, the session will be closed. + * New streams will not be allowed to be created. The returned promise will + * be resolved when the session closes, or will be rejected if the session + * closes abruptly due to an error. + * @returns {Promise} + */ + close() { + if (!this.#isClosedOrClosing) { + this.#isPendingClose = true; + this.#handle?.gracefulClose(); + } + return this.closed; + } + + /** + * A promise that is resolved when the session is closed, or is rejected if + * the session is closed abruptly due to an error. + * @type {Promise} + */ + get closed() { return this.#pendingClose.promise; } + + /** @type {boolean} */ + get destroyed() { return this.#handle === undefined; } + + /** + * Forcefully closes the session abruptly without waiting for streams to be + * completed naturally. Any streams that are still open will be immediately + * destroyed and any queued datagrams will be dropped. If an error is given, + * the closed promise will be rejected with that error. If no error is given, + * the closed promise will be resolved. + * @param {any} error + */ + destroy(error) { + // TODO(@jasnell): Implement. + } + + /** + * Send a datagram. The id of the sent datagram will be returned. The status + * of the sent datagram will be reported via the datagram-status event if + * possible. + * + * If a string is given it will be encoded as UTF-8. + * + * If an ArrayBufferView is given, the view will be copied. + * @param {ArrayBufferView|string} datagram The datagram payload + * @returns {bigint} The datagram ID + */ + sendDatagram(datagram) { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (typeof datagram === 'string') { + datagram = Buffer.from(datagram, 'utf8'); + } else { + if (!isArrayBufferView(datagram)) { + throw new ERR_INVALID_ARG_TYPE('datagram', + ['ArrayBufferView', 'string'], + datagram); + } + datagram = new Uint8Array(datagram.buffer.transfer(), + datagram.byteOffset, + datagram.byteLength); + } + return this.#handle.sendDatagram(datagram); + } + + /** + * @param {Uint8Array} u8 + * @param {boolean} early + */ + [kDatagram](u8, early) { + if (this.destroyed) return; + this.#ondatagram(u8, this, early); + } + + /** + * @param {bigint} id + * @param {'lost'|'acknowledged'} status + */ + [kDatagramStatus](id, status) { + if (this.destroyed) return; + this.#ondatagramstatus(id, status, this); + } + + /** + * @param {'aborted'|'failure'|'success'} result + * @param {SocketAddress} newLocalAddress + * @param {SocketAddress} newRemoteAddress + * @param {SocketAddress} oldLocalAddress + * @param {SocketAddress} oldRemoteAddress + * @param {boolean} preferredAddress + */ + [kPathValidation](result, newLocalAddress, newRemoteAddress, oldLocalAddress, + oldRemoteAddress, preferredAddress) { + if (this.destroyed) return; + this.#onpathvalidation(result, newLocalAddress, newRemoteAddress, + oldLocalAddress, oldRemoteAddress, preferredAddress, + this); + } + + /** + * @param {object} ticket + */ + [kSessionTicket](ticket) { + if (this.destroyed) return; + this.#onsessionticket(ticket, this); + } + + /** + * @param {number} version + * @param {number[]} requestedVersions + * @param {number[]} supportedVersions + */ + [kVersionNegotiation](version, requestedVersions, supportedVersions) { + if (this.destroyed) return; + this.#onversionnegotiation(version, requestedVersions, supportedVersions, this); + } + + /** + * @param {string} sni + * @param {string} alpn + * @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; + this.#onhandshake(sni, alpn, cipher, cipherVersion, validationErrorReason, + validationErrorCode, earlyDataAccepted, this); + } + + /** + * @param {object} handle + * @param {number} direction + */ + [kNewStream](handle, direction) { + const stream = new QuicStream(this.#streamConfig, handle, this, direction); + ArrayPrototypePush(this.#streams, stream); + this.#onstream(stream, this); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `Session ${inspect({ + closed: this.closed, + closing: this.#isPendingClose, + destroyed: this.destroyed, + endpoint: this.endpoint, + path: this.path, + state: this.state, + stats: this.stats, + streams: this.#streams, + }, opts)}`; + } + + [kFinishClose](errorType, code, reason) { + // TODO(@jasnell): Finish the implementation + } + + async [SymbolAsyncDispose]() { await this.close(); } +} + +class EndpointStats { + /** @type {BigUint64Array} */ + #handle; + + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new BigUint64Array(buffer); + } + + /** @type {bigint} */ + get createdAt() { + return this.#handle[IDX_STATS_ENDPOINT_CREATED_AT]; + } + + /** @type {bigint} */ + get destroyedAt() { + return this.#handle[IDX_STATS_ENDPOINT_DESTROYED_AT]; + } + + /** @type {bigint} */ + get bytesReceived() { + return this.#handle[IDX_STATS_ENDPOINT_BYTES_RECEIVED]; + } + + /** @type {bigint} */ + get bytesSent() { + return this.#handle[IDX_STATS_ENDPOINT_BYTES_SENT]; + } + + /** @type {bigint} */ + get packetsReceived() { + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_RECEIVED]; + } + + /** @type {bigint} */ + get packetsSent() { + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_SENT]; + } + + /** @type {bigint} */ + get serverSessions() { + return this.#handle[IDX_STATS_ENDPOINT_SERVER_SESSIONS]; + } + + /** @type {bigint} */ + get clientSessions() { + return this.#handle[IDX_STATS_ENDPOINT_CLIENT_SESSIONS]; + } + + /** @type {bigint} */ + get serverBusyCount() { + return this.#handle[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT]; + } + + /** @type {bigint} */ + get retryCount() { + return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT]; + } + + /** @type {bigint} */ + get versionNegotiationCount() { + return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT]; + } + + /** @type {bigint} */ + get statelessResetCount() { + return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT]; + } + + /** @type {bigint} */ + get immediateCloseCount() { + return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT]; + } + + toJSON() { + return { + __proto__: null, + createdAt: `${this.createdAt}`, + destroyedAt: `${this.destroyedAt}`, + bytesReceived: `${this.bytesReceived}`, + bytesSent: `${this.bytesSent}`, + packetsReceived: `${this.packetsReceived}`, + packetsSent: `${this.packetsSent}`, + serverSessions: `${this.serverSessions}`, + clientSessions: `${this.clientSessions}`, + serverBusyCount: `${this.serverBusyCount}`, + retryCount: `${this.retryCount}`, + versionNegotiationCount: `${this.versionNegotiationCount}`, + statelessResetCount: `${this.statelessResetCount}`, + immediateCloseCount: `${this.immediateCloseCount}`, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `EndpointStats ${inspect({ + createdAt: this.createdAt, + destroyedAt: this.destroyedAt, + bytesReceived: this.bytesReceived, + bytesSent: this.bytesSent, + packetsReceived: this.packetsReceived, + packetsSent: this.packetsSent, + serverSessions: this.serverSessions, + clientSessions: this.clientSessions, + serverBusyCount: this.serverBusyCount, + retryCount: this.retryCount, + versionNegotiationCount: this.versionNegotiationCount, + statelessResetCount: this.statelessResetCount, + immediateCloseCount: this.immediateCloseCount, + }, opts)}`; + } + + [kFinishClose]() { + // Snapshot the stats into a new BigUint64Array since the underlying + // buffer will be destroyed. + this.#handle = new BigUint64Array(this.#handle); + } +} + +class EndpointState { + /** @type {DataView} */ + #handle; + + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + if (!isArrayBuffer(buffer)) { + throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer); + } + this.#handle = new DataView(buffer); + } + + #assertNotClosed() { + if (this.#handle.byteLength === 0) { + throw new ERR_INVALID_STATE('Endpoint is closed'); + } + } + + /** @type {boolean} */ + get isBound() { + this.#assertNotClosed(); + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BOUND); + } + + /** @type {boolean} */ + get isReceiving() { + this.#assertNotClosed(); + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_RECEIVING); + } + + /** @type {boolean} */ + get isListening() { + this.#assertNotClosed(); + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_LISTENING); + } + + /** @type {boolean} */ + get isClosing() { + this.#assertNotClosed(); + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_CLOSING); + } + + /** @type {boolean} */ + get isBusy() { + this.#assertNotClosed(); + return !!DataViewPrototypeGetUint8(this.#handle, IDX_STATE_ENDPOINT_BUSY); + } + + /** + * The number of underlying callbacks that are pending. If the session + * is closing, these are the number of callbacks that the session is + * waiting on before it can be closed. + * @type {bigint} + */ + get pendingCallbacks() { + this.#assertNotClosed(); + return DataViewPrototypeGetBigUint64(this.#handle, IDX_STATE_ENDPOINT_PENDING_CALLBACKS); + } + + toJSON() { + if (this.#handle.byteLength === 0) return {}; + return { + __proto__: null, + isBound: this.isBound, + isReceiving: this.isReceiving, + isListening: this.isListening, + isClosing: this.isClosing, + isBusy: this.isBusy, + pendingCallbacks: `${this.pendingCallbacks}`, + }; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + if (this.#handle.byteLength === 0) { + return 'EndpointState { }'; + } + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `EndpointState ${inspect({ + isBound: this.isBound, + isReceiving: this.isReceiving, + isListening: this.isListening, + isClosing: this.isClosing, + isBusy: this.isBusy, + pendingCallbacks: this.pendingCallbacks, + }, opts)}`; + } + + [kFinishClose]() { + // Snapshot the state into a new DataView since the underlying + // buffer will be destroyed. + if (this.#handle.byteLength === 0) return; + this.#handle = new DataView(new ArrayBuffer(0)); + } +} + +function validateStreamConfig(config, name = 'config') { + validateObject(config, name); + if (config.onerror !== undefined) + validateFunction(config.onerror, `${name}.onerror`); + if (config.onblocked !== undefined) + validateFunction(config.onblocked, `${name}.onblocked`); + if (config.onreset !== undefined) + validateFunction(config.onreset, `${name}.onreset`); + if (config.onheaders !== undefined) + validateFunction(config.onheaders, `${name}.onheaders`); + if (config.ontrailers !== undefined) + validateFunction(config.ontrailers, `${name}.ontrailers`); + return config; +} + +function validateSessionConfig(config, name = 'config') { + validateObject(config, name); + if (config.onstream !== undefined) + validateFunction(config.onstream, `${name}.onstream`); + if (config.ondatagram !== undefined) + validateFunction(config.ondatagram, `${name}.ondatagram`); + if (config.ondatagramstatus !== undefined) + validateFunction(config.ondatagramstatus, `${name}.ondatagramstatus`); + if (config.onpathvalidation !== undefined) + validateFunction(config.onpathvalidation, `${name}.onpathvalidation`); + if (config.onsessionticket !== undefined) + validateFunction(config.onsessionticket, `${name}.onsessionticket`); + if (config.onversionnegotiation !== undefined) + validateFunction(config.onversionnegotiation, `${name}.onversionnegotiation`); + if (config.onhandshake !== undefined) + validateFunction(config.onhandshake, `${name}.onhandshake`); + return config; +} + +function validateEndpointConfig(config) { + validateObject(config, 'config'); + validateFunction(config.onsession, 'config.onsession'); + if (config.session !== undefined) + validateSessionConfig(config.session, 'config.session'); + if (config.stream !== undefined) + validateStreamConfig(config.stream, 'config.stream'); + return config; +} + +class Endpoint { + /** @type {SocketAddress|undefined} */ + #address = undefined; + /** @type {boolean} */ + #busy = false; + /** @type {object} */ + #handle; + /** @type {boolean} */ + #isPendingClose = false; + /** @type {boolean} */ + #listening = false; + /** @type {PromiseWithResolvers} */ + #pendingClose; + /** @type {any} */ + #pendingError = undefined; + /** @type {Session[]} */ + #sessions = []; + /** @type {EndpointState} */ + #state; + /** @type {EndpointStats} */ + #stats; + /** @type {OnSessionCallback} */ + #onsession; + /** @type {SessionCallbackConfiguration} */ + #sessionConfig; + /** @type {StreamCallbackConfiguration */ + #streamConfig; + + /** + * @param {EndpointCallbackConfiguration} config + * @param {EndpointOptions} [options] + */ + constructor(config, options = kEmptyObject) { + validateEndpointConfig(config); + this.#onsession = config.onsession; + this.#sessionConfig = config.session; + this.#streamConfig = config.stream; + + validateObject(options, 'options'); + let { address } = options; + const { + retryTokenExpiration, + tokenExpiration, + maxConnectionsPerHost, + maxConnectionsTotal, + maxStatelessResetsPerHost, + addressLRUSize, + maxRetries, + maxPayloadSize, + unacknowledgedPacketThreshold, + handshakeTimeout, + maxStreamWindow, + maxWindow, + rxDiagnosticLoss, + txDiagnosticLoss, + udpReceiveBufferSize, + udpSendBufferSize, + udpTTL, + noUdpPayloadSizeShaping, + validateAddress, + disableActiveMigration, + ipv6Only, + cc, + resetTokenSecret, + tokenSecret, + } = options; + + // All of the other options will be validated internally by the C++ code + if (address !== undefined && !SocketAddress.isSocketAddress(address)) { + if (typeof address === 'object' && address !== null) { + address = new SocketAddress(address); + } else { + throw new ERR_INVALID_ARG_TYPE('options.address', 'SocketAddress', address); + } + } + + this.#pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials + this.#handle = new Endpoint_({ + __proto__: null, + address: address?.[kSocketAddressHandle], + retryTokenExpiration, + tokenExpiration, + maxConnectionsPerHost, + maxConnectionsTotal, + maxStatelessResetsPerHost, + addressLRUSize, + maxRetries, + maxPayloadSize, + unacknowledgedPacketThreshold, + handshakeTimeout, + maxStreamWindow, + maxWindow, + rxDiagnosticLoss, + txDiagnosticLoss, + udpReceiveBufferSize, + udpSendBufferSize, + udpTTL, + noUdpPayloadSizeShaping, + validateAddress, + disableActiveMigration, + ipv6Only, + cc, + resetTokenSecret, + tokenSecret, + }); + this.#handle[kOwner] = this; + this.#stats = new EndpointStats(this.#handle.stats); + this.#state = new EndpointState(this.#handle.state); + } + + /** @type {EndpointStats} */ + get stats() { return this.#stats; } + + /** @type {EndpointState} */ + get state() { return this.#state; } + + get #isClosedOrClosing() { + return this.#handle === undefined || this.#isPendingClose; + } + + /** + * When an endpoint is marked as busy, it will not accept new connections. + * Existing connections will continue to work. + * @type {boolean} + */ + get busy() { return this.#busy; } + + /** + * @type {boolean} + */ + set busy(val) { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Endpoint is closed'); + } + // The val is allowed to be any truthy value + val = !!val; + // Non-op if there is no change + if (val !== this.#busy) { + this.#busy = val; + this.#handle.markBusy(this.#busy); + } + } + + /** + * The local address the endpoint is bound to (if any) + * @type {SocketAddress|undefined} + */ + get address() { + if (this.#isClosedOrClosing) return undefined; + if (this.#address === undefined) { + const addr = this.#handle.address(); + if (addr !== undefined) this.#address = new InternalSocketAddress(addr); + } + return this.#address; + } + + /** + * Configures the endpoint to listen for incoming connections. + * @param {SessionOptions} [options] + */ + listen(options = kEmptyObject) { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Endpoint is closed'); + } + if (this.#listening) { + throw new ERR_INVALID_STATE('Endpoint is already listening'); + } + + validateObject(options, 'options'); + + const { + version, + minVersion, + preferredAddressPolicy = 'default', + application = kEmptyObject, + transportParams = kEmptyObject, + tls = kEmptyObject, + qlog = false, + } = options; + + this.#handle.listen({ + version, + minVersion, + preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy), + application, + transportParams, + tls: processTlsOptions(tls), + qlog, + }); + this.#listening = true; + } + + /** + * Initiates a session with a remote endpoint. + * @param {SocketAddress} address + * @param {SessionOptions} [options] + * @returns {Session} + */ + connect(address, options = kEmptyObject) { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Endpoint is closed'); + } + if (this.#busy) { + throw new ERR_INVALID_STATE('Endpoint is busy'); + } + + if (address === undefined || !SocketAddress.isSocketAddress(address)) { + if (typeof address === 'object' && address !== null) { + address = new SocketAddress(address); + } else { + throw new ERR_INVALID_ARG_TYPE('address', 'SocketAddress', address); + } + } + + validateObject(options, 'options'); + const { + version, + minVersion, + preferredAddressPolicy = 'default', + application = kEmptyObject, + transportParams = kEmptyObject, + tls = kEmptyObject, + qlog = false, + sessionTicket, + } = options; + + const handle = this.#handle.connect(address[kSocketAddressHandle], { + version, + minVersion, + preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy), + application, + transportParams, + tls: processTlsOptions(tls), + qlog, + }, sessionTicket); + + if (handle === undefined) { + throw new ERR_QUIC_CONNECTION_FAILED(); + } + const session = new Session(this.#sessionConfig, this.#streamConfig, handle, this); + ArrayPrototypePush(this.#sessions, session); + 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 + * due to an error. + * @returns {Promise} + */ + close() { + if (!this.#isClosedOrClosing) { + this.#isPendingClose = true; + this.#handle?.closeGracefully(); + } + return this.closed; + } + + /** + * Returns a promise that is resolved when the endpoint is closed or rejects + * if the endpoint is closed abruptly due to an error. The closed property + * is set to the same promise that is returned by the close() method. + * @type {Promise} + */ + get closed() { return this.#pendingClose.promise; } + + /** @type {boolean} */ + get destroyed() { return this.#handle === undefined; } + + /** + * Forcefully terminates the endpoint by immediately destroying all sessions + * after calling close. If an error is given, the closed promise will be + * rejected with that error. If no error is given, the closed promise will + * be resolved. + * @param {any} error + */ + destroy(error) { + 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... + this.close(); + } + // Now, force all sessions to be abruptly closed... + for (const session of this.#sessions) { + session.destroy(error); + } + } + + ref() { + if (this.#handle !== undefined) this.#handle.ref(true); + } + + unref() { + if (this.#handle !== undefined) this.#handle.ref(false); + } + + [kFinishClose](context, status) { + if (this.#handle === undefined) return; + this.#isPendingClose = false; + this.#address = undefined; + this.#busy = false; + this.#listening = false; + this.#isPendingClose = false; + this.#stats[kFinishClose](); + this.#state[kFinishClose](); + this.#sessions = []; + + switch (context) { + case kCloseContextClose: { + if (this.#pendingError !== undefined) { + this.#pendingClose.reject(this.#pendingError); + } else { + this.#pendingClose.resolve(); + } + break; + } + case kCloseContextBindFailure: { + this.#pendingClose.reject( + new ERR_QUIC_ENDPOINT_CLOSED('Bind failure', status)); + break; + } + case kCloseContextListenFailure: { + this.#pendingClose.reject( + new ERR_QUIC_ENDPOINT_CLOSED('Listen failure', status)); + break; + } + case kCloseContextReceiveFailure: { + this.#pendingClose.reject( + new ERR_QUIC_ENDPOINT_CLOSED('Receive failure', status)); + break; + } + case kCloseContextSendFailure: { + this.#pendingClose.reject( + new ERR_QUIC_ENDPOINT_CLOSED('Send failure', status)); + break; + } + case kCloseContextStartFailure: { + this.#pendingClose.reject( + new ERR_QUIC_ENDPOINT_CLOSED('Start failure', status)); + break; + } + } + + this.#pendingError = undefined; + this.#pendingClose.resolve = undefined; + this.#pendingClose.reject = undefined; + this.#handle = undefined; + } + + [kNewSession](handle) { + const session = new Session(this.#sessionConfig, this.#streamConfig, handle, this); + ArrayPrototypePush(this.#sessions, session); + this.#onsession(session, this); + } + + async [SymbolAsyncDispose]() { await this.close(); } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `Endpoint ${inspect({ + address: this.address, + busy: this.busy, + closed: this.closed, + closing: this.#isPendingClose, + destroyed: this.destroyed, + listening: this.#listening, + sessions: this.#sessions, + stats: this.stats, + state: this.state, + }, opts)}`; + } +}; + +ObjectDefineProperties(QuicStreamStats.prototype, { + createdAt: kEnumerable, + receivedAt: kEnumerable, + ackedAt: kEnumerable, + closingAt: kEnumerable, + destroyedAt: kEnumerable, + bytesReceived: kEnumerable, + bytesSent: kEnumerable, + maxOffset: kEnumerable, + maxOffsetAcknowledged: kEnumerable, + maxOffsetReceived: kEnumerable, + finalSize: kEnumerable, +}); +ObjectDefineProperties(QuicStreamState.prototype, { + id: kEnumerable, + finSent: kEnumerable, + finReceived: kEnumerable, + readEnded: kEnumerable, + writeEnded: kEnumerable, + destroyed: kEnumerable, + paused: kEnumerable, + reset: kEnumerable, + hasReader: kEnumerable, + wantsBlock: kEnumerable, + wantsHeaders: kEnumerable, + wantsReset: kEnumerable, + wantsTrailers: kEnumerable, +}); +ObjectDefineProperties(QuicStream.prototype, { + stats: kEnumerable, + state: kEnumerable, + session: kEnumerable, + id: kEnumerable, +}); +ObjectDefineProperties(SessionStats.prototype, { + createdAt: kEnumerable, + closingAt: kEnumerable, + destroyedAt: kEnumerable, + handshakeCompletedAt: kEnumerable, + handshakeConfirmedAt: kEnumerable, + gracefulClosingAt: kEnumerable, + bytesReceived: kEnumerable, + bytesSent: kEnumerable, + bidiInStreamCount: kEnumerable, + bidiOutStreamCount: kEnumerable, + uniInStreamCount: kEnumerable, + uniOutStreamCount: kEnumerable, + lossRetransmitCount: kEnumerable, + maxBytesInFlights: kEnumerable, + bytesInFlight: kEnumerable, + blockCount: kEnumerable, + cwnd: kEnumerable, + latestRtt: kEnumerable, + minRtt: kEnumerable, + rttVar: kEnumerable, + smoothedRtt: kEnumerable, + ssthresh: kEnumerable, + datagramsReceived: kEnumerable, + datagramsSent: kEnumerable, + datagramsAcknowledged: kEnumerable, + datagramsLost: kEnumerable, +}); +ObjectDefineProperties(SessionState.prototype, { + hasPathValidationListener: kEnumerable, + hasVersionNegotiationListener: kEnumerable, + hasDatagramListener: kEnumerable, + hasSessionTicketListener: kEnumerable, + isClosing: kEnumerable, + isGracefulClose: kEnumerable, + isSilentClose: kEnumerable, + isStatelessReset: kEnumerable, + isDestroyed: kEnumerable, + isHandshakeCompleted: kEnumerable, + isHandshakeConfirmed: kEnumerable, + isStreamOpenAllowed: kEnumerable, + isPrioritySupported: kEnumerable, + isWrapped: kEnumerable, + lastDatagramId: kEnumerable, +}); +ObjectDefineProperties(Session.prototype, { + closed: kEnumerable, + destroyed: kEnumerable, + endpoint: kEnumerable, + path: kEnumerable, + state: kEnumerable, + stats: kEnumerable, +}); +ObjectDefineProperties(EndpointStats.prototype, { + createdAt: kEnumerable, + destroyedAt: kEnumerable, + bytesReceived: kEnumerable, + bytesSent: kEnumerable, + packetsReceived: kEnumerable, + packetsSent: kEnumerable, + serverSessions: kEnumerable, + clientSessions: kEnumerable, + serverBusyCount: kEnumerable, + retryCount: kEnumerable, + versionNegotiationCount: kEnumerable, + statelessResetCount: kEnumerable, + immediateCloseCount: kEnumerable, +}); +ObjectDefineProperties(EndpointState.prototype, { + isBound: kEnumerable, + isReceiving: kEnumerable, + isListening: kEnumerable, + isClosing: kEnumerable, + isBusy: kEnumerable, + pendingCallbacks: kEnumerable, +}); +ObjectDefineProperties(Endpoint.prototype, { + address: kEnumerable, + busy: kEnumerable, + closed: kEnumerable, + destroyed: kEnumerable, + state: kEnumerable, + stats: kEnumerable, +}); +ObjectDefineProperties(Endpoint, { + CC_ALGO_RENO: { + __proto__: null, + value: CC_ALGO_RENO, + writable: false, + configurable: false, + enumerable: true, + }, + CC_ALGO_CUBIC: { + __proto__: null, + value: CC_ALGO_CUBIC, + writable: false, + configurable: false, + enumerable: true, + }, + CC_ALGO_BBR: { + __proto__: null, + value: CC_ALGO_BBR, + writable: false, + configurable: false, + enumerable: true, + }, + CC_ALGO_RENO_STR: { + __proto__: null, + value: CC_ALGO_RENO_STR, + writable: false, + configurable: false, + enumerable: true, + }, + CC_ALGO_CUBIC_STR: { + __proto__: null, + value: CC_ALGO_CUBIC_STR, + writable: false, + configurable: false, + enumerable: true, + }, + CC_ALGO_BBR_STR: { + __proto__: null, + value: CC_ALGO_BBR_STR, + writable: false, + configurable: false, + enumerable: true, + }, +}); +ObjectDefineProperties(Session, { + DEFAULT_CIPHERS: { + __proto__: null, + value: DEFAULT_CIPHERS, + writable: false, + configurable: false, + enumerable: true, + }, + DEFAULT_GROUPS: { + __proto__: null, + value: DEFAULT_GROUPS, + writable: false, + configurable: false, + enumerable: true, + }, +}); + +module.exports = { + Endpoint, + Session, + QuicStream, + // Exported for testing + kFinishClose, + SessionState, + SessionStats, + QuicStreamState, + QuicStreamStats, + EndpointState, + EndpointStats, +}; + +/* c8 ignore stop */ diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 2bc7155f7c075e..4d6e616a1f3590 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -132,6 +132,9 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "internal/http2/core", "internal/http2/compat", "internal/streams/lazy_transform", #endif // !HAVE_OPENSSL +#if !NODE_OPENSSL_HAS_QUIC + "internal/quic/quic", +#endif // !NODE_OPENSSL_HAS_QUIC "sqlite", // Experimental. "sys", // Deprecated. "wasi", // Experimental. diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 8fb6900b9cfe6f..40e700083987ef 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -233,6 +233,7 @@ bool SetOption(Environment* env, Maybe Endpoint::Options::From(Environment* env, Local value) { if (value.IsEmpty() || !value->IsObject()) { + if (value->IsUndefined()) return Just(Endpoint::Options()); THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing(); } @@ -656,6 +657,25 @@ void Endpoint::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH); + static constexpr auto CLOSECONTEXT_CLOSE = + static_cast(CloseContext::CLOSE); + static constexpr auto CLOSECONTEXT_BIND_FAILURE = + static_cast(CloseContext::BIND_FAILURE); + static constexpr auto CLOSECONTEXT_LISTEN_FAILURE = + static_cast(CloseContext::LISTEN_FAILURE); + static constexpr auto CLOSECONTEXT_RECEIVE_FAILURE = + static_cast(CloseContext::RECEIVE_FAILURE); + static constexpr auto CLOSECONTEXT_SEND_FAILURE = + static_cast(CloseContext::SEND_FAILURE); + static constexpr auto CLOSECONTEXT_START_FAILURE = + static_cast(CloseContext::START_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_CLOSE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_BIND_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_LISTEN_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_RECEIVE_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE); + SetConstructorFunction(realm->context(), target, "Endpoint", @@ -682,6 +702,7 @@ Endpoint::Endpoint(Environment* env, udp_(this), addrLRU_(options_.address_lru_size) { MakeWeak(); + STAT_RECORD_TIMESTAMP(Stats, created_at); IF_QUIC_DEBUG(env) { Debug(this, "Endpoint created. Options %s", options.ToString()); } @@ -704,6 +725,7 @@ SocketAddress Endpoint::local_address() const { void Endpoint::MarkAsBusy(bool on) { Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy"); + if (on) STAT_INCREMENT(Stats, server_busy_count); state_->busy = on ? 1 : 0; } @@ -1087,6 +1109,7 @@ void Endpoint::Destroy(CloseContext context, int status) { state_->bound = 0; state_->receiving = 0; BindingData::Get(env()).listening_endpoints.erase(this); + STAT_RECORD_TIMESTAMP(Stats, destroyed_at); EmitClose(close_context_, close_status_); } diff --git a/src/quic/session.cc b/src/quic/session.cc index e3cc27b2c66286..94432c94e0e0d1 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1682,10 +1682,16 @@ void Session::EmitStream(BaseObjectPtr stream) { if (is_destroyed()) return; if (!env()->can_call_into_js()) return; CallbackScope cb_scope(this); - Local arg = stream->object(); + auto isolate = env()->isolate(); + Local argv[] = { + stream->object(), + Integer::NewFromUnsigned(isolate, + static_cast(stream->direction())), + }; Debug(this, "Notifying JavaScript of stream created"); - MakeCallback(BindingData::Get(env()).stream_created_callback(), 1, &arg); + MakeCallback( + BindingData::Get(env()).stream_created_callback(), arraysize(argv), argv); } void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, @@ -2358,6 +2364,11 @@ void Session::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX); NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN); + NODE_DEFINE_STRING_CONSTANT( + target, "DEFAULT_CIPHERS", TLSContext::DEFAULT_CIPHERS); + NODE_DEFINE_STRING_CONSTANT( + target, "DEFAULT_GROUPS", TLSContext::DEFAULT_GROUPS); + #define V(name, _) IDX_STATS_SESSION_##name, enum SessionStatsIdx { SESSION_STATS(V) IDX_STATS_SESSION_COUNT }; #undef V diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 76d11c3d837e2e..995f69ba060c20 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -274,7 +274,7 @@ crypto::SSLCtxPointer TLSContext::Initialize() { break; } case Side::CLIENT: { - ctx_.reset(SSL_CTX_new(TLS_client_method())); + ctx.reset(SSL_CTX_new(TLS_client_method())); CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0); SSL_CTX_set_session_cache_mode( diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/parallel/test-quic-internal-endpoint-listen-defaults.js index 9fb9f9461c58b6..7b073104a148ce 100644 --- a/test/parallel/test-quic-internal-endpoint-listen-defaults.js +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.js @@ -1,76 +1,76 @@ -// Flags: --expose-internals +// Flags: --expose-internals --no-warnings 'use strict'; -const common = require('../common'); -if (!common.hasQuic) - common.skip('missing quic'); +const { hasQuic } = require('../common'); -const { internalBinding } = require('internal/test/binding'); const { - ok, - strictEqual, - deepStrictEqual, -} = require('node:assert'); + describe, + it, +} = require('node:test'); -const { - SocketAddress: _SocketAddress, - AF_INET, -} = internalBinding('block_list'); -const quic = internalBinding('quic'); - -quic.setCallbacks({ - onEndpointClose: common.mustCall((...args) => { - deepStrictEqual(args, [0, 0]); - }), - - // The following are unused in this test - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}); +describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async () => { + const { + ok, + strictEqual, + throws, + } = require('node:assert'); -const endpoint = new quic.Endpoint({}); + const { + SocketAddress, + } = require('net'); -const state = new DataView(endpoint.state); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); -strictEqual(endpoint.address(), undefined); + const { + Endpoint, + } = require('internal/quic/quic'); -endpoint.listen({}); + it('are reasonable and work as expected', async () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); -ok(state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); -ok(state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); -ok(state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); -const address = endpoint.address(); -ok(address instanceof _SocketAddress); + ok(!endpoint.state.isBound); + ok(!endpoint.state.isReceiving); + ok(!endpoint.state.isListening); -const detail = address.detail({ - address: undefined, - port: undefined, - family: undefined, - flowlabel: undefined, -}); + strictEqual(endpoint.address, undefined); + + throws(() => endpoint.listen(123), { + code: 'ERR_INVALID_ARG_TYPE', + }); + + endpoint.listen(); + throws(() => endpoint.listen(), { + code: 'ERR_INVALID_STATE', + }); -strictEqual(detail.address, '127.0.0.1'); -strictEqual(detail.family, AF_INET); -strictEqual(detail.flowlabel, 0); -ok(detail.port !== 0); + ok(endpoint.state.isBound); + ok(endpoint.state.isReceiving); + ok(endpoint.state.isListening); -endpoint.closeGracefully(); + const address = endpoint.address; + ok(address instanceof SocketAddress); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); -ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); -strictEqual(endpoint.address(), undefined); + strictEqual(address.address, '127.0.0.1'); + strictEqual(address.family, 'ipv4'); + strictEqual(address.flowlabel, 0); + ok(address.port !== 0); + + ok(!endpoint.destroyed); + endpoint.destroy(); + strictEqual(endpoint.closed, endpoint.close()); + await endpoint.closed; + ok(endpoint.destroyed); + + throws(() => endpoint.listen(), { + code: 'ERR_INVALID_STATE', + }); + throws(() => { endpoint.busy = true; }, { + code: 'ERR_INVALID_STATE', + }); + await endpoint[Symbol.asyncDispose](); + + strictEqual(endpoint.address, undefined); + }); +}); diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js index 672fac18b43779..88dc42c4dd7ade 100644 --- a/test/parallel/test-quic-internal-endpoint-options.js +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -1,215 +1,230 @@ // Flags: --expose-internals 'use strict'; -const common = require('../common'); -if (!common.hasQuic) - common.skip('missing quic'); +const { hasQuic } = require('../common'); + const { - throws, -} = require('node:assert'); + describe, + it, +} = require('node:test'); -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +describe('quic internal endpoint options', { skip: !hasQuic }, async () => { + const { + strictEqual, + throws, + } = require('node:assert'); -quic.setCallbacks({ - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}); + const { + Endpoint, + } = require('internal/quic/quic'); -throws(() => new quic.Endpoint(), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); + const { + inspect, + } = require('util'); -throws(() => new quic.Endpoint('a'), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); + const callbackConfig = { + onsession() {}, + session: {}, + stream: {}, + }; -throws(() => new quic.Endpoint(null), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); + it('invalid options', async () => { + ['a', null, false, NaN].forEach((i) => { + throws(() => new Endpoint(callbackConfig, i), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + }); -throws(() => new quic.Endpoint(false), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); + it('valid options', async () => { + // Just Works... using all defaults + new Endpoint(callbackConfig, {}); + new Endpoint(callbackConfig); + new Endpoint(callbackConfig, undefined); + }); + + it('various cases', async () => { + const cases = [ + { + key: 'retryTokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'tokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsTotal', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxStatelessResetsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'addressLRUSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxRetries', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxPayloadSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'unacknowledgedPacketThreshold', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'validateAddress', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'disableStatelessReset', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'ipv6Only', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'cc', + valid: [ + Endpoint.CC_ALGO_RENO, + Endpoint.CC_ALGO_CUBIC, + Endpoint.CC_ALGO_BBR, + Endpoint.CC_ALGO_RENO_STR, + Endpoint.CC_ALGO_CUBIC_STR, + Endpoint.CC_ALGO_BBR_STR, + ], + invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpReceiveBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpSendBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpTTL', + valid: [0, 1, 2, 3, 4, 255], + invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'resetTokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + key: 'tokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + // Unknown options are ignored entirely for any value type + key: 'ignored', + valid: ['a', null, false, true, {}, [], () => {}], + invalid: [], + }, + ]; + + for (const { key, valid, invalid } of cases) { + for (const value of valid) { + const options = {}; + options[key] = value; + new Endpoint(callbackConfig, options); + } -{ - // Just Works... using all defaults - new quic.Endpoint({}); -} + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => new Endpoint(callbackConfig, options), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + } + }); -const cases = [ - { - key: 'retryTokenExpiration', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'tokenExpiration', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxConnectionsPerHost', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxConnectionsTotal', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxStatelessResetsPerHost', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'addressLRUSize', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxRetries', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxPayloadSize', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'unacknowledgedPacketThreshold', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'validateAddress', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'disableStatelessReset', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'ipv6Only', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'cc', - valid: [ - quic.CC_ALGO_RENO, - quic.CC_ALGO_CUBIC, - quic.CC_ALGO_BBR, - quic.CC_ALGO_BBR2, - quic.CC_ALGO_RENO_STR, - quic.CC_ALGO_CUBIC_STR, - quic.CC_ALGO_BBR_STR, - quic.CC_ALGO_BBR2_STR, - ], - invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'udpReceiveBufferSize', - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'udpSendBufferSize', - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'udpTTL', - valid: [0, 1, 2, 3, 4, 255], - invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'resetTokenSecret', - valid: [ - new Uint8Array(16), - new Uint16Array(8), - new Uint32Array(4), - ], - invalid: [ - 'a', null, false, true, {}, [], () => {}, - new Uint8Array(15), - new Uint8Array(17), - new ArrayBuffer(16), - ], - }, - { - key: 'tokenSecret', - valid: [ - new Uint8Array(16), - new Uint16Array(8), - new Uint32Array(4), - ], - invalid: [ - 'a', null, false, true, {}, [], () => {}, - new Uint8Array(15), - new Uint8Array(17), - new ArrayBuffer(16), - ], - }, - { - // Unknown options are ignored entirely for any value type - key: 'ignored', - valid: ['a', null, false, true, {}, [], () => {}], - invalid: [], - }, -]; + it('endpoint can be ref/unrefed without error', async () => { + const endpoint = new Endpoint(callbackConfig, {}); + endpoint.unref(); + endpoint.ref(); + endpoint.close(); + await endpoint.closed; + }); -for (const { key, valid, invalid } of cases) { - for (const value of valid) { - const options = {}; - options[key] = value; - new quic.Endpoint(options); - } + it('endpoint can be inspected', async () => { + const endpoint = new Endpoint(callbackConfig, {}); + strictEqual(typeof inspect(endpoint), 'string'); + endpoint.close(); + await endpoint.closed; + }); - for (const value of invalid) { - const options = {}; - options[key] = value; - throws(() => new quic.Endpoint(options), { - code: 'ERR_INVALID_ARG_VALUE', + it('endpoint with object address', () => { + new Endpoint(callbackConfig, { + address: { host: '127.0.0.1:0' }, }); - } -} + throws(() => new Endpoint(callbackConfig, { address: '127.0.0.1:0' }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + +}); diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js index 566dd675d73e26..18a6ed6b138048 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.js +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -1,79 +1,245 @@ // Flags: --expose-internals 'use strict'; -const common = require('../common'); -if (!common.hasQuic) - common.skip('missing quic'); +const { hasQuic } = require('../common'); + const { - strictEqual, -} = require('node:assert'); + describe, + it, +} = require('node:test'); -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { + const { + Endpoint, + QuicStreamState, + QuicStreamStats, + SessionState, + SessionStats, + kFinishClose, + } = require('internal/quic/quic'); -const { - IDX_STATS_ENDPOINT_CREATED_AT, - IDX_STATS_ENDPOINT_DESTROYED_AT, - IDX_STATS_ENDPOINT_BYTES_RECEIVED, - IDX_STATS_ENDPOINT_BYTES_SENT, - IDX_STATS_ENDPOINT_PACKETS_RECEIVED, - IDX_STATS_ENDPOINT_PACKETS_SENT, - IDX_STATS_ENDPOINT_SERVER_SESSIONS, - IDX_STATS_ENDPOINT_CLIENT_SESSIONS, - IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, - IDX_STATS_ENDPOINT_RETRY_COUNT, - IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, - IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, - IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, - IDX_STATS_ENDPOINT_COUNT, - IDX_STATE_ENDPOINT_BOUND, - IDX_STATE_ENDPOINT_BOUND_SIZE, - IDX_STATE_ENDPOINT_RECEIVING, - IDX_STATE_ENDPOINT_RECEIVING_SIZE, - IDX_STATE_ENDPOINT_LISTENING, - IDX_STATE_ENDPOINT_LISTENING_SIZE, - IDX_STATE_ENDPOINT_CLOSING, - IDX_STATE_ENDPOINT_CLOSING_SIZE, - IDX_STATE_ENDPOINT_BUSY, - IDX_STATE_ENDPOINT_BUSY_SIZE, - IDX_STATE_ENDPOINT_PENDING_CALLBACKS, - IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, -} = quic; - -const endpoint = new quic.Endpoint({}); - -const state = new DataView(endpoint.state); -strictEqual(IDX_STATE_ENDPOINT_BOUND_SIZE, 1); -strictEqual(IDX_STATE_ENDPOINT_RECEIVING_SIZE, 1); -strictEqual(IDX_STATE_ENDPOINT_LISTENING_SIZE, 1); -strictEqual(IDX_STATE_ENDPOINT_CLOSING_SIZE, 1); -strictEqual(IDX_STATE_ENDPOINT_BUSY_SIZE, 1); -strictEqual(IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, 8); - -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BOUND), 0); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_RECEIVING), 0); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_LISTENING), 0); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_CLOSING), 0); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); -strictEqual(state.getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS), 0n); - -endpoint.markBusy(true); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 1); -endpoint.markBusy(false); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); - -const stats = new BigUint64Array(endpoint.stats); -strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n); -strictEqual(IDX_STATS_ENDPOINT_COUNT, 13); + const { + inspect, + } = require('util'); + + const { + deepStrictEqual, + strictEqual, + throws, + } = require('node:assert'); + + it('endpoint state', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }); + + strictEqual(endpoint.state.isBound, false); + strictEqual(endpoint.state.isReceiving, false); + strictEqual(endpoint.state.isListening, false); + strictEqual(endpoint.state.isClosing, false); + strictEqual(endpoint.state.isBusy, false); + strictEqual(endpoint.state.pendingCallbacks, 0n); + + deepStrictEqual(JSON.parse(JSON.stringify(endpoint.state)), { + isBound: false, + isReceiving: false, + isListening: false, + isClosing: false, + isBusy: false, + pendingCallbacks: '0', + }); + + endpoint.busy = true; + strictEqual(endpoint.state.isBusy, true); + endpoint.busy = false; + strictEqual(endpoint.state.isBusy, false); + + it('state can be inspected without errors', () => { + strictEqual(typeof inspect(endpoint.state), 'string'); + }); + }); + + it('state is not readable after close', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); + endpoint.state[kFinishClose](); + throws(() => endpoint.state.isBound, { + name: 'Error', + }); + }); + + it('state constructor argument is ArrayBuffer', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); + const Cons = endpoint.state.constructor; + throws(() => new Cons(1), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + it('endpoint stats', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }); + + strictEqual(typeof endpoint.stats.createdAt, 'bigint'); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.bytesReceived, 'bigint'); + strictEqual(typeof endpoint.stats.bytesSent, 'bigint'); + strictEqual(typeof endpoint.stats.packetsReceived, 'bigint'); + strictEqual(typeof endpoint.stats.packetsSent, 'bigint'); + strictEqual(typeof endpoint.stats.serverSessions, 'bigint'); + strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); + strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); + strictEqual(typeof endpoint.stats.retryCount, 'bigint'); + strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); + strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); + strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); + + deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ + 'createdAt', + 'destroyedAt', + 'bytesReceived', + 'bytesSent', + 'packetsReceived', + 'packetsSent', + 'serverSessions', + 'clientSessions', + 'serverBusyCount', + 'retryCount', + 'versionNegotiationCount', + 'statelessResetCount', + 'immediateCloseCount', + ]); + + it('stats can be inspected without errors', () => { + strictEqual(typeof inspect(endpoint.stats), 'string'); + }); + }); + + it('stats are still readble after close', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); + endpoint.stats[kFinishClose](); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); + }); + + it('stats constructor argument is ArrayBuffer', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); + const Cons = endpoint.stats.constructor; + throws(() => new Cons(1), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + + // TODO(@jasnell): The following tests are largely incomplete. + // This is largely here to boost the code coverage numbers + // temporarily while the rest of the functionality is being + // implemented. + it('stream and session states', () => { + const streamState = new QuicStreamState(new ArrayBuffer(1024)); + const sessionState = new SessionState(new ArrayBuffer(1024)); + + strictEqual(streamState.finSent, false); + strictEqual(streamState.finReceived, false); + strictEqual(streamState.readEnded, false); + strictEqual(streamState.writeEnded, false); + strictEqual(streamState.destroyed, false); + strictEqual(streamState.paused, false); + strictEqual(streamState.reset, false); + strictEqual(streamState.hasReader, false); + strictEqual(streamState.wantsBlock, false); + strictEqual(streamState.wantsHeaders, false); + strictEqual(streamState.wantsReset, false); + strictEqual(streamState.wantsTrailers, false); + + strictEqual(sessionState.hasPathValidationListener, false); + strictEqual(sessionState.hasVersionNegotiationListener, false); + strictEqual(sessionState.hasDatagramListener, false); + strictEqual(sessionState.hasSessionTicketListener, false); + strictEqual(sessionState.isClosing, false); + strictEqual(sessionState.isGracefulClose, false); + strictEqual(sessionState.isSilentClose, false); + strictEqual(sessionState.isStatelessReset, false); + strictEqual(sessionState.isDestroyed, false); + strictEqual(sessionState.isHandshakeCompleted, false); + strictEqual(sessionState.isHandshakeConfirmed, false); + strictEqual(sessionState.isStreamOpenAllowed, false); + strictEqual(sessionState.isPrioritySupported, false); + strictEqual(sessionState.isWrapped, false); + strictEqual(sessionState.lastDatagramId, 0n); + + strictEqual(typeof streamState.toJSON(), 'object'); + strictEqual(typeof sessionState.toJSON(), 'object'); + strictEqual(typeof inspect(streamState), 'string'); + strictEqual(typeof inspect(sessionState), 'string'); + }); + + it('stream and session stats', () => { + const streamStats = new QuicStreamStats(new ArrayBuffer(1024)); + const sessionStats = new SessionStats(new ArrayBuffer(1024)); + strictEqual(streamStats.createdAt, undefined); + strictEqual(streamStats.receivedAt, undefined); + strictEqual(streamStats.ackedAt, undefined); + strictEqual(streamStats.closingAt, undefined); + strictEqual(streamStats.destroyedAt, undefined); + strictEqual(streamStats.bytesReceived, undefined); + strictEqual(streamStats.bytesSent, undefined); + strictEqual(streamStats.maxOffset, undefined); + strictEqual(streamStats.maxOffsetAcknowledged, undefined); + strictEqual(streamStats.maxOffsetReceived, undefined); + strictEqual(streamStats.finalSize, undefined); + strictEqual(typeof streamStats.toJSON(), 'object'); + strictEqual(typeof inspect(streamStats), 'string'); + streamStats[kFinishClose](); + + strictEqual(typeof sessionStats.createdAt, 'bigint'); + strictEqual(typeof sessionStats.closingAt, 'bigint'); + strictEqual(typeof sessionStats.destroyedAt, 'bigint'); + strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint'); + strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint'); + strictEqual(typeof sessionStats.gracefulClosingAt, 'bigint'); + strictEqual(typeof sessionStats.bytesReceived, 'bigint'); + strictEqual(typeof sessionStats.bytesSent, 'bigint'); + strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint'); + strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint'); + strictEqual(typeof sessionStats.uniInStreamCount, 'bigint'); + strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint'); + strictEqual(typeof sessionStats.lossRetransmitCount, 'bigint'); + strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint'); + strictEqual(typeof sessionStats.bytesInFlight, 'bigint'); + strictEqual(typeof sessionStats.blockCount, 'bigint'); + strictEqual(typeof sessionStats.cwnd, 'bigint'); + strictEqual(typeof sessionStats.latestRtt, 'bigint'); + strictEqual(typeof sessionStats.minRtt, 'bigint'); + strictEqual(typeof sessionStats.rttVar, 'bigint'); + strictEqual(typeof sessionStats.smoothedRtt, 'bigint'); + strictEqual(typeof sessionStats.ssthresh, 'bigint'); + strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); + strictEqual(typeof sessionStats.datagramsSent, 'bigint'); + strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); + strictEqual(typeof sessionStats.datagramsLost, 'bigint'); + strictEqual(typeof sessionStats.toJSON(), 'object'); + strictEqual(typeof inspect(sessionStats), 'string'); + streamStats[kFinishClose](); + }); +}); diff --git a/test/parallel/test-quic-internal-setcallbacks.js b/test/parallel/test-quic-internal-setcallbacks.js index 881e9161ca9dcc..c503f5f4f25691 100644 --- a/test/parallel/test-quic-internal-setcallbacks.js +++ b/test/parallel/test-quic-internal-setcallbacks.js @@ -1,38 +1,47 @@ -// Flags: --expose-internals +// Flags: --expose-internals --no-warnings 'use strict'; -const common = require('../common'); -if (!common.hasQuic) - common.skip('missing quic'); -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +const { hasQuic } = require('../common'); -const { throws } = require('assert'); +const { + describe, + it, +} = require('node:test'); -const callbacks = { - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}; -// Fail if any callback is missing -for (const fn of Object.keys(callbacks)) { - // eslint-disable-next-line no-unused-vars - const { [fn]: _, ...rest } = callbacks; - throws(() => quic.setCallbacks(rest), { - code: 'ERR_MISSING_ARGS', +describe('quic internal setCallbacks', { skip: !hasQuic }, () => { + const { internalBinding } = require('internal/test/binding'); + const quic = internalBinding('quic'); + + it('require all callbacks to be set', (t) => { + const callbacks = { + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, + }; + // Fail if any callback is missing + for (const fn of Object.keys(callbacks)) { + // eslint-disable-next-line no-unused-vars + const { [fn]: _, ...rest } = callbacks; + t.assert.throws(() => quic.setCallbacks(rest), { + code: 'ERR_MISSING_ARGS', + }); + } + // If all callbacks are present it should work + quic.setCallbacks(callbacks); + + // Multiple calls should just be ignored. + quic.setCallbacks(callbacks); }); -} -// If all callbacks are present it should work -quic.setCallbacks(callbacks); +}); From 82566f80b63dda3a708e953daa4646b46a2e7553 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 28 Sep 2024 10:10:32 -0700 Subject: [PATCH 2/2] src: make minor tweaks to quic c++ for c++20 --- src/quic/application.h | 6 ++---- src/quic/bindingdata.h | 6 ++---- src/quic/cid.cc | 6 ++---- src/quic/cid.h | 6 ++---- src/quic/data.h | 6 ++---- src/quic/defs.h | 6 ++---- src/quic/endpoint.h | 6 ++---- src/quic/http3.cc | 6 ++---- src/quic/http3.h | 7 ++----- src/quic/logstream.h | 6 ++---- src/quic/packet.h | 6 ++---- src/quic/preferredaddress.h | 6 ++---- src/quic/session.h | 6 ++---- src/quic/sessionticket.h | 6 ++---- src/quic/streams.h | 6 ++---- src/quic/tlscontext.h | 6 ++---- src/quic/tokens.cc | 6 ++---- src/quic/tokens.h | 6 ++---- src/quic/transportparams.h | 6 ++---- 19 files changed, 38 insertions(+), 77 deletions(-) diff --git a/src/quic/application.h b/src/quic/application.h index fc228fafe357e5..79b9941f62b2b4 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -10,8 +10,7 @@ #include "sessionticket.h" #include "streams.h" -namespace node { -namespace quic { +namespace node::quic { // An Application implements the ALPN-protocol specific semantics on behalf // of a QUIC Session. @@ -154,8 +153,7 @@ struct Session::Application::StreamData final { std::string ToString() const; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index f5c19d1fbb8498..cbc8c9436de928 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -16,8 +16,7 @@ #include #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { class Endpoint; class Packet; @@ -267,8 +266,7 @@ struct CallbackScope final : public CallbackScopeBase { explicit CallbackScope(T* ptr) : CallbackScopeBase(ptr->env()), ref(ptr) {} }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/cid.cc b/src/quic/cid.cc index f6eb9301a9215c..fdc636145210b2 100644 --- a/src/quic/cid.cc +++ b/src/quic/cid.cc @@ -8,8 +8,7 @@ #include "ncrypto.h" #include "quic/defs.h" -namespace node { -namespace quic { +namespace node::quic { // ============================================================================ // CID @@ -150,6 +149,5 @@ const CID::Factory& CID::Factory::random() { return instance; } -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/cid.h b/src/quic/cid.h index 89d8fd2f2a0fed..15aa6d90748007 100644 --- a/src/quic/cid.h +++ b/src/quic/cid.h @@ -7,8 +7,7 @@ #include #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { // CIDS are used to identify endpoints participating in a QUIC session. // Once created, CID instances are immutable. @@ -122,8 +121,7 @@ class CID::Factory { // of CID::Factory that implement the QUIC Load Balancers spec. }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/data.h b/src/quic/data.h index ef36e248bbf562..7f97403d61f414 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -13,8 +13,7 @@ #include #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { struct Path final : public ngtcp2_path { Path(const SocketAddress& local, const SocketAddress& remote); @@ -143,8 +142,7 @@ class QuicError final : public MemoryRetainer { const ngtcp2_ccerr* ptr_ = nullptr; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/defs.h b/src/quic/defs.h index 0e2e3095ad8676..5491d407390166 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -9,8 +9,7 @@ #include #include -namespace node { -namespace quic { +namespace node::quic { #define NGTCP2_SUCCESS 0 #define NGTCP2_ERR(V) (V != NGTCP2_SUCCESS) @@ -243,5 +242,4 @@ class DebugIndentScope { static int indent_; }; -} // namespace quic -} // namespace node +} // namespace node::quic diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 132ebae18d494d..194f7c3d84c33c 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -17,8 +17,7 @@ #include "sessionticket.h" #include "tokens.h" -namespace node { -namespace quic { +namespace node::quic { #define ENDPOINT_CC(V) \ V(RENO, reno) \ @@ -453,8 +452,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { friend class Session; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 11196a5f6ca060..2fd2b704d07e3d 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -17,8 +17,7 @@ #include "session.h" #include "sessionticket.h" -namespace node { -namespace quic { +namespace node::quic { namespace { struct Http3HeadersTraits { @@ -831,7 +830,6 @@ std::unique_ptr createHttp3Application( return std::make_unique(session, options); } -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/http3.h b/src/quic/http3.h index b56d28b0dd202d..94860c9b771830 100644 --- a/src/quic/http3.h +++ b/src/quic/http3.h @@ -5,14 +5,11 @@ #include "session.h" -namespace node { -namespace quic { - +namespace node::quic { std::unique_ptr createHttp3Application( Session* session, const Session::Application_Options& options); -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/logstream.h b/src/quic/logstream.h index b9d3f8df974477..b5f2b59e3aa747 100644 --- a/src/quic/logstream.h +++ b/src/quic/logstream.h @@ -9,8 +9,7 @@ #include #include -namespace node { -namespace quic { +namespace node::quic { // The LogStream is a utility that the QUIC impl uses to publish both QLog // and Keylog diagnostic data (one instance for each). @@ -76,8 +75,7 @@ class LogStream : public AsyncWrap, public StreamBase { void ensure_space(size_t amt); }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/packet.h b/src/quic/packet.h index 761fb3af4e609d..58ab6f46fa8d21 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -18,8 +18,7 @@ #include "defs.h" #include "tokens.h" -namespace node { -namespace quic { +namespace node::quic { struct PathDescriptor { uint32_t version; @@ -147,8 +146,7 @@ class Packet final : public ReqWrap { std::shared_ptr data_; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/preferredaddress.h b/src/quic/preferredaddress.h index 37003e2eb704c7..369c09c8463dc5 100644 --- a/src/quic/preferredaddress.h +++ b/src/quic/preferredaddress.h @@ -10,8 +10,7 @@ #include #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { // PreferredAddress is a helper class used only when a client Session receives // an advertised preferred address from a server. The helper provides @@ -67,8 +66,7 @@ class PreferredAddress final { const ngtcp2_preferred_addr* paddr_; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/session.h b/src/quic/session.h index 41a797dad6844d..f980af9611c6c7 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -25,8 +25,7 @@ #include "tlscontext.h" #include "transportparams.h" -namespace node { -namespace quic { +namespace node::quic { class Endpoint; @@ -438,8 +437,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { friend class TransportParams; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/sessionticket.h b/src/quic/sessionticket.h index d7ec639baeff74..a2e6e3758f6f9c 100644 --- a/src/quic/sessionticket.h +++ b/src/quic/sessionticket.h @@ -11,8 +11,7 @@ #include "data.h" #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { // A TLS 1.3 Session resumption ticket. Encapsulates both the TLS // ticket and the encoded QUIC transport parameters. The encoded @@ -103,8 +102,7 @@ class SessionTicket::AppData final { SSL* ssl_; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/streams.h b/src/quic/streams.h index 15f86cda0e3c86..0bacb37faf542d 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -15,8 +15,7 @@ #include "bindingdata.h" #include "data.h" -namespace node { -namespace quic { +namespace node::quic { class Session; @@ -227,8 +226,7 @@ class Stream : public AsyncWrap, void Unschedule(); }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index c7ee7f8ef72dc4..3f2f8aff42a8a5 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -13,8 +13,7 @@ #include "defs.h" #include "sessionticket.h" -namespace node { -namespace quic { +namespace node::quic { class Session; class TLSContext; @@ -221,8 +220,7 @@ class TLSContext final : public MemoryRetainer, friend class TLSSession; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index fcb2a24ca3b6f8..d3ebde6225da56 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -10,8 +10,7 @@ #include "nbytes.h" #include "ncrypto.h" -namespace node { -namespace quic { +namespace node::quic { // ============================================================================ // TokenSecret @@ -300,7 +299,6 @@ RegularToken::operator const char*() const { return reinterpret_cast(ptr_.base); } -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/tokens.h b/src/quic/tokens.h index bd62faee9eabd6..4f9f25dd12324b 100644 --- a/src/quic/tokens.h +++ b/src/quic/tokens.h @@ -10,8 +10,7 @@ #include "cid.h" #include "defs.h" -namespace node { -namespace quic { +namespace node::quic { // TokenSecrets are used to generate things like stateless reset tokens, // retry tokens, and token packets. They are always QUIC_TOKENSECRET_LEN @@ -251,8 +250,7 @@ class RegularToken final : public MemoryRetainer { const ngtcp2_vec ptr_; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 349ddd5c948bdc..af6af3fc0266b3 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -13,8 +13,7 @@ #include "data.h" #include "tokens.h" -namespace node { -namespace quic { +namespace node::quic { class Endpoint; class Session; @@ -160,8 +159,7 @@ class TransportParams final { QuicError error_ = QuicError::TRANSPORT_NO_ERROR; }; -} // namespace quic -} // namespace node +} // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS