Skip to content

Commit

Permalink
quic: refactor clientHello
Browse files Browse the repository at this point in the history
Refactor the `'clientHello'` event into a `clientHelloHandler`
configuration option and async function. Remove the addContext
API as it's not needed.

PR-URL: #34541
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
  • Loading branch information
jasnell authored and gengjiawen committed Aug 3, 2020
1 parent e5dacc2 commit 4b0275a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 177 deletions.
68 changes: 36 additions & 32 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -1274,38 +1274,6 @@ The `QuicServerSession` class implements the server side of a QUIC connection.
Instances are created internally and are emitted using the `QuicSocket`
`'session'` event.

#### Event: `'clientHello'`
<!-- YAML
added: REPLACEME
-->

Emitted at the start of the TLS handshake when the `QuicServerSession` receives
the initial TLS Client Hello.

The event handler is given a callback function that *must* be invoked for the
handshake to continue.

The callback is invoked with four arguments:

* `alpn` {string} The ALPN protocol identifier requested by the client.
* `servername` {string} The SNI servername requested by the client.
* `ciphers` {string[]} The list of TLS cipher algorithms requested by the
client.
* `callback` {Function} A callback function that must be called in order for
the TLS handshake to continue.

The `'clientHello'` event will not be emitted more than once.

#### `quicserversession.addContext(servername\[, context\])`
<!-- YAML
added: REPLACEME
-->

* `servername` {string} A DNS name to associate with the given context.
* `context` {tls.SecureContext} A TLS SecureContext to associate with the `servername`.

TBD

### Class: `QuicSocket`
<!-- YAML
added: REPLACEME
Expand Down Expand Up @@ -1766,6 +1734,9 @@ added: REPLACEME
uppercased in order for OpenSSL to accept them.
* `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
client certificate.
* `clientHelloHandler` {Function} An async function that may be used to
set a {tls.SecureContext} for the given server name at the start of the
TLS handshake. See [Handling client hello][] for details.
* `crl` {string|string[]|Buffer|Buffer[]} PEM formatted CRLs (Certificate
Revocation Lists).
* `defaultEncoding` {string} The default encoding that is used when no
Expand Down Expand Up @@ -2479,6 +2450,38 @@ async function ocspClientHandler(type, { data }) {
sock.connect({ ocspHandler: ocspClientHandler });
```

### Handling client hello

When `quicsocket.listen()` is called, a {tls.SecureContext} is created and used
by default for all new `QuicServerSession` instances. There are times, however,
when the {tls.SecureContext} to be used for a `QuicSession` can only be
determined once the client initiates a connection. This is accomplished using
the `clientHelloHandler` option when calling `quicsocket.listen()`.

The value of `clientHelloHandler` is an async function that is called at the
start of a new `QuicServerSession`. It is invoked with three arguments:

* `alpn` {string} The ALPN protocol identifier specified by the client.
* `servername` {string} The SNI server name specified by the client.
* `ciphers` {string[]} The array of TLS 1.3 ciphers specified by the client.

The `clientHelloHandler` *may* return a new {tls.SecureContext} object that will
be used to continue the TLS handshake. If the function returns `undefined`, the
default {tls.SecureContext} will be used. Returning any other value will cause
an error to be thrown that will destroy the `QuicServerSession` instance.

```js
const server = createQuicSocket();

server.listen({
async clientHelloHandler(alpn, servername, ciphers) {
console.log(alpn);
console.log(servername);
console.log(ciphers);
}
});
```

