diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index 7e874ff34d58de..446bec3761374b 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -147,6 +147,7 @@ const { IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT, IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT, IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT, + IDX_QUIC_SESSION_STATS_HANDSHAKE_COMPLETED_AT, IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT, IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT, IDX_QUIC_SESSION_STATS_BLOCK_COUNT, @@ -208,13 +209,17 @@ const { const emit = EventEmitter.prototype.emit; +const kAfterLookup = Symbol('kAfterLookup'); +const kAfterPreferredAddressLookup = Symbol('kAfterPreferredAddressLookup'); const kAddSession = Symbol('kAddSession'); const kAddStream = Symbol('kAddStream'); const kClose = Symbol('kClose'); const kCert = Symbol('kCert'); const kClientHello = Symbol('kClientHello'); -const kContinueBind = Symbol('kContinueBind'); const kContinueConnect = Symbol('kContinueConnect'); +const kCompleteListen = Symbol('kCompleteListen'); +const kContinueListen = Symbol('kContinueListen'); +const kContinueBind = Symbol('kContinueBind'); const kDestroy = Symbol('kDestroy'); const kEndpointBound = Symbol('kEndpointBound'); const kEndpointClose = Symbol('kEndpointClose'); @@ -222,19 +227,30 @@ const kGetStreamOptions = Symbol('kGetStreamOptions'); const kHandshake = Symbol('kHandshake'); const kHandshakePost = Symbol('kHandshakePost'); const kHeaders = Symbol('kHeaders'); +const kInternalState = Symbol('kInternalState'); +const kInternalClientState = Symbol('kInternalClientState'); +const kInternalServerState = Symbol('kInternalServerState'); +const kMakeStream = Symbol('kMakeStream'); const kMaybeBind = Symbol('kMaybeBind'); +const kMaybeReady = Symbol('kMaybeReady'); +const kOnFileOpened = Symbol('kOnFileOpened'); +const kOnFileUnpipe = Symbol('kOnFileUnpipe'); +const kOnPipedFileHandleRead = Symbol('kOnPipedFileHandleRead'); const kSocketReady = Symbol('kSocketReady'); const kRemoveSession = Symbol('kRemove'); const kRemoveStream = Symbol('kRemoveStream'); const kServerBusy = Symbol('kServerBusy'); const kSetHandle = Symbol('kSetHandle'); const kSetSocket = Symbol('kSetSocket'); +const kSetSocketAfterBind = Symbol('kSetSocketAfterBind'); +const kStartFilePipe = Symbol('kStartFilePipe'); const kStreamClose = Symbol('kStreamClose'); const kStreamReset = Symbol('kStreamReset'); const kTrackWriteState = Symbol('kTrackWriteState'); const kUDPHandleForTesting = Symbol('kUDPHandleForTesting'); const kUsePreferredAddress = Symbol('kUsePreferredAddress'); const kVersionNegotiation = Symbol('kVersionNegotiation'); +const kWriteGeneric = Symbol('kWriteGeneric'); const kSocketUnbound = 0; const kSocketPending = 1; @@ -247,19 +263,11 @@ let warnedVerifyHostnameIdentity = false; assert(process.versions.ngtcp2 !== undefined); -// Called by the C++ internals when the socket is closed. -// When this is called, the only thing left to do is destroy -// the QuicSocket instance. -function onSocketClose() { - this[owner_symbol].destroy(); -} - -// Called by the C++ internals when an error occurs on the socket. -// When this is called, the only thing left to do is destroy -// the QuicSocket instance with an error. -// TODO(@jasnell): Should consolidate this with onSocketClose -function onSocketError(err) { - this[owner_symbol].destroy(errnoException(err)); +// Called by the C++ internals when the QuicSocket is closed with +// or without an error. The only thing left to do is destroy the +// QuicSocket instance. +function onSocketClose(err) { + this[owner_symbol].destroy(err != null ? errnoException(err) : undefined); } // Called by the C++ internals when the server busy state of @@ -279,23 +287,24 @@ function onSessionReady(handle) { process.nextTick(emit.bind(socket, 'session', session)); } -// During an immediate close, all currently open QuicStreams are -// abruptly closed. If they are still writable or readable, an abort -// event will be emitted, and RESET_STREAM and STOP_SENDING frames -// will be transmitted as necessary. Once streams have been -// shutdown, a CONNECTION_CLOSE frame will be sent and the -// session will enter the closing period, after which it will -// be destroyed either when the idle timeout expires, the -// QuicSession is silently closed, or destroy is called. -function onSessionClose(code, family) { +// Called when the session needs to be closed and destroyed. +// If silent is true, then the session is going to be closed +// immediately without sending any CONNECTION_CLOSE to the +// connected peer. If silent is false, a CONNECTION_CLOSE +// is going to be sent to the peer. +function onSessionClose(code, family, silent, statelessReset) { if (this[owner_symbol]) { - this[owner_symbol][kClose](family, code); - } else { - // When there's no owner_symbol, the session was closed - // before it could be fully set up. Just immediately - // close everything down on the native side. - this.destroy(code, family); + if (silent) { + this[owner_symbol][kDestroy](statelessReset, family, code); + } else { + this[owner_symbol][kClose](family, code); + } + return; } + // When there's no owner_symbol, the session was closed + // before it could be fully set up. Just immediately + // close everything down on the native side. + this.destroy(code, family); } // Called by the C++ internals when a QuicSession has been destroyed. @@ -481,14 +490,6 @@ function onSessionQlog(str) { } } -// Called when an error occurs in a QuicSession. When this happens, -// the only remedy is to destroy the session. -function onSessionError(error) { - if (this[owner_symbol]) { - this[owner_symbol].destroy(error); - } -} - // Called by the C++ internals when a client QuicSession receives // a version negotiation response from the server. function onSessionVersionNegotiation( @@ -562,14 +563,6 @@ function onStreamHeaders(id, headers, kind, push_id) { this[owner_symbol][kHeaders](id, headers, kind, push_id); } -// During a silent close, all currently open QuicStreams are abruptly -// closed. If they are still writable or readable, an abort event will be -// emitted, otherwise the stream is just destroyed. No RESET_STREAM or -// STOP_SENDING is transmitted to the peer. -function onSessionSilentClose(statelessReset, code, family) { - this[owner_symbol][kDestroy](statelessReset, family, code); -} - // When a stream is flow control blocked, causes a blocked event // to be emitted. This is a purely informational event. function onStreamBlocked() { @@ -579,18 +572,15 @@ function onStreamBlocked() { // Register the callbacks with the QUIC internal binding. setCallbacks({ onSocketClose, - onSocketError, onSocketServerBusy, onSessionReady, onSessionCert, onSessionClientHello, onSessionClose, onSessionDestroyed, - onSessionError, onSessionHandshake, onSessionKeylog, onSessionQlog, - onSessionSilentClose, onSessionStatus, onSessionTicket, onSessionVersionNegotiation, @@ -636,20 +626,29 @@ function onRemoveListener(event) { toggleListeners(this[kHandle], event, false); } +function getStats(obj, idx) { + const stats = obj[kHandle]?.stats || obj[kInternalState].stats; + // If stats is undefined at this point, it's just a bug + assert(stats); + return stats[idx]; +} + // QuicEndpoint wraps a UDP socket and is owned // by a QuicSocket. It does not exist independently // of the QuicSocket. class QuicEndpoint { - #state = kSocketUnbound; - #socket = undefined; - #udpSocket = undefined; - #address = undefined; - #ipv6Only = undefined; - #lookup = undefined; - #port = undefined; - #reuseAddr = undefined; - #type = undefined; - #fd = undefined; + [kInternalState] = { + state: kSocketUnbound, + socket: undefined, + udpSocket: undefined, + address: undefined, + ipv6Only: undefined, + lookup: undefined, + port: undefined, + reuseAddr: undefined, + type: undefined, + fd: undefined + }; constructor(socket, options) { const { @@ -661,35 +660,37 @@ class QuicEndpoint { type, preferred, } = validateQuicEndpointOptions(options); - this.#socket = socket; - this.#address = address || (type === AF_INET6 ? '::' : '0.0.0.0'); - this.#ipv6Only = !!ipv6Only; - this.#lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4); - this.#port = port; - this.#reuseAddr = !!reuseAddr; - this.#type = type; - this.#udpSocket = dgram.createSocket(type === AF_INET6 ? 'udp6' : 'udp4'); + const state = this[kInternalState]; + state.socket = socket; + state.address = address || (type === AF_INET6 ? '::' : '0.0.0.0'); + state.ipv6Only = !!ipv6Only; + state.lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4); + state.port = port; + state.reuseAddr = !!reuseAddr; + state.type = type; + state.udpSocket = dgram.createSocket(type === AF_INET6 ? 'udp6' : 'udp4'); // kUDPHandleForTesting is only used in the Node.js test suite to // artificially test the endpoint. This code path should never be // used in user code. if (typeof options[kUDPHandleForTesting] === 'object') { - this.#udpSocket.bind(options[kUDPHandleForTesting]); - this.#state = kSocketBound; - this.#socket[kEndpointBound](this); + state.udpSocket.bind(options[kUDPHandleForTesting]); + state.state = kSocketBound; + state.socket[kEndpointBound](this); } - const udpHandle = this.#udpSocket[internalDgram.kStateSymbol].handle; + const udpHandle = state.udpSocket[internalDgram.kStateSymbol].handle; const handle = new QuicEndpointHandle(socket[kHandle], udpHandle); handle[owner_symbol] = this; this[kHandle] = handle; - socket[kHandle].addEndpoint(handle, !!preferred); + socket[kHandle].addEndpoint(handle, preferred); } [kInspect]() { + // TODO(@jasnell): Proper custom inspect implementation const obj = { address: this.address, - fd: this.#fd, - type: this.#type === AF_INET6 ? 'udp6' : 'udp4' + fd: this[kInternalState].fd, + type: this[kInternalState].type === AF_INET6 ? 'udp6' : 'udp4' }; return `QuicEndpoint ${util.format(obj)}`; } @@ -699,29 +700,31 @@ class QuicEndpoint { // address. Once resolution is complete, the ip address needs to // be passed on to the [kContinueBind] function or the QuicEndpoint // needs to be destroyed. - static #afterLookup = function(err, ip) { + static [kAfterLookup](err, ip) { if (err) { this.destroy(err); return; } this[kContinueBind](ip); - }; + } // kMaybeBind binds the endpoint on-demand if it is not already // bound. If it is bound, we return immediately, otherwise put // the endpoint into the pending state and initiate the binding // process by calling the lookup to resolve the IP address. [kMaybeBind]() { - if (this.#state !== kSocketUnbound) + const state = this[kInternalState]; + if (state.state !== kSocketUnbound) return; - this.#state = kSocketPending; - this.#lookup(this.#address, QuicEndpoint.#afterLookup.bind(this)); + state.state = kSocketPending; + state.lookup(state.address, QuicEndpoint[kAfterLookup].bind(this)); } // IP address resolution is completed and we're ready to finish // binding to the local port. [kContinueBind](ip) { - const udpHandle = this.#udpSocket[internalDgram.kStateSymbol].handle; + const state = this[kInternalState]; + const udpHandle = state.udpSocket[internalDgram.kStateSymbol].handle; if (udpHandle == null) { // TODO(@jasnell): We may need to throw an error here. Under // what conditions does this happen? @@ -729,22 +732,22 @@ class QuicEndpoint { } const flags = - (this.#reuseAddr ? UV_UDP_REUSEADDR : 0) | - (this.#type === AF_INET6 && this.#ipv6Only ? UV_UDP_IPV6ONLY : 0); + (state.reuseAddr ? UV_UDP_REUSEADDR : 0) | + (state.type === AF_INET6 && state.ipv6Only ? UV_UDP_IPV6ONLY : 0); - const ret = udpHandle.bind(ip, this.#port, flags); + const ret = udpHandle.bind(ip, state.port, flags); if (ret) { - this.destroy(exceptionWithHostPort(ret, 'bind', ip, this.#port || 0)); + this.destroy(exceptionWithHostPort(ret, 'bind', ip, state.port || 0)); return; } // On Windows, the fd will be meaningless, but we always record it. - this.#fd = udpHandle.fd; - this.#state = kSocketBound; + state.fd = udpHandle.fd; + state.state = kSocketBound; // Notify the owning socket that the QuicEndpoint has been successfully // bound to the local UDP port. - this.#socket[kEndpointBound](this); + state.socket[kEndpointBound](this); } [kDestroy](error) { @@ -753,9 +756,10 @@ class QuicEndpoint { this[kHandle] = undefined; handle[owner_symbol] = undefined; handle.ondone = () => { - this.#udpSocket.close((err) => { + const state = this[kInternalState]; + state.udpSocket.close((err) => { if (err) error = err; - this.#socket[kEndpointClose](this, error); + state.socket[kEndpointClose](this, error); }); }; handle.waitForPendingCallbacks(); @@ -766,9 +770,10 @@ class QuicEndpoint { // the local IP address, port, and address type to which it // is bound. Otherwise, returns an empty object. get address() { - if (this.#state !== kSocketDestroyed) { + const state = this[kInternalState]; + if (state.state !== kSocketDestroyed) { try { - return this.#udpSocket.address(); + return state.udpSocket.address(); } catch (err) { if (err.code === 'EBADF') { // If there is an EBADF error, the socket is not bound. @@ -783,97 +788,102 @@ class QuicEndpoint { } get fd() { - return this.#fd; + return this[kInternalState].fd; } // True if the QuicEndpoint has been destroyed and is // no longer usable. get destroyed() { - return this.#state === kSocketDestroyed; + return this[kInternalState].state === kSocketDestroyed; } // True if binding has been initiated and is in progress. get pending() { - return this.#state === kSocketPending; + return this[kInternalState].state === kSocketPending; } - // True if the QuicEndpoint has been bound to the local - // UDP port. + // True if the QuicEndpoint has been bound to the localUDP port. get bound() { - return this.#state === kSocketBound; + return this[kInternalState].state === kSocketBound; } setTTL(ttl) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setTTL'); - this.#udpSocket.setTTL(ttl); + state.udpSocket.setTTL(ttl); return this; } setMulticastTTL(ttl) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setMulticastTTL'); - this.#udpSocket.setMulticastTTL(ttl); + state.udpSocket.setMulticastTTL(ttl); return this; } setBroadcast(on = true) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setBroadcast'); - this.#udpSocket.setBroadcast(on); + state.udpSocket.setBroadcast(on); return this; } setMulticastLoopback(on = true) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setMulticastLoopback'); - this.#udpSocket.setMulticastLoopback(on); + state.udpSocket.setMulticastLoopback(on); return this; } setMulticastInterface(iface) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setMulticastInterface'); - this.#udpSocket.setMulticastInterface(iface); + state.udpSocket.setMulticastInterface(iface); return this; } addMembership(address, iface) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('addMembership'); - this.#udpSocket.addMembership(address, iface); + state.udpSocket.addMembership(address, iface); return this; } dropMembership(address, iface) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('dropMembership'); - this.#udpSocket.dropMembership(address, iface); + state.udpSocket.dropMembership(address, iface); return this; } ref() { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('ref'); - this.#udpSocket.ref(); + state.udpSocket.ref(); return this; } unref() { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('unref'); - this.#udpSocket.unref(); + state.udpSocket.unref(); return this; } destroy(error) { - // If the QuicEndpoint is already destroyed, do nothing - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) return; - - // Mark the QuicEndpoint as being destroyed. - this.#state = kSocketDestroyed; - + state.state = kSocketDestroyed; this[kDestroy](error); } } @@ -882,20 +892,22 @@ class QuicEndpoint { // Protocol state. There may be *multiple* QUIC connections (QuicSession) // associated with a single QuicSocket. class QuicSocket extends EventEmitter { - #alpn = undefined; - #autoClose = undefined; - #client = undefined; - #defaultEncoding = undefined; - #endpoints = new Set(); - #highWaterMark = undefined; - #lookup = undefined; - #server = undefined; - #serverBusy = false; - #serverListening = false; - #serverSecureContext = undefined; - #sessions = new Set(); - #state = kSocketUnbound; - #stats = undefined; + [kInternalState] = { + alpn: undefined, + autoClose: undefined, + client: undefined, + defaultEncoding: undefined, + endpoints: new Set(), + highWaterMark: undefined, + lookup: undefined, + server: undefined, + serverBusy: undefined, + serverListening: false, + serverSecureContext: undefined, + sessions: new Set(), + state: kSocketUnbound, + stats: undefined, + }; constructor(options) { const { @@ -947,10 +959,12 @@ class QuicSocket extends EventEmitter { } = validateQuicSocketOptions(options); super({ captureRejections: true }); - this.#autoClose = autoClose; - this.#client = client; - this.#lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4); - this.#server = server; + const state = this[kInternalState]; + + state.autoClose = autoClose; + state.client = client; + state.lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4); + state.server = server; const socketOptions = (validateAddress ? QUICSOCKET_OPTIONS_VALIDATE_ADDRESS : 0) | @@ -968,7 +982,7 @@ class QuicSocket extends EventEmitter { disableStatelessReset)); this.addEndpoint({ - lookup: this.#lookup, + lookup: state.lookup, // Keep the lookup and ...endpoint in this order // to allow the passed in endpoint options to // override the lookup specifically for that endpoint @@ -981,9 +995,10 @@ class QuicSocket extends EventEmitter { // streams. These are passed on to new client and server // QuicSession instances when they are created. [kGetStreamOptions]() { + const state = this[kInternalState]; return { - highWaterMark: this.#highWaterMark, - defaultEncoding: this.#defaultEncoding, + highWaterMark: state.highWaterMark, + defaultEncoding: state.defaultEncoding, }; } @@ -996,19 +1011,21 @@ class QuicSocket extends EventEmitter { } [kInspect]() { + // TODO(@jasnell): Proper custom inspect implementation + const state = this[kInternalState]; const obj = { endpoints: this.endpoints, - sessions: this.#sessions, + sessions: state.sessions, }; return `QuicSocket ${util.format(obj)}`; } [kAddSession](session) { - this.#sessions.add(session); + this[kInternalState].sessions.add(session); } [kRemoveSession](session) { - this.#sessions.delete(session); + this[kInternalState].sessions.delete(session); this[kMaybeDestroy](); } @@ -1016,29 +1033,31 @@ class QuicSocket extends EventEmitter { // Function is a non-op if the socket is already bound or in the process of // being bound, and will call the callback once the socket is ready. [kMaybeBind](callback = () => {}) { - if (this.#state === kSocketBound) + const state = this[kInternalState]; + if (state.state === kSocketBound) return process.nextTick(callback); this.once('ready', callback); - if (this.#state === kSocketPending) + if (state.state === kSocketPending) return; - this.#state = kSocketPending; + state.state = kSocketPending; - for (const endpoint of this.#endpoints) + for (const endpoint of state.endpoints) endpoint[kMaybeBind](); } [kEndpointBound](endpoint) { - if (this.#state === kSocketBound) + const state = this[kInternalState]; + if (state.state === kSocketBound) return; - this.#state = kSocketBound; + state.state = kSocketBound; // Once the QuicSocket has been bound, we notify all currently // existing QuicSessions. QuicSessions created after this // point will automatically be notified that the QuicSocket // is ready. - for (const session of this.#sessions) + for (const session of state.sessions) session[kSocketReady](); // The ready event indicates that the QuicSocket is ready to be @@ -1050,14 +1069,15 @@ class QuicSocket extends EventEmitter { // Called when a QuicEndpoint closes [kEndpointClose](endpoint, error) { - this.#endpoints.delete(endpoint); + const state = this[kInternalState]; + state.endpoints.delete(endpoint); process.nextTick(emit.bind(this, 'endpointClose', endpoint, error)); // If there are no more QuicEndpoints, the QuicSocket is no // longer usable. - if (this.#endpoints.size === 0) { + if (state.endpoints.size === 0) { // Ensure that there are absolutely no additional sessions - for (const session of this.#sessions) + for (const session of state.sessions) session.destroy(error); if (error) process.nextTick(emit.bind(this, 'error', error)); @@ -1070,7 +1090,7 @@ class QuicSocket extends EventEmitter { [kDestroy](error) { // The QuicSocket will be destroyed once all QuicEndpoints // are destroyed. See [kEndpointClose]. - for (const endpoint of this.#endpoints) + for (const endpoint of this[kInternalState].endpoints) endpoint.destroy(error); } @@ -1078,7 +1098,7 @@ class QuicSocket extends EventEmitter { // is called. The QuicSocket will be destroyed if there are no remaining // open sessions. [kMaybeDestroy]() { - if (this.closing && this.#sessions.size === 0) { + if (this.closing && this[kInternalState].sessions.size === 0) { this.destroy(); return true; } @@ -1087,33 +1107,34 @@ class QuicSocket extends EventEmitter { // Called by the C++ internals to notify when server busy status is toggled. [kServerBusy](on) { - this.#serverBusy = on; + this[kInternalState].serverBusy = on; process.nextTick(emit.bind(this, 'busy', on)); } addEndpoint(options = {}) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('listen'); // TODO(@jasnell): Also forbid adding an endpoint if // the QuicSocket is closing. const endpoint = new QuicEndpoint(this, options); - this.#endpoints.add(endpoint); + state.endpoints.add(endpoint); // If the QuicSocket is already bound at this point, // also bind the newly created QuicEndpoint. - if (this.#state !== kSocketUnbound) + if (state.state !== kSocketUnbound) endpoint[kMaybeBind](); return endpoint; } - // Used only from within the #continueListen function. When a preferred + // Used only from within the [kContinueListen] function. When a preferred // address has been provided, the hostname given must be resolved into an // IP address, which must be passed on to #completeListen or the QuicSocket // needs to be destroyed. - static #afterPreferredAddressLookup = function( + static [kAfterPreferredAddressLookup]( transportParams, port, type, @@ -1123,13 +1144,13 @@ class QuicSocket extends EventEmitter { this.destroy(err); return; } - this.#completeListen(transportParams, { address, port, type }); - }; + this[kCompleteListen](transportParams, { address, port, type }); + } // The #completeListen function is called after all of the necessary // DNS lookups have been performed and we're ready to let the C++ // internals begin listening for new QuicServerSession instances. - #completeListen = function(transportParams, preferredAddress) { + [kCompleteListen](transportParams, preferredAddress) { const { address, port, @@ -1155,15 +1176,16 @@ class QuicSocket extends EventEmitter { // When the handle is told to listen, it will begin acting as a QUIC // server and will emit session events whenever a new QuicServerSession // is created. + const state = this[kInternalState]; this[kHandle].listen( - this.#serverSecureContext.context, + state.serverSecureContext.context, address, type, port, - this.#alpn, + state.alpn, options); process.nextTick(emit.bind(this, 'listening')); - }; + } // When the QuicSocket listen() function is called, the first step is to bind // the underlying QuicEndpoint's. Once at least one endpoint has been bound, @@ -1174,7 +1196,7 @@ class QuicSocket extends EventEmitter { // connecting clients to use. If specified, this will be communicate to the // client as part of the tranport parameters exchanged during the TLS // handshake. - #continueListen = function(transportParams, lookup) { + [kContinueListen](transportParams, lookup) { const { preferredAddress } = transportParams; // TODO(@jasnell): Currently, we wait to start resolving the @@ -1196,7 +1218,7 @@ class QuicSocket extends EventEmitter { // preferred address. lookup( address || (typeVal === AF_INET6 ? '::' : '0.0.0.0'), - QuicSocket.#afterPreferredAddressLookup.bind( + QuicSocket[kAfterPreferredAddressLookup].bind( this, transportParams, port, @@ -1204,19 +1226,20 @@ class QuicSocket extends EventEmitter { return; } // If preferred address is not set, we can skip directly to the listen - this.#completeListen(transportParams); - }; + this[kCompleteListen](transportParams); + } // Begin listening for server connections. The callback that may be // passed to this function is registered as a handler for the // on('session') event. Errors may be thrown synchronously by this // function. listen(options, callback) { - if (this.#serverListening) + const state = this[kInternalState]; + if (state.serverListening) throw new ERR_QUICSOCKET_LISTENING(); - if (this.#state === kSocketDestroyed || - this.#state === kSocketClosing) { + if (state.state === kSocketDestroyed || + state.state === kSocketClosing) { throw new ERR_QUICSOCKET_DESTROYED('listen'); } @@ -1229,7 +1252,7 @@ class QuicSocket extends EventEmitter { throw new ERR_INVALID_CALLBACK(callback); options = { - ...this.#server, + ...state.server, ...options, minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', @@ -1245,15 +1268,15 @@ class QuicSocket extends EventEmitter { // Store the secure context so that it is not garbage collected // while we still need to make use of it. - this.#serverSecureContext = + state.serverSecureContext = createSecureContext( options, initSecureContext); - this.#highWaterMark = highWaterMark; - this.#defaultEncoding = defaultEncoding; - this.#serverListening = true; - this.#alpn = alpn; + state.highWaterMark = highWaterMark; + state.defaultEncoding = defaultEncoding; + state.serverListening = true; + state.alpn = alpn; // If the callback function is provided, it is registered as a // handler for the on('session') event and will be called whenever @@ -1262,10 +1285,10 @@ class QuicSocket extends EventEmitter { this.on('session', callback); // Bind the QuicSocket to the local port if it hasn't been bound already. - this[kMaybeBind](this.#continueListen.bind( + this[kMaybeBind](this[kContinueListen].bind( this, transportParams, - this.#lookup)); + state.lookup)); } // When the QuicSocket connect() function is called, the first step is to bind @@ -1274,7 +1297,7 @@ class QuicSocket extends EventEmitter { // process. // // The immediate next step is to resolve the address into an ip address. - #continueConnect = function(session, lookup, address, type) { + [kContinueConnect](session, lookup, address, type) { // TODO(@jasnell): Currently, we perform the DNS resolution after // the QuicSocket has been bound. We don't have to. We could do // it in parallel while we're waitint to be bound but doing so @@ -1285,14 +1308,13 @@ class QuicSocket extends EventEmitter { lookup( address || (type === AF_INET6 ? '::' : '0.0.0.0'), connectAfterLookup.bind(session, type)); - }; + } // Creates and returns a new QuicClientSession. connect(options, callback) { - if (this.#state === kSocketDestroyed || - this.#state === kSocketClosing) { + const state = this[kInternalState]; + if (state.state === kSocketDestroyed || state.state === kSocketClosing) throw new ERR_QUICSOCKET_DESTROYED('connect'); - } if (typeof options === 'function') { callback = options; @@ -1300,7 +1322,7 @@ class QuicSocket extends EventEmitter { } options = { - ...this.#client, + ...state.client, ...options }; @@ -1316,10 +1338,10 @@ class QuicSocket extends EventEmitter { if (typeof callback === 'function') session.once('ready', callback); - this[kMaybeBind](this.#continueConnect.bind( + this[kMaybeBind](this[kContinueConnect].bind( this, session, - this.#lookup, + state.lookup, address, type)); @@ -1354,7 +1376,8 @@ class QuicSocket extends EventEmitter { // once('close') event (if specified) and the QuicSocket is destroyed // immediately. close(callback) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('close'); // If a callback function is specified, it is registered as a @@ -1370,17 +1393,17 @@ class QuicSocket extends EventEmitter { // If we are already closing, do nothing else and wait // for the close event to be invoked. - if (this.#state === kSocketClosing) + if (state.state === kSocketClosing) return; // If the QuicSocket is otherwise not bound to the local // port, destroy the QuicSocket immediately. - if (this.#state !== kSocketBound) { + if (state.state !== kSocketBound) { this.destroy(); } // Mark the QuicSocket as closing to prevent re-entry - this.#state = kSocketClosing; + state.state = kSocketClosing; // Otherwise, gracefully close each QuicSession, with // [kMaybeDestroy]() being called after each closes. @@ -1393,7 +1416,7 @@ class QuicSocket extends EventEmitter { if (this[kHandle]) { this[kHandle].stopListening(); } - this.#serverListening = false; + state.serverListening = false; // If there are no sessions, calling maybeDestroy // will immediately and synchronously destroy the @@ -1407,7 +1430,7 @@ class QuicSocket extends EventEmitter { // they will call the kMaybeDestroy function. When there // are no remaining session instances, the QuicSocket // will be closed and destroyed. - for (const session of this.#sessions) + for (const session of state.sessions) session.close(maybeDestroy); } @@ -1424,158 +1447,150 @@ class QuicSocket extends EventEmitter { // flushed from the QuicSocket's queue, the QuicSocket C++ instance // will be destroyed and freed from memory. destroy(error) { + const state = this[kInternalState]; // If the QuicSocket is already destroyed, do nothing - if (this.#state === kSocketDestroyed) + if (state.state === kSocketDestroyed) return; // Mark the QuicSocket as being destroyed. - this.#state = kSocketDestroyed; + state.state = kSocketDestroyed; // Immediately close any sessions that may be remaining. // If the udp socket is in a state where it is able to do so, // a final attempt to send CONNECTION_CLOSE frames for each // closed session will be made. - for (const session of this.#sessions) + for (const session of state.sessions) session.destroy(error); this[kDestroy](error); } ref() { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('ref'); - for (const endpoint of this.#endpoints) + for (const endpoint of state.endpoints) endpoint.ref(); return this; } unref() { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('unref'); - for (const endpoint of this.#endpoints) + for (const endpoint of state.endpoints) endpoint.unref(); return this; } get endpoints() { - return Array.from(this.#endpoints); + return Array.from(this[kInternalState].endpoints); } // The sever secure context is the SecureContext specified when calling // listen. It is the context that will be used with all new server // QuicSession instances. get serverSecureContext() { - return this.#serverSecureContext; + return this[kInternalState].serverSecureContext; } // True if at least one associated QuicEndpoint has been successfully // bound to a local UDP port. get bound() { - return this.#state === kSocketBound; + return this[kInternalState].state === kSocketBound; } // True if graceful close has been initiated by calling close() get closing() { - return this.#state === kSocketClosing; + return this[kInternalState].state === kSocketClosing; } // True if the QuicSocket has been destroyed and is no longer usable get destroyed() { - return this.#state === kSocketDestroyed; + return this[kInternalState].state === kSocketDestroyed; } // True if listen() has been called successfully get listening() { - return this.#serverListening; + return this[kInternalState].serverListening; } // True if the QuicSocket is currently waiting on at least one // QuicEndpoint to succesfully bind.g get pending() { - return this.#state === kSocketPending; + return this[kInternalState].state === kSocketPending; } // Marking a server as busy will cause all new // connection attempts to fail with a SERVER_BUSY CONNECTION_CLOSE. setServerBusy(on = true) { - if (this.#state === kSocketDestroyed) + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setServerBusy'); validateBoolean(on, 'on'); - if (this.#serverBusy !== on) + if (state.serverBusy !== on) this[kHandle].setServerBusy(on); } + get serverBusy() { + return this[kInternalState].serverBusy; + } + get duration() { // TODO(@jasnell): If the object is destroyed, it should // use a fixed duration rather than calculating from now - const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; - return now - stats[IDX_QUIC_SOCKET_STATS_CREATED_AT]; + return process.hrtime.bigint() - + getStats(this, IDX_QUIC_SOCKET_STATS_CREATED_AT); } get boundDuration() { // TODO(@jasnell): If the object is destroyed, it should // use a fixed duration rather than calculating from now - const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; - return now - stats[IDX_QUIC_SOCKET_STATS_BOUND_AT]; + return process.hrtime.bigint() - + getStats(this, IDX_QUIC_SOCKET_STATS_BOUND_AT); } get listenDuration() { // TODO(@jasnell): If the object is destroyed, it should // use a fixed duration rather than calculating from now - const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; - return now - stats[IDX_QUIC_SOCKET_STATS_LISTEN_AT]; + return process.hrtime.bigint() - + getStats(this, IDX_QUIC_SOCKET_STATS_LISTEN_AT); } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_BYTES_RECEIVED]; + return getStats(this, IDX_QUIC_SOCKET_STATS_BYTES_RECEIVED); } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_BYTES_SENT]; + return getStats(this, IDX_QUIC_SOCKET_STATS_BYTES_SENT); } get packetsReceived() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_PACKETS_RECEIVED]; + return getStats(this, IDX_QUIC_SOCKET_STATS_PACKETS_RECEIVED); } get packetsSent() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_PACKETS_SENT]; + return getStats(this, IDX_QUIC_SOCKET_STATS_PACKETS_SENT); } get packetsIgnored() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_PACKETS_IGNORED]; - } - - get serverBusy() { - return this.#serverBusy; + return getStats(this, IDX_QUIC_SOCKET_STATS_PACKETS_IGNORED); } get serverSessions() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS]; + return getStats(this, IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS); } get clientSessions() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS]; + return getStats(this, IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS); } get statelessResetCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT]; + return getStats(this, IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT); } get serverBusyCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SOCKET_STATS_SERVER_BUSY_COUNT]; + return getStats(this, IDX_QUIC_SOCKET_STATS_SERVER_BUSY_COUNT); } // Diagnostic packet loss is a testing mechanism that allows simulating @@ -1583,7 +1598,7 @@ class QuicSocket extends EventEmitter { // option is a number between 0 and 1 that identifies the possibility of // packet loss in the given direction. setDiagnosticPacketLoss(options) { - if (this.#state === kSocketDestroyed) + if (this[kInternalState].state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('setDiagnosticPacketLoss'); const { rx = 0.0, @@ -1616,36 +1631,38 @@ class QuicSocket extends EventEmitter { // to be switched on/off dynamically through the lifetime of the // socket. toggleStatelessReset() { - if (this.#state === kSocketDestroyed) + if (this[kInternalState].state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('toggleStatelessReset'); return this[kHandle].toggleStatelessReset(); } } class QuicSession extends EventEmitter { - #alpn = undefined; - #cipher = undefined; - #cipherVersion = undefined; - #closeCode = NGTCP2_NO_ERROR; - #closeFamily = QUIC_ERROR_APPLICATION; - #closing = false; - #destroyed = false; - #earlyData = false; - #handshakeComplete = false; - #idleTimeout = false; - #maxPacketLength = NGTCP2_DEFAULT_MAX_PKTLEN; - #servername = undefined; - #socket = undefined; - #statelessReset = false; - #stats = undefined; - #pendingStreams = new Set(); - #streams = new Map(); - #verifyErrorReason = undefined; - #verifyErrorCode = undefined; - #handshakeAckHistogram = undefined; - #handshakeContinuationHistogram = undefined; - #highWaterMark = undefined; - #defaultEncoding = undefined; + [kInternalState] = { + alpn: undefined, + cipher: undefined, + cipherVersion: undefined, + closeCode: NGTCP2_NO_ERROR, + closeFamily: QUIC_ERROR_APPLICATION, + closing: false, + destroyed: false, + earlyData: false, + handshakeComplete: false, + idleTimeout: false, + maxPacketLength: NGTCP2_DEFAULT_MAX_PKTLEN, + servername: undefined, + socket: undefined, + statelessReset: false, + stats: undefined, + pendingStreams: new Set(), + streams: new Map(), + verifyErrorReason: undefined, + verifyErrorCode: undefined, + handshakeAckHistogram: undefined, + handshakeContinuationHistogram: undefined, + highWaterMark: undefined, + defaultEncoding: undefined, + }; constructor(socket, options) { const { @@ -1657,20 +1674,22 @@ class QuicSession extends EventEmitter { super({ captureRejections: true }); this.on('newListener', onNewListener); this.on('removeListener', onRemoveListener); - this.#socket = socket; - this.#servername = servername; - this.#alpn = alpn; - this.#highWaterMark = highWaterMark; - this.#defaultEncoding = defaultEncoding; + const state = this[kInternalState]; + state.socket = socket; + state.servername = servername; + state.alpn = alpn; + state.highWaterMark = highWaterMark; + state.defaultEncoding = defaultEncoding; socket[kAddSession](this); } // kGetStreamOptions is called to get the configured options // for peer initiated QuicStream instances. [kGetStreamOptions]() { + const state = this[kInternalState]; return { - highWaterMark: this.#highWaterMark, - defaultEncoding: this.#defaultEncoding, + highWaterMark: state.highWaterMark, + defaultEncoding: state.defaultEncoding, }; } @@ -1681,16 +1700,17 @@ class QuicSession extends EventEmitter { // must first perform DNS resolution on the provided address // before the underlying QuicSession handle can be created. [kSetHandle](handle) { + const state = this[kInternalState]; this[kHandle] = handle; if (handle !== undefined) { handle[owner_symbol] = this; - this.#handshakeAckHistogram = new Histogram(handle.ack); - this.#handshakeContinuationHistogram = new Histogram(handle.rate); + state.handshakeAckHistogram = new Histogram(handle.ack); + state.handshakeContinuationHistogram = new Histogram(handle.rate); } else { - if (this.#handshakeAckHistogram) - this.#handshakeAckHistogram[kDestroyHistogram](); - if (this.#handshakeContinuationHistogram) - this.#handshakeContinuationHistogram[kDestroyHistogram](); + if (state.handshakeAckHistogram) + state.handshakeAckHistogram[kDestroyHistogram](); + if (state.handshakeContinuationHistogram) + state.handshakeContinuationHistogram[kDestroyHistogram](); } } @@ -1715,9 +1735,10 @@ class QuicSession extends EventEmitter { // Causes the QuicSession to be immediately destroyed, but with // additional metadata set. [kDestroy](statelessReset, family, code) { - this.#statelessReset = !!statelessReset; - this.#closeCode = code; - this.#closeFamily = family; + const state = this[kInternalState]; + state.statelessReset = statelessReset; + state.closeCode = code; + state.closeFamily = family; this.destroy(); } @@ -1728,21 +1749,22 @@ class QuicSession extends EventEmitter { // CONNECTION_CLOSE will be generated and sent, switching // the session into the closing period. [kClose](family, code) { + const state = this[kInternalState]; // Do nothing if the QuicSession has already been destroyed. - if (this.#destroyed) + if (state.destroyed) return; // Set the close code and family so we can keep track. - this.#closeCode = code; - this.#closeFamily = family; + state.closeCode = code; + state.closeFamily = family; // Shutdown all pending streams. These are Streams that // have been created but do not yet have a handle assigned. - for (const stream of this.#pendingStreams) + for (const stream of state.pendingStreams) stream[kClose](family, code); // Shutdown all of the remaining streams - for (const stream of this.#streams.values()) + for (const stream of state.streams.values()) stream[kClose](family, code); // By this point, all necessary RESET_STREAM and @@ -1755,7 +1777,7 @@ class QuicSession extends EventEmitter { // Closes the specified stream with the given code. The // QuicStream object will be destroyed. [kStreamClose](id, code) { - const stream = this.#streams.get(id); + const stream = this[kInternalState].streams.get(id); if (stream === undefined) return; @@ -1766,7 +1788,7 @@ class QuicSession extends EventEmitter { // instance. This will only be called if the ALPN selected // is known to support headers. [kHeaders](id, headers, kind, push_id) { - const stream = this.#streams.get(id); + const stream = this[kInternalState].streams.get(id); if (stream === undefined) return; @@ -1774,7 +1796,7 @@ class QuicSession extends EventEmitter { } [kStreamReset](id, code) { - const stream = this.#streams.get(id); + const stream = this[kInternalState].streams.get(id); if (stream === undefined) return; @@ -1782,16 +1804,18 @@ class QuicSession extends EventEmitter { } [kInspect]() { + // TODO(@jasnell): A proper custom inspect implementation + const state = this[kInternalState]; const obj = { - alpn: this.#alpn, + alpn: state.alpn, cipher: this.cipher, closing: this.closing, closeCode: this.closeCode, destroyed: this.destroyed, - earlyData: this.#earlyData, + earlyData: state.earlyData, maxStreams: this.maxStreams, servername: this.servername, - streams: this.#streams.size, + streams: state.streams.size, stats: { handshakeAck: this.handshakeAckHistogram, handshakeContinuation: this.handshakeContinuationHistogram, @@ -1801,7 +1825,7 @@ class QuicSession extends EventEmitter { } [kSetSocket](socket) { - this.#socket = socket; + this[kInternalState].socket = socket; } // Called at the completion of the TLS handshake for the local peer @@ -1814,15 +1838,16 @@ class QuicSession extends EventEmitter { verifyErrorReason, verifyErrorCode, earlyData) { - this.#handshakeComplete = true; - this.#servername = servername; - this.#alpn = alpn; - this.#cipher = cipher; - this.#cipherVersion = cipherVersion; - this.#maxPacketLength = maxPacketLength; - this.#verifyErrorReason = verifyErrorReason; - this.#verifyErrorCode = verifyErrorCode; - this.#earlyData = earlyData; + const state = this[kInternalState]; + state.handshakeComplete = true; + state.servername = servername; + state.alpn = alpn; + state.cipher = cipher; + state.cipherVersion = cipherVersion; + state.maxPacketLength = maxPacketLength; + state.verifyErrorReason = verifyErrorReason; + state.verifyErrorCode = verifyErrorCode; + state.earlyData = earlyData; if (!this[kHandshakePost]()) return; @@ -1838,18 +1863,19 @@ class QuicSession extends EventEmitter { } [kRemoveStream](stream) { - this.#streams.delete(stream.id); + this[kInternalState].streams.delete(stream.id); } [kAddStream](id, stream) { stream.once('close', this[kMaybeDestroy].bind(this)); - this.#streams.set(id, stream); + this[kInternalState].streams.set(id, stream); } // The QuicSession will be destroyed if closing has been // called and there are no remaining streams [kMaybeDestroy]() { - if (this.#closing && this.#streams.size === 0) + const state = this[kInternalState]; + if (state.closing && state.streams.size === 0) this.destroy(); } @@ -1867,7 +1893,8 @@ class QuicSession extends EventEmitter { // opened. Calls to openStream() will fail, and new streams // from the peer will be rejected/ignored. close(callback) { - if (this.#destroyed) + const state = this[kInternalState]; + if (state.destroyed) throw new ERR_QUICSESSION_DESTROYED('close'); if (callback) { @@ -1879,10 +1906,10 @@ class QuicSession extends EventEmitter { // If we're already closing, do nothing else. // Callback will be invoked once the session // has been destroyed - if (this.#closing) + if (state.closing) return; - this.#closing = true; + state.closing = true; this[kHandle].gracefulClose(); // See if we can close immediately. @@ -1903,11 +1930,12 @@ class QuicSession extends EventEmitter { // Once destroyed, and after the 'error' event (if any), // the close event is emitted on next tick. destroy(error) { + const state = this[kInternalState]; // Destroy can only be called once. Multiple calls will be ignored - if (this.#destroyed) + if (state.destroyed) return; - this.#destroyed = true; - this.#closing = false; + state.destroyed = true; + state.closing = false; if (typeof error === 'number' || (error != null && @@ -1917,19 +1945,19 @@ class QuicSession extends EventEmitter { closeCode, closeFamily } = validateCloseCode(error); - this.#closeCode = closeCode; - this.#closeFamily = closeFamily; + state.closeCode = closeCode; + state.closeFamily = closeFamily; error = new ERR_QUIC_ERROR(closeCode, closeFamily); } // Destroy any pending streams immediately. These // are streams that have been created but have not // yet been assigned an internal handle. - for (const stream of this.#pendingStreams) + for (const stream of state.pendingStreams) stream.destroy(error); // Destroy any remaining streams immediately. - for (const stream of this.#streams.values()) + for (const stream of state.streams.values()) stream.destroy(error); this.removeListener('newListener', onNewListener); @@ -1940,20 +1968,20 @@ class QuicSession extends EventEmitter { const handle = this[kHandle]; if (handle !== undefined) { // Copy the stats for use after destruction - this.#stats = new BigInt64Array(handle.stats); - this.#idleTimeout = !!handle.state[IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT]; + state.stats = new BigInt64Array(handle.stats); + state.idleTimeout = !!handle.state[IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT]; // Calling destroy will cause a CONNECTION_CLOSE to be // sent to the peer and will destroy the QuicSession // handler immediately. - handle.destroy(this.#closeCode, this.#closeFamily); + handle.destroy(state.closeCode, state.closeFamily); } else { process.nextTick(emit.bind(this, 'close')); } // Remove the QuicSession JavaScript object from the // associated QuicSocket. - this.#socket[kRemoveSession](this); - this.#socket = undefined; + state.socket[kRemoveSession](this); + state.socket = undefined; } // For server QuicSession instances, true if earlyData is @@ -1963,7 +1991,7 @@ class QuicSession extends EventEmitter { // TLS handshake is completed (immeditely before the // secure event is emitted) get usingEarlyData() { - return this.#earlyData; + return this[kInternalState].earlyData; } get maxStreams() { @@ -1977,37 +2005,35 @@ class QuicSession extends EventEmitter { } get address() { - return this.#socket ? this.#socket.address : {}; + return this[kInternalState].socket?.address || {}; } get maxDataLeft() { - return this[kHandle] ? - this[kHandle].state[IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT] : 0; + return this[kHandle]?.state[IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT] || 0; } get bytesInFlight() { - return this[kHandle] ? - this[kHandle].state[IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT] : 0; + return this[kHandle]?.state[IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT] || 0; } get blockCount() { - return this[kHandle] ? - this[kHandle].state[IDX_QUIC_SESSION_STATS_BLOCK_COUNT] : 0; + return this[kHandle]?.state[IDX_QUIC_SESSION_STATS_BLOCK_COUNT] || 0; } get authenticated() { // Specifically check for null. Undefined means the check has not // been performed yet, another other value other than null means // there was an error - return this.#verifyErrorReason == null; + return this[kInternalState].verifyErrorReason == null; } get authenticationError() { if (this.authenticated) return undefined; + const state = this[kInternalState]; // eslint-disable-next-line no-restricted-syntax - const err = new Error(this.#verifyErrorReason); - const code = 'ERR_QUIC_VERIFY_' + this.#verifyErrorCode; + const err = new Error(state.verifyErrorReason); + const code = 'ERR_QUIC_VERIFY_' + state.verifyErrorCode; err.name = `Error [${code}]`; err.code = code; return err; @@ -2021,26 +2047,30 @@ class QuicSession extends EventEmitter { } get handshakeComplete() { - return this.#handshakeComplete; + return this[kInternalState].handshakeComplete; } get handshakeConfirmed() { - return Boolean(this[kHandle] ? - this[kHandle].state[IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED] : 0); + return Boolean( + this[kHandle]?.state[IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED]); } get idleTimeout() { - return this.#idleTimeout; + return this[kInternalState].idleTimeout; } get alpnProtocol() { - return this.#alpn; + return this[kInternalState].alpn; } get cipher() { - const name = this.#cipher; - const version = this.#cipherVersion; - return this.handshakeComplete ? { name, version } : {}; + if (!this.handshakeComplete) + return {}; + const state = this[kInternalState]; + return { + name: state.cipher, + version: state.cipherVersion, + }; } getCertificate() { @@ -2061,34 +2091,36 @@ class QuicSession extends EventEmitter { } get servername() { - return this.#servername; + return this[kInternalState].servername; } get destroyed() { - return this.#destroyed; + return this[kInternalState].destroyed; } get closing() { - return this.#closing; + return this[kInternalState].closing; } get closeCode() { + const state = this[kInternalState]; return { - code: this.#closeCode, - family: this.#closeFamily + code: state.closeCode, + family: state.closeFamily }; } get socket() { - return this.#socket; + return this[kInternalState].socket; } get statelessReset() { - return this.#statelessReset; + return this[kInternalState].statelessReset; } openStream(options) { - if (this.#destroyed || this.#closing) + const state = this[kInternalState]; + if (state.destroyed || state.closing) throw new ERR_QUICSESSION_DESTROYED('openStream'); const { halfOpen, // Unidirectional or Bidirectional @@ -2108,13 +2140,13 @@ class QuicSession extends EventEmitter { stream.read(); } - this.#pendingStreams.add(stream); + state.pendingStreams.add(stream); // If early data is being used, we can create the internal QuicStream on the // ready event, that is immediately after the internal QuicSession handle // has been created. Otherwise, we have to wait until the secure event // signaling the completion of the TLS handshake. - const makeStream = QuicSession.#makeStream.bind(this, stream, halfOpen); + const makeStream = QuicSession[kMakeStream].bind(this, stream, halfOpen); let deferred = false; if (this.allowEarlyData && !this.ready) { deferred = true; @@ -2130,8 +2162,8 @@ class QuicSession extends EventEmitter { return stream; } - static #makeStream = function(stream, halfOpen) { - this.#pendingStreams.delete(stream); + static [kMakeStream](stream, halfOpen) { + this[kInternalState].pendingStreams.delete(stream); const handle = halfOpen ? _openUnidirectionalStream(this[kHandle]) : @@ -2144,102 +2176,89 @@ class QuicSession extends EventEmitter { stream[kSetHandle](handle); this[kAddStream](stream.id, stream); - }; + } get duration() { - const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; - return now - stats[IDX_QUIC_SESSION_STATS_CREATED_AT]; + return process.hrtime.bigint() - + getStats(this, IDX_QUIC_SESSION_STATS_CREATED_AT); } get handshakeDuration() { - const stats = this.#stats || this[kHandle].stats; const end = this.handshakeComplete ? - stats[4] : process.hrtime.bigint(); - return end - stats[IDX_QUIC_SESSION_STATS_HANDSHAKE_START_AT]; + getStats(this, IDX_QUIC_SESSION_STATS_HANDSHAKE_COMPLETED_AT) : + process.hrtime.bigint(); + return end - getStats(this, IDX_QUIC_SESSION_STATS_HANDSHAKE_START_AT); } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_BYTES_RECEIVED]; + return getStats(this, IDX_QUIC_SESSION_STATS_BYTES_RECEIVED); } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_BYTES_SENT]; + return getStats(this, IDX_QUIC_SESSION_STATS_BYTES_SENT); } get bidiStreamCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_BIDI_STREAM_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_BIDI_STREAM_COUNT); } get uniStreamCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_UNI_STREAM_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_UNI_STREAM_COUNT); } get maxInFlightBytes() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT]; + return getStats(this, IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT); } get lossRetransmitCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT); } get ackDelayRetransmitCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT); } get peerInitiatedStreamCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_STREAMS_IN_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_STREAMS_IN_COUNT); } get selfInitiatedStreamCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT); } get keyUpdateCount() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT]; + return getStats(this, IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT); } get minRTT() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_MIN_RTT]; + return getStats(this, IDX_QUIC_SESSION_STATS_MIN_RTT); } get latestRTT() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_LATEST_RTT]; + return getStats(this, IDX_QUIC_SESSION_STATS_LATEST_RTT); } get smoothedRTT() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_SESSION_STATS_SMOOTHED_RTT]; + return getStats(this, IDX_QUIC_SESSION_STATS_SMOOTHED_RTT); } updateKey() { + const state = this[kInternalState]; // Initiates a key update for the connection. - if (this.#destroyed || this.#closing) + if (state.destroyed || state.closing) throw new ERR_QUICSESSION_DESTROYED('updateKey'); - if (!this.handshakeConfirmed) + if (!state.handshakeConfirmed) throw new ERR_QUICSESSION_UPDATEKEY(); return this[kHandle].updateKey(); } get handshakeAckHistogram() { - return this.#handshakeAckHistogram; + return this[kInternalState].handshakeAckHistogram; } get handshakeContinuationHistogram() { - return this.#handshakeContinuationHistogram; + return this[kInternalState].handshakeContinuationHistogram; } // TODO(addaleax): This is a temporary solution for testing and should be @@ -2250,7 +2269,9 @@ class QuicSession extends EventEmitter { } class QuicServerSession extends QuicSession { - #contexts = []; + [kInternalServerState] = { + contexts: [] + }; constructor(socket, handle, options) { const { @@ -2280,11 +2301,12 @@ class QuicServerSession extends QuicSession { // Called only when an OCSPRequest event handler is registered. // Allows user code the ability to answer the OCSP query. [kCert](servername, callback) { + const state = this[kInternalServerState]; const { serverSecureContext } = this.socket; let { context } = serverSecureContext; - for (var i = 0; i < this.#contexts.length; i++) { - const elem = this.#contexts[i]; + for (var i = 0; i < state.contexts.length; i++) { + const elem = state.contexts[i]; if (elem[0].test(servername)) { context = elem[1]; break; @@ -2315,27 +2337,30 @@ class QuicServerSession extends QuicSession { servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1') .replace(/\*/g, '[^.]*') + '$'); - this.#contexts.push([re, _createSecureContext(context)]); + this[kInternalServerState].contexts.push( + [re, _createSecureContext(context)]); } } class QuicClientSession extends QuicSession { - #allowEarlyData = false; - #autoStart = true; - #dcid = undefined; - #handshakeStarted = false; - #ipv6Only = undefined; - #minDHSize = undefined; - #port = undefined; - #ready = 0; - #remoteTransportParams = undefined; - #requestOCSP = undefined; - #secureContext = undefined; - #sessionTicket = undefined; - #transportParams = undefined; - #preferredAddressPolicy; - #verifyHostnameIdentity = true; - #qlogEnabled = false; + [kInternalClientState] = { + allowEarlyData: false, + autoStart: true, + dcid: undefined, + handshakeStarted: false, + ipv6Only: undefined, + minDHSize: undefined, + port: undefined, + ready: 0, + remoteTransportParams: undefined, + requestOCSP: undefined, + secureContext: undefined, + sessionTicket: undefined, + transportParams: undefined, + preferredAddressPolicy: undefined, + verifyHostnameIdentity: true, + qlogEnabled: false, + }; constructor(socket, options) { const sc_options = { @@ -2371,27 +2396,28 @@ class QuicClientSession extends QuicSession { } super(socket, { servername, alpn, highWaterMark, defaultEncoding }); - this.#autoStart = autoStart; - this.#handshakeStarted = autoStart; - this.#dcid = dcid; - this.#ipv6Only = ipv6Only; - this.#minDHSize = minDHSize; - this.#port = port || 0; - this.#preferredAddressPolicy = preferredAddressPolicy; - this.#requestOCSP = requestOCSP; - this.#secureContext = + const state = this[kInternalClientState]; + state.autoStart = autoStart; + state.handshakeStarted = autoStart; + state.dcid = dcid; + state.ipv6Only = ipv6Only; + state.minDHSize = minDHSize; + state.port = port || 0; + state.preferredAddressPolicy = preferredAddressPolicy; + state.requestOCSP = requestOCSP; + state.secureContext = createSecureContext( sc_options, initSecureContextClient); - this.#transportParams = validateTransportParams(options); - this.#verifyHostnameIdentity = verifyHostnameIdentity; - this.#qlogEnabled = qlog; + state.transportParams = validateTransportParams(options); + state.verifyHostnameIdentity = verifyHostnameIdentity; + state.qlogEnabled = qlog; // If provided, indicates that the client is attempting to // resume a prior session. Early data would be enabled. - this.#remoteTransportParams = remoteTransportParams; - this.#sessionTicket = sessionTicket; - this.#allowEarlyData = + state.remoteTransportParams = remoteTransportParams; + state.sessionTicket = sessionTicket; + state.allowEarlyData = remoteTransportParams !== undefined && sessionTicket !== undefined; @@ -2401,7 +2427,7 @@ class QuicClientSession extends QuicSession { [kHandshakePost]() { const { type, size } = this.ephemeralKeyInfo; - if (type === 'DH' && size < this.#minDHSize) { + if (type === 'DH' && size < this[kInternalClientState].minDHSize) { this.destroy(new ERR_TLS_DH_PARAM_SIZE(size)); return false; } @@ -2413,12 +2439,13 @@ class QuicClientSession extends QuicSession { } [kContinueConnect](type, ip) { - setTransportParams(this.#transportParams); + const state = this[kInternalClientState]; + setTransportParams(state.transportParams); const options = - (this.#verifyHostnameIdentity ? + (state.verifyHostnameIdentity ? QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY : 0) | - (this.#requestOCSP ? + (state.requestOCSP ? QUICCLIENTSESSION_OPTION_REQUEST_OCSP : 0); const handle = @@ -2426,23 +2453,23 @@ class QuicClientSession extends QuicSession { this.socket[kHandle], type, ip, - this.#port, - this.#secureContext.context, + state.port, + state.secureContext.context, this.servername || ip, - this.#remoteTransportParams, - this.#sessionTicket, - this.#dcid, - this.#preferredAddressPolicy, + state.remoteTransportParams, + state.sessionTicket, + state.dcid, + state.preferredAddressPolicy, this.alpnProtocol, options, - this.#qlogEnabled, - this.#autoStart); + state.qlogEnabled, + state.autoStart); // We no longer need these, unset them so // memory can be garbage collected. - this.#remoteTransportParams = undefined; - this.#sessionTicket = undefined; - this.#dcid = undefined; + state.remoteTransportParams = undefined; + state.sessionTicket = undefined; + state.dcid = undefined; // If handle is a number, creating the session failed. if (typeof handle === 'number') { @@ -2478,40 +2505,41 @@ class QuicClientSession extends QuicSession { if (this.listenerCount('usePreferredAddress') > 0) toggleListeners(handle, 'usePreferredAddress', true); - this.#maybeReady(0x2); + this[kMaybeReady](0x2); } [kSocketReady]() { - this.#maybeReady(0x1); + this[kMaybeReady](0x1); } // The QuicClientSession is ready for use only after // (a) The QuicSocket has been bound and // (b) The internal handle has been created - #maybeReady = function(flag) { - this.#ready |= flag; + [kMaybeReady](flag) { + this[kInternalClientState].ready |= flag; if (this.ready) process.nextTick(emit.bind(this, 'ready')); - }; + } get allowEarlyData() { - return this.#allowEarlyData; + return this[kInternalClientState].allowEarlyData; } get ready() { - return this.#ready === 0x3; + return this[kInternalClientState].ready === 0x3; } get handshakeStarted() { - return this.#handshakeStarted; + return this[kInternalClientState].handshakeStarted; } startHandshake() { + const state = this[kInternalClientState]; if (this.destroyed) throw new ERR_QUICSESSION_DESTROYED('startHandshake'); - if (this.#handshakeStarted) + if (state.handshakeStarted) return; - this.#handshakeStarted = true; + state.handshakeStarted = true; if (!this.ready) { this.once('ready', () => this[kHandle].startHandshake()); } else { @@ -2525,7 +2553,7 @@ class QuicClientSession extends QuicSession { {}; } - #setSocketAfterBind = function(socket, callback) { + [kSetSocketAfterBind](socket, callback) { if (socket.destroyed) { callback(new ERR_QUICSOCKET_DESTROYED('setSocket')); return; @@ -2544,7 +2572,7 @@ class QuicClientSession extends QuicSession { this[kSetSocket](socket); callback(); - }; + } setSocket(socket, callback) { if (!(socket instanceof QuicSocket)) @@ -2553,7 +2581,7 @@ class QuicClientSession extends QuicSession { if (typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(); - socket[kMaybeBind](() => this.#setSocketAfterBind(socket, callback)); + socket[kMaybeBind](() => this[kSetSocketAfterBind](socket, callback)); } } @@ -2568,19 +2596,21 @@ function streamOnPause() { } class QuicStream extends Duplex { - #closed = false; - #aborted = false; - #defaultEncoding = undefined; - #didRead = false; - #id = undefined; - #highWaterMark = undefined; - #push_id = undefined; - #resetCode = undefined; - #session = undefined; - #dataRateHistogram = undefined; - #dataSizeHistogram = undefined; - #dataAckHistogram = undefined; - #stats = undefined; + [kInternalState] = { + closed: false, + aborted: false, + defaultEncoding: undefined, + didRead: false, + id: undefined, + highWaterMark: undefined, + push_id: undefined, + resetCode: undefined, + session: undefined, + dataRateHistogram: undefined, + dataSizeHistogram: undefined, + dataAckHistogram: undefined, + stats: undefined, + }; constructor(options, session, push_id) { const { @@ -2597,16 +2627,18 @@ class QuicStream extends Duplex { autoDestroy: false, captureRejections: true, }); - this.#highWaterMark = highWaterMark; - this.#defaultEncoding = defaultEncoding; - this.#session = session; - this.#push_id = push_id; + const state = this[kInternalState]; + state.highWaterMark = highWaterMark; + state.defaultEncoding = defaultEncoding; + state.session = session; + state.push_id = push_id; this._readableState.readingMore = true; this.on('pause', streamOnPause); // The QuicStream writes are corked until kSetHandle // is set, ensuring that writes are buffered in JavaScript // until we have somewhere to send them. + // TODO(@jasnell): We need a better mechanism for this. this.cork(); } @@ -2618,28 +2650,29 @@ class QuicStream extends Duplex { // written will be buffered until kSetHandle is called. [kSetHandle](handle) { this[kHandle] = handle; + const state = this[kInternalState]; if (handle !== undefined) { handle.onread = onStreamRead; handle[owner_symbol] = this; this[async_id_symbol] = handle.getAsyncId(); - this.#id = handle.id(); - this.#dataRateHistogram = new Histogram(handle.rate); - this.#dataSizeHistogram = new Histogram(handle.size); - this.#dataAckHistogram = new Histogram(handle.ack); + state.id = handle.id(); + state.dataRateHistogram = new Histogram(handle.rate); + state.dataSizeHistogram = new Histogram(handle.size); + state.dataAckHistogram = new Histogram(handle.ack); this.uncork(); this.emit('ready'); } else { - if (this.#dataRateHistogram) - this.#dataRateHistogram[kDestroyHistogram](); - if (this.#dataSizeHistogram) - this.#dataSizeHistogram[kDestroyHistogram](); - if (this.#dataAckHistogram) - this.#dataAckHistogram[kDestroyHistogram](); + if (state.dataRateHistogram) + state.dataRateHistogram[kDestroyHistogram](); + if (state.dataSizeHistogram) + state.dataSizeHistogram[kDestroyHistogram](); + if (state.dataAckHistogram) + state.dataAckHistogram[kDestroyHistogram](); } } [kStreamReset](code) { - this.#resetCode = code | 0; + this[kInternalState].resetCode = code | 0; this.push(null); this.read(); } @@ -2669,6 +2702,7 @@ class QuicStream extends Duplex { } [kClose](family, code) { + const state = this[kInternalState]; // Trigger the abrupt shutdown of the stream. If the stream is // already no-longer readable or writable, this does nothing. If // the stream is readable or writable, then the abort event will @@ -2679,15 +2713,15 @@ class QuicStream extends Duplex { // having been closed to be destroyed. // Do nothing if we've already been destroyed - if (this.destroyed || this.#closed) + if (this.destroyed || state.closed) return; if (this.pending) return this.once('ready', () => this[kClose](family, code)); - this.#closed = true; + state.closed = true; - this.#aborted = this.readable || this.writable; + state.aborted = this.readable || this.writable; // Trigger scheduling of the RESET_STREAM and STOP_SENDING frames // as appropriate. Notify ngtcp2 that the stream is to be shutdown. @@ -2713,10 +2747,11 @@ class QuicStream extends Duplex { } [kInspect]() { + // TODO(@jasnell): Proper custom inspect implementation const direction = this.bidirectional ? 'bidirectional' : 'unidirectional'; const initiated = this.serverInitiated ? 'server' : 'client'; const obj = { - id: this.#id, + id: this[kInternalState].id, direction, initiated, writableState: this._writableState, @@ -2743,15 +2778,15 @@ class QuicStream extends Duplex { get pending() { // The id is set in the kSetHandle function - return this.#id === undefined; + return this[kInternalState].id === undefined; } get aborted() { - return this.#aborted; + return this[kInternalState].aborted; } get serverInitiated() { - return !!(this.#id & 0b01); + return !!(this[kInternalState].id & 0b01); } get clientInitiated() { @@ -2759,14 +2794,14 @@ class QuicStream extends Duplex { } get unidirectional() { - return !!(this.#id & 0b10); + return !!(this[kInternalState].id & 0b10); } get bidirectional() { return !this.unidirectional; } - #writeGeneric = function(writev, data, encoding, cb) { + [kWriteGeneric](writev, data, encoding, cb) { if (this.destroyed) return; // TODO(addaleax): Can this happen? @@ -2776,7 +2811,7 @@ class QuicStream extends Duplex { // ready event is emitted. if (this.pending) { return this.once('ready', () => { - this.#writeGeneric(writev, data, encoding, cb); + this[kWriteGeneric](writev, data, encoding, cb); }); } @@ -2786,14 +2821,14 @@ class QuicStream extends Duplex { writeGeneric(this, data, encoding, cb); this[kTrackWriteState](this, req.bytes); - }; + } _write(data, encoding, cb) { - this.#writeGeneric(false, data, encoding, cb); + this[kWriteGeneric](false, data, encoding, cb); } _writev(data, cb) { - this.#writeGeneric(true, data, '', cb); + this[kWriteGeneric](true, data, '', cb); } // Called when the last chunk of data has been @@ -2832,19 +2867,20 @@ class QuicStream extends Duplex { this.push(null); return; } - if (!this.#didRead) { + const state = this[kInternalState]; + if (!state.didRead) { this._readableState.readingMore = false; - this.#didRead = true; + state.didRead = true; } streamOnResume.call(this); } sendFile(path, options = {}) { - fs.open(path, 'r', QuicStream.#onFileOpened.bind(this, options)); + fs.open(path, 'r', QuicStream[kOnFileOpened].bind(this, options)); } - static #onFileOpened = function(options, err, fd) { + static [kOnFileOpened](options, err, fd) { const onError = options.onError; if (err) { if (onError) { @@ -2862,10 +2898,10 @@ class QuicStream extends Duplex { } this.sendFD(fd, options, true); - }; + } sendFD(fd, { offset = -1, length = -1 } = {}, ownsFd = false) { - if (this.destroyed || this.#closed) + if (this.destroyed || this[kInternalState].closed) return; validateInteger(offset, 'options.offset', /* min */ -1); @@ -2891,17 +2927,17 @@ class QuicStream extends Duplex { this.end(); defaultTriggerAsyncIdScope(this[async_id_symbol], - QuicStream.#startFilePipe, + QuicStream[kStartFilePipe], this, fd, offset, length); } - static #startFilePipe = (stream, fd, offset, length) => { + static [kStartFilePipe](stream, fd, offset, length) { const handle = new FileHandle(fd, offset, length); - handle.onread = QuicStream.#onPipedFileHandleRead; + handle.onread = QuicStream[kOnPipedFileHandleRead]; handle.stream = stream; const pipe = new StreamPipe(handle, stream[kHandle]); - pipe.onunpipe = QuicStream.#onFileUnpipe; + pipe.onunpipe = QuicStream[kOnFileUnpipe]; pipe.start(); // Exact length of the file doesn't matter here, since the @@ -2910,24 +2946,25 @@ class QuicStream extends Duplex { stream[kTrackWriteState](stream, 1); } - static #onFileUnpipe = function() { // Called on the StreamPipe instance. + static [kOnFileUnpipe]() { // Called on the StreamPipe instance. const stream = this.sink[owner_symbol]; if (stream.ownsFd) this.source.close().catch(stream.destroy.bind(stream)); else this.source.releaseFD(); - }; + } - static #onPipedFileHandleRead = function() { + static [kOnPipedFileHandleRead]() { const err = streamBaseState[kReadBytesOrError]; if (err < 0 && err !== UV_EOF) { this.stream.destroy(errnoException(err, 'sendFD')); } - }; + } get resetReceived() { - return (this.#resetCode !== undefined) ? - { code: this.#resetCode | 0 } : + const state = this[kInternalState]; + return (state.resetCode !== undefined) ? + { code: state.resetCode | 0 } : undefined; } @@ -2937,11 +2974,11 @@ class QuicStream extends Duplex { } get id() { - return this.#id; + return this[kInternalState].id; } get push_id() { - return this.#push_id; + return this[kInternalState].push_id; } close(code) { @@ -2949,17 +2986,18 @@ class QuicStream extends Duplex { } get session() { - return this.#session; + return this[kInternalState].session; } _destroy(error, callback) { - this.#session[kRemoveStream](this); + const state = this[kInternalState]; + state.session[kRemoveStream](this); const handle = this[kHandle]; // Do not use handle after this point as the underlying C++ // object has been destroyed. Any attempt to use the object // will segfault and crash the process. if (handle !== undefined) { - this.#stats = new BigInt64Array(handle.stats); + state.stats = new BigInt64Array(handle.stats); handle.destroy(); } // The destroy callback must be invoked in a nextTick @@ -2971,24 +3009,25 @@ class QuicStream extends Duplex { } get dataRateHistogram() { - return this.#dataRateHistogram; + return this[kInternalState].dataRateHistogram; } get dataSizeHistogram() { - return this.#dataSizeHistogram; + return this[kInternalState].dataSizeHistogram; } get dataAckHistogram() { - return this.#dataAckHistogram; + return this[kInternalState].dataAckHistogram; } pushStream(headers = {}, options = {}) { if (this.destroyed) throw new ERR_QUICSTREAM_DESTROYED('push'); + const state = this[kInternalState]; const { - highWaterMark = this.#highWaterMark, - defaultEncoding = this.#defaultEncoding, + highWaterMark = state.highWaterMark, + defaultEncoding = state.defaultEncoding, } = validateQuicStreamOptions(options); validateObject(headers, 'headers'); @@ -3119,39 +3158,32 @@ class QuicStream extends Duplex { } get duration() { - const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; - return now - stats[IDX_QUIC_STREAM_STATS_CREATED_AT]; + return process.hrtime.bigint() - + getStats(this, IDX_QUIC_STREAM_STATS_CREATED_AT); } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_BYTES_RECEIVED]; + return getStats(this, IDX_QUIC_STREAM_STATS_BYTES_RECEIVED); } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_BYTES_SENT]; + return getStats(this, IDX_QUIC_STREAM_STATS_BYTES_SENT); } get maxExtendedOffset() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET]; + return getStats(this, IDX_QUIC_STREAM_STATS_MAX_OFFSET); } get finalSize() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_FINAL_SIZE]; + return getStats(this, IDX_QUIC_STREAM_STATS_FINAL_SIZE); } get maxAcknowledgedOffset() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_ACK]; + return getStats(this, IDX_QUIC_STREAM_STATS_MAX_OFFSET_ACK); } get maxReceivedOffset() { - const stats = this.#stats || this[kHandle].stats; - return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_RECV]; + return getStats(this, IDX_QUIC_STREAM_STATS_MAX_OFFSET_RECV); } } diff --git a/src/env.h b/src/env.h index 2add16c8eddaa7..f16c79b500171c 100644 --- a/src/env.h +++ b/src/env.h @@ -450,20 +450,17 @@ constexpr size_t kFsStatsBufferLength = #if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC # define QUIC_ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) \ V(quic_on_socket_close_function, v8::Function) \ - V(quic_on_socket_error_function, v8::Function) \ V(quic_on_socket_server_busy_function, v8::Function) \ V(quic_on_session_cert_function, v8::Function) \ V(quic_on_session_client_hello_function, v8::Function) \ V(quic_on_session_close_function, v8::Function) \ V(quic_on_session_destroyed_function, v8::Function) \ - V(quic_on_session_error_function, v8::Function) \ V(quic_on_session_handshake_function, v8::Function) \ V(quic_on_session_keylog_function, v8::Function) \ V(quic_on_session_path_validation_function, v8::Function) \ V(quic_on_session_use_preferred_address_function, v8::Function) \ V(quic_on_session_qlog_function, v8::Function) \ V(quic_on_session_ready_function, v8::Function) \ - V(quic_on_session_silent_close_function, v8::Function) \ V(quic_on_session_status_function, v8::Function) \ V(quic_on_session_ticket_function, v8::Function) \ V(quic_on_session_version_negotiation_function, v8::Function) \ diff --git a/src/quic/node_quic.cc b/src/quic/node_quic.cc index 202767a670a30b..ed147eb92b649f 100644 --- a/src/quic/node_quic.cc +++ b/src/quic/node_quic.cc @@ -55,19 +55,16 @@ void QuicSetCallbacks(const FunctionCallbackInfo& args) { } while (0) SETFUNCTION("onSocketClose", socket_close); - SETFUNCTION("onSocketError", socket_error); SETFUNCTION("onSessionReady", session_ready); SETFUNCTION("onSessionCert", session_cert); SETFUNCTION("onSessionClientHello", session_client_hello); SETFUNCTION("onSessionClose", session_close); SETFUNCTION("onSessionDestroyed", session_destroyed); - SETFUNCTION("onSessionError", session_error); SETFUNCTION("onSessionHandshake", session_handshake); SETFUNCTION("onSessionKeylog", session_keylog); SETFUNCTION("onSessionUsePreferredAddress", session_use_preferred_address); SETFUNCTION("onSessionPathValidation", session_path_validation); SETFUNCTION("onSessionQlog", session_qlog); - SETFUNCTION("onSessionSilentClose", session_silent_close); SETFUNCTION("onSessionStatus", session_status); SETFUNCTION("onSessionTicket", session_ticket); SETFUNCTION("onSessionVersionNegotiation", session_version_negotiation); @@ -192,7 +189,6 @@ void Initialize(Local target, V(NGTCP2_APP_NOERROR) \ V(NGTCP2_PATH_VALIDATION_RESULT_FAILURE) \ V(NGTCP2_PATH_VALIDATION_RESULT_SUCCESS) \ - V(NGTCP2_DEFAULT_MAX_PKTLEN) \ V(NGTCP2_CC_ALGO_CUBIC) \ V(NGTCP2_CC_ALGO_RENO) \ V(QUIC_ERROR_APPLICATION) \ @@ -235,11 +231,29 @@ void Initialize(Local target, QUIC_CONSTANTS(V) #undef V + NODE_DEFINE_CONSTANT(constants, NGTCP2_DEFAULT_MAX_PKTLEN); NODE_DEFINE_CONSTANT(constants, NGTCP2_PROTO_VER); NODE_DEFINE_CONSTANT(constants, NGTCP2_DEFAULT_MAX_ACK_DELAY); NODE_DEFINE_CONSTANT(constants, NGTCP2_MAX_CIDLEN); NODE_DEFINE_CONSTANT(constants, NGTCP2_MIN_CIDLEN); + NODE_DEFINE_CONSTANT(constants, NGTCP2_NO_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_INTERNAL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_CONNECTION_REFUSED); + NODE_DEFINE_CONSTANT(constants, NGTCP2_FLOW_CONTROL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_STREAM_LIMIT_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_STREAM_STATE_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_FINAL_SIZE_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_FRAME_ENCODING_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_TRANSPORT_PARAMETER_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_CONNECTION_ID_LIMIT_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_PROTOCOL_VIOLATION); + NODE_DEFINE_CONSTANT(constants, NGTCP2_INVALID_TOKEN); + NODE_DEFINE_CONSTANT(constants, NGTCP2_APPLICATION_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_CRYPTO_BUFFER_EXCEEDED); + NODE_DEFINE_CONSTANT(constants, NGTCP2_KEY_UPDATE_ERROR); + NODE_DEFINE_CONSTANT(constants, NGTCP2_CRYPTO_ERROR); + NODE_DEFINE_CONSTANT(constants, AF_INET); NODE_DEFINE_CONSTANT(constants, AF_INET6); NODE_DEFINE_STRING_CONSTANT(constants, diff --git a/src/quic/node_quic_session.cc b/src/quic/node_quic_session.cc index 027b5c9ae6abc4..d058035a0b7896 100644 --- a/src/quic/node_quic_session.cc +++ b/src/quic/node_quic_session.cc @@ -293,9 +293,9 @@ void QuicSessionListener::OnSessionDestroyed() { previous_listener_->OnSessionDestroyed(); } -void QuicSessionListener::OnSessionClose(QuicError error) { +void QuicSessionListener::OnSessionClose(QuicError error, int flags) { if (previous_listener_ != nullptr) - previous_listener_->OnSessionClose(error); + previous_listener_->OnSessionClose(error, flags); } void QuicSessionListener::OnStreamReady(BaseObjectPtr stream) { @@ -328,13 +328,6 @@ void QuicSessionListener::OnStreamBlocked(int64_t stream_id) { } } -void QuicSessionListener::OnSessionSilentClose( - bool stateless_reset, - QuicError error) { - if (previous_listener_ != nullptr) - previous_listener_->OnSessionSilentClose(stateless_reset, error); -} - void QuicSessionListener::OnUsePreferredAddress( int family, const PreferredAddress& preferred_address) { @@ -525,14 +518,20 @@ void JSQuicSessionListener::OnSessionDestroyed() { env->quic_on_session_destroyed_function(), 0, nullptr); } -void JSQuicSessionListener::OnSessionClose(QuicError error) { +void JSQuicSessionListener::OnSessionClose(QuicError error, int flags) { Environment* env = session()->env(); HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); Local argv[] = { Number::New(env->isolate(), static_cast(error.code)), - Integer::New(env->isolate(), error.family) + Integer::New(env->isolate(), error.family), + flags & SESSION_CLOSE_FLAG_SILENT + ? v8::True(env->isolate()) + : v8::False(env->isolate()), + flags & SESSION_CLOSE_FLAG_STATELESS_RESET + ? v8::True(env->isolate()) + : v8::False(env->isolate()) }; // Grab a shared pointer to this to prevent the QuicSession @@ -664,26 +663,6 @@ void JSQuicSessionListener::OnSessionTicket(int size, SSL_SESSION* sess) { arraysize(argv), argv); } -void JSQuicSessionListener::OnSessionSilentClose( - bool stateless_reset, - QuicError error) { - Environment* env = session()->env(); - HandleScope scope(env->isolate()); - Context::Scope context_scope(env->context()); - - Local argv[] = { - stateless_reset ? v8::True(env->isolate()) : v8::False(env->isolate()), - Number::New(env->isolate(), static_cast(error.code)), - Integer::New(env->isolate(), error.family) - }; - - // Grab a shared pointer to this to prevent the QuicSession - // from being freed while the MakeCallback is running. - BaseObjectPtr ptr(session()); - session()->MakeCallback( - env->quic_on_session_silent_close_function(), arraysize(argv), argv); -} - void JSQuicSessionListener::OnUsePreferredAddress( int family, const PreferredAddress& preferred_address) { @@ -2323,7 +2302,7 @@ bool QuicSession::set_socket(QuicSocket* socket, bool nat_rebinding) { socket->ReceiveStart(); // Step 4: Update ngtcp2 - auto& local_address = socket->local_address(); + auto local_address = socket->local_address(); if (nat_rebinding) { ngtcp2_addr addr; ngtcp2_addr_init( @@ -2377,7 +2356,10 @@ void QuicSession::SilentClose() { err.code, is_stateless_reset() ? "yes" : "no"); - listener()->OnSessionSilentClose(is_stateless_reset(), err); + int flags = QuicSessionListener::SESSION_CLOSE_FLAG_SILENT; + if (is_stateless_reset()) + flags |= QuicSessionListener::SESSION_CLOSE_FLAG_STATELESS_RESET; + listener()->OnSessionClose(err, flags); } // Begin connection close by serializing the CONNECTION_CLOSE packet. // There are two variants: one to serialize an application close, the diff --git a/src/quic/node_quic_session.h b/src/quic/node_quic_session.h index f27aaabdd98ef1..78fb7ee0a30540 100644 --- a/src/quic/node_quic_session.h +++ b/src/quic/node_quic_session.h @@ -260,6 +260,12 @@ struct QuicSessionStatsTraits { class QuicSessionListener { public: + enum SessionCloseFlags { + SESSION_CLOSE_FLAG_NONE, + SESSION_CLOSE_FLAG_SILENT, + SESSION_CLOSE_FLAG_STATELESS_RESET + }; + virtual ~QuicSessionListener(); virtual void OnKeylog(const char* str, size_t size); @@ -280,7 +286,9 @@ class QuicSessionListener { int64_t stream_id, uint64_t app_error_code); virtual void OnSessionDestroyed(); - virtual void OnSessionClose(QuicError error); + virtual void OnSessionClose( + QuicError error, + int flags = SESSION_CLOSE_FLAG_NONE); virtual void OnStreamReady(BaseObjectPtr stream); virtual void OnHandshakeCompleted(); virtual void OnPathValidation( @@ -291,9 +299,6 @@ class QuicSessionListener { int family, const PreferredAddress& preferred_address); virtual void OnSessionTicket(int size, SSL_SESSION* session); - virtual void OnSessionSilentClose( - bool stateless_reset, - QuicError error); virtual void OnStreamBlocked(int64_t stream_id); virtual void OnVersionNegotiation( uint32_t supported_version, @@ -329,7 +334,9 @@ class JSQuicSessionListener : public QuicSessionListener { int64_t stream_id, uint64_t app_error_code) override; void OnSessionDestroyed() override; - void OnSessionClose(QuicError error) override; + void OnSessionClose( + QuicError error, + int flags = SESSION_CLOSE_FLAG_NONE) override; void OnStreamReady(BaseObjectPtr stream) override; void OnHandshakeCompleted() override; void OnPathValidation( @@ -337,7 +344,6 @@ class JSQuicSessionListener : public QuicSessionListener { const sockaddr* local, const sockaddr* remote) override; void OnSessionTicket(int size, SSL_SESSION* session) override; - void OnSessionSilentClose(bool stateless_reset, QuicError error) override; void OnUsePreferredAddress( int family, const PreferredAddress& preferred_address) override; diff --git a/src/quic/node_quic_socket-inl.h b/src/quic/node_quic_socket-inl.h index 8156fd04ad7bf5..38f3ad927180f0 100644 --- a/src/quic/node_quic_socket-inl.h +++ b/src/quic/node_quic_socket-inl.h @@ -79,8 +79,8 @@ void QuicSocket::AssociateStatelessResetToken( token_map_[token] = session; } -const SocketAddress& QuicSocket::local_address() { - CHECK(preferred_endpoint_); +SocketAddress QuicSocket::local_address() const { + DCHECK(preferred_endpoint_); return preferred_endpoint_->local_address(); } @@ -221,10 +221,6 @@ void QuicSocket::AddEndpoint( endpoint_->ReceiveStart(); } -void QuicSocket::SessionReady(BaseObjectPtr session) { - listener_->OnSessionReady(session); -} - } // namespace quic } // namespace node diff --git a/src/quic/node_quic_socket.cc b/src/quic/node_quic_socket.cc index ce7c5820b2d7b1..e3112887d8e4fb 100644 --- a/src/quic/node_quic_socket.cc +++ b/src/quic/node_quic_socket.cc @@ -83,10 +83,10 @@ bool IsShortHeader( } } // namespace -QuicPacket::QuicPacket(const char* diagnostic_label, size_t len) : - data_{0}, - len_(len), - diagnostic_label_(diagnostic_label) { +QuicPacket::QuicPacket(const char* diagnostic_label, size_t len) + : data_{0}, + len_(len), + diagnostic_label_(diagnostic_label) { CHECK_LE(len, MAX_PKTLEN); } @@ -100,8 +100,6 @@ const char* QuicPacket::diagnostic_label() const { diagnostic_label_ : "unspecified"; } -void QuicPacket::MemoryInfo(MemoryTracker* tracker) const {} - QuicSocketListener::~QuicSocketListener() { if (socket_) socket_->RemoveListener(this); @@ -137,7 +135,7 @@ void JSQuicSocketListener::OnError(ssize_t code) { HandleScope scope(env->isolate()); Context::Scope context_scope(env->context()); Local arg = Number::New(env->isolate(), static_cast(code)); - socket()->MakeCallback(env->quic_on_socket_error_function(), 1, &arg); + socket()->MakeCallback(env->quic_on_socket_close_function(), 1, &arg); } void JSQuicSocketListener::OnSessionReady(BaseObjectPtr session) { @@ -174,10 +172,10 @@ QuicEndpoint::QuicEndpoint( QuicState* quic_state, Local wrap, QuicSocket* listener, - Local udp_wrap) : - BaseObject(quic_state->env(), wrap), - listener_(listener), - quic_state_(quic_state) { + Local udp_wrap) + : BaseObject(quic_state->env(), wrap), + listener_(listener), + quic_state_(quic_state) { MakeWeak(); udp_ = static_cast( udp_wrap->GetAlignedPointerFromInternalField( @@ -187,7 +185,9 @@ QuicEndpoint::QuicEndpoint( strong_ptr_.reset(udp_->GetAsyncWrap()); } -void QuicEndpoint::MemoryInfo(MemoryTracker* tracker) const {} +QuicEndpoint::~QuicEndpoint() { + udp_->set_listener(nullptr); +} uv_buf_t QuicEndpoint::OnAlloc(size_t suggested_size) { return AllocatedBuffer::AllocateManaged(env(), suggested_size).release(); @@ -229,6 +229,14 @@ void QuicEndpoint::OnAfterBind() { listener_->OnBind(this); } +template +void QuicSocketStatsTraits::ToString(const QuicSocket& ptr, Fn&& add_field) { +#define V(_n, name, label) \ + add_field(label, ptr.GetStat(&QuicSocketStats::name)); + SOCKET_STATS(V) +#undef V +} + QuicSocket::QuicSocket( QuicState* quic_state, Local wrap, @@ -240,17 +248,17 @@ QuicSocket::QuicSocket( QlogMode qlog, const uint8_t* session_reset_secret, bool disable_stateless_reset) - : AsyncWrap(quic_state->env(), wrap, AsyncWrap::PROVIDER_QUICSOCKET), - StatsBase(quic_state->env(), wrap), - alloc_info_(MakeAllocator()), - options_(options), - max_connections_(max_connections), - max_connections_per_host_(max_connections_per_host), - max_stateless_resets_per_host_(max_stateless_resets_per_host), - retry_token_expiration_(retry_token_expiration), - qlog_(qlog), - server_alpn_(NGHTTP3_ALPN_H3), - quic_state_(quic_state) { + : AsyncWrap(quic_state->env(), wrap, AsyncWrap::PROVIDER_QUICSOCKET), + StatsBase(quic_state->env(), wrap), + alloc_info_(MakeAllocator()), + options_(options), + max_connections_(max_connections), + max_connections_per_host_(max_connections_per_host), + max_stateless_resets_per_host_(max_stateless_resets_per_host), + retry_token_expiration_(retry_token_expiration), + qlog_(qlog), + server_alpn_(NGHTTP3_ALPN_H3), + quic_state_(quic_state) { MakeWeak(); PushListener(&default_listener_); @@ -279,15 +287,13 @@ QuicSocket::~QuicSocket() { if (listener == listener_) RemoveListener(listener_); - DebugStats(); -} + // In a clean shutdown, all QuicSessions associated with the QuicSocket + // would have been destroyed explicitly. However, if the QuicSocket is + // garbage collected / freed before Destroy having been called, there + // may be sessions remaining. This is not really a good thing. + Debug(this, "Destroying with %d sessions remaining", sessions_.size()); -template -void QuicSocketStatsTraits::ToString(const QuicSocket& ptr, Fn&& add_field) { -#define V(_n, name, label) \ - add_field(label, ptr.GetStat(&QuicSocketStats::name)); - SOCKET_STATS(V) -#undef V + DebugStats(); } void QuicSocket::MemoryInfo(MemoryTracker* tracker) const { @@ -310,7 +316,6 @@ void QuicSocket::Listen( const std::string& alpn, uint32_t options) { CHECK(sc); - CHECK(!server_secure_context_); CHECK(!is_flag_set(QUICSOCKET_FLAGS_SERVER_LISTENING)); Debug(this, "Starting to listen"); server_session_config_.Set(quic_state(), preferred_address); @@ -323,6 +328,7 @@ void QuicSocket::Listen( } void QuicSocket::OnError(QuicEndpoint* endpoint, ssize_t error) { + // TODO(@jasnell): What should we do with the endpoint? Debug(this, "Reading data from UDP socket failed. Error %" PRId64, error); listener_->OnError(error); } @@ -341,7 +347,7 @@ void QuicSocket::OnEndpointDone(QuicEndpoint* endpoint) { } void QuicSocket::OnBind(QuicEndpoint* endpoint) { - const SocketAddress& local_address = endpoint->local_address(); + SocketAddress local_address = endpoint->local_address(); bound_endpoints_[local_address] = BaseObjectWeakPtr(endpoint); Debug(this, "Endpoint %s bound", local_address); @@ -545,6 +551,13 @@ void QuicSocket::OnReceive( IncrementStat(&QuicSocketStats::packets_ignored); return; } + + // The QuicSession was destroyed while it was being set up. There's + // no further processing we can do here. + if (session->is_destroyed()) { + IncrementStat(&QuicSocketStats::packets_ignored); + return; + } } CHECK(session); @@ -683,6 +696,8 @@ bool QuicSocket::SendRetry( } // Shutdown a connection prematurely, before a QuicSession is created. +// This should only be called t the start of a session before the crypto +// keys have been established. void QuicSocket::ImmediateConnectionClose( const QuicCID& scid, const QuicCID& dcid, @@ -819,6 +834,18 @@ BaseObjectPtr QuicSocket::AcceptInitialPacket( listener_->OnSessionReady(session); + // It's possible that the session was destroyed while processing + // the ready callback. If it was, then we need to send an early + // CONNECTION_CLOSE. + if (session->is_destroyed()) { + ImmediateConnectionClose( + QuicCID(hd.scid), + QuicCID(hd.dcid), + local_addr, + remote_addr, + NGTCP2_CONNECTION_REFUSED); + } + return session; } @@ -826,9 +853,9 @@ QuicSocket::SendWrap::SendWrap( QuicState* quic_state, Local req_wrap_obj, size_t total_length) - : ReqWrap(quic_state->env(), req_wrap_obj, PROVIDER_QUICSOCKET), - total_length_(total_length), - quic_state_(quic_state) { + : ReqWrap(quic_state->env(), req_wrap_obj, PROVIDER_QUICSOCKET), + total_length_(total_length), + quic_state_(quic_state) { } std::string QuicSocket::SendWrap::MemoryInfoName() const { @@ -1093,7 +1120,7 @@ void QuicSocketStopListening(const FunctionCallbackInfo& args) { socket->StopListening(); } -void QuicSocketset_server_busy(const FunctionCallbackInfo& args) { +void QuicSocketSetServerBusy(const FunctionCallbackInfo& args) { QuicSocket* socket; ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); CHECK_EQ(args.Length(), 1); @@ -1164,7 +1191,7 @@ void QuicSocket::Initialize( QuicSocketSetDiagnosticPacketLoss); env->SetProtoMethod(socket, "setServerBusy", - QuicSocketset_server_busy); + QuicSocketSetServerBusy); env->SetProtoMethod(socket, "stopListening", QuicSocketStopListening); diff --git a/src/quic/node_quic_socket.h b/src/quic/node_quic_socket.h index 08ccd74dcb52f0..2fcf0fb7f19d31 100644 --- a/src/quic/node_quic_socket.h +++ b/src/quic/node_quic_socket.h @@ -33,6 +33,9 @@ using v8::Value; namespace quic { +class QuicSocket; +class QuicEndpoint; + enum QuicSocketOptions : uint32_t { // When enabled the QuicSocket will validate the address // using a RETRY packet to the peer. @@ -80,9 +83,6 @@ struct QuicSocketStatsTraits { static void ToString(const Base& ptr, Fn&& add_field); }; -class QuicSocket; -class QuicEndpoint; - // This is the generic interface for objects that control QuicSocket // instances. The default `JSQuicSocketListener` emits events to // JavaScript @@ -96,7 +96,7 @@ class QuicSocketListener { virtual void OnEndpointDone(QuicEndpoint* endpoint); virtual void OnDestroy(); - QuicSocket* socket() { return socket_.get(); } + QuicSocket* socket() const { return socket_.get(); } private: BaseObjectWeakPtr socket_; @@ -104,7 +104,7 @@ class QuicSocketListener { friend class QuicSocket; }; -class JSQuicSocketListener : public QuicSocketListener { +class JSQuicSocketListener final : public QuicSocketListener { public: void OnError(ssize_t code) override; void OnSessionReady(BaseObjectPtr session) override; @@ -121,17 +121,21 @@ constexpr size_t MAX_PKTLEN = std::max(NGTCP2_MAX_PKTLEN_IPV4, NGTCP2_MAX_PKTLEN_IPV6); // A serialized QuicPacket to be sent by a QuicSocket instance. +// QuicPackets are intended to be transient. They are created, +// filled with the contents of a serialized packet, and passed +// off immediately to the QuicSocket to be sent. As soon as +// the packet is sent, it is freed. class QuicPacket : public MemoryRetainer { public: // Creates a new QuicPacket. By default the packet will be // stack allocated with a max size of NGTCP2_MAX_PKTLEN_IPV4. // If a larger packet size is specified, it will be heap - // allocated. Generally speaking, a QUIC packet should never - // be larger than the current MTU to avoid IP fragmentation. + // allocated. A QUIC packet should never be larger than the + // current MTU to avoid IP fragmentation. // - // The content of a QuicPacket is provided by ngtcp2. The - // typical use pattern is to create a QuicPacket instance - // and then pass a pointer to it's internal buffer and max + // The content of a QuicPacket is provided by ngtcp2 and is + // opaque for us. The typical use pattern is to create a QuicPacket + // instance and then pass a pointer to it's internal buffer and max // size in to an ngtcp2 function that serializes the data. // ngtcp2 will fill the buffer as much as possible then return // the number of bytes serialized. User code is then responsible @@ -159,17 +163,22 @@ class QuicPacket : public MemoryRetainer { QuicPacket(const char* diagnostic_label, size_t len); QuicPacket(const QuicPacket& other); + uint8_t* data() { return data_; } + size_t length() const { return len_; } + uv_buf_t buf() const { return uv_buf_init( const_cast(reinterpret_cast(&data_)), length()); } + inline void set_length(size_t len); + const char* diagnostic_label() const; - void MemoryInfo(MemoryTracker* tracker) const override; + SET_NO_MEMORY_INFO(); SET_MEMORY_INFO_NAME(QuicPacket); SET_SELF_SIZE(QuicPacket); @@ -198,8 +207,8 @@ class QuicEndpointListener { // A QuicEndpoint wraps a UDPBaseWrap. A single QuicSocket may // have multiple QuicEndpoints, the lifecycles of which are // attached to the QuicSocket. -class QuicEndpoint : public BaseObject, - public UDPListener { +class QuicEndpoint final : public BaseObject, + public UDPListener { public: static void Initialize( Environment* env, @@ -212,9 +221,10 @@ class QuicEndpoint : public BaseObject, QuicSocket* listener, Local udp_wrap); - const SocketAddress& local_address() const { - local_address_ = udp_->GetSockName(); - return local_address_; + ~QuicEndpoint() override; + + SocketAddress local_address() const { + return udp_->GetSockName(); } // Implementation for UDPListener @@ -242,17 +252,16 @@ class QuicEndpoint : public BaseObject, void IncrementPendingCallbacks() { pending_callbacks_++; } void DecrementPendingCallbacks() { pending_callbacks_--; } - bool has_pending_callbacks() { return pending_callbacks_ > 0; } + bool has_pending_callbacks() const { return pending_callbacks_ > 0; } inline void WaitForPendingCallbacks(); QuicState* quic_state() const { return quic_state_.get(); } - void MemoryInfo(MemoryTracker* tracker) const override; + SET_NO_MEMORY_INFO(); SET_MEMORY_INFO_NAME(QuicEndpoint) SET_SELF_SIZE(QuicEndpoint) private: - mutable SocketAddress local_address_; BaseObjectWeakPtr listener_; UDPWrapBase* udp_; BaseObjectPtr strong_ptr_; @@ -298,7 +307,7 @@ class QuicSocket : public AsyncWrap, // Returns the default/preferred local address. Additional // QuicEndpoint instances may be associated with the // QuicSocket bound to other local addresses. - inline const SocketAddress& local_address(); + inline SocketAddress local_address() const; void MaybeClose(); @@ -342,8 +351,6 @@ class QuicSocket : public AsyncWrap, std::unique_ptr packet, BaseObjectPtr session = BaseObjectPtr()); - inline void SessionReady(BaseObjectPtr session); - inline void set_server_busy(bool on); inline void set_diagnostic_packet_loss(double rx = 0.0, double tx = 0.0); diff --git a/src/quic/node_quic_stream-inl.h b/src/quic/node_quic_stream-inl.h index 3da0f5fb3b57cf..3dd1fc216d9783 100644 --- a/src/quic/node_quic_stream-inl.h +++ b/src/quic/node_quic_stream-inl.h @@ -35,7 +35,12 @@ void QuicStream::set_flag(int32_t flag, bool on) { } void QuicStream::set_final_size(uint64_t final_size) { - CHECK_EQ(GetStat(&QuicStreamStats::final_size), 0); + // Only set the final size once. + if (is_flag_set(QUICSTREAM_FLAG_FIN)) { + CHECK_LE(final_size, GetStat(&QuicStreamStats::final_size)); + return; + } + set_flag(QUICSTREAM_FLAG_FIN); SetStat(&QuicStreamStats::final_size, final_size); } diff --git a/src/quic/node_quic_stream.cc b/src/quic/node_quic_stream.cc index 7a1054db407f59..ce8cc78a1ec8c5 100644 --- a/src/quic/node_quic_stream.cc +++ b/src/quic/node_quic_stream.cc @@ -355,7 +355,6 @@ void QuicStream::ReceiveData( // When fin != 0, we've received that last chunk of data for this // stream, indicating that the stream will no longer be readable. if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) { - set_flag(QUICSTREAM_FLAG_FIN); set_final_size(offset + datalen); EmitRead(UV_EOF); } diff --git a/src/quic/node_quic_stream.h b/src/quic/node_quic_stream.h index 97174dcb7b925d..d8297f300ba85c 100644 --- a/src/quic/node_quic_stream.h +++ b/src/quic/node_quic_stream.h @@ -256,7 +256,11 @@ class QuicStream : public AsyncWrap, // Specifies the kind of headers currently being processed. inline void set_headers_kind(QuicStreamHeadersKind kind); - // Set the final size for the QuicStream + // Set the final size for the QuicStream. 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. inline void set_final_size(uint64_t final_size); // The final size is the maximum amount of data that has been diff --git a/test/parallel/test-quic-binding.js b/test/parallel/test-quic-binding.js index 6f28944664ffc9..2044ed43b48e53 100644 --- a/test/parallel/test-quic-binding.js +++ b/test/parallel/test-quic-binding.js @@ -20,6 +20,23 @@ assert(quic.constants); assert.strictEqual(quic.constants.NGTCP2_PROTO_VER.toString(16), 'ff00001d'); assert.strictEqual(quic.constants.NGHTTP3_ALPN_H3, '\u0005h3-29'); +assert.strictEqual(quic.constants.NGTCP2_NO_ERROR, 0); +assert.strictEqual(quic.constants.NGTCP2_INTERNAL_ERROR, 1); +assert.strictEqual(quic.constants.NGTCP2_CONNECTION_REFUSED, 2); +assert.strictEqual(quic.constants.NGTCP2_FLOW_CONTROL_ERROR, 3); +assert.strictEqual(quic.constants.NGTCP2_STREAM_LIMIT_ERROR, 4); +assert.strictEqual(quic.constants.NGTCP2_STREAM_STATE_ERROR, 5); +assert.strictEqual(quic.constants.NGTCP2_FINAL_SIZE_ERROR, 6); +assert.strictEqual(quic.constants.NGTCP2_FRAME_ENCODING_ERROR, 7); +assert.strictEqual(quic.constants.NGTCP2_TRANSPORT_PARAMETER_ERROR, 8); +assert.strictEqual(quic.constants.NGTCP2_CONNECTION_ID_LIMIT_ERROR, 9); +assert.strictEqual(quic.constants.NGTCP2_PROTOCOL_VIOLATION, 0xa); +assert.strictEqual(quic.constants.NGTCP2_INVALID_TOKEN, 0xb); +assert.strictEqual(quic.constants.NGTCP2_APPLICATION_ERROR, 0xc); +assert.strictEqual(quic.constants.NGTCP2_CRYPTO_BUFFER_EXCEEDED, 0xd); +assert.strictEqual(quic.constants.NGTCP2_KEY_UPDATE_ERROR, 0xe); +assert.strictEqual(quic.constants.NGTCP2_CRYPTO_ERROR, 0x100); + // The following just tests for the presence of things we absolutely need. // They don't test the functionality of those things.