From cdae31570669cd24b1251f2be0243230c53344cf Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 1 Jun 2024 20:11:58 -0700 Subject: [PATCH] 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. PR-URL: https://github.com/nodejs/node/pull/53256 Reviewed-By: Stephen Belanger Reviewed-By: Yagiz Nizipli --- .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 | 138 +- .../test-quic-internal-endpoint-options.js | 425 +-- ...test-quic-internal-endpoint-stats-state.js | 304 +- .../test-quic-internal-setcallbacks.js | 75 +- 12 files changed, 3194 insertions(+), 380 deletions(-) create mode 100644 lib/internal/quic/quic.js diff --git a/.nycrc b/.nycrc index 42aad37414a..1ca428df1c8 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 10b0aea8754..a6aaa57d924 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 2869369262f..c7cb7715dbc 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 00000000000..4d0077195d0 --- /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 2bc7155f7c0..4d6e616a1f3 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 8fb6900b9cf..40e70008398 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 e3cc27b2c66..94432c94e0e 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 76d11c3d837..995f69ba060 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 9fb9f9461c5..7b073104a14 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 { internalBinding } = require('internal/test/binding'); -const { - ok, - strictEqual, - deepStrictEqual, -} = require('node:assert'); +const { hasQuic } = require('../common'); const { - SocketAddress: _SocketAddress, - AF_INET, -} = internalBinding('block_list'); -const quic = internalBinding('quic'); + describe, + it, +} = require('node:test'); -quic.setCallbacks({ - onEndpointClose: common.mustCall((...args) => { - deepStrictEqual(args, [0, 0]); - }), +describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async () => { + const { + ok, + strictEqual, + throws, + } = require('node:assert'); - // The following are unused in this test - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, + const { + SocketAddress, + } = require('net'); + + const { + Endpoint, + } = require('internal/quic/quic'); + + it('are reasonable and work as expected', async () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }, {}); + + ok(!endpoint.state.isBound); + ok(!endpoint.state.isReceiving); + ok(!endpoint.state.isListening); + + strictEqual(endpoint.address, undefined); + + throws(() => endpoint.listen(123), { + code: 'ERR_INVALID_ARG_TYPE', + }); + + endpoint.listen(); + throws(() => endpoint.listen(), { + code: 'ERR_INVALID_STATE', + }); + + ok(endpoint.state.isBound); + ok(endpoint.state.isReceiving); + ok(endpoint.state.isListening); + + const address = endpoint.address; + ok(address instanceof SocketAddress); + + 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); + }); }); - -const endpoint = new quic.Endpoint({}); - -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); - -endpoint.listen({}); - -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); - -const detail = address.detail({ - address: undefined, - port: undefined, - family: undefined, - flowlabel: undefined, -}); - -strictEqual(detail.address, '127.0.0.1'); -strictEqual(detail.family, AF_INET); -strictEqual(detail.flowlabel, 0); -ok(detail.port !== 0); - -endpoint.closeGracefully(); - -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); diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js index 672fac18b43..88dc42c4dd7 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' -}); - -throws(() => new quic.Endpoint(false), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -{ - // Just Works... using all defaults - new quic.Endpoint({}); -} - -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: [], - }, -]; - -for (const { key, valid, invalid } of cases) { - for (const value of valid) { - const options = {}; - options[key] = value; - new quic.Endpoint(options); - } - - for (const value of invalid) { - const options = {}; - options[key] = value; - throws(() => new quic.Endpoint(options), { - code: 'ERR_INVALID_ARG_VALUE', + it('invalid options', async () => { + ['a', null, false, NaN].forEach((i) => { + throws(() => new Endpoint(callbackConfig, i), { + code: 'ERR_INVALID_ARG_TYPE', + }); }); - } -} + }); + + 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); + } + + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => new Endpoint(callbackConfig, options), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + } + }); + + it('endpoint can be ref/unrefed without error', async () => { + const endpoint = new Endpoint(callbackConfig, {}); + endpoint.unref(); + endpoint.ref(); + endpoint.close(); + await endpoint.closed; + }); + + it('endpoint can be inspected', async () => { + const endpoint = new Endpoint(callbackConfig, {}); + strictEqual(typeof inspect(endpoint), 'string'); + endpoint.close(); + await endpoint.closed; + }); + + 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 566dd675d73..18a6ed6b138 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 { - strictEqual, -} = require('node:assert'); - -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +const { hasQuic } = require('../common'); 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; + describe, + it, +} = require('node:test'); -const endpoint = new quic.Endpoint({}); +describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { + const { + Endpoint, + QuicStreamState, + QuicStreamStats, + SessionState, + SessionStats, + kFinishClose, + } = require('internal/quic/quic'); -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); + const { + inspect, + } = require('util'); -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); + const { + deepStrictEqual, + strictEqual, + throws, + } = require('node:assert'); -endpoint.markBusy(true); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 1); -endpoint.markBusy(false); -strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); + it('endpoint state', () => { + const endpoint = new Endpoint({ + onsession() {}, + session: {}, + stream: {}, + }); -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); + 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 881e9161ca9..c503f5f4f25 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); +});