Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2020-07-10 02:31:41 +0300
committerJames M Snell <jasnell@gmail.com>2020-07-16 03:16:48 +0300
commit53b12f0c7bf0df695c9640adccead3c9142552d5 (patch)
treec42aaafa4a813705cb557508c3d887b6652d01b5 /lib/internal/quic
parent16b32eae3e76348c954e46965739fbfb91d097bd (diff)
quic: implement QuicEndpoint Promise API
This is the start of a conversion over to a fully Promise-centric API for the QUIC implementation. PR-URL: https://github.com/nodejs/node/pull/34283 Reviewed-By: Anna Henningsen <anna@addaleax.net>
Diffstat (limited to 'lib/internal/quic')
-rw-r--r--lib/internal/quic/core.js160
1 files changed, 148 insertions, 12 deletions
diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js
index 65a204decc3..ab979ec8b92 100644
--- a/lib/internal/quic/core.js
+++ b/lib/internal/quic/core.js
@@ -16,6 +16,7 @@ const {
Error,
Map,
Number,
+ Promise,
RegExp,
Set,
Symbol,
@@ -104,6 +105,7 @@ const {
ERR_QUICSESSION_VERSION_NEGOTIATION,
ERR_TLS_DH_PARAM_SIZE,
},
+ hideStackFrames,
errnoException,
exceptionWithHostPort
} = require('internal/errors');
@@ -200,10 +202,14 @@ const {
const emit = EventEmitter.prototype.emit;
+// TODO(@jasnell): Temporary while converting to Promises-based API
+const { lookup } = require('dns').promises;
+
const kAfterLookup = Symbol('kAfterLookup');
const kAfterPreferredAddressLookup = Symbol('kAfterPreferredAddressLookup');
const kAddSession = Symbol('kAddSession');
const kAddStream = Symbol('kAddStream');
+const kBind = Symbol('kBind');
const kClose = Symbol('kClose');
const kCert = Symbol('kCert');
const kClientHello = Symbol('kClientHello');
@@ -255,6 +261,14 @@ const kSocketDestroyed = 4;
let diagnosticPacketLossWarned = false;
let warnedVerifyHostnameIdentity = false;
+let DOMException;
+
+const lazyDOMException = hideStackFrames((message) => {
+ if (DOMException === undefined)
+ DOMException = internalBinding('messaging').DOMException;
+ return new DOMException(message);
+});
+
assert(process.versions.ngtcp2 !== undefined);
// Called by the C++ internals when the QuicSocket is closed with
@@ -589,12 +603,27 @@ function lookupOrDefault(lookup, type) {
return lookup || (type === AF_INET6 ? lookup6 : lookup4);
}
+function deferredClosePromise(state) {
+ return state.closePromise = new Promise((resolve, reject) => {
+ state.closePromiseResolve = resolve;
+ state.closePromiseReject = reject;
+ }).finally(() => {
+ state.closePromise = undefined;
+ state.closePromiseResolve = undefined;
+ state.closePromiseReject = undefined;
+ });
+}
+
// QuicEndpoint wraps a UDP socket and is owned
// by a QuicSocket. It does not exist independently
// of the QuicSocket.
class QuicEndpoint {
[kInternalState] = {
state: kSocketUnbound,
+ bindPromise: undefined,
+ closePromise: undefined,
+ closePromiseResolve: undefined,
+ closePromiseReject: undefined,
socket: undefined,
udpSocket: undefined,
address: undefined,
@@ -645,15 +674,14 @@ class QuicEndpoint {
return customInspect(this, {
address: this.address,
fd: this.fd,
- type: this[kInternalState].type === AF_INET6 ? 'udp6' : 'udp4'
+ type: this[kInternalState].type === AF_INET6 ? 'udp6' : 'udp4',
+ destroyed: this.destroyed,
+ bound: this.bound,
+ pending: this.pending,
}, depth, options);
}
- // afterLookup is invoked when binding a QuicEndpoint. The first
- // step to binding is to resolve the given hostname into an ip
- // address. Once resolution is complete, the ip address needs to
- // be passed on to the [kContinueBind] function or the QuicEndpoint
- // needs to be destroyed.
+ // TODO(@jasnell): Remove once migration to Promise API is complete
static [kAfterLookup](err, ip) {
if (err) {
this.destroy(err);
@@ -662,10 +690,7 @@ class QuicEndpoint {
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.
+ // TODO(@jasnell): Remove once migration to Promise API is complete
[kMaybeBind]() {
const state = this[kInternalState];
if (state.state !== kSocketUnbound)
@@ -674,8 +699,7 @@ class QuicEndpoint {
state.lookup(state.address, QuicEndpoint[kAfterLookup].bind(this));
}
- // IP address resolution is completed and we're ready to finish
- // binding to the local port.
+ // TODO(@jasnell): Remove once migration to Promise API is complete
[kContinueBind](ip) {
const state = this[kInternalState];
const udpHandle = state.udpSocket[internalDgram.kStateSymbol].handle;
@@ -704,6 +728,95 @@ class QuicEndpoint {
state.socket[kEndpointBound](this);
}
+ bind(options) {
+ const state = this[kInternalState];
+ if (state.bindPromise !== undefined)
+ return state.bindPromise;
+
+ return state.bindPromise = this[kBind]().finally(() => {
+ state.bindPromise = undefined;
+ });
+ }
+
+ // Binds the QuicEndpoint to the local port. Returns a Promise
+ // that is resolved once the QuicEndpoint binds, or rejects if
+ // binding was not successful. Calling bind() multiple times
+ // before the Promise is resolved will return the same Promise.
+ // Calling bind() after the endpoint is already bound will
+ // immediately return a resolved promise. Calling bind() after
+ // the endpoint has been destroyed will cause the Promise to
+ // be rejected.
+ async [kBind](options) {
+ const state = this[kInternalState];
+ if (this.destroyed)
+ throw new ERR_INVALID_STATE('QuicEndpoint is already destroyed');
+
+ if (state.state !== kSocketUnbound)
+ return this.address;
+
+ const { signal } = { ...options };
+ if (signal != null && !('aborted' in signal))
+ throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);
+
+ // If an AbotSignal was passed in, check to make sure it is not already
+ // aborted before we continue on to do any work.
+ if (signal && signal.aborted)
+ throw new lazyDOMException('AbortError');
+
+ state.state = kSocketPending;
+
+ // TODO(@jasnell): Use passed in lookup function once everything
+ // has been converted to Promises-based API
+ const {
+ address: ip
+ } = await lookup(state.address, state.type === AF_INET6 ? 6 : 4);
+
+ // It's possible for the QuicEndpoint to have been destroyed while
+ // we were waiting for the DNS lookup to complete. If so, reject
+ // the Promise.
+ if (this.destroyed)
+ throw new ERR_INVALID_STATE('QuicEndpoint was destroyed');
+
+ // If an AbortSignal was passed in, check to see if it was triggered
+ // while we were waiting.
+ if (signal && signal.aborted) {
+ state.state = kSocketUnbound;
+ throw new lazyDOMException('AbortError');
+ }
+
+ // From here on, any errors are fatal for the QuicEndpoint. Keep in
+ // mind that this means that the Bind Promise will be rejected *and*
+ // the QuicEndpoint will be destroyed with an error.
+ try {
+ const udpHandle = state.udpSocket[internalDgram.kStateSymbol].handle;
+ if (udpHandle == null) {
+ // It's not clear what cases trigger this but it is possible.
+ throw new ERR_OPERATION_FAILED('Acquiring UDP socket handle failed');
+ }
+
+ const flags =
+ (state.reuseAddr ? UV_UDP_REUSEADDR : 0) |
+ (state.ipv6Only ? UV_UDP_IPV6ONLY : 0);
+
+ const ret = udpHandle.bind(ip, state.port, flags);
+ if (ret)
+ throw exceptionWithHostPort(ret, 'bind', ip, state.port);
+
+ // On Windows, the fd will be meaningless, but we always record it.
+ state.fd = udpHandle.fd;
+ state.state = kSocketBound;
+
+ // Notify the owning socket that the QuicEndpoint has been successfully
+ // bound to the local UDP port.
+ state.socket[kEndpointBound](this);
+
+ return this.address;
+ } catch (error) {
+ this.destroy(error);
+ throw error;
+ }
+ }
+
destroy(error) {
if (this.destroyed)
return;
@@ -727,12 +840,35 @@ class QuicEndpoint {
handle.ondone = () => {
state.udpSocket.close((err) => {
if (err) error = err;
+ if (error && typeof state.closePromiseReject === 'function')
+ state.closePromiseReject(error);
+ else if (typeof state.closePromiseResolve === 'function')
+ state.closePromiseResolve();
state.socket[kEndpointClose](this, error);
});
};
handle.waitForPendingCallbacks();
}
+ // Closes the QuicEndpoint. Returns a Promise that is resolved
+ // once the QuicEndpoint closes, or rejects if it closes with
+ // an error. Calling close() multiple times before the Promise
+ // is resolved will return the same Promise. Calling close()
+ // after will return a rejected Promise.
+ close() {
+ return this[kInternalState].closePromise || this[kClose]();
+ }
+
+ [kClose]() {
+ if (this.destroyed) {
+ return Promise.reject(
+ new ERR_INVALID_STATE('QuicEndpoint is already destroyed'));
+ }
+ const promise = deferredClosePromise(this[kInternalState]);
+ this.destroy();
+ return promise;
+ }
+
// If the QuicEndpoint is bound, returns an object detailing
// the local IP address, port, and address type to which it
// is bound. Otherwise, returns an empty object.