[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
[`stream.Readable`]: #stream_class_stream_readable
[`tls.DEFAULT_ECDH_CURVE`]: #tls_tls_default_ecdh_curve
Expand All @@ -2487,6 +2490,7 @@ sock.connect({ ocspHandler: ocspClientHandler });
[RFC 4007]: https://tools.ietf.org/html/rfc4007
[Certificate Object]: https://nodejs.org/dist/latest-v12.x/docs/api/tls.html#tls_certificate_object
[custom DNS lookup function]: #quic_custom_dns_lookup_functions
[Handling client hello]: #quic_handling_client_hello
[modifying the default cipher suite]: tls.html#tls_modifying_the_default_tls_cipher_suite
[OCSP requests]: #quic_online_certificate_status_protocol_ocsp
[OCSP responses]: #quic_online_certificate_status_protocol_ocsp
Expand Down
108 changes: 41 additions & 67 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const {
PromiseAll,
PromiseReject,
PromiseResolve,
RegExp,
Set,
Symbol,
} = primordials;
Expand Down Expand Up @@ -200,7 +199,6 @@ const {
validateBoolean,
validateInteger,
validateObject,
validateString,
} = require('internal/validators');

const emit = EventEmitter.prototype.emit;
Expand Down Expand Up @@ -297,30 +295,18 @@ function onSessionClose(code, family, silent, statelessReset) {
this[owner_symbol][kDestroy](code, family, silent, statelessReset);
}

// Used only within the onSessionClientHello function. Invoked
// to complete the client hello process.
function clientHelloCallback(err, ...args) {
if (err) {
this[owner_symbol].destroy(err);
return;
}
try {
this.onClientHelloDone(...args);
} catch (err) {
this[owner_symbol].destroy(err);
}
}

// This callback is invoked at the start of the TLS handshake to provide
// some basic information about the ALPN, SNI, and Ciphers that are
// being requested. It is only called if the 'clientHello' event is
// listened for.
// being requested. It is only called if the 'clientHelloHandler' option is
// specified on listen().
function onSessionClientHello(alpn, servername, ciphers) {
this[owner_symbol][kClientHello](
alpn,
servername,
ciphers,
clientHelloCallback.bind(this));
this[owner_symbol][kClientHello](alpn, servername, ciphers)
.then((context) => {
if (context !== undefined && !context?.context)
throw new ERR_INVALID_ARG_TYPE('context', 'SecureContext', context);
this.onClientHelloDone(context?.context);
})
.catch((error) => this[owner_symbol].destroy(error));
}

// This callback is only ever invoked for QuicServerSession instances,
Expand All @@ -329,9 +315,7 @@ function onSessionClientHello(alpn, servername, ciphers) {
// TLS handshake to continue.
function onSessionCert(servername) {
this[owner_symbol][kHandleOcsp](servername)
.then(({ context, data }) => {
if (context !== undefined && !context?.context)
throw new ERR_INVALID_ARG_TYPE('context', 'SecureContext', context);
.then((data) => {
if (data !== undefined) {
if (typeof data === 'string')
data = Buffer.from(data);
Expand All @@ -342,7 +326,7 @@ function onSessionCert(servername) {
data);
}
}
this.onCertDone(context?.context, data);
this.onCertDone(data);
})
.catch((error) => this[owner_symbol].destroy(error));
}
Expand Down Expand Up @@ -919,6 +903,7 @@ class QuicSocket extends EventEmitter {
listenPromise: undefined,
lookup: undefined,
ocspHandler: undefined,
clientHelloHandler: undefined,
server: undefined,
serverSecureContext: undefined,
sessions: new Set(),
Expand Down Expand Up @@ -1010,15 +995,14 @@ class QuicSocket extends EventEmitter {
this.destroy(err);
}

// Returns the default QuicStream options for peer-initiated
// streams. These are passed on to new client and server
// QuicSession instances when they are created.
get [kStreamOptions]() {
const state = this[kInternalState];
return {
highWaterMark: state.highWaterMark,
defaultEncoding: state.defaultEncoding,
ocspHandler: state.ocspHandler,
clientHelloHandler: state.clientHelloHandler,
context: state.serverSecureContext,
};
}

Expand Down Expand Up @@ -1212,11 +1196,10 @@ class QuicSocket extends EventEmitter {
defaultEncoding,
highWaterMark,
ocspHandler,
clientHelloHandler,
transportParams,
} = validateQuicSocketListenOptions(options);

// Store the secure context so that it is not garbage collected
// while we still need to make use of it.
state.serverSecureContext =
createSecureContext({
...options,
Expand All @@ -1228,6 +1211,7 @@ class QuicSocket extends EventEmitter {
state.alpn = alpn;
state.listenPending = true;
state.ocspHandler = ocspHandler;
state.clientHelloHandler = clientHelloHandler;

await this[kMaybeBind]();

Expand Down Expand Up @@ -1484,9 +1468,6 @@ class QuicSocket extends EventEmitter {
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[kInternalState].serverSecureContext;
}
Expand Down Expand Up @@ -1639,6 +1620,7 @@ class QuicSession extends EventEmitter {
alpn: undefined,
cipher: undefined,
cipherVersion: undefined,
clientHelloHandler: undefined,
closeCode: NGTCP2_NO_ERROR,
closeFamily: QUIC_ERROR_APPLICATION,
closePromise: undefined,
Expand Down Expand Up @@ -1676,6 +1658,7 @@ class QuicSession extends EventEmitter {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler,
} = options;
super({ captureRejections: true });
this.on('newListener', onNewListener);
Expand All @@ -1687,6 +1670,7 @@ class QuicSession extends EventEmitter {
state.highWaterMark = highWaterMark;
state.defaultEncoding = defaultEncoding;
state.ocspHandler = ocspHandler;
state.clientHelloHandler = clientHelloHandler;
socket[kAddSession](this);
}

Expand Down Expand Up @@ -1751,6 +1735,7 @@ class QuicSession extends EventEmitter {
state.handshakeAckHistogram = new Histogram(handle.ack);
state.handshakeContinuationHistogram = new Histogram(handle.rate);
state.state.ocspEnabled = state.ocspHandler !== undefined;
state.state.clientHelloEnabled = state.clientHelloHandler !== undefined;
if (handle.qlogStream !== undefined) {
this[kSetQLogStream](handle.qlogStream);
handle.qlogStream = undefined;
Expand Down Expand Up @@ -2270,59 +2255,48 @@ class QuicSession extends EventEmitter {
}
class QuicServerSession extends QuicSession {
[kInternalServerState] = {
contexts: []
context: undefined
};

constructor(socket, handle, options) {
const {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler,
context,
} = options;
super(socket, { highWaterMark, defaultEncoding, ocspHandler });
super(socket, {
highWaterMark,
defaultEncoding,
ocspHandler,
clientHelloHandler
});
this[kInternalServerState].context = context;
this[kSetHandle](handle);
}

// Called only when a clientHello event handler is registered.
// Allows user code an opportunity to interject into the start
// of the TLS handshake.
[kClientHello](alpn, servername, ciphers, callback) {
this.emit(
'clientHello',
alpn,
servername,
ciphers,
callback.bind(this[kHandle]));
async [kClientHello](alpn, servername, ciphers) {
const internalState = this[kInternalState];
return internalState.clientHelloHandler?.(alpn, servername, ciphers);
}

async [kHandleOcsp](servername) {
const internalState = this[kInternalState];
const state = this[kInternalServerState];
const { context } = this.socket.serverSecureContext;

return internalState.ocspHandler?.(
'request',
{
servername,
context,
contexts: Array.from(state.contexts)
});
const { context } = this[kInternalServerState];
const certificate = context?.context.getCertificate?.();
const issuer = context?.context.getIssuer?.();
return internalState.ocspHandler?.('request', {
servername,
certificate,
issuer
});
}

get allowEarlyData() { return false; }

addContext(servername, context = {}) {
validateString(servername, 'servername');
validateObject(context, 'context');

// TODO(@jasnell): Consider unrolling regex
const re = new RegExp('^' +
servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1')
.replace(/\*/g, '[^.]*') +
'$');
this[kInternalServerState].contexts.push(
[re, _createSecureContext(context)]);
}
}

class QuicClientSession extends QuicSession {
Expand Down
14 changes: 10 additions & 4 deletions lib/internal/quic/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ function validateQuicSocketListenOptions(options = {}) {
defaultEncoding,
highWaterMark,
ocspHandler,
clientHelloHandler,
requestCert,
rejectUnauthorized,
lookup,
Expand All @@ -626,9 +627,16 @@ function validateQuicSocketListenOptions(options = {}) {
if (ocspHandler !== undefined && typeof ocspHandler !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.ocspHandler',
'functon',
'function',
ocspHandler);
}
if (clientHelloHandler !== undefined &&
typeof clientHelloHandler !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.clientHelloHandler',
'function',
clientHelloHandler);
}
const transportParams =
validateTransportParams(
options,
Expand All @@ -639,6 +647,7 @@ function validateQuicSocketListenOptions(options = {}) {
alpn,
lookup,
ocspHandler,
clientHelloHandler,
rejectUnauthorized,
requestCert,
transportParams,
Expand Down Expand Up @@ -812,9 +821,6 @@ function toggleListeners(state, event, on) {
case 'keylog':
state.keylogEnabled = on;
break;
case 'clientHello':
state.clientHelloEnabled = on;
break;
case 'pathValidation':
state.pathValidatedEnabled = on;
break;
Expand Down
Loading

0 comments on commit 4b0275a

Please sign in to comment.