Skip to content
This repository has been archived by the owner on Aug 11, 2020. It is now read-only.

quic: more test fixups #335

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1703,13 +1703,6 @@ TBD

TBD

<a id="ERR_QUICSOCKET_CLOSING"></a>
### `ERR_QUICSOCKET_CLOSING`

> Stability: 1 - Experimental

TBD

<a id="ERR_QUICSOCKET_DESTROYED"></a>
### `ERR_QUICSOCKET_DESTROYED`

Expand Down
19 changes: 18 additions & 1 deletion doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,15 @@ added: REPLACEME

A `BigInt` representing the length of time taken to complete the TLS handshake.

#### quicsession.idleTimeout
<!-- YAML
added: REPLACEME
-->

* Type: {boolean}

Set to `true` if the `QuicSession` was closed due to an idle timeout.

#### quicsession.keyUpdateCount
<!-- YAML
added: REPLACEME
Expand Down Expand Up @@ -1655,7 +1664,6 @@ added: REPLACEME
`object.passphrase` if provided, or `options.passphrase` if it is not.
* `activeConnectionIdLimit` {number}
* `maxAckDelay` {number}
* `maxActiveConnectionIDLimit` {number}
* `maxData` {number}
* `maxPacketSize` {number}
* `maxStreamsBidi` {number}
Expand Down Expand Up @@ -1707,6 +1715,15 @@ added: REPLACEME
A `BigInt` representing the length of time this `QuicSocket` has been listening
for connections.

#### quicsocket.listening
<!-- YAML
added: REPLACEME
-->

* Type: {boolean}

Set to `true` if the `QuicSocket` is listening for new connections.

