diff --git a/doc/api/errors.md b/doc/api/errors.md index dde490c29b477cc..75257f6a054efcf 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3571,6 +3571,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 8ab4a63a0f7110e..75643133b9e7114 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1645,6 +1645,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/datagrams.js b/lib/internal/quic/datagrams.js new file mode 100644 index 000000000000000..d3f39d9a72b33e7 --- /dev/null +++ b/lib/internal/quic/datagrams.js @@ -0,0 +1,142 @@ +'use strict'; + +const { + Uint8Array, +} = primordials; + +const { + ReadableStream, +} = require('internal/webstreams/readablestream'); + +const { + WritableStream, +} = require('internal/webstreams/writablestream'); + +const { + isArrayBufferView, +} = require('util/types'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + }, +} = require('internal/errors'); + +// QUIC datagrams are unordered, unreliable packets of data exchanged over +// a session. They are unrelated to any specific QUIC stream. Our implementation +// uses a ReadableStream to receive datagrams and a WritableStream to send them. +// Any ArrayBufferView can be used when sending a datagram. Received datagrams +// will always be Uint8Array. +// The DatagramReadableStream and DatagramWritableStream instances are created +// and held internally by the QUIC Session object. Only the readable and writable +// properties will be exposed. + +class DatagramReadableStream { + #readable = undefined; + + /** @type {ReadableStreamDefaultController} */ + #controller = undefined; + + constructor() { + let controller; + this.#readable = new ReadableStream({ + start(c) { controller = c; }, + }); + this.#controller = controller; + } + + get readable() { return this.#readable; } + + // Close the ReadableStream. The underlying source will be closed. Any + // datagrams already in the queue will be preserved and will be read. + close() { + this.#controller.close(); + } + + // Errors the readable stream + error(reason) { + this.#controller.error(reason); + } + + // Enqueue a datagram to be read by the stream. This will always be + // a Uint8Array. + enqueue(datagram) { + this.#controller.enqueue(datagram); + } +} + +class DatagramWritableStream { + #writable = undefined; + /** @type {WritableStreamDefaultController} */ + #controller = undefined; + + /** + * @callback DatagramWrittenCallback + * @param {Uint8Array} chunk + * @returns {Promise} + */ + + /** + * @callback DatagramClosedCallback + * @returns {Promise} + */ + + /** + * @callback DatagramAbortedCallback + * @param {any} reason + * @returns {Promise} + */ + + /** + * @param {DatagramWrittenCallback} written + * @param {DatagramClosedCallback} closed + * @param {DatagramAbortedCallback} aborted + */ + constructor(written, closed, aborted) { + let controller; + this.#writable = new WritableStream({ + start(c) { controller = c; }, + async close(controller) { + try { + await closed(undefined); + } catch (err) { + controller.error(err); + } + }, + async abort(reason) { + try { + await aborted(reason); + } catch { + // There's nothing to do in this case + } + }, + async write(chunk, controller) { + if (!isArrayBufferView(chunk)) { + throw new ERR_INVALID_ARG_TYPE('chunk', ['ArrayBufferView'], chunk); + } + const { + byteOffset, + byteLength, + } = chunk; + chunk = new Uint8Array(chunk.buffer.transfer(), byteOffset, byteLength); + try { + await written(chunk); + } catch (err) { + controller.error(err); + } + }, + }); + this.#controller = controller; + } + + get writable() { return this.#writable; } + + error(reason) { + this.#controller.error(reason); + } +} + +module.exports = { + DatagramReadableStream, + DatagramWritableStream, +}; diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js new file mode 100644 index 000000000000000..708f33a11f5603c --- /dev/null +++ b/lib/internal/quic/quic.js @@ -0,0 +1,2103 @@ +'use strict'; + +const { + ArrayIsArray, + BigUint64Array, + DataView, + ObjectDefineProperties, + PromiseWithResolvers, + ReflectConstruct, + Symbol, + SymbolAsyncDispose, + SymbolDispose, +} = primordials; + +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 underly 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 { + codes: { + ERR_ILLEGAL_CONSTRUCTOR, + 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 { + EventTarget, + Event, + kNewListener, + kRemoveListener, +} = require('internal/event_target'); + +const { + ReadableStream, +} = require('internal/webstreams/readablestream'); + +const { + WritableStream, +} = require('internal/webstreams/writablestream'); + +const { + customInspectSymbol: kInspect, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); + +const { + kHandle: kKeyObjectHandle, + kKeyObject: kKeyObjectInner, +} = require('internal/crypto/util'); + +const { + validateObject, +} = require('internal/validators'); + +const { + DatagramReadableStream, + DatagramWritableStream, +} = require('internal/quic/datagrams'); + +const kOwner = Symbol('kOwner'); +const kHandle = Symbol('kHandle'); +const kFinishClose = Symbol('kFinishClose'); +const kStats = Symbol('kStats'); +const kState = Symbol('kState'); +const kReadable = Symbol('kReadable'); +const kWritable = Symbol('kWritable'); +const kEndpoint = Symbol('kEndpoint'); +const kSession = Symbol('kSession'); +const kStreams = Symbol('kStreams'); +const kNewSession = Symbol('kNewSession'); +const kNewStream = Symbol('kNewStream'); +const kRemoteAddress = Symbol('kRemoteAddress'); +const kDatagramsReadable = Symbol('kDatagramsReadable'); +const kDatagramsWritable = Symbol('kDatagramsWritable'); +const kIsPendingClose = Symbol('kIsPendingClose'); +const kPendingClose = Symbol('kPendingClose'); +const kHandshake = Symbol('kHandshake'); +const kPathValidation = Symbol('kPathValidation'); +const kTicket = Symbol('kTicket'); +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 + * @property {number} [txDiagnosticLoss] The transmit diagnostic loss + * @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} [enableConnectPrototcol] 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 + */ + +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) { + const session = this[kOwner]; + // Make sure the datagram streams are created. + session.datagrams; // eslint-disable-line no-unused-expressions + // Enqueue the datagram that was received. + session[kDatagramsReadable].enqueue(uint8Array); + }, + onSessionDatagramStatus(id, status) { + // TODO(@jasnell): Called when a datagram has been acknowledged + // or lost. Currently there's nothing to do here. We need to + // decide if/how we want to expose this information. + }, + onSessionHandshake(sni, alpn, cipher, cipherVersion, + validationErrorReason, + validationErrorCode, + earlyDataAccepted) { + this[kOwner][kHandshake](sni, alpn, cipher, cipherVersion, + validationErrorReason, + validationErrorCode, + earlyDataAccepted); + }, + onSessionPathValidation(result, + newLocalAddress, + newRemoteAddress, + oldLocalAddress, + oldRemoteAddress, + preferredAddress) { + this[kOwner][kPathValidation](result, newLocalAddress, + newRemoteAddress, oldLocalAddress, + oldRemoteAddress, preferredAddress); + }, + onSessionTicket(ticket) { + this[kOwner][kTicket](ticket); + }, + onSessionVersionNegotiation(version, + requestedVersions, + supportedVersions) { + this[kOwner][kVersionNegotiation](version, requestedVersions, + supportedVersions); + }, + onStreamCreated(stream) {}, + onStreamBlocked() {}, + onStreamClose(error) {}, + onStreamReset(error) {}, + onStreamHeaders(headers, kind) {}, + onStreamTrailers() {}, +}); + +/** + * Emitted when a new server-side session has been initiated. + */ +class SessionEvent extends Event { + #session = undefined; + #endpoint = undefined; + + /** + * @param {Session} session + * @param {Endpoint} endpoint + */ + constructor(session, endpoint) { + super('session'); + this.#session = session; + this.#endpoint = endpoint; + } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {Endpoint} */ + get endpoint() { return this.#endpoint; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `SessionEvent ${inspect({ + session: this.session, + endpoint: this.endpoint, + }, opts)}`; + } +} + +class StreamEvent extends Event { + #stream = undefined; + #session = undefined; + + /** + * @param {Stream} stream + * @param {Session} session + */ + constructor(stream, session) { + super('stream'); + this.#stream = stream; + this.#session = session; + } + + /** @type {Stream} */ + get stream() { return this.#stream; } + + /** @type {Session} */ + get session() { return this.#session; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `StreamEvent ${inspect({ + stream: this.stream, + }, opts)}`; + } +} + +/** + * @param {string} policy + * @returns {number} + */ +function getPreferredAddressPolicy(policy) { + if (policy === 'use') return PREFERRED_ADDRESS_USE; + if (policy === 'ignore') return PREFERRED_ADDRESS_IGNORE; + if (policy === 'default' || policy === undefined) + 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'); + } + keyHandles.push(key[kKeyObjectHandle]); + continue; + } else if (isCryptoKey(key)) { + if (key.type !== 'private') { + throw new ERR_INVALID_ARG_VALUE('options.tls.keys', key, 'must be a private key'); + } + keyHandles.push(key[kKeyObjectInner][kKeyObjectHandle]); + continue; + } 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 StreamStats { + constructor() { + throw new ERR_INVALID_STATE('StreamStats'); + } + + get createdAt() { + return this[kHandle][IDX_STATS_STREAM_CREATED_AT]; + } + + get receivedAt() { + return this[kHandle][IDX_STATS_STREAM_RECEIVED_AT]; + } + + get ackedAt() { + return this[kHandle][IDX_STATS_STREAM_ACKED_AT]; + } + + get closingAt() { + return this[kHandle][IDX_STATS_STREAM_CLOSING_AT]; + } + + get destroyedAt() { + return this[kHandle][IDX_STATS_STREAM_DESTROYED_AT]; + } + + get bytesReceived() { + return this[kHandle][IDX_STATS_STREAM_BYTES_RECEIVED]; + } + + get bytesSent() { + return this[kHandle][IDX_STATS_STREAM_BYTES_SENT]; + } + + get maxOffset() { + return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET]; + } + + get maxOffsetAcknowledged() { + return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET_ACK]; + } + + get maxOffsetReceived() { + return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET_RECV]; + } + + get finalSize() { + return this[kHandle][IDX_STATS_STREAM_FINAL_SIZE]; + } + + toJSON() { + return { + 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[kHandle] = new BigUint64Array(this[kHandle]); + } +} + +/** + * @param {ArrayBuffer} buffer + * @returns {StreamStats} + */ +function createStreamStats(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new BigUint64Array(buffer); + }, [], StreamStats); +} + +class StreamState { + constructor() { + throw new ERR_INVALID_STATE('StreamState'); + } + + /** @type {bigint} */ + get id() { + return this[kHandle].getBigInt64(IDX_STATE_STREAM_ID); + } + + /** @type {boolean} */ + get finSent() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_FIN_SENT); + } + + /** @type {boolean} */ + get finReceived() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_FIN_RECEIVED); + } + + /** @type {boolean} */ + get readEnded() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_READ_ENDED); + } + + /** @type {boolean} */ + get writeEnded() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_WRITE_ENDED); + } + + /** @type {boolean} */ + get destroyed() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_DESTROYED); + } + + /** @type {boolean} */ + get paused() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_PAUSED); + } + + /** @type {boolean} */ + get reset() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_RESET); + } + + /** @type {boolean} */ + get hasReader() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_HAS_READER); + } + + /** @type {boolean} */ + get wantsBlock() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_BLOCK); + } + + /** @type {boolean} */ + set wantsBlock(val) { + this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsHeaders() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_HEADERS); + } + + /** @type {boolean} */ + set wantsHeaders(val) { + this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsReset() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_RESET); + } + + /** @type {boolean} */ + set wantsReset(val) { + this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0); + } + + /** @type {boolean} */ + get wantsTrailers() { + return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_TRAILERS); + } + + /** @type {boolean} */ + set wantsTrailers(val) { + this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0); + } + + toJSON() { + return { + 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)}`; + } +} + +/** + * @param {ArrayBuffer} buffer + * @returns {StreamState} + */ +function createStreamState(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new DataView(buffer); + }, [], StreamState); +} + +class Stream extends EventTarget { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('Stream'); + } + + /** @type {StreamStats} */ + get stats() { return this[kStats]; } + + /** @type {StreamState} */ + get state() { return this[kState]; } + + /** @type {Session} */ + get session() { return this[kSession]; } + + /** @type {bigint} */ + get id() { return this[kState].id; } +} + +class BidirectionalStream extends Stream { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('BidirectionalStream'); + } + + /** @type {ReadableStream} */ + get readable() { return this[kReadable]; } + + /** @type {WritableStream} */ + get writable() { return this[kWritable]; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `BidirectionalStream ${inspect({ + stats: this.stats, + state: this.state, + session: this.session, + readable: this.readable, + writable: this.writable, + }, opts)}`; + } +} + +class UnidirectionalInboundStream extends Stream { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('UnidirectionalInboundStream'); + } + + /** @type {ReadableStream} */ + get readable() { return this[kReadable]; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `UnidirectionalInboundStream ${inspect({ + stats: this.stats, + state: this.state, + session: this.session, + readable: this.readable, + }, opts)}`; + } +} + +class UnidirectionalOutboundStream extends Stream { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('UnidirectionalOutboundStream'); + } + + /** @type {WritableStream} */ + get writable() { return this[kWritable]; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `UnidirectionalOutboundStream ${inspect({ + stats: this.stats, + state: this.state, + session: this.session, + writable: this.writable, + }, opts)}`; + } +} + +/** + * @param {object} handle + * @returns {BidirectionalStream} + */ +function createBidirectionalStream(handle, session) { + const instance = ReflectConstruct(EventTarget, [], BidirectionalStream); + instance[kHandle] = handle; + instance[kStats] = createStreamStats(handle.stats); + instance[kState] = createStreamState(handle.state); + instance[kSession] = session; + instance[kReadable] = new ReadableStream(); + instance[kWritable] = new WritableStream(); + handle[kOwner] = instance; + return instance; +} + +/** + * @param {object} handle + * @returns {UnidirectionalInboundStream} + */ +function createUnidirectionalInboundStream(handle, session) { // eslint-disable-line no-unused-vars + const instance = ReflectConstruct(EventTarget, [], UnidirectionalInboundStream); + instance[kHandle] = handle; + instance[kStats] = createStreamStats(handle.stats); + instance[kState] = createStreamState(handle.state); + instance[kSession] = session; + instance[kReadable] = new ReadableStream(); + handle[kOwner] = instance; + return instance; +} + +/** + * @param {object} handle + * @returns {UnidirectionalOutboundStream} + */ +function createUnidirectionalOutboundStream(handle, session) { + const instance = ReflectConstruct(EventTarget, [], UnidirectionalOutboundStream); + instance[kHandle] = handle; + instance[kStats] = createStreamStats(handle.stats); + instance[kState] = createStreamState(handle.state); + instance[kSession] = session; + instance[kWritable] = new WritableStream(); + handle[kOwner] = instance; + return instance; +} + +class SessionStats { + constructor() { + throw new ERR_INVALID_STATE('SessionStats'); + } + + /** @type {bigint} */ + get createdAt() { + return this[kHandle][IDX_STATS_SESSION_CREATED_AT]; + } + + /** @type {bigint} */ + get closingAt() { + return this[kHandle][IDX_STATS_SESSION_CLOSING_AT]; + } + + /** @type {bigint} */ + get destroyedAt() { + return this[kHandle][IDX_STATS_SESSION_DESTROYED_AT]; + } + + /** @type {bigint} */ + get handshakeCompletedAt() { + return this[kHandle][IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT]; + } + + /** @type {bigint} */ + get handshakeConfirmedAt() { + return this[kHandle][IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT]; + } + + /** @type {bigint} */ + get gracefulClosingAt() { + return this[kHandle][IDX_STATS_SESSION_GRACEFUL_CLOSING_AT]; + } + + /** @type {bigint} */ + get bytesReceived() { + return this[kHandle][IDX_STATS_SESSION_BYTES_RECEIVED]; + } + + /** @type {bigint} */ + get bytesSent() { + return this[kHandle][IDX_STATS_SESSION_BYTES_SENT]; + } + + /** @type {bigint} */ + get bidiInStreamCount() { + return this[kHandle][IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT]; + } + + /** @type {bigint} */ + get bidiOutStreamCount() { + return this[kHandle][IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT]; + } + + /** @type {bigint} */ + get uniInStreamCount() { + return this[kHandle][IDX_STATS_SESSION_UNI_IN_STREAM_COUNT]; + } + + /** @type {bigint} */ + get uniOutStreamCount() { + return this[kHandle][IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT]; + } + + /** @type {bigint} */ + get lossRetransmitCount() { + return this[kHandle][IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT]; + } + + /** @type {bigint} */ + get maxBytesInFlights() { + return this[kHandle][IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT]; + } + + /** @type {bigint} */ + get bytesInFlight() { + return this[kHandle][IDX_STATS_SESSION_BYTES_IN_FLIGHT]; + } + + /** @type {bigint} */ + get blockCount() { + return this[kHandle][IDX_STATS_SESSION_BLOCK_COUNT]; + } + + /** @type {bigint} */ + get cwnd() { + return this[kHandle][IDX_STATS_SESSION_CWND]; + } + + /** @type {bigint} */ + get latestRtt() { + return this[kHandle][IDX_STATS_SESSION_LATEST_RTT]; + } + + /** @type {bigint} */ + get minRtt() { + return this[kHandle][IDX_STATS_SESSION_MIN_RTT]; + } + + /** @type {bigint} */ + get rttVar() { + return this[kHandle][IDX_STATS_SESSION_RTTVAR]; + } + + /** @type {bigint} */ + get smoothedRtt() { + return this[kHandle][IDX_STATS_SESSION_SMOOTHED_RTT]; + } + + /** @type {bigint} */ + get ssthresh() { + return this[kHandle][IDX_STATS_SESSION_SSTHRESH]; + } + + /** @type {bigint} */ + get datagramsReceived() { + return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_RECEIVED]; + } + + /** @type {bigint} */ + get datagramsSent() { + return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_SENT]; + } + + /** @type {bigint} */ + get datagramsAcknowledged() { + return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED]; + } + + /** @type {bigint} */ + get datagramsLost() { + return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_LOST]; + } + + toJSON() { + return { + 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[kHandle] = new BigUint64Array(this[kHandle]); + } +} + +/** + * + * @param {ArrayBuffer} buffer + * @returns {SessionStats} + */ +function createSessionStats(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new BigUint64Array(buffer); + }, [], SessionStats); +} + +class SessionState { + constructor() { + throw new ERR_INVALID_STATE('SessionState'); + } + + /** @type {boolean} */ + get hasPathValidationListener() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_PATH_VALIDATION); + } + + set hasPathValidationListener(val) { + this[kHandle].setUint8(IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasVersionNegotiationListener() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION); + } + + set hasVersionNegotiationListener(val) { + this[kHandle].setUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasDatagramListener() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_DATAGRAM); + } + + set hasDatagramListener(val) { + this[kHandle].setUint8(IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0); + } + + /** @type {boolean} */ + get hasSessionTicketListener() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_SESSION_TICKET); + } + + set hasSessionTicketListener(val) { + this[kHandle].setUint8(IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0); + } + + /** @type {boolean} */ + get isClosing() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_CLOSING); + } + + /** @type {boolean} */ + get isGracefulClose() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_GRACEFUL_CLOSE); + } + + /** @type {boolean} */ + get isSilentClose() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_SILENT_CLOSE); + } + + /** @type {boolean} */ + get isStatelessReset() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_STATELESS_RESET); + } + + /** @type {boolean} */ + get isDestroyed() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_DESTROYED); + } + + /** @type {boolean} */ + get isHandshakeCompleted() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_HANDSHAKE_COMPLETED); + } + + /** @type {boolean} */ + get isHandshakeConfirmed() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED); + } + + /** @type {boolean} */ + get isStreamOpenAllowed() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED); + } + + /** @type {boolean} */ + get isPrioritySupported() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_PRIORITY_SUPPORTED); + } + + /** @type {boolean} */ + get isWrapped() { + return !!this[kHandle].getUint8(IDX_STATE_SESSION_WRAPPED); + } + + /** @type {bigint} */ + get lastDatagramId() { + return this[kHandle].getBigUint64(IDX_STATE_SESSION_LAST_DATAGRAM_ID); + } + + toJSON() { + return { + 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[kHandle] = new DataView(this[kHandle]); + } +} + +/** + * @param {ArrayBuffer} buffer + * @returns {SessionState} + */ +function createSessionState(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new DataView(buffer); + }, [], SessionState); + +} + +class Session extends EventTarget { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('Session'); + } + + get #isClosedOrClosing() { + return this[kHandle] === undefined || this[kIsPendingClose]; + } + + /** @type {SessionStats} */ + get stats() { return this[kStats]; } + + /** @type {SessionState} */ + get state() { return this[kState]; } + + /** @type {Endpoint} */ + get endpoint() { return this[kEndpoint]; } + + /** @type {SocketAddress|undefined} */ + get localAddress() { + if (this.#isClosedOrClosing) return undefined; + return this[kEndpoint].address; + } + + /** @type {SocketAddress|undefined} */ + get remoteAddress() { + if (this.#isClosedOrClosing) return undefined; + if (this[kRemoteAddress] === undefined) { + const addr = this[kHandle].getRemoteAddress(); + if (addr !== undefined) { + this[kRemoteAddress] = new InternalSocketAddress(addr); + } + } + return this[kRemoteAddress]; + } + + /** + * @returns {BidirectionalStream} + */ + openBidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + const handle = this[kHandle].openStream(STREAM_DIRECTION_BIDIRECTIONAL); + if (handle === undefined) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const stream = createBidirectionalStream(handle, this); + this[kStreams].add(stream); + return stream; + } + + /** + * @returns {UnidirectionalInboundStream} + */ + openUnidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + const handle = this[kHandle].openStream(STREAM_DIRECTION_UNIDIRECTIONAL); + if (handle === undefined) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + const stream = createUnidirectionalOutboundStream(handle, this); + this[kStreams].add(stream); + return stream; + } + + updateKey() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + this[kHandle].updateKey(); + } + + close() { + if (!this.#isClosedOrClosing) { + this[kIsPendingClose] = true; + this[kHandle]?.gracefulClose(); + } + return this[kPendingClose].promise; + } + + /** + * Returns a Readable/WritableStream pair that can be used to send and + * receive unreliable datagrams on this session. The order or even success + * of delivery is not guaranteed. + * @type {Datagrams} + */ + get datagrams() { + this[kDatagramsReadable] ??= new DatagramReadableStream(); + this[kDatagramsWritable] ??= new DatagramWritableStream( + async (chunk) => { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + this[kHandle].sendDatagram(chunk); + }, + async () => { + // The writable stream was closed. There's really not much + // for us to do here yet. + }, + async (reason) => { + // The writable stream was aborted. There's really not much + // for us to do here yet. + }, + ); + return { + readable: this[kDatagramsReadable].readable, + writable: this[kDatagramsWritable].writable, + }; + } + + [kNewListener](size, type, listener, once, capture, passive, weak) { + super[kNewListener](size, type, listener, once, capture, passive, weak); + if (type === 'pathValidation') { + this[kState].hasPathValidationListener = true; + } else if (type === 'versionNegotiation') { + this[kState].hasVersionNegotiationListener = true; + } else if (type === 'datagram') { + this[kState].hasDatagramListener = true; + } else if (type === 'sessionTicket') { + this[kState].hasSessionTicketListener = true; + } + } + + [kRemoveListener](size, type, listener, capture) { + super[kRemoveListener](size, type, listener, capture); + if (type === 'pathValidation') { + this[kState].hasPathValidationListener = size > 0; + } else if (type === 'versionNegotiation') { + this[kState].hasVersionNegotiationListener = size > 0; + } else if (type === 'datagram') { + this[kState].hasDatagramListener = size > 0; + } else if (type === 'sessionTicket') { + this[kState].hasSessionTicketListener = size > 0; + } + } + + [kNewStream](handle) { + const stream = createBidirectionalStream(handle, this); + this[kStreams].add(stream); + this.dispatchEvent(new StreamEvent(stream)); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `Session ${inspect({ + stats: this.stats, + state: this.state, + endpoint: this.endpoint, + }, opts)}`; + } + + [kFinishClose](errorType, code, reason) { + // TODO(@jasnell): Finish the implementation + } + + [kHandshake](sni, alpn, cipher, cipherVersion, + validationErrorReason, + validationErrorCode, + earlyDataAccepted) {} + + [kPathValidation](result, + newLocalAddress, + newRemoteAddress, + oldLocalAddress, + oldRemoteAddress, + preferredAddress) {} + + [kTicket](ticket) {} + + [kVersionNegotiation](version, requestedVersions, supportedVersions) {} +} + +/** + * @param {object} handle + * @returns {Session} + */ +function createSession(handle, endpoint) { + const instance = ReflectConstruct(EventTarget, [], Session); + instance[kHandle] = handle; + instance[kStats] = createSessionStats(handle.stats); + instance[kState] = createSessionState(handle.state); + instance[kEndpoint] = endpoint; + instance[kStreams] = []; + instance[kRemoteAddress] = undefined; + instance[kIsPendingClose] = false; + instance[kPendingClose] = PromiseWithResolvers(); + handle[kOwner] = instance; + return instance; +} + +class EndpointStats { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR('EndpointStats'); + } + + /** @type {bigint} */ + get createdAt() { + return this[kHandle][IDX_STATS_ENDPOINT_CREATED_AT]; + } + + /** @type {bigint} */ + get destroyedAt() { + return this[kHandle][IDX_STATS_ENDPOINT_DESTROYED_AT]; + } + + /** @type {bigint} */ + get bytesReceived() { + return this[kHandle][IDX_STATS_ENDPOINT_BYTES_RECEIVED]; + } + + /** @type {bigint} */ + get bytesSent() { + return this[kHandle][IDX_STATS_ENDPOINT_BYTES_SENT]; + } + + /** @type {bigint} */ + get packetsReceived() { + return this[kHandle][IDX_STATS_ENDPOINT_PACKETS_RECEIVED]; + } + + /** @type {bigint} */ + get packetsSent() { + return this[kHandle][IDX_STATS_ENDPOINT_PACKETS_SENT]; + } + + /** @type {bigint} */ + get serverSessions() { + return this[kHandle][IDX_STATS_ENDPOINT_SERVER_SESSIONS]; + } + + /** @type {bigint} */ + get clientSessions() { + return this[kHandle][IDX_STATS_ENDPOINT_CLIENT_SESSIONS]; + } + + /** @type {bigint} */ + get serverBusyCount() { + return this[kHandle][IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT]; + } + + /** @type {bigint} */ + get retryCount() { + return this[kHandle][IDX_STATS_ENDPOINT_RETRY_COUNT]; + } + + /** @type {bigint} */ + get versionNegotiationCount() { + return this[kHandle][IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT]; + } + + /** @type {bigint} */ + get statelessResetCount() { + return this[kHandle][IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT]; + } + + /** @type {bigint} */ + get immediateCloseCount() { + return this[kHandle][IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT]; + } + + toJSON() { + return { + 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[kHandle] = new BigUint64Array(this[kHandle]); + } +} + +/** + * @param {ArrayBuffer} buffer + * @returns {EndpointStats} + */ +function createEndpointStats(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new BigUint64Array(buffer); + }, [], EndpointStats); +} + +class EndpointState { + constructor() { + throw new ERR_INVALID_STATE('EndpointState'); + } + + /** @type {boolean} */ + get isBound() { + return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_BOUND); + } + + /** @type {boolean} */ + get isReceiving() { + return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_RECEIVING); + } + + /** @type {boolean} */ + get isListening() { + return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_LISTENING); + } + + /** @type {boolean} */ + get isClosing() { + return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_CLOSING); + } + + /** @type {boolean} */ + get isBusy() { + return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_BUSY); + } + + /** @type {boolean} */ + get pendingCallbacks() { + return !!this[kHandle].getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS); + } + + toJSON() { + return { + 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; + + 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. + this[kHandle] = new DataView(this[kHandle]); + } +} + +/** + * @param {ArrayBuffer} buffer + * @returns {EndpointState} + */ +function createEndpointState(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new DataView(buffer); + }, [], EndpointState); +} + +class Endpoint extends EventTarget { + #handle = undefined; + #busy = false; + #listening = false; + #isPendingClose = false; + #pendingClose = undefined; + #address = undefined; + #stats = undefined; + #state = undefined; + #sessions = []; + + /** + * @param {EndpointOptions} [options] + */ + constructor(options = {}) { + validateObject(options, 'options'); + + const { + address, + 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)) { + throw new ERR_INVALID_ARG_TYPE('options.address', 'SocketAddress', address); + } + + super(); + this.#pendingClose = PromiseWithResolvers(); + this.#handle = new Endpoint_({ + 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 = createEndpointStats(this.#handle.stats); + this.#state = createEndpointState(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 = {}) { + 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 = {}, + transportParams = {}, + tls = {}, + 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 = {}) { + 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)) { + throw new ERR_INVALID_ARG_TYPE('address', 'SocketAddress', address); + } + + validateObject(options, 'options'); + const { + version, + minVersion, + preferredAddressPolicy, + application, + transportParams, + tls, + qlog, + 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 = createSession(handle, this); + this.#sessions.push(session); + return session; + } + + /** + * Gracefully closes the endpoint. + * @returns {Promise} + */ + close() { + if (!this.#isClosedOrClosing) { + this.#isPendingClose = true; + this.#handle?.closeGracefully(); + } + return this.#pendingClose.promise; + } + + ref() { + if (!this.#isClosedOrClosing) this.#handle.ref(true); + } + + unref() { + if (!this.#isClosedOrClosing) 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](); + + switch (context) { + case kCloseContextClose: { + 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.#pendingClose.resolve = undefined; + this.#pendingClose.reject = undefined; + this.#handle = undefined; + } + + [kNewSession](handle) { + const session = createSession(handle, this); + this.#sessions.push(session); + this.dispatchEvent(new SessionEvent(session, this)); + } + + [SymbolAsyncDispose]() { return this.close(); } + [SymbolDispose]() { 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, + closing: this.#isPendingClose, + destroyed: this.#handle === undefined, + listening: this.#listening, + stats: this.stats, + state: this.state, + }, opts)}`; + } +}; + +ObjectDefineProperties(Endpoint.prototype, { + busy: { __proto__: null, enumerable: true }, + address: { __proto__: null, enumerable: true }, + stats: { __proto__: null, enumerable: true }, +}); +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, + EndpointStats, + EndpointState, + Session, + SessionStats, + SessionState, + Stream, + StreamStats, + StreamState, + BidirectionalStream, + UnidirectionalInboundStream, + UnidirectionalOutboundStream, + SessionEvent, + StreamEvent, +}; diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 9e54e8e08ae0bc5..d06b18353fb370c 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -232,6 +232,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(); } @@ -655,6 +656,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", @@ -681,6 +701,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()); } @@ -703,6 +724,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; } @@ -1086,6 +1108,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_); } @@ -1690,7 +1713,7 @@ void Endpoint::New(const FunctionCallbackInfo& args) { void Endpoint::DoConnect(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); // args[0] is a SocketAddress // args[1] is a Session OptionsObject (see session.cc) @@ -1723,7 +1746,7 @@ void Endpoint::DoConnect(const FunctionCallbackInfo& args) { void Endpoint::DoListen(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); Session::Options options; @@ -1734,20 +1757,20 @@ void Endpoint::DoListen(const FunctionCallbackInfo& args) { void Endpoint::MarkBusy(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->MarkAsBusy(args[0]->IsTrue()); } void Endpoint::DoCloseGracefully(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->CloseGracefully(); } void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); if (endpoint->is_closed() || !endpoint->udp_.is_bound()) return; auto addr = SocketAddressBase::Create( env, std::make_shared(endpoint->local_address())); @@ -1756,7 +1779,7 @@ void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { void Endpoint::Ref(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); if (args[0]->BooleanValue(env->isolate())) { endpoint->udp_.Ref(); diff --git a/src/quic/session.cc b/src/quic/session.cc index c53e7f584470f39..fd1a535e2d4ffd6 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -2357,6 +2357,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 8441e491d1ca6a4..6af5bc3bf63a689 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -276,7 +276,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-options.js b/test/parallel/test-quic-internal-endpoint-options.js index 672fac18b437797..cdb9c13b93a3b74 100644 --- a/test/parallel/test-quic-internal-endpoint-options.js +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -8,50 +8,19 @@ const { throws, } = require('node:assert'); -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +const { Endpoint } = require('internal/quic/quic'); -quic.setCallbacks({ - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}); - -throws(() => new quic.Endpoint(), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -throws(() => new quic.Endpoint('a'), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -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' +['a', null, false, NaN].forEach((i) => { + throws(() => new Endpoint(i), { + code: 'ERR_INVALID_ARG_TYPE', + }); }); { // Just Works... using all defaults - new quic.Endpoint({}); + new Endpoint({}); + new Endpoint(); + new Endpoint(undefined); } const cases = [ @@ -136,14 +105,12 @@ const cases = [ { 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, + 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, {}, [], () => {}], }, @@ -202,13 +169,13 @@ for (const { key, valid, invalid } of cases) { for (const value of valid) { const options = {}; options[key] = value; - new quic.Endpoint(options); + new Endpoint(options); } for (const value of invalid) { const options = {}; options[key] = value; - throws(() => new quic.Endpoint(options), { + throws(() => new Endpoint(options), { code: 'ERR_INVALID_ARG_VALUE', }); } diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js index 566dd675d73e26a..5012ce9c1255680 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.js +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -63,17 +63,17 @@ endpoint.markBusy(false); strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); const stats = new BigUint64Array(endpoint.stats); -strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_CREATED_AT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_BYTES_SENT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 'bigint'); strictEqual(IDX_STATS_ENDPOINT_COUNT, 13);