Skip to content

Commit 34a095b

Browse files
committed
quic: impl. cb for http/3 settings/app. options
Implements a callback that is invoked once http/3 settings are received. Background, http/3 settings usually arrive a bit later than connection establishment, and e.g. for webtransport these settings are used to indicate support. So e.g. the examples for quiche from google, wait for the settings to arrive. (This is different to http/2). The implemented callback mechanism allows to wait for the settings to arrive until connection attempts are made. As settings are stored in the generic applications option object, the callback's name refers to the application rather than the settings. Whether this is a good choice is debatable. Fixes: #63553 Signed-off-by: Marten Richter <marten.richter@freenet.de>
1 parent 8d3245e commit 34a095b

9 files changed

Lines changed: 132 additions & 5 deletions

File tree

doc/api/quic.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,16 @@ added: v23.8.0
887887
The endpoint that created this session. Returns `null` if the session
888888
has been destroyed. Read only.
889889

890+
### `session.onappliation`
891+
892+
<!-- YAML
893+
added: REPLACEME
894+
-->
895+
896+
* Type: {quic.OnApplicationCallback}
897+
898+
The callback to invoke when new application options, e.g. HTTP/3 settings arrived.
899+
890900
### `session.onerror`
891901

892902
<!-- YAML
@@ -3200,11 +3210,11 @@ with that error:
32003210

32013211
* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
32023212
`oninfo`, `onwanttrailers`): the stream is destroyed.
3203-
* Session callbacks (`onstream`, `ondatagram`, `ondatagramstatus`,
3204-
`onpathvalidation`, `onsessionticket`, `onnewtoken`,
3205-
`onversionnegotiation`, `onorigin`, `ongoaway`, `onhandshake`,
3206-
`onkeylog`, `onqlog`): the session is destroyed along with all of its
3207-
streams.
3213+
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
3214+
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
3215+
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
3216+
`onhandshake`, `onkeylog`, `onqlog`): the session is destroyed along
3217+
with all of its streams.
32083218

32093219
Before destruction, the optional [`session.onerror`][] or
32103220
[`stream.onerror`][] callback is invoked (if set), giving the application a
@@ -3732,6 +3742,18 @@ added: v23.8.0
37323742
37333743
Published when an endpoint's busy state changes.
37343744
3745+
### Channel: `quic.session.application`
3746+
3747+
<!-- YAML
3748+
added: v23.8.0
3749+
-->
3750+
3751+
* `applicationoptions` {quic.ApplicationOptions} Current application options.
3752+
* `session` {quic.QuicSession}
3753+
3754+
Published when a locally-initiated stream is opened.
3755+
3756+
37353757
### Channel: `quic.session.created.client`
37363758
37373759
<!-- YAML
@@ -4099,6 +4121,7 @@ throughput issues caused by flow control.
40994121
[`session.createUnidirectionalStream()`]: #sessioncreateunidirectionalstreamoptions
41004122
[`session.destroy()`]: #sessiondestroyerror-options
41014123
[`session.maxPendingDatagrams`]: #sessionmaxpendingdatagrams
4124+
[`session.onapplication`]: #sessiononapplication
41024125
[`session.ondatagram`]: #sessionondatagram
41034126
[`session.ondatagramstatus`]: #sessionondatagramstatus
41044127
[`session.onearlyrejected`]: #sessiononearlyrejected

