diff --git a/doc/api/errors.md b/doc/api/errors.md
index 9bb467af7fc78e..5cf1f13ae69def 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -2491,6 +2491,426 @@ Accessing `Object.prototype.__proto__` has been forbidden using
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
object.
+
+
+### `ERR_QUIC_AEAD_LIMIT_REACHED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_APPLICATION_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_CONNECTION_ID_LIMIT_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_CONNECTION_REFUSED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_CRYPTO_BUFFER_EXCEEDED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_CRYPTO_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_ENDPOINT_FAILURE`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_FINAL_SIZE_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_FLOW_CONTROL_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_FRAME_ENCODING_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_CLOSED_CRITICAL_STREAM`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_CONNECT_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_EXCESSIVE_LOAD`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_FRAME_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_FRAME_UNEXPECTED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_GENERAL_PROTOCOL_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_ID_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_INTERNAL_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_MESSAGE_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_MISSING_SETTINGS`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_REQUEST_CANCELLED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_REQUEST_INCOMPLETE`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_REQUEST_REJECTED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_SETTINGS_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_STREAM_CREATION_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_H3_VERSION_FALLBACK`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_HANDSHAKE_CANCELED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_IDLE_CLOSE`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_INTERNAL_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_INVALID_TOKEN`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_KEY_UPDATE_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_NO_VIABLE_PATH`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_PROTOCOL_VIOLATION`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_QPACK_DECODER_STREAM_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_QPACK_DECOMPRESSION_FAILED`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_QPACK_ENCODER_STREAM_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_STREAM_LIMIT_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_STREAM_STATE_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_TRANSPORT_PARAMETER_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_UNABLE_TO_CREATE_STREAM`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_UNKNOWN_ERROR`
+
+
+
+TBD
+
+
+
+### `ERR_QUIC_VERSION_NEGOTIATION_ERROR`
+
+
+
+TBD
+
### `ERR_REQUIRE_ESM`
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index f6a8ce6549c597..be308a290a38a9 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1524,6 +1524,50 @@ 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_AEAD_LIMIT_REACHED', 'QUIC AEAD limit reached%s', Error);
+E('ERR_QUIC_APPLICATION_ERROR', 'QUIC application error%s', Error);
+E('ERR_QUIC_CONNECTION_ID_LIMIT_ERROR', 'QUIC connection ID limit error%s', Error);
+E('ERR_QUIC_CONNECTION_REFUSED', 'QUIC connection refused%s', Error);
+E('ERR_QUIC_CRYPTO_BUFFER_EXCEEDED', 'QUIC crypto buffer exceeded error%s', Error);
+E('ERR_QUIC_CRYPTO_ERROR', 'QUIC crypto error%s', Error);
+E('ERR_QUIC_ENDPOINT_FAILURE', 'QUIC endpoint failure [%d]: %s', Error);
+E('ERR_QUIC_FINAL_SIZE_ERROR', 'QUIC final size error%s', Error);
+E('ERR_QUIC_FLOW_CONTROL_ERROR', 'QUIC flow control error%s', Error);
+E('ERR_QUIC_FRAME_ENCODING_ERROR', 'QUIC frame encoding error%s', Error);
+E('ERR_QUIC_H3_CLOSED_CRITICAL_STREAM', 'HTTP/3 closed critical stream%s', Error);
+E('ERR_QUIC_H3_CONNECT_ERROR', 'HTTP/3 connect error%s', Error);
+E('ERR_QUIC_H3_EXCESSIVE_LOAD', 'HTTP/3 excessive load%s', Error);
+E('ERR_QUIC_H3_FRAME_ERROR', 'HTTP/3 frame error%s', Error);
+E('ERR_QUIC_H3_FRAME_UNEXPECTED', 'HTTP/3 unexpected frame%s', Error);
+E('ERR_QUIC_H3_GENERAL_PROTOCOL_ERROR', 'HTTP/3 protocol error%s', Error);
+E('ERR_QUIC_H3_ID_ERROR', 'HTTP/3 ID error%s', Error);
+E('ERR_QUIC_H3_INTERNAL_ERROR', 'HTTP/3 internal error%s', Error);
+E('ERR_QUIC_H3_MESSAGE_ERROR', 'HTTP/3 message error%s', Error);
+E('ERR_QUIC_H3_MISSING_SETTINGS', 'HTTP/3 missing settings%s', Error);
+E('ERR_QUIC_H3_REQUEST_CANCELLED', 'HTTP/3 request canceled%s', Error);
+E('ERR_QUIC_H3_REQUEST_INCOMPLETE', 'HTTP/3 request incomplete%s', Error);
+E('ERR_QUIC_H3_REQUEST_REJECTED', 'HTTP/3 request rejected%s', Error);
+E('ERR_QUIC_H3_SETTINGS_ERROR', 'HTTP/3 settings error%s', Error);
+E('ERR_QUIC_H3_STREAM_CREATION_ERROR', 'HTTP/3 stream creation error%s', Error);
+E('ERR_QUIC_H3_VERSION_FALLBACK', 'HTTP/3 version fallback%s', Error);
+E('ERR_QUIC_HANDSHAKE_CANCELED', 'QUIC handshake canceled', Error);
+E('ERR_QUIC_IDLE_CLOSE', 'QUIC idle close%s', Error);
+E('ERR_QUIC_INTERNAL_ERROR', 'QUIC internal error%s', Error);
+E('ERR_QUIC_INVALID_TOKEN', 'QUIC invalid token%s', Error);
+E('ERR_QUIC_KEY_UPDATE_ERROR', 'QUIC key update error%s', Error);
+E('ERR_QUIC_NO_VIABLE_PATH', 'QUIC no viable path%s', Error);
+E('ERR_QUIC_PROTOCOL_VIOLATION', 'QUIC protocol violation%s', Error);
+E('ERR_QUIC_QPACK_DECODER_STREAM_ERROR', 'HTTP/3 qpack decoder stream error%s', Error);
+E('ERR_QUIC_QPACK_DECOMPRESSION_FAILED', 'HTTP/3 qpack decompression failed%s', Error);
+E('ERR_QUIC_QPACK_ENCODER_STREAM_ERROR', 'HTTP/3 qpack encoder stream error%s', Error);
+E('ERR_QUIC_STREAM_LIMIT_ERROR', 'QUIC stream limit error%s', Error);
+E('ERR_QUIC_STREAM_STATE_ERROR', 'QUIC stream state error%s', Error);
+E('ERR_QUIC_TRANSPORT_PARAMETER_ERROR', 'QUIC transport parameter error%s', Error);
+E('ERR_QUIC_UNABLE_TO_CREATE_STREAM', 'QUIC unable to create stream', Error);
+E('ERR_QUIC_UNKNOWN_ERROR', 'QUIC unknown error [%s] %s', Error);
+E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'QUIC version negotiation error%s', Error);
+
E('ERR_REQUIRE_ESM',
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
hideInternalStackFrames(this);
diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js
new file mode 100644
index 00000000000000..2a151808013677
--- /dev/null
+++ b/lib/internal/quic/quic.js
@@ -0,0 +1,3729 @@
+'use strict';
+
+/* eslint-disable no-use-before-define */
+
+const {
+ ArrayIsArray,
+ BigInt,
+ BigUint64Array,
+ Boolean,
+ DataView,
+ NumberIsInteger,
+ ObjectCreate,
+ ObjectDefineProperties,
+ Symbol,
+} = primordials;
+
+const {
+ setCallbacks,
+ createEndpoint,
+ EndpointOptions: EndpointOptions_,
+ SessionOptions: SessionOptions_,
+ ArrayBufferViewSource,
+ StreamSource,
+ StreamBaseSource,
+ BlobSource,
+
+ QUIC_CC_ALGO_CUBIC,
+ QUIC_CC_ALGO_RENO,
+ QUIC_CC_ALGO_BBR,
+ QUIC_CC_ALGO_BBR2,
+ QUIC_MAX_CIDLEN,
+
+ QUIC_ENDPOINT_CLOSE_CONTEXT_CLOSE,
+ QUIC_ENDPOINT_CLOSE_CONTEXT_BIND_FAILURE,
+ QUIC_ENDPOINT_CLOSE_CONTEXT_START_FAILURE,
+ QUIC_ENDPOINT_CLOSE_CONTEXT_RECEIVE_FAILURE,
+ QUIC_ENDPOINT_CLOSE_CONTEXT_SEND_FAILURE,
+ QUIC_ENDPOINT_CLOSE_CONTEXT_LISTEN_FAILURE,
+
+ QUIC_ERROR_TYPE_TRANSPORT,
+ QUIC_ERROR_TYPE_APPLICATION,
+ QUIC_ERROR_TYPE_VERSION_NEGOTIATION,
+ QUIC_ERROR_TYPE_IDLE_CLOSE,
+
+ QUIC_ERR_NO_ERROR,
+ QUIC_ERR_INTERNAL_ERROR,
+ QUIC_ERR_CONNECTION_REFUSED,
+ QUIC_ERR_FLOW_CONTROL_ERROR,
+ QUIC_ERR_STREAM_LIMIT_ERROR,
+ QUIC_ERR_STREAM_STATE_ERROR,
+ QUIC_ERR_FINAL_SIZE_ERROR,
+ QUIC_ERR_FRAME_ENCODING_ERROR,
+ QUIC_ERR_TRANSPORT_PARAMETER_ERROR,
+ QUIC_ERR_CONNECTION_ID_LIMIT_ERROR,
+ QUIC_ERR_PROTOCOL_VIOLATION,
+ QUIC_ERR_INVALID_TOKEN,
+ QUIC_ERR_APPLICATION_ERROR,
+ QUIC_ERR_CRYPTO_BUFFER_EXCEEDED,
+ QUIC_ERR_KEY_UPDATE_ERROR,
+ QUIC_ERR_AEAD_LIMIT_REACHED,
+ QUIC_ERR_NO_VIABLE_PATH,
+ QUIC_ERR_CRYPTO_ERROR,
+ QUIC_ERR_VERSION_NEGOTIATION_ERROR_DRAFT,
+
+ QUIC_ERR_H3_GENERAL_PROTOCOL_ERROR,
+ QUIC_ERR_H3_INTERNAL_ERROR,
+ QUIC_ERR_H3_STREAM_CREATION_ERROR,
+ QUIC_ERR_H3_CLOSED_CRITICAL_STREAM,
+ QUIC_ERR_H3_FRAME_UNEXPECTED,
+ QUIC_ERR_H3_FRAME_ERROR,
+ QUIC_ERR_H3_EXCESSIVE_LOAD,
+ QUIC_ERR_H3_ID_ERROR,
+ QUIC_ERR_H3_SETTINGS_ERROR,
+ QUIC_ERR_H3_MISSING_SETTINGS,
+ QUIC_ERR_H3_REQUEST_REJECTED,
+ QUIC_ERR_H3_REQUEST_CANCELLED,
+ QUIC_ERR_H3_REQUEST_INCOMPLETE,
+ QUIC_ERR_H3_MESSAGE_ERROR,
+ QUIC_ERR_H3_CONNECT_ERROR,
+ QUIC_ERR_H3_VERSION_FALLBACK,
+ QUIC_ERR_QPACK_DECOMPRESSION_FAILED,
+ QUIC_ERR_QPACK_ENCODER_STREAM_ERROR,
+ QUIC_ERR_QPACK_DECODER_STREAM_ERROR,
+
+ QUIC_PREFERRED_ADDRESS_IGNORE,
+ QUIC_PREFERRED_ADDRESS_USE,
+
+ QUIC_STREAM_PRIORITY_DEFAULT,
+ QUIC_STREAM_PRIORITY_LOW,
+ QUIC_STREAM_PRIORITY_HIGH,
+ QUIC_STREAM_PRIORITY_FLAGS_NONE,
+ QUIC_STREAM_PRIORITY_FLAGS_NON_INCREMENTAL,
+
+ QUIC_HEADERS_KIND_INFO,
+ QUIC_HEADERS_KIND_INITIAL,
+ QUIC_HEADERS_KIND_TRAILING,
+
+ QUIC_HEADERS_FLAGS_TERMINAL,
+ QUIC_HEADERS_FLAGS_NONE,
+
+ HTTP3_ALPN,
+
+ DEFAULT_RETRYTOKEN_EXPIRATION,
+ DEFAULT_TOKEN_EXPIRATION,
+ DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ DEFAULT_MAX_CONNECTIONS,
+ DEFAULT_MAX_STATELESS_RESETS,
+ DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE,
+ DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD,
+ DEFAULT_MAX_RETRY_LIMIT,
+ DEFAULT_CIPHERS,
+ DEFAULT_GROUPS,
+
+ 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_SESSION_CREATED_AT,
+ IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
+ IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
+ IDX_STATS_SESSION_GRACEFUL_CLOSING_AT,
+ IDX_STATS_SESSION_CLOSING_AT,
+ IDX_STATS_SESSION_DESTROYED_AT,
+ IDX_STATS_SESSION_BYTES_RECEIVED,
+ IDX_STATS_SESSION_BYTES_SENT,
+ IDX_STATS_SESSION_BIDI_STREAM_COUNT,
+ IDX_STATS_SESSION_UNI_STREAM_COUNT,
+ IDX_STATS_SESSION_STREAMS_IN_COUNT,
+ IDX_STATS_SESSION_STREAMS_OUT_COUNT,
+ IDX_STATS_SESSION_KEYUPDATE_COUNT,
+ IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT,
+ IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT,
+ IDX_STATS_SESSION_BLOCK_COUNT,
+ IDX_STATS_SESSION_BYTES_IN_FLIGHT,
+ IDX_STATS_SESSION_CONGESTION_RECOVERY_START_TS,
+ IDX_STATS_SESSION_CWND,
+ IDX_STATS_SESSION_DELIVERY_RATE_SEC,
+ IDX_STATS_SESSION_FIRST_RTT_SAMPLE_TS,
+ IDX_STATS_SESSION_INITIAL_RTT,
+ IDX_STATS_SESSION_LAST_TX_PKT_TS,
+ IDX_STATS_SESSION_LATEST_RTT,
+ IDX_STATS_SESSION_LOSS_DETECTION_TIMER,
+ IDX_STATS_SESSION_LOSS_TIME,
+ IDX_STATS_SESSION_MAX_UDP_PAYLOAD_SIZE,
+ IDX_STATS_SESSION_MIN_RTT,
+ IDX_STATS_SESSION_PTO_COUNT,
+ IDX_STATS_SESSION_RTTVAR,
+ IDX_STATS_SESSION_SMOOTHED_RTT,
+ IDX_STATS_SESSION_SSTHRESH,
+ IDX_STATS_SESSION_RECEIVE_RATE,
+ IDX_STATS_SESSION_SEND_RATE,
+
+ IDX_STATS_STREAM_CREATED_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_ENDPOINT_LISTENING,
+ IDX_STATE_ENDPOINT_CLOSING,
+ IDX_STATE_ENDPOINT_WAITING_FOR_CALLBACKS,
+ IDX_STATE_ENDPOINT_BUSY,
+ IDX_STATE_ENDPOINT_PENDING_CALLBACKS,
+
+ IDX_STATE_SESSION_VERSION_NEGOTIATION,
+ IDX_STATE_SESSION_PATH_VALIDATION,
+ IDX_STATE_SESSION_DATAGRAM,
+ IDX_STATE_SESSION_SESSION_TICKET,
+ IDX_STATE_SESSION_CLIENT_HELLO,
+ IDX_STATE_SESSION_CLIENT_HELLO_DONE,
+ IDX_STATE_SESSION_CLOSING,
+ IDX_STATE_SESSION_DESTROYED,
+ IDX_STATE_SESSION_GRACEFUL_CLOSING,
+ IDX_STATE_SESSION_HANDSHAKE_COMPLETED,
+ IDX_STATE_SESSION_HANDSHAKE_CONFIRMED,
+ IDX_STATE_SESSION_OCSP,
+ IDX_STATE_SESSION_OCSP_DONE,
+ IDX_STATE_SESSION_SILENT_CLOSE,
+ IDX_STATE_SESSION_STREAM_OPEN_ALLOWED,
+ IDX_STATE_SESSION_TRANSPORT_PARAMS_SET,
+ IDX_STATE_SESSION_USING_PREFERRED_ADDRESS,
+ IDX_STATE_SESSION_PRIORITY_SUPPORTED,
+
+ IDX_STATE_STREAM_ID,
+ IDX_STATE_STREAM_FIN_SENT,
+ IDX_STATE_STREAM_FIN_RECEIVED,
+ IDX_STATE_STREAM_READ_ENDED,
+ IDX_STATE_STREAM_TRAILERS,
+ IDX_STATE_STREAM_DESTROYED,
+ IDX_STATE_STREAM_DATA,
+ IDX_STATE_STREAM_PAUSED,
+ IDX_STATE_STREAM_RESET,
+
+} = internalBinding('quic');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_STATE,
+
+ ERR_QUIC_INTERNAL_ERROR,
+ ERR_QUIC_CONNECTION_REFUSED,
+ ERR_QUIC_FLOW_CONTROL_ERROR,
+ ERR_QUIC_STREAM_LIMIT_ERROR,
+ ERR_QUIC_STREAM_STATE_ERROR,
+ ERR_QUIC_FINAL_SIZE_ERROR,
+ ERR_QUIC_FRAME_ENCODING_ERROR,
+ ERR_QUIC_TRANSPORT_PARAMETER_ERROR,
+ ERR_QUIC_CONNECTION_ID_LIMIT_ERROR,
+ ERR_QUIC_PROTOCOL_VIOLATION,
+ ERR_QUIC_INVALID_TOKEN,
+ ERR_QUIC_APPLICATION_ERROR,
+ ERR_QUIC_CRYPTO_BUFFER_EXCEEDED,
+ ERR_QUIC_KEY_UPDATE_ERROR,
+ ERR_QUIC_AEAD_LIMIT_REACHED,
+ ERR_QUIC_NO_VIABLE_PATH,
+ ERR_QUIC_CRYPTO_ERROR,
+ ERR_QUIC_VERSION_NEGOTIATION_ERROR,
+ ERR_QUIC_UNKNOWN_ERROR,
+ ERR_QUIC_IDLE_CLOSE,
+ ERR_QUIC_UNABLE_TO_CREATE_STREAM,
+ ERR_QUIC_HANDSHAKE_CANCELED,
+ ERR_QUIC_ENDPOINT_FAILURE,
+
+ ERR_QUIC_H3_GENERAL_PROTOCOL_ERROR,
+ ERR_QUIC_H3_INTERNAL_ERROR,
+ ERR_QUIC_H3_STREAM_CREATION_ERROR,
+ ERR_QUIC_H3_CLOSED_CRITICAL_STREAM,
+ ERR_QUIC_H3_FRAME_UNEXPECTED,
+ ERR_QUIC_H3_FRAME_ERROR,
+ ERR_QUIC_H3_EXCESSIVE_LOAD,
+ ERR_QUIC_H3_ID_ERROR,
+ ERR_QUIC_H3_SETTINGS_ERROR,
+ ERR_QUIC_H3_MISSING_SETTINGS,
+ ERR_QUIC_H3_REQUEST_REJECTED,
+ ERR_QUIC_H3_REQUEST_CANCELLED,
+ ERR_QUIC_H3_REQUEST_INCOMPLETE,
+ ERR_QUIC_H3_MESSAGE_ERROR,
+ ERR_QUIC_H3_CONNECT_ERROR,
+ ERR_QUIC_H3_VERSION_FALLBACK,
+ ERR_QUIC_QPACK_DECOMPRESSION_FAILED,
+ ERR_QUIC_QPACK_ENCODER_STREAM_ERROR,
+ ERR_QUIC_QPACK_DECODER_STREAM_ERROR,
+ },
+} = require('internal/errors');
+
+const {
+ kNewListener,
+ kRemoveListener,
+ defineEventHandler,
+ EventTarget,
+ Event,
+} = require('internal/event_target');
+
+const {
+ kHandle: kSocketAddressHandle,
+ InternalSocketAddress,
+} = require('internal/socketaddress');
+
+const {
+ kHandle,
+ kKeyObject,
+} = require('internal/crypto/util');
+
+const {
+ InternalX509Certificate,
+} = require('internal/crypto/x509');
+
+const {
+ validateBoolean,
+ validateNumber,
+ validateObject,
+ validateString,
+ validateUint32,
+} = require('internal/validators');
+
+const {
+ customInspectSymbol: kInspect,
+ kEmptyObject,
+ kEnumerableProperty,
+ createDeferredPromise,
+} = require('internal/util');
+
+const {
+ isArrayBufferView,
+ isCryptoKey,
+ isKeyObject,
+} = require('internal/util/types');
+
+const { inspect } = require('internal/util/inspect');
+
+const kDetach = Symbol('kDetach');
+const kOwner = Symbol('kOwner');
+const kCreateInstance = Symbol('kCreateEndpoint');
+const kListen = Symbol('kListen');
+const kFinishClose = Symbol('kFinishClose');
+const kNewSession = Symbol('kNewSession');
+const kClientHello = Symbol('kClientHello');
+const kOcspRequest = Symbol('kOcspRequest');
+const kOcspResponse = Symbol('kOcspResponse');
+const kDatagram = Symbol('kDatagram');
+const kHandshakeComplete = Symbol('kHandshakeComplete');
+const kSessionTicket = Symbol('kSessionTicket');
+const kVersionNegotiation = Symbol('kVersionNegotiation');
+const kPathValidation = Symbol('kPathValidation');
+const kNewStream = Symbol('kNewStream');
+const kStreamReset = Symbol('kStreamReset');
+const kStreamHeaders = Symbol('kStreamHeaders');
+const kStreamTrailers = Symbol('kStreamTrailers');
+const kStreamSendTrailers = Symbol('kStreamSendTrailers');
+const kStreamBlocked = Symbol('kStreamBlocked');
+const kStreamData = Symbol('kStreamData');
+const kDatagramStatus = Symbol('kDatagramStatus');
+
+/**
+ * @typedef {import('../../net').SocketAddress} SocketAddress
+ * @typedef {import('../crypto/keys').CryptoKey} CryptoKey
+ * @typedef {import('../crypto/keys').KeyObject} KeyObject
+ * @typedef {CryptoKey|KeyObject|Array>} Key
+ * @typedef {import('../crypto/x509').X509Certificate} X509Certificate
+ * @typedef {import('../blob').Blob} Blob
+ * @typedef {import('../webstreams/readablestream').ReadableStream} ReadableStream
+ * @typedef {import('../webstreams/writablestream').WritableStream} WritableStream
+ * @typedef {import('../webstreams/readablestream').QueuingStrategy} QueuingStrategy
+ * @typedef {import('../../stream').Readable} Readable
+ * @typedef {import('../../stream').Writable} Writable
+ * @typedef {import('../fs/promises').FileHandle} FileHandle
+ * @typedef {ArrayBufferViewSource, StreamSource, StreamBaseSource, BlobSource} StreamDataSource
+ */
+
+const StreamPriority = ObjectCreate(null, {
+ /** @type {number} */
+ DEFAULT: {
+ __proto__: null,
+ value: QUIC_STREAM_PRIORITY_DEFAULT,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ LOW: {
+ __proto__: null,
+ value: QUIC_STREAM_PRIORITY_LOW,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ HIGH: {
+ __proto__: null,
+ value: QUIC_STREAM_PRIORITY_HIGH,
+ enumerable: true,
+ configurable: false,
+ }
+});
+
+const StreamPriorityFlags = ObjectCreate(null, {
+ /** @type {number} */
+ NONE: {
+ __proto__: null,
+ value: QUIC_STREAM_PRIORITY_FLAGS_NONE,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ NON_INCREMENTAL: {
+ __proto__: null,
+ value: QUIC_STREAM_PRIORITY_FLAGS_NON_INCREMENTAL,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readonly
+ * @enum {string}
+ */
+const Direction = ObjectCreate(null, {
+ /** @type {string} */
+ BIDI: {
+ __proto__: null,
+ value: 'bidi',
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {string} */
+ UNI: {
+ __proto__: null,
+ value: 'uni',
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readonly
+ * @enum {string}
+ */
+const Side = ObjectCreate(null, {
+ /** @type {string} */
+ CLIENT: {
+ __proto__: null,
+ value: 'client',
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {string} */
+ SERVER: {
+ __proto__: null,
+ value: 'server',
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readonly
+ * @enum {number}
+ */
+const CongestionControlAlgorithm = ObjectCreate(null, {
+ /** @type {number} */
+ CUBIC: {
+ __proto__: null,
+ value: QUIC_CC_ALGO_CUBIC,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ RENO: {
+ __proto__: null,
+ value: QUIC_CC_ALGO_RENO,
+ enumerable: true,
+ configurable: false,
+ },
+ BBR: {
+ __proto__: null,
+ value: QUIC_CC_ALGO_BBR,
+ enumerable: true,
+ configurable: false,
+ },
+ BBR2: {
+ __proto__: null,
+ value: QUIC_CC_ALGO_BBR2,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readonly
+ * @enum {number}
+ */
+const PreferredAddressStrategy = ObjectCreate(null, {
+ /** @type {number} */
+ IGNORE: {
+ __proto__: null,
+ value: QUIC_PREFERRED_ADDRESS_IGNORE,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ USE: {
+ __proto__: null,
+ value: QUIC_PREFERRED_ADDRESS_USE,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readonly
+ * @enum {number}
+ */
+const TransportErrors = ObjectCreate(null, {
+ /** @type {number} */
+ NO_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_NO_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ INTERNAL_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_INTERNAL_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ CONNECTION_REFUSED: {
+ __proto__: null,
+ value: QUIC_ERR_CONNECTION_REFUSED,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ FLOW_CONTROL_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_FLOW_CONTROL_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ STREAM_LIMIT_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_STREAM_LIMIT_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ STREAM_STATE_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_STREAM_STATE_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ FINAL_SIZE_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_FINAL_SIZE_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ FRAME_ENCODING_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_FRAME_ENCODING_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ TRANSPORT_PARAMETER_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_TRANSPORT_PARAMETER_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ CONNECTION_ID_LIMIT_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_CONNECTION_ID_LIMIT_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ PROTOCOL_VIOLATION: {
+ __proto__: null,
+ value: QUIC_ERR_PROTOCOL_VIOLATION,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ INVALID_TOKEN: {
+ __proto__: null,
+ value: QUIC_ERR_INVALID_TOKEN,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ APPLICATION_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_APPLICATION_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ CRYPTO_BUFFER_EXCEEDED: {
+ __proto__: null,
+ value: QUIC_ERR_CRYPTO_BUFFER_EXCEEDED,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ KEY_UPDATE_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_KEY_UPDATE_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ AEAD_LIMIT_REACHED: {
+ __proto__: null,
+ value: QUIC_ERR_AEAD_LIMIT_REACHED,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ NO_VIABLE_PATH: {
+ __proto__: null,
+ value: QUIC_ERR_NO_VIABLE_PATH,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ CRYPTO_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_CRYPTO_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ VERSION_NEGOTIATION_ERROR_DRAFT: {
+ __proto__: null,
+ value: QUIC_ERR_VERSION_NEGOTIATION_ERROR_DRAFT,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/**
+ * @readaonly
+ * @enum {number}
+ */
+const Http3Errors = ObjectCreate(null, {
+ H3_GENERAL_PROTOCOL_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_GENERAL_PROTOCOL_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_INTERNAL_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_INTERNAL_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_STREAM_CREATION_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_STREAM_CREATION_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_CLOSED_CRITICAL_STREAM: {
+ __proto__: null,
+ value: QUIC_ERR_H3_CLOSED_CRITICAL_STREAM,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_FRAME_UNEXPECTED: {
+ __proto__: null,
+ value: QUIC_ERR_H3_FRAME_UNEXPECTED,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_FRAME_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_FRAME_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_EXCESSIVE_LOAD: {
+ __proto__: null,
+ value: QUIC_ERR_H3_EXCESSIVE_LOAD,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_ID_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_ID_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_SETTINGS_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_SETTINGS_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_MISSING_SETTINGS: {
+ __proto__: null,
+ value: QUIC_ERR_H3_MISSING_SETTINGS,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_REQUEST_REJECTED: {
+ __proto__: null,
+ value: QUIC_ERR_H3_REQUEST_REJECTED,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_REQUEST_CANCELLED: {
+ __proto__: null,
+ value: QUIC_ERR_H3_REQUEST_CANCELLED,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_REQUEST_INCOMPLETE: {
+ __proto__: null,
+ value: QUIC_ERR_H3_REQUEST_INCOMPLETE,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_MESSAGE_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_MESSAGE_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_CONNECT_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_H3_CONNECT_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ H3_VERSION_FALLBACK: {
+ __proto__: null,
+ value: QUIC_ERR_H3_VERSION_FALLBACK,
+ enumerable: true,
+ configurable: false,
+ },
+ QPACK_DECOMPRESSION_FAILED: {
+ __proto__: null,
+ value: QUIC_ERR_QPACK_DECOMPRESSION_FAILED,
+ enumerable: true,
+ configurable: false,
+ },
+ QPACK_ENCODER_STREAM_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_QPACK_ENCODER_STREAM_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+ QPACK_DECODER_STREAM_ERROR: {
+ __proto__: null,
+ value: QUIC_ERR_QPACK_DECODER_STREAM_ERROR,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+const constants = ObjectCreate(null, {
+ /** @type {string} */
+ HTTP3_ALPN: {
+ __proto__: null,
+ value: HTTP3_ALPN,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ MAX_CID: {
+ __proto__: null,
+ value: QUIC_MAX_CIDLEN,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_RETRYTOKEN_EXPIRATION: {
+ __proto__: null,
+ value: DEFAULT_RETRYTOKEN_EXPIRATION,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_TOKEN_EXPIRATION: {
+ __proto__: null,
+ value: DEFAULT_TOKEN_EXPIRATION,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_MAX_CONNECTIONS_PER_HOST: {
+ __proto__: null,
+ value: DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_MAX_CONNECTIONS: {
+ __proto__: null,
+ value: DEFAULT_MAX_CONNECTIONS,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_MAX_STATELESS_RESETS: {
+ __proto__: null,
+ value: DEFAULT_MAX_STATELESS_RESETS,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE: {
+ __proto__: null,
+ value: DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_MAX_RETRY_LIMIT: {
+ __proto__: null,
+ value: DEFAULT_MAX_RETRY_LIMIT,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD: {
+ __proto__: null,
+ value: DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_CIPHERS: {
+ __proto__: null,
+ value: DEFAULT_CIPHERS,
+ enumerable: true,
+ configurable: false,
+ },
+ /** @type {number} */
+ DEFAULT_GROUPS: {
+ __proto__: null,
+ value: DEFAULT_GROUPS,
+ enumerable: true,
+ configurable: false,
+ },
+ CongestionControlAlgorithm: {
+ __proto__: null,
+ value: CongestionControlAlgorithm,
+ enumerable: true,
+ configurable: false,
+ },
+ TransportErrors: {
+ __proto__: null,
+ value: TransportErrors,
+ enumerable: true,
+ configurable: false,
+ },
+ Http3Errors: {
+ __proto__: null,
+ value: Http3Errors,
+ enumerable: true,
+ configurable: false,
+ },
+ PreferredAddressStrategy: {
+ __proto__: null,
+ value: PreferredAddressStrategy,
+ enumerable: true,
+ configurable: false,
+ },
+ Direction: {
+ __proto__: null,
+ value: Direction,
+ enumerable: true,
+ configurable: false,
+ },
+ Side: {
+ __proto__: null,
+ value: Side,
+ enumerable: true,
+ configurable: false,
+ },
+ StreamPriority: {
+ __proto__: null,
+ value: StreamPriority,
+ enumerable: true,
+ configurable: false,
+ },
+ StreamPriorityFlags: {
+ __proto__: null,
+ value: StreamPriorityFlags,
+ enumerable: true,
+ configurable: false,
+ }
+});
+
+// ======================================================================================
+// Callbacks
+function onEndpointDone() {
+ this[kOwner][kFinishClose]();
+}
+
+function onEndpointError(context, code) {
+ this[kOwner][kFinishClose](makeEndpointError(context, code));
+}
+
+function onSessionNew(session) {
+ this[kOwner][kNewSession](session);
+}
+
+function onSessionClientHello(alpn, servername, ciphers) {
+ this[kOwner][kClientHello](alpn, servername, ciphers);
+}
+
+function onSessionOcspRequest() {
+ this[kOwner][kOcspRequest]();
+}
+
+function onSessionOcspResponse(response) {
+ this[kOwner][kOcspResponse](response);
+}
+
+function onSessionDatagram(datagram, isEarly) {
+ this[kOwner][kDatagram](datagram, isEarly);
+}
+
+function onSessionClose() {
+ this[kOwner][kFinishClose]();
+}
+
+function onSessionError(type, code, reason) {
+ this[kOwner][kFinishClose](makeQuicError(code, type, reason));
+}
+
+function onSessionHandshake(servername, alpn, cipherName, cipherVersion,
+ validationErrorReason, validationErrorCode,
+ allowEarlyData) {
+ this[kOwner][kHandshakeComplete](
+ servername, alpn, cipherName, cipherVersion, validationErrorReason,
+ validationErrorCode, allowEarlyData);
+}
+
+function onSessionTicket(sessionTicket) {
+ this[kOwner][kSessionTicket](sessionTicket);
+}
+
+function onSessionVersionNegotiation(current, requested, supported) {
+ this[kOwner][kVersionNegotiation](current, requested, supported);
+}
+
+function onSessionPathValidation(result, localAddress, remoteAddress, isPreferredAddress) {
+ this[kOwner][kPathValidation](
+ result,
+ new InternalSocketAddress(localAddress),
+ new InternalSocketAddress(remoteAddress),
+ isPreferredAddress);
+}
+
+function onStreamCreated(stream) {
+ this[kOwner][kNewStream](stream);
+}
+
+function onStreamClose() {
+ this[kOwner][kFinishClose]();
+}
+
+function onStreamError(type, code, reason) {
+ this[kOwner][kFinishClose](makeQuicError(code, type, reason));
+}
+
+function onStreamReset(type, code, reason) {
+ this[kOwner][kStreamReset](makeQuicError(code, type, reason));
+}
+
+function onStreamBlocked() {
+ this[kOwner][kStreamBlocked]();
+}
+
+function onStreamHeaders(headers, kind) {
+ this[kOwner][kStreamHeaders](headers, kind);
+}
+
+function onStreamTrailers() {
+ this[kOwner][kStreamTrailers]();
+}
+
+function onStreamData(chunks, ended) {
+ this[kOwner][kStreamData](chunks, ended);
+}
+
+function onSessionDatagramAcknowledged(datagram) {
+ this[kOwner][kDatagramStatus](datagram, false);
+}
+
+function onSessionDatagramLost(datagram) {
+ this[kOwner][kDatagramStatus](datagram, true);
+}
+
+setCallbacks({
+ onEndpointDone,
+ onEndpointError,
+ onSessionNew,
+ onSessionClientHello,
+ onSessionClose,
+ onSessionError,
+ onSessionDatagram,
+ onSessionHandshake,
+ onSessionOcspRequest,
+ onSessionOcspResponse,
+ onSessionTicket,
+ onSessionVersionNegotiation,
+ onSessionPathValidation,
+ onStreamBlocked,
+ onStreamClose,
+ onStreamCreated,
+ onStreamData,
+ onStreamError,
+ onStreamHeaders,
+ onStreamReset,
+ onStreamTrailers,
+ onSessionDatagramAcknowledged,
+ onSessionDatagramLost,
+});
+
+// ======================================================================================
+
+function makeQuicError(code, type, reason) {
+ const reasonString = reason ? `: ${reason}` : '';
+ switch (type) {
+ case QUIC_ERROR_TYPE_TRANSPORT: {
+ switch (code) {
+ case TransportErrors.INTERNAL_ERROR:
+ return new ERR_QUIC_INTERNAL_ERROR(reasonString);
+ case TransportErrors.CONNECTION_REFUSED:
+ return new ERR_QUIC_CONNECTION_REFUSED(reasonString);
+ case TransportErrors.FLOW_CONTROL_ERROR:
+ return new ERR_QUIC_FLOW_CONTROL_ERROR(reasonString);
+ case TransportErrors.STREAM_LIMIT_ERROR:
+ return new ERR_QUIC_STREAM_LIMIT_ERROR(reasonString);
+ case TransportErrors.STREAM_STATE_ERROR:
+ return new ERR_QUIC_STREAM_STATE_ERROR(reasonString);
+ case TransportErrors.FINAL_SIZE_ERROR:
+ return new ERR_QUIC_FINAL_SIZE_ERROR(reasonString);
+ case TransportErrors.FRAME_ENCODING_ERROR:
+ return new ERR_QUIC_FRAME_ENCODING_ERROR(reasonString);
+ case TransportErrors.TRANSPORT_PARAMETER_ERROR:
+ return new ERR_QUIC_TRANSPORT_PARAMETER_ERROR(reasonString);
+ case TransportErrors.CONNECTION_ID_LIMIT_ERROR:
+ return new ERR_QUIC_CONNECTION_ID_LIMIT_ERROR(reasonString);
+ case TransportErrors.PROTOCOL_VIOLATION:
+ return new ERR_QUIC_PROTOCOL_VIOLATION(reasonString);
+ case TransportErrors.INVALID_TOKEN:
+ return new ERR_QUIC_INVALID_TOKEN(reasonString);
+ case TransportErrors.APPLICATION_ERROR:
+ return new ERR_QUIC_APPLICATION_ERROR(reasonString);
+ case TransportErrors.CRYPTO_BUFFER_EXCEEDED:
+ return new ERR_QUIC_CRYPTO_BUFFER_EXCEEDED(reasonString);
+ case TransportErrors.KEY_UPDATE_ERROR:
+ return new ERR_QUIC_KEY_UPDATE_ERROR(reasonString);
+ case TransportErrors.AEAD_LIMIT_REACHED:
+ return new ERR_QUIC_AEAD_LIMIT_REACHED(reasonString);
+ case TransportErrors.NO_VIABLE_PATH:
+ return new ERR_QUIC_NO_VIABLE_PATH(reasonString);
+ case TransportErrors.CRYPTO_ERROR:
+ return new ERR_QUIC_CRYPTO_ERROR(reasonString);
+ }
+ break;
+ }
+ case QUIC_ERROR_TYPE_APPLICATION: {
+ switch (code) {
+ case Http3Errors.H3_GENERAL_PROTOCOL_ERROR:
+ return new ERR_QUIC_H3_GENERAL_PROTOCOL_ERROR(reasonString);
+ case Http3Errors.H3_INTERNAL_ERROR:
+ return new ERR_QUIC_H3_INTERNAL_ERROR(reasonString);
+ case Http3Errors.H3_STREAM_CREATION_ERROR:
+ return new ERR_QUIC_H3_STREAM_CREATION_ERROR(reasonString);
+ case Http3Errors.H3_CLOSED_CRITICAL_STREAM:
+ return new ERR_QUIC_H3_CLOSED_CRITICAL_STREAM(reasonString);
+ case Http3Errors.H3_FRAME_UNEXPECTED:
+ return new ERR_QUIC_H3_FRAME_UNEXPECTED(reasonString);
+ case Http3Errors.H3_FRAME_ERROR:
+ return new ERR_QUIC_H3_FRAME_ERROR(reasonString);
+ case Http3Errors.H3_EXCESSIVE_LOAD:
+ return new ERR_QUIC_H3_EXCESSIVE_LOAD(reasonString);
+ case Http3Errors.H3_ID_ERROR:
+ return new ERR_QUIC_H3_ID_ERROR(reasonString);
+ case Http3Errors.H3_SETTINGS_ERROR:
+ return new ERR_QUIC_H3_SETTINGS_ERROR(reasonString);
+ case Http3Errors.H3_MISSING_SETTINGS:
+ return new ERR_QUIC_H3_MISSING_SETTINGS(reasonString);
+ case Http3Errors.H3_REQUEST_REJECTED:
+ return new ERR_QUIC_H3_REQUEST_REJECTED(reasonString);
+ case Http3Errors.H3_REQUEST_CANCELLED:
+ return new ERR_QUIC_H3_REQUEST_CANCELLED(reasonString);
+ case Http3Errors.H3_REQUEST_INCOMPLETE:
+ return new ERR_QUIC_H3_REQUEST_INCOMPLETE(reasonString);
+ case Http3Errors.H3_MESSAGE_ERROR:
+ return new ERR_QUIC_H3_MESSAGE_ERROR(reasonString);
+ case Http3Errors.H3_CONNECT_ERROR:
+ return new ERR_QUIC_H3_CONNECT_ERROR(reasonString);
+ case Http3Errors.H3_VERSION_FALLBACK:
+ return new ERR_QUIC_H3_VERSION_FALLBACK(reasonString);
+ case Http3Errors.QPACK_DECOMPRESSION_FAILED:
+ return new ERR_QUIC_QPACK_DECOMPRESSION_FAILED(reasonString);
+ case Http3Errors.QPACK_ENCODER_STREAM_ERROR:
+ return new ERR_QUIC_QPACK_ENCODER_STREAM_ERROR(reasonString);
+ case Http3Errors.QPACK_DECODER_STREAM_ERROR:
+ return new ERR_QUIC_QPACK_DECODER_STREAM_ERROR(reasonString);
+ }
+ break;
+ }
+ case QUIC_ERROR_TYPE_VERSION_NEGOTIATION:
+ return new ERR_QUIC_VERSION_NEGOTIATION_ERROR(reasonString);
+ case QUIC_ERROR_TYPE_IDLE_CLOSE:
+ return new ERR_QUIC_IDLE_CLOSE(reasonString);
+ }
+ return new ERR_QUIC_UNKNOWN_ERROR(code, reasonString);
+}
+
+function makeEndpointError(context, code) {
+ switch (context) {
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_CLOSE: return undefined;
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_BIND_FAILURE:
+ return new ERR_QUIC_ENDPOINT_FAILURE(code, 'bind failed');
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_START_FAILURE:
+ return new ERR_QUIC_ENDPOINT_FAILURE(code, 'start failed');
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_RECEIVE_FAILURE:
+ return new ERR_QUIC_ENDPOINT_FAILURE(code, 'receive failed');
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_SEND_FAILURE:
+ return new ERR_QUIC_ENDPOINT_FAILURE(code, 'send failed');
+ case QUIC_ENDPOINT_CLOSE_CONTEXT_LISTEN_FAILURE:
+ return new ERR_QUIC_ENDPOINT_FAILURE(code, 'listen failed');
+ }
+ return new ERR_QUIC_UNKNOWN_ERROR(code, 'unknown');
+}
+
+// ======================================================================================
+// Validators
+function validateSocketAddress(address, name) {
+ if (address[kSocketAddressHandle] === undefined)
+ throw new ERR_INVALID_ARG_TYPE(name, 'SocketAddress', address);
+}
+
+function validateUint8(value, name) {
+ if (typeof value !== 'number' || value < 0 || value > 0xff)
+ throw new ERR_INVALID_ARG_TYPE(name, 'octet', value);
+}
+
+function validateUint64(value, name) {
+ if ((typeof value === 'bigint' || NumberIsInteger(value)) && BigInt(value) >= 0n) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'unsigned 64-bit integer (bigint)', value);
+}
+
+function validateStreamPriority(value, name) {
+ if (value === StreamPriority.DEFAULT ||
+ value === StreamPriority.LOW ||
+ value === StreamPriority.HIGH) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'StreamPriority', value);
+}
+
+function validateStreamPriorityFlags(value, name) {
+ if (value === StreamPriorityFlags.NONE ||
+ value === StreamPriorityFlags.NON_INCREMENTAL) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'StreamPriorityFlags', value);
+}
+
+function validateDirection(value, name) {
+ if (value === Direction.BIDI || value === Direction.UNI) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'Direction', value);
+}
+
+function validateCongestionControlAlgorithm(value, name) {
+ if (value === CongestionControlAlgorithm.CUBIC ||
+ value === CongestionControlAlgorithm.RENO) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'CongestionControlAlgorithm', value);
+}
+
+function validatePreferredAddressStrategy(value, name) {
+ if (value === PreferredAddressStrategy.IGNORE ||
+ value === PreferredAddressStrategy.USE) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'PreferredAddressStrategy', value);
+}
+
+function validateKey(key, name) {
+ if (isCryptoKey(key) || isKeyObject(key)) return;
+ throw new ERR_INVALID_ARG_TYPE(
+ name,
+ [
+ 'CryptoKey',
+ 'KeyObject',
+ 'Array',
+ 'Array',
+ ], key);
+}
+
+function validateArrayBufferView(value, name) {
+ if (isArrayBufferView(value)) return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'ArrayBufferView', value);
+}
+
+function validateSessionOptions(value, name) {
+ if (typeof value?.[kCreateInstance] === 'function' &&
+ value?.constructor?.name === 'SessionOptions') return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'SessionOptions', value);
+}
+
+function validateEndpointOptions(value, name) {
+ if (typeof value?.[kCreateInstance] === 'function' &&
+ value?.constructor?.name === 'EndpointOptions') return;
+ throw new ERR_INVALID_ARG_TYPE(name, 'EndpointOptions', value);
+}
+
+// ======================================================================================
+// Events
+
+/**
+ * Indicates that an Endpoint, Session, or Stream has closed. The close
+ * event will only ever be emitted once. The event may be followed by
+ * the error event.
+ * @event close
+ */
+class CloseEvent extends Event {
+ constructor() {
+ super('close');
+ }
+}
+
+/**
+ * Indicates that an error has occurred causing the Endpoint, Session, or
+ * Stream to be closed. The error event will always be emitted after the
+ * close event.
+ * @event error
+ * @property {any} error
+ */
+class ErrorEvent extends Event {
+ #error;
+ constructor(error) {
+ super('error');
+ this.#error = error;
+ }
+
+ get error() { return this.#error; }
+}
+
+ObjectDefineProperties(ErrorEvent.prototype, {
+ error: kEnumerableProperty,
+});
+
+/**
+ * Indicates that a new server-side Session has been initiated by an Endpoint.
+ * @event session
+ * @property {Session} session
+ */
+class SessionEvent extends Event {
+ #session;
+ #endpoint;
+ constructor(endpoint, session) {
+ super('session');
+ this.#endpoint = endpoint;
+ this.#session = session;
+ }
+
+ /** @type {Endpoint} */
+ get endpoint() { return this.#endpoint; }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+}
+
+ObjectDefineProperties(SessionEvent.prototype, {
+ session: kEnumerableProperty,
+ endpoint: kEnumerableProperty,
+});
+
+/**
+ * Indicates that a new peer-initiated stream has been opened on the session.
+ * @event stream
+ * @property {Stream} stream
+ */
+class StreamEvent extends Event {
+ #stream;
+ #session;
+ constructor(session, stream) {
+ super('stream');
+ this.#stream = stream;
+ this.#session = session;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+}
+
+ObjectDefineProperties(StreamEvent.prototype, {
+ stream: kEnumerableProperty,
+ session: kEnumerableProperty,
+});
+
+/**
+ * Indicates that a datagram has been received on the session.
+ * @event datagram
+ * @property {Datagram} datagram
+ */
+class DatagramEvent extends Event {
+ #datagram;
+ #session;
+ #early;
+ constructor(session, datagram, early) {
+ super('datagram');
+ this.#session = session;
+ this.#datagram = datagram;
+ this.#early = early;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {Uint8Array} */
+ get datagram() { return this.#datagram; }
+
+ /** @type {boolean} */
+ get early() { return this.#early; }
+}
+
+ObjectDefineProperties(DatagramEvent.prototype, {
+ datagram: kEnumerableProperty,
+ session: kEnumerableProperty,
+ early: kEnumerableProperty,
+});
+
+/**
+ * @event datagram-status
+ */
+class DatagramStatusEvent extends Event {
+ #lost;
+ #session;
+ #datagram;
+
+ constructor(session, datagram, lost) {
+ super('datagram-status');
+ this.#session = session;
+ this.#datagram = datagram;
+ this.#lost = lost;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {bigint} */
+ get datagram() { return this.#datagram; }
+
+ /** @type {boolean} */
+ get lost() { return this.#lost; }
+}
+
+ObjectDefineProperties(DatagramStatusEvent.prototype, {
+ datagram: kEnumerableProperty,
+ session: kEnumerableProperty,
+ lost: kEnumerableProperty,
+});
+
+/**
+ * When OCSP is enabled for a session, this event is emitted only by server
+ * Sessions when an OCSP request has been received during the TLS handshake.
+ * The handshake will be paused until the done() method is called providing
+ * a response to the OCSP request. If done() is not called, the handshake will
+ * timeout and the session will be closed.
+ * @event ocsp
+ */
+class OCSPRequestEvent extends Event {
+ #handle;
+ #session;
+ #done = false;
+ constructor(handle, session) {
+ super('ocsp');
+ this.#handle = handle;
+ this.#session = session;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ done() {
+ if (this.#done)
+ throw new ERR_INVALID_STATE('This OCSP request is already complete.');
+ this.#done = true;
+ this.#handle.onOCSPDone();
+ }
+}
+
+ObjectDefineProperties(OCSPRequestEvent.prototype, {
+ session: kEnumerableProperty,
+});
+
+/**
+ * When OCSP is enabled for a session, this event is emitted only by client
+ * sessions when an OCSP response has been received during the TLS handshake.
+ * @event ocsp
+ * @property {Uint8Array} response
+ */
+class OCSPResponseEvent extends Event {
+ #response;
+ #session;
+ constructor(session, response) {
+ super('ocsp');
+ this.#session = session;
+ this.#response = response;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {Uint8Array} */
+ get response() { return this.#response; }
+}
+
+ObjectDefineProperties(OCSPResponseEvent.prototype, {
+ response: kEnumerableProperty,
+ session: kEnumerableProperty,
+});
+
+/**
+ * Indicates the start of a TLS handshake. The client-hello event is only
+ * emitted by Server sessions and provides the ability to specify new
+ * keys or certificates to use for the session based on the ALPN and
+ * servername requested. The TLS handshake will be paused until the done()
+ * method is called. If done() is not called, the handshake will timeout and
+ * the session will be closed.
+ * @event client-hello
+ */
+class ClientHelloEvent extends Event {
+ #handle;
+ #alpn;
+ #servername;
+ #ciphers;
+ #session;
+ #done = false;
+ constructor(handle, session, alpn, servername, ciphers) {
+ super('client-hello');
+ this.#handle = handle;
+ this.#session = session;
+ this.#alpn = alpn;
+ this.#servername = servername;
+ this.#ciphers = ciphers;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {string} */
+ get alpn() { return this.#alpn; }
+
+ /** @type {string} */
+ get servername() { return this.#servername; }
+
+ /**
+ * @typedef {{
+ * name: string,
+ * standardName: string,
+ * version: string,
+ * }} Cipher
+ * @type {Cipher[]}
+ */
+ get ciphers() { return this.#ciphers; }
+
+ done() {
+ if (this.#done)
+ throw new ERR_INVALID_STATE('This client hello is already complete.');
+ this.#done = true;
+ this.#handle.onClientHelloDone();
+ }
+}
+
+ObjectDefineProperties(ClientHelloEvent.prototype, {
+ session: kEnumerableProperty,
+ alpn: kEnumerableProperty,
+ servername: kEnumerableProperty,
+ ciphers: kEnumerableProperty,
+});
+
+/**
+ * Emitted on client Sessions when a new SessionTicket is available.
+ * SessionTicket's are used for TLS session resumption.
+ * @event session-ticket
+ */
+class SessionTicketEvent extends Event {
+ #sessionTicket;
+ #session;
+ constructor(session, ticket) {
+ super('session-ticket');
+ this.#sessionTicket = ticket;
+ this.#session = session;
+ }
+
+ /**
+ * @typedef {{}} SessionTicket
+ * @type {SessionTicket}
+ */
+ get ticket() { return this.#sessionTicket; }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+}
+
+ObjectDefineProperties(SessionTicketEvent.prototype, {
+ ticket: kEnumerableProperty,
+ session: kEnumerableProperty,
+});
+
+/**
+ * Emitted when path validation is enabled for a session and a network
+ * path validation result is available.
+ * @event 'path-validation'
+ */
+class PathValidationEvent extends Event {
+ #result;
+ #localAddress;
+ #remoteAddress;
+ #preferredAddress;
+ #session;
+
+ constructor(session, result, localAddress, remoteAddress, isPreferredAddress) {
+ super('path-validation');
+ this.#session = session;
+ this.#result = result;
+ this.#localAddress = localAddress;
+ this.#remoteAddress = remoteAddress;
+ this.#preferredAddress = isPreferredAddress;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {string} */
+ get result() { return this.#result; }
+
+ /** @type {SocketAddress} */
+ get localAddress() { return this.#localAddress; }
+
+ /** @type {SocketAddress} */
+ get remoteAddress() { return this.#remoteAddress; }
+
+ /** @type {boolean} */
+ get preferredAddress() { return this.#preferredAddress; }
+
+}
+
+ObjectDefineProperties(PathValidationEvent.prototype, {
+ result: kEnumerableProperty,
+ localAddress: kEnumerableProperty,
+ remoteAddress: kEnumerableProperty,
+ preferredAddress: kEnumerableProperty,
+ session: kEnumerableProperty,
+});
+
+/**
+ * Emitted by client Sessions when a session request has been refused by the
+ * server due to a version mismatch.
+ * @event 'version-negotiation'
+ */
+class VersionNegotiationEvent extends Event {
+ #current;
+ #requested;
+ #supported;
+ #session;
+
+ constructor(session, current, requested, supported) {
+ super('version-negotiation');
+ this.#session = session;
+ this.#current = current;
+ this.#requested = requested;
+ this.#supported = supported;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {number} */
+ get current() { return this.#current; }
+
+ /** @type {number[]} */
+ get requested() { return this.#requested; }
+
+ /** @type {number[]} */
+ get supported() { return this.#supported; }
+}
+
+ObjectDefineProperties(VersionNegotiationEvent.prototype, {
+ current: kEnumerableProperty,
+ requested: kEnumerableProperty,
+ supported: kEnumerableProperty,
+ session: kEnumerableProperty,
+});
+
+/**
+ * Emitted when the TLS handshake has been completed.
+ * @event 'handshake-complete'
+ */
+class HandshakeCompleteEvent extends Event {
+ #session;
+ #servername;
+ #alpn;
+ #cipherName;
+ #cipherVersion;
+ #validationErrorReason;
+ #validationErrorCode;
+ #allowEarlyData;
+
+ constructor(session, servername, alpn, cipherName, cipherVersion,
+ validationErrorReason, validationErrorCode,
+ allowEarlyData) {
+ super('handshake-complete');
+ this.#session = session;
+ this.#servername = servername;
+ this.#alpn = alpn;
+ this.#cipherName = cipherName;
+ this.#cipherVersion = cipherVersion;
+ this.#validationErrorReason = validationErrorReason;
+ this.#validationErrorCode = validationErrorCode;
+ this.#allowEarlyData = allowEarlyData;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {string} */
+ get servername() { return this.#servername; }
+
+ /** @type {string} */
+ get alpn() { return this.#alpn; }
+
+ /** @type {string} */
+ get cipherName() { return this.#cipherName; }
+
+ /** @type {string} */
+ get cipherVersion() { return this.#cipherVersion; }
+
+ /** @type {number} */
+ get validationErrorReason() { return this.#validationErrorReason; }
+
+ /** @type {number} */
+ get validationErrorCode() { return this.#validationErrorCode; }
+
+ /** @type {boolean} */
+ get allowEarlyData() { return this.#allowEarlyData; }
+}
+
+ObjectDefineProperties(HandshakeCompleteEvent.prototype, {
+ servername: kEnumerableProperty,
+ alpn: kEnumerableProperty,
+ cipherName: kEnumerableProperty,
+ cipherVersion: kEnumerableProperty,
+ validationErrorReason: kEnumerableProperty,
+ validationErrorCode: kEnumerableProperty,
+ allowEarlyData: kEnumerableProperty,
+});
+
+/**
+ * Emitted on a stream when a stream reset has been received from the peer.
+ * @event stream-reset
+ */
+class StreamResetEvent extends Event {
+ #stream;
+ #error;
+ constructor(stream, error) {
+ super('stream-reset');
+ this.#stream = stream;
+ this.#error = error;
+ }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+
+ /** @type {any} */
+ get error() { return this.#error; }
+}
+
+ObjectDefineProperties(StreamResetEvent.prototype, {
+ error: kEnumerableProperty,
+ stream: kEnumerableProperty,
+});
+
+/**
+ * Emitted when a block of headers has been received for the stream.
+ * @event headers
+ */
+class HeadersEvent extends Event {
+ #stream;
+ #headers;
+ #kind;
+
+ constructor(stream, headers, kind) {
+ super('headers');
+ this.#stream = stream;
+ this.#headers = headers;
+ this.#kind = kind;
+ }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+
+ /** @type {Record} */
+ get headers() { return this.#headers; }
+
+ /** @type {HeadersKind} */
+ get kind() { return this.#kind; }
+}
+
+ObjectDefineProperties(HeadersEvent.prototype, {
+ headers: kEnumerableProperty,
+ kind: kEnumerableProperty,
+ stream: kEnumerableProperty,
+});
+
+/**
+ * Emitted when the underlying session is ready to receive trailing headers
+ * for the stream. The trailers are provided by calling the send() method
+ * on the TrailersEvent object. The stream will be held open until the
+ * trailers are provided or the idle timeout elapses.
+ * @event trailers
+ */
+class TrailersEvent extends Event {
+ #stream;
+ #done = false;
+ constructor(stream) {
+ super('trailers');
+ this.#stream = stream;
+ }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+
+ /**
+ * @param {Record trailers} trailers
+ */
+ send(trailers) {
+ if (this.#done)
+ throw new ERR_INVALID_STATE('The trailers have already been sent');
+ this.#done = true;
+ this.#stream[kStreamSendTrailers](trailers);
+ }
+}
+
+ObjectDefineProperties(TrailersEvent.prototype, {
+ stream: kEnumerableProperty,
+});
+
+/**
+ * @event data
+ */
+class DataEvent extends Event {
+ #stream;
+ #chunks;
+ #ended;
+ constructor(stream, chunks, ended) {
+ super('data');
+ this.#stream = stream;
+ this.#chunks = chunks;
+ this.#ended = ended;
+ }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+
+ /** @type {Uint8Array[]} */
+ get chunks() { return this.#chunks; }
+
+ /** @type {boolean} */
+ get ended() { return this.#ended; }
+}
+
+ObjectDefineProperties(DataEvent.prototype, {
+ chunks: kEnumerableProperty,
+ ended: kEnumerableProperty,
+ stream: kEnumerableProperty,
+});
+
+
+// ======================================================================================
+// Stream
+class StreamStats {
+ #inner;
+ #detached = false;
+
+ constructor(stats) {
+ this.#inner = stats;
+ }
+
+ /** @type {boolean} */
+ get detached() { return this.#detached; }
+
+ /** @type {bigint} */
+ get createdAt() {
+ return this.#inner[IDX_STATS_STREAM_CREATED_AT];
+ }
+
+ /** @type {bigint} */
+ get acknowledgedAt() {
+ return this.#inner[IDX_STATS_STREAM_ACKED_AT];
+ }
+
+ /** @type {bigint} */
+ get closingAt() {
+ return this.#inner[IDX_STATS_STREAM_CLOSING_AT];
+ }
+
+ /** @type {bigint} */
+ get destroyedAt() {
+ return this.#inner[IDX_STATS_STREAM_DESTROYED_AT];
+ }
+
+ /** @type {bigint} */
+ get bytesReceived() {
+ return this.#inner[IDX_STATS_STREAM_BYTES_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get bytesSent() {
+ return this.#inner[IDX_STATS_STREAM_BYTES_SENT];
+ }
+
+ /** @type {bigint} */
+ get maxOffset() {
+ return this.#inner[IDX_STATS_STREAM_MAX_OFFSET];
+ }
+
+ /** @type {bigint} */
+ get maxOffsetAcknowledged() {
+ return this.#inner[IDX_STATS_STREAM_MAX_OFFSET_ACK];
+ }
+
+ /** @type {bigint} */
+ get maxOffsetReceived() {
+ return this.#inner[IDX_STATS_STREAM_MAX_OFFSET_RECV];
+ }
+
+ /** @type {bigint} */
+ get finalSize() {
+ return this.#inner[IDX_STATS_STREAM_FINAL_SIZE];
+ }
+
+ [kDetach]() {
+ this.#inner = new BigUint64Array(this.#inner);
+ this.#detached = true;
+ }
+
+ [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,
+ acknowledgedAt: this.acknowledgedAt,
+ closingAt: this.closingAt,
+ destroyedAt: this.destroyedAt,
+ bytesReceived: this.bytesReceived,
+ bytesSent: this.bytesSent,
+ maxOffset: this.maxOffset,
+ maxOffsetAcknowledged: this.maxOffsetAcknowledged,
+ maxOffsetReceived: this.maxOffsetReceived,
+ finalSize: this.finalSize,
+ detached: this.detached,
+ }, opts)}}`;
+ }
+}
+
+ObjectDefineProperties(StreamStats.prototype, {
+ detached: kEnumerableProperty,
+ createdAt: kEnumerableProperty,
+ acknowledgedAt: kEnumerableProperty,
+ closingAt: kEnumerableProperty,
+ destroyedAt: kEnumerableProperty,
+ bytesReceived: kEnumerableProperty,
+ bytesSent: kEnumerableProperty,
+ maxOffset: kEnumerableProperty,
+ maxOffsetAcknowledged: kEnumerableProperty,
+ maxOffsetReceived: kEnumerableProperty,
+ finalSize: kEnumerableProperty,
+});
+
+class StreamState {
+ #inner;
+ constructor(state) {
+ this.#inner = new DataView(state);
+ }
+
+ get id() {
+ return this.#inner.getBigInt64(IDX_STATE_STREAM_ID);
+ }
+
+ get finSent() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_FIN_SENT);
+ }
+
+ get finReceived() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_FIN_RECEIVED);
+ }
+
+ get readEnded() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_READ_ENDED);
+ }
+
+ get trailers() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_TRAILERS);
+ }
+
+ get destroyed() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_DESTROYED);
+ }
+
+ get data() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_DATA);
+ }
+
+ set data(on = true) {
+ this.#inner.setUint8(IDX_STATE_STREAM_DATA, on ? 1 : 0);
+ }
+
+ get paused() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_PAUSED);
+ }
+
+ set paused(on = true) {
+ this.#inner.setUint8(IDX_STATE_STREAM_PAUSED, on ? 1 : 0);
+ }
+
+ get reset() {
+ return this.#inner.getUint8(IDX_STATE_STREAM_RESET);
+ }
+}
+
+class Stream extends EventTarget {
+ #inner;
+ #id;
+ #blocked = false;
+ #closed = false;
+ #session;
+ #stats;
+ #state;
+ #headers;
+ #trailers;
+
+ constructor(session, handle) {
+ super();
+ this.#session = session;
+ this.#inner = handle;
+ this.#inner[kOwner] = this;
+ this.#stats = new StreamStats(this.#inner.stats);
+ this.#state = new StreamState(this.#inner.state);
+ this.#id = this.#state.id;
+ }
+
+ /**
+ * @type {boolean}
+ */
+ get blocked() {
+ return this.#blocked;
+ }
+
+ /**
+ * @type {boolean}
+ */
+ get closed() {
+ return this.#closed;
+ }
+
+ /**
+ * @type {bigint}
+ */
+ get id() {
+ return this.#id;
+ }
+
+ /**
+ * @type {Direction}
+ */
+ get direction() {
+ return this.#id & 0b10n ? Direction.UNI : Direction.BIDI;
+ }
+
+ /**
+ * @type {Side}
+ */
+ get origin() {
+ return this.#id & 0b01n ? Side.SERVER : Side.CLIENT;
+ }
+
+ /**
+ * @type {Session}
+ */
+ get session() {
+ return this.#session;
+ }
+
+ /**
+ * @type {Record}
+ */
+ get headers() {
+ return this.#headers;
+ }
+
+ /**
+ * @type {Record}
+ */
+ get trailers() {
+ return this.#trailers;
+ }
+
+ /**
+ * @type {StreamStats}
+ */
+ get stats() {
+ return this.#stats;
+ }
+
+ /**
+ * @param {StreamDataSource} source The source of outbound data for this stream.
+ */
+ attachSource(source) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('This stream is closed.');
+ this.#inner.attachSource(source);
+ return this;
+ }
+
+ /**
+ * @returns {Stream}
+ */
+ resume() {
+ if (this.#state !== undefined && this.#state.paused) {
+ this.#state.paused = false;
+ this.#inner.flushInbound();
+ }
+ return this;
+ }
+
+ /**
+ * @returns {Stream}
+ */
+ pause() {
+ if (this.#state !== undefined)
+ this.#state.paused = true;
+ return this;
+ }
+
+ /**
+ * Sends a request to the peer indicating that it should stop sending data.
+ * Has the side effect of closing the readable side of the stream. After this,
+ * no additional data events will be received.
+ * @param {bigint} [code] An application error code.
+ */
+ stopSending(code = 0) {
+ if (this.#inner === undefined) return;
+ validateUint64(code, 'code');
+ this.#inner.stopSending(code);
+ }
+
+ /**
+ * Sends a signal to the peer that the writable side of the stream is being
+ * abruptly terminated. Has the side effect of closing the writable side of
+ * stream. Data events can still occur.
+ * @param {bigint} [code] An application error code.
+ */
+ reset(code) {
+ if (this.#inner === undefined) return;
+ validateUint64(code, 'code');
+ this.#inner.resetStream(code);
+ }
+
+ /**
+ * Sets the priority of this stream if supported by the selection alpn application.
+ * @param {StreamPriority} priority
+ * @param {StreamPriorityFlags} flag
+ * @returns {Stream}
+ */
+ setPriority(priority = StreamPriority.DEFAULT, flag = StreamPriorityFlags.NONE) {
+ if (!this.#session.prioritySupported || !this.#inner) return;
+ validateStreamPriority(priority, 'priority');
+ validateStreamPriorityFlags(flag, 'flag');
+ this.#inner.setPriority(priority, flag);
+ return this;
+ }
+
+ getPriority() {
+ if (!this.#session.prioritySupported || !this.#inner) return StreamPriority.DEFAULT;
+ return this.#inner.getPriority();
+ }
+
+ /**
+ * @param {string[][]} headers
+ */
+ sendInfoHeaders(headers) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The stream has been closed.');
+ this.#inner.sendHEaders(QUIC_HEADERS_KIND_INFO,
+ headers,
+ QUIC_HEADERS_FLAGS_NONE);
+ }
+
+ /**
+ * @param {string[][]} headers
+ * @param {{
+ * terminal? : boolean
+ * }} options
+ */
+ sendInitialHeaders(headers, options = kEmptyObject) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The stream has been closed.');
+
+ validateObject(options, 'options');
+ const {
+ terminal = false,
+ } = options;
+ validateBoolean(terminal, 'options.terminal');
+
+ this.#inner.sendHEaders(QUIC_HEADERS_KIND_INITIAL,
+ headers,
+ terminal ? QUIC_HEADERS_FLAGS_TERMINAL : QUIC_HEADERS_FLAGS_NONE);
+ }
+
+ [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,
+ origin: this.origin,
+ closed: this.closed,
+ blocked: this.blocked,
+ state: this.#state,
+ stats: this.#stats,
+ }, opts)}}`;
+ }
+
+ [kFinishClose](maybeError) {
+ this.#stats[kDetach]();
+ this.#state = undefined;
+ this.#inner = undefined;
+ this.dispatchEvent(new CloseEvent());
+ if (maybeError)
+ this.dispatchEvent(new ErrorEvent(maybeError));
+ }
+
+ [kStreamReset](error) {
+ // Importantly, the stream reset event is not terminal unless the
+ // application wants it to be. It just means that the peer has
+ // abruptly terminated their side of the stream. We can keep sending
+ // unless the peer also sends a stop sending.
+ this.dispatchEvent(new StreamResetEvent(this, error));
+ }
+
+ [kStreamHeaders](headers, kind) {
+ this.dispatchEvent(new HeadersEvent(this, headers, kind));
+ }
+
+ [kStreamTrailers]() {
+ this.dispatchEvent(new TrailersEvent(this));
+ }
+
+ [kStreamSendTrailers](trailers) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The stream has been closed.');
+ // TODO(@jasnell): Convert the trailers to the appropriate structure.
+ this.#inner.sendHeaders(QUIC_HEADERS_KIND_TRAILING,
+ trailers,
+ QUIC_HEADERS_FLAGS_TERMINAL);
+ }
+
+ [kStreamData](data, ended) {
+ this.dispatchEvent(new DataEvent(this, data, ended));
+ }
+
+ [kNewListener](size, type, listener, once, capture, passive, weak) {
+ super[kNewListener](size, type, listener, once, capture, passive, weak);
+
+ if (this.#state === undefined) return;
+
+ switch (type) {
+ case 'data':
+ this.#state.data = true;
+ if (!this.#state.paused)
+ this.#inner.flushInbound();
+ break;
+ }
+ }
+ [kRemoveListener](size, type, listener, capture) {
+ super[kRemoveListener](size, type, listener, capture);
+
+ if (this.#state === undefined) return;
+
+ switch (type) {
+ case 'data':
+ this.#state.data = false;
+ break;
+ }
+ }
+}
+
+ObjectDefineProperties(Stream.prototype, {
+ blocked: kEnumerableProperty,
+ closed: kEnumerableProperty,
+ id: kEnumerableProperty,
+ direction: kEnumerableProperty,
+ origin: kEnumerableProperty,
+ session: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ trailers: kEnumerableProperty,
+ stats: kEnumerableProperty,
+});
+
+defineEventHandler(Stream.prototype, 'close');
+defineEventHandler(Stream.prototype, 'error');
+defineEventHandler(Stream.prototype, 'streamreset', 'stream-reset');
+defineEventHandler(Stream.prototype, 'headers');
+defineEventHandler(Stream.prototype, 'trailers');
+defineEventHandler(Stream.prototype, 'data');
+
+// ======================================================================================
+// Session
+
+class SessionStats {
+ #stats;
+ #detached = false;
+
+ constructor(stats) {
+ this.#stats = stats;
+ }
+
+ [kDetach]() {
+ this.#detached = true;
+ this.stats = new BigUint64Array(this.#stats);
+ }
+
+ /** @type {boolean} */
+ get detached() { return this.#detached; }
+
+ /** @type {bigint} */
+ get createdAt() {
+ return this.#stats[IDX_STATS_SESSION_CREATED_AT];
+ }
+
+ /** @type {bigint} */
+ get handshakeCompletedAt() {
+ return this.#stats[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
+ }
+
+ /** @type {bigint} */
+ get handshakeConfirmedAt() {
+ return this.#stats[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
+ }
+
+ /** @type {bigint} */
+ get gracefulClosingAt() {
+ return this.#stats[IDX_STATS_SESSION_GRACEFUL_CLOSING_AT];
+ }
+
+ /** @type {bigint} */
+ get closingAt() {
+ return this.#stats[IDX_STATS_SESSION_CLOSING_AT];
+ }
+
+ /** @type {bigint} */
+ get destroyedAt() {
+ return this.#stats[IDX_STATS_SESSION_DESTROYED_AT];
+ }
+
+ /** @type {bigint} */
+ get bytesReceived() {
+ return this.#stats[IDX_STATS_SESSION_BYTES_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get bytesSent() {
+ return this.#stats[IDX_STATS_SESSION_BYTES_SENT];
+ }
+
+ /** @type {bigint} */
+ get bidiStreamCount() {
+ return this.#stats[IDX_STATS_SESSION_BIDI_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get uniStreamCount() {
+ return this.#stats[IDX_STATS_SESSION_UNI_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get inboundStreamsCount() {
+ return this.#stats[IDX_STATS_SESSION_STREAMS_IN_COUNT];
+ }
+
+ /** @type {bigint} */
+ get outboundStreamsCount() {
+ return this.#stats[IDX_STATS_SESSION_STREAMS_OUT_COUNT];
+ }
+
+ /** @type {bigint} */
+ get keyUpdateCount() {
+ return this.#stats[IDX_STATS_SESSION_KEYUPDATE_COUNT];
+ }
+
+ /** @type {bigint} */
+ get retransmitCount() {
+ return this.#stats[IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT];
+ }
+
+ /** @type {bigint} */
+ get maxBytesInFlight() {
+ return this.#stats[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
+ }
+
+ /** @type {bigint} */
+ get blockCount() {
+ return this.#stats[IDX_STATS_SESSION_BLOCK_COUNT];
+ }
+
+ /** @type {bigint} */
+ get bytesInFlight() {
+ return this.#stats[IDX_STATS_SESSION_BYTES_IN_FLIGHT];
+ }
+
+ /** @type {bigint} */
+ get congestionRecoveryStartTs() {
+ return this.#stats[IDX_STATS_SESSION_CONGESTION_RECOVERY_START_TS];
+ }
+
+ /** @type {bigint} */
+ get cwnd() {
+ return this.#stats[IDX_STATS_SESSION_CWND];
+ }
+
+ /** @type {bigint} */
+ get deliveryRate() {
+ return this.#stats[IDX_STATS_SESSION_DELIVERY_RATE_SEC];
+ }
+
+ /** @type {bigint} */
+ get firstRttSampleTs() {
+ return this.#stats[IDX_STATS_SESSION_FIRST_RTT_SAMPLE_TS];
+ }
+
+ /** @type {bigint} */
+ get initialRtt() {
+ return this.#stats[IDX_STATS_SESSION_INITIAL_RTT];
+ }
+
+ /** @type {bigint} */
+ get lastTxPacketTs() {
+ return this.#stats[IDX_STATS_SESSION_LAST_TX_PKT_TS];
+ }
+
+ /** @type {bigint} */
+ get latestRtt() {
+ return this.#stats[IDX_STATS_SESSION_LATEST_RTT];
+ }
+
+ /** @type {bigint} */
+ get lossDetectionTimer() {
+ return this.#stats[IDX_STATS_SESSION_LOSS_DETECTION_TIMER];
+ }
+
+ /** @type {bigint} */
+ get lossTime() {
+ return this.#stats[IDX_STATS_SESSION_LOSS_TIME];
+ }
+
+ /** @type {bigint} */
+ get maxUdpPayloadSize() {
+ return this.#stats[IDX_STATS_SESSION_MAX_UDP_PAYLOAD_SIZE];
+ }
+
+ /** @type {bigint} */
+ get minRtt() {
+ return this.#stats[IDX_STATS_SESSION_MIN_RTT];
+ }
+
+ /** @type {bigint} */
+ get ptoCount() {
+ return this.#stats[IDX_STATS_SESSION_PTO_COUNT];
+ }
+
+ /** @type {bigint} */
+ get rttVar() {
+ return this.#stats[IDX_STATS_SESSION_RTTVAR];
+ }
+
+ /** @type {bigint} */
+ get smoothedRtt() {
+ return this.#stats[IDX_STATS_SESSION_SMOOTHED_RTT];
+ }
+
+ /** @type {bigint} */
+ get ssThreshold() {
+ return this.#stats[IDX_STATS_SESSION_SSTHRESH];
+ }
+
+ /** @type {bigint} */
+ get receiveRate() {
+ return this.#stats[IDX_STATS_SESSION_RECEIVE_RATE];
+ }
+
+ /** @type {bigint} */
+ get sendRate() {
+ return this.#stats[IDX_STATS_SESSION_SEND_RATE];
+ }
+
+ [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,
+ handshakeCompletedAt: this.handshakeCompletedAt,
+ handshakeConfirmedAt: this.handshakeConfirmedAt,
+ lastSentAt: this.lastSentAt,
+ lastReceivedAt: this.lastReceivedAt,
+ gracefulClosingAt: this.gracefulClosingAt,
+ closingAt: this.closingAt,
+ destroyedAt: this.destroyedAt,
+ bytesReceived: this.bytesReceived,
+ bytesSent: this.bytesSent,
+ bidiStreamCount: this.bidiStreamCount,
+ uniStreamCount: this.uniStreamCount,
+ inboundStreamsCount: this.inboundStreamsCount,
+ outboundStreamsCount: this.outboundStreamsCount,
+ keyUpdateCount: this.keyUpdateCount,
+ retransmitCount: this.retransmitCount,
+ maxBytesInFlight: this.maxBytesInFlight,
+ blockCount: this.blockCount,
+ bytesInFlight: this.bytesInFlight,
+ congestionRecoveryStartTs: this.congestionRecoveryStartTs,
+ cwnd: this.cwnd,
+ deliveryRate: this.deliveryRate,
+ firstRttSampleTs: this.firstRttSampleTs,
+ initialRtt: this.initialRtt,
+ lastTxPacketTs: this.lastTxPacketTs,
+ latestRtt: this.latestRtt,
+ lossDetectionTimer: this.lossDetectionTimer,
+ lossTime: this.lossTime,
+ maxUdpPayloadSize: this.maxUdpPayloadSize,
+ minRtt: this.minRtt,
+ ptoCount: this.ptoCount,
+ rttVar: this.rttVar,
+ smoothedRtt: this.smoothedRtt,
+ ssThreshold: this.ssThreshold,
+ receiveRate: this.receiveRate,
+ sendRate: this.sendRate,
+ detached: this.detached,
+ }, opts)}}`;
+ }
+}
+
+ObjectDefineProperties(SessionStats.prototype, {
+ detached: kEnumerableProperty,
+ createdAt: kEnumerableProperty,
+ handshakeCompletedAt: kEnumerableProperty,
+ handshakeConfirmedAt: kEnumerableProperty,
+ gracefulClosingAt: kEnumerableProperty,
+ closingAt: kEnumerableProperty,
+ destroyedAt: kEnumerableProperty,
+ bytesReceived: kEnumerableProperty,
+ bytesSent: kEnumerableProperty,
+ bidiStreamCount: kEnumerableProperty,
+ uniStreamCount: kEnumerableProperty,
+ inboundStreamsCount: kEnumerableProperty,
+ outboundStreamsCount: kEnumerableProperty,
+ keyUpdateCount: kEnumerableProperty,
+ retransmitCount: kEnumerableProperty,
+ maxBytesInFlight: kEnumerableProperty,
+ blockCount: kEnumerableProperty,
+ bytesInFlight: kEnumerableProperty,
+ congestionRecoveryStartTs: kEnumerableProperty,
+ cwnd: kEnumerableProperty,
+ deliveryRate: kEnumerableProperty,
+ firstRttSampleTs: kEnumerableProperty,
+ initialRtt: kEnumerableProperty,
+ lastTxPacketTs: kEnumerableProperty,
+ latestRtt: kEnumerableProperty,
+ lossDetectionTimer: kEnumerableProperty,
+ lossTime: kEnumerableProperty,
+ maxUdpPayloadSize: kEnumerableProperty,
+ minRtt: kEnumerableProperty,
+ ptoCount: kEnumerableProperty,
+ rttVar: kEnumerableProperty,
+ smoothedRtt: kEnumerableProperty,
+ ssThreshold: kEnumerableProperty,
+ receiveRate: kEnumerableProperty,
+ sendRate: kEnumerableProperty,
+});
+
+class SessionState {
+ #inner;
+
+ constructor(state) {
+ this.#inner = new DataView(state);
+ }
+
+ get prioritySupported() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_PRIORITY_SUPPORTED));
+ }
+
+ get versionNegotiation() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION));
+ }
+
+ set versionNegotiation(on = true) {
+ this.#inner.setUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION, on ? 1 : 0);
+ }
+
+ get pathValidation() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_PATH_VALIDATION));
+ }
+
+ set pathValidation(on = true) {
+ this.#inner.setUint8(IDX_STATE_SESSION_PATH_VALIDATION, on ? 1 : 0);
+ }
+
+ get datagram() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_DATAGRAM));
+ }
+
+ set datagram(on = true) {
+ this.#inner.setUint8(IDX_STATE_SESSION_DATAGRAM, on ? 1 : 0);
+ }
+
+ get sessionTicket() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_SESSION_TICKET));
+ }
+
+ set sessionTicket(on) {
+ this.#inner.setUint8(IDX_STATE_SESSION_SESSION_TICKET, on ? 1 : 0);
+ }
+
+ get clientHello() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_CLIENT_HELLO));
+ }
+
+ set clientHello(on) {
+ this.#inner.setUint8(IDX_STATE_SESSION_CLIENT_HELLO, on ? 1 : 0);
+ }
+
+ get clientHelloDone() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_CLIENT_HELLO_DONE));
+ }
+
+ get closing() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_CLOSING));
+ }
+
+ get destroyed() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_DESTROYED));
+ }
+
+ get gracefulClosing() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_GRACEFUL_CLOSING));
+ }
+
+ get handshakeCompleted() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_HANDSHAKE_COMPLETED));
+ }
+
+ get handshakeConfirmed() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED));
+ }
+
+ get ocsp() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_OCSP));
+ }
+
+ set ocsp(on) {
+ this.#inner.setUint8(IDX_STATE_SESSION_OCSP, on ? 1 : 0);
+ }
+
+ get oscpDone() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_OCSP_DONE));
+ }
+
+ get silentClose() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_SILENT_CLOSE));
+ }
+
+ get streamOpenAllowed() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED));
+ }
+
+ get transportParamsSet() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_TRANSPORT_PARAMS_SET));
+ }
+
+ get usingPreferredAddress() {
+ return Boolean(this.#inner.getUint8(IDX_STATE_SESSION_USING_PREFERRED_ADDRESS));
+ }
+}
+
+class SessionOptions {
+ #inner;
+
+ /**
+ * @typedef {{
+ * rejectUnauthorized: boolean?,
+ * clientHello: boolean?,
+ * enableTLSTrace: boolean?,
+ * requestPeerCertificate: boolean?,
+ * ocsp: boolean?,
+ * verifyHostnameIdentity: boolean?,
+ * keylog: boolean?,
+ * sessionID: string?,
+ * ciphers: string?,
+ * groups: string?,
+ * key: Key|Array,
+ * cert: ArrayBufferView|Array?,
+ * ca: ArrayBufferView|Array?,
+ * crl: ArrayBufferView|Array?,
+ * }} SecureOptions
+ * @typedef {{
+ * maxHeaderPairs?: bigint,
+ * maxHeaderLength?: bigint,
+ * maxFieldSectionSize?: bigint,
+ * qpackBlockedStreams?: bigint,
+ * qpackMaxTableCapacity?: bigint,
+ * qpackEncoderMaxTableCapacity?: bigint,
+ * }} ApplicationOptions
+ * @typedef {{
+ * initialMaxStreamDataBidiLocal: bigint?,
+ * initialMaxStreamDataBidiRemote: bigint?,
+ * initialMaxStreamDataUni: bigint?,
+ * initialMaxData: bigint?,
+ * initialMaxStreamsBidi: bigint?,
+ * initialMaxStreamsUni: bigint?,
+ * maxIdleTimeout: bigint?,
+ * activeConnectionIdLimit: bigint?,
+ * ackDelayExponent: bigint?,
+ * maxAckDelay: bigint?,
+ * maxDatagramFrameSize: bigint?,
+ * disableActiveMigration: boolean?,
+ * }} TransportParams
+ * @typedef {{
+ * ipv4: SocketAddress?,
+ * ipv6: SocketAddress?,
+ * }} PreferredAddress
+ * @param {Side} side
+ * @param {{
+ * alpn: string,
+ * servername: string?,
+ * preferredAddressStrategy: PreferredAddressStrategy?,
+ * qlog: boolean?
+ * secure: SecureOptions,
+ * application: ApplicationOptions?,
+ * transport: TransportParams?,
+ * preferredAddress: PreferredAddress?,
+ * }} options
+ */
+ constructor(side, options) {
+ validateObject(options, 'options', options);
+ const {
+ alpn, // No default.
+ servername,
+ preferredAddressStrategy = PreferredAddressStrategy.USE,
+ // cidFactory = undefined, // Currently not used
+ qlog = false,
+ secure = kEmptyObject,
+ application = kEmptyObject,
+ transportParams = kEmptyObject,
+ preferredAddress = kEmptyObject,
+ } = options;
+ validateString(alpn, 'options.alpn');
+ if (side === Side.SERVER) {
+ if (servername !== undefined) validateString(servername, 'options.servername');
+ validatePreferredAddressStrategy(preferredAddressStrategy,
+ 'options.preferredAddressStrategy');
+ // TODO: Validate cidFactory when we support that
+ }
+ validateBoolean(qlog, 'options.qlog');
+ validateObject(secure, 'options.secure');
+ validateObject(application, 'options.application');
+ validateObject(transportParams, 'options.transportParams');
+ if (side === Side.CLIENT) {
+ validateObject(preferredAddress, 'options.preferredAddress');
+ }
+
+ const {
+ rejectUnauthorized = false,
+ clientHello = false,
+ enableTLSTrace = false,
+ requestPeerCertificate = false,
+ ocsp = false,
+ verifyHostnameIdentity = true,
+ keylog = false,
+ sessionID,
+ ciphers,
+ groups,
+ key,
+ certs,
+ ca,
+ crl,
+ } = secure;
+
+ const {
+ maxHeaderPairs,
+ maxHeaderLength,
+ maxFieldSectionSize,
+ qpackBlockedStreams,
+ qpackMaxTableCapacity,
+ qpackEncoderMaxTableCapacity,
+ } = application;
+
+ if (maxHeaderPairs !== undefined)
+ validateUint64(maxHeaderPairs, 'options.application.maxHeaderPairs');
+ if (maxHeaderLength !== undefined)
+ validateUint64(maxHeaderLength, 'options.application.maxHeaderLength');
+ if (maxFieldSectionSize !== undefined)
+ validateUint64(maxFieldSectionSize, 'options.application.maxFieldSectionSize');
+ if (qpackBlockedStreams !== undefined)
+ validateUint64(qpackBlockedStreams, 'options.application.qpackBlockedStreams');
+ if (qpackMaxTableCapacity !== undefined)
+ validateUint64(qpackMaxTableCapacity, 'options.application.qpackMaxTableCapacity');
+ if (qpackEncoderMaxTableCapacity !== undefined) {
+ validateUint64(qpackEncoderMaxTableCapacity,
+ 'options.application.qpackEncoderMaxTableCapacity');
+ }
+
+ const {
+ initialMaxStreamDataBidiLocal,
+ initialMaxStreamDataBidiRemote,
+ initialMaxStreamDataUni,
+ initialMaxData,
+ initialMaxStreamsBidi,
+ initialMaxStreamsUni,
+ maxIdleTimeout,
+ activeConnectionIdLimit,
+ ackDelayExponent,
+ maxAckDelay,
+ maxDatagramFrameSize,
+ disableActiveMigration,
+ } = transportParams;
+
+ const {
+ ipv4: ipv4PreferredAddress,
+ ipv6: ipv6PreferredAddress,
+ } = preferredAddress;
+
+ if (side === Side.SERVER) {
+ validateBoolean(clientHello, 'options.secure.clientHello');
+ if (sessionID !== undefined) validateString(sessionID, 'options.secure.sessionID');
+ validateBoolean(requestPeerCertificate, 'options.secure.requestPeerCertificate');
+ } else if (side === Side.CLIENT) {
+ validateBoolean(rejectUnauthorized, 'options.secure.rejectUnauthorized');
+ validateBoolean(verifyHostnameIdentity, 'options.secure.verifyHostnameIdentity');
+ }
+ validateBoolean(enableTLSTrace, 'options.secure.enableTLSTrace');
+ validateBoolean(ocsp, 'options.secure.ocsp');
+ validateBoolean(keylog, 'options.secure.keylog');
+ if (ciphers !== undefined) validateString(ciphers, 'options.secure.ciphers');
+ if (groups !== undefined) validateString(groups, 'options.secure.groups');
+
+ const keys = [];
+ if (key !== undefined) {
+ if (ArrayIsArray(key)) {
+ for (let i = 0; i < key.length; i++) {
+ if (side === Side.SERVER || key[i] !== undefined)
+ validateKey(key[i], 'options.secure.key[' + i + ']');
+ if (isCryptoKey(key[i]))
+ keys.push(key[i][kKeyObject][kHandle]);
+ else
+ keys.push(key[i][kHandle]);
+ }
+ } else {
+ if (side === Side.SERVER || key !== undefined)
+ validateKey(key, 'options.secure.key');
+ if (isCryptoKey(key)) {
+ keys.push(key[kKeyObject][kHandle]);
+ } else if (isKeyObject(key)) {
+ keys.push(key[kHandle]);
+ }
+ }
+ } else if (side === Side.SERVER) {
+ validateKey(key, 'options.secure.key');
+ }
+
+ let certsArray = [];
+ if (certs !== undefined) {
+ if (ArrayIsArray(certs)) {
+ for (let i = 0; i < certs.length; i++)
+ validateArrayBufferView(certs[i], 'options.secure.certs[' + i + ']');
+ certsArray = certs;
+ } else {
+ validateArrayBufferView(certs, 'options.secure.certs');
+ certsArray.push(certs);
+ }
+ }
+
+ let caArray = [];
+ if (ca !== undefined) {
+ if (ArrayIsArray(ca)) {
+ for (let i = 0; i < ca.length; i++)
+ validateArrayBufferView(ca[i], 'options.secure.ca[' + i + ']');
+ caArray = ca;
+ } else {
+ validateArrayBufferView(ca, 'options.secure.ca');
+ caArray.push(ca);
+ }
+ }
+
+ let crlArray = [];
+ if (crl !== undefined) {
+ if (ArrayIsArray(crl)) {
+ for (let i = 0; i < crl.length; i++)
+ validateArrayBufferView(crl[i], 'options.secure.crl[' + i + ']');
+ crlArray = crl;
+ } else {
+ validateArrayBufferView(crl, 'options.secure.crl');
+ crlArray.push(crl);
+ }
+ }
+
+ if (initialMaxStreamDataBidiLocal !== undefined) {
+ validateUint64(initialMaxStreamDataBidiLocal,
+ 'options.transportParams.initialMaxStreamDataBidiLocal');
+ }
+ if (initialMaxStreamDataBidiRemote !== undefined) {
+ validateUint64(initialMaxStreamDataBidiRemote,
+ 'options.transportParams.initialMaxStreamDataBidiRemote');
+ }
+ if (initialMaxStreamDataUni !== undefined) {
+ validateUint64(initialMaxStreamDataUni,
+ 'options.transportParams.initialMaxStreamDataUni');
+ }
+ if (initialMaxData !== undefined) {
+ validateUint64(initialMaxData,
+ 'options.transportParams.initialMaxData');
+ }
+ if (initialMaxStreamsBidi !== undefined) {
+ validateUint64(initialMaxStreamsBidi,
+ 'options.transportParams.initialMaxStreamsBidi');
+ }
+ if (initialMaxStreamsUni !== undefined) {
+ validateUint64(initialMaxStreamsUni,
+ 'options.transportParams.initialMaxStreamsUni');
+ }
+ if (maxIdleTimeout !== undefined) {
+ validateUint64(maxIdleTimeout,
+ 'options.transportParams.maxIdleTimeout');
+ }
+ if (activeConnectionIdLimit !== undefined) {
+ validateUint64(activeConnectionIdLimit,
+ 'options.transportParams.activeConnectionIdLimit');
+ }
+ if (ackDelayExponent !== undefined) {
+ validateUint64(ackDelayExponent,
+ 'options.transportParams.ackDelayExponent');
+ }
+ if (maxAckDelay !== undefined) {
+ validateUint64(maxAckDelay,
+ 'options.transportParams.maxAckDelay');
+ }
+ if (maxDatagramFrameSize !== undefined) {
+ validateUint64(maxDatagramFrameSize,
+ 'options.transportParams.maxDatagramFrameSize');
+ }
+ if (disableActiveMigration !== undefined) {
+ validateBoolean(disableActiveMigration,
+ 'options.transportParams.disableActiveMigration');
+ }
+
+ if (side === Side.SERVER) {
+ if (ipv4PreferredAddress !== undefined)
+ validateSocketAddress(ipv4PreferredAddress, 'options.preferredAddress.ipv4');
+ if (ipv6PreferredAddress !== undefined)
+ validateSocketAddress(ipv6PreferredAddress, 'options.preferredAddress.ipv6');
+ }
+
+ this.#inner = new SessionOptions_(
+ alpn,
+ servername,
+ preferredAddressStrategy,
+ undefined, // Connection ID Factory, not currently used.
+ qlog,
+ {
+ // TLS Options
+ rejectUnauthorized,
+ clientHello,
+ enableTLSTrace,
+ requestPeerCertificate,
+ ocsp,
+ verifyHostnameIdentity,
+ keylog,
+ sessionID,
+ ciphers,
+ groups,
+ keys,
+ certs: certsArray,
+ ca: caArray,
+ crl: crlArray,
+ },
+ {
+ // Application Options
+ maxHeaderPairs,
+ maxHeaderLength,
+ maxFieldSectionSize,
+ qpackBlockedStreams,
+ qpackMaxTableCapacity,
+ qpackEncoderMaxTableCapacity,
+ },
+ {
+ // Transport Parameters
+ initialMaxStreamDataBidiLocal,
+ initialMaxStreamDataBidiRemote,
+ initialMaxStreamDataUni,
+ initialMaxData,
+ initialMaxStreamsBidi,
+ initialMaxStreamsUni,
+ maxIdleTimeout,
+ activeConnectionIdLimit,
+ ackDelayExponent,
+ maxAckDelay,
+ maxDatagramFrameSize,
+ disableActiveMigration,
+ },
+ ipv4PreferredAddress?.[kSocketAddressHandle],
+ ipv6PreferredAddress?.[kSocketAddressHandle]);
+ }
+
+ [kCreateInstance](endpointHandle, endpoint, address) {
+ const handle = endpointHandle.connect(address[kSocketAddressHandle], this.#inner);
+ if (handle === undefined)
+ throw new ERR_QUIC_UNABLE_TO_CREATE_STREAM();
+ return new Session(endpoint, handle);
+ }
+
+ [kListen](endpointHandle) {
+ endpointHandle.listen(this.#inner);
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0) return this;
+ return 'SessionOptions {}';
+ }
+}
+
+class Session extends EventTarget {
+ #inner;
+ #endpoint;
+ #stats;
+ #state;
+ #closePromise;
+ #peerCertificate;
+ #certificate;
+ #address;
+ #handshake = createDeferredPromise();
+
+ constructor(endpoint, sessionHandle) {
+ super();
+ this.#inner = sessionHandle;
+ this.#endpoint = endpoint;
+ this.#inner[kOwner] = this;
+ this.#stats = new SessionStats(this.#inner.stats);
+ this.#state = new SessionState(this.#inner.state);
+ }
+
+ /**
+ * @type {Endpoint}
+ */
+ get endpoint() { return this.#endpoint; }
+
+ /**
+ * @type {SocketAddress}
+ */
+ get address() {
+ if (this.#address === undefined) {
+ const ret = this.#inner?.getRemoteAddress();
+ this.#address = ret === undefined ? undefined : new InternalSocketAddress(ret);
+ }
+ return this.#address;
+ }
+
+ /**
+ * @type {X509Certificate}
+ */
+ get certificate() {
+ if (this.#certificate === undefined) {
+ const ret = this.#inner?.getCertificate();
+ this.#certificate =
+ ret === undefined ? undefined : new InternalX509Certificate(ret);
+ }
+ return this.#certificate;
+ }
+
+ /**
+ * @type {X509Certificate}
+ */
+ get peerCertificate() {
+ if (this.#peerCertificate === undefined) {
+ const ret = this.#inner?.getPeerCertificate();
+ this.#peerCertificate =
+ ret === undefined ? undefined : new InternalX509Certificate(ret);
+ }
+ return this.#peerCertificate;
+ }
+
+ /**
+ * @type {{}}
+ */
+ get ephemeralKey() { return this.#inner?.getEphemeralKeyInfo(); }
+
+ /**
+ * @type {SessionStats}
+ */
+ get stats() { return this.#stats; }
+
+ /** @type {Promise} */
+ get handshakeCompleted() {
+ return this.#handshake.promise;
+ }
+
+ get handshakeConfirmed() {
+ return this.#state?.handshakeConfirmed;
+ }
+
+ /** @type {boolean} */
+ get prioritySupported() {
+ return this.#state?.prioritySupported;
+ }
+
+ /**
+ * Initiates a graceful close of the session.
+ * @async
+ * @returns {Promise}
+ */
+ close() {
+ if (this.#closePromise !== undefined)
+ return this.#closePromise.promise;
+ this.#closePromise = createDeferredPromise();
+ if (this.#inner === undefined) {
+ this.#closePromise.reject(new ERR_INVALID_STATE('The session is closed.'));
+ } else {
+ this.#inner.gracefulClose();
+ }
+ return this.#closePromise.promise;
+ }
+
+ /**
+ * Immediately destroy the session, causing all open streams to be abruptly terminated.
+ * @param {any} [error] An optional error. If specified, the 'error' event will be triggered.
+ */
+ destroy(error) {
+ this[kFinishClose](error);
+ }
+
+ /**
+ * Initiate a key update for this session.
+ */
+ updateKey() {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The session is closed.');
+ if (this.#state?.gracefulClosing)
+ throw new ERR_INVALID_STATE('The session is closing.');
+ this.#inner.updateKey();
+ }
+
+ /**
+ * Send a datagram if it is supported by the peer.
+ * @param {ArrayBufferView} datagram
+ * @returns {bigint} A bigint identifying the datagram.
+ */
+ send(datagram) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The session is closed.');
+ if (this.#state?.gracefulClosing)
+ throw new ERR_INVALID_STATE('The session is closing.');
+ validateArrayBufferView(datagram, 'datagram');
+ return this.#inner.sendDatagram(datagram);
+ }
+
+ /**
+ * Open a new stream.
+ * @param {Direction} direction = Direction.BIDIRECTIONAL
+ * @returns {Stream} The new Stream.
+ */
+ open(direction = Direction.BIDI) {
+ if (this.#inner === undefined)
+ throw new ERR_INVALID_STATE('The session is closed.');
+ if (this.#state?.gracefulClosing)
+ throw new ERR_INVALID_STATE('The session is closing.');
+ validateDirection(direction, 'direction');
+
+ const stream = this.#inner.openStream(direction === Direction.BIDI ? 0 : 1);
+ if (stream === undefined)
+ throw new ERR_QUIC_UNABLE_TO_CREATE_STREAM();
+ return new Stream(this, stream);
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0) return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1
+ };
+
+ return `Session {${inspect({
+ address: this.address,
+ certificate: this.certificate,
+ peerCertificate: this.peerCertificate,
+ ephemeralKey: this.ephemeralKey,
+ state: this.#state,
+ stats: this.#stats,
+ }, opts)}}`;
+ }
+
+ [kClientHello](...args) {
+ this.dispatchEvent(new ClientHelloEvent(this.#inner, this, ...args));
+ }
+
+ [kOcspRequest]() {
+ this.dispatchEvent(new OCSPRequestEvent(this.#inner, this));
+ }
+
+ [kOcspResponse](response) {
+ this.dispatchEvent(new OCSPResponseEvent(this.#inner, this, response));
+ }
+
+ [kDatagram](...args) {
+ if (this.#inner === undefined) return;
+ this.dispatchEvent(new DatagramEvent(this, ...args));
+ }
+
+ [kFinishClose](maybeError) {
+ if (!this.#state.handshakeCompleted) {
+ if (maybeError) {
+ this.#handshake.reject(maybeError);
+ } else {
+ this.#handshake.reject(new ERR_QUIC_HANDSHAKE_CANCELED());
+ }
+ }
+
+ this.#stats[kDetach]();
+ this.#state = undefined;
+ this.#peerCertificate = undefined;
+ this.#certificate = undefined;
+ this.#address = undefined;
+
+ // Finish cleaning up the Session by calling destroy...
+ this.#inner.destroy();
+ this.#inner = undefined;
+
+ if (this.#closePromise !== undefined) {
+ if (maybeError) {
+ this.#closePromise.reject(maybeError);
+ } else {
+ this.#closePromise.resolve();
+ }
+ }
+
+ this.dispatchEvent(new CloseEvent());
+
+ if (maybeError)
+ this.dispatchEvent(new ErrorEvent(maybeError));
+ }
+
+ [kNewStream](stream) {
+ this.dispatchEvent(new StreamEvent(this, new Stream(this, stream)));
+ }
+
+ [kPathValidation](...args) {
+ this.dispatchEvent(new PathValidationEvent(this, ...args));
+ }
+
+ [kSessionTicket](...args) {
+ this.dispatchEvent(new SessionTicketEvent(this, ...args));
+ }
+
+ [kVersionNegotiation](...args) {
+ this.dispatchEvent(new VersionNegotiationEvent(this, ...args));
+ }
+
+ [kHandshakeComplete](...args) {
+ this.#handshake.resolve();
+ this.dispatchEvent(new HandshakeCompleteEvent(this, ...args));
+ }
+
+ [kDatagramStatus](...args) {
+ this.dispatchEvent(new DatagramStatusEvent(this, ...args));
+ }
+
+ [kNewListener](size, type, listener, once, capture, passive, weak) {
+ super[kNewListener](size, type, listener, once, capture, passive, weak);
+
+ if (this.#state === undefined) return;
+
+ switch (type) {
+ case 'datagram':
+ this.#state.datagram = true;
+ break;
+ case 'client-hello':
+ this.#state.clientHello = true;
+ break;
+ case 'ocsp':
+ this.#state.ocsp = true;
+ break;
+ case 'session-ticket':
+ this.#state.sessionTicket = true;
+ break;
+ case 'path-validation':
+ this.#state.pathValidation = true;
+ break;
+ case 'version-negotiation':
+ this.#state.versionNegotiation = true;
+ break;
+ }
+ }
+ [kRemoveListener](size, type, listener, capture) {
+ super[kRemoveListener](size, type, listener, capture);
+
+ if (this.#state === undefined) return;
+
+ switch (type) {
+ case 'datagram':
+ this.#state.datagram = false;
+ break;
+ case 'client-hello':
+ this.#state.clientHello = false;
+ break;
+ case 'ocsp':
+ this.#state.ocsp = false;
+ break;
+ case 'session-ticket':
+ this.#state.sessionTicket = false;
+ break;
+ case 'version-negotiation':
+ this.#state.versionNegotiation = false;
+ break;
+ case 'path-validation':
+ this.#state.pathValidation = false;
+ break;
+ }
+ }
+}
+
+ObjectDefineProperties(Session.prototype, {
+ endpoint: kEnumerableProperty,
+ address: kEnumerableProperty,
+ certificate: kEnumerableProperty,
+ peerCertificate: kEnumerableProperty,
+ ephemeralKey: kEnumerableProperty,
+ stats: kEnumerableProperty,
+ handshakeCompleted: kEnumerableProperty,
+ handshakeConfirmed: kEnumerableProperty,
+});
+
+defineEventHandler(Session.prototype, 'close');
+defineEventHandler(Session.prototype, 'error');
+defineEventHandler(Session.prototype, 'stream');
+defineEventHandler(Session.prototype, 'datagram');
+defineEventHandler(Session.prototype, 'ocsp');
+defineEventHandler(Session.prototype, 'pathvalidation', 'path-validation');
+defineEventHandler(Session.prototype, 'handshakecomplete', 'handshake-complete');
+defineEventHandler(Session.prototype, 'sessionticket', 'session-ticket');
+defineEventHandler(Session.prototype, 'clienthello', 'client-hello');
+
+// ======================================================================================
+// Endpoint
+class EndpointStats {
+ #buffer;
+ #detached = false;
+
+ constructor(buffer) {
+ this.#buffer = buffer;
+ }
+
+ /** @type {boolean} */
+ get detached() { return this.#detached; }
+
+ /* @type {bigint} */
+ get createdAt() { return this.#buffer[IDX_STATS_ENDPOINT_CREATED_AT]; }
+ /* @type {bigint} */
+ get destroyedAt() { return this.#buffer[IDX_STATS_ENDPOINT_DESTROYED_AT]; }
+ /* @type {bigint} */
+ get bytesReceived() { return this.#buffer[IDX_STATS_ENDPOINT_BYTES_RECEIVED]; }
+ /* @type {bigint} */
+ get bytesSent() { return this.#buffer[IDX_STATS_ENDPOINT_BYTES_SENT]; }
+ /* @type {bigint} */
+ get packetsReceived() { return this.#buffer[IDX_STATS_ENDPOINT_PACKETS_RECEIVED]; }
+ /* @type {bigint} */
+ get packetsSent() { return this.#buffer[IDX_STATS_ENDPOINT_PACKETS_SENT]; }
+ /* @type {bigint} */
+ get serverSessions() { return this.#buffer[IDX_STATS_ENDPOINT_SERVER_SESSIONS]; }
+ /* @type {bigint} */
+ get clientSessions() { return this.#buffer[IDX_STATS_ENDPOINT_CLIENT_SESSIONS]; }
+ /* @type {bigint} */
+ get busyCount() { return this.#buffer[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT]; }
+
+ [kDetach]() {
+ // Copy the buffer so that it is no longer tied to the original memory buffer.
+ this.#buffer = new BigUint64Array(this.#buffer);
+ this.#detached = true;
+ }
+
+ [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,
+ detached: this.detached,
+ }, opts)}}`;
+ }
+}
+
+ObjectDefineProperties(EndpointStats.prototype, {
+ detached: kEnumerableProperty,
+ createdAt: kEnumerableProperty,
+ destroyedAt: kEnumerableProperty,
+ bytesReceived: kEnumerableProperty,
+ bytesSent: kEnumerableProperty,
+ packetsReceived: kEnumerableProperty,
+ packetsSent: kEnumerableProperty,
+ serverSessions: kEnumerableProperty,
+ clientSessions: kEnumerableProperty,
+ busyCount: kEnumerableProperty,
+});
+
+class EndpointState {
+ #inner;
+
+ constructor(state) {
+ this.#inner = new DataView(state);
+ }
+
+ /* @type {boolean} */
+ get listening() {
+ return this.#inner.getUint8(IDX_STATE_ENDPOINT_LISTENING);
+ }
+
+ /* @type {boolean} */
+ get closing() {
+ return this.#inner.getUint8(IDX_STATE_ENDPOINT_CLOSING);
+ }
+
+ /* @type {boolean} */
+ get waitingForCallbacks() {
+ return this.#inner.getUint8(IDX_STATE_ENDPOINT_WAITING_FOR_CALLBACKS);
+ }
+
+ /* @type {boolean} */
+ get busy() {
+ return this.#inner.getUint8(IDX_STATE_ENDPOINT_BUSY);
+ }
+
+ /* @type {bigint} */
+ get pendingCallbacks() {
+ return this.#inner.getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS);
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0) return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1
+ };
+
+ return `EndpointState {${inspect({
+ listening: this.listening,
+ closing: this.closing,
+ waitingForCallbacks: this.waitingForCallbacks,
+ busy: this.busy,
+ pendingCallbacks: this.pendingCallbacks
+ }, opts)}}`;
+ }
+}
+
+class EndpointOptions {
+ #inner;
+
+ /**
+ * @typedef {{
+ * retryTokenExpiration?: number,
+ * tokenExpiration?: number,
+ * maxWindowOverride?: number,
+ * maxStreamWindowOverride?: number,
+ * maxConnectionsPerHost?: number,
+ * maxConnectionsTotal?: number,
+ * maxStatelessResets?: number,
+ * addressLRUSize?: number,
+ * retryLimit?: number,
+ * maxPayloadSize?: number,
+ * unacknowledgedPacketThreshold?: number,
+ * validateAddress?: boolean,
+ * disableStatelessReset?: boolean,
+ * rxPacketLoss?: number,
+ * txPacketLoss?: number,
+ * ccAlgorithm?: CongestionControlAlgorithm,
+ * ipv6Only?: boolean,
+ * receiveBufferSize?: number,
+ * sendBufferSize?: number,
+ * ttl?: number,
+ * }} EndpointOptions
+ * @param {SocketAddress} address
+ * @param {EndpointOptions} [options]
+ */
+ constructor(address, options = kEmptyObject) {
+ validateSocketAddress(address, 'address');
+ validateObject(options, 'options', options);
+
+ const {
+ retryTokenExpiration = DEFAULT_RETRYTOKEN_EXPIRATION,
+ tokenExpiration = DEFAULT_TOKEN_EXPIRATION,
+ maxWindowOverride = 0,
+ maxStreamWindowOverride = 0,
+ maxConnectionsPerHost = DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ maxConnectionsTotal = DEFAULT_MAX_CONNECTIONS,
+ maxStatelessResets = DEFAULT_MAX_STATELESS_RESETS,
+ addressLRUSize = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE,
+ retryLimit = DEFAULT_MAX_RETRY_LIMIT,
+ maxPayloadSize = 1200,
+ unacknowledgedPacketThreshold = DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD,
+ validateAddress = true,
+ disableStatelessReset = false,
+ rxPacketLoss = 0.0,
+ txPacketLoss = 0.0,
+ ccAlgorithm = CongestionControlAlgorithm.CUBIC,
+ ipv6Only = false,
+ receiveBufferSize = 0,
+ sendBufferSize = 0,
+ ttl = 0,
+ } = options;
+
+ validateUint64(retryTokenExpiration, 'options.retryTokenExpiration');
+ validateUint64(tokenExpiration, 'options.tokenExpiration');
+ validateUint64(maxWindowOverride, 'options.maxWindowOverride');
+ validateUint64(maxStreamWindowOverride, 'options.maxStreamWindowOverride');
+ validateUint64(maxConnectionsPerHost, 'options.maxConnectionsPerHost');
+ validateUint64(maxConnectionsTotal, 'options.maxConnectionsTotal');
+ validateUint64(maxStatelessResets, 'options.maxStatelessResets');
+ validateUint64(addressLRUSize, 'options.addressLRUSize');
+ validateUint64(retryLimit, 'options.retryLimit');
+ validateUint64(maxPayloadSize, 'options.maxPayloadSize');
+ validateUint64(unacknowledgedPacketThreshold, 'options.unacknowledgedPacketThreshold');
+ validateBoolean(validateAddress, 'options.validateAddress');
+ validateBoolean(disableStatelessReset, 'options.disableStatelessReset');
+ validateNumber(rxPacketLoss, 'options.rxPacketLoss', 0.0, 1.0);
+ validateNumber(txPacketLoss, 'options.txPacketLoss', 0.0, 1.0);
+ validateCongestionControlAlgorithm(ccAlgorithm, 'options.ccAlgorithm');
+ validateBoolean(ipv6Only, 'options.ipv6Only');
+ validateUint32(receiveBufferSize, 'options.receiveBufferSize');
+ validateUint32(sendBufferSize, 'options.sendBufferSize');
+ validateUint8(ttl, 'options.ttl');
+
+ this.#inner = new EndpointOptions_(address[kSocketAddressHandle], {
+ retryTokenExpiration,
+ tokenExpiration,
+ maxWindowOverride,
+ maxStreamWindowOverride,
+ maxConnectionsPerHost,
+ maxConnectionsTotal,
+ maxStatelessResets,
+ addressLRUSize,
+ retryLimit,
+ maxPayloadSize,
+ unacknowledgedPacketThreshold,
+ validateAddress,
+ disableStatelessReset,
+ rxPacketLoss,
+ txPacketLoss,
+ ccAlgorithm,
+ ipv6Only,
+ receiveBufferSize,
+ sendBufferSize,
+ ttl,
+ });
+ }
+
+ /**
+ * @returns {EndpointOptions}
+ */
+ generateResetTokenSecret() {
+ this.#inner.generateResetTokenSecret();
+ return this;
+ }
+
+ /**
+ * @param {ArrayBufferView} secret - The new secret. Must be exactly 16 bytes.
+ * @returns {EndpointOptions}
+ */
+ setResetTokenSecret(secret) {
+ validateArrayBufferView(secret, 'secret');
+ this.#inner.setResetTokenSecret(secret);
+ return this;
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0) return this;
+ return 'EndpointOptions {}';
+ }
+
+ [kCreateInstance]() {
+ return createEndpoint(this.#inner);
+ }
+}
+
+class Endpoint extends EventTarget {
+ #inner;
+ #closePromise;
+ #stats;
+ #state;
+ #address;
+ #closed = false;
+
+ /**
+ * @param {EndpointOptions} options
+ */
+ constructor(options) {
+ super();
+ validateEndpointOptions(options, 'options');
+ this.#inner = options[kCreateInstance]();
+ this.#inner[kOwner] = this;
+ this.#state = new EndpointState(this.#inner.state);
+ this.#stats = new EndpointStats(this.#inner.stats);
+ }
+
+ /**
+ * Listen for server Sessions.
+ * @param {SessionOptions} options The options to use for the Session.
+ * @return {Endpoint}
+ */
+ listen(options) {
+ if (this.#closed)
+ throw new ERR_INVALID_STATE('The endpoint is closed');
+ if (this.#closePromise !== undefined)
+ throw new ERR_INVALID_STATE('The endpoint is closing');
+ validateSessionOptions(options, 'options');
+ options[kListen](this.#inner);
+ return this;
+ }
+
+ /**
+ * Create a client Session.
+ * @param {SocketAddress} address The address to connect to.
+ * @param {SessionOptions} options The options to use for the Session.
+ * @returns {Session} The client session.
+ */
+ connect(address, options) {
+ if (this.#closed)
+ throw new ERR_INVALID_STATE('The endpoint is closed');
+ if (this.#closePromise !== undefined)
+ throw new ERR_INVALID_STATE('The endpoint is closing');
+ validateSocketAddress(address, 'address');
+ validateSessionOptions(options, 'options');
+ return options[kCreateInstance](this.#inner, this, address);
+ }
+
+ /**
+ * Close the Endpoint gracefully. Packets that are in flight are allowed to finish
+ * and existing sessions are permitted to close. New sessions will not be allowed.
+ * @async
+ * @returns {Promise} A promise resolved when the Endpoint is closed.
+ */
+ close() {
+ if (this.#closePromise !== undefined)
+ return this.#closePromise.promise;
+ this.#closePromise = createDeferredPromise();
+ this.#inner.closeGracefully();
+ return this.#closePromise.promise;
+ }
+
+ /**
+ * Statistics for this Endpoint.
+ * @type {EndpointStats}
+ */
+ get stats() { return this.#stats; }
+
+ /**
+ * The current local address this endpoint is bound to.
+ * @type {SocketAddress}
+ */
+ get address() {
+ if (this.#address) return this.#address;
+
+ const handle = this.#inner.address();
+ if (handle !== undefined) {
+ this.#address = new InternalSocketAddress(handle);
+ return this.#address;
+ }
+
+ return undefined;
+ }
+
+ /**
+ * @param {boolean} on = true
+ * @returns {Endpoint}
+ */
+ markAsBusy(on = true) {
+ if (this.#closed)
+ throw new ERR_INVALID_STATE('The endpoint is closed');
+ if (this.#closePromise !== undefined)
+ throw new ERR_INVALID_STATE('The endpoint is closing');
+ this.#inner.markBusy(on);
+ return this;
+ }
+
+ /**
+ * @returns {Endpoint}
+ */
+ ref() {
+ if (this.#closed) return;
+ this.#inner.ref();
+ return this;
+ }
+
+ /**
+ * @returns {Endpoint}
+ */
+ unref() {
+ if (this.#closed) return;
+ this.#inner.unref();
+ return this;
+ }
+
+ [kFinishClose](maybeError) {
+ this.#closed = true;
+ this.#address = undefined;
+ this.#stats[kDetach]();
+ this.#state = undefined;
+
+ // Were we waiting for a close? If so, resolve the promise.
+ if (this.#closePromise !== undefined) {
+ if (maybeError) {
+ this.#closePromise.reject(maybeError);
+ } else {
+ this.#closePromise.resolve();
+ }
+ }
+
+ this.#inner = undefined;
+
+ this.dispatchEvent(new CloseEvent());
+
+ if (maybeError)
+ this.dispatchEvent(new ErrorEvent(maybeError));
+ }
+
+ [kNewSession](sessionHandle) {
+ this.dispatchEvent(new SessionEvent(this, new Session(this, sessionHandle)));
+ }
+
+ [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,
+ state: this.#state,
+ stats: this.#stats,
+ pendingClose: this.#closePromise !== undefined,
+ }, opts)}}`;
+ }
+}
+
+ObjectDefineProperties(Endpoint.prototype, {
+ address: kEnumerableProperty,
+ stats: kEnumerableProperty,
+});
+defineEventHandler(Endpoint.prototype, 'close');
+defineEventHandler(Endpoint.prototype, 'error');
+defineEventHandler(Endpoint.prototype, 'session');
+
+// ======================================================================================
+// Exports
+module.exports = ObjectCreate(null, {
+ constants: {
+ __proto__: null,
+ value: constants,
+ enumerable: true,
+ configurable: false,
+ },
+ EndpointOptions: {
+ __proto__: null,
+ value: EndpointOptions,
+ enumerable: true,
+ configurable: false,
+ },
+ SessionOptions: {
+ __proto__: null,
+ value: SessionOptions,
+ enumerable: true,
+ configurable: false,
+ },
+ Endpoint: {
+ __proto__: null,
+ value: Endpoint,
+ enumerable: true,
+ configurable: false,
+ },
+ ArrayBufferViewSource: {
+ __proto__: null,
+ value: ArrayBufferViewSource,
+ enumerable: true,
+ configurable: false,
+ },
+ StreamSource: {
+ __proto__: null,
+ value: StreamSource,
+ enumerable: true,
+ configurable: false,
+ },
+ StreamBaseSource: {
+ __proto__: null,
+ value: StreamBaseSource,
+ enumerable: true,
+ configurable: false,
+ },
+ BlobSource: {
+ __proto__: null,
+ value: BlobSource,
+ enumerable: true,
+ configurable: false,
+ },
+});
+
+/* eslint-enable no-use-before-define */
diff --git a/node.gyp b/node.gyp
index 562ff709d837f3..c737e5e3c69c41 100644
--- a/node.gyp
+++ b/node.gyp
@@ -536,6 +536,7 @@
'src/node_zlib.cc',
'src/pipe_wrap.cc',
'src/process_wrap.cc',
+ 'src/quic/quic.cc',
'src/signal_wrap.cc',
'src/spawn_sync.cc',
'src/stream_base.cc',
@@ -643,6 +644,7 @@
'src/node_watchdog.h',
'src/node_worker.h',
'src/pipe_wrap.h',
+ 'src/quic/quic.h',
'src/req_wrap.h',
'src/req_wrap-inl.h',
'src/spawn_sync.h',
@@ -751,6 +753,21 @@
'Ws2_32',
],
}],
+ [ 'openssl_quic=="true"', {
+ 'sources': [
+ 'src/quic/crypto.cc',
+ 'src/quic/endpoint.cc',
+ 'src/quic/http3.cc',
+ 'src/quic/session.cc',
+ 'src/quic/stream.cc',
+ 'src/quic/crypto.h',
+ 'src/quic/defs.h',
+ 'src/quic/endpoint.h',
+ 'src/quic/http3.h',
+ 'src/quic/session.h',
+ 'src/quic/stream.h',
+ ]
+ }],
[ 'node_use_openssl=="true"', {
'sources': [
'src/crypto/crypto_aes.cc',
diff --git a/src/async_wrap.h b/src/async_wrap.h
index f7ed25f9eea318..33db98ac010e0d 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -77,21 +77,31 @@ namespace node {
V(ZLIB)
#if HAVE_OPENSSL
-#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \
- V(CHECKPRIMEREQUEST) \
- V(PBKDF2REQUEST) \
- V(KEYPAIRGENREQUEST) \
- V(KEYGENREQUEST) \
- V(KEYEXPORTREQUEST) \
- V(CIPHERREQUEST) \
- V(DERIVEBITSREQUEST) \
- V(HASHREQUEST) \
- V(RANDOMBYTESREQUEST) \
- V(RANDOMPRIMEREQUEST) \
- V(SCRYPTREQUEST) \
- V(SIGNREQUEST) \
- V(TLSWRAP) \
- V(VERIFYREQUEST)
+#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \
+ V(CHECKPRIMEREQUEST) \
+ V(PBKDF2REQUEST) \
+ V(KEYPAIRGENREQUEST) \
+ V(KEYGENREQUEST) \
+ V(KEYEXPORTREQUEST) \
+ V(CIPHERREQUEST) \
+ V(DERIVEBITSREQUEST) \
+ V(HASHREQUEST) \
+ V(RANDOMBYTESREQUEST) \
+ V(RANDOMPRIMEREQUEST) \
+ V(SCRYPTREQUEST) \
+ V(SIGNREQUEST) \
+ V(TLSWRAP) \
+ V(VERIFYREQUEST) \
+ V(QUICSESSION) \
+ V(QUICENDPOINT) \
+ V(QUICSTREAM) \
+ V(QUICPACKET) \
+ V(QUICENDPOINT_UDP) \
+ V(QUICSTREAMSOURCE) \
+ V(QUICSTREAMBASESOURCE) \
+ V(QUICBLOBSOURCE) \
+ V(QUICLOGSTREAM)
+
#else
#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V)
#endif // HAVE_OPENSSL
diff --git a/src/debug_utils.h b/src/debug_utils.h
index e2e702f586e20f..29b59b90721952 100644
--- a/src/debug_utils.h
+++ b/src/debug_utils.h
@@ -49,7 +49,8 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
V(CODE_CACHE) \
V(NGTCP2_DEBUG) \
V(WASI) \
- V(MKSNAPSHOT)
+ V(MKSNAPSHOT) \
+ V(QUIC)
enum class DebugCategory : unsigned int {
#define V(name) name,
diff --git a/src/node_binding.cc b/src/node_binding.cc
index fa67a45386e159..19e57a5ef52c8b 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -61,6 +61,7 @@
V(pipe_wrap) \
V(process_wrap) \
V(process_methods) \
+ V(quic) \
V(report) \
V(serdes) \
V(signal_wrap) \
diff --git a/src/node_errors.h b/src/node_errors.h
index 68a95835812e50..794207db79ffcc 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -64,6 +64,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_INVALID_ADDRESS, Error) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \
+ V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
V(ERR_INVALID_ARG_TYPE, TypeError) \
V(ERR_INVALID_OBJECT_DEFINE_PROPERTY, TypeError) \
V(ERR_INVALID_MODULE, Error) \
@@ -86,7 +87,8 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_VM_MODULE_LINK_FAILURE, Error) \
V(ERR_WASI_NOT_STARTED, Error) \
V(ERR_WORKER_INIT_FAILED, Error) \
- V(ERR_PROTO_ACCESS, Error)
+ V(ERR_PROTO_ACCESS, Error) \
+ V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, Error)
#define V(code, type) \
template \
@@ -152,6 +154,7 @@ ERRORS_WITH_CODE(V)
V(ERR_DLOPEN_FAILED, "DLOpen failed") \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \
"Context not associated with Node.js environment") \
+ V(ERR_ILLEGAL_CONSTRUCTOR, "Illegal constructor") \
V(ERR_INVALID_ADDRESS, "Invalid socket address") \
V(ERR_INVALID_MODULE, "No such module") \
V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \
@@ -176,7 +179,8 @@ ERRORS_WITH_CODE(V)
V(ERR_WORKER_INIT_FAILED, "Worker initialization failure") \
V(ERR_PROTO_ACCESS, \
"Accessing Object.prototype.__proto__ has been " \
- "disallowed with --disable-proto=throw")
+ "disallowed with --disable-proto=throw") \
+ V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, "Failure to set SNI context")
#define V(code, message) \
inline v8::Local code(v8::Isolate* isolate) { \
diff --git a/src/node_http_common.h b/src/node_http_common.h
index 099623c3b15d0c..b58e8cb5607aba 100644
--- a/src/node_http_common.h
+++ b/src/node_http_common.h
@@ -24,61 +24,61 @@ class Environment;
V(PATH, ":path") \
V(PROTOCOL, ":protocol")
-#define HTTP_REGULAR_HEADERS(V) \
- V(ACCEPT_ENCODING, "accept-encoding") \
- V(ACCEPT_LANGUAGE, "accept-language") \
- V(ACCEPT_RANGES, "accept-ranges") \
- V(ACCEPT, "accept") \
- V(ACCESS_CONTROL_ALLOW_CREDENTIALS, "access-control-allow-credentials") \
- V(ACCESS_CONTROL_ALLOW_HEADERS, "access-control-allow-headers") \
- V(ACCESS_CONTROL_ALLOW_METHODS, "access-control-allow-methods") \
- V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \
- V(ACCESS_CONTROL_EXPOSE_HEADERS, "access-control-expose-headers") \
- V(ACCESS_CONTROL_REQUEST_HEADERS, "access-control-request-headers") \
- V(ACCESS_CONTROL_REQUEST_METHOD, "access-control-request-method") \
- V(AGE, "age") \
- V(AUTHORIZATION, "authorization") \
- V(CACHE_CONTROL, "cache-control") \
- V(CONNECTION, "connection") \
- V(CONTENT_DISPOSITION, "content-disposition") \
- V(CONTENT_ENCODING, "content-encoding") \
- V(CONTENT_LENGTH, "content-length") \
- V(CONTENT_TYPE, "content-type") \
- V(COOKIE, "cookie") \
- V(DATE, "date") \
- V(ETAG, "etag") \
- V(FORWARDED, "forwarded") \
- V(HOST, "host") \
- V(IF_MODIFIED_SINCE, "if-modified-since") \
- V(IF_NONE_MATCH, "if-none-match") \
- V(IF_RANGE, "if-range") \
- V(LAST_MODIFIED, "last-modified") \
- V(LINK, "link") \
- V(LOCATION, "location") \
- V(RANGE, "range") \
- V(REFERER, "referer") \
- V(SERVER, "server") \
- V(SET_COOKIE, "set-cookie") \
- V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \
- V(TRANSFER_ENCODING, "transfer-encoding") \
- V(TE, "te") \
- V(UPGRADE_INSECURE_REQUESTS, "upgrade-insecure-requests") \
- V(UPGRADE, "upgrade") \
- V(USER_AGENT, "user-agent") \
- V(VARY, "vary") \
- V(X_CONTENT_TYPE_OPTIONS, "x-content-type-options") \
- V(X_FRAME_OPTIONS, "x-frame-options") \
- V(KEEP_ALIVE, "keep-alive") \
- V(PROXY_CONNECTION, "proxy-connection") \
- V(X_XSS_PROTECTION, "x-xss-protection") \
- V(ALT_SVC, "alt-svc") \
- V(CONTENT_SECURITY_POLICY, "content-security-policy") \
- V(EARLY_DATA, "early-data") \
- V(EXPECT_CT, "expect-ct") \
- V(ORIGIN, "origin") \
- V(PURPOSE, "purpose") \
- V(TIMING_ALLOW_ORIGIN, "timing-allow-origin") \
- V(X_FORWARDED_FOR, "x-forwarded-for") \
+#define HTTP_REGULAR_HEADERS(V) \
+ V(ACCEPT_ENCODING, "accept-encoding") \
+ V(ACCEPT_LANGUAGE, "accept-language") \
+ V(ACCEPT_RANGES, "accept-ranges") \
+ V(ACCEPT, "accept") \
+ V(ACCESS_CONTROL_ALLOW_CREDENTIALS, "access-control-allow-credentials") \
+ V(ACCESS_CONTROL_ALLOW_HEADERS, "access-control-allow-headers") \
+ V(ACCESS_CONTROL_ALLOW_METHODS, "access-control-allow-methods") \
+ V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \
+ V(ACCESS_CONTROL_EXPOSE_HEADERS, "access-control-expose-headers") \
+ V(ACCESS_CONTROL_REQUEST_HEADERS, "access-control-request-headers") \
+ V(ACCESS_CONTROL_REQUEST_METHOD, "access-control-request-method") \
+ V(AGE, "age") \
+ V(AUTHORIZATION, "authorization") \
+ V(CACHE_CONTROL, "cache-control") \
+ V(CONNECTION, "connection") \
+ V(CONTENT_DISPOSITION, "content-disposition") \
+ V(CONTENT_ENCODING, "content-encoding") \
+ V(CONTENT_LENGTH, "content-length") \
+ V(CONTENT_TYPE, "content-type") \
+ V(COOKIE, "cookie") \
+ V(DATE, "date") \
+ V(ETAG, "etag") \
+ V(FORWARDED, "forwarded") \
+ V(HOST, "host") \
+ V(IF_MODIFIED_SINCE, "if-modified-since") \
+ V(IF_NONE_MATCH, "if-none-match") \
+ V(IF_RANGE, "if-range") \
+ V(LAST_MODIFIED, "last-modified") \
+ V(LINK, "link") \
+ V(LOCATION, "location") \
+ V(RANGE, "range") \
+ V(REFERER, "referer") \
+ V(SERVER, "server") \
+ V(SET_COOKIE, "set-cookie") \
+ V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \
+ V(TRANSFER_ENCODING, "transfer-encoding") \
+ V(TE, "te") \
+ V(UPGRADE_INSECURE_REQUESTS, "upgrade-insecure-requests") \
+ V(UPGRADE, "upgrade") \
+ V(USER_AGENT, "user-agent") \
+ V(VARY, "vary") \
+ V(X_CONTENT_TYPE_OPTIONS, "x-content-type-options") \
+ V(X_FRAME_OPTIONS, "x-frame-options") \
+ V(KEEP_ALIVE, "keep-alive") \
+ V(PROXY_CONNECTION, "proxy-connection") \
+ V(X_XSS_PROTECTION, "x-xss-protection") \
+ V(ALT_SVC, "alt-svc") \
+ V(CONTENT_SECURITY_POLICY, "content-security-policy") \
+ V(EARLY_DATA, "early-data") \
+ V(EXPECT_CT, "expect-ct") \
+ V(ORIGIN, "origin") \
+ V(PURPOSE, "purpose") \
+ V(TIMING_ALLOW_ORIGIN, "timing-allow-origin") \
+ V(X_FORWARDED_FOR, "x-forwarded-for") \
V(PRIORITY, "priority")
#define HTTP_ADDITIONAL_HEADERS(V) \
diff --git a/src/quic/README.md b/src/quic/README.md
new file mode 100644
index 00000000000000..840274ece9bd68
--- /dev/null
+++ b/src/quic/README.md
@@ -0,0 +1,646 @@
+# A QUIC Introduction
+
+Welcome! You've found the source of the Node.js QUIC implementation. This guide
+will start you on your journey to understanding how this implementation works.
+
+## First, what is QUIC?
+
+QUIC is a UDP-based transport protocol developed by the IETF and published as
+[RFC 9000][]. I strongly recommend that you take the time to read through that
+specification before continuing as it will introduce many of the underlying
+concepts.
+
+Just go ahead and go read all of the following QUIC related specs now:
+
+* [RFC 8999][]: Version-Independent Properties of QUIC
+* [RFC 9000][]: QUIC: A UDP-Based Multiplexed and Secure Transport
+* [RFC 9001][]: Using TLS to Secure QUIC
+* [RFC 9002][]: QUIC Loss Detection and Congestion Control
+
+### Isn't QUIC just HTTP/3?
+
+HTTP/3 is an application of the HTTP protocol semantics on top of QUIC. The two
+are not the same thing. It is possible (and will be common) to implement
+applications of QUIC that have nothing to do with HTTP/3, but HTTP/3 will always
+be implemented on top of QUIC.
+
+At the time I'm writing this, the QUIC RFC's have been finished, but the HTTP/3
+specification is still under development. I'd recommend also reading through
+these draft specifications before continuing on:
+
+* [draft-ietf-quic-http-34][]: Hypertext Transfer Protocol Version 3 (HTTP/3)
+* [draft-ietf-quic-qpack-21][]: QPACK: Header Compression for HTTP/3
+
+The IETF working group is working on a number of other documents that you'll
+also want to get familiar with. Check those out here:
+
+* [https://datatracker.ietf.org/wg/quic/documents/]()
+
+This guide will first deal with explaining QUIC and the QUIC implementation in
+general, and then will address HTTP/3.
+
+### So if QUIC is not HTTP/3, what is it?
+
+QUIC is a stateful, connection-oriented, client-server UDP protocol that
+includes flow-control, multiplexed streams, network-path migration, low-latency
+connection establishment, and integrated TLS 1.3.
+
+A QUIC connection is always initiated by the client and starts with a handshake
+phase that includes a TLS 1.3 CLIENT-HELLO and a set of configuration
+parameters that QUIC calls "transport parameters". [RFC 9001][] details exactly
+how QUIC uses TLS 1.3.
+
+The TLS handshake establishes cryptographic keys that will be used to encrypt
+all QUIC protocol data that is passed back and forth. As I will explain in a
+bit, it is possible for the client and server start exchanging data before
+these keys have been fully negotiated, but for now we'll ignore that and focus
+on the fully-protected case.
+
+After the TLS 1.3 handshake is completed, and the packet protection keys have
+been established, the QUIC protocol primarily consists of opening undirectional
+or bidirectional streams of data between the client and server. An important
+characteristic is that streams can be opened by _either_ of the two peers.
+
+Unsurprisingly, a unidirectional stream allows sending data in only one
+direction on the connection. Bidirectional streams allow both endpoints to send
+and receive data. QUIC streams are nothing more than a sequence of bytes. There
+are no headers, no trailers, just a sequence of octets that are spread out of
+one or more QUIC packets encoded into one or more UDP packets.
+
+The simplistic life cycle of a QUIC connection, then, is:
+
+* Initiate the connection with a TLS Handshake.
+* Open one or more streams to transmit data.
+* Close the connection when done transmitting data.
+
+Nice and simple, right? Well, there's a bit more that happens in there.
+
+QUIC includes built-in reliability and flow-control mechanisms. Because UDP is
+notoriously unreliable, every QUIC packet sent by an endpoint is identified by
+a monotonically increasing packet number. Each endpoint keeps track of which
+packets it has received and sends an acknowledgement to the other endpoint. If
+the sending endpoint does not receive an acknowledgement for a packet it has
+sent within some specified period of time, then the endpoint will assume the
+packet was lost and will retransmit it. As I'll show later, this causes some
+complexity in the implementation when it comes to how long data must be
+retained in memory.
+
+For flow-control, QUIC implements separate congestion windows (cwnd) for both
+the overall connection _and_ per individual stream. To keep it simple, the
+receiving endpoint tells the sending endpoint how much additional data it is
+willing to accept right now. The sending endpoint could choose to send more but
+the receiving endpoint could also choose to ignore that additional data or even
+close the connection if the sender does not is not cooperating.
+
+There are also a number of built in mechanisms that endpoints can use to
+validate that a usable network path exists between the endpoints. This is a
+particularly important and unique characteristic of QUIC given that it is built
+on UDP. Unlike TCP, where a connection establishes a persistent flow of data
+that is tightly bound to a specific network path, UDP traffic is much more
+flexible. With QUIC, it is possible to start a connection on one network
+connection but transition it to another (for instance, when a mobile device
+changes from a WiFi connection to a mobile data connection). When such a
+transition happens, the QUIC endpoints must verify that they can still
+successfully send packets to each other securely (this implementation currently
+does not support connection migration).
+
+All of this adds additional complexity to the protocol implementation. With
+TCP, which also includes flow control, reliability, and so forth, Node.js can
+rely on the operating system handling everything. QUIC, however, is designed to
+be implementable in user-space -- that is, it is designed such that, to the
+kernel, it looks like ordinary UDP traffic. This is a good thing, in general,
+but it means the Node.js implementation needs to be quite a bit more complex
+than "regular" UDP datagrams or TCP connections that are abstracted behind
+libuv APIs and operating system syscalls.
+
+There are lots of details we could go into but for now, let's turn the focus to
+how the QUIC implementation here is structured and how it operates.
+
+## Code organization
+
+Thankfully for us, there are open source libraries that implement most of the
+complex details of QUIC packet serialization and deserialization, flow control,
+data loss, connection state management, and more. We've chosen to use the
+[ngtcp2][] and [nghttp3][] libraries. These can be found in the `deps` folder
+along with the other vendored-in dependencies.
+
+The directory in which you've found this README.md (`src/quic`) contains the
+C++ code that bridges `ngtcp2` and `nghttp3` into the Node.js environment.
+These provide the _internal_ API surface and underlying implementation that
+supports the intermediate JavaScript API that can be found in
+`lib/internal/quic`.
+
+So, in summary:
+
+* `deps/ngtcp2` and `deps/nghttp3` provide the low-level protocol
+ implementation.
+* `src/quic` provides the Node.js-specific I/O, state management, and internal
+ API.
+* `lib/internal/quic` provides the intermediate JavaScript API.
+
+## The Internals
+
+The Node.js QUIC implementation is built around a model that tracks closely
+with the key elements of the protocol. There are three main components:
+
+* The `Endpoint` represents a binding to a local UDP socket. The `Endpoint` is
+ ultimately responsible for:
+ * Transmitting and receiving UDP packets, and verifying that received packets
+ are valid QUIC packets; and
+ * Creating new client and server `Sessions`.
+* A `Session` represents either the client-side or server-side of a QUIC
+ connection. The `Session` is responsible for persisting the current state of
+ the connection, including the TLS 1.3 packet protection keys, the current set
+ of open streams, and handling the serialization/deserialization of QUIC
+ packets.
+* A `Stream` represents an individual unidirectional, or bidirectional flow of
+ data through a `Session`.
+
+### `Endpoint`
+
+You can think of the `Endpoint` as the entry point into the QUIC protocol.
+Creating the `Endpoint` is always the first step, regardless of whether you'll
+be acting as a client or a server. The `Endpoint` manages a local binding to a
+`uv_udp_t` UDP socket handle. It controls the full lifecycle of that socket and
+manages the flow of data through that socket. If you're already familiar with
+Node.js internals, the `Endpoint` has many similarities to the `UDPWrap` class
+that underlies the UDP/Datagram module. However, there are a some important
+differences.
+
+Within the internal API, and `Endpoint` consists of three primary elements:
+
+* An `Endpoint::UDP` object that directly wraps the `uv_udp_t` handle. This is
+ internal to the `Endpoint`.
+* An `Endpoint` that performs all of the protocol-specific work.
+* An `EndpointOptions` objet that is used to configure the Endpoint.
+
+When the `Endpoint` is told to listen as a server, or connect as a client, it's
+internal `UDP` instance will automatically bind to the local UDP port.
+
+When the `Endpoint::UDP` receives a packet, it will immediately forward that on
+to the owning `Endpoint`, which will perform some basic validation checks on
+the packet to first determine whether it is an acceptable QUIC packet. Second
+it will determine if it is a new initial packet (intended to create a new QUIC
+connection) or a packet for an already established QUIC connection.
+
+If the received packet is a valid initial packet, the `Endpoint` will create a
+new server-side `Session`. If the packet is valid but not an initial packet, it
+will forward it to the appropriate existing `Session`.
+
+#### Network-Path Validation
+
+Aside from managing the lifecycle of the UDP handle, and managing the routing
+of packets the `Endpoint` is also responsible for performing initial network
+path validation and keeping track of validated remote socket addresses.
+
+By default, the first time the `Endpoint` receives an initial QUIC packet from
+a peer, it will respond with a "retry challenge". A retry is a special QUIC
+packet that contains a cryptographic token that the client must receive and
+return in subsequent packets in order to demonstrate that the network path
+between the two peers is viable. Once the path has been verified, the
+`Endpoint` uses an LRU cache to remember the socket address so that it can
+skip validation on future connections. That cache is transient and only
+persists as long as the `Endpoint` instance exists. Also, the cache is not
+shared among multiple `Endpoint` instances, so the path verification may be
+repeated.
+
+The retry token is cryptographically generated and incorporates protections
+that protect it against abuse or forgery. For the client peer, the token is
+opaque and should appear random. For the server that created it, however, the
+token will incorporate a timestamp and the remote socket address so that
+attempts at forging, reusing, or re-routing packets to alternative endpoints
+can be detected.
+
+The retry does add an additional network round-trip to the establishment of the
+connection, but a necessary one. The cost should be minimal, but there is a
+configuration option that disables initial network-path validation. This can be
+a good (albeit minor) performance optimization when using QUIC on trusted
+network segments.
+
+#### Stateless Reset
+
+Bad things happen. Sometimes a client or server crashes abruptly in the middle
+of a connection. When this happens, the `Endpoint` and `Session` will be lost
+along with all associated state. Because QUIC is designed to be resilient
+across physical network connections, such a crash means the remote peer will
+not become immediately aware of the failure. To deal with such situations, QUIC
+supports a mechanism called "Stateless Reset", how it works is fairly simple.
+
+Each peer in a QUIC connection uses a unique identifier called a "connection
+ID" to identify the QUIC connection. Optionally associated with every
+connection ID is a cryptographically generated stateless reset token that can
+be reliably and deterministically reproduced by a peer in the event it has lost
+all state related to a connection. The peer that has failed will send that
+token back to the remote peer discreetly wrapped in a packet that looks just
+like any other valid QUIC packet. When the remote peer receives it, it will
+recognize the failure that has occured and will shut things down on it's end
+also.
+
+Obviously there are some security ramifications of such a mechanism. Packets
+that contain stateless reset tokens are not encrypted (they can't be because
+the peer lost all of it's state, including the TLS 1.3 packet protection
+keys). It is important that Stateless Reset tokens be difficult for an attacker
+to guess and incorporate information that only the peers _should_ know.
+
+Stateless Reset is enabled by default, but there is a configuration option that
+disables it.
+
+#### Implicit UDP socket binding and graceful close
+
+When an `Endpoint` is created the local UDP socket is not bound until the
+`Endpoint` is told to create a new client `Session` or listen for new QUIC
+initial packets to create a server `Session`. Unlike `UDPWrap`, which requires
+the user-code to explicitly bind the UDP socket before using it, `Endpoint`
+will automatically bind when necessary.
+
+When the `Endpoint` is gracefully closed, all existing `Session` instances
+associated with the `Endpoint` will be permitted to close naturally. New client
+and server `Session` instances won't be created. As soon as the last remaining
+`Session` closes and the `Endpoint` receives notification that all UDP packets
+that have already been dispatched to the `uv_udp_t` have been sent, the
+`Endpoint` will be destroyed and will no longer be usable. It is possible to
+abruptly destroy the `Endpoint` when an error occurs, but the default is for
+all `Endpoint`s to close gracefully.
+
+The bound `UDP` socket (and the underlying `uv_udp_t` handle) will be closed
+automatically when the `Endpoint` is destroyed.
+
+### `Session`
+
+A `Session` encapsulates and manages the local state for either the client or
+server side of a connection. More specifically, the `Session` wraps the
+`ngtcp2_connection` object. We'll get to that in a bit.
+
+`Session` instances have a fairly large API surface API but they are actually
+fairly simple. They are composed of a couple of key elements:
+
+* A `CryptoContext` that encapsulates the state and operation of the TLS 1.3
+ handshake.
+* A `Session::Application` implementation that handles the QUIC application-
+ specific semantics (for instance, the bits specific to HTTP/3), and
+* A collection of open `Stream` instances owned by the `Session`.
+
+When a new client `Session` is created, the TLS handshake is started
+immediately with a new QUIC initial packet being generated and dispatched to
+the owning `Endpoint`. On the server-side, when a new initial packet is
+received by the `Endpoint` the server-side of the handshake starts
+automatically. This is all handled internally by the `CryptoContext`. Aside
+from driving the TLS handshake, the `CryptoContext` maintains the negotiated
+packet protection keys used throughout the lifetime of the `Session`.
+
+_Code review note_: This is a key difference between QUIC and a TLS-protected
+TCP connection. Specifically, with TCP+TLS, the TLS session is associated with
+the actual physical network connection. When the TCP network connection
+changes, the TLS session is destroyed. With QUIC, however, a Connection is
+independent of the actual network binding. In theory, you could start a QUIC
+Connection over a UDP socket and move it to a Unix Domain Socket (for example)
+and the TLS session remains unchanged. Note, however, none of this is designed
+to work on UDS yet, but there's no reason it couldn't be.
+
+#### The Application
+
+The `Session::Application` implements the application-specific semantics of a
+QUIC-based protocol. HTTP/3 is an example. The application for a `Session` is
+identified using the TLS 1.3 ALPN extension and is always included as part of
+the initial QUIC packet. As soon as the `CryptoContext` has confirmed the TLS
+handshake, an appropriate `Session::Application` implementation is selected.
+Curently there are two such implementations:
+
+* `DefaultApplication` - Implements generic QUIC protocol handling that defers
+ application-specific semantics to user-code in JavaScript. Eseentially, this
+ just means receiving and sending stream data as generically as possible
+ without applying any application-specific semantics in the `Session`. More on
+ this later. Any non-recognized alpn identifier will use the default.
+* `Http3Application` - Implements HTTP/3 specific semantics.
+
+Things are designed such that we can add new `Application` implementations in
+the future.
+
+#### Streams
+
+Every `Session` owns zero or more `Streams`. These will be covered in greater
+detail later. For now it's just important to know that a `Stream` can never
+outlive it's owning `Session`. Like the `Endpoint`, a `Session` supports a
+graceful shutdown mode that will allow all currently opened `Stream`s to end
+naturally before destroying the `Session`. During graceful shutdown, creating
+new `Stream`s will not be allowed, and any attempt by the remote peer to start
+a new `Stream` will be ignored.
+
+#### Early Data
+
+By design most QUIC packets for a `Session` are encrypted. In typical use, a
+`Stream` should only be created once the TLS handshake has been completed and
+the encryption keys have been fully negotiated. That, however, does require at
+least one full network round trip ('1RTT') before any application data can be
+sent. In some cases, that full round trip means additional latency that can be
+avoided if you're willing to sacrifice a little bit of security.
+
+QUIC supports what is called 0RTT and 0.5RTT `Stream`s. That is, `Stream`
+instances initiated within the initial set of QUIC packets exchanged by the
+client and server peers. These will have only limited protection as they are
+transmitted before the TLS keys can be negotiated. In the implementation, we
+call this "early data".
+
+Client `Session`s support 0RTT early data if they use TLS Session Resumption.
+
+Server `Session`s always support 0.5RTT early sessions. That is, they can
+always initiate streams once the TLS handshake has started.
+
+### `Stream`
+
+A `Stream` is a undirectional or bidirectional data flow within a `Session`.
+They are conceptually similar to a `Duplex` but are implemented very
+differently.
+
+Let's talk a bit about the lifecycle of a `Stream`.
+
+#### Lifecycle
+
+Once a QUIC Connection has been established, either peer is permitted to open
+a `Stream`, subject to limits set by the receiving peer. Specifically, the
+remote peer sets a limit on the number of open unidirectional and bidirectional
+`Stream`s that the peer can concurrently create.
+
+A `Stream` is created by sending data. If no data is sent, the `Stream` is
+never created.
+
+As mentioned previous, A `Stream` can be undirectional (data can only be sent
+by the peer that initiated it) or bidirectional (data can be sent by both
+peers).
+
+Every `Stream` has a numeric identifier that uniquely identifies it only within
+the scope of the owning `Session`. The stream ID indentifies whether the stream
+was initiated by the client or server, and identifies whether it is
+bidirectional or unidirectional.
+
+A peer can transmit data indefinitely on a `Stream`. The final packet of data
+will include `FIN` flag that signals the peer is done. Alternatively, the
+receiving peer can send a `STOP_SENDING` signal asking the peer to stop. Each
+peer can close it's end of the stream independently of the other, and may do so
+at different times. There are some complexities involved here relating to flow
+control and reliability that we'll get into shortly, but the key things to
+remember is that each peer maintains it's own independent view of the `Stream`s
+open/close state. For instance, a peer that initiates a unidirectional `Stream`
+can consider that `Stream` to be closed immediately after sending the final
+chunk of data, even if the remote peer has not fully received or processed that
+data.
+
+A `Session` will create a new `Stream` the first time it receives a QUIC
+`STREAM` packet with an identifier it has not previously seen.
+
+As with `Endpoint` and `Session`, `Stream` instances support a graceful
+shutdown that allow the flow of data to complete naturally before destroying
+the `Stream` instance and freeing resources. When an error occurs, however, the
+`Stream` can be destroyed abruptly.
+
+When the `Session` receives data for a `Stream`, it will push that data into
+the `Stream` for processing. It will do so first by passing the data through
+the `Session::Application` so that any Application-protocol specific semantics
+can be applied. By the time the `Stream` receives the data, it can be safely
+assumed that any Application specific semantics understood by Node.js have been
+applied.
+
+As the `Stream` receives data it will either push that directly out to
+JavaScript if there is a `'data'` event listener attached, or will store the
+data in an internal buffer until a `'data'` listener is attached. A `Stream`
+can also be temporarily paused which will cause the inbound data to be buffered
+also.
+
+This queue will be retained even after the `Stream`, and even the `Session` has
+been closed. The data will be freed only after it is consumed or the `Stream`
+is garbage collected and destroyed.
+
+#### Sources and Buffers
+
+A `Source` provides the outbound data that is to be sent by the `Session` for a
+`Stream`. They are complicated largely because there are several ways in which
+user code might want to sent data, and by the fact that we can never really
+know how much `Stream` data is going to be encoded in any given QUIC packet the
+`Session` is asked to send at any given time, and by the fact that sent data
+must be retained in memory until an explicit acknowledgement has been received
+from the remote peer indicating that the data have been successfully received.
+
+Central to implementing a Source is the `Buffer`.
+
+Internally, a `Buffer` maintains zero or more separate chunks of data. These
+may or may not be contiguous in memory. Externally, the `Buffer` makes these
+chunks of memory _appear_ contiguous.
+
+The `Buffer` maintains two data pointers:
+
+* The `read` pointer identifies the largest offset of data that has been
+ encoded at least once into a QUIC packet. When the `read` pointer reaches the
+ logical end of the `Buffer`s encapsulated data, the `Buffer`s data has been
+ transmitted at least once to the remote peer. However, we're not done with
+ the data yet, and we must keep it in memory _without changing the pointers of
+ that data in memory_. The ngtcp2 library will automatically handle
+ retransmitting ranges of that data if packet loss is suspected.
+* The `ack` pointer identifies the largest offset of data that has been
+ acknowledged as received by the remote peer. Buffered data can only be freed
+ from memory once it has been acknowledged (that is, the ack pointer has moved
+ beyond the data offset).
+
+User code may provide all of the data to be sent on a `Stream` synchronously,
+all at once, or may space it out asynchronously over time. Regardless of how
+the data fills the `Buffer`, it must always be read in a reliable and
+consistent way. Because of flow-control, retransmissions, and a range of other
+factors, we can never know whenever a QUIC packet is encoded, exactly how much
+`Stream` data will be included. To handle this, we provide an API that allows
+`Session` and `ngtcp2` to pull data from the `Buffer` on-demand, even if the
+`Source` is asynchronously pushing data in.
+
+Sources inherit from `Buffer::Source`. There are a handful of Source types
+implemented:
+
+* `NullSource` - Essentially, no data will be provided.
+* `ArrayBufferViewSource` - The `Buffer` data is provided all at once in a
+ single `ArrayBuffer` or `ArrayBufferView` (that is, any `TypedArray` or
+ `DataView`). When `Stream` data is provided as a JavaScript string, this
+ Source implementation is also used.
+* `BlobSource` - The `Buffer` data is provided all at once in a `node::Blob`
+ instance.
+* `StreamSource` - The `Buffer` data is provided asynchronously in multiple
+ chunks by a JavaScript stream. That can be either a Node.js `stream.Readable`
+ or a Web `ReadableStream`.
+* `StreamBaseSource` - The `Buffer` data is provided asynchronously in multiple
+ chunks by a `node::StreamBase` instance (such as `FileHandle`).
+
+All `Stream` instances are created with no `Source` attached. During the
+`Stream`s lifetime, no more than a single `Source` can be attached. Once
+attached, the outbound flow of data will begin. Once all of the data provided
+by the `Source` has been encoded in QUIC packets transmitted to the peer (even
+if not yet acknowledged) the writable side of the `Stream` can be considered to
+be closed. The `Stream` and the `Buffer`, however, must be retained in memory
+until the proper acknowledgements have been received.
+
+#### A Word On Bob Streams
+
+`Buffer` and `Stream` use a different stream API mechanism than other existing
+things in Node.js such as `StreamBase`. This model, affectionately known as
+"Bob" is a simple pull stream API defined in the `src/node_bob.h` header.
+
+The Bob API was first conceived in the hallways of one of the first Node.js
+Interactive Conferences in Vancouver, British Colombia. It was further
+developed by Node.js core contributor Fishrock123 as a separate project, and
+integrated into Node.js as part of the initial prototype QUIC implementation.
+
+The API works by pairing a Source with a Sink. The Source provides data, the
+Sink consumes it. The Sink will repeatedly call the Source's `Pull()` method
+until there is no more data to consume. The API is capable of working in
+synchronous and asynchronous modes, and includes backpressure signals for when
+the Source does not yet have data to provide.
+
+While `StreamBase` is the more common way in which streaming data is processed
+in Node.js, it is just simply not compatible with the way we need to be able to
+push data into a QUIC connection.
+
+#### Headers
+
+QUIC Streams are _just_ a stream of bytes. At the protocol level, QUIC has no
+concept of headers. However, QUIC applications like HTTP/3 do have a concept
+of Headers. We'll explain how exactly HTTP/3 accomplishes that in a bit.
+
+In order to more easily support HTTP/3 and future applications that are known
+to support Headers, the `Stream` object includes a generic mechanism for
+accumulating blocks of headers associated with the `Stream`. When a `Session`
+is using the `DefaultApplication`, the Header APIs will be unused. If the
+application supports headers, it will be up to the JavaScript layer to work
+that out.
+
+For the `Http3Application` implementation, however, Headers will be processed
+and accumulated in the `Stream`. These headers are simple name+value pairs with
+additional flags. Three kinds of header blocks are supported:
+
+* `Informational` (or `Hints`) - These correspond to 1xx HTTP response headers.
+* `Initial` - Corresponding to HTTP request and response headers.
+* `Trailing` - Corresponding to HTTP trailers.
+
+### The Rest
+
+There are a range of additional utility classes and functions used throughout
+the implementation. I won't go into every one of those here but I want to
+touch on a few of the more critical and visible ones.
+
+#### `BindingState`
+
+The `BindingState` maintains persistent state for the `internalBinding('quic')`
+internal module. These include commonly used strings, constructor templates,
+and callback functions that are used to push events out to the JavaScript side.
+Collecting all of this in the `BindingState` keeps us from having to add a
+whole bunch of QUIC specific stuff to the `node::Environment`. You'll see this
+used extensively throughout the implementation in `src/quic`.
+
+#### `CID`
+
+Encapsulates a QUIC Connection ID. This is pretty simple. It just makes it
+easier to work with a connection identifier.
+
+#### `Packet`
+
+Encapulates an encoded QUIC packet that is to be sent by the `Endpoint`. The
+lifecycle here is simple: Create a `Packet`, encode QUIC data into it, pass it
+off to the `Endpoint` to send, then destroy it once the `uv_udp_t` indicates
+that the packet has been successfully sent.
+
+#### `LogStream`
+
+The `LogStream` is a utility `StreamBase` implementation that pushes diagnostic
+keylog and QLog data out to the JavaScript API.
+
+QLog is a QUIC-specific logging format being defined by the IETF working group.
+You can read more about it here:
+
+* [https://www.ietf.org/archive/id/draft-ietf-quic-load-balancers-07.html]()
+* [https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-00.html]()
+* [https://www.ietf.org/archive/id/draft-ietf-quic-qlog-h3-events-00.html]()
+
+These are disabled by default. A `Session` can be configured to emit diagnostic
+logs.
+
+## HTTP/3
+
+[HTTP/3][] layers on top of the QUIC protocol. It essentially just maps HTTP
+request and response messages over multiple QUIC streams.
+
+Upon completion of the QUIC TLS handshake, each of the two peers (HTTP client
+and server) will create three new unidirectional streams in each direction.
+These are "control" streams that manage the state of the HTTP connection. For
+each HTTP request, the client will initiate a bidirectional stream with the
+server. The HTTP request headers, payload, and trailing footers will be
+transmitted using that bidirectional stream with some associated control data
+being sent over the unidirectional control streams.
+
+It's important to understand the unidirectional control streams are handled
+completely internally by the `Session` and the `Http3Application`. They are
+never exposed to the JavaScript API layer.
+
+Fortunately, there is a lot of complexity involved in the implementation of
+HTTP/3 that is hidden away and handled by the `nghttp3` dependency.
+
+As we'll see in a bit, at the JavaScript API level there is no HTTP/3 specific
+API. It's all just QUIC. The HTTP/3 semantics are applied internally, as
+transparently as possible.
+
+## The JavaScript API
+
+The QUIC JavaScript API closely follows the structure laid out by the
+internals.
+
+There are the same three fundamental components: `Endpoint`, `Session`, and
+`Stream`.
+
+Unlike older Node.js APIs, these build on more modern Web Platform API
+compatible pieces such as `EventTarget`. Overall, the API style diverges quite
+a bit from the older, more specific `net` module.
+
+```js
+const endpointOptions = new EndpointOptions(
+ new SocketAddress({ address: '123.123.123.123', port: 1234}),
+ {
+ maxConnectionsTotal: 100,
+ validateAddress: true,
+ }
+);
+
+const key = getPrivateKeySomehow();
+const certs = getCertificateSomehow();
+
+const sessionOptions({
+ alpn: 'zzz',
+ secure: {
+ key,
+ certs,
+ },
+});
+
+const server = new Endpoint(endpointOptions);
+server.listen(sessionOptions);
+```
+
+### Configuration
+
+One unique characteristics of the QUIC JavaScript API is that there are
+distinct "Options" objects that encapuslate all of the configuration options --
+and QUIC has a _lot_ of options. By separating these out we make the
+implementation much cleaner by separating out option validation, defaults, and
+so on. These config objects are backed by objects and structs at the C++ level
+that make it more efficient to pass those configurations down to the native
+layer.
+
+### Stats
+
+Each of the `Endpoint`, `Session`, and `Stream` objects expose an additional
+`stats` property that provides access to statistics actively collected while
+the objects are in use. These can be used not only to monitor the performance
+and behavior of the QUIC implementation, but also dynamically respond and
+adjust to the current load on the endpoints.
+
+[HTTP/3]: https://www.ietf.org/archive/id/draft-ietf-quic-http-34.html
+[RFC 8999]: https://www.rfc-editor.org/rfc/rfc8999.html
+[RFC 9000]: https://www.rfc-editor.org/rfc/rfc9000.html
+[RFC 9001]: https://www.rfc-editor.org/rfc/rfc9001.html
+[RFC 9002]: https://www.rfc-editor.org/rfc/rfc9002.html
+[draft-ietf-quic-http-34]: https://www.ietf.org/archive/id/draft-ietf-quic-http-34.html
+[draft-ietf-quic-qpack-21]: https://www.ietf.org/archive/id/draft-ietf-quic-qpack-21.html
+[nghttp3]: https://github.com/ngtcp2/nghttp3
+[ngtcp2]: https://github.com/ngtcp2/ngtcp2
diff --git a/src/quic/crypto.cc b/src/quic/crypto.cc
new file mode 100644
index 00000000000000..6301f1577c04f8
--- /dev/null
+++ b/src/quic/crypto.cc
@@ -0,0 +1,1221 @@
+#include "crypto.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "openssl/ssl.h"
+#include "quic/defs.h"
+#include "session.h"
+
+namespace node {
+
+using v8::ArrayBuffer;
+using v8::ArrayBufferView;
+using v8::BackingStore;
+using v8::FunctionTemplate;
+using v8::Just;
+using v8::Local;
+using v8::Maybe;
+using v8::MaybeLocal;
+using v8::Nothing;
+using v8::Object;
+using v8::Uint8Array;
+using v8::Value;
+
+namespace quic {
+
+// ======================================================================================
+namespace {
+class AeadContextPointer final {
+ public:
+ enum class Mode { ENCRYPT, DECRYPT };
+
+ QUIC_MOVE_NO_COPY(AeadContextPointer)
+
+ ~AeadContextPointer() { ngtcp2_crypto_aead_ctx_free(&ctx_); }
+
+ inline ngtcp2_crypto_aead_ctx* operator->() { return &ctx_; }
+ inline const ngtcp2_crypto_aead_ctx* operator->() const { return &ctx_; }
+ inline ngtcp2_crypto_aead_ctx& operator*() { return ctx_; }
+ inline const ngtcp2_crypto_aead_ctx& operator*() const { return ctx_; }
+
+ inline const ngtcp2_crypto_aead_ctx* get() const { return &ctx_; }
+
+ static AeadContextPointer forEncrypt(const uint8_t* key,
+ const ngtcp2_crypto_aead& aead) {
+ AeadContextPointer ptr;
+ CHECK(NGTCP2_OK(ngtcp2_crypto_aead_ctx_encrypt_init(
+ &ptr.ctx_, &aead, key, kCryptoTokenIvlen)));
+ return ptr;
+ }
+
+ static AeadContextPointer forDecrypt(const uint8_t* key,
+ const ngtcp2_crypto_aead& aead) {
+ AeadContextPointer ptr;
+ CHECK(NGTCP2_OK(ngtcp2_crypto_aead_ctx_decrypt_init(
+ &ptr.ctx_, &aead, key, kCryptoTokenIvlen)));
+ return ptr;
+ }
+
+ private:
+ AeadContextPointer() = default;
+ ngtcp2_crypto_aead_ctx ctx_;
+};
+} // namespace
+
+// ======================================================================================
+// SessionTicketAppData
+
+SessionTicketAppData::SessionTicketAppData(SSL_SESSION* session)
+ : session_(session) {}
+
+bool SessionTicketAppData::Set(const uint8_t* data, size_t len) {
+ if (set_) return false;
+ set_ = true;
+ SSL_SESSION_set1_ticket_appdata(session_, data, len);
+ return set_;
+}
+
+bool SessionTicketAppData::Get(uint8_t** data, size_t* len) const {
+ if (!set_) return false;
+ return SSL_SESSION_get0_ticket_appdata(
+ session_, reinterpret_cast(data), len) == 1;
+}
+
+// ======================================================================================
+// A stateless reset token is used when a QUIC endpoint receives a QUIC packet
+// with a short header but the associated connection ID cannot be matched to any
+// known Session. In such cases, the receiver may choose to send a subtle opaque
+// indication to the sending peer that state for the Session has apparently been
+// lost. For any on- or off- path attacker, a stateless reset packet resembles
+// any other QUIC packet with a short header. In order to be successfully
+// handled as a stateless reset, the peer must have already seen a reset token
+// issued to it associated with the given CID. The token itself is opaque to the
+// peer that receives is but must be possible to statelessly recreate by the
+// peer that originally created it. The actual implementation is Node.js
+// specific but we currently defer to a utility function provided by ngtcp2.
+bool StatelessResetToken::GenerateResetToken(const uint8_t* secret,
+ const CID& cid) {
+ return NGTCP2_OK(ngtcp2_crypto_generate_stateless_reset_token(
+ buf_, secret, NGTCP2_STATELESS_RESET_TOKENLEN, cid));
+}
+
+// ======================================================================================
+// A RETRY packet communicates a retry token to the client. Retry tokens are
+// generated only by QUIC servers for the purpose of validating the network path
+// between a client and server. The content payload of the RETRY packet is
+// opaque to the clientand must not be guessable by on- or off-path attackers.
+//
+// A QUIC server sends a RETRY token as a way of initiating explicit path
+// validation in response to an initial QUIC packet. The client, upon receiving
+// a RETRY, must abandon the initial connection attempt and try again with the
+// received retry token included with the new initial packet sent to the server.
+// If the server is performing explicit validation, it will look for the
+// presence of the retry token and attempt to validate it if found. The internal
+// structure of the retry token must be meaningful to the server, and the server
+// must be able to validate that the token is correct without relying on any
+// state left over from the previous connection attempt. We use an
+// implementation that is provided by ngtcp2.
+//
+// The token secret must be kept secret on the QUIC server that generated the
+// retry. When multiple QUIC servers are used in a cluster, it cannot be
+// guaranteed that the same QUIC server instance will receive the subsequent new
+// Initial packet. Therefore, all QUIC servers in the cluster should either
+// share or be aware of the same token secret or a mechanism needs to be
+// implemented to ensure that subsequent packets are routed to the same QUIC
+// server instance.
+//
+// A malicious peer could attempt to guess the token secret by sending a large
+// number specially crafted RETRY-eliciting packets to a server then analyzing
+// the resulting retry tokens. To reduce the possibility of such attacks, the
+// current implementation of QuicSocket generates the token secret randomly for
+// each instance, and the number of RETRY responses sent to a given remote
+// address should be limited. Such attacks should be of little actual value in
+// most cases. In cases where the secret is shared across multiple servers, be
+// sure to implement a mechanism to rotate those.
+
+// Validates a retry token, returning the original CID extracted from the token
+// if it is valid.
+Maybe ValidateRetryToken(quic_version version,
+ const ngtcp2_vec& token,
+ const SocketAddress& addr,
+ const CID& dcid,
+ const uint8_t* token_secret,
+ uint64_t verification_expiration) {
+ CID ocid;
+ int ret = ngtcp2_crypto_verify_retry_token(ocid,
+ token.base,
+ token.len,
+ token_secret,
+ kTokenSecretLen,
+ version,
+ addr.data(),
+ addr.length(),
+ dcid,
+ verification_expiration,
+ uv_hrtime());
+ return ret == 0 ? Just(ocid) : Nothing();
+}
+
+// Generates a RETRY packet.
+bool GenerateRetryPacket(const BaseObjectPtr& packet,
+ quic_version version,
+ const uint8_t* token_secret,
+ const CID& dcid,
+ const CID& scid,
+ const SocketAddress& remote_addr) {
+ uint8_t token[256];
+ auto cid = CIDFactory::random().Generate(NGTCP2_MAX_CIDLEN);
+
+ auto ret = ngtcp2_crypto_generate_retry_token(token,
+ token_secret,
+ kTokenSecretLen,
+ version,
+ remote_addr.data(),
+ remote_addr.length(),
+ cid,
+ dcid,
+ uv_hrtime());
+
+ if (ret == -1) return false;
+ size_t tokenlen = ret;
+ size_t pktlen = tokenlen + (2 * NGTCP2_MAX_CIDLEN) + scid.length() + 8;
+
+ ngtcp2_vec vec = *packet;
+
+ ssize_t nwrite = ngtcp2_crypto_write_retry(
+ vec.base, pktlen, version, scid, cid, dcid, token, tokenlen);
+
+ if (nwrite <= 0) return false;
+ packet->Truncate(nwrite);
+
+ return true;
+}
+
+// ======================================================================================
+Maybe GenerateToken(quic_version version,
+ uint8_t* token,
+ const SocketAddress& addr,
+ const uint8_t* token_secret) {
+ auto ret = ngtcp2_crypto_generate_regular_token(token,
+ token_secret,
+ kTokenSecretLen,
+ addr.data(),
+ addr.length(),
+ uv_hrtime());
+
+ if (ret == -1) {
+ return Nothing();
+ }
+
+ return Just(static_cast(ret));
+}
+
+bool ValidateToken(quic_version version,
+ const ngtcp2_vec& token,
+ const SocketAddress& addr,
+ const uint8_t* token_secret,
+ uint64_t verification_expiration) {
+ return NGTCP2_OK(ngtcp2_crypto_verify_regular_token(token.base,
+ token.len,
+ token_secret,
+ kTokenSecretLen,
+ addr.data(),
+ addr.length(),
+ verification_expiration,
+ uv_hrtime()));
+}
+// ======================================================================================
+// OpenSSL Helpers
+
+// Get the ALPN protocol identifier that was negotiated for the session
+Local GetALPNProtocol(Session* session) {
+ auto env = session->env();
+ auto alpn = session->crypto_context().selected_alpn();
+ return alpn == &NGHTTP3_ALPN_H3[1]
+ ? BindingState::Get(env).http3_alpn_string().As()
+ : ToV8Value(env->context(), alpn, env->isolate())
+ .FromMaybe(Local());
+}
+
+namespace {
+Session* GetSession(const SSL* ssl) {
+ ngtcp2_crypto_conn_ref* ref =
+ static_cast(SSL_get_app_data(ssl));
+ return static_cast(ref->user_data);
+}
+
+int TlsStatusCallback(SSL* ssl, void* arg) {
+ Session* session = GetSession(ssl);
+ int ret;
+ if (SSL_get_tlsext_status_type(ssl) == TLSEXT_STATUSTYPE_ocsp) {
+ ret = session->crypto_context().OnOCSP();
+ // We need to check if the session is destroyed after the OnOCSP check.
+ return UNLIKELY(session->is_destroyed()) ? 0 : ret;
+ }
+ return 1;
+}
+
+int TlsExtStatusCallback(SSL* ssl, void* arg) {
+ return GetSession(ssl)->crypto_context().OnTLSStatus();
+}
+
+void KeylogCallback(const SSL* ssl, const char* line) {
+ GetSession(ssl)->crypto_context().Keylog(line);
+}
+
+int ClientHelloCallback(SSL* ssl, int* tls_alert, void* arg) {
+ Session* session = GetSession(ssl);
+
+ int ret = session->crypto_context().OnClientHello();
+ if (UNLIKELY(session->is_destroyed())) {
+ *tls_alert = SSL_R_SSL_HANDSHAKE_FAILURE;
+ return 0;
+ }
+ switch (ret) {
+ case 0:
+ return 1;
+ case -1:
+ return -1;
+ default:
+ *tls_alert = ret;
+ return 0;
+ }
+}
+
+int AlpnSelectionCallback(SSL* ssl,
+ const unsigned char** out,
+ unsigned char* outlen,
+ const unsigned char* in,
+ unsigned int inlen,
+ void* arg) {
+ Session* session = GetSession(ssl);
+
+ size_t alpn_len = session->alpn().length();
+ if (alpn_len > 255) return SSL_TLSEXT_ERR_NOACK;
+
+ // The Session supports exactly one ALPN identifier. If that does not match
+ // any of the ALPN identifiers provided in the client request, then we fail
+ // here. Note that this will not fail the TLS handshake, so we have to check
+ // later if the ALPN matches the expected identifier or not.
+ //
+ // We might eventually want to support the ability to negotiate multiple
+ // possible ALPN's on a single endpoint/session but for now, we only support
+ // one.
+ if (SSL_select_next_proto(
+ const_cast(out),
+ outlen,
+ reinterpret_cast(session->alpn().c_str()),
+ alpn_len,
+ in,
+ inlen) == OPENSSL_NPN_NO_OVERLAP) {
+ return SSL_TLSEXT_ERR_NOACK;
+ }
+
+ return SSL_TLSEXT_ERR_OK;
+}
+
+int AllowEarlyDataCallback(SSL* ssl, void* arg) {
+ return GetSession(ssl)->allow_early_data() ? 1 : 0;
+}
+
+int NewSessionCallback(SSL* ssl, SSL_SESSION* session) {
+ auto ret = GetSession(ssl)->crypto_context().OnNewSession(session);
+ return ret;
+}
+
+int GenerateSessionTicket(SSL* ssl, void* arg) {
+ SessionTicketAppData app_data(SSL_get_session(ssl));
+ GetSession(ssl)->SetSessionTicketAppData(app_data);
+ return 1;
+}
+
+SSL_TICKET_RETURN DecryptSessionTicket(SSL* ssl,
+ SSL_SESSION* session,
+ const unsigned char* keyname,
+ size_t keyname_len,
+ SSL_TICKET_STATUS status,
+ void* arg) {
+ switch (status) {
+ default:
+ return SSL_TICKET_RETURN_IGNORE;
+ case SSL_TICKET_EMPTY:
+ // Fall through
+ case SSL_TICKET_NO_DECRYPT:
+ return SSL_TICKET_RETURN_IGNORE_RENEW;
+ case SSL_TICKET_SUCCESS_RENEW:
+ // Fall through
+ case SSL_TICKET_SUCCESS:
+ SessionTicketAppData app_data(session);
+ switch (GetSession(ssl)->GetSessionTicketAppData(
+ app_data, SessionTicketAppData::Flag::STATUS_NONE)) {
+ default:
+ return SSL_TICKET_RETURN_IGNORE;
+ case SessionTicketAppData::Status::TICKET_IGNORE:
+ return SSL_TICKET_RETURN_IGNORE;
+ case SessionTicketAppData::Status::TICKET_IGNORE_RENEW:
+ return SSL_TICKET_RETURN_IGNORE_RENEW;
+ case SessionTicketAppData::Status::TICKET_USE:
+ return SSL_TICKET_RETURN_USE;
+ case SessionTicketAppData::Status::TICKET_USE_RENEW:
+ return SSL_TICKET_RETURN_USE_RENEW;
+ }
+ UNREACHABLE();
+ }
+}
+
+bool SetTransportParams(Session* session, const crypto::SSLPointer& ssl) {
+ const ngtcp2_transport_params* params =
+ ngtcp2_conn_get_local_transport_params(session->connection());
+ uint8_t buf[512];
+ ssize_t nwrite = ngtcp2_encode_transport_params(
+ buf,
+ arraysize(buf),
+ NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS,
+ params);
+ return nwrite >= 0 &&
+ SSL_set_quic_transport_params(ssl.get(), buf, nwrite) == 1;
+}
+
+void SetHostname(const crypto::SSLPointer& ssl, const std::string& hostname) {
+ // If the hostname is an IP address, use an empty string as the hostname
+ // instead.
+ X509_VERIFY_PARAM* param = SSL_get0_param(ssl.get());
+ X509_VERIFY_PARAM_set_hostflags(param, 0);
+
+ if (UNLIKELY(SocketAddress::is_numeric_host(hostname.c_str()))) {
+ SSL_set_tlsext_host_name(ssl.get(), "");
+ CHECK_EQ(X509_VERIFY_PARAM_set1_host(param, "", 0), 1);
+ return;
+ }
+
+ SSL_set_tlsext_host_name(ssl.get(), hostname.c_str());
+ CHECK_EQ(
+ X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), hostname.length()),
+ 1);
+}
+
+} // namespace
+
+ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level) {
+ switch (ossl_level) {
+ case ssl_encryption_initial:
+ return NGTCP2_CRYPTO_LEVEL_INITIAL;
+ case ssl_encryption_early_data:
+ return NGTCP2_CRYPTO_LEVEL_EARLY;
+ case ssl_encryption_handshake:
+ return NGTCP2_CRYPTO_LEVEL_HANDSHAKE;
+ case ssl_encryption_application:
+ return NGTCP2_CRYPTO_LEVEL_APPLICATION;
+ }
+ UNREACHABLE();
+}
+
+const char* crypto_level_name(ngtcp2_crypto_level level) {
+ switch (level) {
+ case NGTCP2_CRYPTO_LEVEL_INITIAL:
+ return "initial";
+ case NGTCP2_CRYPTO_LEVEL_EARLY:
+ return "early";
+ case NGTCP2_CRYPTO_LEVEL_HANDSHAKE:
+ return "handshake";
+ case NGTCP2_CRYPTO_LEVEL_APPLICATION:
+ return "app";
+ }
+ UNREACHABLE();
+}
+
+MaybeLocal GetCertificateData(Environment* env,
+ crypto::SecureContext* sc,
+ GetCertificateType type) {
+ X509* cert;
+ switch (type) {
+ case GetCertificateType::SELF:
+ cert = sc->cert().get();
+ break;
+ case GetCertificateType::ISSUER:
+ cert = sc->issuer().get();
+ break;
+ default:
+ UNREACHABLE();
+ }
+
+ Local ret = v8::Undefined(env->isolate());
+ int size = i2d_X509(cert, nullptr);
+ if (size > 0) {
+ std::shared_ptr store =
+ ArrayBuffer::NewBackingStore(env->isolate(), size);
+ unsigned char* buf = reinterpret_cast(store->Data());
+ i2d_X509(cert, &buf);
+ ret = ArrayBuffer::New(env->isolate(), store);
+ }
+
+ return ret;
+}
+
+// ======================================================================================
+// CryptoContext
+
+struct CryptoContext::CallbackScope final {
+ CryptoContext* context;
+
+ inline explicit CallbackScope(CryptoContext* context_) : context(context_) {
+ context_->in_tls_callback_ = true;
+ }
+
+ inline ~CallbackScope() { context->in_tls_callback_ = false; }
+
+ inline static bool is_in_callback(const CryptoContext& context) {
+ return context.in_tls_callback_;
+ }
+};
+
+struct CryptoContext::ResumeHandshakeScope final {
+ using DoneCB = std::function;
+ CryptoContext* context;
+ DoneCB done;
+
+ template
+ inline ResumeHandshakeScope(CryptoContext* context_, Fn done_)
+ : context(context_), done(std::forward(done_)) {}
+
+ inline ~ResumeHandshakeScope() {
+ if (!is_handshake_suspended()) return;
+
+ done();
+
+ if (!CallbackScope::is_in_callback(*context)) context->ResumeHandshake();
+ }
+
+ inline bool is_handshake_suspended() const {
+ return context->in_ocsp_request_ || context->in_client_hello_;
+ }
+};
+
+BaseObjectPtr CryptoContext::InitializeSecureContext(
+ const Session& session,
+ const CryptoContext::Options& options,
+ ngtcp2_crypto_side side) {
+ auto context = crypto::SecureContext::Create(session.env());
+ bool failed = false;
+
+ context->Initialize([&](crypto::SSLCtxPointer& ctx) {
+ switch (side) {
+ case NGTCP2_CRYPTO_SIDE_SERVER: {
+ ctx.reset(SSL_CTX_new(TLS_server_method()));
+ SSL_CTX_set_app_data(ctx.get(), context);
+
+ if (NGTCP2_ERR(
+ ngtcp2_crypto_openssl_configure_server_context(ctx.get()))) {
+ failed = true;
+ return;
+ }
+
+ SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX);
+ SSL_CTX_set_allow_early_data_cb(
+ ctx.get(), AllowEarlyDataCallback, nullptr);
+ SSL_CTX_set_options(ctx.get(),
+ (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
+ SSL_OP_SINGLE_ECDH_USE |
+ SSL_OP_CIPHER_SERVER_PREFERENCE |
+ SSL_OP_NO_ANTI_REPLAY);
+ SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
+ SSL_CTX_set_alpn_select_cb(ctx.get(), AlpnSelectionCallback, nullptr);
+ if (options.client_hello) {
+ SSL_CTX_set_client_hello_cb(ctx.get(), ClientHelloCallback, nullptr);
+ }
+ SSL_CTX_set_session_ticket_cb(
+ ctx.get(), GenerateSessionTicket, DecryptSessionTicket, nullptr);
+
+ const unsigned char* sid_ctx = reinterpret_cast(
+ options.session_id_ctx.c_str());
+ SSL_CTX_set_session_id_context(
+ ctx.get(), sid_ctx, options.session_id_ctx.length());
+
+ break;
+ }
+ case NGTCP2_CRYPTO_SIDE_CLIENT: {
+ ctx.reset(SSL_CTX_new(TLS_client_method()));
+ SSL_CTX_set_app_data(ctx.get(), context);
+
+ if (NGTCP2_ERR(
+ ngtcp2_crypto_openssl_configure_client_context(ctx.get()))) {
+ failed = true;
+ return;
+ }
+
+ SSL_CTX_set_session_cache_mode(
+ ctx.get(),
+ SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
+ SSL_CTX_sess_set_new_cb(ctx.get(), NewSessionCallback);
+ break;
+ }
+ default:
+ UNREACHABLE();
+ }
+
+ SSL_CTX_set_default_verify_paths(ctx.get());
+ SSL_CTX_set_tlsext_status_cb(ctx.get(), TlsExtStatusCallback);
+ SSL_CTX_set_tlsext_status_arg(ctx.get(), nullptr);
+
+ if (options.keylog) SSL_CTX_set_keylog_callback(ctx.get(), KeylogCallback);
+
+ if (SSL_CTX_set_ciphersuites(ctx.get(), options.ciphers.c_str()) != 1) {
+ failed = true;
+ return;
+ }
+
+ if (SSL_CTX_set1_groups_list(ctx.get(), options.groups.c_str()) != 1) {
+ failed = true;
+ return;
+ }
+ });
+
+ if (failed) {
+ return BaseObjectPtr();
+ }
+
+ // Handle CA certificates...
+
+ const auto addCACert = [&](uv_buf_t ca) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(ca.base, ca.len);
+ if (!bio) return false;
+ context->SetCACert(bio);
+ return true;
+ };
+
+ const auto addRootCerts = [&] {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ context->SetRootCerts();
+ };
+
+ if (!options.ca.empty()) {
+ for (auto& ca : options.ca) {
+ if (!addCACert(ca)) {
+ return BaseObjectPtr();
+ }
+ }
+ } else {
+ addRootCerts();
+ }
+
+ // Handle Certs
+
+ const auto addCert = [&](uv_buf_t cert) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(cert.base, cert.len);
+ if (!bio) return false;
+ auto ret = context->AddCert(std::move(bio));
+ return ret;
+ };
+
+ for (auto& cert : options.certs) {
+ if (!addCert(cert)) {
+ return BaseObjectPtr();
+ }
+ }
+
+ // Handle keys
+
+ const auto addKey = [&](auto& key) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ return context->UseKey(key);
+ // TODO(@jasnell): Maybe SSL_CTX_check_private_key also?
+ };
+
+ for (auto& key : options.keys) {
+ if (!addKey(key)) {
+ return BaseObjectPtr();
+ }
+ }
+
+ // Handle CRL
+
+ const auto addCRL = [&](uv_buf_t crl) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(crl.base, crl.len);
+ if (!bio) return false;
+ return context->SetCRL(bio);
+ };
+
+ for (auto& crl : options.crl) {
+ if (!addCRL(crl)) return BaseObjectPtr();
+ }
+
+ // TODO(@jasnell): Possibly handle other bits. Such a pfx, client cert engine,
+ // and session timeout.
+ return BaseObjectPtr(context);
+}
+
+CryptoContext::CryptoContext(Session* session,
+ const Options& options,
+ ngtcp2_crypto_side side)
+ : conn_ref_({getConnection, session}),
+ session_(session),
+ options_(options),
+ secure_context_(InitializeSecureContext(*session, options, side)),
+ side_(side) {
+ DEBUG(session, "Crypto context created");
+
+ CHECK(secure_context_);
+
+ ssl_.reset(SSL_new(secure_context_->ctx().get()));
+ CHECK(ssl_);
+ CHECK(SSL_is_quic(ssl_.get()));
+
+ SSL_set_app_data(ssl_.get(), &conn_ref_);
+
+ SSL_set_cert_cb(ssl_.get(), TlsStatusCallback, session);
+ SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback);
+
+ // Enable tracing if the `--trace-tls` command line flag is used.
+ if (UNLIKELY(session->env()->options()->trace_tls ||
+ options.enable_tls_trace)) {
+ auto& state = BindingState::Get(session->env());
+ EnableTrace();
+ if (state.warn_trace_tls) {
+ state.warn_trace_tls = false;
+ ProcessEmitWarning(session->env(),
+ "Enabling --trace-tls can expose sensitive data in "
+ "the resulting log");
+ }
+ }
+
+ switch (side) {
+ case NGTCP2_CRYPTO_SIDE_CLIENT: {
+ DEBUG(session, "Initializing crypto client");
+ SSL_set_connect_state(ssl_.get());
+ crypto::SetALPN(ssl_, session->options_.alpn);
+ SetHostname(ssl_, session->options_.hostname);
+ if (options.ocsp)
+ SSL_set_tlsext_status_type(ssl_.get(), TLSEXT_STATUSTYPE_ocsp);
+ break;
+ }
+ case NGTCP2_CRYPTO_SIDE_SERVER: {
+ DEBUG(session, "Initializing crypto server");
+ SSL_set_accept_state(ssl_.get());
+ if (options.request_peer_certificate) {
+ int verify_mode = SSL_VERIFY_PEER;
+ if (options.reject_unauthorized)
+ verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+ SSL_set_verify(ssl_.get(), verify_mode, crypto::VerifyCallback);
+ }
+ break;
+ }
+ default:
+ UNREACHABLE();
+ }
+}
+
+void CryptoContext::Start() {
+ ngtcp2_conn_set_tls_native_handle(session_->connection(), ssl_.get());
+ SetTransportParams(session_, ssl_);
+ DEBUG(session_, "Crypto context started");
+}
+
+Store CryptoContext::ocsp_response(bool release) {
+ return LIKELY(release) ? std::move(ocsp_response_) : ocsp_response_;
+}
+
+ngtcp2_crypto_level CryptoContext::read_crypto_level() const {
+ return from_ossl_level(SSL_quic_read_level(ssl_.get()));
+}
+
+ngtcp2_crypto_level CryptoContext::write_crypto_level() const {
+ return from_ossl_level(SSL_quic_write_level(ssl_.get()));
+}
+
+void CryptoContext::Keylog(const char* line) const {
+ session_->EmitKeylog(line);
+}
+
+int CryptoContext::OnClientHello() {
+ DEBUG(session_, "Crypto context OnClientHello");
+
+ if (LIKELY(!session_->options_.crypto_options.client_hello) ||
+ session_->state_->client_hello_done == 1)
+ return 0;
+
+ CallbackScope cb_scope(this);
+
+ if (in_client_hello_) return -1;
+ in_client_hello_ = true;
+
+ // Returning 1 signals an error condition.
+ if (!session_->EmitClientHello()) return 1;
+
+ // Returning -1 here will keep the TLS handshake paused until the client hello
+ // callback is invoked. Returning 0 means that the handshake is ready to
+ // proceed. When the OnClientHello callback is called, it may be resolved
+ // synchronously or asynchronously. In case it is resolved synchronously, we
+ // need the check below.
+ return in_client_hello_ ? -1 : 0;
+}
+
+void CryptoContext::OnClientHelloDone() {
+ DEBUG(session_, "Crypto context OnClientHelloDone");
+ ResumeHandshakeScope handshake_scope(this,
+ [this] { in_client_hello_ = false; });
+ session_->state_->client_hello_done = 1;
+}
+
+int CryptoContext::OnOCSP() {
+ DEBUG(session_, "Crypto context OnOCSP");
+ if (LIKELY(!session_->options_.crypto_options.ocsp) ||
+ session_->state_->ocsp_done == 1)
+ return 1;
+
+ if (!session_->is_server()) return 1;
+
+ CallbackScope cb_scope(this);
+
+ if (in_ocsp_request_) return -1;
+ in_ocsp_request_ = true;
+
+ // Returning 1 signals an error condition.
+ if (!session_->EmitOCSP()) return 1;
+
+ // Returning -1 here means that we are still waiting for the OCSP
+ // request to be completed. When the OnCert handler is invoked
+ // above, it can be resolve synchronously or asynchonously. If
+ // resolved synchronously, we need the check below.
+ return in_ocsp_request_ ? -1 : 1;
+}
+
+void CryptoContext::OnOCSPDone(Store&& ocsp_response) {
+ DEBUG(session_, "Crypto context OnOCSPDone");
+ ResumeHandshakeScope handshake_scope(this,
+ [this] { in_ocsp_request_ = false; });
+ session_->state_->ocsp_done = 1;
+ ocsp_response_ = std::move(ocsp_response);
+}
+
+int CryptoContext::Receive(ngtcp2_crypto_level crypto_level,
+ uint64_t offset,
+ const uint8_t* data,
+ size_t datalen) {
+ // ngtcp2 provides an implementation of this in
+ // ngtcp2_crypto_recv_crypto_data_cb but given that we are using the
+ // implementation specific error codes below, we can't use it.
+
+ DEBUG(session_, "Receiving crypto data");
+
+ if (UNLIKELY(session_->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
+
+ // Internally, this passes the handshake data off to openssl for processing.
+ // The handshake may or may not complete.
+ int ret = ngtcp2_crypto_read_write_crypto_data(
+ session_->connection(), crypto_level, data, datalen);
+
+ switch (ret) {
+ case 0:
+ // Fall-through
+
+ // In either of following cases, the handshake is being paused waiting for
+ // user code to take action (for instance OCSP requests or client hello
+ // modification)
+ case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_X509_LOOKUP:
+ // Fall-through
+ case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_CLIENT_HELLO_CB:
+ return 0;
+ }
+ return ret;
+}
+
+int CryptoContext::OnTLSStatus() {
+ if (LIKELY(!session_->options_.crypto_options.ocsp)) return 1;
+
+ switch (side_) {
+ case NGTCP2_CRYPTO_SIDE_SERVER: {
+ DEBUG(session_, "Crypto context OnTLSStatus (server)");
+ if (!ocsp_response_) return SSL_TLSEXT_ERR_NOACK;
+
+ DEBUG(session_, "Handling ocsp response (server)");
+ uv_buf_t buf = ocsp_response_;
+
+ unsigned char* data = crypto::MallocOpenSSL(buf.len);
+ memcpy(data, buf.base, buf.len);
+
+ if (!SSL_set_tlsext_status_ocsp_resp(ssl_.get(), data, buf.len))
+ OPENSSL_free(data);
+
+ USE(std::move(ocsp_response_));
+ return SSL_TLSEXT_ERR_OK;
+ }
+ case NGTCP2_CRYPTO_SIDE_CLIENT: {
+ DEBUG(session_, "Crypto context OnTLSStatus (server)");
+ session_->EmitOCSPResponse();
+ return 1;
+ }
+ }
+ UNREACHABLE();
+}
+
+int CryptoContext::OnNewSession(SSL_SESSION* session) {
+ // If there is nothing listening for the session ticket, don't both emitting
+ // it.
+ if (LIKELY(session_->state_->session_ticket == 0)) return 0;
+
+ size_t size = i2d_SSL_SESSION(session, nullptr);
+ if (size > crypto::SecureContext::kMaxSessionSize) return 0;
+
+ std::shared_ptr ticket;
+
+ auto isolate = session_->env()->isolate();
+
+ if (size > 0) {
+ ticket = ArrayBuffer::NewBackingStore(isolate, size);
+ unsigned char* data = reinterpret_cast(ticket->Data());
+ if (i2d_SSL_SESSION(session, &data) <= 0) return 0;
+ } else {
+ ticket = ArrayBuffer::NewBackingStore(isolate, 0);
+ }
+
+ session_->EmitSessionTicket(Store(std::move(ticket), size));
+
+ return 0;
+}
+
+void CryptoContext::ResumeHandshake() {
+ DEBUG(session_, "Crypto context resuming handshake");
+ Session::SendPendingDataScope scope(session_);
+ Receive(read_crypto_level(), 0, nullptr, 0);
+}
+
+bool CryptoContext::InitiateKeyUpdate() {
+ if (UNLIKELY(session_->is_destroyed()) || in_key_update_) return false;
+
+ DEBUG(session_, "Crypto context initiating key update");
+
+ // There's no user code that should be able to run while UpdateKey
+ // is running, but we need to gate on it just to be safe.
+ auto leave = OnScopeLeave([this]() { in_key_update_ = false; });
+ in_key_update_ = true;
+
+ session_->IncrementStat(&SessionStats::keyupdate_count);
+
+ return ngtcp2_conn_initiate_key_update(session_->connection(), uv_hrtime()) ==
+ 0;
+}
+
+int CryptoContext::VerifyPeerIdentity() {
+ return crypto::VerifyPeerCertificate(ssl_);
+}
+
+void CryptoContext::EnableTrace() {
+#if HAVE_SSL_TRACE
+ DEBUG(session_, "Enabling TLS trace");
+ if (!bio_trace_) {
+ bio_trace_.reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT));
+ SSL_set_msg_callback(
+ ssl_.get(),
+ [](int write_p,
+ int version,
+ int content_type,
+ const void* buf,
+ size_t len,
+ SSL* ssl,
+ void* arg) -> void {
+ crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
+ SSL_trace(write_p, version, content_type, buf, len, ssl, arg);
+ });
+ SSL_set_msg_callback_arg(ssl_.get(), bio_trace_.get());
+ }
+#endif
+}
+
+void CryptoContext::MaybeSetEarlySession(
+ const BaseObjectPtr& sessionTicket) {
+ // Nothing to do if there is no ticket
+ if (!sessionTicket) return;
+
+ DEBUG(session_, "Crypto context setting early session ticket");
+
+ Session::TransportParams rtp(
+ Session::TransportParams::Type::ENCRYPTED_EXTENSIONS,
+ sessionTicket->transport_params());
+
+ // Ignore invalid remote transport parameters.
+ if (!rtp) return;
+
+ uv_buf_t buf = sessionTicket->ticket();
+ crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
+ reinterpret_cast(buf.base), buf.len);
+
+ // Silently ignore invalid TLS session
+ if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return;
+
+ // We don't care about the return value here. The early data will just be
+ // ignored if it's invalid.
+ USE(crypto::SetTLSSession(ssl_, ticket));
+
+ ngtcp2_conn_set_early_remote_transport_params(session_->connection(), rtp);
+
+ DEBUG(session_, "Crypto context early session enabled");
+ session_->state_->stream_open_allowed = 1;
+}
+
+void CryptoContext::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("secure_context", secure_context_);
+ tracker->TrackFieldWithSize("ocsp_response", ocsp_response_);
+}
+
+v8::MaybeLocal CryptoContext::cert(Environment* env) const {
+ return crypto::X509Certificate::GetCert(env, ssl_);
+}
+
+v8::MaybeLocal CryptoContext::peer_cert(Environment* env) const {
+ crypto::X509Certificate::GetPeerCertificateFlag flag =
+ session_->is_server()
+ ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
+ : crypto::X509Certificate::GetPeerCertificateFlag::NONE;
+ return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
+}
+
+v8::MaybeLocal CryptoContext::cipher_name(Environment* env) const {
+ return crypto::GetCurrentCipherName(env, ssl_);
+}
+
+v8::MaybeLocal CryptoContext::cipher_version(
+ Environment* env) const {
+ return crypto::GetCurrentCipherVersion(env, ssl_);
+}
+
+v8::MaybeLocal CryptoContext::ephemeral_key(
+ Environment* env) const {
+ return crypto::GetEphemeralKey(env, ssl_);
+}
+
+v8::MaybeLocal CryptoContext::hello_ciphers(Environment* env) const {
+ return crypto::GetClientHelloCiphers(env, ssl_);
+}
+
+v8::MaybeLocal CryptoContext::hello_servername(
+ Environment* env) const {
+ const char* servername = crypto::GetClientHelloServerName(ssl_);
+ if (servername == nullptr) return Undefined(env->isolate());
+ return OneByteString(env->isolate(), crypto::GetClientHelloServerName(ssl_));
+}
+
+v8::MaybeLocal CryptoContext::hello_alpn(Environment* env) const {
+ const char* alpn = crypto::GetClientHelloALPN(ssl_);
+ if (alpn == nullptr) return Undefined(env->isolate());
+ return OneByteString(env->isolate(), alpn);
+}
+
+std::string CryptoContext::servername() const {
+ const char* servername = crypto::GetServerName(ssl_.get());
+ return servername != nullptr ? std::string(servername) : std::string();
+}
+
+std::string CryptoContext::selected_alpn() const {
+ const unsigned char* alpn_buf = nullptr;
+ unsigned int alpnlen;
+ SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
+ return alpnlen ? std::string(reinterpret_cast(alpn_buf), alpnlen)
+ : std::string();
+}
+
+bool CryptoContext::was_early_data_accepted() const {
+ return (early_data_ &&
+ SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED);
+}
+
+void CryptoContext::Options::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("keys", keys);
+ tracker->TrackField("certs", certs);
+ tracker->TrackField("ca", ca);
+ tracker->TrackField("crl", crl);
+}
+
+ngtcp2_conn* CryptoContext::getConnection(ngtcp2_crypto_conn_ref* ref) {
+ CryptoContext* context = ContainerOf(&CryptoContext::conn_ref_, ref);
+ return context->session_->connection();
+}
+
+// ======================================================================================
+// SessionTicket
+
+Local SessionTicket::GetConstructorTemplate(
+ Environment* env) {
+ auto& state = BindingState::Get(env);
+ auto tmpl = state.sessionticket_constructor_template();
+ if (tmpl.IsEmpty()) {
+ tmpl = NewFunctionTemplate(env->isolate(), New);
+ tmpl->Inherit(BaseObject::GetConstructorTemplate(env));
+ tmpl->InstanceTemplate()->SetInternalFieldCount(
+ SessionTicket::kInternalFieldCount);
+ tmpl->SetClassName(state.sessionticket_string());
+ SetProtoMethod(env->isolate(), tmpl, "encoded", GetEncoded);
+ state.set_sessionticket_constructor_template(tmpl);
+ }
+ return tmpl;
+}
+
+void SessionTicket::Initialize(Environment* env, Local