mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
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:
parent
ec26b1c01a
commit
d52cd04591
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
48
lib/quic.js
48
lib/quic.js
|
|
@ -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),
|
||||
}));
|
||||
|
|
|
|||
43
test/parallel/test-quic-exports.mjs
Normal file
43
test/parallel/test-quic-exports.mjs
Normal 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);
|
||||
67
test/parallel/test-quic-handshake-ipv6-only.mjs
Normal file
67
test/parallel/test-quic-handshake-ipv6-only.mjs
Normal 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();
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user