lib/internal/quic/diagnostics.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const onEndpointErrorChannel = dc.channel('quic.endpoint.error');
1414
const onEndpointBusyChangeChannel = dc.channel('quic.endpoint.busy.change');
1515
const onEndpointClientSessionChannel = dc.channel('quic.session.created.client');
1616
const onEndpointServerSessionChannel = dc.channel('quic.session.created.server');
17+
const onSessionApplicationChannel = dc.channel('quic.session.application');
1718
const onSessionOpenStreamChannel = dc.channel('quic.session.open.stream');
1819
const onSessionReceivedStreamChannel = dc.channel('quic.session.received.stream');
1920
const onSessionSendDatagramChannel = dc.channel('quic.session.send.datagram');
@@ -48,6 +49,7 @@ module.exports = {
4849
onEndpointBusyChangeChannel,
4950
onEndpointClientSessionChannel,
5051
onEndpointServerSessionChannel,
52+
onSessionApplicationChannel,
5153
onSessionOpenStreamChannel,
5254
onSessionReceivedStreamChannel,
5355
onSessionSendDatagramChannel,

lib/internal/quic/quic.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ const {
199199
kPrivateConstructor,
200200
kReset,
201201
kSendHeaders,
202+
kSessionApplication,
202203
kSessionTicket,
203204
kTrailers,
204205
kVersionNegotiation,
@@ -247,6 +248,7 @@ const {
247248
onSessionReceiveDatagramStatusChannel,
248249
onSessionPathValidationChannel,
249250
onSessionNewTokenChannel,
251+
onSessionApplicationChannel,
250252
onSessionTicketChannel,
251253
onSessionVersionNegotiationChannel,
252254
onSessionOriginChannel,
@@ -436,6 +438,7 @@ const endpointRegistry = new SafeSet();
436438
* @property {OnGoawayCallback} [ongoaway] GOAWAY frame callback.
437439
* @property {OnKeylogCallback} [onkeylog] TLS key-log callback.
438440
* @property {OnQlogCallback} [onqlog] qlog data callback.
441+
* @property {OnApplicationCallback} [onapplication] application options callback.
439442
* @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback.
440443
* @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback.
441444
* @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback.
@@ -566,6 +569,13 @@ const endpointRegistry = new SafeSet();
566569
* @returns {void}
567570
*/
568571

572+
/**
573+
* @callback OnApplicationCallback
574+
* @this {QuicSession}
575+
* @param {ApplicationOptions} applicationoptions
576+
* @returns {void}
577+
*/
578+
569579
/**
570580
* @callback OnSessionTicketCallback
571581
* @this {QuicSession}
@@ -643,6 +653,14 @@ const endpointRegistry = new SafeSet();
643653
* @returns {void}
644654
*/
645655

656+
/**
657+
* Called when initial request or response headers are received.
658+
* @callback OnApplicationCallback
659+
* @this {QuicSession}
660+
* @param {ApplicationOptions} applicationoptions ApplicationOptions object
661+
* @returns {void}
662+
*/
663+
646664
/**
647665
* @callback OnBlockedCallback
648666
* @this {QuicStream}
@@ -798,6 +816,16 @@ setCallbacks({
798816
preferredAddress);
799817
},
800818

819+
/**
820+
* Called when the session's application object is updated
821+
* E.g. http/3 session arrived.
822+
* @param {ApplicationOptions} applicationoptions An application object
823+
*/
824+
onSessionApplication(applicationoptions) {
825+
debug('session application callback', this[kOwner]);
826+
this[kOwner][kSessionApplication](applicationoptions);
827+
},
828+
801829
/**
802830
* Called when the session generates a new TLS session ticket
803831
* @param {object} ticket An opaque session ticket
@@ -1208,6 +1236,7 @@ function applyCallbacks(session, cbs) {
12081236
if (cbs.ongoaway) session.ongoaway = cbs.ongoaway;
12091237
if (cbs.onkeylog) session.onkeylog = cbs.onkeylog;
12101238
if (cbs.onqlog) session.onqlog = cbs.onqlog;
1239+
if (cbs.onapplication) session.onapplication = cbs.application;
12111240
if (cbs.onheaders || cbs.ontrailers || cbs.oninfo || cbs.onwanttrailers) {
12121241
session[kStreamCallbacks] = {
12131242
__proto__: null,
@@ -2896,6 +2925,25 @@ class QuicSession {
28962925
}
28972926
}
28982927

2928+
/** @type {Function|undefined} */
2929+
get onapplication() {
2930+
assertIsQuicSession(this);
2931+
return this.#inner.onapplication;
2932+
}
2933+
2934+
set onapplication(fn) {
2935+
assertIsQuicSession(this);
2936+
const inner = this.#inner;
2937+
if (fn === undefined) {
2938+
inner.onapplication = undefined;
2939+
inner.state.hasApplicationListener = false;
2940+
} else {
2941+
validateFunction(fn, 'onsessionticket');
2942+
inner.onapplication = FunctionPrototypeBind(fn, this);
2943+
inner.state.hasApplicationListener = true;
2944+
}
2945+
}
2946+
28992947
/** @type {Function|undefined} */
29002948
get onversionnegotiation() {
29012949
assertIsQuicSession(this);
@@ -3483,6 +3531,7 @@ class QuicSession {
34833531
inner.ondatagramstatus = undefined;
34843532
inner.onpathvalidation = undefined;
34853533
inner.onsessionticket = undefined;
3534+
inner.onapplication = undefined;
34863535
inner.onkeylog = undefined;
34873536
inner.onversionnegotiation = undefined;
34883537
inner.onhandshake = undefined;
@@ -3704,6 +3753,23 @@ class QuicSession {
37043753
safeCallbackInvoke(inner.onsessionticket, this, ticket);
37053754
}
37063755

3756+
/**
3757+
* @param {ApplicationOptions} applicationoptions
3758+
*/
3759+
[kSessionApplication](applicationoptions) {
3760+
if (this.destroyed) return;
3761+
if (onSessionApplicationChannel.hasSubscribers) {
3762+
onSessionApplicationChannel.publish({
3763+
__proto__: null,
3764+
applicationoptions,
3765+
session: this,
3766+
});
3767+
}
3768+
const inner = this.#inner;
3769+
if (typeof inner.onapplication === 'function')
3770+
safeCallbackInvoke(inner.onapplication, this, applicationoptions);
3771+
}
3772+
37073773
/**
37083774
* @param {Buffer} token
37093775
* @param {SocketAddress} address
@@ -4222,6 +4288,7 @@ class QuicEndpoint {
42224288
ongoaway,
42234289
onkeylog,
42244290
onqlog,
4291+
onapplication,
42254292
// Stream-level callbacks applied to each incoming stream.
42264293
onheaders,
42274294
ontrailers,
@@ -4247,6 +4314,7 @@ class QuicEndpoint {
42474314
ongoaway,
42484315
onkeylog,
42494316
onqlog,
4317+
onapplication,
42504318
onheaders,
42514319
ontrailers,
42524320
oninfo,
@@ -4955,6 +5023,8 @@ function processSessionOptions(options, config = kEmptyObject) {
49555023
ongoaway,
49565024
onkeylog,
49575025
onqlog,
5026+
onapplication,
5027+
// Application level options changed, e.g. HTTP/3 settings related
49585028
// Stream-level callbacks.
49595029
onheaders,
49605030
ontrailers,
@@ -5060,6 +5130,7 @@ function processSessionOptions(options, config = kEmptyObject) {
50605130
ongoaway,
50615131
onkeylog,
50625132
onqlog,
5133+
onapplication,
50635134
onheaders,
50645135
ontrailers,
50655136
oninfo,

lib/internal/quic/symbols.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const kRemoveSession = Symbol('kRemoveSession');
5454
const kRemoveStream = Symbol('kRemoveStream');
5555
const kReset = Symbol('kReset');
5656
const kSendHeaders = Symbol('kSendHeaders');
57+
const kSessionApplication = Symbol('kSessionApplication');
5758
const kSessionTicket = Symbol('kSessionTicket');
5859
const kTrailers = Symbol('kTrailers');
5960
const kVersionNegotiation = Symbol('kVersionNegotiation');
@@ -88,6 +89,7 @@ module.exports = {
8889
kRemoveStream,
8990
kReset,
9091
kSendHeaders,
92+
kSessionApplication,
9193
kSessionTicket,
9294
kTrailers,
9395
kVersionNegotiation,

src/quic/bindingdata.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SessionManager;
4141
#define QUIC_JS_CALLBACKS(V) \
4242
V(endpoint_close, EndpointClose) \
4343
V(session_close, SessionClose) \
44+
V(session_application, SessionApplication) \
4445
V(session_early_data_rejected, SessionEarlyDataRejected) \
4546
V(session_goaway, SessionGoaway) \
4647
V(session_datagram, SessionDatagram) \

src/quic/http3.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,8 @@ class Http3ApplicationImpl final : public Session::Application {
10121012
Debug(&session(),
10131013
"HTTP/3 application received updated settings: %s",
10141014
options_);
1015+
// The settings are part of the application
1016+
session().EmitApplication();
10151017
}
10161018

10171019
bool started_ = false;

src/quic/session.cc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3601,6 +3601,27 @@ void Session::EmitSessionTicket(Store&& ticket) {
36013601
}
36023602
}
36033603

3604+
void Session::EmitApplication() {
3605+
if (is_destroyed()) return;
3606+
if (!env()->can_call_into_js()) return;
3607+
3608+
CallbackScope<Session> cb_scope(this);
3609+
3610+
if (!has_application()) {
3611+
// The application has not yet been selected (ALPN negotiation is not
3612+
// yet complete on the server) or the session has been destroyed. In
3613+
// either case, the application options are not available.
3614+
// Should not happen, but we bail out
3615+
return;
3616+
}
3617+
Local<Value> argv;
3618+
auto& options = application().options();
3619+
if (options.ToObject(env()).ToLocal(&argv)) {
3620+
MakeCallback(
3621+
BindingData::Get(env()).session_application_callback(), 1, &argv);
3622+
}
3623+
}
3624+
36043625
void Session::DestroyAllStreams(const QuicError& error) {
36053626
DCHECK(!is_destroyed());
36063627
// Copy the streams map since streams remove themselves during

src/quic/session.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
602602
void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
603603
const uint32_t* sv,
604604
size_t nsv);
605+
void EmitApplication();
605606
void DatagramStatus(datagram_id datagramId, DatagramStatus status);
606607
void DatagramReceived(const uint8_t* data,
607608
size_t datalen,

test/parallel/test-quic-h3-settings.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ const decoder = new TextDecoder();
166166
servername: 'localhost',
167167
application: { enableConnectProtocol: true, enableDatagrams: true },
168168
});
169+
clientSession.onapplication = mustCall((appopt) => {
170+
strictEqual(appopt.enableConnectProtocol, true);
171+
strictEqual(appopt.enableDatagrams, true);
172+
});
169173
await clientSession.opened;
170174

171175
const stream = await clientSession.createBidirectionalStream({

0 commit comments

Comments
 (0)