Skip to content

Commit 6009eb2

Browse files
committed
feat: QUICConnection and QUICClient handles ctx and timeouts
[ci skip]
1 parent b1ea509 commit 6009eb2

File tree

5 files changed

+216
-119
lines changed

5 files changed

+216
-119
lines changed

src/QUICClient.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -200,32 +200,32 @@ class QUICClient extends EventTarget {
200200
`Cannot connect to ${host_} an IPv4 mapped IPv6 QUICClient`,
201201
);
202202
}
203-
const connection = new QUICConnection({
204-
type: 'client',
205-
scid,
206-
socket,
207-
remoteInfo: {
208-
host: host_,
209-
port,
210-
},
211-
config: quicConfig,
212-
reasonToCode,
213-
codeToReason,
214-
verifyCallback,
215-
logger: logger.getChild(
216-
`${QUICConnection.name} ${scid.toString().slice(32)}`,
217-
),
218-
});
219203
const abortController = new AbortController();
220-
const abortHandler = (r) => {
221-
abortController.abort(r);
204+
const abortHandler = () => {
205+
abortController.abort(ctx.signal.reason);
222206
};
223207
ctx.signal.addEventListener('abort', abortHandler);
208+
const connectionProm = QUICConnection.createQUICConnection(
209+
{
210+
type: 'client',
211+
scid,
212+
socket,
213+
remoteInfo: {
214+
host: host_,
215+
port,
216+
},
217+
config: quicConfig,
218+
reasonToCode,
219+
codeToReason,
220+
verifyCallback,
221+
logger: logger.getChild(
222+
`${QUICConnection.name} ${scid.toString().slice(32)}`,
223+
),
224+
},
225+
ctx,
226+
);
224227
try {
225-
await Promise.race([
226-
connection.start({ ...ctx, signal: abortController.signal }),
227-
socketErrorP,
228-
]);
228+
await Promise.race([connectionProm, socketErrorP]);
229229
} catch (e) {
230230
// In case the `connection.start` is on-going, we need to abort it
231231
abortController.abort(e);
@@ -238,6 +238,7 @@ class QUICClient extends EventTarget {
238238
socket.removeEventListener('socketError', handleQUICSocketError);
239239
ctx.signal.removeEventListener('abort', abortHandler);
240240
}
241+
const connection = await connectionProm;
241242
address = utils.buildAddress(host_, port);
242243
const client = new this({
243244
socket,

src/QUICConnection.ts

Lines changed: 140 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import QUICStream from './QUICStream';
2828
import { quiche } from './native';
2929
import * as events from './events';
3030
import * as utils from './utils';
31-
import { never } from './utils';
31+
import { never, promise } from './utils';
3232
import * as errors from './errors';
3333

3434
/**
@@ -207,6 +207,103 @@ class QUICConnection extends EventTarget {
207207
protected count = 0;
208208
protected verifyCallback: VerifyCallback | undefined;
209209

210+
public static createQUICConnection(
211+
args:
212+
| {
213+
type: 'client';
214+
scid: QUICConnectionId;
215+
dcid?: undefined;
216+
remoteInfo: RemoteInfo;
217+
config: QUICConfig;
218+
socket: QUICSocket;
219+
reasonToCode?: StreamReasonToCode;
220+
codeToReason?: StreamCodeToReason;
221+
verifyCallback?: VerifyCallback;
222+
logger?: Logger;
223+
}
224+
| {
225+
type: 'server';
226+
scid: QUICConnectionId;
227+
dcid: QUICConnectionId;
228+
remoteInfo: RemoteInfo;
229+
config: QUICConfig;
230+
socket: QUICSocket;
231+
reasonToCode?: StreamReasonToCode;
232+
codeToReason?: StreamCodeToReason;
233+
verifyCallback?: VerifyCallback;
234+
logger?: Logger;
235+
},
236+
ctx?: Partial<ContextTimed>,
237+
): PromiseCancellable<QUICConnection>;
238+
@timedCancellable(true, Infinity, errors.ErrorQUICConnectionStartTimeOut)
239+
public static async createQUICConnection(
240+
args:
241+
| {
242+
type: 'client';
243+
scid: QUICConnectionId;
244+
dcid?: undefined;
245+
remoteInfo: RemoteInfo;
246+
config: QUICConfig;
247+
socket: QUICSocket;
248+
reasonToCode?: StreamReasonToCode;
249+
codeToReason?: StreamCodeToReason;
250+
verifyCallback?: VerifyCallback;
251+
logger?: Logger;
252+
}
253+
| {
254+
type: 'server';
255+
scid: QUICConnectionId;
256+
dcid: QUICConnectionId;
257+
remoteInfo: RemoteInfo;
258+
config: QUICConfig;
259+
socket: QUICSocket;
260+
reasonToCode?: StreamReasonToCode;
261+
codeToReason?: StreamCodeToReason;
262+
verifyCallback?: VerifyCallback;
263+
logger?: Logger;
264+
},
265+
@context ctx: ContextTimed,
266+
): Promise<QUICConnection> {
267+
ctx.signal.throwIfAborted();
268+
const abortProm = promise<never>();
269+
const abortHandler = () => {
270+
abortProm.rejectP(ctx.signal.reason);
271+
};
272+
ctx.signal.addEventListener('abort', abortHandler);
273+
const connection = new this(args);
274+
// This ensures that TLS has been established and verified on both sides
275+
try {
276+
await Promise.race([
277+
Promise.all([
278+
connection.start(),
279+
connection.establishedP,
280+
connection.secureEstablishedP,
281+
]),
282+
abortProm.p,
283+
]);
284+
} catch (e) {
285+
await connection.stop({
286+
applicationError: false,
287+
errorCode: 42, // FIXME: use a proper code
288+
errorMessage: e.message,
289+
force: true,
290+
});
291+
throw e;
292+
} finally {
293+
ctx.signal.removeEventListener('abort', abortHandler);
294+
}
295+
connection.logger.warn('secured');
296+
// After this is done
297+
// We need to establish the keep alive interval time
298+
if (connection.config.keepAliveIntervalTime != null) {
299+
connection.startKeepAliveIntervalTimer(
300+
connection.config.keepAliveIntervalTime,
301+
);
302+
}
303+
304+
return connection;
305+
}
306+
210307
public constructor({
211308
type,
212309
scid,
@@ -351,79 +448,13 @@ class QUICConnection extends EventTarget {
351448
}
352449

353450
/**
354-
* This is the same as basically waiting for `secureEstablishedP`
355-
* While this is occurring one can call the `recv` and `send` to make this happen
451+
* This will set up the connection initiate sending
356452
*/
357-
public start(ctx?: Partial<ContextTimed>): PromiseCancellable<void>;
358-
@timedCancellable(true, Infinity, errors.ErrorQUICConnectionStartTimeOut)
359-
public async start(@context ctx: ContextTimed): Promise<void> {
453+
public async start(): Promise<void> {
360454
this.logger.info(`Start ${this.constructor.name}`);
361-
ctx.signal.throwIfAborted();
362-
const abortHandler = (r) => {
363-
this.rejectEstablishedP(r);
364-
this.rejectSecureEstablishedP(r);
365-
366-
// Is this actually true?
367-
// Technically the connection is closed
368-
this.rejectClosedP(r);
369-
};
370-
ctx.signal.addEventListener('abort', abortHandler);
371455
// Set the connection up
372456
this.socket.connectionMap.set(this.connectionId, this);
373-
// Waits for the first short packet after establishment
374-
// This ensures that TLS has been established and verified on both sides
375457
await this.send();
376-
await this.secureEstablishedP
377-
.catch((e) => {
378-
this.socket.connectionMap.delete(this.connectionId);
379-
380-
if (this.conn.isTimedOut()) {
381-
// We don't dispatch an event here, it was already done in the timeout.
382-
throw new errors.ErrorQUICConnectionStartTimeOut();
383-
}
384-
385-
// Emit error if local error
386-
const localError = this.conn.localError();
387-
if (localError != null) {
388-
const message = `connection start failed with localError ${Buffer.from(
389-
localError.reason,
390-
).toString()}(${localError.errorCode})`;
391-
this.logger.info(message);
392-
throw new errors.ErrorQUICConnectionInternal(message, {
393-
data: {
394-
type: 'local',
395-
...localError,
396-
},
397-
});
398-
}
399-
// Emit error if peer error
400-
const peerError = this.conn.peerError();
401-
if (peerError != null) {
402-
const message = `Connection start failed with peerError ${Buffer.from(
403-
peerError.reason,
404-
).toString()}(${peerError.errorCode})`;
405-
this.logger.info(message);
406-
throw new errors.ErrorQUICConnectionInternal(message, {
407-
data: {
408-
type: 'local',
409-
...peerError,
410-
},
411-
});
412-
}
413-
// Throw the default error if none of the above were true, this shouldn't really happen
414-
throw e;
415-
})
416-
.finally(() => {
417-
ctx.signal.removeEventListener('abort', abortHandler);
418-
});
419-
this.logger.warn('secured');
420-
// After this is done
421-
// We need to established the keep alive interval time
422-
if (this.config.keepAliveIntervalTime != null) {
423-
this.startKeepAliveIntervalTimer(this.config.keepAliveIntervalTime);
424-
}
425-
// Do we remove the on abort event listener?
426-
// I forgot...
427458
this.logger.info(`Started ${this.constructor.name}`);
428459
}
429460

@@ -472,7 +503,8 @@ class QUICConnection extends EventTarget {
472503
this.stopKeepAliveIntervalTimer();
473504
try {
474505
mon = mon ?? new Monitor<RWLockWriter>(this.lockbox, RWLockWriter);
475-
await mon.withF(this.lockCode, async (mon) => {
506+
// Trigger closing connection in the background and await close later.
507+
void mon.withF(this.lockCode, async (mon) => {
476508
// If this is already closed, then `Done` will be thrown
477509
// Otherwise it can send `CONNECTION_CLOSE` frame
478510
// This can be 0x1c close at the QUIC layer or no errors
@@ -481,10 +513,19 @@ class QUICConnection extends EventTarget {
481513
// 1 packet containing a `CONNECTION_CLOSE` frame too
482514
// (with `NO_ERROR` code if appropriate)
483515
// It must enter into a draining state, and no other packets can be sent
484-
this.conn.close(applicationError, errorCode, Buffer.from(errorMessage));
485-
// If we get a `Done` exception we don't bother calling send
486-
// The send only gets sent if the `Done` is not the case
487-
await this.send(mon);
516+
try {
517+
this.conn.close(
518+
applicationError,
519+
errorCode,
520+
Buffer.from(errorMessage),
521+
);
522+
// If we get a `Done` exception we don't bother calling send
523+
// The send only gets sent if the `Done` is not the case
524+
await this.send(mon);
525+
} catch (e) {
526+
// Ignore 'Done' if already closed
527+
if (e.message !== 'Done') throw e;
528+
}
488529
});
489530
} catch (e) {
490531
// If the connection is already closed, `Done` will be thrown
@@ -507,9 +548,15 @@ class QUICConnection extends EventTarget {
507548
this.socket.connectionMap.delete(this.connectionId);
508549

509550
if (this.conn.isTimedOut()) {
551+
const error = this.secured
552+
? new errors.ErrorQUICConnectionIdleTimeOut()
553+
: new errors.ErrorQUICConnectionStartTimeOut();
554+
555+
this.rejectEstablishedP(error);
556+
this.rejectSecureEstablishedP(error);
510557
this.dispatchEvent(
511558
new events.QUICConnectionErrorEvent({
512-
detail: new errors.ErrorQUICConnectionIdleTimeOut(),
559+
detail: error,
513560
}),
514561
);
515562
}
@@ -521,14 +568,17 @@ class QUICConnection extends EventTarget {
521568
peerError.reason,
522569
).toString()}(${peerError.errorCode})`;
523570
this.logger.info(message);
571+
const error = new errors.ErrorQUICConnectionInternal(message, {
572+
data: {
573+
type: 'local',
574+
...peerError,
575+
},
576+
});
577+
this.rejectEstablishedP(error);
578+
this.rejectSecureEstablishedP(error);
524579
this.dispatchEvent(
525580
new events.QUICConnectionErrorEvent({
526-
detail: new errors.ErrorQUICConnectionInternal(message, {
527-
data: {
528-
type: 'local',
529-
...peerError,
530-
},
531-
}),
581+
detail: error,
532582
}),
533583
);
534584
}
@@ -539,14 +589,17 @@ class QUICConnection extends EventTarget {
539589
localError.reason,
540590
).toString()}(${localError.errorCode})`;
541591
this.logger.info(message);
592+
const error = new errors.ErrorQUICConnectionInternal(message, {
593+
data: {
594+
type: 'local',
595+
...localError,
596+
},
597+
});
598+
this.rejectEstablishedP(error);
599+
this.rejectSecureEstablishedP(error);
542600
this.dispatchEvent(
543601
new events.QUICConnectionErrorEvent({
544-
detail: new errors.ErrorQUICConnectionInternal(message, {
545-
data: {
546-
type: 'local',
547-
...localError,
548-
},
549-
}),
602+
detail: error,
550603
}),
551604
);
552605
}
@@ -864,7 +917,6 @@ class QUICConnection extends EventTarget {
864917
// Then we just have to proceed!
865918
// Plus if we are called here
866919
this.resolveClosedP();
867-
868920
await this.stop(
869921
this.conn.localError() ?? this.conn.peerError() ?? {},
870922
mon,

src/QUICServer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ class QUICServer extends EventTarget {
332332
`Accepting new connection from QUIC packet from ${remoteInfo.host}:${remoteInfo.port}`,
333333
);
334334
const clientConnRef = Buffer.from(header.scid).toString('hex').slice(32);
335-
const connection = new QUICConnection({
335+
const connectionProm = QUICConnection.createQUICConnection({
336336
type: 'server',
337337
scid: newScid,
338338
dcid: dcidOriginal,
@@ -347,12 +347,13 @@ class QUICServer extends EventTarget {
347347
),
348348
});
349349
try {
350-
await connection.start(); // TODO: pass ctx
350+
await connectionProm; // TODO: pass ctx
351351
} catch (e) {
352352
// Ignoring any errors here as a failure to connect
353353
// FIXME: should we emit a connection error here?
354354
return;
355355
}
356+
const connection = await connectionProm;
356357
this.dispatchEvent(
357358
new events.QUICServerConnectionEvent({ detail: connection }),
358359
);

0 commit comments

Comments
 (0)