quic: continue working on quic api bits

PR-URL: https://github.com/nodejs/node/pull/60123
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
James M Snell 2025-10-07 07:04:53 -07:00 committed by GitHub
parent ec26b1c01a
commit d52cd04591
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 465 additions and 158 deletions

View File

@ -36,19 +36,19 @@ const {
setCallbacks,
// The constants to be exposed to end users for various options.
CC_ALGO_RENO_STR,
CC_ALGO_CUBIC_STR,
CC_ALGO_BBR_STR,
PREFERRED_ADDRESS_IGNORE,
PREFERRED_ADDRESS_USE,
DEFAULT_PREFERRED_ADDRESS_POLICY,
CC_ALGO_RENO_STR: CC_ALGO_RENO,
CC_ALGO_CUBIC_STR: CC_ALGO_CUBIC,
CC_ALGO_BBR_STR: CC_ALGO_BBR,
DEFAULT_CIPHERS,
DEFAULT_GROUPS,
STREAM_DIRECTION_BIDIRECTIONAL,
STREAM_DIRECTION_UNIDIRECTIONAL,
// Internal constants for use by the implementation.
// These are not exposed to end users.
PREFERRED_ADDRESS_IGNORE: kPreferredAddressIgnore,
PREFERRED_ADDRESS_USE: kPreferredAddressUse,
DEFAULT_PREFERRED_ADDRESS_POLICY: kPreferredAddressDefault,
STREAM_DIRECTION_BIDIRECTIONAL: kStreamDirectionBidirectional,
STREAM_DIRECTION_UNIDIRECTIONAL: kStreamDirectionUnidirectional,
CLOSECONTEXT_CLOSE: kCloseContextClose,
CLOSECONTEXT_BIND_FAILURE: kCloseContextBindFailure,
CLOSECONTEXT_LISTEN_FAILURE: kCloseContextListenFailure,
@ -60,6 +60,7 @@ const {
const {
isArrayBuffer,
isArrayBufferView,
isSharedArrayBuffer,
} = require('util/types');
const {
@ -72,6 +73,7 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
ERR_INVALID_THIS,
ERR_MISSING_ARGS,
ERR_QUIC_APPLICATION_ERROR,
ERR_QUIC_CONNECTION_FAILED,
@ -104,6 +106,7 @@ const {
validateFunction,
validateNumber,
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');
@ -135,7 +138,6 @@ const {
kReset,
kSendHeaders,
kSessionTicket,
kState,
kTrailers,
kVersionNegotiation,
kInspect,
@ -544,10 +546,30 @@ setCallbacks({
function validateBody(body) {
// TODO(@jasnell): Support streaming sources
if (body === undefined) return body;
if (isArrayBuffer(body)) return ArrayBufferPrototypeTransfer(body);
// Transfer ArrayBuffers...
if (isArrayBuffer(body)) {
return ArrayBufferPrototypeTransfer(body);
}
// With a SharedArrayBuffer, we always copy. We cannot transfer
// and it's likely unsafe to use the underlying buffer directly.
if (isSharedArrayBuffer(body)) {
return new Uint8Array(body).slice();
}
if (isArrayBufferView(body)) {
const size = body.byteLength;
const offset = body.byteOffset;
// We have to be careful in this case. If the ArrayBufferView is a
// subset of the underlying ArrayBuffer, transferring the entire
// ArrayBuffer could be incorrect if other views are also using it.
// So if offset > 0 or size != buffer.byteLength, we'll copy the
// subset into a new ArrayBuffer instead of transferring.
if (isSharedArrayBuffer(body.buffer) ||
offset !== 0 || size !== body.buffer.byteLength) {
return new Uint8Array(body, offset, size).slice();
}
// It's still possible that the ArrayBuffer is being used elsewhere,
// but we really have no way of knowing. We'll just have to trust
// the caller in this case.
return new Uint8Array(ArrayBufferPrototypeTransfer(body.buffer), offset, size);
}
if (isBlob(body)) return body[kBlobHandle];
@ -559,6 +581,11 @@ function validateBody(body) {
], body);
}
// Functions used specifically for internal testing purposes only.
let getQuicStreamState;
let getQuicSessionState;
let getQuicEndpointState;
class QuicStream {
/** @type {object} */
#handle;
@ -581,8 +608,22 @@ class QuicStream {
/** @type {Promise<void>} */
#pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials
#reader;
/** @type {ReadableStream} */
#readable;
static {
getQuicStreamState = function(stream) {
QuicStream.#assertIsQuicStream(stream);
return stream.#state;
};
}
static #assertIsQuicStream(val) {
if (val == null || !(#handle in val)) {
throw new ERR_INVALID_THIS('QuicStream');
}
}
/**
* @param {symbol} privateSymbol
* @param {object} handle
@ -609,7 +650,12 @@ class QuicStream {
}
}
/**
* Returns a ReadableStream to consume incoming data on the stream.
* @type {ReadableStream<ArrayBufferView>}
*/
get readable() {
QuicStream.#assertIsQuicStream(this);
if (this.#readable === undefined) {
assert(this.#reader);
this.#readable = createBlobReaderStream(this.#reader);
@ -617,13 +663,24 @@ class QuicStream {
return this.#readable;
}
/** @type {boolean} */
get pending() { return this.#state.pending; }
/**
* True if the stream is still pending (i.e. it has not yet been opened
* and assigned an ID).
* @type {boolean}
*/
get pending() {
QuicStream.#assertIsQuicStream(this);
return this.#state.pending;
}
/** @type {OnBlockedCallback} */
get onblocked() { return this.#onblocked; }
get onblocked() {
QuicStream.#assertIsQuicStream(this);
return this.#onblocked;
}
set onblocked(fn) {
QuicStream.#assertIsQuicStream(this);
if (fn === undefined) {
this.#onblocked = undefined;
this.#state.wantsBlock = false;
@ -635,9 +692,13 @@ class QuicStream {
}
/** @type {OnStreamErrorCallback} */
get onreset() { return this.#onreset; }
get onreset() {
QuicStream.#assertIsQuicStream(this);
return this.#onreset;
}
set onreset(fn) {
QuicStream.#assertIsQuicStream(this);
if (fn === undefined) {
this.#onreset = undefined;
this.#state.wantsReset = false;
@ -649,7 +710,9 @@ class QuicStream {
}
/** @type {OnHeadersCallback} */
get [kOnHeaders]() { return this.#onheaders; }
get [kOnHeaders]() {
return this.#onheaders;
}
set [kOnHeaders](fn) {
if (fn === undefined) {
@ -676,44 +739,76 @@ class QuicStream {
}
}
/** @type {QuicStreamStats} */
get stats() { return this.#stats; }
/**
* The statistics collected for this stream.
* @type {QuicStreamStats}
*/
get stats() {
QuicStream.#assertIsQuicStream(this);
return this.#stats;
}
/** @type {QuicStreamState} */
get state() { return this.#state; }
/** @type {QuicSession} */
get session() { return this.#session; }
/**
* The session this stream belongs to. If the stream is destroyed,
* `null` will be returned.
* @type {QuicSession | null}
*/
get session() {
QuicStream.#assertIsQuicStream(this);
if (this.destroyed) return null;
return this.#session;
}
/**
* Returns the id for this stream. If the stream is destroyed or still pending,
* `undefined` will be returned.
* @type {bigint}
* `null` will be returned.
* @type {bigint | null}
*/
get id() {
if (this.destroyed || this.pending) return undefined;
QuicStream.#assertIsQuicStream(this);
if (this.destroyed || this.pending) return null;
return this.#state.id;
}
/** @type {'bidi'|'uni'} */
/**
* Returns the directionality of this stream. If the stream is destroyed
* or still pending, `null` will be returned.
* @type {'bidi'|'uni'|null}
*/
get direction() {
return this.#direction === STREAM_DIRECTION_BIDIRECTIONAL ? 'bidi' : 'uni';
QuicStream.#assertIsQuicStream(this);
if (this.destroyed || this.pending) return null;
return this.#direction === kStreamDirectionBidirectional ? 'bidi' : 'uni';
}
/** @returns {boolean} */
/**
* True if the stream has been destroyed.
* @returns {boolean}
*/
get destroyed() {
QuicStream.#assertIsQuicStream(this);
return this.#handle === undefined;
}
/** @type {Promise<void>} */
/**
* A promise that will be resolved when the stream is closed.
* @type {Promise<void>}
*/
get closed() {
QuicStream.#assertIsQuicStream(this);
return this.#pendingClose.promise;
}
/**
* @param {ArrayBuffer|ArrayBufferView|Blob} outbound
* Sets the outbound data source for the stream. This can only be called
* once and must be called before any data will be sent. The body can be
* an ArrayBuffer, a TypedArray or DataView, or a Blob. If the stream
* is destroyed or already has an outbound data source, an error will
* be thrown.
* @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob} outbound
*/
setOutbound(outbound) {
QuicStream.#assertIsQuicStream(this);
if (this.destroyed) {
throw new ERR_INVALID_STATE('Stream is destroyed');
}
@ -724,44 +819,53 @@ class QuicStream {
}
/**
* @param {bigint} code
* Tells the peer to stop sending data for this stream. The optional error
* code will be sent to the peer as part of the request. If the stream is
* already destroyed, this is a no-op. No acknowledgement of this action
* will be provided.
* @param {number|bigint} code
*/
stopSending(code = 0n) {
if (this.destroyed) {
throw new ERR_INVALID_STATE('Stream is destroyed');
}
QuicStream.#assertIsQuicStream(this);
if (this.destroyed) return;
this.#handle.stopSending(BigInt(code));
}
/**
* @param {bigint} code
* Tells the peer that this end will not send any more data on this stream.
* The optional error code will be sent to the peer as part of the
* request. If the stream is already destroyed, this is a no-op. No
* acknowledgement of this action will be provided.
* @param {number|bigint} code
*/
resetStream(code = 0n) {
if (this.destroyed) {
throw new ERR_INVALID_STATE('Stream is destroyed');
}
QuicStream.#assertIsQuicStream(this);
if (this.destroyed) return;
this.#handle.resetStream(BigInt(code));
}
/** @type {'default' | 'low' | 'high'} */
/**
* The priority of the stream. If the stream is destroyed or if
* the session does not support priority, `null` will be
* returned.
* @type {'default' | 'low' | 'high' | null}
*/
get priority() {
if (this.destroyed || !this.session.state.isPrioritySupported) return undefined;
switch (this.#handle.getPriority()) {
case 3: return 'default';
case 7: return 'low';
case 0: return 'high';
default: return 'default';
}
QuicStream.#assertIsQuicStream(this);
if (this.destroyed || !this.session.state.isPrioritySupported) return null;
const priority = this.#handle.getPriority();
return priority < 3 ? 'high' : priority > 3 ? 'low' : 'default';
}
set priority(val) {
QuicStream.#assertIsQuicStream(this);
if (this.destroyed || !this.session.state.isPrioritySupported) return;
validateOneOf(val, 'priority', ['default', 'low', 'high']);
switch (val) {
case 'default': this.#handle.setPriority(3, 1); break;
case 'low': this.#handle.setPriority(7, 1); break;
case 'high': this.#handle.setPriority(0, 1); break;
}
// Otherwise ignore the value as invalid.
}
/**
@ -868,6 +972,7 @@ class QuicStream {
};
return `Stream ${inspect({
__proto__: null,
id: this.id,
direction: this.direction,
pending: this.pending,
@ -902,6 +1007,19 @@ class QuicSession {
/** @type {{}} */
#sessionticket = undefined;
static {
getQuicSessionState = function(session) {
QuicSession.#assertIsQuicSession(session);
return session.#state;
};
}
static #assertIsQuicSession(val) {
if (val == null || !(#handle in val)) {
throw new ERR_INVALID_THIS('QuicSession');
}
}
/**
* @param {symbol} privateSymbol
* @param {object} handle
@ -930,13 +1048,23 @@ class QuicSession {
return this.#handle === undefined || this.#isPendingClose;
}
/** @type {any} */
get sessionticket() { return this.#sessionticket; }
/**
* Get the session ticket associated with this session, if any.
* @type {any}
*/
get sessionticket() {
QuicSession.#assertIsQuicSession(this);
return this.#sessionticket;
}
/** @type {OnStreamCallback} */
get onstream() { return this.#onstream; }
get onstream() {
QuicSession.#assertIsQuicSession(this);
return this.#onstream;
}
set onstream(fn) {
QuicSession.#assertIsQuicSession(this);
if (fn === undefined) {
this.#onstream = undefined;
} else {
@ -946,9 +1074,13 @@ class QuicSession {
}
/** @type {OnDatagramCallback} */
get ondatagram() { return this.#ondatagram; }
get ondatagram() {
QuicSession.#assertIsQuicSession(this);
return this.#ondatagram;
}
set ondatagram(fn) {
QuicSession.#assertIsQuicSession(this);
if (fn === undefined) {
this.#ondatagram = undefined;
this.#state.hasDatagramListener = false;
@ -959,14 +1091,25 @@ class QuicSession {
}
}
/** @type {QuicSessionStats} */
get stats() { return this.#stats; }
/**
* The statistics collected for this session.
* @type {QuicSessionStats}
*/
get stats() {
QuicSession.#assertIsQuicSession(this);
return this.#stats;
}
/** @type {QuicSessionState} */
get [kState]() { return this.#state; }
/** @type {QuicEndpoint} */
get endpoint() { return this.#endpoint; }
/**
* The endpoint this session belongs to. If the session has been destroyed,
* `null` will be returned.
* @type {QuicEndpoint|null}
*/
get endpoint() {
QuicSession.#assertIsQuicSession(this);
if (this.destroyed) return null;
return this.#endpoint;
}
/**
* @param {number} direction
@ -977,7 +1120,7 @@ class QuicSession {
if (this.#isClosedOrClosing) {
throw new ERR_INVALID_STATE('Session is closed. New streams cannot be opened.');
}
const dir = direction === STREAM_DIRECTION_BIDIRECTIONAL ? 'bidi' : 'uni';
const dir = direction === kStreamDirectionBidirectional ? 'bidi' : 'uni';
if (this.#state.isStreamOpenAllowed) {
debug(`opening new pending ${dir} stream`);
} else {
@ -1025,11 +1168,14 @@ class QuicSession {
}
/**
* Creates a new bidirectional stream on this session. If the session
* does not allow new streams to be opened, an error will be thrown.
* @param {OpenStreamOptions} [options]
* @returns {Promise<QuicStream>}
*/
async createBidirectionalStream(options = kEmptyObject) {
return await this.#createStream(STREAM_DIRECTION_BIDIRECTIONAL, options);
QuicSession.#assertIsQuicSession(this);
return await this.#createStream(kStreamDirectionBidirectional, options);
}
/**
@ -1037,7 +1183,8 @@ class QuicSession {
* @returns {Promise<QuicStream>}
*/
async createUnidirectionalStream(options = kEmptyObject) {
return await this.#createStream(STREAM_DIRECTION_UNIDIRECTIONAL, options);
QuicSession.#assertIsQuicSession(this);
return await this.#createStream(kStreamDirectionUnidirectional, options);
}
/**
@ -1052,6 +1199,7 @@ class QuicSession {
* @returns {Promise<void>}
*/
async sendDatagram(datagram) {
QuicSession.#assertIsQuicSession(this);
if (this.#isClosedOrClosing) {
throw new ERR_INVALID_STATE('Session is closed');
}
@ -1086,6 +1234,7 @@ class QuicSession {
* Initiate a key update.
*/
updateKey() {
QuicSession.#assertIsQuicSession(this);
if (this.#isClosedOrClosing) {
throw new ERR_INVALID_STATE('Session is closed');
}
@ -1111,6 +1260,7 @@ class QuicSession {
* @returns {Promise<void>}
*/
close() {
QuicSession.#assertIsQuicSession(this);
if (!this.#isClosedOrClosing) {
this.#isPendingClose = true;
@ -1127,17 +1277,26 @@ class QuicSession {
}
/** @type {Promise<QuicSessionInfo>} */
get opened() { return this.#pendingOpen.promise; }
get opened() {
QuicSession.#assertIsQuicSession(this);
return this.#pendingOpen.promise;
}
/**
* A promise that is resolved when the session is closed, or is rejected if
* the session is closed abruptly due to an error.
* @type {Promise<void>}
*/
get closed() { return this.#pendingClose.promise; }
get closed() {
QuicSession.#assertIsQuicSession(this);
return this.#pendingClose.promise;
}
/** @type {boolean} */
get destroyed() { return this.#handle === undefined; }
get destroyed() {
QuicSession.#assertIsQuicSession(this);
return this.#handle === undefined;
}
/**
* Forcefully closes the session abruptly without waiting for streams to be
@ -1148,6 +1307,7 @@ class QuicSession {
* @param {any} error
*/
destroy(error) {
QuicSession.#assertIsQuicSession(this);
if (this.destroyed) return;
debug('destroying the session');
@ -1502,6 +1662,19 @@ class QuicEndpoint {
*/
#onsession = undefined;
static {
getQuicEndpointState = function(endpoint) {
QuicEndpoint.#assertIsQuicEndpoint(endpoint);
return endpoint.#state;
};
}
static #assertIsQuicEndpoint(val) {
if (val == null || !(#handle in val)) {
throw new ERR_INVALID_THIS('QuicEndpoint');
}
}
/**
* @param {EndpointOptions} options
* @returns {EndpointOptions}
@ -1592,10 +1765,10 @@ class QuicEndpoint {
* Statistics collected while the endpoint is operational.
* @type {QuicEndpointStats}
*/
get stats() { return this.#stats; }
/** @type {QuicEndpointState} */
get [kState]() { return this.#state; }
get stats() {
QuicEndpoint.#assertIsQuicEndpoint(this);
return this.#stats;
}
get #isClosedOrClosing() {
return this.destroyed || this.#isPendingClose;
@ -1606,12 +1779,16 @@ class QuicEndpoint {
* Existing connections will continue to work.
* @type {boolean}
*/
get busy() { return this.#busy; }
get busy() {
QuicEndpoint.#assertIsQuicEndpoint(this);
return this.#busy;
}
/**
* @type {boolean}
*/
set busy(val) {
QuicEndpoint.#assertIsQuicEndpoint(this);
if (this.#isClosedOrClosing) {
throw new ERR_INVALID_STATE('Endpoint is closed');
}
@ -1635,6 +1812,7 @@ class QuicEndpoint {
* @type {SocketAddress|undefined}
*/
get address() {
QuicEndpoint.#assertIsQuicEndpoint(this);
if (this.#isClosedOrClosing) return undefined;
if (this.#address === undefined) {
const addr = this.#handle.address();
@ -1701,6 +1879,7 @@ class QuicEndpoint {
* @returns {Promise<void>} Returns this.closed
*/
close() {
QuicEndpoint.#assertIsQuicEndpoint(this);
if (!this.#isClosedOrClosing) {
if (onEndpointClosingChannel.hasSubscribers) {
onEndpointClosingChannel.publish({
@ -1723,15 +1902,25 @@ class QuicEndpoint {
* is set to the same promise that is returned by the close() method.
* @type {Promise<void>}
*/
get closed() { return this.#pendingClose.promise; }
get closed() {
QuicEndpoint.#assertIsQuicEndpoint(this);
return this.#pendingClose.promise;
}
/**
* True if the endpoint is pending close.
* @type {boolean}
*/
get closing() { return this.#isPendingClose; }
get closing() {
QuicEndpoint.#assertIsQuicEndpoint(this);
return this.#isPendingClose;
}
/** @type {boolean} */
get destroyed() { return this.#handle === undefined; }
get destroyed() {
QuicEndpoint.#assertIsQuicEndpoint(this);
return this.#handle === undefined;
}
/**
* Forcefully terminates the endpoint by immediately destroying all sessions
@ -1742,6 +1931,7 @@ class QuicEndpoint {
* @returns {Promise<void>} Returns this.closed
*/
destroy(error) {
QuicEndpoint.#assertIsQuicEndpoint(this);
debug('destroying the endpoint');
if (!this.#isClosedOrClosing) {
this.#pendingError = error;
@ -1858,8 +2048,6 @@ class QuicEndpoint {
this.#sessions.delete(session);
}
async [SymbolAsyncDispose]() { await this.close(); }
[kInspect](depth, options) {
if (depth < 0)
return this;
@ -1881,17 +2069,9 @@ class QuicEndpoint {
state: this.#state,
}, opts)}`;
}
};
function readOnlyConstant(value) {
return {
__proto__: null,
value,
writable: false,
configurable: false,
enumerable: true,
async [SymbolAsyncDispose]() { await this.close(); }
};
}
/**
* @param {EndpointOptions} endpoint
@ -1899,21 +2079,13 @@ function readOnlyConstant(value) {
*/
function processEndpointOption(endpoint) {
if (endpoint === undefined) {
return {
endpoint: new QuicEndpoint(),
created: true,
};
// No endpoint or endpoint options were given. Create a default.
return new QuicEndpoint();
} else if (endpoint instanceof QuicEndpoint) {
return {
endpoint,
created: false,
};
// We were given an existing endpoint. Use it as-is.
return endpoint;
}
validateObject(endpoint, 'options.endpoint');
return {
endpoint: new QuicEndpoint(endpoint),
created: true,
};
return new QuicEndpoint(endpoint);
}
/**
@ -2037,19 +2209,19 @@ function processTlsOptions(tls, forServer) {
*/
function getPreferredAddressPolicy(policy = 'default') {
switch (policy) {
case 'use': return PREFERRED_ADDRESS_USE;
case 'ignore': return PREFERRED_ADDRESS_IGNORE;
case 'default': return DEFAULT_PREFERRED_ADDRESS_POLICY;
case 'use': return kPreferredAddressUse;
case 'ignore': return kPreferredAddressIgnore;
case 'default': return kPreferredAddressDefault;
}
throw new ERR_INVALID_ARG_VALUE('options.preferredAddressPolicy', policy);
}
/**
* @param {SessionOptions} options
* @param {boolean} [forServer]
* @param {{forServer: boolean, addressFamily: string}} [config]
* @returns {SessionOptions}
*/
function processSessionOptions(options, forServer = false) {
function processSessionOptions(options, config = {}) {
validateObject(options, 'options');
const {
endpoint,
@ -2068,26 +2240,23 @@ function processSessionOptions(options, forServer = false) {
[kApplicationProvider]: provider,
} = options;
const {
forServer = false,
} = config;
if (provider !== undefined) {
validateObject(provider, 'options[kApplicationProvider]');
}
if (cc !== undefined) {
validateString(cc, 'options.cc');
if (cc !== 'reno' || cc !== 'bbr' || cc !== 'cubic') {
throw new ERR_INVALID_ARG_VALUE('options.cc', cc);
}
validateOneOf(cc, 'options.cc', [CC_ALGO_RENO, CC_ALGO_BBR, CC_ALGO_CUBIC]);
}
const {
endpoint: actualEndpoint,
created: endpointCreated,
} = processEndpointOption(endpoint);
const actualEndpoint = processEndpointOption(endpoint);
return {
__proto__: null,
endpoint: actualEndpoint,
endpointCreated,
version,
minVersion,
preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy),
@ -2117,7 +2286,7 @@ async function listen(callback, options = kEmptyObject) {
const {
endpoint,
...sessionOptions
} = processSessionOptions(options, true /* for server */);
} = processSessionOptions(options, { forServer: true });
endpoint[kListen](callback, sessionOptions);
if (onEndpointListeningChannel.hasSubscribers) {
@ -2203,15 +2372,15 @@ module.exports = {
QuicSession,
QuicStream,
Http3,
CC_ALGO_RENO,
CC_ALGO_CUBIC,
CC_ALGO_BBR,
DEFAULT_CIPHERS,
DEFAULT_GROUPS,
// These are exported only for internal testing purposes.
getQuicStreamState,
getQuicSessionState,
getQuicEndpointState,
};
ObjectDefineProperties(module.exports, {
CC_ALGO_RENO: readOnlyConstant(CC_ALGO_RENO_STR),
CC_ALGO_CUBIC: readOnlyConstant(CC_ALGO_CUBIC_STR),
CC_ALGO_BBR: readOnlyConstant(CC_ALGO_BBR_STR),
DEFAULT_CIPHERS: readOnlyConstant(DEFAULT_CIPHERS),
DEFAULT_GROUPS: readOnlyConstant(DEFAULT_GROUPS),
});
/* c8 ignore stop */

View File

@ -45,7 +45,6 @@ const kRemoveStream = Symbol('kRemoveStream');
const kReset = Symbol('kReset');
const kSendHeaders = Symbol('kSendHeaders');
const kSessionTicket = Symbol('kSessionTicket');
const kState = Symbol('kState');
const kTrailers = Symbol('kTrailers');
const kVersionNegotiation = Symbol('kVersionNegotiation');
const kWantsHeaders = Symbol('kWantsHeaders');
@ -76,7 +75,6 @@ module.exports = {
kReset,
kSendHeaders,
kSessionTicket,
kState,
kTrailers,
kVersionNegotiation,
kWantsHeaders,

View File

@ -1,5 +1,10 @@
'use strict';
const {
ObjectCreate,
ObjectSeal,
} = primordials;
const {
emitExperimentalWarning,
} = require('internal/util');
@ -18,15 +23,36 @@ const {
DEFAULT_GROUPS,
} = require('internal/quic/quic');
module.exports = {
connect,
listen,
QuicEndpoint,
QuicSession,
QuicStream,
CC_ALGO_RENO,
CC_ALGO_CUBIC,
CC_ALGO_BBR,
DEFAULT_CIPHERS,
DEFAULT_GROUPS,
function getEnumerableConstant(value) {
return {
__proto__: null,
value,
enumerable: true,
configurable: false,
writable: false,
};
}
const cc = ObjectSeal(ObjectCreate(null, {
__proto__: null,
RENO: getEnumerableConstant(CC_ALGO_RENO),
CUBIC: getEnumerableConstant(CC_ALGO_CUBIC),
BBR: getEnumerableConstant(CC_ALGO_BBR),
}));
const constants = ObjectSeal(ObjectCreate(null, {
__proto__: null,
cc: getEnumerableConstant(cc),
DEFAULT_CIPHERS: getEnumerableConstant(DEFAULT_CIPHERS),
DEFAULT_GROUPS: getEnumerableConstant(DEFAULT_GROUPS),
}));
module.exports = ObjectSeal(ObjectCreate(null, {
__proto__: null,
connect: getEnumerableConstant(connect),
listen: getEnumerableConstant(listen),
QuicEndpoint: getEnumerableConstant(QuicEndpoint),
QuicSession: getEnumerableConstant(QuicSession),
QuicStream: getEnumerableConstant(QuicStream),
constants: getEnumerableConstant(constants),
}));

View File

@ -0,0 +1,43 @@
// Flags: --experimental-quic --no-warnings
import { hasQuic, skip } from '../common/index.mjs';
import { strictEqual, throws } from 'node:assert';
if (!hasQuic) {
skip('QUIC is not enabled');
}
const quic = await import('node:quic');
// Test that the main exports exist and are of the correct type.
strictEqual(typeof quic.connect, 'function');
strictEqual(typeof quic.listen, 'function');
strictEqual(typeof quic.QuicEndpoint, 'function');
strictEqual(typeof quic.QuicSession, 'function');
strictEqual(typeof quic.QuicStream, 'function');
strictEqual(typeof quic.QuicEndpoint.Stats, 'function');
strictEqual(typeof quic.QuicSession.Stats, 'function');
strictEqual(typeof quic.QuicStream.Stats, 'function');
strictEqual(typeof quic.constants, 'object');
strictEqual(typeof quic.constants.cc, 'object');
// Test that the constants exist and are of the correct type.
strictEqual(quic.constants.cc.RENO, 'reno');
strictEqual(quic.constants.cc.CUBIC, 'cubic');
strictEqual(quic.constants.cc.BBR, 'bbr');
strictEqual(quic.constants.DEFAULT_CIPHERS,
'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:' +
'TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256');
strictEqual(quic.constants.DEFAULT_GROUPS, 'X25519:P-256:P-384:P-521');
// Ensure the constants are.. well, constant.
throws(() => { quic.constants.cc.RENO = 'foo'; }, TypeError);
strictEqual(quic.constants.cc.RENO, 'reno');
throws(() => { quic.constants.cc.NEW_CONSTANT = 'bar'; }, TypeError);
strictEqual(quic.constants.cc.NEW_CONSTANT, undefined);
throws(() => { quic.constants.DEFAULT_CIPHERS = 123; }, TypeError);
strictEqual(typeof quic.constants.DEFAULT_CIPHERS, 'string');
throws(() => { quic.constants.NEW_CONSTANT = 456; }, TypeError);
strictEqual(quic.constants.NEW_CONSTANT, undefined);

View File

@ -0,0 +1,67 @@
// Flags: --experimental-quic --no-warnings
import { hasQuic, hasIPv6, skip } from '../common/index.mjs';
import { ok, partialDeepStrictEqual } from 'node:assert';
import { readKey } from '../common/fixtures.mjs';
if (!hasQuic) {
skip('QUIC is not enabled');
}
if (!hasIPv6) {
skip('IPv6 is not supported');
}
// Import after the hasQuic check
const { listen, connect } = await import('node:quic');
const { createPrivateKey } = await import('node:crypto');
const keys = createPrivateKey(readKey('agent1-key.pem'));
const certs = readKey('agent1-cert.pem');
const check = {
// The SNI value
servername: 'localhost',
// The selected ALPN protocol
protocol: 'h3',
// The negotiated cipher suite
cipher: 'TLS_AES_128_GCM_SHA256',
cipherVersion: 'TLSv1.3',
};
// The opened promise should resolve when the handshake is complete.
const serverOpened = Promise.withResolvers();
const clientOpened = Promise.withResolvers();
const serverEndpoint = await listen(async (serverSession) => {
const info = await serverSession.opened;
partialDeepStrictEqual(info, check);
serverOpened.resolve();
serverSession.close();
}, { keys, certs, endpoint: {
address: {
address: '::1',
family: 'ipv6',
},
ipv6Only: true,
} });
// The server must have an address to connect to after listen resolves.
ok(serverEndpoint.address !== undefined);
const clientSession = await connect(serverEndpoint.address, {
endpoint: {
address: {
address: '::',
family: 'ipv6',
},
}
});
clientSession.opened.then((info) => {
partialDeepStrictEqual(info, check);
clientOpened.resolve();
});
await Promise.all([serverOpened.promise, clientOpened.promise]);
clientSession.close();

View File

@ -17,16 +17,16 @@ if (!hasQuic) {
// Import after the hasQuic check
const { listen, QuicEndpoint } = await import('node:quic');
const { createPrivateKey } = await import('node:crypto');
const { kState } = (await import('internal/quic/symbols')).default;
const { getQuicEndpointState } = (await import('internal/quic/quic')).default;
const keys = createPrivateKey(readKey('agent1-key.pem'));
const certs = readKey('agent1-cert.pem');
const endpoint = new QuicEndpoint();
ok(!endpoint[kState].isBound);
ok(!endpoint[kState].isReceiving);
ok(!endpoint[kState].isListening);
const state = getQuicEndpointState(endpoint);
ok(!state.isBound);
ok(!state.isReceiving);
ok(!state.isListening);
strictEqual(endpoint.address, undefined);
@ -42,10 +42,9 @@ await listen(() => {}, { keys, certs, endpoint });
await rejects(listen(() => {}, { keys, certs, endpoint }), {
code: 'ERR_INVALID_STATE',
});
ok(endpoint[kState].isBound);
ok(endpoint[kState].isReceiving);
ok(endpoint[kState].isListening);
ok(state.isBound);
ok(state.isReceiving);
ok(state.isListening);
const address = endpoint.address;
ok(address instanceof SocketAddress);

View File

@ -23,20 +23,23 @@ const {
const {
kFinishClose,
kPrivateConstructor,
kState,
} = (await import('internal/quic/symbols')).default;
const {
getQuicEndpointState,
} = (await import('internal/quic/quic')).default;
{
const endpoint = new QuicEndpoint();
const state = getQuicEndpointState(endpoint);
strictEqual(endpoint[kState].isBound, false);
strictEqual(endpoint[kState].isReceiving, false);
strictEqual(endpoint[kState].isListening, false);
strictEqual(endpoint[kState].isClosing, false);
strictEqual(endpoint[kState].isBusy, false);
strictEqual(endpoint[kState].pendingCallbacks, 0n);
strictEqual(state.isBound, false);
strictEqual(state.isReceiving, false);
strictEqual(state.isListening, false);
strictEqual(state.isClosing, false);
strictEqual(state.isBusy, false);
strictEqual(state.pendingCallbacks, 0n);
deepStrictEqual(JSON.parse(JSON.stringify(endpoint[kState])), {
deepStrictEqual(JSON.parse(JSON.stringify(state)), {
isBound: false,
isReceiving: false,
isListening: false,
@ -46,23 +49,25 @@ const {
});
endpoint.busy = true;
strictEqual(endpoint[kState].isBusy, true);
strictEqual(state.isBusy, true);
endpoint.busy = false;
strictEqual(endpoint[kState].isBusy, false);
strictEqual(typeof inspect(endpoint[kState]), 'string');
strictEqual(state.isBusy, false);
strictEqual(typeof inspect(state), 'string');
}
{
// It is not bound after close.
const endpoint = new QuicEndpoint();
endpoint[kState][kFinishClose]();
strictEqual(endpoint[kState].isBound, undefined);
const state = getQuicEndpointState(endpoint);
state[kFinishClose]();
strictEqual(state.isBound, undefined);
}
{
// State constructor argument is ArrayBuffer
const endpoint = new QuicEndpoint();
const StateCons = endpoint[kState].constructor;
const state = getQuicEndpointState(endpoint);
const StateCons = state.constructor;
throws(() => new StateCons(kPrivateConstructor, 1), {
code: 'ERR_INVALID_ARG_TYPE'
});