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 target) { + SetConstructorFunction(env->context(), + target, + "SessionTicket", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +BaseObjectPtr SessionTicket::Create(Environment* env, + Store&& ticket, + Store&& transport_params) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env, + obj, + std::forward(ticket), + std::forward(transport_params)); +} + +SessionTicket::SessionTicket(Environment* env, + Local object, + Store&& ticket, + Store&& transport_params) + : BaseObject(env, object), + ticket_(std::move(ticket)), + transport_params_(std::move(transport_params)) { + MakeWeak(); +} + +void SessionTicket::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("ticket", ticket_); + tracker->TrackField("transport_params", transport_params_); +} + +void SessionTicket::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(GetEncoded); +} + +MaybeLocal SessionTicket::Encoded() { + auto context = env()->context(); + v8::ValueSerializer ser(env()->isolate()); + ser.WriteHeader(); + + if (ser.WriteValue(context, ticket_.ToArrayBufferView(env())) + .IsNothing() || + ser.WriteValue(context, + transport_params_.ToArrayBufferView(env())) + .IsNothing()) { + return MaybeLocal(); + } + + auto result = ser.Release(); + return node::Buffer::Copy( + env(), reinterpret_cast(result.first), result.second); +} + +void SessionTicket::New(const v8::FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + if (!args[0]->IsArrayBufferView()) { + THROW_ERR_INVALID_ARG_TYPE(env, "The ticket must be an ArrayBufferView."); + return; + } + + auto context = env->context(); + crypto::ArrayBufferOrViewContents view(args[0]); + v8::ValueDeserializer des(env->isolate(), view.data(), view.size()); + + if (des.ReadHeader(context).IsNothing()) { + THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid."); + return; + } + + Local ticket; + Local transport_params; + + if (!des.ReadValue(context).ToLocal(&ticket) || + !ticket->IsArrayBufferView()) { + THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid."); + return; + } + + if (!des.ReadValue(context).ToLocal(&transport_params) || + !transport_params->IsArrayBufferView()) { + THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid."); + return; + } + + new SessionTicket(env, + args.This(), + Store(ticket.As()), + Store(transport_params.As())); +} + +void SessionTicket::GetEncoded( + const v8::FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + SessionTicket* sessionTicket; + CHECK(SessionTicket::HasInstance(env, args.Holder())); + ASSIGN_OR_RETURN_UNWRAP(&sessionTicket, args.Holder()); + Local encoded; + if (sessionTicket->Encoded().ToLocal(&encoded)) + args.GetReturnValue().Set(encoded); +} + +// ====================================================================================== +// StatelessResetToken + +StatelessResetToken::StatelessResetToken(const uint8_t* token) { + memcpy(buf_, token, sizeof(buf_)); +} + +StatelessResetToken::StatelessResetToken(const uint8_t* secret, + const CID& cid) { + GenerateResetToken(secret, cid); +} + +StatelessResetToken::StatelessResetToken(uint8_t* token, + const uint8_t* secret, + const CID& cid) + : StatelessResetToken(secret, cid) { + memcpy(buf_, token, sizeof(buf_)); +} + +std::string StatelessResetToken::ToString() const { + std::vector dest(NGTCP2_STATELESS_RESET_TOKENLEN * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = StringBytes::hex_encode(reinterpret_cast(buf_), + NGTCP2_STATELESS_RESET_TOKENLEN, + dest.data(), + dest.size()); + return std::string(dest.data(), written); +} + +size_t StatelessResetToken::Hash::operator()( + const StatelessResetToken& token) const { + size_t hash = 0; + for (size_t n = 0; n < NGTCP2_STATELESS_RESET_TOKENLEN; n++) + hash ^= std::hash{}(token.buf_[n]) + 0x9e3779b9 + (hash << 6) + + (hash >> 2); + return hash; +} + +} // namespace quic +} // namespace node diff --git a/src/quic/crypto.h b/src/quic/crypto.h new file mode 100644 index 00000000000000..99838ec01c7ab9 --- /dev/null +++ b/src/quic/crypto.h @@ -0,0 +1,389 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include +#include +#include +#include +#include "quic.h" + +namespace node { +namespace quic { + +// ============================================================================= +// SessionTicket + +class SessionTicket final : public BaseObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + static BaseObjectPtr Create(Environment* env, + Store&& ticket, + Store&& transport_params); + + SessionTicket(Environment* env, + v8::Local object, + Store&& ticket, + Store&& transport_params); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SessionTicket) + SET_SELF_SIZE(SessionTicket) + + inline uv_buf_t ticket() const { return ticket_; } + inline ngtcp2_vec transport_params() const { return transport_params_; } + + private: + v8::MaybeLocal Encoded(); + + static void New(const v8::FunctionCallbackInfo& args); + static void GetEncoded(const v8::FunctionCallbackInfo& args); + + Store ticket_; + Store transport_params_; +}; + +// ============================================================================= +class StatelessResetToken final : public MemoryRetainer { + public: + StatelessResetToken() = default; + StatelessResetToken(uint8_t* token, const uint8_t* secret, const CID& cid); + + // Generates a new token derived from the secret and CID + StatelessResetToken(const uint8_t* secret, const CID& cid); + + // Copies the given token + explicit StatelessResetToken(const uint8_t* token); + + std::string ToString() const; + + inline const uint8_t* data() const { return buf_; } + + struct Hash { + size_t operator()(const StatelessResetToken& token) const; + }; + + inline bool operator==(const StatelessResetToken& other) const { + return memcmp(data(), other.data(), NGTCP2_STATELESS_RESET_TOKENLEN) == 0; + } + + inline bool operator!=(const StatelessResetToken& other) const { + return !(*this == other); + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(StatelessResetToken) + SET_SELF_SIZE(StatelessResetToken) + + template + using Map = + std::unordered_map; + + private: + // Generates a stateless reset token using HKDF with the cid and token secret + // as input. The token secret is either provided by user code when an Endpoint + // is created or is generated randomly. + // + // QUIC leaves the generation of stateless session tokens up to the + // implementation to figure out. The idea, however, is that it ought to be + // possible to generate a stateless reset token reliably even when all state + // for a connection has been lost. We use the cid as it's the only reliably + // consistent bit of data we have when a session is destroyed. + bool GenerateResetToken(const uint8_t* secret, const CID& cid); + + uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN]; +}; + +// ============================================================================= + +// The Retry Token is an encrypted token that is sent to the client by the +// server as part of the path validation flow. The plaintext format within the +// token is opaque and only meaningful the server. We can structure it any way +// we want. It needs to: +// * be hard to guess +// * be time limited +// * be specific to the client address +// * be specific to the original cid +// * contain random data. +bool GenerateRetryPacket(const BaseObjectPtr& packet, + quic_version version, + const uint8_t* token_secret, + const CID& dcid, + const CID& scid, + const SocketAddress& remote_addr); + +enum class GetCertificateType { + SELF, + ISSUER, +}; + +v8::MaybeLocal GetCertificateData( + Environment* env, + crypto::SecureContext* sc, + GetCertificateType type = GetCertificateType::SELF); + +// Validates a retry token. Returns Nothing() if the token is *not valid*, +// returns the OCID otherwise. +v8::Maybe ValidateRetryToken(quic_version version, + const ngtcp2_vec& token, + const SocketAddress& addr, + const CID& dcid, + const uint8_t* token_secret, + uint64_t verification_expiration); + +v8::Maybe GenerateToken(quic_version version, + uint8_t* token, + const SocketAddress& addr, + const uint8_t* token_secret); + +bool ValidateToken(quic_version version, + const ngtcp2_vec& token, + const SocketAddress& addr, + const uint8_t* token_secret, + uint64_t verification_expiration); + +// Get the ALPN protocol identifier that was negotiated for the session +v8::Local GetALPNProtocol(Session* session); + +ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level); +const char* crypto_level_name(ngtcp2_crypto_level level); + +// ====================================================================================== +// SessionTicketAppData is a utility class that is used only during the +// generation or access of TLS stateless sesson tickets. It exists solely to +// provide a easier way for Session::Application instances to set relevant +// metadata in the session ticket when it is created, and the exract and +// subsequently verify that data when a ticket is received and is being +// validated. The app data is completely opaque to anything other than the +// server-side of the Session::Application that sets it. +class SessionTicketAppData final { + public: + enum class Status { + TICKET_USE, + TICKET_USE_RENEW, + TICKET_IGNORE, + TICKET_IGNORE_RENEW + }; + + enum class Flag { STATUS_NONE, STATUS_RENEW }; + + explicit SessionTicketAppData(SSL_SESSION* session); + QUIC_NO_COPY_OR_MOVE(SessionTicketAppData) + + bool Set(const uint8_t* data, size_t len); + bool Get(uint8_t** data, size_t* len) const; + + private: + bool set_ = false; + SSL_SESSION* session_; +}; + +// ====================================================================================== +// Every Session has exactly one CryptoContext that maintains the state of the +// TLS handshake and negotiated cipher keys after the handshake has been +// completed. It is separated out from the main Session class only as a +// convenience to help make the code more maintainable and understandable. +class CryptoContext final : public MemoryRetainer { + public: + struct Options final : public MemoryRetainer { + // When true, TLS keylog data will be emitted to the JavaScript session + // object. + bool keylog = false; + + // When set, the peer certificate is verified against the list of supplied + // CAs. If verification fails, the connection will be refused. + bool reject_unauthorized = true; + + // When set, the clienthello event will be emitted on the Session to allow + // user code an opportunity to provide a different SecureContext based on + // alpn, SNI servername, and ciphers. + bool client_hello = false; + + // When set, enables TLS tracing for the session. This should only be used + // for debugging. + bool enable_tls_trace = false; + + // Options only used by server sessions: + + // When set, instructs the server session to request a client authentication + // certificate. + bool request_peer_certificate = false; + + // Options pnly used by client sessions: + + // When set, instructs the client session to include an OCSP request in the + // initial TLS handshake. For server sessions, instructs the session not to + // ignore ocsp requests. + bool ocsp = false; + + // When set, instructs the client session to verify the hostname default. + // This is required by QUIC and enabled by default. We allow disabling it + // only for debugging. + bool verify_hostname_identity = true; + + // The TLS session ID context (only used on the server) + std::string session_id_ctx = "Node.js QUIC Server"; + + // TLS cipher suite + std::string ciphers = DEFAULT_CIPHERS; + + // TLS groups + std::string groups = DEFAULT_GROUPS; + + // The TLS private key to use for this session. + std::vector> keys; + + // Collection of certificates to use for this session. + std::vector certs; + + // Optional certificate authority overrides to use. + std::vector ca; + + // Optional certificate revocation lists to use. + std::vector crl; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoContext::Options) + SET_SELF_SIZE(Options); + }; + + CryptoContext(Session* session, + const Options& options, + ngtcp2_crypto_side side); + + QUIC_NO_COPY_OR_MOVE(CryptoContext); + + // Returns the server's prepared OCSP response for transmission (if any). The + // store will be empty if there was an error or if no OCSP response was + // provided. If release is true, the internal std::shared_ptr will be reset + // (which is the default). + Store ocsp_response(bool release = true); + + // Returns ngtcp2's understanding of the current inbound crypto level + ngtcp2_crypto_level read_crypto_level() const; + + // Returns ngtcp2's understanding of the current outbound crypto level + ngtcp2_crypto_level write_crypto_level() const; + + void Start(); + + // TLS Keylogging is enabled per-Session by attaching an handler to the + // "keylog" event. Each keylog line is emitted to JavaScript where it can be + // routed to whatever destination makes sense. Typically, this will be to a + // keylog file that can be consumed by tools like Wireshark to intercept and + // decrypt QUIC network traffic. + void Keylog(const char* line) const; + + // Causes the session to emit the "client hello" callback. The TLS handshake + // will be paused pending user code triggering the OnClientHelloDone. + int OnClientHello(); + + // TODO(@jasnell): For now, this doesn't accept any parameters. In the future, + // we'll want to use this to allow additional keys, certs, etc to be added. + void OnClientHelloDone(); + + // The OnCert callback provides an opportunity to prompt the server to perform + // on OCSP request on behalf of the client (when the client requests it). If + // there is a listener for the 'OCSPRequest' event on the JavaScript side, the + // IDX_QUIC_SESSION_STATE_CERT_ENABLED session state slot will equal 1, which + // will cause the callback to be invoked. The callback will be given a + // reference to a JavaScript function that must be called in order for the TLS + // handshake to continue. + int OnOCSP(); + void OnOCSPDone(Store&& ocsp_response); + + // When the client has requested OSCP, this function will be called to provide + // the OSCP response. The OnOSCP() callback should have already been called by + // this point if any data is to be provided. If it hasn't, and ocsp_response_ + // is empty, no OCSP response will be sent. + int OnTLSStatus(); + + // Called when a chunk of peer TLS handshake data is received. For every + // chunk, we move the TLS handshake further along until it is complete. + int Receive(ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen); + + v8::MaybeLocal cert(Environment* env) const; + v8::MaybeLocal peer_cert(Environment* env) const; + v8::MaybeLocal cipher_name(Environment* env) const; + v8::MaybeLocal cipher_version(Environment* env) const; + v8::MaybeLocal ephemeral_key(Environment* env) const; + v8::MaybeLocal hello_ciphers(Environment* env) const; + v8::MaybeLocal hello_servername(Environment* env) const; + v8::MaybeLocal hello_alpn(Environment* env) const; + std::string servername() const; + std::string selected_alpn() const; + + // Triggers key update to begin. This will fail and return false if either a + // previous key update is in progress and has not been confirmed or if the + // initial handshake has not yet been confirmed. + bool InitiateKeyUpdate(); + + int VerifyPeerIdentity(); + void EnableTrace(); + + inline const Session& session() const { return *session_; } + inline ngtcp2_crypto_side side() const { return side_; } + + bool was_early_data_accepted() const; + + inline bool verify_hostname_identity() const { + return options_.verify_hostname_identity; + } + + int OnNewSession(SSL_SESSION* session); + + void MaybeSetEarlySession(const BaseObjectPtr& sessionTicket); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoContext) + SET_SELF_SIZE(CryptoContext) + + private: + static ngtcp2_conn* getConnection(ngtcp2_crypto_conn_ref* ref); + ngtcp2_crypto_conn_ref conn_ref_; + + void ResumeHandshake(); + + Session* session_; + Options options_; + BaseObjectPtr secure_context_; + ngtcp2_crypto_side side_; + crypto::SSLPointer ssl_; + crypto::BIOPointer bio_trace_; + + bool in_tls_callback_ = false; + bool in_ocsp_request_ = false; + bool in_client_hello_ = false; + bool in_key_update_ = false; + bool early_data_ = false; + + Store ocsp_response_; + + struct CallbackScope; + struct ResumeHandshakeScope; + + static BaseObjectPtr InitializeSecureContext( + const Session& session, + const CryptoContext::Options& options, + ngtcp2_crypto_side side); + + friend class Session; + friend struct CallbackScope; + friend struct ResumeHandshakeScope; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/defs.h b/src/quic/defs.h new file mode 100644 index 00000000000000..04a05aedbd44c5 --- /dev/null +++ b/src/quic/defs.h @@ -0,0 +1,511 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include + +namespace node { +namespace quic { + +// many ngtcp2 functions return 0 to indicate success and non-zero to indicate +// failure. Most of the time, for such functions we don't care about the +// specific return value so we simplify using a macro. +#define NGTCP2_ERR(V) (V != 0) +#define NGTCP2_OK(V) (V == 0) +#define NGTCP2_SUCCESS 0 + +#define NGTCP2_VERSION_NEGOTIATION_ERROR NGTCP2_VERSION_NEGOTIATION_ERROR_DRAFT +#define HTTP3_ERR_NO_ERROR NGHTTP3_H3_NO_ERROR + +#define QUIC_CC_ALGO_CUBIC NGTCP2_CC_ALGO_CUBIC +#define QUIC_CC_ALGO_RENO NGTCP2_CC_ALGO_RENO +#define QUIC_CC_ALGO_BBR NGTCP2_CC_ALGO_BBR +#define QUIC_CC_ALGO_BBR2 NGTCP2_CC_ALGO_BBR2 +#define QUIC_MAX_CIDLEN NGTCP2_MAX_CIDLEN + +#define QUIC_HEADERS_KIND_INFO Session::Application::HeadersKind::INFO +#define QUIC_HEADERS_KIND_INITIAL Session::Application::HeadersKind::INITIAL +#define QUIC_HEADERS_KIND_TRAILING Session::Application::HeadersKind::TRAILING +#define QUIC_HEADERS_FLAGS_NONE Session::Application::HeadersFlags::NONE +#define QUIC_HEADERS_FLAGS_TERMINAL Session::Application::HeadersFlags::TERMINAL + +#define QUIC_STREAM_PRIORITY_DEFAULT \ + Session::Application::StreamPriority::DEFAULT +#define QUIC_STREAM_PRIORITY_LOW Session::Application::StreamPriority::LOW +#define QUIC_STREAM_PRIORITY_HIGH Session::Application::StreamPriority::HIGH +#define QUIC_STREAM_PRIORITY_FLAGS_NONE \ + Session::Application::StreamPriorityFlags::NONE +#define QUIC_STREAM_PRIORITY_FLAGS_NON_INCREMENTAL \ + Session::Application::StreamPriorityFlags::NON_INCREMENTAL + +#define QUIC_ERROR_TYPE_TRANSPORT QuicError::Type::TRANSPORT +#define QUIC_ERROR_TYPE_APPLICATION QuicError::Type::APPLICATION +#define QUIC_ERROR_TYPE_VERSION_NEGOTIATION QuicError::Type::VERSION_NEGOTIATION +#define QUIC_ERROR_TYPE_IDLE_CLOSE QuicError::Type::IDLE_CLOSE + +#define QUIC_ENDPOINT_CLOSE_CONTEXT_CLOSE Endpoint::CloseContext::CLOSE +#define QUIC_ENDPOINT_CLOSE_CONTEXT_BIND_FAILURE \ + Endpoint::CloseContext::BIND_FAILURE +#define QUIC_ENDPOINT_CLOSE_CONTEXT_START_FAILURE \ + Endpoint::CloseContext::START_FAILURE +#define QUIC_ENDPOINT_CLOSE_CONTEXT_RECEIVE_FAILURE \ + Endpoint::CloseContext::RECEIVE_FAILURE +#define QUIC_ENDPOINT_CLOSE_CONTEXT_SEND_FAILURE \ + Endpoint::CloseContext::SEND_FAILURE +#define QUIC_ENDPOINT_CLOSE_CONTEXT_LISTEN_FAILURE \ + Endpoint::CloseContext::LISTEN_FAILURE + +#define QUIC_TRANSPORT_ERRORS(V) \ + V(NO_ERROR) \ + V(INTERNAL_ERROR) \ + V(CONNECTION_REFUSED) \ + V(FLOW_CONTROL_ERROR) \ + V(STREAM_LIMIT_ERROR) \ + V(STREAM_STATE_ERROR) \ + V(FINAL_SIZE_ERROR) \ + V(FRAME_ENCODING_ERROR) \ + V(TRANSPORT_PARAMETER_ERROR) \ + V(CONNECTION_ID_LIMIT_ERROR) \ + V(PROTOCOL_VIOLATION) \ + V(INVALID_TOKEN) \ + V(APPLICATION_ERROR) \ + V(CRYPTO_BUFFER_EXCEEDED) \ + V(KEY_UPDATE_ERROR) \ + V(AEAD_LIMIT_REACHED) \ + V(NO_VIABLE_PATH) \ + V(CRYPTO_ERROR) \ + V(VERSION_NEGOTIATION_ERROR_DRAFT) + +#define HTTP3_APPLICATION_ERRORS(V) \ + V(H3_GENERAL_PROTOCOL_ERROR) \ + V(H3_INTERNAL_ERROR) \ + V(H3_STREAM_CREATION_ERROR) \ + V(H3_CLOSED_CRITICAL_STREAM) \ + V(H3_FRAME_UNEXPECTED) \ + V(H3_FRAME_ERROR) \ + V(H3_EXCESSIVE_LOAD) \ + V(H3_ID_ERROR) \ + V(H3_SETTINGS_ERROR) \ + V(H3_MISSING_SETTINGS) \ + V(H3_REQUEST_REJECTED) \ + V(H3_REQUEST_CANCELLED) \ + V(H3_REQUEST_INCOMPLETE) \ + V(H3_MESSAGE_ERROR) \ + V(H3_CONNECT_ERROR) \ + V(H3_VERSION_FALLBACK) \ + V(QPACK_DECOMPRESSION_FAILED) \ + V(QPACK_ENCODER_STREAM_ERROR) \ + V(QPACK_DECODER_STREAM_ERROR) + +#define QUIC_FLAG(Name) \ + ((flags & Name) == Name) /* NOLINT(runtime/references) */ + +#define QUIC_NO_COPY_OR_MOVE(Name) \ + Name(Name&) = delete; \ + Name(const Name&) = delete; \ + Name(const Name&&) = delete; \ + Name& operator=(Name&) = delete; \ + Name& operator=(const Name&) = delete; \ + Name& operator=(Name&&) = delete; + +#define QUIC_MOVE_NO_COPY(Name) \ + Name(Name&&) = default; \ + Name(Name&) = delete; \ + Name(const Name&) = delete; \ + Name& operator=(Name&&) = default; \ + Name& operator=(Name&) = delete; \ + Name& operator=(const Name&) = delete; + +#define QUIC_COPY_NO_MOVE(Name) \ + Name(Name&&) = delete; \ + Name(Name&) = default; \ + Name(const Name&) = default; \ + Name& operator=(Name&&) = delete; \ + Name& operator=(Name&) = default; \ + Name& operator=(const Name&) = default; + +// The constructors are v8::FunctionTemplates that are stored persistently in +// the quic::BindingState class. These are used for creating instances of the +// various objects, as well as for performing HasInstance type checks. We choose +// to store these on the BindingData instead of the Environment in order to keep +// like-things together and to reduce the additional memory overhead on the +// Environment when QUIC is not being used. +#define QUIC_CONSTRUCTORS(V) \ + V(arraybufferviewsource) \ + V(blobsource) \ + V(endpoint) \ + V(endpoint_config) \ + V(http3_options) \ + V(jsquicbufferconsumer) \ + V(logstream) \ + V(nullsource) \ + V(random_connection_id_strategy) \ + V(send_wrap) \ + V(session) \ + V(session_options) \ + V(stream) \ + V(streamsource) \ + V(streambasesource) \ + V(udp) \ + V(cidfactorybase) \ + V(sessionticket) + +// The callbacks are persistent v8::Function references that are set in the +// quic::BindingState used to communicate data and events back out to the JS +// environment. They are set once from the JavaScript side when the +// internalBinding('quic') is first loaded. +// +// The corresponding implementations of the callbacks can be found in +// lib/internal/quic/binding.js +#define QUIC_JS_CALLBACKS(V) \ + V(endpoint_done, EndpointDone) \ + V(endpoint_error, EndpointError) \ + V(session_new, SessionNew) \ + V(session_client_hello, SessionClientHello) \ + V(session_close, SessionClose) \ + V(session_error, SessionError) \ + V(session_datagram, SessionDatagram) \ + V(session_datagram_ack, SessionDatagramAcknowledged) \ + V(session_datagram_lost, SessionDatagramLost) \ + V(session_handshake, SessionHandshake) \ + V(session_ocsp_request, SessionOcspRequest) \ + V(session_ocsp_response, SessionOcspResponse) \ + V(session_ticket, SessionTicket) \ + V(session_version_negotiation, SessionVersionNegotiation) \ + V(session_path_validation, SessionPathValidation) \ + V(stream_close, StreamClose) \ + V(stream_error, StreamError) \ + V(stream_data, StreamData) \ + V(stream_created, StreamCreated) \ + V(stream_reset, StreamReset) \ + V(stream_headers, StreamHeaders) \ + V(stream_blocked, StreamBlocked) \ + V(stream_trailers, StreamTrailers) + +// The strings are persistent/eternal v8::Strings that are set in the +// quic::BindingState. +#define QUIC_STRINGS(V) \ + V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(aborted, "aborted") \ + V(ack_delay_exponent, "ackDelayExponent") \ + V(active_connection_id_limit, "activeConnectionIdLimit") \ + V(address_lru_size, "addressLRUSize") \ + V(arraybufferviewsource, "ArrayBufferViewSource") \ + V(blobsource, "BlobSource") \ + V(ca, "ca") \ + V(cc_algorithm, "ccAlgorithm") \ + V(certs, "certs") \ + V(cidfactorybase, "CIDFactoryBase") \ + V(ciphers, "ciphers") \ + V(client_hello, "clientHello") \ + V(crl, "crl") \ + V(disable_active_migration, "disableActiveMigration") \ + V(disable_stateless_reset, "disableStatelessReset") \ + V(enable_tls_trace, "enableTLSTrace") \ + V(endpoint, "Endpoint") \ + V(endpoint_udp, "Endpoint::UDP") \ + V(endpoint_options, "Endpoint::Options") \ + V(err_stream_closed, "ERR_QUIC_STREAM_CLOSED") \ + V(failure, "failure") \ + V(groups, "groups") \ + V(http3_application_options, "Http3ApplicationOptions") \ + V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ + V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \ + V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \ + V(initial_max_data, "initialMaxData") \ + V(initial_max_streams_bidi, "initialMaxStreamsBidi") \ + V(initial_max_streams_uni, "initialMaxStreamsUni") \ + V(ipv6_only, "ipv6Only") \ + V(jsquicbufferconsumer, "JSQuicBufferConsumer") \ + V(keys, "keys") \ + V(keylog, "keylog") \ + V(logstream, "LogStream") \ + V(max_ack_delay, "maxAckDelay") \ + V(max_connections_per_host, "maxConnectionsPerHost") \ + V(max_connections_total, "maxConnectionsTotal") \ + V(max_datagram_frame_size, "maxDatagramFrameSize") \ + V(max_field_section_size, "maxFieldSectionSize") \ + V(max_header_pairs, "maxHeaderPairs") \ + V(max_header_length, "maxHeaderLength") \ + V(max_idle_timeout, "maxIdleTimeout") \ + V(max_payload_size, "maxPayloadSize") \ + V(max_stateless_resets, "maxStatelessResets") \ + V(max_stream_window_override, "maxStreamWindowOverride") \ + V(max_window_override, "maxWindowOverride") \ + V(nullsource, "NullSource") \ + V(ocsp, "ocsp") \ + V(packetwrap, "PacketWrap") \ + V(pskcallback, "pskCallback") \ + V(qlog, "qlog") \ + V(qpack_blocked_streams, "qpackBlockedStreams") \ + V(qpack_max_table_capacity, "qpackMaxTableCapacity") \ + V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxTableCapacity") \ + V(reject_unauthorized, "rejectUnauthorized") \ + V(request_peer_certificate, "requestPeerCertificate") \ + V(retry_limit, "retryLimit") \ + V(retry_token_expiration, "retryTokenExpiration") \ + V(rx_packet_loss, "rxPacketLoss") \ + V(session, "Session") \ + V(sessionticket, "SessionTicket") \ + V(session_id, "sessionID") \ + V(session_options, "Session::Options") \ + V(stream, "Stream") \ + V(streamsource, "StreamSource") \ + V(streambasesource, "StreamBaseSource") \ + V(success, "success") \ + V(token_expiration, "tokenExpiration") \ + V(tx_packet_loss, "txPacketLoss") \ + V(udp_receive_buffer_size, "receiveBufferSize") \ + V(udp_send_buffer_size, "sendBufferSize") \ + V(udp_ttl, "ttl") \ + V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ + V(verify_hostname_identity, "verifyHostnameIdentity") \ + V(validate_address, "validateAddress") + +#define SESSION_STATS(V) \ + V(CREATED_AT, created_at, "Created at") \ + V(HANDSHAKE_COMPLETED_AT, handshake_completed_at, "Handshake completed") \ + V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at, "Handshake confirmed") \ + V(GRACEFUL_CLOSING_AT, graceful_closing_at, "Graceful Closing") \ + V(CLOSING_AT, closing_at, "Closing") \ + V(DESTROYED_AT, destroyed_at, "Destroyed at") \ + V(BYTES_RECEIVED, bytes_received, "Bytes received") \ + V(BYTES_SENT, bytes_sent, "Bytes sent") \ + V(BIDI_STREAM_COUNT, bidi_stream_count, "Bidi stream count") \ + V(UNI_STREAM_COUNT, uni_stream_count, "Uni stream count") \ + V(STREAMS_IN_COUNT, streams_in_count, "Streams in count") \ + V(STREAMS_OUT_COUNT, streams_out_count, "Streams out count") \ + V(KEYUPDATE_COUNT, keyupdate_count, "Key update count") \ + V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count, "Loss retransmit count") \ + V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight, "Max bytes in flight") \ + V(BLOCK_COUNT, block_count, "Block count") \ + V(BYTES_IN_FLIGHT, bytes_in_flight, "Bytes in flight") \ + V(CONGESTION_RECOVERY_START_TS, \ + congestion_recovery_start_ts, \ + "Congestion recovery start time") \ + V(CWND, cwnd, "Size of the congestion window") \ + V(DELIVERY_RATE_SEC, delivery_rate_sec, "Delivery bytes/sec") \ + V(FIRST_RTT_SAMPLE_TS, first_rtt_sample_ts, "First RTT sample time") \ + V(INITIAL_RTT, initial_rtt, "Initial RTT") \ + V(LAST_TX_PKT_TS, last_tx_pkt_ts, "Last TX packet time") \ + V(LATEST_RTT, latest_rtt, "Latest RTT") \ + V(LOSS_DETECTION_TIMER, \ + loss_detection_timer, \ + "Loss detection timer deadline") \ + V(LOSS_TIME, loss_time, "Loss time") \ + V(MAX_UDP_PAYLOAD_SIZE, max_udp_payload_size, "Max UDP payload size") \ + V(MIN_RTT, min_rtt, "Minimum RTT so far") \ + V(PTO_COUNT, pto_count, "PTO count") \ + V(RTTVAR, rttvar, "Mean deviation of observed RTT") \ + V(SMOOTHED_RTT, smoothed_rtt, "Smoothed RTT") \ + V(SSTHRESH, ssthresh, "Slow start threshold") + +#define ENDPOINT_STATS(V) \ + V(CREATED_AT, created_at, "Created at") \ + V(DESTROYED_AT, destroyed_at, "Destroyed at") \ + V(BYTES_RECEIVED, bytes_received, "Bytes received") \ + V(BYTES_SENT, bytes_sent, "Bytes sent") \ + V(PACKETS_RECEIVED, packets_received, "Packets received") \ + V(PACKETS_SENT, packets_sent, "Packets sent") \ + V(SERVER_SESSIONS, server_sessions, "Server sessions") \ + V(CLIENT_SESSIONS, client_sessions, "Client sessions") \ + V(SERVER_BUSY_COUNT, server_busy_count, "Server busy count") + +#define STREAM_STATS(V) \ + V(CREATED_AT, created_at, "Created At") \ + V(RECEIVED_AT, received_at, "Last Received At") \ + V(ACKED_AT, acked_at, "Last Acknowledged At") \ + V(CLOSING_AT, closing_at, "Closing At") \ + V(DESTROYED_AT, destroyed_at, "Destroyed At") \ + V(BYTES_RECEIVED, bytes_received, "Bytes Received") \ + V(BYTES_SENT, bytes_sent, "Bytes Sent") \ + V(MAX_OFFSET, max_offset, "Max Offset") \ + V(MAX_OFFSET_ACK, max_offset_ack, "Max Acknowledged Offset") \ + V(MAX_OFFSET_RECV, max_offset_received, "Max Received Offset") \ + V(FINAL_SIZE, final_size, "Final Size") + +#define SESSION_STATE(V) \ + /* Set if the JavaScript wrapper has a path-validation event listener */ \ + V(PATH_VALIDATION, path_validation, uint8_t) \ + /* Set if the JavaScript wrapper has a version-negotiation event listener */ \ + V(VERSION_NEGOTIATION, version_negotiation, uint8_t) \ + /* Set if the JavaScript wrapper has a datagram event listener */ \ + V(DATAGRAM, datagram, uint8_t) \ + /* Set if the JavaScript wrapper has a session-ticket event listener */ \ + V(SESSION_TICKET, session_ticket, uint8_t) \ + /* Set if the JavaScript wrapper has a client-hello event listener */ \ + V(CLIENT_HELLO, client_hello, uint8_t) \ + /* Set when the client-hello event has been responded to */ \ + V(CLIENT_HELLO_DONE, client_hello_done, uint8_t) \ + /* Set when the JavaScript wrapper has a ocsp event listener */ \ + V(OCSP, ocsp, uint8_t) \ + /* Set when the ocsp event has been responded to */ \ + V(OCSP_DONE, ocsp_done, uint8_t) \ + V(CLOSING, closing, uint8_t) \ + V(CONNECTION_CLOSE_SCOPE, in_connection_close_scope, uint8_t) \ + V(DESTROYED, destroyed, uint8_t) \ + V(GRACEFUL_CLOSING, graceful_closing, uint8_t) \ + V(HANDSHAKE_COMPLETED, handshake_completed, uint8_t) \ + V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \ + V(NGTCP2_CALLBACK, in_ngtcp2_callback, uint8_t) \ + V(STATELESS_RESET, stateless_reset, uint8_t) \ + V(SILENT_CLOSE, silent_close, uint8_t) \ + V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \ + V(USING_PREFERRED_ADDRESS, using_preferred_address, uint8_t) \ + V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \ + V(WRAPPED, wrapped, uint8_t) + +#define ENDPOINT_STATE(V) \ + /* Bound to the UDP port */ \ + V(BOUND, bound, uint8_t) \ + /* Receiving packets on the UDP port */ \ + V(RECEIVING, receiving, uint8_t) \ + /* Listening as a QUIC server */ \ + V(LISTENING, listening, uint8_t) \ + /* In the process of closing down */ \ + V(CLOSING, closing, uint8_t) \ + /* In the process of closing down, waiting for pending send callbacks */ \ + V(WAITING_FOR_CALLBACKS, waiting_for_callbacks, uint8_t) \ + /* Temporarily paused serving new initial requests */ \ + V(BUSY, busy, uint8_t) \ + /* The number of pending send callbacks */ \ + V(PENDING_CALLBACKS, pending_callbacks, size_t) + +#define STREAM_STATE(V) \ + V(FIN_SENT, fin_sent, uint8_t) \ + V(FIN_RECEIVED, fin_received, uint8_t) \ + V(READ_ENDED, read_ended, uint8_t) \ + V(TRAILERS, trailers, uint8_t) \ + V(DESTROYED, destroyed, uint8_t) \ + /* Set when the JavaScript wrapper has a data event listener */ \ + V(DATA, data, uint8_t) \ + V(PAUSED, paused, uint8_t) \ + V(RESET, reset, uint8_t) \ + V(ID, id, int64_t) + +#define SOURCE_STATE(V) \ + V(EOS, eos, uint8_t) \ + V(CLOSED, closed, uint8_t) + +#define V(name, _, __) IDX_STATS_SESSION_##name, +enum SessionStatsIdx : int { SESSION_STATS(V) IDX_STATS_SESSION_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATE_SESSION_##name, +enum SessionStateIdx { SESSION_STATE(V) IDX_STATE_SESSION_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATS_ENDPOINT_##name, +enum EndpointStatsIdx { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATE_ENDPOINT_##name, +enum EndpointStateIdx { ENDPOINT_STATE(V) IDX_STATE_ENDPOINT_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATS_STREAM_##name, +enum StreamStatsIdx { STREAM_STATS(V) IDX_STATS_STREAM_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATE_STREAM_##name, +enum StreamStateIdx { STREAM_STATE(V) IDX_STATE_STREAM_COUNT }; +#undef V + +#define V(name, _, __) IDX_STATE_STREAM_##name, +enum SourceStateIdx { SOURCE_STATE(V) IDX_STATE_SOURCE_COUNT }; +#undef V + +using error_code = uint64_t; +using quic_version = uint32_t; +using datagram_id = uint64_t; +using stream_id = int64_t; + +// The kNNN-style constants are used internally only. +// The ALLCAPS-style constants are intended to be exposed to JavaScript. + +constexpr uint64_t kMaxUint64 = std::numeric_limits::max(); +constexpr size_t kMaxSizeT = std::numeric_limits::max(); +constexpr size_t kMaxVectorCount = 16; +constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE; +constexpr size_t kTokenSecretLen = 16; +constexpr uint64_t kSocketAddressInfoTimeout = 10000000000; // 10 seconds +constexpr uint8_t kRetryTokenMagic = 0xb6; +constexpr uint8_t kTokenMagic = 0x36; +constexpr uint8_t kCryptoTokenKeylen = 16; +constexpr uint8_t kCryptoTokenIvlen = 12; +constexpr uint8_t kTokenRandLen = 16; +constexpr uint8_t kMaxRetryTokenLen = + 1 + sizeof(uint64_t) + NGTCP2_MAX_CIDLEN + 16 + kTokenRandLen; +constexpr uint8_t kMinRetryTokenLen = 1 + kTokenRandLen; +constexpr uint8_t kMaxTokenLen = 1 + sizeof(uint64_t) + 16 + kTokenRandLen; + +constexpr uint64_t DEFAULT_ACTIVE_CONNECTION_ID_LIMIT = 2; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_UNI = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAMS_BIDI = 100; +constexpr uint64_t DEFAULT_MAX_STREAMS_UNI = 3; +constexpr uint64_t DEFAULT_MAX_DATA = 1 * 1024 * 1024; +constexpr uint64_t DEFAULT_MAX_IDLE_TIMEOUT = 10; // seconds +constexpr size_t DEFAULT_MAX_CONNECTIONS = + std::min(kMaxSizeT, static_cast(kMaxSafeJsInteger)); +constexpr size_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; +constexpr size_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = + (DEFAULT_MAX_CONNECTIONS_PER_HOST * 10); +constexpr size_t DEFAULT_MAX_STATELESS_RESETS = 10; +constexpr size_t DEFAULT_MAX_RETRY_LIMIT = 10; +constexpr uint64_t DEFAULT_RETRYTOKEN_EXPIRATION = 10; // seconds +constexpr uint64_t DEFAULT_TOKEN_EXPIRATION = 10; // seconds +constexpr uint64_t NGTCP2_APP_NOERROR = 65280; + +constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:" + "TLS_AES_256_GCM_SHA384:" + "TLS_CHACHA20_POLY1305_" + "SHA256:TLS_AES_128_CCM_SHA256"; +constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521"; + +enum class EndpointLabel { + LOCAL, + REMOTE, +}; + +enum class Direction { + BIDIRECTIONAL, + UNIDIRECTIONAL, +}; + +// Used for routable CIDs if we implement those +// constexpr size_t kMaxDynamicServerIDLength = 7; +// constexpr size_t kMinDynamicServerIDLength = 1; + +static constexpr size_t kCryptoTokenSecretlen = 32; +static constexpr size_t kCryptoKeylen = 64; +static constexpr size_t kCryptoIvlen = 64; +static constexpr char kQuicClientEarlyTrafficSecret[] = + "QUIC_CLIENT_EARLY_TRAFFIC_SECRET"; +static constexpr char kQuicClientHandshakeTrafficSecret[] = + "QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET"; +static constexpr char kQuicClientTrafficSecret0[] = + "QUIC_CLIENT_TRAFFIC_SECRET_0"; +static constexpr char kQuicServerHandshakeTrafficSecret[] = + "QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET"; +static constexpr char kQuicServerTrafficSecret[] = + "QUIC_SERVER_TRAFFIC_SECRET_0"; + +#define HAS_INSTANCE() \ + static inline bool HasInstance(Environment* env, \ + v8::Local value) { \ + return !value.IsEmpty() && value->IsObject() && \ + GetConstructorTemplate(env)->HasInstance(value); \ + } + +#define DEBUG(self, message) Debug(self, message); + +#define DEBUG_ARGS(self, message, ...) Debug(self, message, __VA_ARGS__); + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc new file mode 100644 index 00000000000000..b6c855f91c419b --- /dev/null +++ b/src/quic/endpoint.cc @@ -0,0 +1,1659 @@ +#include "endpoint.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crypto.h" +#include "quic/defs.h" +#include "quic/quic.h" +#include "v8-local-handle.h" + +namespace node { + +using crypto::EntropySource; +using v8::BackingStore; +using v8::BigInt; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::PropertyAttribute; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace quic { + +// ====================================================================================== +// Endpoint::UDP and Endpoint::UDP::Impl + +class Endpoint::UDP::Impl final : public HandleWrap { + public: + static Local GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.udp_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = NewFunctionTemplate(env->isolate(), IllegalConstructor); + tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + HandleWrap::kInternalFieldCount); + tmpl->SetClassName(state.endpoint_udp_string()); + state.set_udp_constructor_template(tmpl); + } + return tmpl; + } + + static BaseObjectPtr Create(Environment* env, Endpoint* endpoint) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env, obj, endpoint); + } + + static Impl* From(uv_handle_t* handle) { + Impl* impl = + ContainerOf(&Impl::handle_, reinterpret_cast(handle)); + return impl; + } + + Impl(Environment* env, v8::Local object, Endpoint* endpoint) + : HandleWrap(env, + object, + reinterpret_cast(&handle_), + AsyncWrap::PROVIDER_QUICENDPOINT_UDP), + endpoint_(endpoint) { + CHECK_EQ(uv_udp_init(env->event_loop(), &handle_), 0); + handle_.data = this; + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Endpoint::UDP) + SET_SELF_SIZE(UDP) + + private: + static void ClosedCb(uv_handle_t* handle) { + std::unique_ptr ptr(From(handle)); + } + + static void OnAlloc(uv_handle_t* handle, + size_t suggested_size, + uv_buf_t* buf) { + *buf = From(handle)->env()->allocate_managed_buffer(suggested_size); + } + + static void OnReceive(uv_udp_t* handle, + ssize_t nread, + const uv_buf_t* buf, + const sockaddr* addr, + unsigned int flags) { + // Nothing to do it in this case. + if (nread == 0) return; + + Impl* impl = ContainerOf(&Impl::handle_, handle); + + CHECK_NOT_NULL(impl); + CHECK_NOT_NULL(impl->endpoint_); + + if (nread < 0) { + impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE, + static_cast(nread)); + return; + } + + if (UNLIKELY(flags & UV_UDP_PARTIAL)) { + impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOBUFS); + return; + } + + impl->endpoint_->Receive( + static_cast(nread), *buf, SocketAddress(addr)); + } + + uv_udp_t handle_; + Endpoint* endpoint_; + + friend class UDP; +}; + +Endpoint::UDP::UDP(Environment* env, Endpoint* endpoint) + : impl_(Impl::Create(env, endpoint)) { + env->AddCleanupHook(CleanupHook, this); +} + +Endpoint::UDP::~UDP() { + Close(); +} + +int Endpoint::UDP::Bind(const Endpoint::Options& options) { + if (is_closed()) return UV_EBADF; + + DEBUG(impl_.get(), "Binding"); + + int flags = 0; + if (options.local_address.family() == AF_INET6 && options.ipv6_only) + flags |= UV_UDP_IPV6ONLY; + int err = uv_udp_bind(&impl_->handle_, options.local_address.data(), flags); + int size; + + if (!err) { + size = static_cast(options.udp_receive_buffer_size); + if (size > 0) { + err = uv_recv_buffer_size(reinterpret_cast(&impl_->handle_), + &size); + if (err) return err; + } + + size = static_cast(options.udp_send_buffer_size); + if (size > 0) { + err = uv_send_buffer_size(reinterpret_cast(&impl_->handle_), + &size); + if (err) return err; + } + + size = static_cast(options.udp_ttl); + if (size > 0) { + err = uv_udp_set_ttl(&impl_->handle_, size); + if (err) return err; + } + } + + return err; +} + +void Endpoint::UDP::Ref() { + if (!is_closed()) uv_ref(reinterpret_cast(&impl_->handle_)); +} + +void Endpoint::UDP::Unref() { + if (!is_closed()) uv_unref(reinterpret_cast(&impl_->handle_)); +} + +int Endpoint::UDP::Start() { + if (is_closed()) return UV_EBADF; + if (impl_->IsHandleClosing()) return UV_EBADF; + DEBUG(impl_.get(), "Waiting for packets"); + int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive); + return err == UV_EALREADY ? 0 : err; +} + +void Endpoint::UDP::Stop() { + if (is_closed()) return; + if (!impl_->IsHandleClosing()) { + DEBUG(impl_.get(), "No longer waiting for packets"); + USE(uv_udp_recv_stop(&impl_->handle_)); + } +} + +void Endpoint::UDP::Close() { + if (is_closed()) return; + Stop(); + impl_->env()->RemoveCleanupHook(CleanupHook, this); + impl_->Close(); + impl_.reset(); +} + +bool Endpoint::UDP::is_closed() const { + return impl_.get() == nullptr; +} +Endpoint::UDP::operator bool() const { + return !is_closed(); +} + +Maybe Endpoint::UDP::local_address() const { + if (is_closed()) return Nothing(); + return Just(SocketAddress::FromSockName(impl_->handle_)); +} + +int Endpoint::UDP::Send(BaseObjectPtr req) { + if (is_closed()) return UV_EBADF; + CHECK(req && !req->is_pending()); + // Attach this UDP::Impl instance to the packet to that we are sure it will + // stay alive until the callback is invoked... + DEBUG_ARGS(impl_.get(), "Sending a packet [%s]", req->ToString()); + req->Attach(impl_); + uv_buf_t buf = *req; + // The packet maintains a strong reference to itself to keep it from being + // gc'd until the callback is invoked. + + return req->Dispatch(uv_udp_send, + &impl_->handle_, + &buf, + 1, + req->destination().data(), + uv_udp_send_cb{[](uv_udp_send_t* req, int status) { + BaseObjectPtr ptr( + static_cast(PacketReq::from_req(req))); + ptr->Done(status); + }}); +} + +void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("impl", impl_); +} + +void Endpoint::UDP::CleanupHook(void* data) { + static_cast(data)->Close(); +} + +// ====================================================================================== +// Endpoint::Options + +Endpoint::Options::Options() { + GenerateResetTokenSecret(); +} + +void Endpoint::Options::GenerateResetTokenSecret() { + crypto::EntropySource(reinterpret_cast(&reset_token_secret), + NGTCP2_STATELESS_RESET_TOKENLEN); +} + +// ====================================================================================== +// Endpoint + +Local Endpoint::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.endpoint_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->SetClassName(state.endpoint_string()); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Endpoint::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "listen", DoListen); + SetProtoMethod(isolate, tmpl, "closeGracefully", DoCloseGracefully); + SetProtoMethod(isolate, tmpl, "connect", DoConnect); + SetProtoMethod(isolate, tmpl, "markBusy", MarkBusy); + SetProtoMethod(isolate, tmpl, "ref", Ref); + SetProtoMethod(isolate, tmpl, "unref", Unref); + SetProtoMethodNoSideEffect(isolate, tmpl, "address", LocalAddress); + state.set_endpoint_constructor_template(tmpl); + } + return tmpl; +} + +void Endpoint::Initialize(Environment* env, Local target) { + SetMethod(env->context(), target, "createEndpoint", CreateEndpoint); + + OptionsObject::Initialize(env, target); + +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); + ENDPOINT_STATS(V) + NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT); +#undef V +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); + ENDPOINT_STATE(V) + NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_COUNT); +#undef V +} + +void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(IllegalConstructor); + registry->Register(CreateEndpoint); + registry->Register(DoConnect); + registry->Register(DoListen); + registry->Register(DoCloseGracefully); + registry->Register(LocalAddress); + registry->Register(Ref); + registry->Register(Unref); + OptionsObject::RegisterExternalReferences(registry); +} + +void Endpoint::OptionsObject::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(GenerateResetTokenSecret); + registry->Register(SetResetTokenSecret); +} + +BaseObjectPtr Endpoint::Create(Environment* env, + const Endpoint::Options& options) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env, obj, options); +} + +Endpoint::Endpoint(Environment* env, + Local object, + const Endpoint::Options& options) + : EndpointStatsBase(env), + AsyncWrap(env, object, AsyncWrap::PROVIDER_QUICENDPOINT), + state_(env->isolate()), + options_(options), + udp_(env, this), + addrLRU_(options.address_lru_size) { + MakeWeak(); + + DEBUG(this, "Created"); + + EntropySource(reinterpret_cast(token_secret_), + kTokenSecretLen); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env->state_string(), state_.GetArrayBuffer()); + defineProperty(env->stats_string(), ToBigUint64Array(env)); +} + +Endpoint::~Endpoint() { + DEBUG(this, "Destroyed"); + if (udp_) udp_.Close(); + CHECK_EQ(state_->pending_callbacks, 0); + CHECK(sessions_.empty()); + CHECK(is_closed()); +} + +v8::Maybe Endpoint::local_address() const { + if (is_closed()) return v8::Nothing(); + return udp_.local_address(); +} + +void Endpoint::MarkAsBusy(bool on) { + DEBUG_ARGS(this, "Marked as busy? %s", on ? "yes" : "no"); + if (!is_closed()) state_->busy = on ? 1 : 0; +} + +Maybe Endpoint::GenerateNewToken(quic_version version, + uint8_t* token, + const SocketAddress& remote_address) { + if (is_closed() || is_closing()) return Nothing(); + DEBUG(this, "Creating a new regular token"); + return GenerateToken(version, token, remote_address, token_secret_); +} + +void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { + if (is_closed() || is_closing()) return; + DEBUG_ARGS(this, + "Adding a new %s session [%s]", + session->is_server() ? "server" : "client", + cid); + sessions_[cid] = session; + IncrementSocketAddressCounter(session->remote_address()); + IncrementStat(session->is_server() ? &EndpointStats::server_sessions + : &EndpointStats::client_sessions); + if (session->is_server()) EmitNewSession(session); +} + +void Endpoint::RemoveSession(const CID& cid, const SocketAddress& addr) { + if (is_closed()) return; + + DEBUG_ARGS(this, "Removing a session [%s]", cid); + + DecrementSocketAddressCounter(addr); + sessions_.erase(cid); + + if (state_->waiting_for_callbacks == 1) MaybeDestroy(); +} + +BaseObjectPtr Endpoint::FindSession(const CID& cid) { + BaseObjectPtr session; + auto session_it = sessions_.find(cid); + if (session_it == std::end(sessions_)) { + auto scid_it = dcid_to_scid_.find(cid); + if (scid_it != std::end(dcid_to_scid_)) { + session_it = sessions_.find(scid_it->second); + CHECK_NE(session_it, std::end(sessions_)); + session = session_it->second; + } + } else { + session = session_it->second; + } + return session; +} + +void Endpoint::AssociateCID(const CID& cid, const CID& scid) { + if (!is_closed() && !is_closing() && cid && scid && cid != scid && + dcid_to_scid_[cid] != scid) { + DEBUG_ARGS(this, "Associating %s to %s", cid, scid); + dcid_to_scid_[cid] = scid; + } +} + +void Endpoint::DisassociateCID(const CID& cid) { + if (!cid) return; + if (!is_closed() && cid) { + DEBUG_ARGS(this, "Disassociating %s", cid); + dcid_to_scid_.erase(cid); + } +} + +void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token, + Session* session) { + if (is_closed() || is_closing()) return; + DEBUG(this, "Associating stateless reset token"); + token_map_[token] = session; +} + +void Endpoint::DisassociateStatelessResetToken( + const StatelessResetToken& token) { + DEBUG(this, "Disassociating stateless reset token"); + if (!is_closed()) token_map_.erase(token); +} + +void Endpoint::Send(BaseObjectPtr packet) { + if (is_closed() || is_closing() || packet->length() == 0) return; + state_->pending_callbacks++; + int err = udp_.Send(packet); + + if (err != 0) { + packet->Done(err); + Destroy(CloseContext::SEND_FAILURE, err); + } + IncrementStat(&EndpointStats::bytes_sent, packet->length()); + IncrementStat(&EndpointStats::packets_sent); + DEBUG_ARGS( + this, "Packets sent: %" PRIu64, GetStat(&EndpointStats::packets_sent)); + DEBUG_ARGS(this, "Bytes sent: %" PRIu64, GetStat(&EndpointStats::bytes_sent)); +} + +void Endpoint::SendRetry(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + auto info = addrLRU_.Upsert(remote_address); + if (++(info->retry_count) <= options_.retry_limit) { + DEBUG(this, "Sending retry packet"); + // A retry packet will never be larger than the default 1200 so we're safe + // not providing a size here... + auto packet = Packet::Create(env(), this, remote_address, "retry"); + if (GenerateRetryPacket( + packet, version, token_secret_, dcid, scid, remote_address)) + Send(std::move(packet)); + // If creating the retry is unsuccessful, we just drop things on the floor. + // It's not worth committing any further resources to this one packet. We + // might want to log the failure at some point tho. + } +} + +void Endpoint::SendVersionNegotiation(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + const auto generateReservedVersion = [&] { + socklen_t addrlen = remote_address.length(); + quic_version h = 0x811C9DC5u; + quic_version ver = htonl(version); + const uint8_t* p = remote_address.raw(); + const uint8_t* ep = p + addrlen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + p = reinterpret_cast(&ver); + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= NGTCP2_RESERVED_VERSION_MASK; + return h; + }; + + uint32_t sv[2] = {generateReservedVersion(), NGTCP2_PROTO_VER_MAX}; + + DEBUG(this, "Sending version negotiation packet"); + + uint8_t unused_random; + EntropySource(&unused_random, 1); + size_t pktlen = dcid.length() + scid.length() + (sizeof(sv)) + 7; + + auto packet = + Packet::Create(env(), this, remote_address, "version negotiation"); + ngtcp2_vec vec = *packet; + + ssize_t nwrite = ngtcp2_pkt_write_version_negotiation(vec.base, + pktlen, + unused_random, + dcid, + dcid.length(), + scid, + scid.length(), + sv, + arraysize(sv)); + if (nwrite > 0) { + packet->Truncate(nwrite); + Send(std::move(packet)); + } +} + +bool Endpoint::SendStatelessReset(const CID& cid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + size_t source_len) { + if (UNLIKELY(options_.disable_stateless_reset)) return false; + + static constexpr size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5; + static constexpr size_t kMinStatelessResetLen = 41; + uint8_t random[kRandlen]; + + const auto exceeds_limits = [&] { + SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address); + auto count = counts != nullptr ? counts->reset_count : 0; + return count >= options_.max_stateless_resets; + }; + + // Per the QUIC spec, we need to protect against sending too many stateless + // reset tokens to an endpoint to prevent endless looping. + if (exceeds_limits()) return false; + + DEBUG(this, "Sending stateless reset packet"); + + // Per the QUIC spec, a stateless reset token must be strictly smaller than + // the packet that triggered it. This is one of the mechanisms to prevent + // infinite looping exchange of stateless tokens with the peer. An endpoint + // should never send a stateless reset token smaller than 41 bytes per the + // QUIC spec. The reason is that packets less than 41 bytes may allow an + // observer to reliably determine that it's a stateless reset. + size_t pktlen = source_len - 1; + if (pktlen < kMinStatelessResetLen) return false; + + StatelessResetToken token(options_.reset_token_secret, cid); + crypto::EntropySource(random, kRandlen); + + auto packet = Packet::Create(env(), this, remote_address, "stateless reset"); + ngtcp2_vec vec = *packet; + + ssize_t nwrite = ngtcp2_pkt_write_stateless_reset( + vec.base, pktlen, const_cast(token.data()), random, kRandlen); + if (nwrite >= static_cast(kMinStatelessResetLen)) { + addrLRU_.Upsert(remote_address)->reset_count++; + packet->Truncate(nwrite); + Send(std::move(packet)); + return true; + } + return false; +} + +void Endpoint::SendImmediateConnectionClose(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + QuicError reason) { + DEBUG(this, "Sending immediate connection close"); + auto packet = Packet::Create( + env(), this, remote_address, "immediate connection close (endpoint)"); + ngtcp2_vec vec = *packet; + ssize_t nwrite = ngtcp2_crypto_write_connection_close( + vec.base, + vec.len, + version, + dcid, + scid, + reason.code(), + // We do not bother sending a reason string here, even if + // there is one in the QuicError + nullptr, + 0); + if (nwrite > 0) { + packet->Truncate(static_cast(nwrite)); + Send(std::move(packet)); + } +} + +bool Endpoint::Start() { + if (is_closed() || is_closing()) return false; + if (state_->receiving == 1) return true; + + DEBUG(this, "Starting"); + + int err = 0; + if (state_->bound == 0) { + err = udp_.Bind(options_); + if (err != 0) { + // If we failed to bind, destroy the endpoint. There's nothing we can do. + Destroy(CloseContext::BIND_FAILURE, err); + return false; + } + state_->bound = 1; + } + + err = udp_.Start(); + if (err != 0) { + // If we failed to start listening, destroy the endpoint. There's nothing we + // can do. + Destroy(CloseContext::START_FAILURE, err); + return false; + } + + BindingState::Get(env()).listening_endpoints_[this] = + BaseObjectPtr(this); + state_->receiving = 1; + return true; +} + +bool Endpoint::Listen(const Session::Options& options) { + if (is_closed() || is_closing()) return false; + if (state_->listening == 1) return true; + + server_options_ = options; + + if (Start()) { + state_->listening = 1; + + DEBUG(this, "Listening as a server"); + + return true; + } + return false; +} + +BaseObjectPtr Endpoint::Connect(const SocketAddress& remote_address, + const Session::Options& options, + SessionTicket* sessionTicket) { + // If starting fails, the endpoint will be destroyed. + if (!Start()) return BaseObjectPtr(); + + DEBUG_ARGS(this, "Connecting to %s", remote_address); + + auto local = local_address().ToChecked(); + + auto config = NewSessionConfig( + // For client sessions, we always randomly generate an intial CID for the + // server. This is generally just a throwaway. The server will generate + // it's own CID and send that back to us. + CIDFactory::random().Generate(NGTCP2_MIN_INITIAL_DCIDLEN), + local, + remote_address, + NGTCP2_PROTO_VER_MAX, + NGTCP2_CRYPTO_SIDE_CLIENT); + + DEBUG_ARGS(this, "QLOG?? %s", options.qlog ? "Yes" : "No"); + if (options.qlog) config.EnableQLog(); + + if (sessionTicket != nullptr) + config.session_ticket = BaseObjectPtr(sessionTicket); + + DEBUG_ARGS(this, "Client CID %s", config.scid); + DEBUG_ARGS(this, "Server CID (random) %s", config.dcid); + + auto session = + Session::Create(BaseObjectPtr(this), config, options); + if (!session) return BaseObjectPtr(); + + DEBUG(this, "Client connection created"); + + session->set_wrapped(); + + Session::SendPendingDataScope send(session); + return session; +} + +void Endpoint::MaybeDestroy() { + if (!is_closing() && sessions_.empty() && state_->pending_callbacks == 0 && + state_->listening == 0) { + Destroy(); + } +} + +void Endpoint::Destroy(CloseContext context, int status) { + if (is_closed() || is_closing()) return; + + DEBUG_ARGS(this, "Destroying [%d, %d]", static_cast(context), status); + + RecordTimestamp(&EndpointStats::destroyed_at); + + state_->closing = true; + + // Stop listening for new connections while we shut things down. + state_->listening = 0; + + // If there are open sessions still, shut them down. As those clean themselves + // up, they will remove themselves. The cleanup here will be synchronous and + // no attempt will be made to communicate further with the peer. + if (!sessions_.empty()) { + close_context_ = context; + close_status_ = status; + for (auto& session : sessions_) session.second->CloseSilently(); + } + CHECK(sessions_.empty()); + + state_->closing = false; + + udp_.Close(); + state_->bound = 0; + state_->receiving = 0; + token_map_.clear(); + dcid_to_scid_.clear(); + BindingState::Get(env()).listening_endpoints_.erase(this); + + return context == CloseContext::CLOSE + ? EmitEndpointDone() + : EmitError(close_context_, close_status_); +} + +void Endpoint::CloseGracefully() { + DEBUG(this, "Close gracefully"); + + if (!is_closed() && !is_closing() && state_->waiting_for_callbacks == 0) { + state_->listening = 0; + state_->waiting_for_callbacks = 1; + } + + MaybeDestroy(); +} + +Session::Config Endpoint::NewSessionConfig(const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + quic_version version, + ngtcp2_crypto_side side) { + return Session::Config( + *this, scid, local_address, remote_address, version, side); +} + +void Endpoint::Receive(size_t nread, + const uv_buf_t& buf, + const SocketAddress& remote_address) { + const auto is_diagnostic_packet_loss = [](auto probability) { + if (LIKELY(probability == 0.0)) return false; + unsigned char c = 255; + EntropySource(&c, 1); + return (static_cast(c) / 255) < probability; + }; + + const auto receive = [&](Store&& store, + const SocketAddress& local_address, + const SocketAddress& remote_address, + const CID& dcid, + const CID& scid) { + IncrementStat(&EndpointStats::bytes_received, store.length()); + BaseObjectPtr session = FindSession(dcid); + return session && !session->is_destroyed() + ? session->Receive( + std::move(store), local_address, remote_address) + : false; + }; + + const auto accept = [&](const Session::Config& config, Store&& store) { + if (is_closed() || is_closing() || !is_listening()) return false; + + DEBUG(this, "Accepting initial packet"); + + BaseObjectPtr session = + Session::Create(BaseObjectPtr(this), config, server_options_); + + CHECK(session); + + DEBUG_ARGS(this, "Client CID %s", config.dcid); + DEBUG_ARGS(this, "Server CID %s", config.scid); + if (config.ocid.IsJust()) + DEBUG_ARGS(this, "Original Server CID %s", config.ocid.FromJust()); + + return session + ? session->Receive( + std::move(store), config.local_addr, config.remote_addr) + : false; + }; + + const auto acceptInitialPacket = [&](const quic_version version, + const CID& dcid, + const CID& scid, + Store&& store, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + // Conditionally accept an initial packet to create a new session. + + // If we're not listening, do not accept. + if (state_->listening == 0) return false; + + DEBUG(this, "Evaluating initial packet"); + + ngtcp2_pkt_hd hd; + + // This is our first condition check... A minimal check to see if ngtcp2 can + // even recognize this packet as a quic packet with the correct version. + ngtcp2_vec vec = store; + switch (ngtcp2_accept(&hd, vec.base, vec.len)) { + case 1: + // The requested QUIC protocol version is not supported + SendVersionNegotiation( + version, dcid, scid, local_address, remote_address); + // The packet was successfully processed, even if we did refuse the + // connection and send a version negotiation in response. + return true; + case -1: + // The packet is invalid and we're just going to ignore it. + return false; + } + + // This is the second condition check... If the server has been marked busy + // or the remote peer has exceeded their maximum number of concurrent + // connections, any new connections will be shut down immediately. + const auto limits_exceeded = [&] { + if (sessions_.size() >= options_.max_connections_total) return true; + + SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address); + auto count = counts != nullptr ? counts->active_connections : 0; + return count >= options_.max_connections_per_host; + }; + + if (state_->busy || limits_exceeded()) { + if (state_->busy) IncrementStat(&EndpointStats::server_busy_count); + // Endpoint is busy or the connection count is exceeded. The connection is + // refused. + IncrementStat(&EndpointStats::server_busy_count); + DEBUG( + this, + "Immediately closing because server is busy or limits are exceeded"); + SendImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address, + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + // The packet was successfully processed, even if we did refuse the + // connection. + return true; + } + + // At this point, we start to set up the configuration for our local + // session. The second argument to the Config constructor here is the dcid. + // We pass the received scid here as the value because that is the value + // *this* session will use as it's outbound dcid. + auto config = NewSessionConfig(scid, + local_address, + remote_address, + version, + NGTCP2_CRYPTO_SIDE_SERVER); + + // The this point, the config.scid and config.dcid represent *our* views of + // the CIDs. Specifically, config.dcid identifies the peer and config.scid + // identifies us. config.dcid should equal scid. config.scid should *not* + // equal dcid. + + const auto is_remote_address_validated = [&] { + auto info = addrLRU_.Peek(remote_address); + return info != nullptr ? info->validated : false; + }; + + config.ocid = Just(dcid); + + // QUIC has address validation built in to the handshake but allows for + // an additional explicit validation request using RETRY frames. If we + // are using explicit validation, we check for the existence of a valid + // retry token in the packet. If one does not exist, we send a retry with + // a new token. If it does exist, and if it's valid, we grab the original + // cid and continue. + if (!is_remote_address_validated()) { + switch (hd.type) { + case NGTCP2_PKT_INITIAL: + // First, let's see if we need to do anything here. + + if (options_.validate_address) { + // If there is no token, generate and send one. + if (hd.token.len == 0) { + SendRetry(version, dcid, scid, local_address, remote_address); + return true; + } + + // We have two kinds of tokens, each prefixed with a different magic + // byte. + switch (hd.token.base[0]) { + case kRetryTokenMagic: { + CID ocid; + if (!ValidateRetryToken( + version, + hd.token, + remote_address, + dcid, + token_secret_, + options_.retry_token_expiration * NGTCP2_SECONDS) + .To(&ocid)) { + // Invalid retry token was detected. Close the connection. + DEBUG( + this, + "Immediately closing because the retry token was invalid") + SendImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address, + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + return true; + } + + // The ocid is the original dcid that was encoded into the + // original retry packet sent to the client. We use it for + // validation. + config.ocid = Just(ocid); + config.retry_scid = Just(dcid); + break; + } + case kTokenMagic: { + if (!ValidateToken( + version, + hd.token, + remote_address, + token_secret_, + options_.token_expiration * NGTCP2_SECONDS)) { + SendRetry(version, dcid, scid, local_address, remote_address); + return true; + } + hd.token.base = nullptr; + hd.token.len = 0; + break; + } + default: { + SendRetry(version, dcid, scid, local_address, remote_address); + return true; + } + } + + // Ok! If we've got this far, our token is valid! Which means our + // path to the remote address is valid (for now). Let's record that + // so we don't have to do this dance again for this endpoint + // instance. + addrLRU_.Upsert(remote_address)->validated = true; + config.token = hd.token; + } else if (hd.token.len > 0) { + DEBUG(this, + "Address validation is disabled but there is a token! " + "Ignoring"); + // If validation is turned off and there is a token, that's weird. + // The peer should only have a token if we sent it to them and we + // wouldn't have sent it unless validation was turned on. Let's + // assume the peer is buggy or malicious and drop the packet on the + // floor. + return false; + } else { + DEBUG( + this, + "Warning: Address validation is turned off for this endpoint"); + } + break; + case NGTCP2_PKT_0RTT: + // If it's a 0RTT packet, we're always going to perform path + // validation no matter what. + SendRetry(version, dcid, scid, local_address, remote_address); + return true; + } + } + + return accept(config, std::move(store)); + }; + + // When a received packet contains a QUIC short header but cannot be matched + // to a known Session, it is either (a) garbage, (b) a valid packet for a + // connection we no longer have state for, or (c) a stateless reset. Because + // we do not yet know if we are going to process the packet, we need to try to + // quickly determine -- with as little cost as possible -- whether the packet + // contains a reset token. We do so by checking the final + // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the packet to see if they match + // one of the known reset tokens previously given by the remote peer. If + // there's a match, then it's a reset token, if not, we move on the to the + // next check. It is very important that this check be as inexpensive as + // possible to avoid a DOS vector. + const auto maybeStatelessReset = [&](const CID& dcid, + const CID& scid, + Store& store, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + if (options_.disable_stateless_reset || + store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) + return false; + + ngtcp2_vec vec = store; + vec.base += vec.len; + vec.base -= NGTCP2_STATELESS_RESET_TOKENLEN; + + Session* session = nullptr; + auto it = token_map_.find(StatelessResetToken(vec.base)); + if (it != token_map_.end()) session = it->second; + + DEBUG(this, "Received stateless reset"); + + return session != nullptr ? receive(std::move(store), + local_address, + remote_address, + dcid, + scid) + : false; + }; + + CHECK_LE(nread, buf.len); + + // When diagnostic packet loss is enabled, the packet will be randomly + // dropped. + if (UNLIKELY(is_diagnostic_packet_loss(options_.rx_loss))) { + // Simulating rx packet loss + return; + } + + // TODO(@jasnell): Implement blocklist support + // if (UNLIKELY(block_list_->Apply(remote_address))) { + // Debug(this, "Ignoring blocked remote address: %s", remote_address); + // return; + // } + + std::shared_ptr backing = env()->release_managed_buffer(buf); + if (UNLIKELY(!backing)) + return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM); + + Store store(backing, nread, 0); + + ngtcp2_vec vec = store; + + quic_version pversion; + const uint8_t* pdcid; + size_t pdcidlen; + const uint8_t* pscid; + size_t pscidlen; + + // This is our first check to see if the received data can be processed as a + // QUIC packet. If this fails, then the QUIC packet header is invalid and + // cannot be processed; all we can do is ignore it. If it succeeds, we have a + // valid QUIC header but there is still no guarantee that the packet can be + // successfully processed. + if (ngtcp2_pkt_decode_version_cid(&pversion, + &pdcid, + &pdcidlen, + &pscid, + &pscidlen, + vec.base, + vec.len, + NGTCP2_MAX_CIDLEN) < 0) { + return; // Ignore the packet! + } + + // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. The ngtcp2 + // API allows non-standard lengths, and we may want to allow non-standard + // lengths later. But for now, we're going to ignore any packet with a + // non-standard CID length. + if (pdcidlen > NGTCP2_MAX_CIDLEN || pscidlen > NGTCP2_MAX_CIDLEN) + return; // Ignore the packet! + + // Each QUIC peer has two CIDs: The Source Connection ID (or scid), and the + // Destination Connection ID (or dcid). For each peer, the dcid is the CID + // identifying the other peer, and the scid is the CID identifying itself. + // That is, the client's scid is the server dcid; likewise the server's scid + // is the client's dcid. + // + // The dcid and scid below are the values sent from the peer received in the + // current packet, so in this case, dcid represents who the peer sent the + // packet too (this endpoint) and the scid represents who sent the packet. + CID dcid(pdcid, pdcidlen); + CID scid(pscid, pscidlen); + + DEBUG_ARGS(this, "Received packet DCID %s (destination)", dcid); + DEBUG_ARGS(this, "Received packet SCID %s (source)", scid) + + // We index the current sessions by the dcid of the client. For initial + // packets, the dcid is some random value and the scid is omitted from the + // header (it uses what quic calls a "short header"). It is unlikely (but not + // impossible) that this randomly selected dcid will be in our index. If we do + // happen to have a collision, as unlikely as it is, ngtcp2 will do the right + // thing when it tries to process the packet so we really don't have to worry + // about it here. If the dcid is not known, the listener here will be nullptr. + // + // When the session is established, this peer will create it's own scid and + // will send that back to the remote peer to use as it's new dcid on + // subsequent packets. When that session is added, we will index it by the + // local scid, so as long as the client sends the subsequent packets with the + // right dcid, everything will just work. + + auto session = FindSession(dcid); + auto addr = local_address().ToChecked(); + + // Once we start receiving, it's likely that we'll create a bunch of JS + // objects, so let's go ahead and create a v8::HandleScope here. + HandleScope handle_scope(env()->isolate()); + + // If a session is not found, there are four possible reasons: + // 1. The session has not been created yet + // 2. The session existed once but we've lost the local state for it + // 3. The packet is a stateless reset sent by the peer + // 4. This is a malicious or malformed packet. + if (!session) { + // No existing session. + + // For the current version of QUIC, it is a short header if there is no + // scid. + bool is_short_header = (pversion == NGTCP2_PROTO_VER_MAX && !scid); + + // Handle possible reception of a stateless reset token... If it is a + // stateless reset, the packet will be handled with no additional action + // necessary here. We want to return immediately without committing any + // further resources. + if (is_short_header && + maybeStatelessReset(dcid, scid, store, addr, remote_address)) + return; // Stateless reset! Don't do any further processing. + + if (acceptInitialPacket( + pversion, dcid, scid, std::move(store), addr, remote_address)) { + // Packet was successfully received. + return IncrementStat(&EndpointStats::packets_received); + } + return; // Ignore the packet! + } + + DEBUG_ARGS(this, "Forwarding to session (dcid: %s, scid: %s)", dcid, scid); + // If we got here, the dcid matched the scid of a known local session. Yay! + if (receive(std::move(store), addr, remote_address, dcid, scid)) + IncrementStat(&EndpointStats::packets_received); // Success! +} + +void Endpoint::OnSendDone(int status) { + if (is_closed()) return; + DEBUG_ARGS(this, "Packet sent [%d]", status); + state_->pending_callbacks--; + // Can we go ahead and close now? Yes, so long as there are no pending + // callbacks and no sessions open. + if (state_->waiting_for_callbacks == 1) { + HandleScope scope(env()->isolate()); + MaybeDestroy(); + } +} + +void Endpoint::IncrementSocketAddressCounter(const SocketAddress& addr) { + addrLRU_.Upsert(addr)->active_connections++; +} + +void Endpoint::DecrementSocketAddressCounter(const SocketAddress& addr) { + auto* counts = addrLRU_.Peek(addr); + if (counts != nullptr && counts->active_connections > 0) + counts->active_connections--; +} + +void Endpoint::Ref() { + if (!is_closed()) udp_.Ref(); +} + +void Endpoint::Unref() { + if (!is_closed()) udp_.Unref(); +} + +// ====================================================================================== +// Endpoint::SocketAddressInfoTraits + +bool Endpoint::SocketAddressInfoTraits::CheckExpired( + const SocketAddress& address, const Type& type) { + return (uv_hrtime() - type.timestamp) > kSocketAddressInfoTimeout; +} + +void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address, + Type* type) { + type->timestamp = uv_hrtime(); +} + +// ====================================================================================== +// JavaScript call outs + +void Endpoint::EmitNewSession(const BaseObjectPtr& session) { + if (!env()->can_call_into_js()) return; + CallbackScope scope(this); + session->set_wrapped(); + Local arg = session->object(); + + DEBUG(this, "Emitting new session callback"); + + MakeCallback(BindingState::Get(env()).session_new_callback(), 1, &arg); +} + +void Endpoint::EmitEndpointDone() { + if (!env()->can_call_into_js()) return; + CallbackScope scope(this); + + DEBUG(this, "Emitting endpoint done callback"); + + MakeCallback(BindingState::Get(env()).endpoint_done_callback(), 0, nullptr); +} + +void Endpoint::EmitError(CloseContext context, int status) { + if (!env()->can_call_into_js()) return; + CallbackScope scope(this); + DEBUG(this, "Emitting error callback"); + auto isolate = env()->isolate(); + Local argv[] = {Integer::New(isolate, static_cast(context)), + Integer::New(isolate, static_cast(status))}; + + MakeCallback(BindingState::Get(env()).endpoint_error_callback(), + arraysize(argv), + argv); +} + +void Endpoint::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); + tracker->TrackField("udp", udp_); + tracker->TrackField("server_options", server_options_); + tracker->TrackField("token_map", token_map_); + tracker->TrackField("sessions", sessions_); + tracker->TrackField("cid_map", dcid_to_scid_); + tracker->TrackField("address LRU", addrLRU_); +} + +// ====================================================================================== +// Endpoint JavaScript API + +void Endpoint::CreateEndpoint(const FunctionCallbackInfo& args) { + CHECK(!args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + CHECK(OptionsObject::HasInstance(env, args[0])); + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args[0]); + + BaseObjectPtr endpoint = Create(env, *options); + if (endpoint) args.GetReturnValue().Set(endpoint->object()); +} + +void Endpoint::DoConnect(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + CHECK(Session::OptionsObject::HasInstance(env, args[1])); + CHECK_IMPLIES(!args[2]->IsUndefined(), + SessionTicket::HasInstance(env, args[2])); + + SocketAddressBase* address; + Session::OptionsObject* options; + SessionTicket* sessionTicket = nullptr; + + ASSIGN_OR_RETURN_UNWRAP(&address, args[0]); + ASSIGN_OR_RETURN_UNWRAP(&options, args[1]); + if (!args[2]->IsUndefined()) ASSIGN_OR_RETURN_UNWRAP(&sessionTicket, args[2]); + + BaseObjectPtr session = + endpoint->Connect(*address->address(), *options, sessionTicket); + if (session) args.GetReturnValue().Set(session->object()); +} + +void Endpoint::DoListen(const FunctionCallbackInfo& args) { + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + Environment* env = Environment::GetCurrent(args); + CHECK(Session::OptionsObject::HasInstance(env, args[0])); + Session::OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args[0].As()); + args.GetReturnValue().Set(endpoint->Listen(options->options())); +} + +void Endpoint::MarkBusy(const FunctionCallbackInfo& args) { + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->MarkAsBusy(args[0]->IsTrue()); +} + +void Endpoint::DoCloseGracefully(const FunctionCallbackInfo& args) { + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->CloseGracefully(); +} + +void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + SocketAddress address; + if (endpoint->local_address().To(&address)) { + auto addr = SocketAddressBase::Create( + env, std::make_shared(address)); + if (addr) args.GetReturnValue().Set(addr->object()); + } +} + +void Endpoint::Ref(const FunctionCallbackInfo& args) { + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->Ref(); +} + +void Endpoint::Unref(const FunctionCallbackInfo& args) { + Endpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->Unref(); +} + +// ====================================================================================== +// Endpoint::OptionsObject + +Local Endpoint::OptionsObject::GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.endpoint_config_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount(kInternalFieldCount); + tmpl->SetClassName(state.endpoint_options_string()); + SetProtoMethod( + isolate, tmpl, "generateResetTokenSecret", GenerateResetTokenSecret); + SetProtoMethod(isolate, tmpl, "setResetTokenSecret", SetResetTokenSecret); + state.set_endpoint_config_constructor_template(tmpl); + } + return tmpl; +} + +void Endpoint::OptionsObject::Initialize(Environment* env, + Local target) { + SetConstructorFunction(env->context(), + target, + "EndpointOptions", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + uint64_t Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + + uint64_t val = 0; + if (value->IsBigInt()) { + bool lossless = true; + val = value.As()->Uint64Value(&lossless); + if (!lossless) { + Utf8Value label(env()->isolate(), name); + THROW_ERR_OUT_OF_RANGE( + env(), + (std::string("options.") + (*label) + " is out of range").c_str()); + return Nothing(); + } + } else { + val = static_cast(value.As()->Value()); + } + options_.*member = val; + return Just(true); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + uint32_t Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK(value->IsUint32()); + + uint32_t val = value.As()->Value(); + options_.*member = val; + return Just(true); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + uint8_t Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK(value->IsUint32()); + + uint32_t val = value.As()->Value(); + if (val > 255) return Just(false); + options_.*member = static_cast(val); + return Just(true); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + double Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK(value->IsNumber()); + double val = value.As()->Value(); + options_.*member = val; + return Just(true); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + ngtcp2_cc_algo Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + ngtcp2_cc_algo val = static_cast(value.As()->Value()); + switch (val) { + case NGTCP2_CC_ALGO_CUBIC: + // Fall through + case NGTCP2_CC_ALGO_RENO: + // Fall through + case NGTCP2_CC_ALGO_BBR: + // Fall through + case NGTCP2_CC_ALGO_BBR2: + options_.*member = val; + break; + default: + UNREACHABLE(); + } + + return Just(true); +} + +template <> +Maybe Endpoint::OptionsObject::SetOption( + const Local& object, + const Local& name, + bool Endpoint::Options::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + if (value->IsUndefined()) return Just(false); + CHECK(value->IsBoolean()); + options_.*member = value->IsTrue(); + return Just(true); +} + +void Endpoint::OptionsObject::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + + OptionsObject* options = + new OptionsObject(env, args.This(), Endpoint::Options{}); + options->options().GenerateResetTokenSecret(); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + SocketAddressBase* address; + ASSIGN_OR_RETURN_UNWRAP(&address, args[0]); + + options->options().local_address = *address->address(); + + if (LIKELY(args[1]->IsObject())) { + auto& state = BindingState::Get(env); + Local object = args[1].As(); + if (UNLIKELY(options + ->SetOption(object, + state.retry_token_expiration_string(), + &Endpoint::Options::retry_token_expiration) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.token_expiration_string(), + &Endpoint::Options::token_expiration) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_window_override_string(), + &Endpoint::Options::max_window_override) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_stream_window_override_string(), + &Endpoint::Options::max_stream_window_override) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_connections_per_host_string(), + &Endpoint::Options::max_connections_per_host) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_connections_total_string(), + &Endpoint::Options::max_connections_total) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_stateless_resets_string(), + &Endpoint::Options::max_stateless_resets) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.address_lru_size_string(), + &Endpoint::Options::address_lru_size) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.retry_limit_string(), + &Endpoint::Options::retry_limit) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.max_payload_size_string(), + &Endpoint::Options::max_payload_size) + .IsNothing()) || + UNLIKELY( + options + ->SetOption(object, + state.unacknowledged_packet_threshold_string(), + &Endpoint::Options::unacknowledged_packet_threshold) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.validate_address_string(), + &Endpoint::Options::validate_address) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.disable_stateless_reset_string(), + &Endpoint::Options::disable_stateless_reset) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.rx_packet_loss_string(), + &Endpoint::Options::rx_loss) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.tx_packet_loss_string(), + &Endpoint::Options::tx_loss) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.cc_algorithm_string(), + &Endpoint::Options::cc_algorithm) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.ipv6_only_string(), + &Endpoint::Options::ipv6_only) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.udp_receive_buffer_size_string(), + &Endpoint::Options::udp_receive_buffer_size) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.udp_send_buffer_size_string(), + &Endpoint::Options::udp_send_buffer_size) + .IsNothing()) || + UNLIKELY(options + ->SetOption(object, + state.udp_ttl_string(), + &Endpoint::Options::udp_ttl) + .IsNothing())) { + // The if block intentionally does nothing. The code is structured like + // this to shortcircuit if any of the SetOptions() returns Nothing. + } + } +} + +void Endpoint::OptionsObject::GenerateResetTokenSecret( + const FunctionCallbackInfo& args) { + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args.Holder()); + options->options().GenerateResetTokenSecret(); +} + +void Endpoint::OptionsObject::SetResetTokenSecret( + const FunctionCallbackInfo& args) { + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args.Holder()); + + crypto::ArrayBufferOrViewContents secret(args[0]); + CHECK_EQ(secret.size(), kTokenSecretLen); + memcpy(options->options().reset_token_secret, secret.data(), secret.size()); +} + +Endpoint::OptionsObject::OptionsObject(Environment* env, + Local object, + Endpoint::Options options) + : BaseObject(env, object), options_(options) { + MakeWeak(); +} + +void Endpoint::OptionsObject::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h new file mode 100644 index 00000000000000..8251ac399dde4b --- /dev/null +++ b/src/quic/endpoint.h @@ -0,0 +1,499 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crypto.h" +#include "quic.h" +#include "session.h" + +namespace node { +namespace quic { + +#define V(_, name, __) uint64_t name; +struct EndpointStats final { + ENDPOINT_STATS(V) +}; +#undef V + +struct EndpointStatsTraits final { + using Stats = EndpointStats; + using Base = StatsBase; + + template + static void ToString(const Stats& stats, Fn&& add_field) { +#define V(_, id, desc) add_field(desc, stats.id); + ENDPOINT_STATS(V) +#undef V + } +}; + +using EndpointStatsBase = StatsBase; + +class Endpoint final : public EndpointStatsBase, public AsyncWrap { + public: + // Endpoint configuration options + struct Options final : public MemoryRetainer { + // The local socket address to which the UDP port will be bound. The port + // may be 0 to have Node.js select an available port. IPv6 or IPv4 addresses + // may be used. When using IPv6, dual mode will be supported by default. + SocketAddress local_address; + + // Retry tokens issued by the Endpoint are time-limited. By default, retry + // tokens expire after DEFAULT_RETRYTOKEN_EXPIRATION *seconds*. This is an + // arbitrary choice that is not mandated by the QUIC specification; so we + // can choose any value that makes sense here. Retry tokens are sent to the + // client, which echoes them back to the server in a subsequent set of + // packets, which means the expiration must be set high enough to allow a + // reasonable round-trip time for the session TLS handshake to complete. + uint64_t retry_token_expiration = DEFAULT_RETRYTOKEN_EXPIRATION; + + // Tokens issued using NEW_TOKEN are time-limited. By default, tokens expire + // after DEFAULT_TOKEN_EXPIRATION *seconds*. + uint64_t token_expiration = DEFAULT_TOKEN_EXPIRATION; + + // The max_window_override and max_stream_window_override parameters + // determine the maximum flow control window sizes that will be used. + // Setting these at zero causes ngtcp2 to use defaults, which is ideal. + // Setting things to any other value will disable the automatic flow control + // management, which is already optimized. Settings these should be rare, + // and should only be done if there's a really good reason. + uint64_t max_window_override = 0; + uint64_t max_stream_window_override = 0; + + // Each Endpoint places limits on the number of concurrent connections from + // a single host, and the total number of concurrent connections allowed as + // a whole. These are set to fairly modest, and arbitrary defaults. We can + // set these to whatever we'd like. + uint64_t max_connections_per_host = DEFAULT_MAX_CONNECTIONS_PER_HOST; + uint64_t max_connections_total = DEFAULT_MAX_CONNECTIONS; + + // A stateless reset in QUIC is a discrete mechanism that one endpoint can + // use to communicate to a peer that it has lost whatever state it + // previously held about a session. Because generating a stateless reset + // consumes resources (even very modestly), they can be a DOS vector in + // which a malicious peer intentionally sends a large number of stateless + // reset eliciting packets. To protect against that risk, we limit the + // number of stateless resets that may be generated for a given remote host + // within a window of time. This is not mandated by QUIC, and the limit is + // arbitrary. We can set it to whatever we'd like. + uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS; + + // For tracking the number of connections per host, the number of stateless + // resets that have been sent, and tracking the path verification status of + // a remote host, we maintain an LRU cache of the most recently seen hosts. + // The address_lru_size parameter determines the size of that cache. The + // default is set modestly at 10 times the default max connections per host. + uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; + + // Similar to stateless resets, we enforce a limit on the number of retry + // packets that can be generated and sent for a remote host. Generating + // retry packets consumes a modest amount of resources and it's fairly + // trivial for a malcious peer to trigger generation of a large number of + // retries, so limiting them helps prevent a DOS vector. + uint64_t retry_limit = DEFAULT_MAX_RETRY_LIMIT; + + // The max_payload_size is the maximum size of a serialized QUIC packet. It + // should always be set small enough to fit within a single MTU without + // fragmentation. The default is set by the QUIC specification at 1200. This + // value should not be changed unless you know for sure that the entire path + // supports a given MTU without fragmenting at any point in the path. + uint64_t max_payload_size = kDefaultMaxPacketLength; + + // The unacknowledged_packet_threshold is the maximum number of + // unacknowledged packets that an ngtcp2 session will accumulate before + // sending an acknowledgement. Setting this to 0 uses the ngtcp2 defaults, + // which is what most will want. The value can be changed to fine tune some + // of the performance characteristics of the session. This should only be + // changed if you have a really good reason for doing so. + uint64_t unacknowledged_packet_threshold = 0; + + // The validate_address parameter instructs the Endpoint to perform explicit + // address validation using retry tokens. This is strongly recommended and + // should only be disabled in trusted, closed environments as a performance + // optimization. + bool validate_address = true; + + // The stateless reset mechanism can be disabled. This should rarely ever be + // needed, and should only ever be done in trusted, closed environments as a + // performance optimization. + bool disable_stateless_reset = false; + + // The rx_loss and tx_loss parameters are debugging tools that allow the + // Endpoint to simulate random packet loss. The value for each parameter is + // a value between 0.0 and 1.0 indicating a probability of packet loss. Each + // time a packet is sent or received, the packet loss bit is calculated and + // if true, the packet is silently dropped. This should only ever be used + // for testing and debugging. There is never a reason why rx_loss and + // tx_loss should ever be used in a production system. + double rx_loss = 0.0; + double tx_loss = 0.0; + + // There are several common congestion control algorithms that ngtcp2 uses + // to determine how it manages the flow control window: RENO, CUBIC, BBR, + // and BBR2. The details of how each works is not relevant here. The choice + // of which to use by default is arbitrary and we can choose whichever we'd + // like. Additional performance profiling will be needed to determine which + // is the better of the two for our needs. + ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC; + + // By default, when Node.js starts, it will generate a reset_token_secret at + // random. This is a secret used in generating stateless reset tokens. In + // order for stateless reset to be effective, however, it is necessary to + // use a deterministic secret that persists across ngtcp2 endpoints and + // sessions. + uint8_t reset_token_secret[NGTCP2_STATELESS_RESET_TOKENLEN]; + + // When the local_address specifies an IPv6 local address to bind to, the + // ipv6_only parameter determines whether dual stack mode (supporting both + // IPv6 and IPv4) transparently is supported. This sets the UV_UDP_IPV6ONLY + // flag on the underlying uv_udp_t. + bool ipv6_only = false; + + uint32_t udp_receive_buffer_size = 0; + uint32_t udp_send_buffer_size = 0; + + // The UDP TTL configuration is the number of network hops a packet will be + // forwarded through. The default is 64. The value is in the range 1 to 255. + // Setting to 0 uses the default. + uint8_t udp_ttl = 0; + + Options(); + + void GenerateResetTokenSecret(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Endpoint::Config) + SET_SELF_SIZE(Options) + }; + + class OptionsObject : public BaseObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static void New(const v8::FunctionCallbackInfo& args); + static void GenerateResetTokenSecret( + const v8::FunctionCallbackInfo& args); + static void SetResetTokenSecret( + const v8::FunctionCallbackInfo& args); + + OptionsObject(Environment* env, + v8::Local object, + Endpoint::Options config); + + inline const Endpoint::Options& options() const { return options_; } + inline Endpoint::Options& options() { return options_; } + + inline operator const Endpoint::Options&() const { return options_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint::OptionsObject) + SET_SELF_SIZE(OptionsObject) + + private: + template + v8::Maybe SetOption(const v8::Local& object, + const v8::Local& name, + T Endpoint::Options::*member); + + Endpoint::Options options_; + }; + + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static BaseObjectPtr Create(Environment* env, + const Endpoint::Options& config); + + Endpoint(Environment* env, + v8::Local object, + const Endpoint::Options& options); + ~Endpoint() override; + + inline const Options& options() const { return options_; } + + // While the busy flag is set, the Endpoint will reject all initial packets + // with a SERVER_BUSY response, even if there are available listening + // Endpoints. This allows us to build a circuit breaker directly in to the + // implementation, explicitly signaling that the server is blocked when + // activity is high. + void MarkAsBusy(bool on = true); + + // Use the endpoint's token secret to generate a new token. + v8::Maybe GenerateNewToken(quic_version version, + uint8_t* token, + const SocketAddress& remote_address); + + // Session Management + + void AddSession(const CID& cid, BaseObjectPtr session); + void RemoveSession(const CID& cid, const SocketAddress& address); + BaseObjectPtr FindSession(const CID& cid); + // A single session may be associated with multiple CIDs. + // AssociateCID registers the mapping both in the Endpoint and the inner + // Endpoint. + void AssociateCID(const CID& cid, const CID& scid); + void DisassociateCID(const CID& cid); + void DisassociateStatelessResetToken(const StatelessResetToken& token); + // Associates a given stateless reset token with the session. This allows + // stateless reset tokens to be recognized and dispatched to the proper + // Endpoint and Session for processing. + void AssociateStatelessResetToken(const StatelessResetToken& token, + Session* session); + + // Packets... + + void Send(BaseObjectPtr packet); + + // Generates and sends a retry packet. This is terminal for the connection. + // Retry packets are used to force explicit path validation by issuing a token + // to the peer that it must thereafter include in all subsequent initial + // packets. Upon receiving a retry packet, the peer must termination it's + // initial attempt to establish a connection and start a new attempt. + // + // Retry packets will only ever be generated by QUIC servers, and only if the + // QuicSocket is configured for explicit path validation. There is no way for + // a client to force a retry packet to be created. However, once a client + // determines that explicit path validation is enabled, it could attempt to + // DOS by sending a large number of malicious initial packets to intentionally + // ellicit retry packets (It can do so by intentionally sending initial + // packets that ignore the retry token). To help mitigate that risk, we limit + // the number of retries we send to a given remote endpoint. + void SendRetry(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address); + + // Sends a version negotiation packet. This is terminal for the connection and + // is sent only when a QUIC packet isreceived for an unsupported Node.js + // version.It is possible that a malicious packet triggered thisso we need to + // be careful not to commit too many resources.Currently, we only support one + // QUIC version at a time. + void SendVersionNegotiation(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address); + + // Possibly generates and sends a stateless reset packet. This is terminal for + // the connection. It is possible that a malicious packet triggered this so we + // need to be careful not to commit too many resources. + bool SendStatelessReset(const CID& cid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + size_t source_len); + + // Shutdown a connection prematurely, before a Session is created. This should + // only be called at the start of a session before the crypto keys have been + // established. + void SendImmediateConnectionClose(const quic_version version, + const CID& dcid, + const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + QuicError error); + + bool Listen(const Session::Options& options); + BaseObjectPtr Connect(const SocketAddress& remote_address, + const Session::Options& options, + SessionTicket* sessionTicket); + + v8::Maybe local_address() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint); + SET_SELF_SIZE(Endpoint); + + enum class CloseContext { + CLOSE, + BIND_FAILURE, + START_FAILURE, + RECEIVE_FAILURE, + SEND_FAILURE, + LISTEN_FAILURE, + }; + + private: + struct State final { +#define V(_, name, type) type name; + ENDPOINT_STATE(V) +#undef V + }; + + class UDP final : public MemoryRetainer { + public: + UDP(Environment* env, Endpoint* endpoint); + inline ~UDP() override; + + int Bind(const Endpoint::Options& config); + void Ref(); + void Unref(); + int Start(); + void Stop(); + void Close(); + v8::Maybe local_address() const; + int Send(BaseObjectPtr req); + + bool is_closed() const; + operator bool() const; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint::UDP) + SET_SELF_SIZE(UDP) + + private: + class Impl; + + static void CleanupHook(void* data); + + BaseObjectPtr impl_; + }; + + struct SocketAddressInfoTraits final { + struct Type final { + size_t active_connections; + size_t reset_count; + size_t retry_count; + uint64_t timestamp; + bool validated; + }; + + static bool CheckExpired(const SocketAddress& address, const Type& type); + static void Touch(const SocketAddress& address, Type* type); + }; + + using CallbackScope = CallbackScopeBase; + + inline bool is_closed() const { + return !udp_; + } + inline bool is_closing() const { + return state_->closing; + } + inline bool is_listening() const { + return state_->listening; + } + + bool Start(); + + // Destroy the endpoint if... + // * There are no sessions, + // * There are no sent packets with pending done callbacks, and + // * We're not listening for new initial packets. + void MaybeDestroy(); + + void Destroy(CloseContext context = CloseContext::CLOSE, int status = 0); + + void CloseGracefully(); + + void Release(); + + void OnSendDone(int status); + + // JavaScript Callouts + void EmitNewSession(const BaseObjectPtr& session); + void EmitEndpointDone(); + void EmitError(CloseContext context, int status); + + void IncrementSocketAddressCounter(const SocketAddress& address); + void DecrementSocketAddressCounter(const SocketAddress& address); + + void Ref(); + void Unref(); + + // JavaScript API + + // Create a new Endpoint instance. `createEndpoint()` is exposed as a method + // on the internalBinding('quic') object. + // @param Endpoint::Options options - Options to configure the Endpoint. + static void CreateEndpoint(const v8::FunctionCallbackInfo& args); + + // Methods on the Endpoint instance: + + // Create a new client Session on this endpoint. + // @param node::SocketAddress local_address - The local address to bind to. + // @param Session::Options options - Options to configure the Session. + // @param v8::ArrayBufferView session_ticket - The session ticket to use for + // the Session. + // @param v8::ArrayBufferView remote_transport_params - The remote transport + // params. + static void DoConnect(const v8::FunctionCallbackInfo& args); + + // Start listening as a QUIC server + // @param Session::Options options - Options to configure the Session. + static void DoListen(const v8::FunctionCallbackInfo& args); + + // Mark the Endpoint as busy, temporarily pausing handling of new initial + // packets. + // @param bool on - If true, mark the Endpoint as busy. + static void MarkBusy(const v8::FunctionCallbackInfo& args); + + // DoCloseGracefully is the signal that endpoint should close. Any packets + // that are already in the queue or in flight will be allowed to finish, but + // the EndpoingWrap will be otherwise no longer able to receive or send + // packets. + static void DoCloseGracefully( + const v8::FunctionCallbackInfo& args); + + // Get the local address of the Endpoint. + // @return node::SocketAddress - The local address of the Endpoint. + static void LocalAddress(const v8::FunctionCallbackInfo& args); + + // Ref() causes a listening Endpoint to keep the event loop active. + static void Ref(const v8::FunctionCallbackInfo& args); + + // Unref() allows the event loop to close even if the Endpoint is listening. + static void Unref(const v8::FunctionCallbackInfo& args); + + Session::Config NewSessionConfig(const CID& scid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + quic_version version, + ngtcp2_crypto_side side); + + void Receive(size_t size, const uv_buf_t& buf, const SocketAddress& addr); + + // Internal fields + uint8_t token_secret_[kTokenSecretLen]; + + AliasedStruct state_; + Options options_; + UDP udp_; + + // Set when the endpoint is configured to listen. + Session::Options server_options_; + + // Session management + StatelessResetToken::Map token_map_; + CID::Map> sessions_; + CID::Map dcid_to_scid_; + + SocketAddressLRU addrLRU_; + + CloseContext close_context_ = CloseContext::CLOSE; + int close_status_ = 0; + + friend class UDP; + friend class Packet; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/http3.cc b/src/quic/http3.cc new file mode 100644 index 00000000000000..3a2dbbdfad9d93 --- /dev/null +++ b/src/quic/http3.cc @@ -0,0 +1,791 @@ +#include "http3.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "defs.h" +#include "endpoint.h" +#include "quic.h" +#include "session.h" + +namespace node { + +using v8::Just; +using v8::Local; +using v8::Maybe; + +namespace quic { + +// ====================================================================================== +// Http3RcBufferPointerTraits + +void Http3RcBufferPointerTraits::inc(rcbuf_t* buf) { + nghttp3_rcbuf_incref(buf); +} + +void Http3RcBufferPointerTraits::dec(rcbuf_t* buf) { + nghttp3_rcbuf_decref(buf); +} + +Http3RcBufferPointerTraits::vector_t Http3RcBufferPointerTraits::get_vec( + const rcbuf_t* buf) { + return nghttp3_rcbuf_get_buf(buf); +} + +bool Http3RcBufferPointerTraits::is_static(const rcbuf_t* buf) { + return nghttp3_rcbuf_is_static(buf); +} + +// nghttp3 uses a numeric identifier for a large number of known HTTP header +// names. These allow us to use static strings for those rather than allocating +// new strings all of the time. The list of strings supported is included in +// node_http_common.h +#define V1(name, value) \ + case NGHTTP3_QPACK_TOKEN__##name: \ + return value; +#define V2(name, value) \ + case NGHTTP3_QPACK_TOKEN_##name: \ + return value; +const char* Http3HeaderTraits::ToHttpHeaderName(int32_t token) { + switch (token) { + default: + // Fall through + case -1: + return nullptr; + HTTP_SPECIAL_HEADERS(V1) + HTTP_REGULAR_HEADERS(V2) + } +} +#undef V1 +#undef V2 + +// ====================================================================================== +// Http3Application + +Http3Application::Http3Application(Session* session, const Options& options) + : Session::Application(session, options), + alloc_info_(BindingState::Get(session->env())) { + DEBUG(session, "Using Http3Application"); + session->state_->priority_supported = 1; +} + +void Http3Application::CreateConnection() { + nghttp3_conn* conn; + + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.max_field_section_size = options().max_field_section_size; + settings.qpack_blocked_streams = options().qpack_blocked_streams; + settings.qpack_encoder_max_dtable_capacity = + options().qpack_encoder_max_dtable_capacity; + settings.qpack_max_dtable_capacity = options().qpack_max_dtable_capacity; + + switch (session().crypto_context().side()) { + case NGTCP2_CRYPTO_SIDE_CLIENT: + CHECK_EQ(nghttp3_conn_client_new( + &conn, &callbacks_, &settings, &alloc_info_, this), + 0); + break; + case NGTCP2_CRYPTO_SIDE_SERVER: + CHECK_EQ(nghttp3_conn_server_new( + &conn, &callbacks_, &settings, &alloc_info_, this), + 0); + break; + } + + CHECK_NOT_NULL(conn); + connection_.reset(conn); +} + +bool Http3Application::Start() { + // The Session must allow for at least three local unidirectional streams. + // This number is fixed by the http3 specification and represent the control + // stream and two qpack management streams. + if (session().max_local_streams_uni() < 3) return false; + + CreateConnection(); + + if (session().is_server()) + nghttp3_conn_set_max_client_streams_bidi( + connection_.get(), session().max_local_streams_bidi()); + + return CreateAndBindControlStream() && CreateAndBindQPackStreams(); +} + +bool Http3Application::ReceiveStreamData(Stream* stream, + ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + DEBUG_ARGS(&session(), + "Receiving %" PRIu64 " bytes for h3 stream %" PRIu64 "%s %s", + datalen, + stream->id(), + flags.fin ? " (fin)" : "", + flags.early ? " (early)" : ""); + ssize_t nread = nghttp3_conn_read_stream( + connection_.get(), stream->id(), data, datalen, flags.fin ? 1 : 0); + + if (nread < 0) { + DEBUG_ARGS( + &session(), "Failure to read h3 stream data [%" PRIi64 "]", nread); + return false; + } + + session().ExtendStreamOffset(stream->id(), nread); + session().ExtendOffset(nread); + + return true; +} + +void Http3Application::AcknowledgeStreamData(Stream* stream, + uint64_t offset, + size_t datalen) { + if (nghttp3_conn_add_ack_offset(connection_.get(), stream->id(), datalen) != + 0) + DEBUG(&session(), "Failure to acknowledge h3 stream data"); +} + +void Http3Application::SetStreamPriority(Stream* stream, + StreamPriority priority, + StreamPriorityFlags flags) { + if (stream->direction() == Direction::BIDIRECTIONAL && + stream->origin() == Stream::Origin::CLIENT) { + nghttp3_pri pri = { + /* .urgency = */ static_cast(priority), + /* .inc = */ flags == StreamPriorityFlags::NON_INCREMENTAL ? 0 : 1, + }; + CHECK_EQ(nghttp3_conn_set_stream_priority(connection(), stream->id(), &pri), + 0); + } +} + +Session::Application::StreamPriority Http3Application::GetStreamPriority( + Stream* stream) { + // nghttp3 requires that the get_stream_priority function can only be called + // by the server. + if (!session().is_server()) return StreamPriority::DEFAULT; + + nghttp3_pri pri; + CHECK_EQ(nghttp3_conn_get_stream_priority(connection(), &pri, stream->id()), + 0); + // We're only interested in the urgency field. The incremental flag is only + // relevant when setting. + return static_cast(pri.urgency); +} + +bool Http3Application::CanAddHeader(size_t current_count, + size_t current_headers_length, + size_t this_header_length) { + // We cannot add the header if we've either reached + // * the max number of header pairs or + // * the max number of header bytes + return current_count < options().max_header_pairs && + current_headers_length + this_header_length <= + options().max_header_length; +} + +bool Http3Application::BlockStream(stream_id id) { + nghttp3_conn_block_stream(connection_.get(), id); + Application::BlockStream(id); + return true; +} + +void Http3Application::ExtendMaxStreams(EndpointLabel label, + Direction direction, + uint64_t max_streams) { + switch (label) { + case EndpointLabel::LOCAL: { + return; + } + case EndpointLabel::REMOTE: { + switch (direction) { + case Direction::BIDIRECTIONAL: + ngtcp2_conn_extend_max_streams_bidi(session().connection(), + max_streams); + return; + case Direction::UNIDIRECTIONAL: + ngtcp2_conn_extend_max_streams_uni(session().connection(), + max_streams); + return; + } + UNREACHABLE(); + } + } + UNREACHABLE(); +} + +void Http3Application::ExtendMaxStreamData(Stream* stream, uint64_t max_data) { + nghttp3_conn_unblock_stream(connection_.get(), stream->id()); +} + +void Http3Application::ResumeStream(stream_id id) { + nghttp3_conn_resume_stream(connection_.get(), id); +} + +void Http3Application::SetSessionTicketAppData( + const SessionTicketAppData& app_data) { + // There's currently nothing to store but we might do so later. +} + +SessionTicketAppData::Status Http3Application::GetSessionTicketAppData( + const SessionTicketAppData& app_data, SessionTicketAppData::Flag flag) { + // There's currently nothing stored here but we might do so later. + return flag == SessionTicketAppData::Flag::STATUS_RENEW + ? SessionTicketAppData::Status::TICKET_USE_RENEW + : SessionTicketAppData::Status::TICKET_USE; +} + +void Http3Application::StreamClose(Stream* stream, Maybe error) { + error_code code = NGHTTP3_H3_NO_ERROR; + if (error.IsJust()) { + auto err = error.FromJust(); + CHECK_EQ(err.type(), QuicError::Type::APPLICATION); + code = err.code(); + } + + int rv = nghttp3_conn_close_stream(connection_.get(), stream->id(), code); + // If the call is successful, Http3Application::OnStreamClose callback will + // be invoked when the stream is ready to be closed. We'll handle destroying + // the actual Stream object there. + if (rv == 0) return; + + switch (rv) { + case NGHTTP3_ERR_STREAM_NOT_FOUND: + ExtendMaxStreams(EndpointLabel::REMOTE, stream->direction(), 1); + return; + } + + session().SetLastError( + QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(rv))); + session().Close(); +} + +void Http3Application::StreamReset(Stream* stream, + uint64_t final_size, + QuicError error) { + DEBUG_ARGS(&session(), + "Application resetting stream %" PRIi64 " with final size %" PRIu64 + " [%s]", + stream->id(), + final_size, + error); + // We are shutting down the readable side of the local stream here. + CHECK_EQ(nghttp3_conn_shutdown_stream_read(connection_.get(), stream->id()), + 0); + stream->ReceiveResetStream(final_size, error); +} + +void Http3Application::StreamStopSending(Stream* stream, QuicError error) { + DEBUG_ARGS(&session(), + "Application stop sending stream %" PRIi64 " [%s]", + stream->id(), + error); + stream->ReceiveStopSending(error); +} + +bool Http3Application::SendHeaders(stream_id id, + HeadersKind kind, + const v8::Local& headers, + HeadersFlags flags) { + Session::SendPendingDataScope send_scope(&session()); + Http3Headers nva(env(), headers); + + switch (kind) { + case HeadersKind::INFO: { + return nghttp3_conn_submit_info( + connection_.get(), id, nva.data(), nva.length()) == 0; + break; + } + case HeadersKind::INITIAL: { + static constexpr nghttp3_data_reader reader = { + Http3Application::OnReadData}; + const nghttp3_data_reader* reader_ptr = nullptr; + + // If the terminal flag is set, that means that we + // know we're only sending headers and no body and + // the stream should writable side should be closed + // immediately because there is no nghttp3_data_reader + // provided. + if (flags != HeadersFlags::TERMINAL) reader_ptr = &reader; + + if (session().is_server()) { + return nghttp3_conn_submit_response( + connection_.get(), id, nva.data(), nva.length(), reader_ptr); + } else { + return nghttp3_conn_submit_request(connection_.get(), + id, + nva.data(), + nva.length(), + reader_ptr, + nullptr) == 0; + } + break; + } + case HeadersKind::TRAILING: { + return nghttp3_conn_submit_trailers( + connection_.get(), id, nva.data(), nva.length()) == 0; + break; + } + } + + return false; +} + +bool Http3Application::ShouldSetFin(const StreamData& stream_data) { + return stream_data.id > -1 && !is_control_stream(stream_data.id) && + stream_data.fin == 1; +} + +int Http3Application::GetStreamData(StreamData* stream_data) { + ssize_t ret = 0; + if (connection_ && session().max_data_left()) { + ret = nghttp3_conn_writev_stream( + connection_.get(), + &stream_data->id, + &stream_data->fin, + reinterpret_cast(stream_data->data), + sizeof(stream_data->data)); + if (ret < 0) + return static_cast(ret); + else + stream_data->remaining = stream_data->count = static_cast(ret); + } + if (stream_data->id > -1) { + DEBUG_ARGS(&session(), + "Selected %" PRIi64 " buffers for stream %" PRIi64 "%s", + stream_data->count, + stream_data->id, + stream_data->fin == 1 ? " (fin)" : ""); + } + return 0; +} + +bool Http3Application::StreamCommit(StreamData* stream_data, size_t datalen) { + int err = nghttp3_conn_add_write_offset(connection_.get(), stream_data->id, datalen); + if (err != 0) { + session().SetLastError( + QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(err))); + return false; + } + return true; +} + +BaseObjectPtr Http3Application::FindOrCreateStream( + stream_id id, void* stream_user_data) { + if (stream_user_data != nullptr) { + return BaseObjectPtr(static_cast(stream_user_data)); + } + + auto stream = session().FindStream(id); + if (!stream) { + if (!session().can_create_streams()) return BaseObjectPtr(); + + stream = session().CreateStream(id); + if (LIKELY(stream)) + nghttp3_conn_set_stream_user_data(connection_.get(), id, stream.get()); + } + + CHECK(stream); + return stream; +} + +bool Http3Application::CreateAndBindControlStream() { + auto stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!stream) return false; + + DEBUG_ARGS(&session(), + "Open stream %" PRIi64 " and bind as h3 control stream", + stream->id()); + return nghttp3_conn_bind_control_stream(connection_.get(), stream->id()) == 0; +} + +bool Http3Application::CreateAndBindQPackStreams() { + auto enc_stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!enc_stream) return false; + + auto dec_stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!dec_stream) return false; + + DEBUG_ARGS(&session(), + "Open streams %" PRIi64 " and %" PRIi64 " as h3 qpack streams", + qpack_enc_stream_id_, + qpack_dec_stream_id_); + return nghttp3_conn_bind_qpack_streams(connection_.get(), + qpack_enc_stream_id_, + qpack_dec_stream_id_) == 0; +} + +void Http3Application::ScheduleStream(stream_id id) {} + +void Http3Application::UnscheduleStream(stream_id id) {} + +ssize_t Http3Application::ReadData(stream_id id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags) { + BaseObjectPtr stream = session().FindStream(id); + + DEBUG_ARGS(&session(), "Reading data for h3 stream %" PRIi64, id); + + // This case really shouldn't happen but we're going to safely handle it + // anyway just in case. We interpret the lack of a stream as a stream that has + // been closed. The only way to get here would be if the stream was never + // created or the stream was destroyed, in either case, there's no data. + if (!stream) { + *pflags |= NGHTTP3_DATA_FLAG_EOF; + return 0; + } + + ssize_t ret = NGHTTP3_ERR_WOULDBLOCK; + + auto next = + [&](int status, const ngtcp2_vec* data, size_t count, bob::Done done) { + CHECK_LE(count, veccnt); + + switch (status) { + case bob::Status::STATUS_BLOCK: + // Fall through + case bob::Status::STATUS_WAIT: + // Fall through + case bob::Status::STATUS_EOS: + return; + case bob::Status::STATUS_END: + *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (UNLIKELY(stream->might_send_trailers())) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + // We let the stream know that it can send trailers now. The + // stream will not be ended until the trailers are sent. + stream->ReadyForTrailers(); + } + break; + } + + ret = count; + size_t numbytes = + nghttp3_vec_len(reinterpret_cast(data), count); + std::move(done)(numbytes); + + DEBUG_ARGS(&session(), + "Sending %" PRIu64 " bytes for h3 stream %" PRIi64, + numbytes, + id); + }; + + CHECK_GE( + stream->Pull(std::move(next), + // Set OPTIONS_END here because nghttp3 takes over + // responsibility for ensuring the data all gets written out. + bob::Options::OPTIONS_END | bob::Options::OPTIONS_SYNC, + reinterpret_cast(vec), + veccnt, + kMaxVectorCount), + 0); + + return ret; +} + +// ====================================================================================== +// nghttp3 callbacks +const nghttp3_callbacks Http3Application::callbacks_ = {OnAckedStreamData, + OnStreamClose, + OnReceiveData, + OnDeferredConsume, + OnBeginHeaders, + OnReceiveHeader, + OnEndHeaders, + OnBeginTrailers, + OnReceiveTrailer, + OnEndTrailers, + OnStopSending, + OnEndStream, + OnResetStream, + OnShutdown}; + +Http3Application& GetApplication(nghttp3_conn* conn, void* conn_user_data) { + Http3Application* app = static_cast(conn_user_data); + CHECK_EQ(app->connection(), conn); + return *app; +} + +int Http3Application::OnAckedStreamData(nghttp3_conn* conn, + stream_id id, + uint64_t datalen, + void* conn_user_data, + void* stream_user_data) { + if (stream_user_data == nullptr) return 0; + auto& app = GetApplication(conn, conn_user_data); + // If we don't find the stream, it either has never been created or was + // destroyed. In either case, we just ignore the acknowledgement. + auto stream = app.session().FindStream(id); + if (stream) app.AcknowledgeStreamData(stream.get(), 0, datalen); + return 0; +} + +int Http3Application::OnStreamClose(nghttp3_conn* conn, + stream_id id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (stream) { + DEBUG_ARGS(&app.session(), + "H3 stream %" PRIi64 " closed with error code %" PRIu64, + id, + app_error_code); + auto stream = static_cast(stream_user_data); + auto direction = stream->direction(); + stream->Destroy(Just(QuicError::ForApplication(app_error_code))); + app.ExtendMaxStreams(EndpointLabel::REMOTE, direction, 1); + } + return 0; +} + +int Http3Application::OnReceiveData(nghttp3_conn* conn, + stream_id id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data) { + // At this point, the QUIC stack should have already created the Stream, so if + // it's not found here, something has gone wrong (likely the stream was + // destroyed). Let's not ignore the case and fail the callback if that + // happens. + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + DEBUG_ARGS(&app.session(), + "Received %" PRIu64 " bytes for h3 stream %" PRIi64, + datalen, + id); + stream->ReceiveData(ReceiveStreamDataFlags{}, data, datalen, 0); + return 0; +} + +int Http3Application::OnDeferredConsume(nghttp3_conn* conn, + stream_id id, + size_t consumed, + void* conn_user_data, + void* stream_user_data) { + // This is a notification from nghttp3 to let ngtcp2 know that a certain + // amount of data has been consumed. ngtcp2 uses this to update internal + // accounting for flow control. + auto& app = GetApplication(conn, conn_user_data); + app.session().ExtendStreamOffset(id, consumed); + app.session().ExtendOffset(consumed); + return 0; +} + +int Http3Application::OnBeginHeaders(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data) { + // The QUIC layer should have created the Stream already. If it doesn't exist + // here, something has gone wrong. + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + DEBUG_ARGS(&app.session(), "Begin headers block for h3 stream %" PRIi64, id); + stream->BeginHeaders(HeadersKind::INITIAL); + return 0; +} + +int Http3Application::OnEndHeaders(nghttp3_conn* conn, + stream_id id, + int fin, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + DEBUG_ARGS(&app.session(), "End headers block for h3 stream %" PRIi64, id); + stream->EndHeaders(); + if (fin != 0) { + // The stream is done. There's no more data to receive! + stream->ReceiveData(ReceiveStreamDataFlags{/* .fin = */ true, + /* .early = */ false}, + nullptr, + 0, + 0); + } + return 0; +} + +int Http3Application::OnReceiveHeader(nghttp3_conn* conn, + stream_id id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + + // Protect against zero-length headers (zero-length if either the + // name or value are zero-length). Such headers are simply ignored. + if (!Http3Header::IsZeroLength(name, value)) { + if (token == NGHTTP3_QPACK_TOKEN__STATUS) { + nghttp3_vec vec = nghttp3_rcbuf_get_buf(value); + if (vec.base[0] == '1') + stream->set_headers_kind(HeadersKind::INFO); + else + stream->set_headers_kind(HeadersKind::INITIAL); + } + stream->AddHeader( + Http3Header(app.session().env(), token, name, value, flags)); + } + return 0; +} + +int Http3Application::OnBeginTrailers(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data) { + // The QUIC layer should have created the Stream already. If it doesn't exist + // here, something has gone wrong. + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + DEBUG_ARGS(&app.session(), "Begin trailers block for h3 stream %" PRIi64, id); + stream->BeginHeaders(HeadersKind::TRAILING); + return 0; +} + +int Http3Application::OnReceiveTrailer(nghttp3_conn* conn, + stream_id id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + + // Protect against zero-length headers (zero-length if either the + // name or value are zero-length). Such headers are simply ignored. + if (!Http3Header::IsZeroLength(name, value)) + stream->AddHeader( + Http3Header(app.session().env(), token, name, value, flags)); + return 0; +} + +int Http3Application::OnEndTrailers(nghttp3_conn* conn, + stream_id id, + int fin, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + DEBUG_ARGS(&app.session(), "End trailers block for h3 stream %" PRIi64, id); + stream->EndHeaders(); + if (fin != 0) { + // The stream is done. There's no more data to receive! + stream->ReceiveData(ReceiveStreamDataFlags{/* .fin = */ true, + /* .early = */ false}, + nullptr, + 0, + 0); + } + return 0; +} + +int Http3Application::OnStopSending(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + // This event can be a bit confusing. When this is triggered, we're being + // asked to *send* a STOP_SENDING frame, not that we received one. + DEBUG_ARGS(&app.session(), "Stop sending for h3 stream %" PRIi64, id); + stream->StopSending(QuicError::ForApplication(app_error_code)); + return 0; +} + +int Http3Application::OnResetStream(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + // Similar to OnStopSending, this event is not that we *received* a + // RESET_STREAM frame, we need to send one. + DEBUG_ARGS(&app.session(), "Reset stream for h3 stream %" PRIi64, id); + stream->ResetStream(QuicError::ForApplication(app_error_code)); + return 0; +} + +int Http3Application::OnEndStream(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data) { + auto& app = GetApplication(conn, conn_user_data); + auto stream = app.session().FindStream(id); + if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE; + + DEBUG_ARGS(&app.session(), "Done receiving data for h3 stream %" PRIi64, id); + + // This callback is invoked when the receiving side of a stream completes. + // Specifically, for HTTP requests, this signals completion of the complete + // HTTP request. For HTTP responses, this signals completion of the complete + // HTTP response. + + stream->ReceiveData(ReceiveStreamDataFlags{/* .fin = */ true, + /* .early = */ false}, + nullptr, + 0, + 0); + + return 0; +} + +int Http3Application::OnShutdown(nghttp3_conn* conn, + stream_id id, + void* conn_user_data) { + // This callback is invoked when we receive a request to gracefully shutdown + // the http3 connection. For client, the id is the stream id of a client + // initiated stream. For server, the id is the stream id of a server initiated + // stream. Once received, the other side is guaranteed not to process any more + // data. + + // On the client side, if id is equal to NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID, + // or on the server if the id is equal to NGHTTP3_SHUSTDOWN_NOTICE_PUSH_ID, + // then this is a request to begin a graceful shutdown. + + // This can be called multiple times but the id can only stay the same or + // *decrease*. + + // Need to determine exactly how to handle. + + return 0; +} + +ssize_t Http3Application::OnReadData(nghttp3_conn* conn, + stream_id id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags, + void* conn_user_data, + void* stream_user_data) { + return GetApplication(conn, conn_user_data).ReadData(id, vec, veccnt, pflags); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/http3.h b/src/quic/http3.h new file mode 100644 index 00000000000000..6d01f33dac3b9f --- /dev/null +++ b/src/quic/http3.h @@ -0,0 +1,248 @@ +#pragma once + +#include "quic/defs.h" +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "quic.h" +#include "session.h" +#include "stream.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +constexpr uint64_t kVarintMax = ((1ull << 62) - 1); +constexpr uint64_t DEFAULT_HTTP3_MAX_PUSHES = 0; +constexpr uint64_t DEFAULT_HTTP3_QPACK_MAX_TABLE_CAPACITY = 4096; +constexpr uint64_t DEFAULT_HTTP3_QPACK_BLOCKED_STREAMS = 100; + +struct Http3RcBufferPointerTraits final { + typedef nghttp3_rcbuf rcbuf_t; + typedef nghttp3_vec vector_t; + static void inc(rcbuf_t* buf); + static void dec(rcbuf_t* buf); + static vector_t get_vec(const rcbuf_t* buf); + static bool is_static(const rcbuf_t* buf); +}; + +struct Http3HeadersTraits final { + typedef nghttp3_nv nv_t; +}; + +using Http3ConnectionPointer = DeleteFnPtr; +using Http3RcBufferPointer = NgRcBufPointer; +using Http3Headers = NgHeaders; + +struct Http3HeaderTraits final { + typedef Http3RcBufferPointer rcbufferpointer_t; + typedef BindingState allocator_t; + + static const char* ToHttpHeaderName(int32_t token); +}; + +using Http3Header = NgHeader; + +class Http3Application; + +class Http3Application final : public Session::Application { + public: + Http3Application(Session* session, const Options& options); + QUIC_NO_COPY_OR_MOVE(Http3Application) + + bool Start() override; + + // Called when the QUIC Session receives data for a stream. This is the + // primary entry point of data into nghttp3 from ngtcp2. + bool ReceiveStreamData(Stream* stream, + ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) override; + + void AcknowledgeStreamData(Stream* stream, + uint64_t offset, + size_t datalen) override; + + bool CanAddHeader(size_t current_count, + size_t current_headers_length, + size_t this_header_length) override; + + void SetStreamPriority( + Stream* stream, + StreamPriority priority = StreamPriority::DEFAULT, + StreamPriorityFlags flags = StreamPriorityFlags::NONE) override; + + StreamPriority GetStreamPriority(Stream* stream) override; + + // Called when the QUIC Session detects that a stream is blocked by flow + // control. This tells nghttp3 not to generate http3 stream data temporarily. + bool BlockStream(stream_id id) override; + + void ResumeStream(stream_id id) override; + + void ExtendMaxStreams(EndpointLabel label, + Direction direction, + uint64_t max_streams) override; + + void ExtendMaxStreamData(Stream* stream, uint64_t max_data) override; + + void SetSessionTicketAppData(const SessionTicketAppData& app_data) override; + + SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag) override; + + // Called by the QUIC Session when a stream is closed. + void StreamClose(Stream* stream, v8::Maybe error) override; + + // Called by the QUIC Session when a stream reset is received from the remote + // peer. + void StreamReset(Stream* stream, + uint64_t final_size, + QuicError error) override; + + // TODO(@jasnell): Need to verify specifically when this is called. + void StreamStopSending(Stream* stream, QuicError error) override; + + bool SendHeaders(stream_id id, + HeadersKind kind, + const v8::Local& headers, + HeadersFlags flags = HeadersFlags::NONE) override; + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Http3Application) + SET_SELF_SIZE(Http3Application) + + inline nghttp3_conn* connection() const { return connection_.get(); } + + protected: + int GetStreamData(StreamData* data) override; + bool StreamCommit(StreamData* data, size_t datalen) override; + bool ShouldSetFin(const StreamData& data) override; + + private: + BaseObjectPtr FindOrCreateStream(stream_id id, + void* stream_user_data = nullptr); + bool CreateAndBindControlStream(); + bool CreateAndBindQPackStreams(); + void CreateConnection(); + void ScheduleStream(stream_id id); + void UnscheduleStream(stream_id id); + + ssize_t ReadData(stream_id id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags); + + inline bool is_control_stream(int64_t stream_id) const { + return stream_id == control_stream_id_ || + stream_id == qpack_dec_stream_id_ || + stream_id == qpack_enc_stream_id_; + } + + nghttp3_mem alloc_info_; + Http3ConnectionPointer connection_; + stream_id control_stream_id_; + stream_id qpack_enc_stream_id_; + stream_id qpack_dec_stream_id_; + + // ==================================================================================== + // nghttp3 callbacks + + static const nghttp3_callbacks callbacks_; + + static int OnAckedStreamData(nghttp3_conn* conn, + stream_id id, + uint64_t datalen, + void* conn_user_data, + void* stream_user_data); + + static int OnStreamClose(nghttp3_conn* conn, + stream_id id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data); + static int OnReceiveData(nghttp3_conn* conn, + stream_id id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data); + static int OnDeferredConsume(nghttp3_conn* conn, + stream_id id, + size_t consumed, + void* conn_user_data, + void* stream_user_data); + + static int OnBeginHeaders(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data); + static int OnReceiveHeader(nghttp3_conn* conn, + stream_id id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data); + static int OnEndHeaders(nghttp3_conn* conn, + stream_id id, + int fin, + void* conn_user_data, + void* stream_user_data); + + static int OnBeginTrailers(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data); + static int OnReceiveTrailer(nghttp3_conn* conn, + stream_id id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data); + static int OnEndTrailers(nghttp3_conn* conn, + stream_id id, + int fin, + void* conn_user_data, + void* stream_user_data); + static int OnStopSending(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data); + static int OnEndStream(nghttp3_conn* conn, + stream_id id, + void* conn_user_data, + void* stream_user_data); + static int OnResetStream(nghttp3_conn* conn, + stream_id id, + error_code app_error_code, + void* conn_user_data, + void* stream_user_data); + static int OnShutdown(nghttp3_conn* conn, int64_t id, void* conn_user_data); + + static ssize_t OnReadData(nghttp3_conn* conn, + stream_id id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags, + void* conn_user_data, + void* stream_user_data); +}; + +} // namespace quic +} // namespace node + +#endif // #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/quic.cc b/src/quic/quic.cc new file mode 100644 index 00000000000000..da7beff21867ff --- /dev/null +++ b/src/quic/quic.cc @@ -0,0 +1,619 @@ + +#include "node.h" +#include "quic/defs.h" +#if NODE_OPENSSL_HAS_QUIC +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crypto.h" +#include "endpoint.h" +#include "quic.h" +#include "session.h" +#include "stream.h" +#endif // NODE_OPENSSL_HAS_QUIC + +namespace node { + +using crypto::EntropySource; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace quic { + +// The internalBinding('quic') will be available even if quic support is not +// enabled. This prevents the internalBinding call from throwing. However, if +// quic is not enabled, the binding will have no exports. + +#if NODE_OPENSSL_HAS_QUIC + +// ====================================================================================== +// BindingState + +constexpr FastStringKey BindingState::type_name; + +BindingState& BindingState::Get(Environment* env) { + return *env->GetBindingData(env->context()); +} + +bool BindingState::Initialize(Environment* env, Local target) { + BindingState* const state = + env->AddBindingData(env->context(), target); + return state != nullptr; +} + +BindingState::BindingState(Environment* env, v8::Local object) + : BaseObject(env, object) { + MakeWeak(); +} + +BindingState::operator ngtcp2_mem() const { + return BindingState::Get(env()).MakeAllocator(); +} + +BindingState::operator nghttp3_mem() const { + ngtcp2_mem allocator = *this; + nghttp3_mem http3_allocator = { + allocator.user_data, + allocator.malloc, + allocator.free, + allocator.calloc, + allocator.realloc, + }; + return http3_allocator; +} + +void BindingState::MemoryInfo(MemoryTracker* tracker) const { +#define V(name, _) tracker->TrackField(#name, name##_callback()); + QUIC_JS_CALLBACKS(V) +#undef V +#define V(name, _) tracker->TrackField(#name, name##_string()); + QUIC_STRINGS(V) +#undef V +} + +void BindingState::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_ngtcp2_memory_, previous_size); +} + +void BindingState::IncreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ += size; +} + +void BindingState::DecreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ -= size; +} + +#define V(name) \ + void BindingState::set_##name##_constructor_template( \ + Local tmpl) { \ + name##_constructor_template_.Reset(env()->isolate(), tmpl); \ + } \ + Local BindingState::name##_constructor_template() const { \ + return PersistentToLocal::Default(env()->isolate(), \ + name##_constructor_template_); \ + } +QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) \ + void BindingState::set_##name##_callback(Local fn) { \ + name##_callback_.Reset(env()->isolate(), fn); \ + } \ + Local BindingState::name##_callback() const { \ + return PersistentToLocal::Default(env()->isolate(), name##_callback_); \ + } +QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, value) \ + Local BindingState::name##_string() const { \ + if (name##_string_.IsEmpty()) \ + name##_string_.Set(env()->isolate(), \ + OneByteString(env()->isolate(), value)); \ + return name##_string_.Get(env()->isolate()); \ + } +QUIC_STRINGS(V) +#undef V + +// ============================================================================= +// CID + +CID::CID(const uint8_t* cid, size_t length) : CID() { + CHECK_EQ(ptr_, &cid_); + CHECK_LE(length, NGTCP2_MAX_CIDLEN); + ngtcp2_cid_init(&cid_, cid, length); +} + +CID::CID(const CID& other) : CID() { + CHECK_EQ(ptr_, &cid_); + ngtcp2_cid_init(&cid_, other.ptr_->data, other.ptr_->datalen); +} + +CID& CID::operator=(const CID& other) { + CHECK_EQ(ptr_, &cid_); + if (this == &other) return *this; + ngtcp2_cid_init(&cid_, other.ptr_->data, other.ptr_->datalen); + return *this; +} + +std::string CID::ToString() const { + std::vector dest(ptr_->datalen * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = + StringBytes::hex_encode(reinterpret_cast(ptr_->data), + ptr_->datalen, + dest.data(), + dest.size()); + return std::string(dest.data(), written); +} + +void CIDFactory::Generate(CID* cid, size_t length) { + Generate(&cid->cid_, length); +} + +CID CIDFactory::Generate(size_t length_hint) { + CID cid; + Generate(&cid, length_hint); + return cid; +} + +void CIDFactory::RandomCIDFactory::Generate(ngtcp2_cid* cid, + size_t length_hint) { + CHECK_LE(length_hint, NGTCP2_MAX_CIDLEN); + if (length_hint == 0) return; + EntropySource(cid->data, length_hint); + cid->datalen = length_hint; +} + +Local CIDFactory::Base::GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.cidfactorybase_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount(Base::kInternalFieldCount); + tmpl->SetClassName(state.cidfactorybase_string()); + state.set_cidfactorybase_constructor_template(tmpl); + } + return tmpl; +} + +CIDFactory::Base::Base(Environment* env, v8::Local object) + : BaseObject(env, object) { + MakeWeak(); +} + +BaseObjectPtr CIDFactory::Base::StrongRef() { + return BaseObjectPtr(this); +} + +// ============================================================================= +// Packet + +Local Packet::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.send_wrap_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = NewFunctionTemplate(env->isolate(), IllegalConstructor); + tmpl->Inherit(PacketReq::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Packet::kInternalFieldCount); + tmpl->SetClassName(state.packetwrap_string()); + state.set_send_wrap_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr Packet::Create(Environment* env, + Endpoint* endpoint, + const SocketAddress& destination, + const char* diagnostic_label, + size_t length) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject( + env, endpoint, obj, destination, length, diagnostic_label); +} + +BaseObjectPtr Packet::Clone() const { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env()) + ->InstanceTemplate() + ->NewInstance(env()->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env(), endpoint_, obj, destination_, data_); +} + +Packet::Packet(Environment* env, + Endpoint* endpoint, + v8::Local object, + const SocketAddress& destination, + std::shared_ptr data) + : PacketReq(env, object, AsyncWrap::PROVIDER_QUICPACKET), + endpoint_(endpoint), + destination_(destination), + data_(std::move(data)) {} + +Packet::Packet(Environment* env, + Endpoint* endpoint, + v8::Local object, + const SocketAddress& destination, + size_t length, + const char* diagnostic_label) + : Packet(env, + endpoint, + object, + destination, + std::make_shared(length, diagnostic_label)) {} + +void Packet::Done(int status) { + CHECK_NOT_NULL(endpoint_); + endpoint_->OnSendDone(status); + handle_.reset(); +} + +Packet::operator uv_buf_t() const { + CHECK(data_); + uv_buf_t buf; + buf.base = reinterpret_cast(data_->ptr_); + buf.len = data_->len_; + return buf; +} + +Packet::operator ngtcp2_vec() const { + CHECK(data_); + ngtcp2_vec vec; + vec.base = data_->ptr_; + vec.len = data_->len_; + return vec; +} + +void Packet::Truncate(size_t len) { + CHECK(data_); + CHECK_LE(len, data_->len_); + data_->len_ = len; +} + +std::string Packet::ToString() const { + if (!data_) return "Packet ()"; + return std::string("Packet (") + data_->diagnostic_label_ + ", " + + std::to_string(data_->len_) + ")"; +} + +Packet::Data::Data(size_t length, const char* diagnostic_label) + : ptr_(length <= kDefaultMaxPacketLength ? data_ : Malloc(length)), + len_(length), + diagnostic_label_(diagnostic_label) {} + +Packet::Data::~Data() { + if (ptr_ != nullptr && ptr_ != data_) std::unique_ptr free_me(ptr_); +} + +void Packet::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("destination", destination_); + tracker->TrackField("data", data_); + tracker->TrackField("handle", handle_); +} + +void Packet::Data::MemoryInfo(MemoryTracker* tracker) const { + if (ptr_ != data_) tracker->TrackFieldWithSize("buffer", len_); +} + +// ============================================================================= +// QuicError + +namespace { +std::string TypeName(QuicError::Type type) { + switch (type) { + case QuicError::Type::APPLICATION: + return "APPLICATION"; + case QuicError::Type::TRANSPORT: + return "TRANSPORT"; + case QuicError::Type::VERSION_NEGOTIATION: + return "VERSION_NEGOTIATION"; + case QuicError::Type::IDLE_CLOSE: + return "IDLE_CLOSE"; + } + UNREACHABLE(); +} +} // namespace + +QuicError QuicError::ForTransport(error_code code, const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForApplication(error_code code, + const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_application_error( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForVersionNegotiation(const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_liberr( + &error.error_, + NGTCP2_ERR_RECV_VERSION_NEGOTIATION, + error.reason_c_str(), + reason.length()); + return error; +} + +QuicError QuicError::ForIdleClose(const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_liberr( + &error.error_, + NGTCP2_ERR_IDLE_CLOSE, + error.reason_c_str(), + reason.length()); + return error; +} + +QuicError QuicError::ForNgtcp2Error(int code, const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_liberr( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForTlsAlert(int code, const std::string& reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::From(Session* session) { + QuicError error(""); + ngtcp2_conn_get_connection_close_error(session->connection(), &error.error_); + return error; +} + +std::string QuicError::ToString() const { + auto str = std::string("QuicError(") + TypeName(type()) + ") " + + std::to_string(code()); + if (reason_.length() > 0) str += ": " + reason_; + return str; +} + +void QuicError::MemoryInfo(MemoryTracker* tracker) const { + if (ptr_ == &error_) tracker->TrackField("reason", reason_); +} + +void IllegalConstructor(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + THROW_ERR_ILLEGAL_CONSTRUCTOR(env); +} + +// ====================================================================================== +// Store + +void Store::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("store", store_); +} + +Store::Store(std::shared_ptr store, + size_t length, + size_t offset) + : store_(std::move(store)), offset_(offset), length_(length) {} + +Store::Store(std::unique_ptr store, + size_t length, + size_t offset) + : store_(std::move(store)), offset_(offset), length_(length) {} + +Store::Store(v8::Local buffer) + : Store(buffer->GetBackingStore(), buffer->ByteLength()) {} + +Store::Store(v8::Local view) + : Store(view->Buffer()->GetBackingStore(), + view->ByteLength(), + view->ByteOffset()) {} + +Store::operator uv_buf_t() const { + uv_buf_t buf; + buf.base = store_ != nullptr ? static_cast(store_->Data()) + offset_ + : nullptr, + buf.len = length_; + return buf; +} + +Store::operator ngtcp2_vec() const { + ngtcp2_vec vec; + vec.base = store_ != nullptr ? static_cast(store_->Data()) + offset_ + : nullptr; + vec.len = length_; + return vec; +} + +// ====================================================================================== +namespace { +void SetCallbacks(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + auto isolate = env->isolate(); + BindingState& state = BindingState::Get(env); + CHECK(!state.initialized); + if (!args[0]->IsObject()) + return THROW_ERR_INVALID_ARG_TYPE(env, "Missing Callbacks"); + + Local obj = args[0].As(); + +#define V(name, key) \ + do { \ + Local val; \ + if (!obj->Get(env->context(), FIXED_ONE_BYTE_STRING(isolate, "on" #key)) \ + .ToLocal(&val) || \ + !val->IsFunction()) { \ + return THROW_ERR_MISSING_ARGS(isolate, "Missing Callback: on" #key); \ + } \ + state.set_##name##_callback(val.As()); \ + } while (0); + QUIC_JS_CALLBACKS(V) + +#undef V + state.initialized = true; +} +} // namespace + +#endif // NODE_OPENSSL_HAS_QUIC + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { +#if NODE_OPENSSL_HAS_QUIC + auto env = Environment::GetCurrent(context); + + if (UNLIKELY(!BindingState::Initialize(env, target))) return; + + Endpoint::Initialize(env, target); + Session::Initialize(env, target); + Stream::Initialize(env, target); + SessionTicket::Initialize(env, target); + + SetMethod(context, target, "setCallbacks", SetCallbacks); + + constexpr uint32_t QUIC_PREFERRED_ADDRESS_USE = + static_cast(PreferredAddress::Policy::USE); + constexpr uint32_t QUIC_PREFERRED_ADDRESS_IGNORE = + static_cast(PreferredAddress::Policy::IGNORE_PREFERED); + +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##Name); + ENDPOINT_STATS(V) +#undef V +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##Name); + ENDPOINT_STATE(V) +#undef V +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##Name); + SESSION_STATS(V) +#undef V +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_##Name); + SESSION_STATE(V) +#undef V +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_##Name); + STREAM_STATS(V) +#undef V +#define V(Name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_STREAM_##Name); + STREAM_STATE(V) +#undef V + + NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_CUBIC); + NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_RENO); + NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_BBR); + NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_BBR2); + NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_IGNORE); + NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_USE); + NODE_DEFINE_CONSTANT(target, QUIC_MAX_CIDLEN); + NODE_DEFINE_CONSTANT(target, QUIC_HEADERS_KIND_INFO); + NODE_DEFINE_CONSTANT(target, QUIC_HEADERS_KIND_INITIAL); + NODE_DEFINE_CONSTANT(target, QUIC_HEADERS_KIND_TRAILING); + NODE_DEFINE_CONSTANT(target, QUIC_HEADERS_FLAGS_NONE); + NODE_DEFINE_CONSTANT(target, QUIC_HEADERS_FLAGS_TERMINAL); + + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_PRIORITY_DEFAULT); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_PRIORITY_LOW); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_PRIORITY_HIGH); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_PRIORITY_FLAGS_NONE); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_PRIORITY_FLAGS_NON_INCREMENTAL); + +#define V(Name) \ + do { \ + constexpr auto QUIC_ERR_##Name = NGTCP2_##Name; \ + NODE_DEFINE_CONSTANT(target, QUIC_ERR_##Name); \ + } while (false); + QUIC_TRANSPORT_ERRORS(V) +#undef V + +#define V(Name) \ + do { \ + constexpr auto QUIC_ERR_##Name = NGHTTP3_##Name; \ + NODE_DEFINE_CONSTANT(target, QUIC_ERR_##Name); \ + } while (false); + HTTP3_APPLICATION_ERRORS(V) +#undef V + + NODE_DEFINE_CONSTANT(target, QUIC_ERROR_TYPE_TRANSPORT); + NODE_DEFINE_CONSTANT(target, QUIC_ERROR_TYPE_APPLICATION); + NODE_DEFINE_CONSTANT(target, QUIC_ERROR_TYPE_VERSION_NEGOTIATION); + NODE_DEFINE_CONSTANT(target, QUIC_ERROR_TYPE_IDLE_CLOSE); + + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_CLOSE); + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_BIND_FAILURE); + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_START_FAILURE); + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_RECEIVE_FAILURE); + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_SEND_FAILURE); + NODE_DEFINE_CONSTANT(target, QUIC_ENDPOINT_CLOSE_CONTEXT_LISTEN_FAILURE); + +#define DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD 0 + + NODE_DEFINE_STRING_CONSTANT(target, "HTTP3_ALPN", &NGHTTP3_ALPN_H3[1]); + NODE_DEFINE_STRING_CONSTANT(target, "QUIC_DEFAULT_CIPHERS", DEFAULT_CIPHERS); + NODE_DEFINE_STRING_CONSTANT(target, "QUIC_DEFAULT_GROUPS", DEFAULT_GROUPS); + + NODE_DEFINE_CONSTANT(target, DEFAULT_RETRYTOKEN_EXPIRATION); + NODE_DEFINE_CONSTANT(target, DEFAULT_TOKEN_EXPIRATION); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS_PER_HOST); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT); + NODE_DEFINE_CONSTANT(target, DEFAULT_UNACKNOWLEDGED_PACKET_THRESHOLD); + + NODE_DEFINE_CONSTANT(target, HTTP3_ERR_NO_ERROR); +#endif // NODE_OPENSSL_HAS_QUIC +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { +#if NODE_OPENSSL_HAS_QUIC + registry->Register(SetCallbacks); + Endpoint::RegisterExternalReferences(registry); + Session::RegisterExternalReferences(registry); + Stream::RegisterExternalReferences(registry); + SessionTicket::RegisterExternalReferences(registry); +#endif // NODE_OPENSSL_HAS_QUIC +} + +} // namespace quic +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(quic, node::quic::Initialize) +NODE_MODULE_EXTERNAL_REFERENCE(quic, node::quic::RegisterExternalReferences) diff --git a/src/quic/quic.h b/src/quic/quic.h new file mode 100644 index 00000000000000..11ca7522f153f0 --- /dev/null +++ b/src/quic/quic.h @@ -0,0 +1,555 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "defs.h" + +namespace node { +namespace quic { + +class BindingState; +class CryptoContext; +class Endpoint; +class Session; +class Stream; + +using QuicMemoryManager = mem::NgLibMemoryManager; + +// ============================================================================= +// The BindingState object holds state for the internalBinding('quic') binding +// instance. It is mostly used to hold the persistent constructors, strings, and +// callback references used for the rest of the implementation. +class BindingState final : public BaseObject, public QuicMemoryManager { + public: + static constexpr FastStringKey type_name{"quic"}; + + static bool Initialize(Environment* env, v8::Local target); + static BindingState& Get(Environment* env); + + operator ngtcp2_mem() const; + operator nghttp3_mem() const; + + BindingState(Environment* env, v8::Local object); + QUIC_NO_COPY_OR_MOVE(BindingState) + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(BindingState); + SET_SELF_SIZE(BindingState); + + // NgLibMemoryManager (QuicMemoryManager) + void CheckAllocatedSize(size_t previous_size) const; + void IncreaseAllocatedSize(size_t size); + void DecreaseAllocatedSize(size_t size); + + // A set of listening Endpoints. We maintain this to ensure that the Endpoint + // cannot be gc'd while it is still listening and there are active + // connections. + std::unordered_map> listening_endpoints_; + + bool warn_trace_tls = true; + bool initialized = false; + +#define V(name) \ + void set_##name##_constructor_template( \ + v8::Local tmpl); \ + v8::Local name##_constructor_template() const; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) \ + void set_##name##_callback(v8::Local fn); \ + v8::Local name##_callback() const; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) v8::Local name##_string() const; + QUIC_STRINGS(V) +#undef V + + private: + size_t current_ngtcp2_memory_ = 0; + +#define V(name) v8::Global name##_constructor_template_; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) v8::Global name##_callback_; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) mutable v8::Eternal name##_string_; + QUIC_STRINGS(V) +#undef V +}; + +// ============================================================================= +// CIDs are used to identify endpoints participating in a QUIC session + +class CIDFactory; + +class CID final : public MemoryRetainer { + public: + inline CID() : ptr_(&cid_) {} + + // Copies the given cid. + CID(const uint8_t* cid, size_t len); + + CID(const CID& other); + CID& operator=(const CID& other); + + // Wraps the given ngtcp2_cid + inline explicit CID(const ngtcp2_cid* cid) : ptr_(cid) {} + + // Copies the given cid. + inline explicit CID(const ngtcp2_cid& cid) : CID(cid.data, cid.datalen) {} + + struct Hash final { + inline size_t operator()(const CID& cid) const { + size_t hash = 0; + for (size_t n = 0; n < cid->datalen; n++) { + hash ^= std::hash{}(cid->data[n]) + 0x9e3779b9 + (hash << 6) + + (hash >> 2); + } + return hash; + } + }; + + inline bool operator==(const CID& other) const noexcept { + return memcmp(ptr_->data, other.ptr_->data, ptr_->datalen) == 0; + } + + inline bool operator!=(const CID& other) const noexcept { + return !(*this == other); + } + + inline const ngtcp2_cid& operator*() const { return *ptr_; } + inline const ngtcp2_cid* operator->() const { return ptr_; } + + inline operator ngtcp2_cid*() { return &cid_; } + inline operator const ngtcp2_cid*() const { return ptr_; } + inline operator const uint8_t*() const { return ptr_->data; } + + std::string ToString() const; + + inline operator bool() const { return ptr_->datalen > 0; } + inline size_t length() const { return ptr_->datalen; } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(CID) + SET_SELF_SIZE(CID) + + template + using Map = std::unordered_map; + + private: + inline void set_length(size_t length) { + CHECK_EQ(ptr_, &cid_); + cid_.datalen = length; + } + + ngtcp2_cid cid_; + const ngtcp2_cid* ptr_ = nullptr; + + friend class CIDFactory; +}; + +// A CIDFactory, as the name suggests, is used to create new CIDs. +// Per https://datatracker.ietf.org/doc/draft-ietf-quic-load-balancers/, QUIC +// implementations may use the Connection IDs associated with a QUIC session as +// a routing mechanism, with each CID instance securely encoding the routing +// information. By default, our implementation creates CIDs randomly but allows +// user code to provide their own CIDFactory implementation. +class CIDFactory { + public: + virtual ~CIDFactory() = default; + + virtual void Generate(ngtcp2_cid* cid, + size_t length_hint = NGTCP2_MAX_CIDLEN) = 0; + + void Generate(CID* cid, size_t length_hint = NGTCP2_MAX_CIDLEN); + + CID Generate(size_t length_hint = NGTCP2_MAX_CIDLEN); + + virtual BaseObjectPtr StrongRef() { return {}; } + + virtual void UpdateCIDState(const CID& cid) { + // By default, do nothing. + } + + class RandomCIDFactory; + class Base; + + static CIDFactory& random(); +}; + +// The default random CIDFactory implementation. +class CIDFactory::RandomCIDFactory final : public CIDFactory { + public: + RandomCIDFactory() = default; + QUIC_NO_COPY_OR_MOVE(RandomCIDFactory) + void Generate(ngtcp2_cid* cid, + size_t length_hint = NGTCP2_MAX_CIDLEN) override; +}; + +// A virtual base class for CIDFactory implementations implemented as Base +// objects. +class CIDFactory::Base : public CIDFactory, public BaseObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + Base(Environment* env, v8::Local object); + BaseObjectPtr StrongRef() override; +}; + +// ============================================================================= + +using PacketReq = ReqWrap; + +// A Packet encapsulates serialized outbound QUIC data. +class Packet final : public PacketReq { + private: + struct Data; + + public: + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create( + Environment* env, + Endpoint* endpoint, + const SocketAddress& remote_address, + const char* diagnostic_label = "", + size_t length = kDefaultMaxPacketLength); + + inline const SocketAddress& destination() const { return destination_; } + inline bool is_pending() const { return !!handle_; } + + operator uv_buf_t() const; + operator ngtcp2_vec() const; + void Truncate(size_t len); + inline size_t length() const { return data_ ? data_->len_ : 0; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint::Packet) + SET_SELF_SIZE(Packet) + + std::string ToString() const; + + BaseObjectPtr Clone() const; + + using Queue = std::deque>; + + // Really should be private but MakeBaseObject needs to be able to see it. + // Use Create() to create instances. + Packet(Environment* env, + Endpoint* endpoint, + v8::Local object, + const SocketAddress& destination, + size_t length, + const char* diagnostic_label = ""); + + // Really should be private but MakeBaseObject needs to be able to see it. + // Use Create() to create instances. + Packet(Environment* env, + Endpoint* endpoint, + v8::Local object, + const SocketAddress& destination, + std::shared_ptr data); + + private: + struct Data final : public MemoryRetainer { + uint8_t data_[kDefaultMaxPacketLength]; + uint8_t* ptr_ = data_; + size_t len_ = kDefaultMaxPacketLength; + std::string diagnostic_label_; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Data) + SET_SELF_SIZE(Data) + + Data(size_t length, const char* diagnostic_label); + ~Data() override; + }; + + inline void Attach(BaseObjectPtr handle) { + handle_ = std::move(handle); + } + void Done(int status); + + Endpoint* endpoint_; + SocketAddress destination_; + std::shared_ptr data_; + + BaseObjectPtr handle_; + + friend class Endpoint; + friend class UDP; +}; + +// ============================================================================= +class QuicError final : public MemoryRetainer { + public: + enum class Type : int { + TRANSPORT = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT, + APPLICATION = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION, + VERSION_NEGOTIATION = + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_VERSION_NEGOTIATION, + IDLE_CLOSE = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_IDLE_CLOSE, + }; + + inline QuicError(const std::string& reason = std::string()) + : reason_(reason), ptr_(&error_) { + ngtcp2_connection_close_error_default(&error_); + } + + inline QuicError(const ngtcp2_connection_close_error* ptr) + : reason_(reinterpret_cast(ptr->reason), ptr->reasonlen), + ptr_(ptr) {} + + inline QuicError(const ngtcp2_connection_close_error& error) + : reason_(reinterpret_cast(error.reason), error.reasonlen), + error_(error) {} + + inline Type type() const { return static_cast(ptr_->type); } + inline error_code code() const { return ptr_->error_code; } + inline const std::string& reason() const { return reason_; } + inline uint64_t frameType() const { return ptr_->frame_type; } + + inline const ngtcp2_connection_close_error& operator*() const { + return *ptr_; + } + inline const ngtcp2_connection_close_error* operator->() const { + return ptr_; + } + inline const ngtcp2_connection_close_error* get() const { return ptr_; } + + operator const ngtcp2_connection_close_error*() const { return ptr_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicError) + SET_SELF_SIZE(QuicError) + + static QuicError ForTransport(error_code code, + const std::string& reason = std::string()); + static QuicError ForApplication(error_code code, + const std::string& reason = std::string()); + static QuicError ForVersionNegotiation( + const std::string& reason = std::string()); + static QuicError ForIdleClose(const std::string& reason = std::string()); + static QuicError ForNgtcp2Error(int code, + const std::string& reason = std::string()); + static QuicError ForTlsAlert(int code, + const std::string& reason = std::string()); + + static QuicError From(Session* session); + + std::string ToString() const; + + private: + const uint8_t* reason_c_str() const { + return reinterpret_cast(reason_.c_str()); + } + + std::string reason_; + ngtcp2_connection_close_error error_; + const ngtcp2_connection_close_error* ptr_ = nullptr; +}; + +// ============================================================================= +// StatsBase is a base utility helper for classes (like Endpoint, Session, and +// Stream) that want to record statistics. +template +class StatsBase { + public: + using Stats = typename Traits::Stats; + + inline explicit StatsBase(Environment* env, bool shared = false) + : shared_(shared), + stats_store_( + v8::ArrayBuffer::NewBackingStore(env->isolate(), sizeof(Stats))), + stats_(new(stats_store_->Data()) Stats) { + DCHECK_NOT_NULL(stats_); + stats_->created_at = uv_hrtime(); + } + + QUIC_NO_COPY_OR_MOVE(StatsBase) + + virtual ~StatsBase() = default; + + inline v8::Local ToBigUint64Array(Environment* env) { + size_t size = sizeof(Stats); + size_t count = size / sizeof(uint64_t); + v8::Local stats_buffer = + v8::ArrayBuffer::New(env->isolate(), stats_store_); + return v8::BigUint64Array::New(stats_buffer, 0, count); + } + + struct StatsDebug final { + using Base = typename Traits::Base; + Base& ptr; + inline explicit StatsDebug(Base& ptr_) : ptr(ptr_) {} + inline std::string ToString() const { + const auto gen = [&] { + std::string out = "Statistics:\n"; + auto add_field = [&out](const char* name, uint64_t val) { + out += " "; + out += std::string(name); + out += ": "; + out += std::to_string(val); + out += "\n"; + }; + add_field("Duration", uv_hrtime() - ptr.GetStat(&Stats::created_at)); + Traits::ToString(ptr.stats(), add_field); + return out; + }; + if (ptr.shared_) { + RwLock::ScopedLock lock(ptr->mutex_); + return gen(); + } + return gen(); + } + }; + + StatsDebug debug() const { return StatsDebug(*this); } + + inline const Stats& stats() const { return *stats_; } + + inline void StatsMemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("stats_store", stats_store_); + } + + protected: + // Increments the given stat field by the given amount or 1 if no amount is + // specified. + inline void IncrementStat(uint64_t Stats::*member, uint64_t amount = 1) { + if (shared_) { + RwLock::ScopedLock lock(rwlock_); + stats_->*member += std::min(amount, kMaxUint64 - stats_->*member); + } else { + stats_->*member += std::min(amount, kMaxUint64 - stats_->*member); + } + } + + // Sets an entirely new value for the given stat field + inline void SetStat(uint64_t Stats::*member, uint64_t value) { + if (shared_) { + RwLock::ScopedLock lock(rwlock_); + stats_->*member = value; + } else { + stats_->*member = value; + } + } + + // Sets the given stat field to the current uv_hrtime() + inline void RecordTimestamp(uint64_t Stats::*member) { + if (shared_) { + RwLock::ScopedLock lock(rwlock_); + stats_->*member = uv_hrtime(); + } else { + stats_->*member = uv_hrtime(); + } + } + + // Gets the current value of the given stat field + inline uint64_t GetStat(uint64_t Stats::*member) const { + return stats_->*member; + } + + private: + const bool shared_ = false; + std::shared_ptr stats_store_; + Stats* stats_ = nullptr; + + RwLock rwlock_; +}; + +void IllegalConstructor(const v8::FunctionCallbackInfo& args); + +// ============================================================================= +// Store +class Store final : public MemoryRetainer { + public: + Store() = default; + + explicit Store(std::shared_ptr store, + size_t length, + size_t offset = 0); + + explicit Store(std::unique_ptr store, + size_t length, + size_t offset = 0); + + explicit Store(v8::Local buffer); + + explicit Store(v8::Local view); + + operator uv_buf_t() const; + + operator ngtcp2_vec() const; + + inline operator bool() const { return store_ != nullptr; } + + size_t length() const { return length_; } + + template + v8::Local ToArrayBufferView(Environment* env) { + if (!store_) { + return View::New(v8::ArrayBuffer::New(env->isolate(), 0), 0, 0); + } + auto buffer = v8::ArrayBuffer::New(env->isolate(), store_); + return View::New(buffer, offset_, length_); + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Store) + SET_SELF_SIZE(Store) + + private: + std::shared_ptr store_; + size_t offset_ = 0; + size_t length_ = 0; +}; + +// ============================================================================= + +template +struct CallbackScopeBase final { + BaseObjectPtr ref; + v8::Context::Scope context_scope; + v8::TryCatch try_catch; + + explicit CallbackScopeBase(T* ptr) + : ref(ptr), + context_scope(ptr->env()->context()), + try_catch(ptr->env()->isolate()) {} + + ~CallbackScopeBase() { + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + errors::TriggerUncaughtException(ref->env()->isolate(), try_catch); + } + } +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/session.cc b/src/quic/session.cc new file mode 100644 index 00000000000000..5f9f3a70241f19 --- /dev/null +++ b/src/quic/session.cc @@ -0,0 +1,3506 @@ +#include "session.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crypto.h" +#include "defs.h" +#include "endpoint.h" +#include "http3.h" +#include "quic.h" +#include "stream.h" +#include "uv.h" + +namespace node { + +using crypto::EntropySource; +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::BigInt; +using v8::False; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::PropertyAttribute; +using v8::String; +using v8::True; +using v8::Uint32; +using v8::Uint8Array; +using v8::Undefined; +using v8::Value; + +namespace quic { + +// ============================================================================= + +class LogStream final : public AsyncWrap, public StreamBase { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + v8::Local tmpl = + state.logstream_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = v8::FunctionTemplate::New(env->isolate()); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + StreamBase::kInternalFieldCount); + tmpl->SetClassName(state.logstream_string()); + StreamBase::AddMethods(env, tmpl); + state.set_logstream_constructor_template(tmpl); + } + return tmpl; + } + static BaseObjectPtr Create(Environment* env) { + v8::Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return BaseObjectPtr(); + } + return MakeDetachedBaseObject(env, obj); + } + + LogStream(Environment* env, Local obj) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_QUICLOGSTREAM), + StreamBase(env) { + MakeWeak(); + StreamBase::AttachToObject(GetObject()); + } + + void Emit(const uint8_t* data, size_t len, uint32_t flags) { + size_t remaining = len; + while (remaining != 0) { + uv_buf_t buf = EmitAlloc(len); + ssize_t avail = std::min(remaining, buf.len); + memcpy(buf.base, data, avail); + remaining -= avail; + data += avail; + if (reading_) + EmitRead(avail, buf); + else + buffer_.emplace_back(Chunk{buf, avail}); + } + + if (ended_ && flags & NGTCP2_QLOG_WRITE_FLAG_FIN) EmitRead(UV_EOF); + } + + void Emit(const std::string& line, uint32_t flags = 0) { + Emit(reinterpret_cast(line.c_str()), line.length(), flags); + } + + inline void End() { ended_ = true; } + + int ReadStart() override { + reading_ = true; + for (const Chunk& chunk : buffer_) EmitRead(chunk.avail, chunk.buf); + return 0; + } + + int ReadStop() override { + reading_ = false; + return 0; + } + + inline int DoShutdown(ShutdownWrap* req_wrap) override { UNREACHABLE(); } + + inline int DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) override { + UNREACHABLE(); + } + + inline bool IsAlive() override { return !ended_; } + inline bool IsClosing() override { return ended_; } + inline AsyncWrap* GetAsyncWrap() override { return this; } + + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(LogStream); + SET_SELF_SIZE(LogStream); + + private: + struct Chunk { + uv_buf_t buf; + ssize_t avail; + }; + + bool ended_ = false; + bool reading_ = false; + std::vector buffer_; +}; + +// ============================================================================= +// PreferredAddress + +Maybe PreferredAddress::ipv4() const { + if (!paddr_->ipv4_present) return Nothing(); + + AddressInfo address; + address.family = AF_INET; + address.port = paddr_->ipv4_port; + + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET, paddr_->ipv4_addr, host, sizeof(host)) == 0) + address.address = std::string(host); + + return Just(address); +} + +Maybe PreferredAddress::ipv6() const { + if (!paddr_->ipv6_present) return Nothing(); + + AddressInfo address; + address.family = AF_INET6; + address.port = paddr_->ipv6_port; + + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET6, paddr_->ipv6_addr, host, sizeof(host)) == 0) + address.address = std::string(host); + + return Just(address); +} + +bool PreferredAddress::Use(const AddressInfo& address) const { + uv_getaddrinfo_t req; + + if (!Resolve(address, &req)) return false; + + dest_->remote.addrlen = req.addrinfo->ai_addrlen; + memcpy(dest_->remote.addr, req.addrinfo->ai_addr, req.addrinfo->ai_addrlen); + uv_freeaddrinfo(req.addrinfo); + return true; +} + +void PreferredAddress::CopyToTransportParams(ngtcp2_transport_params* params, + const sockaddr* addr) { + CHECK_NOT_NULL(params); + CHECK_NOT_NULL(addr); + params->preferred_address_present = 1; + switch (addr->sa_family) { + case AF_INET: { + const sockaddr_in* src = reinterpret_cast(addr); + memcpy(params->preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(params->preferred_address.ipv4_addr)); + params->preferred_address.ipv4_port = SocketAddress::GetPort(addr); + return; + } + case AF_INET6: { + const sockaddr_in6* src = reinterpret_cast(addr); + memcpy(params->preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(params->preferred_address.ipv6_addr)); + params->preferred_address.ipv6_port = SocketAddress::GetPort(addr); + return; + } + } + UNREACHABLE(); +} + +bool PreferredAddress::Resolve(const AddressInfo& address, + uv_getaddrinfo_t* req) const { + addrinfo hints{}; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_family = address.family; + hints.ai_socktype = SOCK_DGRAM; + + // Unfortunately ngtcp2 requires the selection of the + // preferred address to be synchronous, which means we + // have to do a sync resolve using uv_getaddrinfo here. + return uv_getaddrinfo(env_->event_loop(), + req, + nullptr, + address.address.c_str(), + std::to_string(address.port).c_str(), + &hints) == 0 && + req->addrinfo != nullptr; +} + +// ============================================================================= +// Path + +Path::Path(const SocketAddress& local, const SocketAddress& remote) { + ngtcp2_addr_init(&this->local, local.data(), local.length()); + ngtcp2_addr_init(&this->remote, remote.data(), remote.length()); +} + +// ====================================================================================== +// Utilities + +namespace { + +// The default random CIDFactory instance. +CIDFactory::RandomCIDFactory kDefaultCIDFactory; + +// Qlog is a JSON-based logging format that is being standardized for low-level +// debug logging of QUIC connections and dataflows. The qlog output is generated +// optionally by ngtcp2 for us. The OnQlogWrite callback is registered with +// ngtcp2 to emit the qlog information. Every Session will have it's own qlog +// stream. +void OnQlogWrite(void* user_data, + uint32_t flags, + const void* data, + size_t len) { + Session* session = static_cast(user_data); + Environment* env = session->env(); + + // Fun fact... ngtcp2 does not emit the final qlog statement until the + // ngtcp2_conn object is destroyed. Ideally, destroying is explicit, but + // sometimes the Session object can be garbage collected without being + // explicitly destroyed. During those times, we cannot call out to JavaScript. + // Because we don't know for sure if we're in in a GC when this is called, it + // is safer to just defer writes to immediate, and to keep it consistent, + // let's just always defer (this is not performance sensitive so the deferring + // is fine). + + DEBUG_ARGS(session, + "QLOG: %s", + std::string(reinterpret_cast(data), len)); + + if (session->qlogstream()) { + std::vector buffer(len); + memcpy(buffer.data(), data, len); + env->SetImmediate( + [ptr = session->qlogstream(), buffer = std::move(buffer), flags]( + Environment*) { ptr->Emit(buffer.data(), buffer.size(), flags); }); + } +} + +// Forwards detailed(verbose) debugging information from ngtcp2. Enabled using +// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category. +void Ngtcp2DebugLog(void* user_data, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string format(fmt, strlen(fmt) + 1); + format[strlen(fmt)] = '\n'; + // Debug() does not work with the va_list here. So we use vfprintf + // directly instead. Ngtcp2DebugLog is only enabled when the debug + // category is enabled. + vfprintf(stderr, format.c_str(), ap); + va_end(ap); +} +} // namespace + +CIDFactory& CIDFactory::random() { + return kDefaultCIDFactory; +} + +struct Session::NgCallbackScope final { + Session* session; + inline explicit NgCallbackScope(Session* session_) : session(session_) { + CHECK(!session->in_ng_callback_); + session->in_ng_callback_ = true; + } + QUIC_NO_COPY_OR_MOVE(NgCallbackScope) + + inline ~NgCallbackScope() { session->in_ng_callback_ = false; } + + static inline bool InNgCallbackScope(const Session& session) { + return session.in_ng_callback_; + } +}; + +struct Session::MaybeCloseConnectionScope final { + Session* session; + bool silent = false; + MaybeCloseConnectionScope(Session* session_, bool silent_) + : session(session_), + silent(silent_ || session->connection_close_depth_ > 0) { + session->connection_close_depth_++; + } + QUIC_NO_COPY_OR_MOVE(MaybeCloseConnectionScope) + ~MaybeCloseConnectionScope() noexcept { + // We only want to trigger the sending the connection close if ... + // a) Silent is not explicitly true, + // b) We're not within the scope of an ngtcp2 callback, and + // c) We are not already in a closing or draining period. + session->connection_close_depth_--; + if (session->connection_close_depth_ == 0 && !silent && + !NgCallbackScope::InNgCallbackScope(*session) && + !session->is_destroyed() && !session->is_in_closing_period() && + !session->is_in_draining_period()) { + session->SendConnectionClose(); + } + } +}; + +Session::SendPendingDataScope::SendPendingDataScope(Session* session_) + : session(session_) { + session->send_scope_depth_++; +} + +Session::SendPendingDataScope::~SendPendingDataScope() { + --(session->send_scope_depth_); + if (session->send_scope_depth_ == 0 && + !NgCallbackScope::InNgCallbackScope(*session) && + !session->is_in_closing_period() && !session->is_in_draining_period()) { + session->SendPendingData(); + } +} + +// ====================================================================================== +// Session::Config + +Session::Config::Config(const Endpoint& endpoint, + const CID& dcid_, + const SocketAddress& local_address, + const SocketAddress& remote_address, + quic_version version_, + ngtcp2_crypto_side side) + : side(side), + version(version_), + // For now, we're always using the default, but we will make this + // configurable soon. + cid_factory(kDefaultCIDFactory), + local_addr(local_address), + remote_addr(remote_address), + dcid(dcid_), + scid(cid_factory.Generate()) { + ngtcp2_settings_default(this); + initial_ts = uv_hrtime(); + + if (UNLIKELY(endpoint.env()->enabled_debug_list()->enabled( + DebugCategory::NGTCP2_DEBUG))) + log_printf = Ngtcp2DebugLog; + + auto& config = endpoint.options(); + + cc_algo = config.cc_algorithm; + max_udp_payload_size = config.max_payload_size; + + if (config.max_window_override > 0) max_window = config.max_window_override; + + if (config.max_stream_window_override > 0) + max_stream_window = config.max_stream_window_override; + + if (config.unacknowledged_packet_threshold > 0) + ack_thresh = config.unacknowledged_packet_threshold; +} + +void Session::Config::EnableQLog(const CID& ocid) { + if (ocid) { + qlog.odcid = *ocid; + this->ocid = Just(ocid); + } + qlog.write = OnQlogWrite; +} + +// ====================================================================================== +// Session::Options + +void Session::Options::MemoryInfo(MemoryTracker* tracker) const { + if (preferred_address_ipv4.IsJust()) + tracker->TrackField("preferred_address_ipv4", + preferred_address_ipv4.FromJust()); + if (preferred_address_ipv6.IsJust()) + tracker->TrackField("preferred_address_ipv6", + preferred_address_ipv6.FromJust()); + tracker->TrackField("tls", crypto_options); +} + +// ====================================================================================== +// Session::TransportParams + +Session::TransportParams::TransportParams(const Config& config, + const Options& options) + : TransportParams(Type::ENCRYPTED_EXTENSIONS) { + ngtcp2_transport_params_default(¶ms_); + params_.active_connection_id_limit = options.active_connection_id_limit; + params_.initial_max_stream_data_bidi_local = + options.initial_max_stream_data_bidi_local; + params_.initial_max_stream_data_bidi_remote = + options.initial_max_stream_data_bidi_remote; + params_.initial_max_stream_data_uni = options.initial_max_stream_data_uni; + params_.initial_max_streams_bidi = options.initial_max_streams_bidi; + params_.initial_max_streams_uni = options.initial_max_streams_uni; + params_.initial_max_data = options.initial_max_data; + params_.max_idle_timeout = options.max_idle_timeout * NGTCP2_SECONDS; + params_.max_ack_delay = options.max_ack_delay; + params_.ack_delay_exponent = options.ack_delay_exponent; + params_.max_datagram_frame_size = options.max_datagram_frame_size; + params_.disable_active_migration = options.disable_active_migration ? 1 : 0; + params_.preferred_address_present = 0; + params_.stateless_reset_token_present = 0; + params_.retry_scid_present = 0; + + if (config.side == NGTCP2_CRYPTO_SIDE_SERVER) { + // For the server side, the original dcid is always set. + params_.original_dcid = *config.ocid.ToChecked(); + + // The retry_scid is only set if the server validated a retry token. + if (config.retry_scid.IsJust()) { + params_.retry_scid = *config.retry_scid.FromJust(); + params_.retry_scid_present = 1; + } + } + + if (options.preferred_address_ipv4.IsJust()) + SetPreferredAddress(options.preferred_address_ipv4.FromJust()); + + if (options.preferred_address_ipv6.IsJust()) + SetPreferredAddress(options.preferred_address_ipv6.FromJust()); +} + +Session::TransportParams::TransportParams(Type type, const ngtcp2_vec& vec) + : TransportParams(type) { + int ret = ngtcp2_decode_transport_params( + ¶ms_, + static_cast(type), + vec.base, + vec.len); + + if (ret != 0) { + ptr_ = nullptr; + error_ = QuicError::ForNgtcp2Error(ret); + } +} + +Store Session::TransportParams::Encode(Environment* env) { + if (ptr_ == nullptr) { + error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); + return Store(); + } + + // Preflight to see how much storage we'll need. + ssize_t size = ngtcp2_encode_transport_params( + nullptr, 0, static_cast(type_), ¶ms_); + + CHECK_GT(size, 0); + + auto result = ArrayBuffer::NewBackingStore(env->isolate(), size); + + auto ret = ngtcp2_encode_transport_params( + static_cast(result->Data()), + size, + static_cast(type_), + ¶ms_); + + if (ret != 0) { + error_ = QuicError::ForNgtcp2Error(ret); + return Store(); + } + + return Store(std::move(result), size); +} + +void Session::TransportParams::SetPreferredAddress( + const SocketAddress& address) { + CHECK(ptr_ == ¶ms_); + params_.preferred_address_present = 1; + switch (address.family()) { + case AF_INET: { + const sockaddr_in* src = + reinterpret_cast(address.data()); + memcpy(params_.preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(params_.preferred_address.ipv4_addr)); + params_.preferred_address.ipv4_port = address.port(); + return; + } + case AF_INET6: { + const sockaddr_in6* src = + reinterpret_cast(address.data()); + memcpy(params_.preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(params_.preferred_address.ipv6_addr)); + params_.preferred_address.ipv6_port = address.port(); + return; + } + } + UNREACHABLE(); +} + +void Session::TransportParams::GenerateStatelessResetToken( + const Endpoint& endpoint, const CID& cid) { + CHECK(ptr_ == ¶ms_); + CHECK(cid); + params_.stateless_reset_token_present = 1; + + StatelessResetToken token(params_.stateless_reset_token, + endpoint.options().reset_token_secret, + cid); +} + +void Session::TransportParams::GeneratePreferredAddressToken(Session* session, + CID* pscid) { + CHECK_NOT_NULL(session); + CHECK(ptr_ == ¶ms_); + CHECK(pscid); + *pscid = session->cid_factory_.Generate(); + params_.preferred_address.cid = **pscid; + StatelessResetToken new_token( + params_.preferred_address.stateless_reset_token, + session->endpoint().options().reset_token_secret, + *pscid); + session->endpoint_->AssociateStatelessResetToken(new_token, session); +} + +// ====================================================================================== +// Session::Application + +std::string Session::Application::StreamData::ToString() const { + return std::string("StreamData [") + std::to_string(id) + + "]: buffers = " + std::to_string(count) + + ", remaining = " + std::to_string(remaining) + + ", fin = " + std::to_string(fin); +} + +bool Session::Application::Start() { + if (!started_) { + DEBUG(session_, "Application started"); + started_ = true; + } + return true; +} + +void Session::Application::AcknowledgeStreamData(Stream* stream, + uint64_t offset, + size_t datalen) { + DEBUG_ARGS( + session_, "Acknowledging %" PRIu64 " bytes of stream data", datalen); + stream->Acknowledge(offset, datalen); +} + +bool Session::Application::BlockStream(stream_id id) { + DEBUG_ARGS(session_, "Application block stream %" PRIi64, id); + auto stream = session().FindStream(id); + if (stream) stream->Blocked(); + return true; +} + +// Called to determine if a Header can be added to this application. +// Applications that do not support headers (which is the default) will always +// return false. +bool Session::Application::CanAddHeader(size_t current_count, + size_t current_headers_length, + size_t this_header_length) { + return false; +} + +SessionTicketAppData::Status Session::Application::GetSessionTicketAppData( + const SessionTicketAppData& app_data, SessionTicketAppData::Flag flag) { + DEBUG(session_, "Application getting session ticket app data"); + return flag == SessionTicketAppData::Flag::STATUS_RENEW + ? SessionTicketAppData::Status::TICKET_USE_RENEW + : SessionTicketAppData::Status::TICKET_USE; +} + +BaseObjectPtr Session::Application::CreateStreamDataPacket() { + return Packet::Create( + env(), + session_->endpoint_.get(), + session_->remote_address_, + "stream data", + ngtcp2_conn_get_max_udp_payload_size(session_->connection())); +} + +void Session::Application::StreamClose(Stream* stream, Maybe error) { + DEBUG_ARGS(session_, "Application closing stream %" PRIi64, stream->id()); + stream->Destroy(error); +} + +void Session::Application::StreamReset(Stream* stream, + uint64_t final_size, + QuicError error) { + DEBUG_ARGS(session_, + "Application resetting stream %" PRIi64 " with final size %" PRIu64 + " [%s]", + stream->id(), + final_size, + error); + stream->ReceiveResetStream(final_size, error); +} + +void Session::Application::StreamStopSending(Stream* stream, QuicError error) { + DEBUG_ARGS(session_, + "Application stop sending stream %" PRIi64 " [%s]", + stream->id(), + error); + stream->ReceiveStopSending(error); +} + +void Session::Application::SendPendingData() { + PathStorage path; + + DEBUG(session_, "Starting pending data loop"); + BaseObjectPtr packet; + uint8_t* pos = nullptr; + int err = 0; + + size_t maxPacketCount = + std::min(static_cast(64000), + ngtcp2_conn_get_send_quantum(session_->connection())); + size_t packetSendCount = 0; + DEBUG_ARGS(session_, + "Maximum number of packets to send this iteration: %" PRIu64, + maxPacketCount); + + const auto updateTimer = [&] { + ngtcp2_conn_update_pkt_tx_time(session_->connection(), uv_hrtime()); + session_->UpdateTimer(); + }; + + const auto congestionLimited = [&](auto packet) { + auto len = pos - ngtcp2_vec(*packet).base; + // We are either congestion limited or done. + if (len) { + // Some data was serialized into the packet. We need to send it. + packet->Truncate(len); + session_->Send(std::move(packet), path); + } + + updateTimer(); + }; + + for (;;) { + ssize_t ndatalen; + StreamData stream_data; + + DEBUG(session_, "Selecting stream data to encode"); + err = GetStreamData(&stream_data); + + if (err < 0) { + session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); + return session_->CloseSilently(); + } + + DEBUG_ARGS(session_, "Stream Data: %s", stream_data); + + if (!packet) { + packet = CreateStreamDataPacket(); + if (!packet) { + session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); + return session_->CloseSilently(); + } + pos = ngtcp2_vec(*packet).base; + } + + ssize_t nwrite = WriteVStream(&path, pos, &ndatalen, stream_data); + + DEBUG_ARGS(session_, "Total amount written to packet: %" PRId64, nwrite); + DEBUG_ARGS( + session_, "Stream data amount written to packet: %" PRId64, ndatalen); + + if (nwrite <= 0) { + switch (nwrite) { + case 0: + DEBUG(session_, "No data written to packet. Congestion limited?"); + if (stream_data.id >= 0) ResumeStream(stream_data.id); + return congestionLimited(std::move(packet)); + case NGTCP2_ERR_STREAM_DATA_BLOCKED: { + DEBUG(session_, + "Stream data is blocked. Congestion limited? We'll try again."); + session().StreamDataBlocked(stream_data.id); + if (session().max_data_left() == 0) { + if (stream_data.id >= 0) ResumeStream(stream_data.id); + return congestionLimited(std::move(packet)); + } + CHECK_LE(ndatalen, 0); + continue; + } + case NGTCP2_ERR_STREAM_SHUT_WR: { + // Indicates that the writable side of the stream has been closed + // locally or the stream is being reset. In either case, we can't send + // any stream data! + CHECK_GE(stream_data.id, 0); + DEBUG_ARGS( + session_, "Stream %" PRIi64 " is not writable.", stream_data.id); + // We need to notify the stream that the writable side has been closed + // and no more outbound data can be sent. + CHECK_LE(ndatalen, 0); + auto stream = session_->FindStream(stream_data.id); + if (stream) stream->EndWritable(); + continue; + } + case NGTCP2_ERR_WRITE_MORE: { + DEBUG(session_, "Packet is not yet full. Continuing to write more."); + CHECK_GT(ndatalen, 0); + if (!StreamCommit(&stream_data, ndatalen)) return session_->Close(); + pos += ndatalen; + continue; + } + } + + DEBUG(session_, + "An unexpected error occurred while write packet data. Closing " + "session."); + session_->last_error_ = QuicError::ForNgtcp2Error(nwrite); + return session_->CloseSilently(); + } + + pos += nwrite; + if (ndatalen > 0 && !StreamCommit(&stream_data, ndatalen)) { + // Since we are closing the session here, we don't worry about updating + // the pkt tx time. The failed StreamCommit should have updated the + // last_error_ appropriately. + return session_->CloseSilently(); + } + + if (stream_data.id >= 0 && ndatalen < 0) ResumeStream(stream_data.id); + + packet->Truncate(nwrite); + session_->Send(std::move(packet), path); + + pos = nullptr; + + if (++packetSendCount == maxPacketCount) { + DEBUG(session_, + "Packet count limit reached. Breaking out of pending data loop."); + break; + } + } + + updateTimer(); +} + +ssize_t Session::Application::WriteVStream(PathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data) { + CHECK_LE(stream_data.count, kMaxVectorCount); + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE; + if (stream_data.remaining > 0) flags |= NGTCP2_WRITE_STREAM_FLAG_MORE; + if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + ssize_t ret = ngtcp2_conn_writev_stream( + session_->connection(), + &path->path, + nullptr, + buf, + ngtcp2_conn_get_max_udp_payload_size(session_->connection()), + ndatalen, + flags, + stream_data.id, + stream_data.buf, + stream_data.count, + uv_hrtime()); + return ret; +} + +// ====================================================================================== +// Session +Session* Session::From(ngtcp2_conn* conn, void* user_data) { + auto session = static_cast(user_data); + CHECK_EQ(conn, session->connection()); + return session; +} + +Local Session::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + auto tmpl = state.session_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + tmpl->SetClassName(state.session_string()); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Session::kInternalFieldCount); + SetProtoMethodNoSideEffect( + isolate, tmpl, "getRemoteAddress", GetRemoteAddress); + SetProtoMethodNoSideEffect(isolate, tmpl, "getCertificate", GetCertificate); + SetProtoMethodNoSideEffect( + isolate, tmpl, "getPeerCertificate", GetPeerCertificate); + SetProtoMethodNoSideEffect( + isolate, tmpl, "getEphemeralKeyInfo", GetEphemeralKeyInfo); + SetProtoMethod(isolate, tmpl, "destroy", DoDestroy); + SetProtoMethod(isolate, tmpl, "gracefulClose", GracefulClose); + SetProtoMethod(isolate, tmpl, "silentClose", SilentClose); + SetProtoMethod(isolate, tmpl, "updateKey", UpdateKey); + SetProtoMethod(isolate, tmpl, "onClientHelloDone", OnClientHelloDone); + SetProtoMethod(isolate, tmpl, "onOCSPDone", OnOCSPDone); + SetProtoMethod(isolate, tmpl, "openStream", DoOpenStream); + SetProtoMethod(isolate, tmpl, "sendDatagram", DoSendDatagram); + state.set_session_constructor_template(tmpl); + } + return tmpl; +} + +void Session::Initialize(Environment* env, Local target) { + USE(GetConstructorTemplate(env)); + + OptionsObject::Initialize(env, target); + +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##name); + SESSION_STATS(V) + NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); +#undef V +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_##name); + SESSION_STATE(V) + NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_COUNT); +#undef V + + constexpr uint32_t STREAM_DIRECTION_BIDIRECTIONAL = + static_cast(Direction::BIDIRECTIONAL); + constexpr uint32_t STREAM_DIRECTION_UNIDIRECTIONAL = + static_cast(Direction::UNIDIRECTIONAL); + + NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_BIDIRECTIONAL); + NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_UNIDIRECTIONAL); +} + +void Session::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(DoDestroy); + registry->Register(GetRemoteAddress); + registry->Register(GetCertificate); + registry->Register(GetEphemeralKeyInfo); + registry->Register(GetPeerCertificate); + registry->Register(GracefulClose); + registry->Register(SilentClose); + registry->Register(UpdateKey); + registry->Register(OnClientHelloDone); + registry->Register(OnOCSPDone); + registry->Register(DoOpenStream); + registry->Register(DoSendDatagram); + OptionsObject::RegisterExternalReferences(registry); +} + +BaseObjectPtr Session::Create(BaseObjectPtr endpoint, + const Config& config, + const Options& options) { + auto env = endpoint->env(); + Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) + return BaseObjectPtr(); + return MakeDetachedBaseObject( + obj, std::move(endpoint), config, options); +} + +std::string Session::diagnostic_name() const { + const auto get_type = [&] { return is_server() ? "server" : "client"; }; + + return MemoryInfoName() + " (" + get_type() + "," + + std::to_string(env()->thread_id()) + ":" + + std::to_string(static_cast(get_async_id())) + ")"; +} + +Session::Session(Local object, + BaseObjectPtr endpoint, + const Config& config, + const Options& options) + : SessionStatsBase(endpoint->env()), + AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUICSESSION), + allocator_(BindingState::Get(env())), + options_(std::move(options)), + endpoint_(std::move(endpoint)), + state_(env()->isolate()), + cid_factory_(config.cid_factory), + maybe_cid_factory_ref_(config.cid_factory.StrongRef()), + local_address_(config.local_addr), + remote_address_(config.remote_addr), + application_(SelectApplication(config, options_)), + crypto_context_(this, options_.crypto_options, config.side), + timer_(env(), + [this, self = BaseObjectPtr(this)] { OnTimeout(); }), + dcid_(config.dcid), + scid_(config.scid), + preferred_address_cid_(nullptr, 0) { + MakeWeak(); + timer_.Unref(); + + if (config.ocid.IsJust()) ocid_ = config.ocid; + + ExtendMaxStreams( + EndpointLabel::LOCAL, Direction::BIDIRECTIONAL, DEFAULT_MAX_STREAMS_BIDI); + ExtendMaxStreams( + EndpointLabel::LOCAL, Direction::UNIDIRECTIONAL, DEFAULT_MAX_STREAMS_UNI); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env()->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env()->state_string(), state_.GetArrayBuffer()); + defineProperty(env()->stats_string(), ToBigUint64Array(env())); + + if (UNLIKELY(options.qlog)) { + qlogstream_ = LogStream::Create(env()); + if (LIKELY(qlogstream_)) { + defineProperty(BindingState::Get(env()).qlog_string(), + qlogstream_->object()); + } + } + + if (UNLIKELY(options.crypto_options.keylog)) { + keylogstream_ = LogStream::Create(env()); + if (LIKELY(keylogstream_)) { + defineProperty(BindingState::Get(env()).keylog_string(), + keylogstream_->object()); + } + } + + ngtcp2_conn* conn; + Path path(local_address_, remote_address_); + TransportParams transport_params(config, options); + switch (config.side) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + DEBUG_ARGS(this, "Initializing as server session [%s]", scid_); + transport_params.GenerateStatelessResetToken(*endpoint_, scid_); + if (transport_params->preferred_address_present) + transport_params.GeneratePreferredAddressToken(this, + &preferred_address_cid_); + CHECK_EQ(ngtcp2_conn_server_new(&conn, + dcid_, + scid_, + &path, + config.version, + &callbacks[config.side], + &config, + transport_params, + &allocator_, + this), + 0); + break; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + DEBUG_ARGS(this, "Initializing as client session [%s]", scid_); + CHECK_EQ(ngtcp2_conn_client_new(&conn, + dcid_, + scid_, + &path, + config.version, + &callbacks[config.side], + &config, + transport_params, + &allocator_, + this), + 0); + crypto_context_.MaybeSetEarlySession(config.session_ticket); + break; + } + default: + UNREACHABLE(); + } + + connection_.reset(conn); + + // We index the Session by our local CID (the scid) and dcid (the peer's cid) + endpoint_->AddSession(scid_, BaseObjectPtr(this)); + endpoint_->AssociateCID(dcid_, scid_); + + crypto_context_.Start(); + + UpdateDataStats(); +} + +Session::~Session() { + DEBUG_ARGS(this, "Destroying session [%s]", scid_); + if (qlogstream_) { + env()->SetImmediate( + [ptr = std::move(qlogstream_)](Environment*) { ptr->End(); }); + } + if (keylogstream_) { + env()->SetImmediate( + [ptr = std::move(keylogstream_)](Environment*) { ptr->End(); }); + } + CHECK(streams_.empty()); +} + +void Session::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); + tracker->TrackField("endpoint", endpoint_); + tracker->TrackField("streams", streams_); + tracker->TrackField("local_address", local_address_); + tracker->TrackField("remote_address", remote_address_); + tracker->TrackField("application", application_); + tracker->TrackField("crypto_context", crypto_context_); + tracker->TrackField("timer", timer_); + tracker->TrackField("conn_closebuf", conn_closebuf_); + tracker->TrackField("qlogstream", qlogstream_); + tracker->TrackField("keylogstream", keylogstream_); +} + +const Endpoint& Session::endpoint() const { + return *endpoint_.get(); +} + +BaseObjectPtr Session::FindStream(stream_id id) const { + auto it = streams_.find(id); + return it == std::end(streams_) ? BaseObjectPtr() : it->second; +} + +bool Session::HandshakeCompleted() { + state_->handshake_completed = true; + RecordTimestamp(&SessionStats::handshake_completed_at); + DEBUG(this, "Handshake is completed"); + + if (!crypto_context_.was_early_data_accepted()) { + ngtcp2_conn_early_data_rejected(connection()); + } + + // When in a server session, handshake completed == handshake confirmed. + if (is_server()) { + HandshakeConfirmed(); + + uint8_t token[NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN]; + size_t tokenlen = 0; + + if (!endpoint_->GenerateNewToken(version(), token, remote_address_) + .To(&tokenlen)) { + // Failed to generate a new token on handshake complete. + // This isn't the end of the world, just keep going. + return true; + } + + if (NGTCP2_ERR( + ngtcp2_conn_submit_new_token(connection_.get(), token, tokenlen))) { + // Submitting the new token failed... + return false; + } + } + + EmitHandshakeComplete(); + + return true; +} + +void Session::HandshakeConfirmed() { + state_->handshake_confirmed = true; + RecordTimestamp(&SessionStats::handshake_confirmed_at); + DEBUG(this, "Handshake is confirmed"); +} + +void Session::Close() { + if (!is_destroyed()) { + DEBUG(this, "Close"); + DoClose(); + } +} + +void Session::CloseSilently() { + if (!is_destroyed()) { + DEBUG(this, "Close silently"); + DoClose(true); + } +} + +void Session::CloseGracefully() { + if (is_destroyed()) return; + + // If there are no open streams, then we can close just immediately and not + // worry about waiting around for the right moment. + if (streams_.empty()) return DoClose(); + + DEBUG(this, "Close gracefully"); + + state_->graceful_closing = 1; + RecordTimestamp(&SessionStats::graceful_closing_at); +} + +void Session::DoClose(bool silent) { + CHECK(!is_destroyed()); + // Once Close has been called, we cannot re-enter + if (state_->closing) return; + state_->closing = 1; + + DEBUG(this, "Closing immediately"); + + // Iterate through all of the known streams and close them. The streams + // will remove themselves from the Session as soon as they are closed. + // Note: we create a copy because the streams will remove themselves + // while they are cleaning up which will invalidate the iterator. + auto streams = streams_; + for (auto& stream : streams) stream.second->Destroy(Just(last_error_)); + streams.clear(); + + // If the state has not been passed out to JavaScript yet, we can skip closing + // entirely and drop directly out to Destroy. + if (!state_->wrapped) return Destroy(); + + // If we're not running within a ngtcp2 callback scope, schedule a + // CONNECTION_CLOSE to be sent when Close exits. If we are within a ngtcp2 + // callback scope, sending the CONNECTION_CLOSE will be deferred. + { + MaybeCloseConnectionScope close_scope(this, silent); + RecordTimestamp(&SessionStats::closing_at); + + state_->closing = true; + state_->silent_close = silent ? 1 : 0; + } + + // We emit a close callback so that the JavaScript side can clean up anything + // it needs to clean up before destroying. It's the JavaScript side's + // responsibility to call destroy() when ready. + EmitClose(); +} + +void Session::Destroy() { + if (is_destroyed()) return; + + DEBUG(this, "Destroyed"); + + // The DoClose() method should have already been called. + CHECK(state_->closing); + + // We create a copy of the streams because they will remove themselves + // from streams_ as they are cleaning up, causing the iterator to be + // invalidated. + auto streams = streams_; + for (auto& stream : streams) stream.second->Destroy(Just(last_error_)); + + CHECK(streams_.empty()); + + RecordTimestamp(&SessionStats::destroyed_at); + state_->closing = 0; + state_->graceful_closing = 0; + + timer_.Stop(); + + // The Session instances are kept alive using a in the Endpoint. Removing the + // Session from the Endpoint will free that pointer, allowing the Session to + // be deconstructed once the stack unwinds and any remaining + // BaseObjectPtr instances fall out of scope. + + std::vector cids(ngtcp2_conn_get_num_scid(connection())); + std::vector tokens( + ngtcp2_conn_get_num_active_dcid(connection())); + ngtcp2_conn_get_scid(connection(), cids.data()); + ngtcp2_conn_get_active_dcid(connection(), tokens.data()); + + endpoint_->DisassociateCID(dcid_); + endpoint_->DisassociateCID(preferred_address_cid_); + + for (auto cid : cids) endpoint_->DisassociateCID(CID(&cid)); + + for (auto token : tokens) { + if (token.token_present) + endpoint_->DisassociateStatelessResetToken( + StatelessResetToken(token.token)); + } + + state_->destroyed = 1; + + BaseObjectPtr endpoint = std::move(endpoint_); + + endpoint->RemoveSession(scid_, remote_address_); +} + +Session::TransportParams Session::GetLocalTransportParams() const { + CHECK(!is_destroyed()); + return TransportParams(TransportParams::Type::ENCRYPTED_EXTENSIONS, + ngtcp2_conn_get_local_transport_params(connection())); +} + +Session::TransportParams Session::GetRemoteTransportParams() const { + CHECK(!is_destroyed()); + return TransportParams(TransportParams::Type::ENCRYPTED_EXTENSIONS, + ngtcp2_conn_get_remote_transport_params(connection())); +} + +bool Session::is_in_closing_period() const { + return ngtcp2_conn_is_in_closing_period(connection()); +} + +bool Session::is_in_draining_period() const { + return ngtcp2_conn_is_in_draining_period(connection()); +} + +bool Session::is_unable_to_send_packets() const { + return NgCallbackScope::InNgCallbackScope(*this) || is_destroyed() || + is_in_draining_period() || (is_server() && is_in_closing_period()) || + !endpoint_; +} + +uint64_t Session::max_data_left() const { + return ngtcp2_conn_get_max_data_left(connection()); +} + +uint64_t Session::max_local_streams_uni() const { + return ngtcp2_conn_get_max_local_streams_uni(connection()); +} + +uint64_t Session::max_local_streams_bidi() const { + return ngtcp2_conn_get_local_transport_params(connection()) + ->initial_max_streams_bidi; +} + +quic_version Session::version() const { + return ngtcp2_conn_get_negotiated_version(connection()); +} + +void Session::ExtendOffset(size_t amount) { + DEBUG_ARGS(this, "Extending offset by %" PRIu64 " bytes", amount); + ngtcp2_conn_extend_max_offset(connection(), amount); +} + +void Session::ExtendStreamOffset(stream_id id, size_t amount) { + DEBUG_ARGS(this, + "Extending stream %" PRIi64 " offset by %" PRIu64 " bytes", + id, + amount); + ngtcp2_conn_extend_max_stream_offset(connection(), id, amount); +} + +void Session::UpdateTimer() { + // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. + uint64_t expiry = ngtcp2_conn_get_expiry(connection()); + uint64_t now = uv_hrtime(); + + if (expiry <= now) { + // The timer has already expired. + return OnTimeout(); + } + + auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; + + DEBUG_ARGS(this, "Setting transmission timer to %" PRIu64, timeout); + + // If timeout is zero here, it means our timer is less than a millisecond + // off from expiry. Let's bump the timer to 1. + timer_.Update(timeout == 0 ? 1 : timeout); +} + +void Session::OnTimeout() { + HandleScope scope(env()->isolate()); + + if (is_destroyed()) return; + + DEBUG(this, "Transmission timer has expired"); + DEBUG_ARGS( + this, "In Closing Period? %s", is_in_closing_period() ? "Yes" : "No"); + DEBUG_ARGS( + this, "In Draining Period? %s", is_in_draining_period() ? "Yes" : "No"); + + int ret = ngtcp2_conn_handle_expiry(connection(), uv_hrtime()); + if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { + DEBUG(this, "Retransmitting"); + SendPendingDataScope send_scope(this); + return; + } + + DEBUG(this, "Closing silently after timeout"); + last_error_ = QuicError::ForNgtcp2Error(ret); + CloseSilently(); +} + +void Session::SendConnectionClose() { + CHECK(!NgCallbackScope::InNgCallbackScope(*this)); + + if (is_destroyed() || is_in_draining_period() || state_->silent_close) return; + + DEBUG(this, "Sending connection close"); + + auto on_exit = OnScopeLeave([this] { UpdateTimer(); }); + + switch (crypto_context_.side()) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + if (!is_in_closing_period() && !StartClosingPeriod()) { + DEBUG(this, "Failed to start closing period gracefully."); + CloseSilently(); + } else { + DEBUG(this, "Started closing period"); + CHECK(conn_closebuf_); + Send(conn_closebuf_->Clone()); + } + return; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + Path path(local_address_, remote_address_); + auto packet = Packet::Create(env(), + endpoint_.get(), + remote_address_, + "immediate connection close (client)"); + ngtcp2_vec vec = *packet; + ssize_t nwrite = ngtcp2_conn_write_connection_close(connection(), + &path, + nullptr, + vec.base, + vec.len, + last_error_, + uv_hrtime()); + + if (UNLIKELY(nwrite < 0)) { + DEBUG(this, "Failed to start closing gracefully"); + last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); + CloseSilently(); + } else { + DEBUG(this, "Starting closing period"); + packet->Truncate(nwrite); + Send(std::move(packet)); + } + return; + } + } + UNREACHABLE(); +} + +void Session::Send(BaseObjectPtr packet) { + CHECK(!is_destroyed()); + CHECK(!is_in_draining_period()); + + if (packet->length() > 0) { + DEBUG_ARGS(this, "Sending packet [%s]", packet.get()); + IncrementStat(&SessionStats::bytes_sent, packet->length()); + endpoint_->Send(std::move(packet)); + } +} + +void Session::Send(BaseObjectPtr packet, const PathStorage& path) { + UpdatePath(path); + return Send(std::move(packet)); +} + +void Session::UpdatePath(const PathStorage& storage) { + remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen); + local_address_.Update(storage.path.local.addr, storage.path.local.addrlen); + DEBUG_ARGS(this, "Path updated %s <=> %s", remote_address_, local_address_); +} + +void Session::SendPendingData() { + if (!is_unable_to_send_packets()) application_->SendPendingData(); +} + +bool Session::StartClosingPeriod() { + if (is_destroyed()) return false; + if (is_in_closing_period()) return true; + + auto packet = Packet::Create( + env(), endpoint_.get(), remote_address_, "server connection close"); + ngtcp2_vec vec = *packet; + + ssize_t nwrite = ngtcp2_conn_write_connection_close(connection(), + nullptr, + nullptr, + vec.base, + vec.len, + last_error_.get(), + uv_hrtime()); + if (nwrite < 0) { + last_error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); + return false; + } + + packet->Truncate(nwrite); + conn_closebuf_ = std::move(packet); + return true; +} + +void Session::UpdateDataStats() { + if (state_->destroyed) return; + + ngtcp2_conn_stat stat; + ngtcp2_conn_get_conn_stat(connection(), &stat); + + SetStat(&SessionStats::bytes_in_flight, stat.bytes_in_flight); + SetStat(&SessionStats::congestion_recovery_start_ts, + stat.congestion_recovery_start_ts); + SetStat(&SessionStats::cwnd, stat.cwnd); + SetStat(&SessionStats::delivery_rate_sec, stat.delivery_rate_sec); + SetStat(&SessionStats::first_rtt_sample_ts, stat.first_rtt_sample_ts); + SetStat(&SessionStats::initial_rtt, stat.initial_rtt); + SetStat(&SessionStats::last_tx_pkt_ts, + reinterpret_cast(stat.last_tx_pkt_ts)); + SetStat(&SessionStats::latest_rtt, stat.latest_rtt); + SetStat(&SessionStats::loss_detection_timer, stat.loss_detection_timer); + SetStat(&SessionStats::loss_time, reinterpret_cast(stat.loss_time)); + SetStat(&SessionStats::max_udp_payload_size, stat.max_udp_payload_size); + SetStat(&SessionStats::min_rtt, stat.min_rtt); + SetStat(&SessionStats::pto_count, stat.pto_count); + SetStat(&SessionStats::rttvar, stat.rttvar); + SetStat(&SessionStats::smoothed_rtt, stat.smoothed_rtt); + SetStat(&SessionStats::ssthresh, stat.ssthresh); + + if (stat.bytes_in_flight > GetStat(&SessionStats::max_bytes_in_flight)) + SetStat(&SessionStats::max_bytes_in_flight, stat.bytes_in_flight); +} + +void Session::AcknowledgeStreamDataOffset(Stream* stream, + uint64_t offset, + uint64_t datalen) { + if (is_destroyed()) return; + application_->AcknowledgeStreamData(stream, offset, datalen); +} + +void Session::ActivateConnectionId(uint64_t seq, + const CID& cid, + Maybe reset_token) { + DEBUG_ARGS(this, "Activating new CID %s", cid); + endpoint_->AssociateCID(scid_, cid); + if (reset_token.IsJust()) { + endpoint_->AssociateStatelessResetToken(reset_token.FromJust(), this); + } +} + +void Session::DeactivateConnectionId(uint64_t seq, + const CID& cid, + Maybe reset_token) { + DEBUG_ARGS(this, "Deactivating CID %s", cid); + endpoint_->DisassociateCID(cid); + if (reset_token.IsJust()) { + endpoint_->DisassociateStatelessResetToken(reset_token.FromJust()); + } +} + +datagram_id Session::SendDatagram(Store&& data) { + auto tp = ngtcp2_conn_get_remote_transport_params(connection()); + uint64_t max_datagram_size = tp->max_datagram_frame_size; + if (max_datagram_size == 0 || data.length() > max_datagram_size) { + DEBUG(this, "Datagram is too large"); + return 0; + } + + BaseObjectPtr packet; + uint8_t* pos = nullptr; + int accepted = 0; + ngtcp2_vec vec = data; + PathStorage path; + int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; + datagram_id did = last_datagram_id_ + 1; + + // Let's give it a max number of attempts to send the datagram + static const int kMaxAttempts = 16; + int attempts = 0; + + for (;;) { + if (!packet) { + packet = + Packet::Create(env(), + endpoint_.get(), + remote_address_, + "datagram", + ngtcp2_conn_get_max_udp_payload_size(connection())); + if (!packet) { + last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); + CloseSilently(); + return 0; + } + pos = ngtcp2_vec(*packet).base; + } + + ssize_t nwrite = ngtcp2_conn_writev_datagram(connection(), + &path.path, + nullptr, + pos, + packet->length(), + &accepted, + flags, + did, + &vec, + 1, + uv_hrtime()); + + if (nwrite < 0) { + switch (nwrite) { + case 0: { + // We cannot send data because of congestion control or the data will + // not fit. Since datagrams are best effort, we are going to abandon + // the attempt and just return. + DEBUG(this, + "Cannot send datagram because of congestion control or size"); + CHECK_EQ(accepted, 0); + return 0; + } + case NGTCP2_ERR_WRITE_MORE: { + // We keep on looping! Keep on sending! + continue; + } + case NGTCP2_ERR_INVALID_STATE: { + // The remote endpoint does not want to accept datagrams. That's ok, + // just return 0. + DEBUG(this, "The remote endpoint does not support datagrams"); + return 0; + } + case NGTCP2_ERR_INVALID_ARGUMENT: { + // The datagram is too large. That should have been caught above but + // that's ok. We'll just abandon the attempt and return. + DEBUG(this, "The datagram is too large"); + return 0; + } + } + last_error_ = QuicError::ForNgtcp2Error(nwrite); + CloseSilently(); + return 0; + } + + // In this case, a complete packet was written and we need to send it along. + packet->Truncate(nwrite); + Send(std::move(packet)); + ngtcp2_conn_update_pkt_tx_time(connection(), uv_hrtime()); + + if (accepted != 0) { + // Yay! The datagram was accepted into the packet we just sent and we can + // just return the datagram ID. + last_datagram_id_ = did; + return did; + } + + // We sent a packet, but it wasn't the datagram packet. That can happen. + // Let's loop around and try again. + if (++attempts == kMaxAttempts) { + DEBUG(this, "Too many attempts to send datagram"); + break; + } + } + + return 0; +} + +void Session::DatagramAcknowledged(datagram_id id) { + EmitDatagramAcknowledged(id); +} + +void Session::DatagramLost(datagram_id id) { + EmitDatagramLost(id); +} + +void Session::DatagramReceived(const uint8_t* data, + size_t datalen, + DatagramReceivedFlag flag) { + // If there is nothing watching for the datagram on the JavaScript side, + // we just drop it on the floor. + if (state_->datagram == 0 || datalen == 0) return; + + DEBUG_ARGS(this, "%" PRIu64 " byte datagram received", datalen); + + auto backing = ArrayBuffer::NewBackingStore(env()->isolate(), datalen); + memcpy(backing->Data(), data, datalen); + EmitDatagram(Store(std::move(backing), datalen), flag); +} + +void Session::ExtendMaxStreamData(Stream* stream, uint64_t max) { + application_->ExtendMaxStreamData(stream, max); +} + +void Session::ExtendMaxStreams(EndpointLabel label, + Direction direction, + uint64_t max) { + application_->ExtendMaxStreams(label, direction, max); +} + +bool Session::GenerateNewConnectionId(ngtcp2_cid* cid, + size_t len, + uint8_t* token) { + CID cid_(cid); + cid_factory_.Generate(cid, len); + DEBUG_ARGS(this, "Generated new CID %s", cid_); + StatelessResetToken new_token( + token, endpoint_->options().reset_token_secret, cid_); + endpoint_->AssociateCID(cid_, scid_); + endpoint_->AssociateStatelessResetToken(new_token, this); + return true; +} + +bool Session::Receive(Store&& store, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + CHECK(!is_destroyed()); + + const auto receivePacket = [&](ngtcp2_path* path, ngtcp2_vec vec) { + CHECK(!is_destroyed()); + + uint64_t now = uv_hrtime(); + ngtcp2_pkt_info pi{}; // Not used but required. + DEBUG_ARGS(this, "Reading %" PRIu64 " byte packet", vec.len); + int err = + ngtcp2_conn_read_pkt(connection(), path, &pi, vec.base, vec.len, now); + switch (err) { + case 0: { + DEBUG(this, "Packet received successfully"); + // Return true so we send after receiving. + return true; + } + case NGTCP2_ERR_DRAINING: { + DEBUG(this, "Connection entered draining state"); + // Connection has entered the draining state, no further data should be + // sent. This happens when the remote peer has sent a CONNECTION_CLOSE. + return false; + } + case NGTCP2_ERR_CRYPTO: { + // Crypto error happened! Set the last error to the tls alert + last_error_ = + QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(connection())); + DEBUG_ARGS(this, "Encountered crypto error [%s]", last_error_); + Close(); + return false; + } + case NGTCP2_ERR_RETRY: { + // This should only ever happen on the server. We have to sent a path + // validation challenge in the form of a RETRY packet to the peer and + // drop the connection. + CHECK(is_server()); + DEBUG(this, "Needs path validation"); + endpoint_->SendRetry( + version(), dcid_, scid_, local_address_, remote_address_); + CloseSilently(); + return false; + } + case NGTCP2_ERR_DROP_CONN: { + DEBUG(this, "Needs drop connection"); + // There's nothing else to do but drop the connection state. + CloseSilently(); + return false; + } + } + // Shouldn't happen but just in case. + last_error_ = QuicError::ForNgtcp2Error(err); + DEBUG_ARGS(this, + "There was an error processing the packet [%d, %s]", + err, + last_error_); + Close(); + return false; + }; + + DEBUG_ARGS(this, + "Receiving a packet with %" PRIu64 " bytes [%s => %s]", + store.length(), + remote_address, + local_address); + + auto update_stats = OnScopeLeave([&] { UpdateDataStats(); }); + remote_address_ = remote_address; + Path path(local_address, remote_address_); + IncrementStat(&SessionStats::bytes_received, store.length()); + if (receivePacket(&path, store)) SendPendingDataScope send_scope(this); + + if (!is_destroyed()) UpdateTimer(); + + return true; +} + +int Session::ReceiveCryptoData(ngtcp2_crypto_level level, + uint64_t offset, + const uint8_t* data, + size_t datalen) { + DEBUG_ARGS(this, "Receiving %" PRIu64 " bytes of crypto data", datalen); + return crypto_context_.Receive(level, offset, data, datalen); +} + +bool Session::ReceiveRxKey(ngtcp2_crypto_level level) { + // If this is the client side, we'll initialize our application now. + DEBUG(this, "Received rx key"); + return !is_server() && level == NGTCP2_CRYPTO_LEVEL_APPLICATION + ? application_->Start() + : true; +} + +bool Session::ReceiveTxKey(ngtcp2_crypto_level level) { + // If this is the server side, we'll initialize our application now. + DEBUG(this, "Received tx key"); + return is_server() && level == NGTCP2_CRYPTO_LEVEL_APPLICATION + ? application_->Start() + : true; +} + +void Session::ReceiveNewToken(const ngtcp2_vec* token) { + // Currently, we don't do anything with this. We may want to use it in the + // future. + DEBUG(this, "Received new token"); +} + +void Session::ReceiveStatelessReset(const ngtcp2_pkt_stateless_reset* sr) { + state_->stateless_reset = 1; + DEBUG(this, "Received stateless reset"); + // TODO(@jasnell): Currently, we don't do anything more with this. What should + // we do? + // It's not yet clear if we handle this or if ngtcp2 does the + // right thing. +} + +void Session::ReceiveStreamData(Stream* stream, + Application::ReceiveStreamDataFlags flags, + uint64_t offset, + const uint8_t* data, + size_t datalen) { + application_->ReceiveStreamData(stream, flags, data, datalen, offset); +} + +void Session::RemoveConnectionId(const CID& cid) { + endpoint_->DisassociateCID(cid); +} + +void Session::SelectPreferredAddress(const PreferredAddress& preferredAddress) { + if (options_.preferred_address_strategy == + PreferredAddress::Policy::IGNORE_PREFERED) { + DEBUG(this, "Ignoring preferred address"); + return; + } + + auto local_address = endpoint_->local_address().ToChecked(); + int family = local_address.family(); + + switch (family) { + case AF_INET: { + auto ipv4 = preferredAddress.ipv4(); + if (ipv4.IsJust()) { + PreferredAddress::AddressInfo info = ipv4.FromJust(); + if (info.address.empty() || info.port == 0) return; + SocketAddress::New( + AF_INET, info.address.c_str(), info.port, &remote_address_); + DEBUG_ARGS(this, "Using preferred IPv4 address [%s]", remote_address_); + state_->using_preferred_address = 1; + preferredAddress.Use(info); + } + break; + } + case AF_INET6: { + auto ipv6 = preferredAddress.ipv6(); + if (ipv6.IsJust()) { + PreferredAddress::AddressInfo info = ipv6.FromJust(); + if (info.address.empty() || info.port == 0) return; + SocketAddress::New( + AF_INET, info.address.c_str(), info.port, &remote_address_); + DEBUG_ARGS(this, "Using preferred IPv6 address [%s]", remote_address_); + state_->using_preferred_address = 1; + preferredAddress.Use(info); + } + break; + } + } +} + +void Session::StreamClose(Stream* stream, Maybe error) { + application_->StreamClose(stream, error); +} + +void Session::StreamOpen(stream_id id) { + // Currently, we don't do anything with stream open. That may change later. +} + +void Session::StreamReset(Stream* stream, + uint64_t final_size, + QuicError error) { + application_->StreamReset(stream, final_size, error); +} + +void Session::StreamStopSending(Stream* stream, QuicError error) { + application_->StreamStopSending(stream, error); +} + +void Session::ReportPathValidationStatus(PathValidationResult result, + PathValidationFlags flags, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + EmitPathValidation(result, flags, local_address, remote_address); +} + +void Session::SetSessionTicketAppData(const SessionTicketAppData& app_data) { + application_->SetSessionTicketAppData(app_data); +} + +SessionTicketAppData::Status Session::GetSessionTicketAppData( + const SessionTicketAppData& app_data, SessionTicketAppData::Flag flag) { + return application_->GetSessionTicketAppData(app_data, flag); +} + +bool Session::can_create_streams() const { + return !state_->destroyed && !state_->graceful_closing && !state_->closing && + !is_in_closing_period() && !is_in_draining_period(); +} + +BaseObjectPtr Session::CreateStream(stream_id id) { + if (!can_create_streams()) { + DEBUG(this, "Unable to create streams right now"); + // No-can-do-s-ville, My friend. + return BaseObjectPtr(); + } + auto stream = Stream::Create(env(), this, id); + if (stream) DEBUG_ARGS(this, "Created stream %" PRIu64, id); + AddStream(stream); + return stream; +} + +BaseObjectPtr Session::OpenStream(Direction direction) { + if (!can_create_streams()) { + return BaseObjectPtr(); + } + stream_id id; + switch (direction) { + case Direction::BIDIRECTIONAL: + DEBUG(this, "Opening bidirectional stream"); + if (ngtcp2_conn_open_bidi_stream(connection(), &id, nullptr) == 0) { + return CreateStream(id); + } + break; + case Direction::UNIDIRECTIONAL: + DEBUG(this, "Opening unidirectional stream"); + if (ngtcp2_conn_open_uni_stream(connection(), &id, nullptr) == 0) { + return CreateStream(id); + } + break; + default: + UNREACHABLE(); + } + return BaseObjectPtr(); +} + +void Session::AddStream(const BaseObjectPtr& stream) { + streams_[stream->id()] = stream; + + DEBUG_ARGS(this, "Added stream %" PRIi64, stream->id()); + + // Update tracking statistics for the number of streams associated with this + // session. + switch (stream->origin()) { + case Stream::Origin::CLIENT: + if (is_server()) + IncrementStat(&SessionStats::streams_in_count); + else + IncrementStat(&SessionStats::streams_out_count); + break; + case Stream::Origin::SERVER: + if (is_server()) + IncrementStat(&SessionStats::streams_out_count); + else + IncrementStat(&SessionStats::streams_in_count); + } + IncrementStat(&SessionStats::streams_out_count); + switch (stream->direction()) { + case Direction::BIDIRECTIONAL: + IncrementStat(&SessionStats::bidi_stream_count); + break; + case Direction::UNIDIRECTIONAL: + IncrementStat(&SessionStats::uni_stream_count); + break; + } +} + +void Session::RemoveStream(stream_id id) { + // ngtcp2 does not extend the max streams count automatically except in very + // specific conditions, none of which apply once we've gotten this far. We + // need to manually extend when a remote peer initiated stream is removed. + if (!is_in_draining_period() && !is_in_closing_period() && + !state_->silent_close && + !ngtcp2_conn_is_local_stream(connection_.get(), id)) { + if (ngtcp2_is_bidi_stream(id)) + ngtcp2_conn_extend_max_streams_bidi(connection_.get(), 1); + else + ngtcp2_conn_extend_max_streams_uni(connection_.get(), 1); + } + + DEBUG_ARGS(this, "Removing stream %" PRIi64, id); + + // Frees the persistent reference to the Stream object, allowing it to be gc'd + // any time after the JS side releases it's own reference. + streams_.erase(id); +} + +void Session::ResumeStream(stream_id id) { + application_->ResumeStream(id); +} + +void Session::ShutdownStream(stream_id id, QuicError code) { + if (is_in_closing_period() || is_in_draining_period() || + state_->silent_close == 1) { + DEBUG_ARGS(this, "Shutdown stream " PRIi64 " [%s]", code); + SendPendingDataScope send_scope(this); + ngtcp2_conn_shutdown_stream(connection(), + id, + code.type() == QuicError::Type::APPLICATION + ? code.code() + : NGTCP2_APP_NOERROR); + } +} + +void Session::StreamDataBlocked(stream_id id) { + IncrementStat(&SessionStats::block_count); + application_->BlockStream(id); +} + +void Session::ShutdownStreamWrite(stream_id id, QuicError code) { + if (is_in_closing_period() || is_in_draining_period() || + state_->silent_close == 1) { + return; // Nothing to do because we can't send any frames. + } + DEBUG_ARGS(this, "Shutdown stream %" PRIi64 " write [%s]", id, code); + SendPendingDataScope send_scope(this); + ngtcp2_conn_shutdown_stream_write( + connection(), + id, + code.type() == QuicError::Type::APPLICATION ? code.code() : 0); +} + +// ====================================================================================== +// V8 Callouts + +void Session::EmitDatagramAcknowledged(datagram_id id) { + CHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + auto& state = BindingState::Get(env()); + + CallbackScope cb_scope(this); + + Local arg = BigInt::NewFromUnsigned(env()->isolate(), id); + MakeCallback(state.session_datagram_ack_callback(), 1, &arg); +} + +void Session::EmitDatagramLost(datagram_id id) { + CHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + auto& state = BindingState::Get(env()); + + CallbackScope cb_scope(this); + + Local arg = BigInt::NewFromUnsigned(env()->isolate(), id); + MakeCallback(state.session_datagram_lost_callback(), 1, &arg); +} + +void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlag flag) { + CHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + auto& state = BindingState::Get(env()); + + CallbackScope cbv_scope(this); + + Local argv[] = { + datagram.ToArrayBufferView(env()), + flag.early ? v8::True(env()->isolate()) : v8::False(env()->isolate()), + }; + + DEBUG(this, "Emitting datagram event"); + + MakeCallback(state.session_datagram_callback(), arraysize(argv), argv); +} + +void Session::EmitHandshakeComplete() { + CHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + auto& state = BindingState::Get(env()); + + CallbackScope cb_scope(this); + + auto isolate = env()->isolate(); + Local argv[] = {Undefined(isolate), // The negotiated server name + GetALPNProtocol(this), // The negotiated alpn protocol + Undefined(isolate), // Cipher name + Undefined(isolate), // Cipher version + Undefined(isolate), // Validation error reason + Undefined(isolate), // Validation error code + crypto_context_.was_early_data_accepted() + ? v8::True(isolate) + : v8::False(isolate)}; + + static constexpr auto kServerName = 0; + static constexpr auto kCipherName = 2; + static constexpr auto kCipherVersion = 3; + static constexpr auto kValidationErrorReason = 4; + static constexpr auto kValidationErrorCode = 5; + + int err = crypto_context_.VerifyPeerIdentity(); + + if (!ToV8Value(env()->context(), crypto_context_.servername()) + .ToLocal(&argv[kServerName]) || + !crypto_context_.cipher_name(env()).ToLocal(&argv[kCipherName]) || + !crypto_context_.cipher_version(env()).ToLocal(&argv[kCipherVersion])) { + return; + } + + if (err != X509_V_OK && (!crypto::GetValidationErrorReason(env(), err) + .ToLocal(&argv[kValidationErrorReason]) || + !crypto::GetValidationErrorCode(env(), err) + .ToLocal(&argv[kValidationErrorCode]))) { + return; + } + + DEBUG(this, "Emitting handshake complete event"); + + MakeCallback(state.session_handshake_callback(), arraysize(argv), argv); +} + +void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, + const quic_version* sv, + size_t nsv) { + CHECK(!is_destroyed()); + CHECK(!is_server()); + if (!env()->can_call_into_js()) return; + + auto& state = BindingState::Get(env()); + + CallbackScope cb_scope(this); + + std::vector> versions(nsv); + + auto isolate = env()->isolate(); + + for (size_t n = 0; n < nsv; n++) versions[n] = Integer::New(isolate, sv[n]); + + // Currently, we only support one version of QUIC but in + // the future that may change. The callback below passes + // an array back to the JavaScript side to future-proof. + Local supported = Integer::New(isolate, NGTCP2_PROTO_VER_MAX); + + Local argv[] = {// The version configured for this session. + Integer::New(isolate, version()), + // The versions requested. + Array::New(isolate, versions.data(), nsv), + // The versions we actually support. + Array::New(isolate, &supported, 1)}; + + DEBUG(this, "Emitting version negotiation event"); + + MakeCallback( + state.session_version_negotiation_callback(), arraysize(argv), argv); +} + +void Session::EmitSessionTicket(Store&& ticket) { + CHECK(!is_destroyed()); + if (!env()->can_call_into_js()) return; + + // If there is nothing listening for the session ticket, don't both emitting + // it. + if (LIKELY(state_->session_ticket == 0)) return; + + CallbackScope cb_scope(this); + + auto remote_transport_params = GetRemoteTransportParams(); + Store transport_params; + if (remote_transport_params) + transport_params = remote_transport_params.Encode(env()); + + auto sessionTicket = SessionTicket::Create( + env(), std::move(ticket), std::move(transport_params)); + Local argv = sessionTicket->object(); + + DEBUG(this, "Emitting session ticket event"); + + MakeCallback(BindingState::Get(env()).session_ticket_callback(), 1, &argv); +} + +void Session::EmitError(QuicError error) { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return Destroy(); + + CallbackScope cb_scope(this); + Local argv[] = { + Integer::New(env()->isolate(), static_cast(error.type())), + BigInt::NewFromUnsigned(env()->isolate(), error.code()), + Undefined(env()->isolate()), + }; + if (error->reasonlen > 0 && + !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { + return; + } + MakeCallback( + BindingState::Get(env()).session_error_callback(), arraysize(argv), argv); +} + +void Session::EmitClose() { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return Destroy(); + + CallbackScope cb_scope(this); + + // If last_error_.code() is anything other than 0, then we will emit + // an error. Otherwise, nothing will be emitted. + if (last_error_->error_code == NGTCP2_NO_ERROR || + (last_error_->type == + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION && + last_error_->error_code == NGTCP2_APP_NOERROR)) { + MakeCallback(BindingState::Get(env()).session_close_callback(), 0, nullptr); + return; + } + + // Otherwise, we will emit the codes and let the JavaScript side construct a + // proper error from them. + + EmitError(last_error_); +} + +bool Session::EmitClientHello() { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) { + // If we can't call into JavaScript here, technically we probably shouldn't + // be able to do anything... but, let's try to gracefully handle it without + // blocking the tls handshake. If there's an error, we'll handle down the + // line. + crypto_context_.OnClientHelloDone(); + return true; + } + + auto isolate = env()->isolate(); + + CallbackScope cb_scope(this); + + Local argv[3] = { + Undefined(isolate), Undefined(isolate), Undefined(isolate)}; + + if (!crypto_context_.hello_alpn(env()).ToLocal(&argv[0]) || + !crypto_context_.hello_servername(env()).ToLocal(&argv[1]) || + !crypto_context_.hello_ciphers(env()).ToLocal(&argv[2])) { + return false; + } + + DEBUG(this, "Emitting client hello event"); + + MakeCallback(BindingState::Get(env()).session_client_hello_callback(), + arraysize(argv), + argv); + + return true; +} + +bool Session::EmitOCSP() { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) { + // If we can't call into JavaScript here, technically we probably shouldn't + // be able to do anything... but, let's try to gracefully handle it without + // blocking the tls handshake. If there's an error, we'll handle down the + // line. + crypto_context_.OnOCSPDone(Store()); + return true; + } + + auto isolate = env()->isolate(); + + CallbackScope cb_scope(this); + + Local argv[2] = {v8::Undefined(isolate), v8::Undefined(isolate)}; + + if (!GetCertificateData(env(), + crypto_context_.secure_context_.get(), + GetCertificateType::SELF) + .ToLocal(&argv[0]) || + !GetCertificateData(env(), + crypto_context_.secure_context_.get(), + GetCertificateType::ISSUER) + .ToLocal(&argv[1])) { + return false; + } + + DEBUG(this, "Emitting ocsp request event"); + + MakeCallback(BindingState::Get(env()).session_ocsp_request_callback(), + arraysize(argv), + argv); + return true; +} + +void Session::EmitOCSPResponse() { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return; + + auto isolate = env()->isolate(); + + CallbackScope cb_scope(this); + + Local res = v8::Undefined(isolate); + + const unsigned char* resp; + int len = SSL_get_tlsext_status_ocsp_resp(crypto_context_.ssl_.get(), &resp); + if (resp != nullptr && len > 0) { + std::shared_ptr store = + ArrayBuffer::NewBackingStore(isolate, len); + memcpy(store->Data(), resp, len); + res = ArrayBuffer::New(isolate, store); + } + + DEBUG(this, "Emitting ocsp response event"); + + MakeCallback( + BindingState::Get(env()).session_ocsp_response_callback(), 1, &res); +} + +void Session::EmitPathValidation(PathValidationResult result, + PathValidationFlags flags, + const SocketAddress& local_address, + const SocketAddress& remote_address) { + CHECK(!is_destroyed()); + + if (!env()->can_call_into_js()) return; + + if (LIKELY(state_->path_validation == 0)) return; + + auto isolate = env()->isolate(); + + CallbackScope cb_scope(this); + + auto& state = BindingState::Get(env()); + + const auto resultToString = [&] { + switch (result) { + case PathValidationResult::ABORTED: + return state.aborted_string(); + case PathValidationResult::FAILURE: + return state.failure_string(); + case PathValidationResult::SUCCESS: + return state.success_string(); + } + UNREACHABLE(); + }; + + Local argv[4] = { + resultToString(), + SocketAddressBase::Create(env(), + std::make_shared(local_address)) + ->object(), + SocketAddressBase::Create(env(), + std::make_shared(remote_address)) + ->object(), + flags.preferredAddress ? v8::True(isolate) : v8::False(isolate), + }; + + DEBUG(this, "Emitting path validation event"); + + MakeCallback(BindingState::Get(env()).session_ocsp_request_callback(), + arraysize(argv), + argv); +} + +void Session::EmitKeylog(const char* line) { + if (!env()->can_call_into_js()) return; + if (keylogstream_) { + std::string data = line; + data += "\n"; + env()->SetImmediate([ptr = keylogstream_, data = std::move(data)]( + Environment* env) { ptr->Emit(data); }); + } +} + +void Session::EmitNewStream(const BaseObjectPtr& stream) { + if (is_destroyed()) return; + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + Local arg = stream->object(); + + DEBUG(this, "Emitting new stream event"); + + MakeCallback(BindingState::Get(env()).stream_created_callback(), 1, &arg); +} + +// ====================================================================================== +// V8 Callbacks + +void Session::DoDestroy(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->Destroy(); +} + +void Session::GetRemoteAddress(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + auto address = session->remote_address(); + args.GetReturnValue().Set( + SocketAddressBase::Create(env, std::make_shared(address)) + ->object()); +} + +void Session::GetCertificate(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context().cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::GetEphemeralKeyInfo(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (!session->is_server() && + session->crypto_context().ephemeral_key(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::GetPeerCertificate(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context().peer_cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::GracefulClose(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->CloseGracefully(); +} + +void Session::SilentClose(const FunctionCallbackInfo& args) { + // This is intended for testing only! + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->CloseSilently(); +} + +void Session::UpdateKey(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + // Initiating a key update may fail if it is done too early (either + // before the TLS handshake has been confirmed or while a previous + // key update is being processed). When it fails, InitiateKeyUpdate() + // will return false. + session->IncrementStat(&SessionStats::keyupdate_count); + args.GetReturnValue().Set(session->crypto_context().InitiateKeyUpdate()); +} + +void Session::OnClientHelloDone(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->crypto_context().OnClientHelloDone(); +} + +void Session::OnOCSPDone(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + if (args[0]->IsArrayBufferView()) { + session->crypto_context().OnOCSPDone(Store(args[0].As())); + } else { + session->crypto_context().OnOCSPDone(Store()); + } +} + +void Session::DoOpenStream(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsUint32()); + Direction direction = static_cast(args[0].As()->Value()); + BaseObjectPtr stream = session->OpenStream(direction); + + if (stream) args.GetReturnValue().Set(stream->object()); +} + +void Session::DoSendDatagram(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsArrayBufferView()); + args.GetReturnValue().Set( + BigInt::New(env->isolate(), + session->SendDatagram(Store(args[0].As())))); +} + +// ====================================================================================== +// ngtcp2 static callbacks + +const ngtcp2_callbacks Session::callbacks[2] = { + // NGTCP2_CRYPTO_SIDE_CLIENT + {ngtcp2_crypto_client_initial_cb, + nullptr, + OnReceiveCryptoData, + OnHandshakeCompleted, + OnReceiveVersionNegotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAcknowledgeStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnReceiveStatelessReset, + ngtcp2_crypto_recv_retry_cb, + OnExtendMaxStreamsBidi, + OnExtendMaxStreamsUni, + OnRand, + OnGetNewConnectionId, + OnRemoveConnectionId, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + OnSelectPreferredAddress, + OnStreamReset, + OnExtendMaxRemoteStreamsBidi, + OnExtendMaxRemoteStreamsUni, + OnExtendMaxStreamData, + OnConnectionIdStatus, + OnHandshakeConfirmed, + OnReceiveNewToken, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + OnReceiveDatagram, + OnAcknowledgeDatagram, + OnLostDatagram, + OnGetPathChallengeData, + OnStreamStopSending, + ngtcp2_crypto_version_negotiation_cb, + OnReceiveRxKey, + OnReceiveTxKey}, + // NGTCP2_CRYPTO_SIDE_SERVER + {nullptr, + ngtcp2_crypto_recv_client_initial_cb, + OnReceiveCryptoData, + OnHandshakeCompleted, + nullptr, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAcknowledgeStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnReceiveStatelessReset, + nullptr, + OnExtendMaxStreamsBidi, + OnExtendMaxStreamsUni, + OnRand, + OnGetNewConnectionId, + OnRemoveConnectionId, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + nullptr, + OnStreamReset, + OnExtendMaxRemoteStreamsBidi, + OnExtendMaxRemoteStreamsUni, + OnExtendMaxStreamData, + OnConnectionIdStatus, + nullptr, + nullptr, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + OnReceiveDatagram, + OnAcknowledgeDatagram, + OnLostDatagram, + OnGetPathChallengeData, + OnStreamStopSending, + ngtcp2_crypto_version_negotiation_cb, + OnReceiveRxKey, + OnReceiveTxKey}}; + +#define NGTCP2_CALLBACK_SCOPE(name) \ + auto name = Session::From(conn, user_data); \ + if (UNLIKELY(name->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; \ + NgCallbackScope callback_scope(name); + +int Session::OnAcknowledgeStreamDataOffset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->AcknowledgeStreamDataOffset( + Stream::From(conn, stream_user_data), offset, datalen); + return NGTCP2_SUCCESS; +} + +int Session::OnAcknowledgeDatagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->DatagramAcknowledged(dgram_id); + return NGTCP2_SUCCESS; +} + +int Session::OnConnectionIdStatus(ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + switch (type) { + case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: + session->ActivateConnectionId(seq, + CID(cid), + token != nullptr + ? Just(StatelessResetToken(token)) + : Nothing()); + break; + case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: + session->DeactivateConnectionId(seq, + CID(cid), + token != nullptr + ? Just(StatelessResetToken(token)) + : Nothing()); + break; + default: + UNREACHABLE(); + } + return NGTCP2_SUCCESS; +} + +int Session::OnExtendMaxRemoteStreamsBidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ExtendMaxStreams( + EndpointLabel::REMOTE, Direction::BIDIRECTIONAL, max_streams); + return NGTCP2_SUCCESS; +} + +int Session::OnExtendMaxRemoteStreamsUni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ExtendMaxStreams( + EndpointLabel::REMOTE, Direction::UNIDIRECTIONAL, max_streams); + return NGTCP2_SUCCESS; +} + +int Session::OnExtendMaxStreamsBidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ExtendMaxStreams( + EndpointLabel::LOCAL, Direction::BIDIRECTIONAL, max_streams); + return NGTCP2_SUCCESS; +} + +int Session::OnExtendMaxStreamData(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ExtendMaxStreamData(Stream::From(conn, stream_user_data), max_data); + return NGTCP2_SUCCESS; +} + +int Session::OnExtendMaxStreamsUni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ExtendMaxStreams( + EndpointLabel::LOCAL, Direction::BIDIRECTIONAL, max_streams); + return NGTCP2_SUCCESS; +} + +int Session::OnGetNewConnectionId(ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + return session->GenerateNewConnectionId(cid, cidlen, token) + ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnGetPathChallengeData(ngtcp2_conn* conn, + uint8_t* data, + void* user_data) { + // For now, simple random data will suffice. Later we might need to make this + // more cryptographically secure / pseudorandom for more protection. + EntropySource(data, NGTCP2_PATH_CHALLENGE_DATALEN); + return NGTCP2_SUCCESS; +} + +int Session::OnHandshakeCompleted(ngtcp2_conn* conn, void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + return session->HandshakeCompleted() ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnHandshakeConfirmed(ngtcp2_conn* conn, void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->HandshakeConfirmed(); + return NGTCP2_SUCCESS; +} + +int Session::OnLostDatagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->DatagramLost(dgram_id); + return NGTCP2_SUCCESS; +} + +int Session::OnPathValidation(ngtcp2_conn* conn, + uint32_t flags, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + + session->ReportPathValidationStatus( + static_cast(res), + PathValidationFlags{/* .preferredAddres = */ QUIC_FLAG( + NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR)}, + SocketAddress(path->local.addr), + SocketAddress(path->remote.addr)); + return NGTCP2_SUCCESS; +} + +int Session::OnReceiveCryptoData(ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + return session->ReceiveCryptoData(crypto_level, offset, data, datalen); +} + +int Session::OnReceiveDatagram(ngtcp2_conn* conn, + uint32_t flags, + const uint8_t* data, + size_t datalen, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->DatagramReceived(data, + datalen, + DatagramReceivedFlag{/* .early = */ QUIC_FLAG( + NGTCP2_DATAGRAM_FLAG_EARLY)}); + return NGTCP2_SUCCESS; +} + +int Session::OnReceiveNewToken(ngtcp2_conn* conn, + const ngtcp2_vec* token, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ReceiveNewToken(token); + return NGTCP2_SUCCESS; +} + +int Session::OnReceiveRxKey(ngtcp2_conn* conn, + ngtcp2_crypto_level level, + void* user_data) { + return Session::From(conn, user_data)->ReceiveRxKey(level) + ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnReceiveTxKey(ngtcp2_conn* conn, + ngtcp2_crypto_level level, + void* user_data) { + return Session::From(conn, user_data)->ReceiveTxKey(level) + ? NGTCP2_SUCCESS + : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnReceiveStatelessReset(ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->ReceiveStatelessReset(sr); + return NGTCP2_SUCCESS; +} + +int Session::OnReceiveStreamData(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session); + + Application::ReceiveStreamDataFlags receive_flags{ + /* .fin = */ QUIC_FLAG(NGTCP2_STREAM_DATA_FLAG_FIN), + /* .early = */ QUIC_FLAG(NGTCP2_STREAM_DATA_FLAG_EARLY)}; + + if (stream_user_data == nullptr) { + // What we likely have here is an implicitly created stream. Let's try to + // create it. If successful, we'll pass our lovely bit of received data on + // to it for processing. Otherwise, we are going to tell ngtcp2 to shut down + // the stream. + auto stream = session->CreateStream(stream_id); + if (stream) { + session->EmitNewStream(stream); + session->ReceiveStreamData( + stream.get(), receive_flags, offset, data, datalen); + } else { + USE(ngtcp2_conn_shutdown_stream( + session->connection(), stream_id, NGTCP2_APP_NOERROR)); + } + } else { + session->ReceiveStreamData(Stream::From(conn, stream_user_data), + receive_flags, + offset, + data, + datalen); + } + return NGTCP2_SUCCESS; +} + +int Session::OnReceiveVersionNegotiation(ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->EmitVersionNegotiation(*hd, sv, nsv); + return NGTCP2_SUCCESS; +} + +int Session::OnRemoveConnectionId(ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->RemoveConnectionId(CID(cid)); + return NGTCP2_SUCCESS; +} + +int Session::OnSelectPreferredAddress(ngtcp2_conn* conn, + ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->SelectPreferredAddress( + PreferredAddress(session->env(), dest, paddr)); + return NGTCP2_SUCCESS; +} + +int Session::OnStreamClose(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->StreamClose(Stream::From(conn, stream_user_data), + QUIC_FLAG(NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) + ? Just(QuicError::ForApplication(app_error_code)) + : Nothing()); + return NGTCP2_SUCCESS; +} + +int Session::OnStreamOpen(ngtcp2_conn* conn, + int64_t stream_id, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->StreamOpen(stream_id); + return NGTCP2_SUCCESS; +} + +int Session::OnStreamReset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->StreamReset(Stream::From(conn, stream_user_data), + final_size, + QuicError::ForApplication(app_error_code)); + return NGTCP2_SUCCESS; +} + +int Session::OnStreamStopSending(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + NGTCP2_CALLBACK_SCOPE(session); + session->StreamStopSending(Stream::From(conn, stream_user_data), + QuicError::ForApplication(app_error_code)); + return NGTCP2_SUCCESS; +} + +void Session::OnRand(uint8_t* dest, size_t destlen, const ngtcp2_rand_ctx*) { + EntropySource(dest, destlen); +} + +// ====================================================================================== +// Session::OptionsObject + +void Session::OptionsObject::Initialize(Environment* env, + Local target) { + SetConstructorFunction(env->context(), + target, + "SessionOptions", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void Session::OptionsObject::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); +} + +Local Session::OptionsObject::GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.session_options_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + OptionsObject::kInternalFieldCount); + tmpl->SetClassName(state.session_options_string()); + state.set_session_options_constructor_template(tmpl); + } + return tmpl; +} + +Session::OptionsObject::OptionsObject(Environment* env, + v8::Local object) + : BaseObject(env, object) { + MakeWeak(); +} + +template +Maybe Session::OptionsObject::SetOption(Opt* options, + const Local& object, + const Local& name, + uint64_t Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + + uint64_t val = 0; + if (value->IsBigInt()) { + bool lossless = true; + val = value.As()->Uint64Value(&lossless); + if (!lossless) { + Utf8Value label(env()->isolate(), name); + THROW_ERR_OUT_OF_RANGE( + env(), + (std::string("options.") + (*label) + " is out of range").c_str()); + return Nothing(); + } + } else { + val = static_cast(value.As()->Value()); + } + options->*member = val; + return Just(true); +} + +template +Maybe Session::OptionsObject::SetOption(Opt* options, + const Local& object, + const Local& name, + uint32_t Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + + if (value->IsUndefined()) return Just(false); + + CHECK(value->IsUint32()); + uint32_t val = value.As()->Value(); + options->*member = val; + return Just(true); +} + +template +Maybe Session::OptionsObject::SetOption(Opt* options, + const Local& object, + const Local& name, + bool Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + if (value->IsUndefined()) return Just(false); + CHECK(value->IsBoolean()); + options->*member = value->IsTrue(); + return Just(true); +} + +template +Maybe Session::OptionsObject::SetOption(Opt* options, + const Local& object, + const Local& name, + std::string Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + if (value->IsUndefined()) return Just(false); + Utf8Value val(env()->isolate(), value); + options->*member = val.ToString(); + return Just(true); +} + +template +Maybe Session::OptionsObject::SetOption( + Opt* options, + const Local& object, + const Local& name, + std::vector> Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + if (value->IsArray()) { + auto context = env()->context(); + auto values = value.As(); + uint32_t count = values->Length(); + for (uint32_t n = 0; n < count; n++) { + Local item; + if (!values->Get(context, n).ToLocal(&item)) { + return Nothing(); + } + if (crypto::KeyObjectHandle::HasInstance(env(), item)) { + crypto::KeyObjectHandle* handle; + ASSIGN_OR_RETURN_UNWRAP(&handle, item, Nothing()); + (options->*member).push_back(handle->Data()); + } + } + } else if (crypto::KeyObjectHandle::HasInstance(env(), value)) { + crypto::KeyObjectHandle* handle; + ASSIGN_OR_RETURN_UNWRAP(&handle, value, Nothing()); + (options->*member).push_back(handle->Data()); + } else { + UNREACHABLE(); + } + return Just(true); +} + +template +Maybe Session::OptionsObject::SetOption(Opt* options, + const Local& object, + const Local& name, + std::vector Opt::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + if (value->IsArray()) { + auto context = env()->context(); + auto values = value.As(); + uint32_t count = values->Length(); + for (uint32_t n = 0; n < count; n++) { + Local item; + if (!values->Get(context, n).ToLocal(&item)) { + return Nothing(); + } + if (item->IsArrayBufferView()) { + Store store(item.As()); + (options->*member).push_back(std::move(store)); + } + } + } else if (value->IsArrayBufferView()) { + Store store(value.As()); + (options->*member).push_back(std::move(store)); + } + + return Just(true); +} + +void Session::OptionsObject::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + auto env = Environment::GetCurrent(args); + auto& state = BindingState::Get(env); + + static constexpr auto kAlpn = 0; + static constexpr auto kHostname = 1; + static constexpr auto kPreferredAddressStrategy = 2; + static constexpr auto kConnectionIdFactory = 3; + static constexpr auto kQlogEnabled = 4; + static constexpr auto kTlsOptions = 5; + static constexpr auto kApplicationOptions = 6; + static constexpr auto kTransportParams = 7; + static constexpr auto kIpv4PreferredAddress = 8; + static constexpr auto kIpv6PreferredAddress = 9; + + CHECK(args[kAlpn]->IsString()); + CHECK_IMPLIES(!args[kHostname]->IsUndefined(), args[kHostname]->IsString()); + CHECK_IMPLIES(!args[kPreferredAddressStrategy]->IsUndefined(), + args[kPreferredAddressStrategy]->IsInt32()); + CHECK_IMPLIES(!args[kConnectionIdFactory]->IsUndefined(), + args[kConnectionIdFactory]->IsObject()); + CHECK_IMPLIES(!args[kQlogEnabled]->IsUndefined(), + args[kQlogEnabled]->IsBoolean()); + CHECK_IMPLIES(!args[kTlsOptions]->IsUndefined(), + args[kTlsOptions]->IsObject()); + CHECK_IMPLIES(!args[kApplicationOptions]->IsUndefined(), + args[kApplicationOptions]->IsObject()); + CHECK_IMPLIES(!args[kTransportParams]->IsUndefined(), + args[kTransportParams]->IsObject()); + CHECK_IMPLIES( + !args[kIpv4PreferredAddress]->IsUndefined(), + SocketAddressBase::HasInstance(env, args[kIpv4PreferredAddress])); + CHECK_IMPLIES( + !args[kIpv6PreferredAddress]->IsUndefined(), + SocketAddressBase::HasInstance(env, args[kIpv6PreferredAddress])); + + OptionsObject* options = new OptionsObject(env, args.This()); + + Utf8Value alpn(env->isolate(), args[kAlpn]); + options->options_.alpn = std::string(1, alpn.length()) + (*alpn); + + if (!args[kHostname]->IsUndefined()) { + Utf8Value hostname(env->isolate(), args[kHostname]); + options->options_.hostname = *hostname; + } + + if (!args[kPreferredAddressStrategy]->IsUndefined()) { + auto value = args[kPreferredAddressStrategy].As()->Value(); + if (value < 0 || value > static_cast(PreferredAddress::Policy::USE)) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid preferred address policy."); + return; + } + options->options_.preferred_address_strategy = + static_cast(value); + } + + // TODO(@jasnell): Skipping this for now + // Add support for the other strategies once implemented + // if (RandomConnectionIDBase::HasInstance(env, args[5])) { + // RandomConnectionIDBase* cid_strategy; + // ASSIGN_OR_RETURN_UNWRAP(&cid_strategy, args[5]); + // options->options()->cid_strategy = cid_strategy->strategy(); + // options->options()->cid_strategy_strong_ref.reset(cid_strategy); + // } else { + // UNREACHABLE(); + // } + + options->options_.qlog = args[kQlogEnabled]->IsTrue(); + + if (!args[kTlsOptions]->IsUndefined()) { + // TLS Options + Local tls_options = args[kTlsOptions].As(); + + if (UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.reject_unauthorized_string(), + &CryptoContext::Options::reject_unauthorized) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.client_hello_string(), + &CryptoContext::Options::client_hello) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.enable_tls_trace_string(), + &CryptoContext::Options::enable_tls_trace) + .IsNothing()) || + UNLIKELY( + options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.request_peer_certificate_string(), + &CryptoContext::Options::request_peer_certificate) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.ocsp_string(), + &CryptoContext::Options::ocsp) + .IsNothing()) || + UNLIKELY( + options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.verify_hostname_identity_string(), + &CryptoContext::Options::verify_hostname_identity) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.keylog_string(), + &CryptoContext::Options::keylog) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.session_id_string(), + &CryptoContext::Options::session_id_ctx) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.ciphers_string(), + &CryptoContext::Options::ciphers) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.groups_string(), + &CryptoContext::Options::groups) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.keys_string(), + &CryptoContext::Options::keys) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.certs_string(), + &CryptoContext::Options::certs) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.ca_string(), + &CryptoContext::Options::ca) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.crypto_options, + tls_options, + state.crl_string(), + &CryptoContext::Options::crl) + .IsNothing())) { + return; // Failed! + } + } + + if (!args[kApplicationOptions]->IsUndefined()) { + Local app_options = args[kApplicationOptions].As(); + + if (UNLIKELY(options + ->SetOption(&options->options_.application, + app_options, + state.max_header_pairs_string(), + &Application::Options::max_header_pairs) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.application, + app_options, + state.max_header_length_string(), + &Application::Options::max_header_length) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.application, + app_options, + state.max_field_section_size_string(), + &Application::Options::max_field_section_size) + .IsNothing()) || + UNLIKELY( + options + ->SetOption(&options->options_.application, + app_options, + state.qpack_max_table_capacity_string(), + &Application::Options::qpack_max_dtable_capacity) + .IsNothing()) || + UNLIKELY( + options + ->SetOption( + &options->options_.application, + app_options, + state.qpack_encoder_max_dtable_capacity_string(), + &Application::Options::qpack_encoder_max_dtable_capacity) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_.application, + app_options, + state.qpack_blocked_streams_string(), + &Application::Options::qpack_blocked_streams) + .IsNothing())) { + // Intentionally do not return here. + } + } + // TODO(@jasnell): Skipping this for now + // if (Http3OptionsObject::HasInstance(env, args[kApplicationOptions])) { + // Http3OptionsObject* http3Options; + // ASSIGN_OR_RETURN_UNWRAP(&http3Options, args[kApplicationOptions]); + // options->options()->application = http3Options->options(); + // } + + if (!args[kTransportParams]->IsUndefined()) { + // Transport params + Local params = args[kTransportParams].As(); + + if (UNLIKELY(options + ->SetOption( + &options->options_, + params, + state.initial_max_stream_data_bidi_local_string(), + &Session::Options::initial_max_stream_data_bidi_local) + .IsNothing()) || + UNLIKELY(options + ->SetOption( + &options->options_, + params, + state.initial_max_stream_data_bidi_remote_string(), + &Session::Options::initial_max_stream_data_bidi_remote) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.initial_max_stream_data_uni_string(), + &Session::Options::initial_max_stream_data_uni) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.initial_max_data_string(), + &Session::Options::initial_max_data) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.initial_max_streams_bidi_string(), + &Session::Options::initial_max_streams_bidi) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.initial_max_streams_uni_string(), + &Session::Options::initial_max_streams_uni) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.max_idle_timeout_string(), + &Session::Options::max_idle_timeout) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.active_connection_id_limit_string(), + &Session::Options::active_connection_id_limit) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.ack_delay_exponent_string(), + &Session::Options::ack_delay_exponent) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.max_ack_delay_string(), + &Session::Options::max_ack_delay) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.max_datagram_frame_size_string(), + &Session::Options::max_datagram_frame_size) + .IsNothing()) || + UNLIKELY(options + ->SetOption(&options->options_, + params, + state.disable_active_migration_string(), + &Session::Options::disable_active_migration) + .IsNothing())) { + return; + } + } + + if (!args[kIpv4PreferredAddress]->IsUndefined()) { + SocketAddressBase* preferred_addr; + ASSIGN_OR_RETURN_UNWRAP(&preferred_addr, args[kIpv4PreferredAddress]); + CHECK_EQ(preferred_addr->address()->family(), AF_INET); + options->options_.preferred_address_ipv4 = Just(*preferred_addr->address()); + } + + if (!args[kIpv6PreferredAddress]->IsUndefined()) { + SocketAddressBase* preferred_addr; + ASSIGN_OR_RETURN_UNWRAP(&preferred_addr, args[kIpv6PreferredAddress]); + CHECK_EQ(preferred_addr->address()->family(), AF_INET6); + options->options_.preferred_address_ipv6 = Just(*preferred_addr->address()); + } +} + +void Session::OptionsObject::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); +} + +// ====================================================================================== +// Default Application + +namespace { +inline void Consume(ngtcp2_vec** pvec, size_t* pcnt, size_t len) { + ngtcp2_vec* v = *pvec; + size_t cnt = *pcnt; + + for (; cnt > 0; --cnt, ++v) { + if (v->len > len) { + v->len -= len; + v->base += len; + break; + } + len -= v->len; + } + + *pvec = v; + *pcnt = cnt; +} + +inline int IsEmpty(const ngtcp2_vec* vec, size_t cnt) { + size_t i; + for (i = 0; i < cnt && vec[i].len == 0; ++i) { + } + return i == cnt; +} + +template +size_t get_length(const T* vec, size_t count) { + CHECK_NOT_NULL(vec); + size_t len = 0; + for (size_t n = 0; n < count; n++) len += vec[n].len; + return len; +} +} // namespace + +class DefaultApplication final : public Session::Application { + public: + DefaultApplication(Session* session, + const Session::Config& config, + const Application::Options& options); + + QUIC_NO_COPY_OR_MOVE(DefaultApplication) + + bool ReceiveStreamData(Stream* stream, + ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) override; + + int GetStreamData(StreamData* stream_data) override; + + void ResumeStream(stream_id id) override; + bool ShouldSetFin(const StreamData& stream_data) override; + bool StreamCommit(StreamData* stream_data, size_t datalen) override; + + SET_SELF_SIZE(DefaultApplication) + SET_MEMORY_INFO_NAME(DefaultApplication) + SET_NO_MEMORY_INFO() + + private: + void ScheduleStream(stream_id id); + void UnscheduleStream(stream_id id); + + Stream::Queue stream_queue_; +}; + +DefaultApplication::DefaultApplication(Session* session, + const Session::Config& config, + const Application::Options& options) + : Session::Application(session, options) {} + +void DefaultApplication::ScheduleStream(stream_id id) { + auto stream = session().FindStream(id); + if (LIKELY(stream && !stream->is_destroyed())) { + DEBUG_ARGS(&session(), "Scheduling stream %" PRIi64, id); + stream->Schedule(&stream_queue_); + } +} + +void DefaultApplication::UnscheduleStream(stream_id id) { + auto stream = session().FindStream(id); + if (LIKELY(stream)) { + DEBUG_ARGS(&session(), "Unscheduling stream %" PRIi64, id); + stream->Unschedule(); + } +} + +void DefaultApplication::ResumeStream(stream_id id) { + ScheduleStream(id); +} + +bool DefaultApplication::ReceiveStreamData(Stream* stream, + ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + // One potential DOS attack vector is to send a bunch of empty stream frames + // to commit resources. Check that here. Essentially, we only want to create a + // new stream if the datalen is greater than 0, otherwise, we ignore the + // packet. ngtcp2 should be handling this for us, but we handle it just to be + // safe. We also want to make sure that the stream hasn't been destroyed. + if (LIKELY(datalen > 0 && !stream->is_destroyed())) { + DEBUG_ARGS(&session(), "Receive stream %" PRIi64 " data", stream->id()); + stream->ReceiveData(flags, data, datalen, offset); + } + return true; +} + +int DefaultApplication::GetStreamData(StreamData* stream_data) { + if (stream_queue_.IsEmpty()) return 0; + + Stream* stream = stream_queue_.PopFront(); + CHECK_NOT_NULL(stream); + stream_data->stream.reset(stream); + stream_data->id = stream->id(); + auto next = + [&](int status, const ngtcp2_vec* data, size_t count, bob::Done done) { + switch (status) { + case bob::Status::STATUS_BLOCK: + // Fall through + case bob::Status::STATUS_WAIT: + return; + case bob::Status::STATUS_EOS: + case bob::Status::STATUS_END: + stream_data->fin = 1; + } + + stream_data->count = count; + + if (count > 0) { + stream->Schedule(&stream_queue_); + stream_data->remaining = get_length(data, count); + } else { + stream_data->remaining = 0; + } + + // Not calling done here because we defer committing + // the data until after we're sure it's written. + }; + + if (LIKELY(!stream->is_eos())) { + int ret = stream->Pull(std::move(next), + bob::Options::OPTIONS_SYNC, + stream_data->data, + arraysize(stream_data->data), + kMaxVectorCount); + switch (ret) { + case bob::Status::STATUS_EOS: + case bob::Status::STATUS_END: + stream_data->fin = 1; + break; + } + } else { + stream_data->fin = 1; + } + + return 0; +} + +bool DefaultApplication::StreamCommit(StreamData* stream_data, size_t datalen) { + CHECK(stream_data->stream); + stream_data->remaining -= datalen; + Consume(&stream_data->buf, &stream_data->count, datalen); + stream_data->stream->Commit(datalen); + return true; +} + +bool DefaultApplication::ShouldSetFin(const StreamData& stream_data) { + if (!stream_data.stream || !IsEmpty(stream_data.buf, stream_data.count)) + return false; + return true; +} + +// ====================================================================================== + +std::unique_ptr Session::SelectApplication( + const Config& config, const Options& options) { + if (options.alpn == NGHTTP3_ALPN_H3) + return std::make_unique(this, options.application); + + // In the future, we may end up supporting additional QUIC protocols. As they + // are added, extend the cases here to create and return them. + + return std::make_unique( + this, config, options.application); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/session.h b/src/quic/session.h new file mode 100644 index 00000000000000..39f0ddaee78ce8 --- /dev/null +++ b/src/quic/session.h @@ -0,0 +1,1080 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crypto.h" +#include "defs.h" +#include "quic.h" + +namespace node { +namespace quic { + +using QuicConnectionPointer = DeleteFnPtr; +class Http3Application; + +#define V(_, name, __) uint64_t name; +struct SessionStats final { + SESSION_STATS(V) +}; +#undef V + +struct SessionStatsTraits final { + using Stats = SessionStats; + using Base = StatsBase; + + template + static void ToString(const Stats& stats, Fn&& add_field) { +#define V(_, id, desc) add_field(desc, stats.id); + SESSION_STATS(V) +#undef V + } +}; + +using SessionStatsBase = StatsBase; + +class LogStream; +class DefaultApplication; + +// ============================================================================= +// PreferredAddress is a helper class used only when a client Session receives +// an advertised preferred address from a server. The helper provides +// information about the servers advertised preferred address. Call Use() to let +// ngtcp2 know which preferred address to use (if any). +class PreferredAddress final { + public: + enum class Policy { IGNORE_PREFERED, USE }; + + struct AddressInfo final { + int family; + uint16_t port; + std::string address; + }; + + inline PreferredAddress(Environment* env, + ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr) + : env_(env), dest_(dest), paddr_(paddr) {} + + QUIC_NO_COPY_OR_MOVE(PreferredAddress) + + // When a preferred address is advertised by a server, the advertisement also + // includes a new CID and (optionally) a stateless reset token. If the + // preferred address is selected, then the client Session will make use of + // these new values. Access to the cid and reset token are provided via the + // PreferredAddress class only as a convenience. + inline CID cid() const { return CID(paddr_->cid); } + + // The stateless reset token associated with the preferred address CID + inline const uint8_t* stateless_reset_token() const { + return paddr_->stateless_reset_token; + } + + // A preferred address advertisement may include both an IPv4 and IPv6 + // address. Only one of which will be used. + + v8::Maybe ipv4() const; + + v8::Maybe ipv6() const; + + // Instructs the Session to use the advertised preferred address matching the + // given family. If the advertisement does not include a matching address, the + // preferred address is ignored. If the given address cannot be successfully + // resolved using uv_getaddrinfo it is ignored. + bool Use(const AddressInfo& address) const; + + void CopyToTransportParams(ngtcp2_transport_params* params, + const sockaddr* addr); + + private: + bool Resolve(const AddressInfo& address, uv_getaddrinfo_t* req) const; + + Environment* env_; + mutable ngtcp2_path* dest_; + const ngtcp2_preferred_addr* paddr_; +}; + +struct Path final : public ngtcp2_path { + Path(const SocketAddress& local, const SocketAddress& remote); +}; + +struct PathStorage final : public ngtcp2_path_storage { + inline PathStorage() { ngtcp2_path_storage_zero(this); } +}; + +// A Session represents one half of a persistent connection between two QUIC +// peers. Every Session is established first by performing a TLS handshake in +// which the client sends an initial packet to the server containing a TLS +// client hello. Once the TLS handshake has been completed, the Session can be +// used to open one or more Streams for the actual data flow back and forth. +// +// While client and server Sessions are created in slightly different ways, +// their lifecycles are generally identical: +// +// A Session is either acting as a Client or as a Server. +// +// Client Sessions are always created using Endpoint::Connect() +// +// Server Sessions are always created by an Endpoint receiving a valid initial +// request received from a remote client. +// +// As soon as Sessions of either type are created, they will immediately start +// working through the TLS handshake to establish the crypographic keys used to +// secure the communication. Once those keys are established, the Session can be +// used to open Streams. Based on how the Session is configured, any number of +// Streams can exist concurrently on a single Session. +class Session : public SessionStatsBase, public AsyncWrap { + public: + // An Application encapsulates the ALPN-identified application specific + // semantics associated with the Session. + class Application : public MemoryRetainer { + public: + // A base class for configuring the Application. Specific Application + // subclasses may extend this with additional configuration properties. + struct Options { + // The maximum number of header pairs permitted for a Stream. + uint64_t max_header_pairs = DEFAULT_MAX_HEADER_LIST_PAIRS; + + // The maximum total number of header bytes (including header + // name and value) permitted for a Stream. + uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; + + // HTTP/3 specific options. We keep these here instead of in the + // HTTP3Application to keep things easy. + + uint64_t max_field_section_size; + size_t qpack_max_dtable_capacity; + size_t qpack_encoder_max_dtable_capacity; + size_t qpack_blocked_streams; + }; + + Application(Session* session, const Options& options) + : session_(session), options_(options) {} + QUIC_NO_COPY_OR_MOVE(Application) + + // The Session will call Start as soon as the TLS secrets have been + // negotiated. + virtual bool Start(); + + struct ReceiveStreamDataFlags final { + // Identifies the final chunk of data that the peer will send for the + // stream. + bool fin = false; + // Indicates that this chunk of data was received in a 0RTT packet before + // the TLS handshake completed, suggesting that is is not as secure and + // could be replayed by an attacker. + bool early = false; + }; + + // Session will forward all received stream data immediately on to the + // Application. The only additional processing the Session does is to + // automatically adjust the session-level flow control window. It is up to + // the Application to do the same for the Stream-level flow control. + virtual bool ReceiveStreamData(Stream* stream, + ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) = 0; + + // Session will forward all data acknowledgements for a stream to the + // Application. + virtual void AcknowledgeStreamData(Stream* stream, + uint64_t offset, + size_t datalen); + + // Called to determine if a Header can be added to this application. + // Applications that do not support headers will always return false. + virtual bool CanAddHeader(size_t current_count, + size_t current_headers_length, + size_t this_header_length); + + // Called to mark the identified stream as being blocked. Not all + // Application types will support blocked streams, and those that do will do + // so differently. The default implementation here is to simply acknowledge + // the notification. + virtual bool BlockStream(stream_id id); + + // Called when the Session determines that the maximum number of + // remotely-initiated unidirectional streams has been extended. Not all + // Application types will require this notification so the default is to do + // nothing. + virtual void ExtendMaxStreams(EndpointLabel label, + Direction direction, + uint64_t max_streams) {} + + // Called when the Session determines that the flow control window for the + // given stream has been expanded. Not all Application types will require + // this notification so the default is to do nothing. + virtual void ExtendMaxStreamData(Stream* stream, uint64_t max_data) {} + + // Called when the session determines that there is outbound data available + // to send for the given stream. + virtual void ResumeStream(stream_id id) {} + + // Different Applications may wish to set some application data in the + // session ticket (e.g. http/3 would set server settings in the application + // data). By default, there's nothing to set. + virtual void SetSessionTicketAppData(const SessionTicketAppData& app_data) { + DEBUG(session_, "Set session ticket app data does nothing"); + } + + // Different Applications may set some application data in the session + // ticket (e.g. http/3 would set server settings in the application data). + // By default, there's nothing to get. + virtual SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, SessionTicketAppData::Flag flag); + + // Notifies the Application that the identified stream has been closed. + virtual void StreamClose(Stream* stream, v8::Maybe error); + + // Notifies the Application that the identified stream has been reset. + virtual void StreamReset(Stream* stream, + uint64_t final_size, + QuicError error); + + // Notifies the Application that the identified stream should stop sending. + virtual void StreamStopSending(Stream* stream, QuicError error); + + enum class HeadersKind { + INFO, + INITIAL, + TRAILING, + }; + + enum class HeadersFlags { + NONE, + TERMINAL, + }; + + // Submits an outbound block of headers for the given stream. Not all + // Application types will support headers, in which case this function + // should return false. + virtual bool SendHeaders(stream_id id, + HeadersKind kind, + const v8::Local& headers, + HeadersFlags flags = HeadersFlags::NONE) { + return false; + } + + // Signals to the Application that it should serialize and transmit any + // pending session and stream packets it has accumulated. + void SendPendingData(); + + enum class StreamPriority { + DEFAULT = NGHTTP3_DEFAULT_URGENCY, + LOW = NGHTTP3_URGENCY_LOW, + HIGH = NGHTTP3_URGENCY_HIGH, + }; + + enum class StreamPriorityFlags { + NONE, + NON_INCREMENTAL, + }; + + // Set the priority level of the stream if supported by the application. Not + // all applications support priorities, in which case this function is a + // non-op. + virtual void SetStreamPriority( + Stream* stream, + StreamPriority priority = StreamPriority::DEFAULT, + StreamPriorityFlags flags = StreamPriorityFlags::NONE) { + // By default do nothing. + } + + virtual StreamPriority GetStreamPriority(Stream* stream) { + return StreamPriority::DEFAULT; + } + + inline Environment* env() const { return session_->env(); } + inline Session& session() { return *session_; } + inline const Options& options() const { return options_; } + + protected: + BaseObjectPtr CreateStreamDataPacket(); + + struct StreamData final { + size_t count = 0; + size_t remaining = 0; + stream_id id = -1; + int fin = 0; + ngtcp2_vec data[kMaxVectorCount]{}; + ngtcp2_vec* buf = data; + BaseObjectPtr stream; + + std::string ToString() const; + }; + + virtual int GetStreamData(StreamData* data) = 0; + virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; + virtual bool ShouldSetFin(const StreamData& data) = 0; + + ssize_t WriteVStream(PathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data); + + private: + Session* session_; + Options options_; + bool started_ = false; + }; + + // A utility that wraps the configuration settings for the Session and the + // underlying ngtcp2_conn. This struct is created when a new Client or Server + // session is created. + struct Config final : public ngtcp2_settings { + ngtcp2_crypto_side side; + + // The QUIC protocol version requested for the Session. + quic_version version = NGTCP2_PROTO_VER_MAX; + + CIDFactory& cid_factory; + + SocketAddress local_addr; + SocketAddress remote_addr; + + // The initial destination CID. For server sessions, the dcid value is + // provided to us by the remote peer in their initial packet, and may be + // updated as we go using NEW_CONNECTION_ID frames. For client sessions, the + // dcid value is initially selected at random and will be replaced one or + // more times when either a RETRY or Initial packet is received from the + // server. + CID dcid; + + // The locally selected source CID. This value is always generated locally. + CID scid; + + // The ocid configuration field is only used by client sessions to identify + // the original DCID used. + v8::Maybe ocid = v8::Nothing(); + + v8::Maybe retry_scid = v8::Nothing(); + + // For client sessions, the SessionTicket stores the crypto session and + // stored remote transport parameters that are used to support 0RTT session + // resumption. This field is unused by server sessions. + BaseObjectPtr session_ticket; + + Config(const Endpoint& endpoint, + // For server session, The dcid here is the identifier the remote + // peer sent us to identify itself. It is the CID we will use as the + // outbound dcid for all packets. For client sessions, the dcid here + // is initially a randomly generated value that MUST be between 8 and + // 20 bytes in length. + const CID& dcid, + const SocketAddress& local_address, + const SocketAddress& remote_address, + quic_version version, + ngtcp2_crypto_side side); + + Config(const Endpoint& endpoint, + quic_version version, + ngtcp2_crypto_side side); + + void EnableQLog(const CID& ocid = CID()); + }; + + // The Options struct contains all of the usercode specified options for the + // session. Most of the options correlate to the transport parameters that are + // communicated to the remote peer once the session is created. + struct Options final : public MemoryRetainer { + // The protocol identifier to be used by this Session. + std::string alpn = NGHTTP3_ALPN_H3; + + // The SNI hostname to be used. This is used only by client Sessions to + // identify the SNI host in the TLS client hello message. + std::string hostname = ""; + + PreferredAddress::Policy preferred_address_strategy = + PreferredAddress::Policy::USE; + + bool qlog = false; + + // Set only on server Sessions, the preferred address communicates the IP + // address and port that the server would prefer the client to use when + // communicating with it. See the QUIC specification for more detail on how + // the preferred address mechanism works. + v8::Maybe preferred_address_ipv4 = + v8::Nothing(); + v8::Maybe preferred_address_ipv6 = + v8::Nothing(); + + // The initial size of the flow control window of locally initiated streams. + // This is the maximum number of bytes that the *remote* endpoint can send + // when the connection is started. + uint64_t initial_max_stream_data_bidi_local = + DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL; + + // The initial size of the flow control window of remotely initiated + // streams. This is the maximum number of bytes that the remote endpoint can + // send when the connection is started. + uint64_t initial_max_stream_data_bidi_remote = + DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE; + + // The initial size of the flow control window of remotely initiated + // unidirectional streams. This is the maximum number of bytes that the + // remote endpoint can send when the connection is started. + uint64_t initial_max_stream_data_uni = DEFAULT_MAX_STREAM_DATA_UNI; + + // The initial size of the session-level flow control window. + uint64_t initial_max_data = DEFAULT_MAX_DATA; + + // The initial maximum number of concurrent bidirectional streams the remote + // endpoint is permitted to open. + uint64_t initial_max_streams_bidi = DEFAULT_MAX_STREAMS_BIDI; + + // The initial maximum number of concurrent unidirectional streams the + // remote endpoint is permitted to open. + uint64_t initial_max_streams_uni = DEFAULT_MAX_STREAMS_UNI; + + // The maximum amount of time that a Session is permitted to remain idle + // before it is silently closed and state is discarded. + uint64_t max_idle_timeout = DEFAULT_MAX_IDLE_TIMEOUT; + + // The maximum number of Connection IDs that the peer can store. A single + // Session may have several connection IDs over it's lifetime. + uint64_t active_connection_id_limit = DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; + + // Establishes the exponent used in ACK Delay field in the ACK frame. See + // the QUIC specification for details. This is an advanced option that + // should rarely be modified and only if there is really good reason. + uint64_t ack_delay_exponent = NGTCP2_DEFAULT_ACK_DELAY_EXPONENT; + + // The maximum amount of time by which the endpoint will delay sending + // acknowledgements. This is an advanced option that should rarely be + // modified and only if there is a really good reason. It is used to + // determine how long a Session will wait to determine that a packet has + // been lost. + uint64_t max_ack_delay = NGTCP2_DEFAULT_MAX_ACK_DELAY; + + // The maximum size of DATAGRAM frames that the endpoint will accept. + // Setting the value to 0 will disable DATAGRAM support. + uint64_t max_datagram_frame_size = kDefaultMaxPacketLength; + + // When true, communicates that the Session does not support active + // connection migration. See the QUIC specification for more details on + // connection migration. + bool disable_active_migration = false; + + // ================================================================================== + // TLS Options + + CryptoContext::Options crypto_options; + + // ================================================================================== + // Application Options + + Application::Options application; + + Options() = default; + // QUIC_COPY_NO_MOVE(Options) + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Session::Options) + SET_SELF_SIZE(Options) + }; + + class OptionsObject : public BaseObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static void New(const v8::FunctionCallbackInfo& args); + + OptionsObject(Environment* env, v8::Local object); + QUIC_NO_COPY_OR_MOVE(OptionsObject) + + inline const Options& options() const { return options_; } + inline operator const Options&() const { return options_; } + + void MemoryInfo(MemoryTracker*) const override; + SET_MEMORY_INFO_NAME(Session::OptionsObject) + SET_SELF_SIZE(OptionsObject); + + private: + template + v8::Maybe SetOption(Opt* options, + const v8::Local& object, + const v8::Local& name, + bool Opt::*member); + + template + v8::Maybe SetOption(Opt* options, + const v8::Local& object, + const v8::Local& name, + uint64_t Opt::*member); + + template + v8::Maybe SetOption(Opt* options, + const v8::Local& object, + const v8::Local& name, + uint32_t Opt::*member); + + template + v8::Maybe SetOption(Opt* options, + const v8::Local& object, + const v8::Local& name, + std::string Opt::*member); + + template + v8::Maybe SetOption( + Opt* options, + const v8::Local& object, + const v8::Local& name, + std::vector> Opt::*member); + + template + v8::Maybe SetOption(Opt* options, + const v8::Local& object, + const v8::Local& name, + std::vector Opt::*member); + + Options options_; + }; + + // The Transport Params are the set of configuration options that are sent to + // the remote peer. They communicate the protocol options the other peer + // should use when communicating with this session. + class TransportParams final { + public: + enum class Type { + CLIENT_HELLO = NGTCP2_TRANSPORT_PARAMS_TYPE_CLIENT_HELLO, + ENCRYPTED_EXTENSIONS = NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, + }; + + Type type() const { return type_; } + + inline const ngtcp2_transport_params& operator*() const { + CHECK_NOT_NULL(ptr_); + return *ptr_; + } + + inline const ngtcp2_transport_params* operator->() const { + CHECK_NOT_NULL(ptr_); + return ptr_; + } + + inline operator bool() const { return ptr_ != nullptr; } + + inline operator const ngtcp2_transport_params*() const { + CHECK_NOT_NULL(ptr_); + return ptr_; + } + + QuicError error() const { return error_; } + + // Returns an ArrayBuffer containing the encoded transport parameters. + // If an error occurs during encoding, an empty shared_ptr will be returned + // and the error() property will be set to an appropriate QuicError. + Store Encode(Environment* env); + + private: + // Creates an instance of TransportParams from an existing const + // ngtcp2_transport_params pointer. + inline TransportParams(Type type, const ngtcp2_transport_params* ptr) + : type_(type), ptr_(ptr) {} + + // Creates an instance of TransportParams by decoding the given backing + // store. If the parameters cannot be successfully decoded, the error() + // property will be set with an appropriate QuicError and the bool() + // operator will return false. + TransportParams(Type type, const ngtcp2_vec& buf); + + TransportParams(const Config& config, const Options& options); + + TransportParams(Type type) : type_(type), ptr_(¶ms_) {} + + void GenerateStatelessResetToken(const Endpoint& endpoint, const CID& cid); + void GeneratePreferredAddressToken(Session*, CID* pscid); + void SetPreferredAddress(const SocketAddress& address); + + Type type_; + ngtcp2_transport_params params_{}; + const ngtcp2_transport_params* ptr_; + QuicError error_; + + friend class Session; + friend class CryptoContext; + }; + + // SendPendingDataScope triggers SendPendingData() on scope exit when not + // executing within the context of an ngtcp2 callback. When within an ngtcp2 + // callback, SendPendingData will always be called when the callbacks + // complete. + struct SendPendingDataScope final { + Session* session; + explicit SendPendingDataScope(Session* session_); + explicit SendPendingDataScope(const BaseObjectPtr& session_) + : SendPendingDataScope(session_.get()) {} + QUIC_NO_COPY_OR_MOVE(SendPendingDataScope) + ~SendPendingDataScope(); + }; + + // JavaScript API + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + // Internal API + static BaseObjectPtr Create(BaseObjectPtr endpoint, + const Config& config, + const Options& options); + + ~Session() override; + + quic_version version() const; + const Endpoint& endpoint() const; + inline CryptoContext& crypto_context() { return crypto_context_; } + inline Application& application() { return *application_; } + inline const SocketAddress& remote_address() const { return remote_address_; } + inline const SocketAddress& local_address() const { return local_address_; } + inline bool is_destroyed() const { return state_->destroyed; } + inline bool is_server() const { + return crypto_context_.side() == NGTCP2_CRYPTO_SIDE_SERVER; + } + + inline BaseObjectPtr& qlogstream() { return qlogstream_; } + inline BaseObjectPtr& keylogstream() { return keylogstream_; } + + inline void set_wrapped() { state_->wrapped = true; } + + inline std::string alpn() const { return options_.alpn; } + inline std::string hostname() const { return options_.hostname; } + // TODO(@jasnell): For now, we always allow early data. We'll make this + // configurable. + bool allow_early_data() const { return true; } + + inline ngtcp2_conn* connection() const { + CHECK(!is_destroyed()); + return connection_.get(); + } + + // Returns the Stream associated + BaseObjectPtr FindStream(stream_id) const; + + inline void SetLastError(QuicError error) { last_error_ = error; } + + // Initiate closing of the Session. This will round trip through JavaScript, + // causing all currently opened streams to be closed. An attempt will be made + // to send a CONNECTION_CLOSE frame to the peer. If Close is called while + // within the ngtcp2 callback scope, sending the CONNECTION_CLOSE will be + // deferred until the ngtcp2 callback scope exits. + void Close(); + + // Like Close(), except that the connected peer will not be notified. + void CloseSilently(); + + // Closing gracefully disables the ability to open or accept new streams for + // this Session. Existing streams are allowed to close naturally on their own. + // Once called, the Session will be immediately closed once there are no + // remaining streams. No notification is given to the connected peer that we + // are in a graceful closing state. A CONNECTION_CLOSE will be sent only once + // Close() is called. + void CloseGracefully(); + + // Immediately destroy the Session, after which it cannot be used. The only + // remaining state will be the collected statistics. There must not be any + // Streams remaining on the Session. + void Destroy(); + + // Get the local transport parameters established for this Session. + TransportParams GetLocalTransportParams() const; + + // Get the remote transport parameters established for this Session. If the + // remote transport parameters are not yet known, the returned TransportParams + // bool() operator will return false. + TransportParams GetRemoteTransportParams() const; + + inline bool is_graceful_closing() const { return state_->graceful_closing; } + + BaseObjectPtr CreateStream(stream_id id); + BaseObjectPtr OpenStream(Direction direction); + void AddStream(const BaseObjectPtr& stream); + void RemoveStream(stream_id id); + void ResumeStream(stream_id id); + void ShutdownStream(stream_id id, QuicError error); + void StreamDataBlocked(stream_id id); + void ShutdownStreamWrite(stream_id id, QuicError code); + void UpdatePath(const PathStorage& path); + void Send(BaseObjectPtr packet); + void Send(BaseObjectPtr packet, const PathStorage& path); + datagram_id SendDatagram(Store&& data); + + void SetSessionTicketAppData(const SessionTicketAppData& app_data); + + SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, SessionTicketAppData::Flag flag); + + bool Receive(Store&& store, + const SocketAddress& local_address, + const SocketAddress& remote_address); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Session) + SET_SELF_SIZE(Session) + + // Really should be private, but can't be because MakeDetachedBaseObject needs + // visibility. + Session(v8::Local object, + BaseObjectPtr endpoint, + const Config& config, + const Options& options); + + std::string diagnostic_name() const override; + + const Options& options() const { return options_; } + + private: + // JavaScript API + + static void DoDestroy(const v8::FunctionCallbackInfo& args); + static void GetRemoteAddress(const v8::FunctionCallbackInfo& args); + static void GetCertificate(const v8::FunctionCallbackInfo& args); + static void GetEphemeralKeyInfo( + const v8::FunctionCallbackInfo& args); + static void GetPeerCertificate( + const v8::FunctionCallbackInfo& args); + static void GracefulClose(const v8::FunctionCallbackInfo& args); + static void SilentClose(const v8::FunctionCallbackInfo& args); + static void UpdateKey(const v8::FunctionCallbackInfo& args); + static void OnClientHelloDone( + const v8::FunctionCallbackInfo& args); + static void OnOCSPDone(const v8::FunctionCallbackInfo& args); + static void DoOpenStream(const v8::FunctionCallbackInfo& args); + static void DoSendDatagram(const v8::FunctionCallbackInfo& args); + + // Internal API + + enum class PathValidationResult : uint8_t { + SUCCESS = NGTCP2_PATH_VALIDATION_RESULT_SUCCESS, + FAILURE = NGTCP2_PATH_VALIDATION_RESULT_FAILURE, + ABORTED = NGTCP2_PATH_VALIDATION_RESULT_ABORTED, + }; + struct PathValidationFlags { + bool preferredAddress = false; + }; + + struct DatagramReceivedFlag { + bool early = false; + }; + + std::unique_ptr SelectApplication(const Config& config, + const Options& options); + + // Returns true if the Session has entered the closing period after sending a + // CONNECTION_CLOSE. While true, the Session is only permitted to transmit + // CONNECTION_CLOSE frames until either the idle timeout period elapses or + // until the Session is explicitly destroyed. + bool is_in_closing_period() const; + + // Returns true if the Session has received a CONNECTION_CLOSE frame from the + // peer. Once in the draining period, the Session is not permitted to send any + // frames to the peer. The Session will be silently closed after either the + // idle timeout period elapses or until the Session is explicitly destroyed. + bool is_in_draining_period() const; + + // Returns true if the Session is currently in a state where it is unable to + // transmit any packets. + bool is_unable_to_send_packets() const; + + bool can_create_streams() const; + + uint64_t max_data_left() const; + uint64_t max_local_streams_uni() const; + uint64_t max_local_streams_bidi() const; + void DetachFromEndpoint(); + void DoClose(bool silent = false); + void ExtendStreamOffset(stream_id id, size_t amount); + void ExtendOffset(size_t amount); + void UpdateDataStats(); + void SendConnectionClose(); + void OnTimeout(); + void UpdateTimer(); + void SendPendingData(); + bool StartClosingPeriod(); + + void EmitClose(); + void EmitError(QuicError error); + void EmitSessionTicket(Store&& ticket); + void EmitPathValidation(PathValidationResult result, + PathValidationFlags flags, + const SocketAddress& local_address, + const SocketAddress& remote_address); + bool EmitClientHello(); + bool EmitOCSP(); + void EmitOCSPResponse(); + void EmitDatagram(Store&& datagram, DatagramReceivedFlag flag); + void EmitKeylog(const char* line); + void EmitNewStream(const BaseObjectPtr& stream); + void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, + const quic_version* sv, + size_t nsv); + void EmitHandshakeComplete(); + void EmitDatagramAcknowledged(datagram_id id); + void EmitDatagramLost(datagram_id id); + + void AcknowledgeStreamDataOffset(Stream* stream, + uint64_t offset, + uint64_t datalen); + void ActivateConnectionId(uint64_t seq, + const CID& cid, + v8::Maybe maybe_reset_token); + void DatagramAcknowledged(datagram_id datagramId); + void DatagramLost(datagram_id id); + void DatagramReceived(const uint8_t* data, + size_t datalen, + DatagramReceivedFlag flag); + void DeactivateConnectionId(uint64_t seq, + const CID& cid, + v8::Maybe maybe_reset_token); + void ExtendMaxStreamData(Stream* stream, uint64_t max); + void ExtendMaxStreams(EndpointLabel label, Direction direction, uint64_t max); + bool GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token); + bool HandshakeCompleted(); + void HandshakeConfirmed(); + int ReceiveCryptoData(ngtcp2_crypto_level level, + uint64_t offset, + const uint8_t* data, + size_t datalen); + bool ReceiveRxKey(ngtcp2_crypto_level level); + bool ReceiveTxKey(ngtcp2_crypto_level level); + void ReceiveNewToken(const ngtcp2_vec* token); + void ReceiveStatelessReset(const ngtcp2_pkt_stateless_reset* sr); + void ReceiveStreamData(Stream* stream, + Application::ReceiveStreamDataFlags flags, + uint64_t offset, + const uint8_t* data, + size_t datalen); + void RemoveConnectionId(const CID& cid); + void SelectPreferredAddress(const PreferredAddress& preferredAddress); + void StreamClose(Stream* stream, v8::Maybe app_error_code); + void StreamOpen(stream_id id); + void StreamReset(Stream* stream, + uint64_t final_size, + QuicError app_error_code); + void StreamStopSending(Stream* stream, QuicError app_error_code); + bool InitApplication(); + + void ReportPathValidationStatus(PathValidationResult result, + PathValidationFlags flags, + const SocketAddress& local_address, + const SocketAddress& remote_address); + + void SessionTicketCallback(Store&& ticket, Store&& transport_params); + + using StreamsMap = std::unordered_map>; + + // Internal Fields + +#define V(_, name, type) type name; + struct State final { + SESSION_STATE(V) + }; +#undef V + + ngtcp2_mem allocator_; + Options options_; + QuicConnectionPointer connection_; + BaseObjectPtr endpoint_; + AliasedStruct state_; + StreamsMap streams_; + + datagram_id last_datagram_id_ = 0; + + CIDFactory& cid_factory_; + BaseObjectPtr maybe_cid_factory_ref_; + + SocketAddress local_address_; + SocketAddress remote_address_; + + std::unique_ptr application_; + CryptoContext crypto_context_; + + TimerWrapHandle timer_; + + // The CID of the remote peer. This value might change throughout the lifetime + // of the session. + CID dcid_; + + // The CID of *this* session. This value should not change, but other CIDs + // might be generated to also identify this session. + CID scid_; + + v8::Maybe ocid_ = v8::Nothing(); + + // If this is a server session, and a preferred address is advertised, this is + // the CID associated with the preferred address advertisement. + CID preferred_address_cid_; + + ngtcp2_transport_params transport_params_; + bool in_ng_callback_ = false; + size_t send_scope_depth_ = 0; + size_t connection_close_depth_ = 0; + + QuicError last_error_; + BaseObjectPtr conn_closebuf_; + + BaseObjectPtr qlogstream_; + BaseObjectPtr keylogstream_; + + friend class CryptoContext; + friend class Stream; + friend class Http3Application; + + // ====================================================================================== + // MaybeCloseConnectionScope triggers sending a CONNECTION_CLOSE when not + // executing within the context of an ngtcp2 callback and the session is in + // the correct state. + struct MaybeCloseConnectionScope; + + using CallbackScope = CallbackScopeBase; + + // The ngtcp2 callbacks are not re-entrant. The NgCallbackScope is used as a + // guard in the static callback functions to prevent re-entry. + struct NgCallbackScope; + + // ====================================================================================== + // ngtcp2 static callback functions + static const ngtcp2_callbacks callbacks[2]; + + static Session* From(ngtcp2_conn*, void* user_data); + static int OnAcknowledgeStreamDataOffset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t offset, + uint64_t datalen, + void* user_data, + void* stream_user_data); + static int OnAcknowledgeDatagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data); + static int OnConnectionIdStatus(ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data); + static int OnExtendMaxRemoteStreamsBidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + static int OnExtendMaxRemoteStreamsUni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + static int OnExtendMaxStreamsBidi(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + static int OnExtendMaxStreamData(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data); + static int OnExtendMaxStreamsUni(ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + static int OnGetNewConnectionId(ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data); + static int OnGetPathChallengeData(ngtcp2_conn* conn, + uint8_t* data, + void* user_data); + static int OnHandshakeCompleted(ngtcp2_conn* conn, void* user_data); + static int OnHandshakeConfirmed(ngtcp2_conn* conn, void* user_data); + static int OnLostDatagram(ngtcp2_conn* conn, + uint64_t dgram_id, + void* user_data); + static int OnPathValidation(ngtcp2_conn* conn, + uint32_t flags, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data); + static int OnReceiveCryptoData(ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data); + static int OnReceiveDatagram(ngtcp2_conn* conn, + uint32_t flags, + const uint8_t* data, + size_t datalen, + void* user_data); + static int OnReceiveNewToken(ngtcp2_conn* conn, + const ngtcp2_vec* token, + void* user_data); + static int OnReceiveRxKey(ngtcp2_conn* conn, + ngtcp2_crypto_level level, + void* user_data); + static int OnReceiveStatelessReset(ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data); + static int OnReceiveStreamData(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data); + static int OnReceiveTxKey(ngtcp2_conn* conn, + ngtcp2_crypto_level level, + void* user_data); + static int OnReceiveVersionNegotiation(ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data); + static int OnRemoveConnectionId(ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data); + static int OnSelectPreferredAddress(ngtcp2_conn* conn, + ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data); + static int OnStreamClose(ngtcp2_conn* conn, + uint32_t flags, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + static int OnStreamOpen(ngtcp2_conn* conn, + int64_t stream_id, + void* user_data); + static int OnStreamReset(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + static int OnStreamStopSending(ngtcp2_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + + static void OnRand(uint8_t* dest, + size_t destlen, + const ngtcp2_rand_ctx* rand_ctx); +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/stream.cc b/src/quic/stream.cc new file mode 100644 index 00000000000000..17b078193b905d --- /dev/null +++ b/src/quic/stream.cc @@ -0,0 +1,1403 @@ +#include "stream.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "endpoint.h" +#include "quic.h" +#include "session.h" + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BigInt; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Integer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::PropertyAttribute; +using v8::Uint32; +using v8::Uint8Array; +using v8::Undefined; +using v8::Value; + +namespace quic { + +// ====================================================================================== +// The NullSource is used when no payload source is provided for a Stream. +// Whenever DoPull is called, it simply immediately responds with no data and +// EOS set. +namespace { +class NullSource final : public Buffer::Source { + public: + NullSource() : Source() {} + + BaseObjectPtr GetStrongPtr() override { + return BaseObjectPtr(); + } + + size_t Acknowledge(uint64_t offset, size_t datalen) override { return 0; } + size_t Seek(size_t amount) override { return 0; } + bool is_closed() const override { return true; } + bool is_finished() const override { return true; } + void set_finished() override {} + void set_closed() override {} + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override { + std::move(next)(bob::Status::STATUS_END, nullptr, 0, [](size_t len) {}); + return bob::Status::STATUS_END; + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(NullSource) + SET_SELF_SIZE(NullSource) +}; + +NullSource null_source_; +} // namespace + +// ====================================================================================== +// Stream + +Stream* Stream::From(ngtcp2_conn* conn, void* stream_user_data) { + Stream* stream = static_cast(stream_user_data); + CHECK_NOT_NULL(stream); + CHECK_EQ(stream->session()->connection(), conn); + return stream; +} + +Local Stream::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.stream_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Stream")); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Stream::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "destroy", DoDestroy); + SetProtoMethod(isolate, tmpl, "attachSource", AttachSource); + SetProtoMethod(isolate, tmpl, "flushInbound", FlushInbound); + SetProtoMethod(isolate, tmpl, "sendHeaders", DoSendHeaders); + SetProtoMethod(isolate, tmpl, "stopSending", DoStopSending); + SetProtoMethod(isolate, tmpl, "resetStream", DoResetStream); + SetProtoMethod(isolate, tmpl, "setPriority", DoSetPriority); + SetProtoMethodNoSideEffect(isolate, tmpl, "getPriority", DoGetPriority); + state.set_stream_constructor_template(tmpl); + } + return tmpl; +} + +void Stream::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(DoDestroy); + registry->Register(AttachSource); + registry->Register(FlushInbound); + registry->Register(DoSendHeaders); + registry->Register(DoStopSending); + registry->Register(DoResetStream); + ArrayBufferViewSource::RegisterExternalReferences(registry); + StreamSource::RegisterExternalReferences(registry); + StreamBaseSource::RegisterExternalReferences(registry); + BlobSource::RegisterExternalReferences(registry); +} + +void ArrayBufferViewSource::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); +} +void BlobSource::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); +} +void StreamSource::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(End); + registry->Register(Write); + registry->Register(WriteV); +} +void StreamBaseSource::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); +} + +void Stream::Initialize(Environment* env, Local target) { + USE(GetConstructorTemplate(env)); + + ArrayBufferViewSource::Initialize(env, target); + StreamSource::Initialize(env, target); + StreamBaseSource::Initialize(env, target); + BlobSource::Initialize(env, target); + +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_##name); + STREAM_STATS(V) + NODE_DEFINE_CONSTANT(target, IDX_STATS_STREAM_COUNT); +#undef V +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_STREAM_##name); + STREAM_STATE(V) + NODE_DEFINE_CONSTANT(target, IDX_STATE_STREAM_COUNT); +#undef V + + constexpr int QUIC_STREAM_HEADERS_KIND_INFO = + static_cast(Session::Application::HeadersKind::INFO); + constexpr int QUIC_STREAM_HEADERS_KIND_INITIAL = + static_cast(Session::Application::HeadersKind::INITIAL); + constexpr int QUIC_STREAM_HEADERS_KIND_TRAILING = + static_cast(Session::Application::HeadersKind::TRAILING); + + constexpr int QUIC_STREAM_HEADERS_FLAGS_NONE = + static_cast(Session::Application::HeadersFlags::NONE); + constexpr int QUIC_STREAM_HEADERS_FLAGS_TERMINAL = + static_cast(Session::Application::HeadersFlags::TERMINAL); + + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_INFO); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_INITIAL); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_TRAILING); + + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_NONE); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_TERMINAL); +} + +BaseObjectPtr Stream::Create(Environment* env, + Session* session, + stream_id id) { + Local obj; + Local tmpl = GetConstructorTemplate(env); + CHECK(!tmpl.IsEmpty()); + if (!tmpl->InstanceTemplate()->NewInstance(env->context()).ToLocal(&obj)) + return BaseObjectPtr(); + + return MakeBaseObject(BaseObjectPtr(session), obj, id); +} + +Stream::Stream(BaseObjectPtr session, + Local object, + stream_id id, + Buffer::Source* source) + : AsyncWrap(session->env(), object, AsyncWrap::PROVIDER_QUICSTREAM), + StreamStatsBase(session->env()), + session_(session), + state_(session->env()->isolate()) { + MakeWeak(); + state_->id = id; + + USE(ngtcp2_conn_set_stream_user_data(session->connection(), id, this)); + AttachOutboundSource(source); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env()->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env()->state_string(), state_.GetArrayBuffer()); + defineProperty(env()->stats_string(), ToBigUint64Array(env())); + + auto params = ngtcp2_conn_get_local_transport_params(session->connection()); + IncrementStat(&StreamStats::max_offset, params->initial_max_data); +} + +void Stream::Acknowledge(uint64_t offset, size_t datalen) { + if (is_destroyed() || outbound_source_ == nullptr) return; + + // ngtcp2 guarantees that offset must always be greater than the previously + // received offset. + DCHECK_GE(offset, GetStat(&StreamStats::max_offset_ack)); + SetStat(&StreamStats::max_offset_ack, offset); + + // Consumes the given number of bytes in the buffer. + CHECK_LE(outbound_source_->Acknowledge(offset, datalen), datalen); +} + +bool Stream::AddHeader(const Header& header) { + if (is_destroyed()) return false; + size_t len = header.length(); + auto& app = session()->application(); + if (!app.CanAddHeader(headers_.size(), current_headers_length_, len)) + return false; + + current_headers_length_ += len; + + auto& state = BindingState::Get(env()); + + const auto push = [&](auto raw) { + Local value; + if (UNLIKELY(!raw.ToLocal(&value))) return false; + headers_.push_back(value); + return true; + }; + + return push(header.GetName(&state)) && push(header.GetValue(&state)); +} + +void Stream::AttachOutboundSource(Buffer::Source* source) { + outbound_source_ = source; + outbound_source_strong_ptr_.reset(); + if (source != nullptr) { + outbound_source_strong_ptr_ = source->GetStrongPtr(); + Resume(); + } +} + +void Stream::BeginHeaders(Session::Application::HeadersKind kind) { + if (is_destroyed()) return; + headers_.clear(); + headers_kind_ = kind; +} + +void Stream::Blocked() { + if (is_destroyed()) return; + EmitBlocked(); +} + +Maybe Stream::EmitData(Buffer::Chunk::Queue queue, bool ended) { + if (!env()->can_call_into_js()) return Nothing(); + CallbackScope cb_scope(this); + auto& state = BindingState::Get(env()); + + std::vector> items; + size_t len = 0; + while (!queue.empty()) { + Local val; + len += queue.front().length(); + // If this fails, the error is unrecoverable and neither is the data. Return + // nothing to signal error and handle upstream. + if (!queue.front().Release(env()).ToLocal(&val)) return Nothing(); + queue.pop_front(); + items.emplace_back(val); + } + + Local args[] = { + Array::New(env()->isolate(), items.data(), items.size()), + ended ? v8::True(env()->isolate()) : v8::False(env()->isolate())}; + MakeCallback(state.stream_data_callback(), arraysize(args), args); + return Just(len); +} + +void Stream::EmitBlocked() { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + MakeCallback(BindingState::Get(env()).stream_blocked_callback(), 0, nullptr); +} + +void Stream::EmitClose() { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + MakeCallback(BindingState::Get(env()).stream_close_callback(), 0, nullptr); +} + +void Stream::EmitReset(QuicError error) { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + Local argv[] = { + Integer::New(env()->isolate(), static_cast(error.type())), + BigInt::NewFromUnsigned(env()->isolate(), error.code()), + Undefined(env()->isolate()), + }; + + if (error->reasonlen > 0 && + !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { + return; + } + + MakeCallback( + BindingState::Get(env()).stream_reset_callback(), arraysize(argv), argv); +} + +void Stream::EmitError(QuicError error) { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + Local argv[] = { + Integer::New(env()->isolate(), static_cast(error.type())), + BigInt::NewFromUnsigned(env()->isolate(), error.code()), + Undefined(env()->isolate()), + }; + if (error->reasonlen > 0 && + !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { + return; + } + MakeCallback( + BindingState::Get(env()).stream_error_callback(), arraysize(argv), argv); +} + +void Stream::Commit(size_t amount) { + if (is_destroyed() || outbound_source_ == nullptr) return; + size_t actual = outbound_source_->Seek(amount); + CHECK_LE(actual, amount); +} + +int Stream::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + if (is_destroyed() || state_->reset == 1) return bob::Status::STATUS_EOS; + // If an outbound source has not yet been attached, block until one is + // available. When AttachOutboundSource() is called the stream will be + // resumed. + if (outbound_source_ == nullptr) { + int status = bob::Status::STATUS_BLOCK; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + return outbound_source_->Pull( + std::move(next), options, data, count, max_count_hint); +} + +void Stream::EndHeaders() { + // Nothing to do here. +} + +void Stream::EmitHeaders() { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + + Local argv[] = { + Array::New(env()->isolate(), headers_.data(), headers_.size()), + Integer::NewFromUnsigned(env()->isolate(), + static_cast(headers_kind_))}; + + headers_.clear(); + + MakeCallback(BindingState::Get(env()).stream_headers_callback(), + arraysize(argv), + argv); +} + +void Stream::DoStopSending(const FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + if (!args[0]->IsUndefined()) { + CHECK(args[0]->IsBigInt()); + bool lossless = false; // not used. + error_code code = args[0].As()->Uint64Value(&lossless); + stream->StopSending(QuicError::ForApplication(code)); + } else { + stream->StopSending(QuicError::ForApplication(NGTCP2_APP_NOERROR)); + } +} + +void Stream::DoResetStream(const FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + if (!args[0]->IsUndefined()) { + CHECK(args[0]->IsBigInt()); + bool lossless = false; // not used. + error_code code = args[0].As()->Uint64Value(&lossless); + stream->ResetStream(QuicError::ForApplication(code)); + } else { + stream->ResetStream(QuicError::ForApplication(NGTCP2_APP_NOERROR)); + } +} + +void Stream::DoSendHeaders(const FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsUint32()); // Kind + CHECK(args[1]->IsArray()); // Headers + CHECK(args[2]->IsUint32()); // Flags + + Session::Application::HeadersKind kind = + static_cast( + args[0].As()->Value()); + Local headers = args[1].As(); + Session::Application::HeadersFlags flags = + static_cast( + args[2].As()->Value()); + + args.GetReturnValue().Set(stream->SendHeaders(kind, headers, flags)); +} + +void Stream::DoDestroy(const FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->Destroy(); +} + +void Stream::AttachSource(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + CHECK_IMPLIES(!args[0]->IsUndefined(), args[0]->IsObject()); + + Buffer::Source* source = nullptr; + + if (args[0]->IsUndefined()) { + source = &null_source_; + } else if (ArrayBufferViewSource::HasInstance(env, args[0])) { + ArrayBufferViewSource* view; + ASSIGN_OR_RETURN_UNWRAP(&view, args[0]); + source = view; + } else if (StreamSource::HasInstance(env, args[0])) { + StreamSource* view; + ASSIGN_OR_RETURN_UNWRAP(&view, args[0]); + source = view; + } else if (StreamBaseSource::HasInstance(env, args[0])) { + StreamBaseSource* view; + ASSIGN_OR_RETURN_UNWRAP(&view, args[0]); + source = view; + } else if (BlobSource::HasInstance(env, args[0])) { + BlobSource* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args[0]); + source = blob; + } else { + UNREACHABLE(); + } + + stream->AttachOutboundSource(source); +} + +void Stream::EndWritable() { + if (is_destroyed() || outbound_source_ == nullptr) return; + + Unschedule(); + + outbound_source_->set_closed(); + outbound_source_->set_finished(); + outbound_source_ = nullptr; + outbound_source_strong_ptr_.reset(); +} + +void Stream::FlushInbound(const FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->ProcessInbound(); +} + +void Stream::DoSetPriority(const v8::FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsUint32()); // Priority + CHECK(args[1]->IsUint32()); // Priority flag + + Session::Application::StreamPriority priority = + static_cast( + args[0].As()->Value()); + Session::Application::StreamPriorityFlags flags = + static_cast( + args[1].As()->Value()); + + stream->SetPriority(priority, flags); +} + +void Stream::DoGetPriority(const v8::FunctionCallbackInfo& args) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + args.GetReturnValue().Set(static_cast(stream->GetPriority())); +} + +void Stream::Destroy(Maybe error) { + if (is_destroyed()) return; + + // End the writable before marking as destroyed. + EndWritable(); + + state_->destroyed = 1; + + DEBUG_ARGS(this, "Closing stream %" PRIi64, id()); + + // Immediately try flushing any pending inbound data. + if (!inbound_.is_ended()) { + inbound_.End(); + state_->paused = 0; + ProcessInbound(); + } + + auto on_exit = OnScopeLeave([&]() { + // Remove the stream from the owning session and reset the pointer. + // Note that this must be done after the EmitClose and EmitError + // calls below since once the stream is removed, there might not be + // any strong pointers remaining keeping it from being destroyed. + session_->RemoveStream(id()); + }); + + if (error.IsJust()) { + EmitError(error.FromJust()); + } else { + EmitClose(); + } +} + +void Stream::ProcessInbound() { + if (is_destroyed() || inbound_.is_finished()) return; + + if (state_->data == 0 || // We don't have a data listener + state_->paused == 1 || // We are paused + inbound_.is_finished()) { // Or the inbound buffer has been ended and + // drained. + return; + } + + // Dumps all of the data currently held in the inbound buffer out + // to the JavaScript data listener, returning the amount of data + // that was delivered. + Maybe amt = inbound_.Release(this); + + if (amt.IsNothing()) + return Destroy(Just(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL))); + + size_t len = amt.FromJust(); + IncrementStat(&StreamStats::max_offset, len); + if (session_) session_->ExtendStreamOffset(id(), len); +} + +void Stream::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("outbound", outbound_source_); + tracker->TrackField("outbound_strong_ptr", outbound_source_strong_ptr_); + tracker->TrackField("inbound", inbound_); + tracker->TrackField("headers", headers_); + StatsBase::StatsMemoryInfo(tracker); +} + +void Stream::ReadyForTrailers() { + if (LIKELY(state_->trailers == 0)) return; + + EmitTrailers(); +} + +void Stream::EmitTrailers() { + if (!env()->can_call_into_js()) return; + CallbackScope cb_scope(this); + MakeCallback(BindingState::Get(env()).stream_trailers_callback(), 0, nullptr); +} + +void Stream::ReceiveData(Session::Application::ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + if (is_destroyed()) return; + + // If reading has ended, do nothing and drop the data on the floor. + if (state_->read_ended == 1) return; + + // ngtcp2 guarantees that datalen will only be 0 if fin is set. + DCHECK_IMPLIES(datalen == 0, flags.fin); + + // ngtcp2 guarantees that offset is greater than the previously received. + DCHECK_GE(offset, GetStat(&StreamStats::max_offset_received)); + SetStat(&StreamStats::max_offset_received, offset); + + if (datalen > 0) { + // IncrementStats will update the data_rx_rate_ and data_rx_size_ + // histograms. These will provide data necessary to detect and + // prevent Slow Send DOS attacks specifically by allowing us to + // see if a connection is sending very small chunks of data at very + // slow speeds. It is important to emphasize, however, that slow send + // rates may be perfectly legitimate so we cannot simply take blanket + // action when slow rates are detected. Nor can we reliably define what + // a slow rate even is! Will will need to determine some reasonable + // default and allow user code to change the default as well as determine + // what action to take. The current strategy will be to trigger an event + // on the stream when data transfer rates are likely to be considered too + // slow. + UpdateStats(datalen); + inbound_.Push(env(), data, datalen); + } + + if (flags.fin) { + set_final_size(offset + datalen); + inbound_.End(); + } + + ProcessInbound(); +} + +void Stream::ReceiveResetStream(size_t final_size, QuicError error) { + if (is_destroyed()) return; + set_final_size(final_size); + // Importantly, reset stream only impacts the inbound data flow. + // It has no impact on the outbound data flow. + inbound_.End(); + state_->read_ended = 1; + ProcessInbound(); + EmitReset(error); +} + +void Stream::ReceiveStopSending(QuicError error) { + if (is_destroyed() || state_->read_ended) return; + // Note that this comes from *this* endpoint, not the other side. We handle it + // if we haven't already shutdown our *receiving* side of the stream. + + ngtcp2_conn_shutdown_stream_read(session()->connection(), id(), error.code()); + inbound_.End(); + state_->read_ended = 1; +} + +void Stream::ResetStream(QuicError error) { + if (is_destroyed()) return; + CHECK_EQ(error.type(), QuicError::Type::APPLICATION); + Session::SendPendingDataScope send_scope(session()); + EndWritable(); + ngtcp2_conn_shutdown_stream_write( + session()->connection(), id(), error.code()); + state_->reset = 1; +} + +void Stream::StopSending(QuicError error) { + if (is_destroyed()) return; + CHECK_EQ(error.type(), QuicError::Type::APPLICATION); + Session::SendPendingDataScope send_scope(session()); + // Now we shut down the stream readable side. + ngtcp2_conn_shutdown_stream_read(session()->connection(), id(), error.code()); + inbound_.End(); + state_->read_ended = 1; +} + +void Stream::Resume() { + if (is_destroyed() || outbound_source_ == nullptr) return; + if (!outbound_source_->is_finished()) { + Session::SendPendingDataScope send_scope(session()); + session()->ResumeStream(id()); + } else { + // What are we doing here if we're already finished? + EndWritable(); + } +} + +bool Stream::SendHeaders(Session::Application::HeadersKind kind, + const Local& headers, + Session::Application::HeadersFlags flags) { + if (is_destroyed()) return false; + return session_->application().SendHeaders(id(), kind, headers, flags); +} + +void Stream::UpdateStats(size_t datalen) { + uint64_t len = static_cast(datalen); + IncrementStat(&StreamStats::bytes_received, len); +} + +void Stream::set_final_size(uint64_t final_size) { + CHECK_IMPLIES(state_->fin_received == 1, + final_size <= GetStat(&StreamStats::final_size)); + state_->fin_received = 1; + SetStat(&StreamStats::final_size, final_size); +} + +void Stream::Schedule(Queue* queue) { + if (is_destroyed() || outbound_source_ == nullptr) return; + // If this stream is not already in the queue to send data, add it. + if (stream_queue_.IsEmpty()) queue->PushBack(this); +} + +void Stream::SetPriority(Session::Application::StreamPriority priority, + Session::Application::StreamPriorityFlags flags) { + if (is_destroyed()) return; + session_->application().SetStreamPriority(this, priority, flags); +} + +Session::Application::StreamPriority Stream::GetPriority() { + if (is_destroyed()) return Session::Application::StreamPriority::DEFAULT; + return session_->application().GetStreamPriority(this); +} + +// ====================================================================================== +// Buffer::SourceObject + +BaseObjectPtr Buffer::Source::GetStrongPtr() { + return BaseObjectPtr(); +} + +void Buffer::Source::Resume() { + if (stream != nullptr) stream->Resume(); +} + +Buffer::SourceObject::SourceObject(Environment* env) : state_(env->isolate()) {} + +void Buffer::SourceObject::set_finished() { + state_->eos = 1; +} + +void Buffer::SourceObject::set_closed() { + state_->closed = 1; +} + +bool Buffer::SourceObject::is_finished() const { + return state_->eos == 1; +} + +bool Buffer::SourceObject::is_closed() const { + return state_->closed == 1; +} + +// ====================================================================================== +// ArrayBufferViewSource + +Local ArrayBufferViewSource::GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = + state.arraybufferviewsource_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = NewFunctionTemplate(env->isolate(), New); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + BaseObject::kInternalFieldCount); + tmpl->SetClassName(state.arraybufferviewsource_string()); + state.set_arraybufferviewsource_constructor_template(tmpl); + } + return tmpl; +} + +void ArrayBufferViewSource::Initialize(Environment* env, Local target) { + SetConstructorFunction(env->context(), + target, + "ArrayBufferViewSource", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void ArrayBufferViewSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsArrayBufferView()); + Environment* env = Environment::GetCurrent(args); + new ArrayBufferViewSource( + env, args.This(), Store(args[0].As())); +} + +ArrayBufferViewSource::ArrayBufferViewSource(Environment* env, + Local object, + Store&& source) + : Buffer::SourceObject(env), + BaseObject(env, object), + buffer_(std::move(source)) { + MakeWeak(); + + // All of the data for this source is provided by the provided Store + set_closed(); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env->state_string(), state_.GetArrayBuffer()); +} + +int ArrayBufferViewSource::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return buffer_.Pull(std::move(next), options, data, count, max_count_hint); +} + +size_t ArrayBufferViewSource::Acknowledge(uint64_t offset, size_t datalen) { + return buffer_.Acknowledge(datalen); +} + +size_t ArrayBufferViewSource::Seek(size_t amount) { + size_t ret = buffer_.Seek(amount); + if (buffer_.is_finished()) set_finished(); + return ret; +} + +void ArrayBufferViewSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("buffer", buffer_); +} + +BaseObjectPtr ArrayBufferViewSource::GetStrongPtr() { + return BaseObjectPtr(this); +} + +// ====================================================================================== +// StreamSource + +Local StreamSource::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.streamsource_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "end", End); + SetProtoMethod(isolate, tmpl, "write", Write); + SetProtoMethod(isolate, tmpl, "writev", WriteV); + tmpl->InstanceTemplate()->Set(env->owner_symbol(), Null(env->isolate())); + tmpl->SetClassName(state.streamsource_string()); + state.set_streamsource_constructor_template(tmpl); + } + return tmpl; +} + +void StreamSource::Initialize(Environment* env, Local target) { + SetConstructorFunction(env->context(), + target, + "StreamSource", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void StreamSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new StreamSource(env, args.This()); +} + +StreamSource::StreamSource(Environment* env, Local object) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_QUICSTREAMSOURCE), + Buffer::SourceObject(env) { + MakeWeak(); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env->state_string(), state_.GetArrayBuffer()); +} + +int StreamSource::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return queue_.Pull(std::move(next), options, data, count, max_count_hint); +} + +void StreamSource::set_closed() { + queue_.End(); + SourceObject::set_closed(); +} + +void StreamSource::End(const FunctionCallbackInfo& args) { + StreamSource* source; + ASSIGN_OR_RETURN_UNWRAP(&source, args.Holder()); + + CHECK(!source->is_finished()); + CHECK(!source->is_closed()); + + // There's no more data to be sent. Close the source and let the stream know. + DEBUG(source, "Received end() from JavaScript."); + source->set_closed(); + source->Resume(); +} + +void StreamSource::Write(const FunctionCallbackInfo& args) { + StreamSource* source; + ASSIGN_OR_RETURN_UNWRAP(&source, args.Holder()); + + // The JavaScript side is responsible for making sure not to write + // when closed or finished are set. + CHECK(!source->is_closed()); + CHECK(!source->is_finished()); + + CHECK(args[0]->IsArrayBufferView()); + + auto view = args[0].As(); + if (view->ByteLength() == 0) return; + + DEBUG_ARGS( + source, "Writing %" PRIu64 " bytes from JavaScript.", view->ByteLength()); + + source->queue_.Push(Store(view)); + source->Resume(); +} + +void StreamSource::WriteV(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + StreamSource* source; + ASSIGN_OR_RETURN_UNWRAP(&source, args.Holder()); + + // The JavaScript side is responsible for making sure not to write + // when closed or finished are set. + + CHECK(args[0]->IsArray()); + Local data = args[0].As(); + for (size_t n = 0; n < data->Length(); n++) { + Local item; + if (!data->Get(env->context(), n).ToLocal(&item)) return; + + CHECK(item->IsArrayBufferView()); + + auto view = item.As(); + if (view->ByteLength() == 0) continue; + + DEBUG_ARGS(source, + "Writing %" PRIu64 " bytes from JavaScript.", + view->ByteLength()); + + source->queue_.Push(Store(item.As())); + } + + source->Resume(); +} + +size_t StreamSource::Acknowledge(uint64_t offset, size_t datalen) { + return queue_.Acknowledge(datalen); +} + +size_t StreamSource::Seek(size_t amount) { + size_t ret = queue_.Seek(amount); + if (queue_.is_finished()) set_finished(); + return ret; +} + +void StreamSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", queue_); +} + +BaseObjectPtr StreamSource::GetStrongPtr() { + return BaseObjectPtr(this); +} + +// ====================================================================================== +// StreamBaseSource + +Local StreamBaseSource::GetConstructorTemplate( + Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.streambasesource_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = NewFunctionTemplate(env->isolate(), New); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + tmpl->SetClassName(state.streambasesource_string()); + state.set_streambasesource_constructor_template(tmpl); + } + return tmpl; +} + +void StreamBaseSource::Initialize(Environment* env, Local target) { + SetConstructorFunction(env->context(), + target, + "StreamBaseSource", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void StreamBaseSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsObject()); + + Environment* env = Environment::GetCurrent(args); + + StreamBase* wrap = StreamBase::FromObject(args[0].As()); + CHECK_NOT_NULL(wrap); + StreamBaseSource* source = new StreamBaseSource( + env, args.This(), wrap, BaseObjectPtr(wrap->GetAsyncWrap())); + wrap->PushStreamListener(source); + wrap->ReadStart(); +} + +StreamBaseSource::StreamBaseSource(Environment* env, + Local object, + StreamBase* resource, + BaseObjectPtr strong_ptr) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_QUICSTREAMBASESOURCE), + Buffer::SourceObject(env), + resource_(resource), + strong_ptr_(std::move(strong_ptr)) { + MakeWeak(); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env->state_string(), state_.GetArrayBuffer()); + + CHECK_NOT_NULL(resource); +} + +void StreamBaseSource::set_closed() { + buffer_.End(); + resource_->ReadStop(); + resource_->RemoveStreamListener(this); + SourceObject::set_closed(); +} + +uv_buf_t StreamBaseSource::OnStreamAlloc(size_t suggested_size) { + uv_buf_t buf; + buf.base = Malloc(suggested_size); + buf.len = suggested_size; + return buf; +} + +void StreamBaseSource::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) { + if (nread == UV_EOF && buffer_.is_ended()) { + CHECK_NULL(buf_.base); + return; + } + CHECK(!buffer_.is_ended()); + + if (nread < 0) { + DEBUG_ARGS( + this, "Done reading from StreamBase or Error [%" PRIi64 "]", nread); + // TODO(@jasnell): There's either been an error or we've reached the end. + // handle appropriately. For now, we're not reporting the error, just + // closing the source and moving on. + set_closed(); + Resume(); + } else if (nread > 0) { + DEBUG_ARGS(this, "Read %" PRIi64 " bytes from StreamBase", nread); + CHECK_NOT_NULL(buf_.base); + size_t read = nread; + buffer_.Push(Store(ArrayBuffer::NewBackingStore( + static_cast(buf_.base), + read, + [](void* ptr, size_t len, void* deleter_data) { + std::unique_ptr delete_me( + static_cast(ptr)); + }, + nullptr), + read)); + Resume(); + } else if (nread == 0 && buf_.base != nullptr) { + DEBUG(this, "Received zero byte read from StreamBase?"); + // An empty read is odd but not an error. Just drop it on the floor and + // continue. + std::unique_ptr delete_me(buf_.base); + } +} + +int StreamBaseSource::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return buffer_.Pull(std::move(next), options, data, count, max_count_hint); +} + +size_t StreamBaseSource::Acknowledge(uint64_t offset, size_t datalen) { + return buffer_.Acknowledge(datalen); +} + +size_t StreamBaseSource::Seek(size_t amount) { + size_t ret = buffer_.Seek(amount); + if (buffer_.is_finished()) set_finished(); + return ret; +} + +void StreamBaseSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", buffer_); +} + +BaseObjectPtr StreamBaseSource::GetStrongPtr() { + return BaseObjectPtr(this); +} + +// ====================================================================================== +// BlobSource + +Local BlobSource::GetConstructorTemplate(Environment* env) { + auto& state = BindingState::Get(env); + Local tmpl = state.blobsource_constructor_template(); + if (tmpl.IsEmpty()) { + auto isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + tmpl->SetClassName(state.blobsource_string()); + state.set_blobsource_constructor_template(tmpl); + } + return tmpl; +} + +void BlobSource::Initialize(Environment* env, Local target) { + SetConstructorFunction(env->context(), + target, + "BlobSource", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void BlobSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + CHECK(Blob::HasInstance(env, args[0])); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args[0]); + new BlobSource(env, args.This(), BaseObjectPtr(blob)); +} + +BlobSource::BlobSource(Environment* env, + Local object, + BaseObjectPtr blob) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_QUICBLOBSOURCE), + Buffer::SourceObject(env), + buffer_(*blob) { + MakeWeak(); + set_closed(); + + const auto defineProperty = [&](auto name, auto value) { + object + ->DefineOwnProperty( + env->context(), name, value, PropertyAttribute::ReadOnly) + .Check(); + }; + + defineProperty(env->state_string(), state_.GetArrayBuffer()); +} + +int BlobSource::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return buffer_.Pull(std::move(next), options, data, count, max_count_hint); +} + +size_t BlobSource::Acknowledge(uint64_t offset, size_t datalen) { + return buffer_.Acknowledge(datalen); +} + +size_t BlobSource::Seek(size_t amount) { + size_t ret = buffer_.Seek(amount); + if (buffer_.is_finished()) set_finished(); + return ret; +} + +void BlobSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("buffer", buffer_); +} + +BaseObjectPtr BlobSource::GetStrongPtr() { + return BaseObjectPtr(this); +} + +// ====================================================================================== +// Buffer::Chunk + +Buffer::Chunk::Chunk(Store&& store) + : data_(std::move(store)), unacknowledged_(data_.length()) {} + +Buffer::Chunk Buffer::Chunk::Create(Environment* env, + const uint8_t* data, + size_t len) { + auto store = v8::ArrayBuffer::NewBackingStore(env->isolate(), len); + memcpy(store->Data(), data, len); + return Buffer::Chunk(Store(std::move(store), len)); +} + +Buffer::Chunk Buffer::Chunk::Create(Store&& data) { + return Buffer::Chunk(std::move(data)); +} + +MaybeLocal Buffer::Chunk::Release(Environment* env) { + CHECK(data_); + auto ret = data_.ToArrayBufferView(env); + USE(std::move(data_)); + read_ = 0; + unacknowledged_ = 0; + return ret; +} + +size_t Buffer::Chunk::Seek(size_t amount) { + CHECK(data_); + amount = std::min(amount, remaining()); + read_ += amount; + CHECK_LE(read_, data_.length()); + return amount; +} + +size_t Buffer::Chunk::Acknowledge(size_t amount) { + amount = std::min(amount, unacknowledged_); + unacknowledged_ -= amount; + return amount; +} + +Buffer::Chunk::operator ngtcp2_vec() const { + CHECK(data_); + ngtcp2_vec vec = data_; + CHECK_LE(remaining(), vec.len); + ngtcp2_vec ret; + ret.base = vec.base; + ret.len = remaining(); + return ret; +} + +Buffer::Chunk::operator uv_buf_t() const { + CHECK(data_); + uv_buf_t buf = data_; + CHECK_LE(remaining(), buf.len); + uv_buf_t ret; + ret.base = buf.base; + ret.len = remaining(); + return ret; +} + +void Buffer::Chunk::MemoryInfo(MemoryTracker* tracker) const { + if (data_) tracker->TrackFieldWithSize("data", data_ ? data_.length() : 0); +} + +const uint8_t* Buffer::Chunk::data() const { + CHECK(data_); + ngtcp2_vec vec = data_; + return vec.base + read_; +} + +// ====================================================================================== +// Buffer + +Buffer::Buffer(const Blob& blob) { + for (const auto& entry : blob.entries()) + Push(Store(entry.store, entry.length, entry.offset)); + End(); +} + +Buffer::Buffer(Store&& store) { + Push(std::move(store)); + End(); +} + +void Buffer::Push(Environment* env, const uint8_t* data, size_t len) { + CHECK(!ended_); + queue_.emplace_back(Buffer::Chunk::Create(env, data, len)); + length_ += len; + remaining_ += len; +} + +void Buffer::Push(Store&& store) { + CHECK(!ended_); + length_ += store.length(); + remaining_ += store.length(); + queue_.push_back(Chunk::Create(std::move(store))); +} + +size_t Buffer::Seek(size_t amount) { + if (queue_.empty()) { + CHECK_EQ(remaining_, 0); // Remaining should be zero + if (ended_) finished_ = true; + return 0; + } + amount = std::min(amount, remaining_); + size_t len = 0; + while (amount > 0) { + size_t chunk_remaining_ = queue_[head_].remaining(); + size_t actual = queue_[head_].Seek(amount); + CHECK_LE(actual, amount); + amount -= actual; + remaining_ -= actual; + len += actual; + if (actual >= chunk_remaining_) { + head_++; + // head_ should never extend beyond queue size! + CHECK_LE(head_, queue_.size()); + } + } + if (remaining_ == 0 && ended_) finished_ = true; + return len; +} + +size_t Buffer::Acknowledge(size_t amount) { + if (queue_.empty()) return 0; + amount = std::min(amount, length_); + size_t len = 0; + while (amount > 0) { + CHECK_GT(queue_.size(), 0); + size_t actual = queue_.front().Acknowledge(amount); + + CHECK_LE(actual, amount); + amount -= actual; + length_ -= actual; + len += actual; + // If we've acknowledged all of the bytes in the current chunk, pop it to + // free the memory and decrement the head_ pointer if necessary. + if (queue_.front().length() == 0) { + queue_.pop_front(); + if (head_ > 0) head_--; + } + } + return len; +} + +void Buffer::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", queue_); +} + +int Buffer::DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + size_t len = 0; + size_t numbytes = 0; + int status = bob::Status::STATUS_CONTINUE; + + // There's no data to read. + if (queue_.empty() || !remaining_) { + status = ended_ ? bob::Status::STATUS_END : bob::Status::STATUS_BLOCK; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + // Ensure that there's storage space. + MaybeStackBuffer vec; + size_t queue_size = queue_.size() - head_; + + max_count_hint = + (max_count_hint == 0) ? queue_size : std::min(max_count_hint, queue_size); + + CHECK_IMPLIES(data == nullptr, count == 0); + if (data == nullptr) { + vec.AllocateSufficientStorage(max_count_hint); + data = vec.out(); + count = max_count_hint; + } + + // Build the list of buffers. + for (size_t n = head_; n < queue_.size() && len < count; n++, len++) { + data[len] = queue_[n]; + numbytes += data[len].len; + } + + // If the buffer is ended, and the number of bytes matches the total + // remaining, and OPTIONS_END is used, set the status to STATUS_END. + if (is_ended() && numbytes == remaining() && options & bob::OPTIONS_END) { + status = bob::Status::STATUS_END; + } + + // Pass the data back out to the caller. + std::move(next)(status, data, len, [this](size_t len) { + size_t actual = Seek(len); + CHECK_LE(actual, len); + }); + + return status; +} + +Maybe Buffer::Release(Stream* stream) { + if (queue_.empty()) return Just(static_cast(0)); + head_ = 0; + length_ = 0; + remaining_ = 0; + if (ended_) finished_ = true; + return stream->EmitData(std::move(queue_), ended_); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/stream.h b/src/quic/stream.h new file mode 100644 index 00000000000000..1ffb2558ed0f52 --- /dev/null +++ b/src/quic/stream.h @@ -0,0 +1,708 @@ +#pragma once + +#include "aliased_struct.h" +#include "memory_tracker.h" +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include +#include "quic.h" +#include "session.h" + +namespace node { +namespace quic { + +#define V(_, name, __) uint64_t name; +struct StreamStats final { + STREAM_STATS(V) +}; +#undef V + +struct StreamStatsTraits final { + using Stats = StreamStats; + using Base = StatsBase; + + template + static void ToString(const Stats& stats, Fn&& add_field) { +#define V(_, id, desc) add_field(desc, stats.id); + STREAM_STATS(V) +#undef V + } +}; + +using StreamStatsBase = StatsBase; +using Ngtcp2Source = bob::SourceImpl; + +class ArrayBufferViewSource; +class BlobSource; +class StreamSource; +class StreamBaseSource; + +// ============================================================================= +// ngtcp2 is responsible for determining when and how to encode stream data into +// a packet. +// +// An individual QUIC packet may contain multiple QUIC frames. Whenever we +// create a QUIC packet, we really have no idea what frames are going to be +// encoded or how much buffered handshake or stream data is going to be included +// within that Packet (if any). If there is buffered data available for a +// stream, we provide an array of pointers to that data and an indication about +// how much data is available, then we leave it entirely up to ngtcp2 and +// nghttp3 to determine how much of the data to encode into the QUIC packet (if +// any). It is only *after* the QUIC packet is encoded that we can know how much +// was actually written. +// +// Once stream data is written to a Packet, we have to keep the data in memory +// until an acknowledgement is received. In QUIC, acknowledgements are received +// per range of packets, but (fortunately) ngtcp2 gives us that information as +// byte offsets instead. +// +// Buffer is complicated because it needs to be able to accomplish three things: +// (a) buffering v8::BackingStore instances passed down from JavaScript without +// memcpy, (b) tracking what data has already been encoded in a QUIC packet and +// what data is remaining +// to be read, and +// (c) tracking which data has been acknowledged and which hasn't. +// +// Buffer contains a deque of Buffer::Chunk instances. A single Buffer::Chunk +// wraps a v8::BackingStore with length and offset. When the Buffer::Chunk is +// created, we capture the total length of the buffer (minus the offset) and the +// total number of bytes remaining to be sent. Initially, these numbers are +// identical. +// +// When data is encoded into a Packet, we advance the Buffer::Chunk's +// remaining-to-be-read by the number of bytes actually encoded. If there are no +// more bytes remaining to be encoded, we move to the next chunk in the deque +// (but we do not yet pop it off the deque). +// +// When an acknowledgement is received, we decrement the Buffer::Chunk's length +// by the number of acknowledged bytes. Once the unacknowledged length reaches 0 +// we pop the chunk off the deque. +class Buffer : public Ngtcp2Source, public MemoryRetainer { + public: + // Stores chunks of both inbound and outbound data. Each chunk stores a shared + // pointer to a v8::BackingStore with appropriate length and offset details. + // Each Buffer::Chunk is stored in a deque in Buffer which manages the + // aggregate collection of all chunks. + class Chunk final : public MemoryRetainer { + public: + // Copies len bytes from data into a new Chunk. + static Chunk Create(Environment* env, const uint8_t* data, size_t len); + + // Stores the given BackingStore directly without copying. One important + // thing here is the fact the data is not immutable. If user code passes a + // TypedArray or ArrayBuffer in, the user code can continue to modify it + // after. For now that's an acceptable risk as it is definitely an edge + // case. Later, we might want to consider allowing for a copy with the + // understanding that doing so will introduce a small performance hit. + static Chunk Create(Store&& store); + + // Identifies an amount of stored data to be acknowledged. Once the amount + // of acknowledged data equals length_, the chunk can be freed from memory. + // Returns the actual amount of data acknowledged. + size_t Acknowledge(size_t amount); + + // Releases the chunk to a v8 Uint8Array. data_ is reset and offset_, + // length_, and consumed_ are all set to 0 and the strong_ptr_, if any, is + // reset. This is used only for inbound data and only when queued data is + // being flushed out to the JavaScript side. + v8::MaybeLocal Release(Environment* env); + + // Increments consumed_ by amount bytes. If amount is greater than + // remaining(), remaining() bytes are advanced. Returns the actual number of + // bytes advanced. + size_t Seek(size_t amount); + + // Returns a pointer to the remaining data. This is used only for outbound + // data. + const uint8_t* data() const; + + // Returns the total remaining number of unacknowledged bytes. + inline size_t length() const { return unacknowledged_; } + + // Returns the total remaining number of non-transmitted bytes. + inline size_t remaining() const { + if (!data_) return 0; + return data_.length() - read_; + } + + operator ngtcp2_vec() const; + operator uv_buf_t() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Buffer::Chunk) + SET_SELF_SIZE(Chunk) + + using Queue = std::deque; + + private: + Chunk(Store&& store); + + Store data_; + size_t read_ = 0; + size_t unacknowledged_ = 0; + }; + + // Provides outbound data for a stream + struct Source : public Ngtcp2Source, public MemoryRetainer { + Stream* stream = nullptr; + + void Attach(Stream* stream); + void Resume(); + + virtual BaseObjectPtr GetStrongPtr() = 0; + virtual size_t Acknowledge(uint64_t offset, size_t amount) = 0; + virtual size_t Seek(size_t amount) = 0; + virtual bool is_finished() const = 0; + virtual bool is_closed() const = 0; + virtual void set_finished() = 0; + virtual void set_closed() = 0; + }; + + class SourceObject : public Source { + public: + explicit SourceObject(Environment* env); + + bool is_finished() const override; + bool is_closed() const override; + void set_finished() override; + void set_closed() override; + + private: + struct State final { +#define V(_, name, type) type name; + SOURCE_STATE(V) +#undef V + }; + + AliasedStruct state_; + + friend class ArrayBufferViewSource; + friend class BlobSource; + friend class StreamSource; + friend class StreamBaseSource; + }; + + Buffer() = default; + explicit Buffer(const Blob& blob); + explicit Buffer(Store&& store); + QUIC_NO_COPY_OR_MOVE(Buffer) + + // Marks the Buffer as having ended, preventing new Buffer::Chunk instances + // from being added and allowing the Pull operation to know when to signal + // that the flow of data is completed. + inline void End() { + ended_ = true; + } + inline bool is_ended() const { + return ended_; + } + inline bool is_finished() const { + return ended_ && remaining_ == 0 && finished_; + } + + // Push inbound data onto the buffer. + void Push(Environment* env, const uint8_t* data, size_t len); + + // Push outbound data onto the buffer. + void Push(Store&& store); + + // Increment the given number of bytes within the buffer. If amount is greater + // than length(), length() bytes are advanced. Returns the actual number of + // bytes advanced. Will not cause bytes to be freed. + size_t Seek(size_t amount); + + // Acknowledge the given number of bytes in the buffer. May cause bytes to be + // freed. + size_t Acknowledge(size_t amount); + + // Clears any bytes remaining in the buffer. + inline void Clear() { + queue_.clear(); + head_ = 0; + length_ = 0; + remaining_ = 0; + } + + // The total number of unacknowledged bytes remaining. The length is + // incremented by Push and decremented by Acknowledge. + inline size_t length() const { + return length_; + } + + // The total number of unread bytes remaining. The remaining length is + // incremental by Push and decremented by Seek. + inline size_t remaining() const { + return remaining_; + } + + // Flushes the entire inbound queue into a v8::Local of Uint8Array + // instances, returning the total number of bytes released to the consumer. + v8::Maybe Release(Stream* consumer); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Buffer); + SET_SELF_SIZE(Buffer); + + private: + Chunk::Queue queue_; + bool ended_ = false; + bool finished_ = false; + + // The queue_ index of the current read head. This is incremented by Seek() as + // necessary and decremented by Acknowledge() as data is consumed. + size_t head_ = 0; + size_t length_ = 0; + size_t remaining_ = 0; +}; + +// QUIC Stream's are simple data flows that may be: +// +// * Bidirectional or Unidirectional +// * Server or Client Initiated +// +// The flow direction and origin of the stream are important in determining the +// write and read state (Open or Closed). Specifically: +// +// A Unidirectional stream originating with the Server is: +// +// * Server Writable (Open) but not Client Writable (Closed) +// * Client Readable (Open) but not Server Readable (Closed) +// +// Likewise, a Unidirectional stream originating with the Client is: +// +// * Client Writable (Open) but not Server Writable (Closed) +// * Server Readable (Open) but not Client Readable (Closed) +// +// Bidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// +// Unidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// +// All data sent via the Stream is buffered internally until either receipt is +// acknowledged from the peer or attempts to send are abandoned. The fact that +// data is buffered in memory makes it essential that the flow control for the +// session and the stream are properly handled. For now, we are largely relying +// on ngtcp2's default flow control mechanisms which generally should be doing +// the right thing but we may need to switch to a more manual management process +// if too much data ends up being buffered for too long. +// +// A Stream may be in a fully closed state (No longer readable nor writable) +// state but still have unacknowledged data in it's outbound queue. +// +// A Stream is gracefully closed when (a) both Read and Write states are Closed, +// (b) all queued data has been acknowledged. +// +// The Stream may be forcefully closed immediately using destroy(err). This +// causes all queued data and pending JavaScript writes to be abandoned, and +// causes the Stream to be immediately closed at the ngtcp2 level without +// waiting for any outstanding acknowledgements. Keep in mind, however, that the +// peer is not notified that the stream is destroyed and may attempt to continue +// sending data and acknowledgements. +class Stream : public AsyncWrap, public Ngtcp2Source, public StreamStatsBase { + public: + using Header = NgHeaderBase; + + enum class Origin { + SERVER, + CLIENT, + }; + + static Stream* From(ngtcp2_conn*, void* stream_user_data); + + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local object); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static BaseObjectPtr Create(Environment* env, + Session* session, + stream_id id); + + Stream(BaseObjectPtr session, + v8::Local object, + stream_id id, + Buffer::Source* source = nullptr); + + inline bool is_destroyed() const { return state_->destroyed; } + inline bool might_send_trailers() const { return state_->trailers; } + + inline stream_id id() const { return state_->id; } + + inline Direction direction() const { + return id() & 0b10 ? Direction::UNIDIRECTIONAL : Direction::BIDIRECTIONAL; + } + + inline Origin origin() const { + return id() & 0b01 ? Origin::SERVER : Origin::CLIENT; + } + + inline Session* session() const { return session_.get(); } + + void SetPriority(Session::Application::StreamPriority priority, + Session::Application::StreamPriorityFlags flags); + Session::Application::StreamPriority GetPriority(); + + // The final size is the maximum amount of data that has been acknowleged to + // have been received for a Stream. + inline uint64_t final_size() const { + return GetStat(&StreamStats::final_size); + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Stream) + SET_SELF_SIZE(Stream) + + private: + struct State final { +#define V(_, name, type) type name; + STREAM_STATE(V) +#undef V + }; + + // JavaScript API + static void AttachSource(const v8::FunctionCallbackInfo& args); + static void DoDestroy(const v8::FunctionCallbackInfo& args); + static void DoSendHeaders(const v8::FunctionCallbackInfo& args); + static void DoStopSending(const v8::FunctionCallbackInfo& args); + static void DoResetStream(const v8::FunctionCallbackInfo& args); + static void DoSetPriority(const v8::FunctionCallbackInfo& args); + static void DoGetPriority(const v8::FunctionCallbackInfo& args); + static void FlushInbound(const v8::FunctionCallbackInfo& args); + + // Internal API + + void UpdateStats(size_t datalen); + + void ProcessInbound(); + void ReadyForTrailers(); + + // Signals the beginning of a new block of headers. + void BeginHeaders(Session::Application::HeadersKind kind); + // Returns false if the header cannot be added. This will typically only + // happen if a maximimum number of headers, or the maximum total header length + // is received. + bool AddHeader(const Header& header); + // Signals the ending of the current block of headers. + void EndHeaders(); + + void Commit(size_t amount); + void Acknowledge(uint64_t offset, size_t datalen); + void AttachOutboundSource(Buffer::Source* source); + + // In the typical case there is no reason to explicitly close a Stream as it + // will be closed automatically when both the readable and writable sides are + // both closed. However, in some cases it is necessary to close the Stream + // immediately, such as when the owning Session is being closed immediately. + // Once a stream is destroyed, there is nothing else it can do and the stream + // should not be used for anything else. The only state remaining will be the + // collected statistics. + void Destroy(v8::Maybe error = v8::Nothing()); + + void ReceiveData(Session::Application::ReceiveStreamDataFlags flags, + const uint8_t* data, + size_t datalen, + uint64_t offset); + + // When we have received a RESET_STREAM frame from the peer, it is an + // indication that they have abruptly terminated their side and will not be + // sending any more data. The final size is an indicator of the amount of data + // *they* recognize as having been sent to us. The QUIC spec leaves the + // specific handling of this frame up to the application. We can choose to + // drop buffered inbound data on the floor or to deliver it to the + // application. We choose to deliver it then end the readable side of the + // stream. Importantly, receiving a RESET_STREAM does *not* destroy the + // stream. It only ends the readable side. If there is a reset-stream event + // registered on the JavaScript wrapper, we will emit the event. + void ReceiveResetStream(size_t final_size, QuicError error); + + void ReceiveStopSending(QuicError error); + + // ResetStream will cause ngtcp2 to queue a RESET_STREAM for this stream, + // signaling abrupt termination of the outbound flow of data. + void ResetStream(QuicError error = QuicError()); + + // StopSending will cause ngtcp2 to queue a STOP_SENDING frame for this stream + // if appropriate. For unidirectional streams for which we are the origin, + // this ends up being a non-op. For Bidirectional streams, a STOP_SENDING + // frame is essentially a polite request to the other side to stop sending + // data on this stream. The other stream is expected to respond with a + // RESET_STREAM frame that indicates abrupt termination of the inbound flow of + // data into this stream. + // + // Calling this will have the effect of shutting down the readable side of + // this stream. Any data currently in the buffer can still be read but no new + // data will be accepted and ngtcp2 should not attempt to push any more in. + void StopSending(QuicError error = QuicError()); + + // Notifies that the stream writable side has been closed. + void EndWritable(); + + void Resume(); + void Blocked(); + + // Sends headers to the QUIC Application. If headers are not supported, false + // will be returned. Otherwise, returns true + bool SendHeaders(Session::Application::HeadersKind kind, + const v8::Local& headers, + Session::Application::HeadersFlags flags = + Session::Application::HeadersFlags::NONE); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + // Set the final size for the Stream. This only works the first time it is + // called. Subsequent calls will be ignored unless the subsequent size is + // greater than the prior set size, in which case we have a bug and we'll + // assert. + void set_final_size(uint64_t final_size); + inline void set_headers_kind(Session::Application::HeadersKind headers_kind) { + headers_kind_ = headers_kind; + } + + // ============================================================================================ + // JavaScript Outcalls + using CallbackScope = CallbackScopeBase; + + void EmitBlocked(); + void EmitClose(); + void EmitError(QuicError error); + void EmitHeaders(); + void EmitReset(QuicError error); + void EmitTrailers(); + v8::Maybe EmitData(Buffer::Chunk::Queue queue, bool ended); + + // ============================================================================================ + // Internal fields + BaseObjectPtr session_; + AliasedStruct state_; + + // The outbound_source_ provides the data that is to be sent by this Stream. + // After the source is read the writable side of the Stream will be closed by + // sending a fin data frame. + Buffer::Source* outbound_source_ = nullptr; + BaseObjectPtr outbound_source_strong_ptr_; + + // The inbound_ buffer contains the data that has been received by this Stream + // but not yet delivered to the JavaScript wrapper. + Buffer inbound_; + + std::vector> headers_; + Session::Application::HeadersKind headers_kind_ = + Session::Application::HeadersKind::INITIAL; + + // The current total byte length of the headers + size_t current_headers_length_ = 0; + + ListNode stream_queue_; + + friend class Session::Application; + friend class Http3Application; + friend class DefaultApplication; + friend class Session; + friend class Buffer; + friend class ArrayBufferViewSource; + friend class BlobSource; + friend class StreamSource; + friend class StreamBaseSource; + + public: + // The Queue/Schedule/Unschedule here are part of the mechanism used to + // determine which streams have data to send on the session. When a stream + // potentially has data available, it will be scheduled in the Queue. Then, + // when the Session::Application starts sending pending data, it will check + // the queue to see if there are streams waiting. If there are, it will grab + // one and check to see if there is data to send. When a stream does not have + // data to send (such as when it is initially created or is using an async + // source that is still waiting for data to be pushed) it will not appear in + // the queue. + using Queue = ListHead; + + void Schedule(Queue* queue); + + inline void Unschedule() { + stream_queue_.Remove(); + } +}; + +// Receives a single ArrayBufferView and uses it's contents as the complete +// source of outbound data for the Stream. +class ArrayBufferViewSource final : public Buffer::SourceObject, + public BaseObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + static void New(const v8::FunctionCallbackInfo& args); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + BaseObjectPtr GetStrongPtr() override; + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ArrayBufferViewSource); + SET_SELF_SIZE(ArrayBufferViewSource); + + private: + ArrayBufferViewSource(Environment* env, + v8::Local wrap, + Store&& store); + + Buffer buffer_; +}; + +// Wraps a Blob instance that provides the outbound data. +class BlobSource final : public AsyncWrap, public Buffer::SourceObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + static void New(const v8::FunctionCallbackInfo& args); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + BaseObjectPtr GetStrongPtr() override; + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(BlobSource); + SET_SELF_SIZE(BlobSource); + + private: + BlobSource(Environment* env, + v8::Local wrap, + BaseObjectPtr blob); + + Buffer buffer_; +}; + +// Implements StreamBase to asynchronously accept outbound data from the +// JavaScript side. +class StreamSource final : public AsyncWrap, public Buffer::SourceObject { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + static void New(const v8::FunctionCallbackInfo& args); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + BaseObjectPtr GetStrongPtr() override; + void set_closed() override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(StreamSource); + SET_SELF_SIZE(StreamSource); + + private: + static void End(const v8::FunctionCallbackInfo& args); + static void Write(const v8::FunctionCallbackInfo& args); + static void WriteV(const v8::FunctionCallbackInfo& args); + + StreamSource(Environment* env, v8::Local wrap); + + Buffer queue_; +}; + +// Implements StreamListener to receive data from any native level StreamBase +// implementation. +class StreamBaseSource final : public AsyncWrap, + public Buffer::SourceObject, + public StreamListener { + public: + HAS_INSTANCE() + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + static void New(const v8::FunctionCallbackInfo& args); + + int DoPull(bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + uv_buf_t OnStreamAlloc(size_t suggested_size) override; + void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override; + BaseObjectPtr GetStrongPtr() override; + void set_closed() override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(StreamBaseSource) + SET_SELF_SIZE(StreamBaseSource) + + private: + StreamBaseSource( + Environment* env, + v8::Local wrap, + StreamBase* resource, + BaseObjectPtr strong_ptr = BaseObjectPtr()); + + StreamBase* resource_; + BaseObjectPtr strong_ptr_; + Buffer buffer_; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/parallel/test-quic-internal-basics.js b/test/parallel/test-quic-internal-basics.js new file mode 100644 index 00000000000000..fbc4a532eae77e --- /dev/null +++ b/test/parallel/test-quic-internal-basics.js @@ -0,0 +1,59 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +require('../common'); + +const { internalBinding } = require('internal/test/binding'); +const { + strictEqual, +} = require('assert'); + +const test = require('node:test'); + +test('The internal binding is there', () => { + + const quic = internalBinding('quic'); + + test('It has the stuff we expect...', () => { + strictEqual(typeof quic.setCallbacks, 'function'); + strictEqual(typeof quic.createEndpoint, 'function'); + + strictEqual(typeof quic.EndpointOptions, 'function'); + strictEqual(typeof quic.SessionOptions, 'function'); + strictEqual(typeof quic.ArrayBufferViewSource, 'function'); + strictEqual(typeof quic.StreamSource, 'function'); + strictEqual(typeof quic.StreamBaseSource, 'function'); + strictEqual(typeof quic.BlobSource, 'function'); + + strictEqual(quic.QUIC_CC_ALGO_CUBIC, 1); + strictEqual(quic.QUIC_CC_ALGO_RENO, 0); + strictEqual(quic.QUIC_MAX_CIDLEN, 20); + strictEqual(quic.QUIC_ERR_NO_ERROR, 0); + strictEqual(quic.QUIC_ERR_INTERNAL_ERROR, 1); + strictEqual(quic.QUIC_ERR_CONNECTION_REFUSED, 2); + strictEqual(quic.QUIC_ERR_FLOW_CONTROL_ERROR, 3); + strictEqual(quic.QUIC_ERR_STREAM_LIMIT_ERROR, 4); + strictEqual(quic.QUIC_ERR_STREAM_STATE_ERROR, 5); + strictEqual(quic.QUIC_ERR_FINAL_SIZE_ERROR, 6); + strictEqual(quic.QUIC_ERR_FRAME_ENCODING_ERROR, 7); + strictEqual(quic.QUIC_ERR_TRANSPORT_PARAMETER_ERROR, 8); + strictEqual(quic.QUIC_ERR_CONNECTION_ID_LIMIT_ERROR, 9); + strictEqual(quic.QUIC_ERR_PROTOCOL_VIOLATION, 10); + strictEqual(quic.QUIC_ERR_INVALID_TOKEN, 11); + strictEqual(quic.QUIC_ERR_APPLICATION_ERROR, 12); + strictEqual(quic.QUIC_ERR_CRYPTO_BUFFER_EXCEEDED, 13); + strictEqual(quic.QUIC_ERR_KEY_UPDATE_ERROR, 14); + strictEqual(quic.QUIC_ERR_AEAD_LIMIT_REACHED, 15); + strictEqual(quic.QUIC_ERR_NO_VIABLE_PATH, 16); + strictEqual(quic.QUIC_ERR_CRYPTO_ERROR, 256); + strictEqual(quic.QUIC_ERR_VERSION_NEGOTIATION_ERROR_DRAFT, 21496); + strictEqual(quic.QUIC_PREFERRED_ADDRESS_IGNORE, 0); + strictEqual(quic.QUIC_PREFERRED_ADDRESS_USE, 1); + + strictEqual(quic.QUIC_DEFAULT_CIPHERS, 'TLS_AES_128_GCM_SHA256:' + + 'TLS_AES_256_GCM_SHA384:' + + 'TLS_CHACHA20_POLY1305_' + + 'SHA256:TLS_AES_128_CCM_SHA256'); + strictEqual(quic.QUIC_DEFAULT_GROUPS, 'X25519:P-256:P-384:P-521'); + }); +}); diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js new file mode 100644 index 00000000000000..688fcaa7f1aaca --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -0,0 +1,58 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +require('../common'); + +const { + constants, + EndpointOptions, +} = require('internal/quic/quic'); + +const test = require('node:test'); +const { + SocketAddress +} = require('net'); + +test('Creating a simple internalBinding(\'quic\').EndpointOptions works', () => { + const addr = new SocketAddress({}); + + test('With no second arg...', () => new EndpointOptions(addr)); + test('With an empty second arg...', () => new EndpointOptions(addr, {})); +}); + +test('We can set some options', () => { + const addr = new SocketAddress({}); + + // Note that we cannot test passing the incorrect types here because the code will + // assert and fail. This is testing the low-level binding. The API layer on top will + // need to perform proper type checking. + const options = new EndpointOptions(addr, { + retryTokenExpiration: 123, + tokenExpiration: 123, + maxWindowOverride: 123, + maxStreamWindowOverride: 123, + maxConnectionsPerHost: 123, + maxConnectionsTotal: 123, + maxStatelessResets: 123, + addressLRUSize: 123, + retryLimit: 123, + maxPayloadSize: 1234, + unacknowledgedPacketThreshold: 123, + validateAddress: true, + disableStatelessReset: false, + rxPacketLoss: 1.0, + txPacketLoss: 0.0, + ccAlgorithm: constants.CongestionControlAlgorithm.CUBIC, + ipv6Only: false, + receiveBufferSize: 123, + sendBufferSize: 123, + ttl: 99, + + // Other properties are ignored + abcfoo: '123', + }); + + options.generateResetTokenSecret(); + // The secret must be exactly 16 bytes long + options.setResetTokenSecret(Buffer.from('hellotherequic!!')); +}); diff --git a/test/parallel/test-quic-internal-endpoint.js b/test/parallel/test-quic-internal-endpoint.js new file mode 100644 index 00000000000000..342d1b87359ddd --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint.js @@ -0,0 +1,235 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); + +const { + constants, + EndpointOptions, + SessionOptions, + Endpoint, + ArrayBufferViewSource, + BlobSource, +} = require('internal/quic/quic'); + +const { + Blob, +} = require('buffer'); + +const { + kHandle, +} = require('internal/blob'); + +const test = require('node:test'); + +const { + SocketAddress +} = require('net'); + +const { + createPrivateKey, + webcrypto: { + subtle, + }, +} = require('node:crypto'); + +const { + strictEqual, +} = require('assert'); + +const fixtures = require('../common/fixtures'); + +async function getPrivateKey() { + const { + privateKey + } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-521', + }, true, ['sign', 'verify']); + return privateKey; +} + +const key = createPrivateKey(fixtures.readKey('rsa_private.pem')); +const ca = [fixtures.readKey('rsa_ca.crt')]; +const certs = fixtures.readKey('rsa_cert.crt'); + +test('We can create a local unused endpoint', async () => { + const options = new EndpointOptions(new SocketAddress()); + const endpoint = new Endpoint(options); + + strictEqual(endpoint.stats.destroyedAt, 0n); + strictEqual(endpoint.stats.bytesReceived, 0n); + strictEqual(endpoint.stats.bytesSent, 0n); + strictEqual(endpoint.stats.packetsReceived, 0n); + strictEqual(endpoint.stats.packetsSent, 0n); + strictEqual(endpoint.stats.serverSessions, 0n); + strictEqual(endpoint.stats.clientSessions, 0n); + strictEqual(endpoint.stats.busyCount, 0n); + + // If the endpoint is unused, it should not hold the event loop open or have any errors. +}); + +test('Create an unref\'d listening endpoint', async () => { + const endpointOptions = new EndpointOptions(new SocketAddress()); + const endpoint = new Endpoint(endpointOptions); + + endpoint.listen(new SessionOptions(constants.Side.SERVER, { + alpn: 'zzz', + secure: { + key: await getPrivateKey(), + } + })).unref(); + + // The listening endpoint should not keep the event loop open and + // exiting should not cause any crashes. +}); + +test('Create a listening endpoint', async () => { + const endpointOptions = new EndpointOptions(new SocketAddress()); + const endpoint = new Endpoint(endpointOptions); + + endpoint.listen(new SessionOptions(constants.Side.SERVER, { + alpn: 'zzz', + secure: { + key: await getPrivateKey(), + } + })); + + endpoint.markAsBusy(); + endpoint.markAsBusy(false); + + const address = endpoint.address; + + strictEqual(address.address, '127.0.0.1'); + strictEqual(address.family, 'ipv4'); + + endpoint.addEventListener('close', common.mustCall()); + + // Stop listening and gracefully close. + await endpoint.close(); + + // The stats are still present, but detached. + strictEqual(endpoint.stats.detached, true); + strictEqual(endpoint.stats.bytesReceived, 0n); + strictEqual(endpoint.stats.bytesSent, 0n); + strictEqual(endpoint.stats.packetsReceived, 0n); + strictEqual(endpoint.stats.packetsSent, 0n); + strictEqual(endpoint.stats.serverSessions, 0n); + strictEqual(endpoint.stats.clientSessions, 0n); + strictEqual(endpoint.stats.busyCount, 0n); +}); + +test('Create a client session', async () => { + const endpointOptions = new EndpointOptions(new SocketAddress()); + const endpoint = new Endpoint(endpointOptions); + const client = endpoint.connect( + new SocketAddress(), + new SessionOptions(constants.Side.CLIENT, { + alpn: 'zzz', + transportParams: { + maxIdleTimeout: 1 + } + })); + + client.addEventListener('close', () => endpoint.close()); +}); + +test('Client and server', async () => { + const serverOptions = new EndpointOptions(new SocketAddress({}), {}); + const server = new Endpoint(serverOptions); + server.listen(new SessionOptions(constants.Side.SERVER, { + alpn: 'zzz', + secure: { + key, + certs, + ca, + clientHello: true, + }, + })); + + server.addEventListener('session', ({ session }) => { + + session.onclienthello = (event) => { + console.log(event.alpn); + console.log(event.servername); + console.log(event.ciphers); + event.done(); + }; + + session.addEventListener('handshake-complete', () => { + console.log('Server handshake is complete', session.handshakeConfirmed); + }); + + session.addEventListener('close', () => { + console.log('Server session closed'); + }); + session.addEventListener('error', ({ error }) => { + console.log('Server session error', error); + }); + session.addEventListener('stream', ({ stream }) => { + stream.addEventListener('data', ({ chunks }) => { + console.log(chunks); + }); + + stream.attachSource(new BlobSource((new Blob(['world']))[kHandle])); + }); + session.addEventListener('datagram', console.log); + session.addEventListener('ocsp', console.log); + session.addEventListener('path-validation', console.log); + }); + + console.log(server.address); + + // --------- + const clientOptions = new EndpointOptions(new SocketAddress({})); + const client = new Endpoint(clientOptions); + + const session = client.connect( + server.address, + new SessionOptions(constants.Side.CLIENT, { + alpn: 'zzz', + servername: 'localhost', + secure: { + rejectUnauthorized: false, + enableTLSTrace: true, + verifyHostnameIdentity: false, + } + })); + + session.addEventListener('close', () => { + console.log('Client session closed'); + client.close(); + server.close(); + }); + session.addEventListener('error', ({ error }) => { + console.log('Client session error', error); + }); + session.addEventListener('stream', console.log); + session.addEventListener('datagram', console.log); + session.addEventListener('ocsp', console.log); + + session.addEventListener('path-validation', console.log); + + session.onhandshakecomplete = () => { + console.log('Client handshake is complete', session.handshakeConfirmed); + }; + + session.onsessionticket = ({ ticket }) => { + console.log('Updated session ticket received', ticket); + }; + + await session.handshakeCompleted; + + const stream = session.open(); + + const enc = new TextEncoder(); + const source = new ArrayBufferViewSource(enc.encode('hello')); + stream.attachSource(source); + + stream.onclose = () => console.log('stream closed!'); + stream.onerror = ({ error }) => console.log('stream error', error); + stream.ondata = ({ chunks, ended }) => { + console.log(chunks); + console.log(ended); + }; +}); diff --git a/test/parallel/test-quic-internal-session-options.js b/test/parallel/test-quic-internal-session-options.js new file mode 100644 index 00000000000000..40c5903cf88804 --- /dev/null +++ b/test/parallel/test-quic-internal-session-options.js @@ -0,0 +1,111 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); + +const { + constants, + SessionOptions, +} = require('internal/quic/quic'); + +const test = require('node:test'); + +const { + SocketAddress, +} = require('net'); + +const { + webcrypto: { + subtle, + }, +} = require('node:crypto'); + +async function getPrivateKey() { + const { + privateKey + } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-521', + }, true, ['sign', 'verify']); + return privateKey; +} + +test('Creating a simple internalBinding(\'quic\'.SessionOptions works', async () => { + new SessionOptions(constants.Side.SERVER, { + alpn: constants.HTTP3_ALPN, + secure: { + key: await getPrivateKey(), + } + }); +}); + +test('We can set some options', async () => { + new SessionOptions(constants.Side.SERVER, { + alpn: 'abc', + servername: 'localhost', + secure: { + key: await getPrivateKey(), + } + }); + + new SessionOptions(constants.Side.CLIENT, { + alpn: 'abc', + servername: 'localhost', + preferredAddressStrategy: constants.PreferredAddressStrategy.USE, + secure: { + key: await getPrivateKey(), + } + }); + + test('Setting TLS options works...', async () => { + + new SessionOptions(constants.Side.SERVER, { + alpn: 'abc', + servername: 'localhost', + preferredAddressStrategy: constants.PreferredAddressStrategy.IGNORE, + secure: { + rejectUnauthorized: true, + clientHello: false, + enableTLSTrace: false, + requestPeerCertificate: true, + ocsp: false, + verifyHostnameIdentity: true, + keylog: false, + sessionID: 'Hello World', + ciphers: constants.DEFAULT_CIPHERS, + groups: constants.DEFAULT_GROUPS, + key: await getPrivateKey(), + certs: [Buffer.from('hello')], + ca: Buffer.from('what'), + crl: Buffer.from('what is happening') + } + }); + }); + + test('Setting Transport Param options works...', async () => { + const ipv4 = new SocketAddress({ address: '123.123.123.123', family: 'ipv4' }); + const ipv6 = new SocketAddress({ address: '::1', family: 'ipv6' }); + + new SessionOptions(constants.Side.CLIENT, { + alpn: 'abc', + secure: { + key: await getPrivateKey(), + }, + transportParams: { + initialMaxStreamDataBidiLocal: 1, + initialMaxStreamDataBidiRemote: 2, + initialMaxStreamDataUni: 3, + initialMaxData: 4, + initialMaxStreamsBidi: 5, + initialMaxStreamsUni: 6, + maxIdleTimeout: 7, + activeConnectionIdLimit: 8, + ackDelayExponent: 9, + maxAckDelay: 10, + maxDatagramFrameSize: 11, + disableActiveMigration: true, + }, + preferredAddress: { ipv4, ipv6 } + }); + }); +}); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index e6464466aa43e5..53cced49dc4afb 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -66,6 +66,15 @@ const { getSystemErrorName } = require('util'); delete providers.FIXEDSIZEBLOBCOPY; delete providers.RANDOMPRIMEREQUEST; delete providers.CHECKPRIMEREQUEST; + delete providers.QUICSESSION; + delete providers.QUICENDPOINT; + delete providers.QUICSTREAM; + delete providers.QUICPACKET; + delete providers.QUICENDPOINT_UDP; + delete providers.QUICSTREAMSOURCE; + delete providers.QUICSTREAMBASESOURCE; + delete providers.QUICBLOBSOURCE; + delete providers.QUICLOGSTREAM; const objKeys = Object.keys(providers); if (objKeys.length > 0)