From b5fe31ef19a5852fe806675f1c56436c7d36bd8f Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 30 Jun 2020 11:21:03 -0700 Subject: [PATCH] quic: avoid using private JS fields for now They introduce a significant performance bottleneck at the moment. PR-URL: https://github.com/nodejs/node/pull/34137 Reviewed-By: Anna Henningsen Reviewed-By: David Carlier --- lib/internal/quic/core.js | 977 +++++++++++++++++++++----------------- 1 file changed, 530 insertions(+), 447 deletions(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index f7d801acb067ee..a052ba6e2f3d03 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -208,13 +208,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 +226,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; @@ -614,16 +629,18 @@ function onRemoveListener(event) { // 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 { @@ -635,35 +652,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)}`; } @@ -673,29 +692,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? @@ -703,22 +724,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) { @@ -727,9 +748,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(); @@ -740,9 +762,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. @@ -757,97 +780,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); } } @@ -856,20 +884,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 { @@ -921,10 +951,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) | @@ -942,7 +974,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 @@ -955,9 +987,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, }; } @@ -970,19 +1003,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](); } @@ -990,29 +1025,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 @@ -1024,14 +1061,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)); @@ -1044,7 +1082,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); } @@ -1052,7 +1090,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; } @@ -1061,33 +1099,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, @@ -1097,13 +1136,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, @@ -1129,15 +1168,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, @@ -1148,7 +1188,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 @@ -1170,7 +1210,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, @@ -1178,19 +1218,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'); } @@ -1203,7 +1244,7 @@ class QuicSocket extends EventEmitter { throw new ERR_INVALID_CALLBACK(callback); options = { - ...this.#server, + ...state.server, ...options, minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', @@ -1219,15 +1260,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 @@ -1236,10 +1277,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 @@ -1248,7 +1289,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 @@ -1259,14 +1300,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; @@ -1274,7 +1314,7 @@ class QuicSocket extends EventEmitter { } options = { - ...this.#client, + ...state.client, ...options }; @@ -1290,10 +1330,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)); @@ -1328,7 +1368,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 @@ -1344,17 +1385,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. @@ -1367,7 +1408,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 @@ -1381,7 +1422,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); } @@ -1398,84 +1439,88 @@ 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); } @@ -1483,7 +1528,7 @@ class QuicSocket extends EventEmitter { // 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; + const stats = this[kInternalState].stats || this[kHandle].stats; return now - stats[IDX_QUIC_SOCKET_STATS_CREATED_AT]; } @@ -1491,7 +1536,7 @@ class QuicSocket extends EventEmitter { // 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; + const stats = this[kInternalState].stats || this[kHandle].stats; return now - stats[IDX_QUIC_SOCKET_STATS_BOUND_AT]; } @@ -1499,56 +1544,56 @@ class QuicSocket extends EventEmitter { // 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; + const stats = this[kInternalState].stats || this[kHandle].stats; return now - stats[IDX_QUIC_SOCKET_STATS_LISTEN_AT]; } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_BYTES_RECEIVED]; } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_BYTES_SENT]; } get packetsReceived() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_PACKETS_RECEIVED]; } get packetsSent() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_PACKETS_SENT]; } get packetsIgnored() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_PACKETS_IGNORED]; } get serverBusy() { - return this.#serverBusy; + return this[kInternalState].serverBusy; } get serverSessions() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS]; } get clientSessions() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS]; } get statelessResetCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT]; } get serverBusyCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SOCKET_STATS_SERVER_BUSY_COUNT]; } @@ -1557,7 +1602,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, @@ -1590,36 +1635,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 { @@ -1631,20 +1678,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, }; } @@ -1655,16 +1704,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](); } } @@ -1689,9 +1739,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(); } @@ -1702,21 +1753,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 @@ -1729,7 +1781,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; @@ -1740,7 +1792,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; @@ -1748,7 +1800,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; @@ -1756,16 +1808,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, @@ -1775,7 +1829,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 @@ -1788,15 +1842,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; @@ -1812,18 +1867,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(); } @@ -1841,7 +1897,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) { @@ -1853,10 +1910,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. @@ -1877,11 +1934,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 && @@ -1891,19 +1949,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); @@ -1914,20 +1972,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 @@ -1937,7 +1995,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() { @@ -1951,37 +2009,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; @@ -1995,26 +2051,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() { @@ -2035,34 +2095,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 @@ -2082,13 +2144,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; @@ -2104,8 +2166,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]) : @@ -2118,16 +2180,16 @@ 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; + const stats = this[kInternalState].stats || this[kHandle].stats; return now - stats[IDX_QUIC_SESSION_STATS_CREATED_AT]; } get handshakeDuration() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; const end = this.handshakeComplete ? stats[4] : process.hrtime.bigint(); @@ -2135,85 +2197,86 @@ class QuicSession extends EventEmitter { } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_BYTES_RECEIVED]; } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_BYTES_SENT]; } get bidiStreamCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_BIDI_STREAM_COUNT]; } get uniStreamCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_UNI_STREAM_COUNT]; } get maxInFlightBytes() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT]; } get lossRetransmitCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT]; } get ackDelayRetransmitCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT]; } get peerInitiatedStreamCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_STREAMS_IN_COUNT]; } get selfInitiatedStreamCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT]; } get keyUpdateCount() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT]; } get minRTT() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_MIN_RTT]; } get latestRTT() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_SESSION_STATS_LATEST_RTT]; } get smoothedRTT() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[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 @@ -2224,7 +2287,9 @@ class QuicSession extends EventEmitter { } class QuicServerSession extends QuicSession { - #contexts = []; + [kInternalServerState] = { + contexts: [] + }; constructor(socket, handle, options) { const { @@ -2254,11 +2319,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; @@ -2289,27 +2355,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 = { @@ -2345,27 +2414,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; @@ -2375,7 +2445,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; } @@ -2387,12 +2457,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 = @@ -2400,23 +2471,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') { @@ -2452,40 +2523,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 { @@ -2499,7 +2571,7 @@ class QuicClientSession extends QuicSession { {}; } - #setSocketAfterBind = function(socket, callback) { + [kSetSocketAfterBind](socket, callback) { if (socket.destroyed) { callback(new ERR_QUICSOCKET_DESTROYED('setSocket')); return; @@ -2518,7 +2590,7 @@ class QuicClientSession extends QuicSession { this[kSetSocket](socket); callback(); - }; + } setSocket(socket, callback) { if (!(socket instanceof QuicSocket)) @@ -2527,7 +2599,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)); } } @@ -2542,19 +2614,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 { @@ -2571,16 +2645,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(); } @@ -2592,28 +2668,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(); } @@ -2643,6 +2720,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 @@ -2653,15 +2731,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. @@ -2687,10 +2765,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, @@ -2717,15 +2796,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() { @@ -2733,14 +2812,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? @@ -2750,7 +2829,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); }); } @@ -2760,14 +2839,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 @@ -2806,19 +2885,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) { @@ -2836,10 +2916,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); @@ -2865,17 +2945,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 @@ -2884,24 +2964,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; } @@ -2911,11 +2992,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) { @@ -2923,17 +3004,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 @@ -2945,24 +3027,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'); @@ -3094,37 +3177,37 @@ class QuicStream extends Duplex { get duration() { const now = process.hrtime.bigint(); - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return now - stats[IDX_QUIC_STREAM_STATS_CREATED_AT]; } get bytesReceived() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_BYTES_RECEIVED]; } get bytesSent() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_BYTES_SENT]; } get maxExtendedOffset() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET]; } get finalSize() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_FINAL_SIZE]; } get maxAcknowledgedOffset() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_ACK]; } get maxReceivedOffset() { - const stats = this.#stats || this[kHandle].stats; + const stats = this[kInternalState].stats || this[kHandle].stats; return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_RECV]; } }