#### quicsocket.packetsIgnored
<!-- YAML
added: REPLACEME
Expand Down
2 changes: 0 additions & 2 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1240,8 +1240,6 @@ E('ERR_QUICSESSION_VERSION_NEGOTIATION',
`Supported: ${supportedVersions.join(', ')}`;
},
Error);
E('ERR_QUICSOCKET_CLOSING',
'Cannot call %s while a QuicSocket is closing', Error);
E('ERR_QUICSOCKET_DESTROYED',
'Cannot call %s after a QuicSocket has been destroyed', Error);
E('ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH',
Expand Down
69 changes: 51 additions & 18 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ const {
ERR_QUIC_ERROR,
ERR_QUICSESSION_DESTROYED,
ERR_QUICSESSION_VERSION_NEGOTIATION,
ERR_QUICSOCKET_CLOSING,
ERR_QUICSOCKET_DESTROYED,
ERR_QUICSOCKET_LISTENING,
ERR_QUICCLIENTSESSION_FAILED,
Expand Down Expand Up @@ -136,6 +135,7 @@ const {
IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI,
IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT,
IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED,
IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT,
IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT,
IDX_QUIC_SESSION_STATS_CREATED_AT,
IDX_QUIC_SESSION_STATS_HANDSHAKE_START_AT,
Expand Down Expand Up @@ -1118,17 +1118,16 @@ class QuicSocket extends EventEmitter {
options = {};
}

if (callback && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
if (callback != null && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);

if (this.#serverListening)
throw new ERR_QUICSOCKET_LISTENING();

if (this.#state === kSocketDestroyed)
if (this.#state === kSocketDestroyed ||
this.#state === kSocketClosing) {
throw new ERR_QUICSOCKET_DESTROYED('listen');

if (this.#state === kSocketClosing)
throw new ERR_QUICSOCKET_CLOSING('listen');
}

options = {
secureProtocol: 'TLSv1_3_server_method',
Expand All @@ -1137,17 +1136,46 @@ class QuicSocket extends EventEmitter {
};

// The ALPN protocol identifier is strictly required.
const { alpn = NGTCP2_ALPN_H3 } = options;
const {
alpn = NGTCP2_ALPN_H3,
rejectUnauthorized,
requestCert,
} = options;

if (typeof alpn !== 'string')
throw new ERR_INVALID_ARG_TYPE('options.alpn', 'string', alpn);

if (rejectUnauthorized !== undefined &&
typeof rejectUnauthorized !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE(
'options.rejectUnauthorized',
'boolean',
rejectUnauthorized);
}

if (requestCert !== undefined && typeof requestCert !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE(
'options.requestCert',
'boolean',
requestCert);
}

// Store the secure context so that it is not garbage collected
// while we still need to make use of it.
// TODO(@jasnell): We could store a reference at the C++ level instead
// since we do not need to access this anywhere else.
// The secure context options will be validated. Keep this before the
// kMaybeBind call below.
this.#serverSecureContext = createSecureContext(options, initSecureContext);
const transportParams =
validateTransportParams(
options,
NGTCP2_MAX_CIDLEN,
NGTCP2_MIN_CIDLEN);
this.#serverSecureContext =
createSecureContext(
options,
initSecureContext);

this.#serverListening = true;
this.#alpn = alpn;

Expand All @@ -1160,11 +1188,7 @@ class QuicSocket extends EventEmitter {
// Bind the QuicSocket to the local port if it hasn't been bound already.
this[kMaybeBind]();

const doListen =
continueListen.bind(
this,
validateTransportParams(options, NGTCP2_MAX_CIDLEN, NGTCP2_MIN_CIDLEN),
this.#lookup);
const doListen = continueListen.bind(this, transportParams, this.#lookup);

// If the QuicSocket is already bound, we'll begin listening
// immediately. If we're still pending, however, wait until
Expand Down Expand Up @@ -1195,11 +1219,10 @@ class QuicSocket extends EventEmitter {
address,
} = options;

if (this.#state === kSocketDestroyed)
if (this.#state === kSocketDestroyed ||
this.#state === kSocketClosing) {
throw new ERR_QUICSOCKET_DESTROYED('connect');

if (this.#state === kSocketClosing)
throw new ERR_QUICSOCKET_CLOSING('connect');
}

const session = new QuicClientSession(this, options);

Expand Down Expand Up @@ -1411,6 +1434,10 @@ class QuicSocket extends EventEmitter {
return this.#state === kSocketDestroyed;
}

get listening() {
return this.#serverListening;
}

get pending() {
return this.#state === kSocketPending;
}
Expand Down Expand Up @@ -1543,6 +1570,7 @@ class QuicSession extends EventEmitter {
#closing = false;
#destroyed = false;
#handshakeComplete = false;
#idleTimeout = false;
#maxPacketLength = IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT;
#servername = undefined;
#socket = undefined;
Expand Down Expand Up @@ -1797,6 +1825,7 @@ class QuicSession extends EventEmitter {
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];
// Calling destroy will cause a CONNECTION_CLOSE to be
// sent to the peer and will destroy the QuicSession
// handler immediately.
Expand Down Expand Up @@ -1874,6 +1903,10 @@ class QuicSession extends EventEmitter {
this[kHandle].state[IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED] : 0);
}

get idleTimeout() {
return this.#idleTimeout;
}

get alpnProtocol() {
return this.#alpn;
}
Expand Down
26 changes: 25 additions & 1 deletion lib/internal/quic/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function getAllowUnauthorized() {
return allowUnauthorized;
}

function getSocketType(type) {
function getSocketType(type = 'udp4') {
switch (type) {
case 'udp4': return AF_INET;
case 'udp6': return AF_INET6;
Expand Down Expand Up @@ -264,6 +264,30 @@ function validateTransportParams(params) {
'options.maxHeaderLength',
'>=0');

if (preferredAddress !== undefined) {
if (preferredAddress == null || typeof preferredAddress !== 'object') {
throw new ERR_INVALID_ARG_TYPE(
'options.preferredAddress',
'Object',
preferredAddress);
}
if (preferredAddress.address !== undefined &&
typeof preferredAddress.address !== 'string') {
throw new ERR_INVALID_ARG_TYPE(
'options.preferredAddress.address',
'string',
preferredAddress.address);
}
if (preferredAddress.port !== undefined &&
!isLegalPort(preferredAddress.port)) {
throw new ERR_INVALID_ARG_VALUE(
'options.port',
preferredAddress.port,
'is not a valid IP port');
}
getSocketType(preferredAddress.type);
}

return {
activeConnectionIdLimit,
maxStreamDataBidiLocal,
Expand Down
1 change: 1 addition & 0 deletions src/quic/node_quic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ void Initialize(Local<Object> target,
V(IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT) \
V(IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT) \
V(IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED) \
V(IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT) \
V(MAX_RETRYTOKEN_EXPIRATION) \
V(MIN_RETRYTOKEN_EXPIRATION) \
V(NGTCP2_APP_NOERROR) \
Expand Down
1 change: 1 addition & 0 deletions src/quic/node_quic_session-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ void QuicSession::InitApplication() {
// the peer. All existing streams are abandoned and closed.
void QuicSession::OnIdleTimeout() {
if (!is_flag_set(QUICSESSION_FLAG_DESTROYED)) {
state_[IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT] = 1;
Debug(this, "Idle timeout");
SilentClose();
}
Expand Down
3 changes: 3 additions & 0 deletions src/quic/node_quic_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ enum QuicSessionState : int {

IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED,

// Communicates whether a session was closed due to idle timeout
IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT,

// Just the number of session state enums for use when
// creating the AliasedBuffer.
IDX_QUIC_SESSION_STATE_COUNT
Expand Down
9 changes: 7 additions & 2 deletions test/parallel/test-quic-binding.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Flags: --expose-internals --no-warnings
'use strict';

// Tests the availability and correctness of internalBinding(quic)

const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');
Expand All @@ -11,8 +13,10 @@ const { internalBinding } = require('internal/test/binding');
const quic = internalBinding('quic');
assert(quic);

// Version numbers used to identify IETF drafts are created by adding the draft
// number to 0xff0000, in this case 19 (25).
assert(quic.constants);

// Version numbers used to identify IETF drafts are created by
// adding the draft number to 0xff0000, in this case 19 (25).
assert.strictEqual(quic.constants.NGTCP2_PROTO_VER.toString(16), 'ff000019');
assert.strictEqual(quic.constants.NGTCP2_ALPN_H3, '\u0005h3-25');

Expand All @@ -32,3 +36,4 @@ assert.strictEqual(typeof quic.openUnidirectionalStream, 'function');
assert.strictEqual(typeof quic.setCallbacks, 'function');
assert.strictEqual(typeof quic.initSecureContext, 'function');
assert.strictEqual(typeof quic.initSecureContextClient, 'function');
assert.strictEqual(typeof quic.silentCloseSession, 'function');
48 changes: 26 additions & 22 deletions test/parallel/test-quic-client-empty-preferred-address.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,46 @@ if (!common.hasQuic)
const assert = require('assert');
const { key, cert, ca } = require('../common/quic');
const { createSocket } = require('quic');
const { once } = require('events');

const kServerName = 'agent2';
const server = createSocket();
(async () => {
const server = createSocket();

let client;
const options = { key, cert, ca, alpn: 'zzz' };
server.listen(options);
let client;
const options = { key, cert, ca, alpn: 'zzz' };
server.listen(options);

server.on('session', common.mustCall((serverSession) => {
serverSession.on('stream', common.mustCall(async (stream) => {
stream.on('data', common.mustCall((data) => {
assert.strictEqual(data.toString('utf8'), 'hello');
}));

await once(stream, 'end');

server.on('session', common.mustCall((serverSession) => {
serverSession.on('stream', common.mustCall((stream) => {
stream.on('data', common.mustCall((data) => {
assert.strictEqual(data.toString('utf8'), 'hello');
}));
stream.on('end', common.mustCall(() => {
stream.close();
client.close();
server.close();
}));
}));
}));

server.on('ready', common.mustCall(() => {
await once(server, 'ready');

client = createSocket({ client: options });

const clientSession = client.connect({
address: 'localhost',
address: common.localhostIPv4,
port: server.endpoints[0].address.port,
servername: kServerName,
preferredAddressPolicy: 'accept',
});

clientSession.on('close', common.mustCall());
await once(clientSession, 'secure');

clientSession.on('secure', common.mustCall(() => {
const stream = clientSession.openStream();
stream.end('hello');
stream.on('close', common.mustCall());
}));
}));
const stream = clientSession.openStream();
stream.end('hello');

await Promise.all([
once(stream, 'close'),
once(client, 'close'),
once(server, 'close')]);
})().then(common.mustCall());
Loading