Skip to content
Open
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
47 changes: 42 additions & 5 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,8 @@ added: REPLACEME
The current application-level options for this session. These include settings
that are specific to the negotiated application protocol (e.g. HTTP/3) and may
be negotiated separately from the transport parameters. Read only.
You can use the callback [`session.onapplication`][] to be informed, when settings
from the remote arrive.

### `session.close([options])`

Expand Down Expand Up @@ -887,6 +889,16 @@ added: v23.8.0
The endpoint that created this session. Returns `null` if the session
has been destroyed. Read only.

### `session.onapplication`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming here is confusing, as you mentioned in the description, since it doesn't emit an application as such. This isn't a generic QUIC thing at all (transport parameters are a separate thing with a different flow) and right now this won't work for anything except HTTP/3. While some other protocols have equivalents it's not clear how closely they would all map to the model, and they definitely all have different names & structures anyway.

For HTTP/3, the RFC calls those 'settings', we use localSettings/remoteSettings in our existing HTTP/2 API. Since this is basically HTTP only and there's no umbrella term, I'd suggest this say 'settings' somewhere explicitly. Just onsettings would work fine from my pov. The rest of our HTTP-specific stuff (onheaders) doesn't explicitly name HTTP in the API naming so that seems consistent.

Beyond this PR: the HTTP/QUIC ambiguity here in the API ties a bit into my thoughts about the request timeout stuff @jasnell - starting to wonder whether we should separate the QUIC & HTTP/3 APIs more explicitly, to make this kind of thing cleaner. The distinction is very blurred together right now and we do lots of HTTP-shaped things for QUIC. Looks doable to split it a bit, really just an API restructuring rather than internals, but there'd be plenty of details to work out. Not required for this PR at all, just fyi, when I have time next week I'll take a look around it as a more general topic and try to open a rough draft to discuss, I have some ideas that'd be fun to explore 😄.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with the naming is, that it dates back to the choice that the http/3 settings are all inside the quic.ApplicationOptions . So I had to decide to call it onsettings , which would be my preferred choice for http/3 settings, or stick with the generic term applicationoptions (though I shortened it to onapplication as it was already very long).

So I think it is kind of a general decision. I have so far just flipped a coin.

Regarding the HTTP/QUIC ambiguity, I am playing around with how to implement webtransport and ngtcp2, and I think you can even wrap the WT streams to the QuicStream and have a mixture of streams. Even if it is confusing, it works some way.

P.S: I try to cover the minor stuff. As I do it in my free time, and the holiday day today is over it may take until next weekend.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor stuff should be all covered. I am now waiting for consensus on the naming scheme.


<!-- YAML
added: REPLACEME
-->

* Type: {quic.OnApplicationCallback}
Comment thread
martenrichter marked this conversation as resolved.

The callback to invoke when new application options, e.g. HTTP/3 settings arrived.

### `session.onerror`

<!-- YAML
Expand Down Expand Up @@ -3200,11 +3212,11 @@ with that error:

* Stream callbacks (`onblocked`, `onreset`, `onheaders`, `ontrailers`,
`oninfo`, `onwanttrailers`): the stream is destroyed.
* Session callbacks (`onstream`, `ondatagram`, `ondatagramstatus`,
`onpathvalidation`, `onsessionticket`, `onnewtoken`,
`onversionnegotiation`, `onorigin`, `ongoaway`, `onhandshake`,
`onkeylog`, `onqlog`): the session is destroyed along with all of its
streams.
* Session callbacks (`onapplication`, `onstream`, `ondatagram`,
`ondatagramstatus`, `onpathvalidation`, `onsessionticket`,
`onnewtoken`, `onversionnegotiation`, `onorigin`, `ongoaway`,
`onhandshake`, `onkeylog`, `onqlog`): the session is destroyed along
with all of its streams.

Before destruction, the optional [`session.onerror`][] or
[`stream.onerror`][] callback is invoked (if set), giving the application a
Expand Down Expand Up @@ -3258,6 +3270,19 @@ added: v23.8.0
datagram was never sent on the wire (dropped due to queue overflow,
send attempt limit exceeded, or frame size rejection).

### Callback: `OnApplicationCallback`

<!-- YAML
added: v23.8.0
-->

* `this` {quic.QuicSession}
* `applicationoption` {quic.QuicSession}

The callback function that is invoked when application options change.
E.g. for http/3 settings are included in applications options and
may arrive after the connection is established.

### Callback: `OnPathValidationCallback`

<!-- YAML
Expand Down Expand Up @@ -3732,6 +3757,17 @@ added: v23.8.0

Published when an endpoint's busy state changes.

### Channel: `quic.session.application`

<!-- YAML
added: v23.8.0
-->

* `applicationoptions` {quic.ApplicationOptions} Current application options.
* `session` {quic.QuicSession}

Published when a locally-initiated stream is opened.

### Channel: `quic.session.created.client`

<!-- YAML
Expand Down Expand Up @@ -4099,6 +4135,7 @@ throughput issues caused by flow control.
[`session.createUnidirectionalStream()`]: #sessioncreateunidirectionalstreamoptions
[`session.destroy()`]: #sessiondestroyerror-options
[`session.maxPendingDatagrams`]: #sessionmaxpendingdatagrams
[`session.onapplication`]: #sessiononapplication
[`session.ondatagram`]: #sessionondatagram
[`session.ondatagramstatus`]: #sessionondatagramstatus
[`session.onearlyrejected`]: #sessiononearlyrejected
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/quic/diagnostics.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const onEndpointErrorChannel = dc.channel('quic.endpoint.error');
const onEndpointBusyChangeChannel = dc.channel('quic.endpoint.busy.change');
const onEndpointClientSessionChannel = dc.channel('quic.session.created.client');
const onEndpointServerSessionChannel = dc.channel('quic.session.created.server');
const onSessionApplicationChannel = dc.channel('quic.session.application');
const onSessionOpenStreamChannel = dc.channel('quic.session.open.stream');
const onSessionReceivedStreamChannel = dc.channel('quic.session.received.stream');
const onSessionSendDatagramChannel = dc.channel('quic.session.send.datagram');
Expand Down Expand Up @@ -48,6 +49,7 @@ module.exports = {
onEndpointBusyChangeChannel,
onEndpointClientSessionChannel,
onEndpointServerSessionChannel,
onSessionApplicationChannel,
onSessionOpenStreamChannel,
onSessionReceivedStreamChannel,
onSessionSendDatagramChannel,
Expand Down
71 changes: 71 additions & 0 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ const {
kPrivateConstructor,
kReset,
kSendHeaders,
kSessionApplication,
kSessionTicket,
kTrailers,
kVersionNegotiation,
Expand Down Expand Up @@ -247,6 +248,7 @@ const {
onSessionReceiveDatagramStatusChannel,
onSessionPathValidationChannel,
onSessionNewTokenChannel,
onSessionApplicationChannel,
onSessionTicketChannel,
onSessionVersionNegotiationChannel,
onSessionOriginChannel,
Expand Down Expand Up @@ -436,6 +438,7 @@ const endpointRegistry = new SafeSet();
* @property {OnGoawayCallback} [ongoaway] GOAWAY frame callback.
* @property {OnKeylogCallback} [onkeylog] TLS key-log callback.
* @property {OnQlogCallback} [onqlog] qlog data callback.
* @property {OnApplicationCallback} [onapplication] application options callback.
* @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback.
* @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback.
* @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback.
Expand Down Expand Up @@ -566,6 +569,13 @@ const endpointRegistry = new SafeSet();
* @returns {void}
*/

/**
* @callback OnApplicationCallback
* @this {QuicSession}
* @param {ApplicationOptions} applicationoptions
* @returns {void}
*/

/**
* @callback OnSessionTicketCallback
* @this {QuicSession}
Expand Down Expand Up @@ -643,6 +653,14 @@ const endpointRegistry = new SafeSet();
* @returns {void}
*/

/**
* Called when `ApplicationOptions` are changed, e.g. HTTP/3 settings.
* @callback OnApplicationCallback
* @this {QuicSession}
* @param {ApplicationOptions} applicationoptions ApplicationOptions object
* @returns {void}
*/
Comment thread
martenrichter marked this conversation as resolved.

/**
* @callback OnBlockedCallback
* @this {QuicStream}
Expand Down Expand Up @@ -798,6 +816,16 @@ setCallbacks({
preferredAddress);
},

/**
* Called when the session's application object is updated
* E.g. http/3 session arrived.
* @param {ApplicationOptions} applicationoptions An application object
*/
onSessionApplication(applicationoptions) {
debug('session application callback', this[kOwner]);
this[kOwner][kSessionApplication](applicationoptions);
},

/**
* Called when the session generates a new TLS session ticket
* @param {object} ticket An opaque session ticket
Expand Down Expand Up @@ -1208,6 +1236,7 @@ function applyCallbacks(session, cbs) {
if (cbs.ongoaway) session.ongoaway = cbs.ongoaway;
if (cbs.onkeylog) session.onkeylog = cbs.onkeylog;
if (cbs.onqlog) session.onqlog = cbs.onqlog;
if (cbs.onapplication) session.onapplication = cbs.onapplication;
if (cbs.onheaders || cbs.ontrailers || cbs.oninfo || cbs.onwanttrailers) {
session[kStreamCallbacks] = {
__proto__: null,
Expand Down Expand Up @@ -2896,6 +2925,25 @@ class QuicSession {
}
}

/** @type {Function|undefined} */
get onapplication() {
assertIsQuicSession(this);
return this.#inner.onapplication;
}

set onapplication(fn) {
assertIsQuicSession(this);
const inner = this.#inner;
if (fn === undefined) {
inner.onapplication = undefined;
inner.state.hasApplicationListener = false;
} else {
validateFunction(fn, 'onapplication');
inner.onapplication = FunctionPrototypeBind(fn, this);
inner.state.hasApplicationListener = true;
}
}

/** @type {Function|undefined} */
get onversionnegotiation() {
assertIsQuicSession(this);
Expand Down Expand Up @@ -3483,6 +3531,7 @@ class QuicSession {
inner.ondatagramstatus = undefined;
inner.onpathvalidation = undefined;
inner.onsessionticket = undefined;
inner.onapplication = undefined;
inner.onkeylog = undefined;
inner.onversionnegotiation = undefined;
inner.onhandshake = undefined;
Expand Down Expand Up @@ -3704,6 +3753,23 @@ class QuicSession {
safeCallbackInvoke(inner.onsessionticket, this, ticket);
}

/**
* @param {ApplicationOptions} applicationoptions
*/
[kSessionApplication](applicationoptions) {
if (this.destroyed) return;
if (onSessionApplicationChannel.hasSubscribers) {
onSessionApplicationChannel.publish({
__proto__: null,
applicationoptions,
session: this,
});
}
const inner = this.#inner;
if (typeof inner.onapplication === 'function')
safeCallbackInvoke(inner.onapplication, this, applicationoptions);
}

/**
* @param {Buffer} token
* @param {SocketAddress} address
Expand Down Expand Up @@ -4222,6 +4288,7 @@ class QuicEndpoint {
ongoaway,
onkeylog,
onqlog,
onapplication,
// Stream-level callbacks applied to each incoming stream.
onheaders,
ontrailers,
Expand All @@ -4247,6 +4314,7 @@ class QuicEndpoint {
ongoaway,
onkeylog,
onqlog,
onapplication,
onheaders,
ontrailers,
oninfo,
Expand Down Expand Up @@ -4955,6 +5023,8 @@ function processSessionOptions(options, config = kEmptyObject) {
ongoaway,
onkeylog,
onqlog,
onapplication,
// Application level options changed, e.g. HTTP/3 settings related
// Stream-level callbacks.
onheaders,
ontrailers,
Expand Down Expand Up @@ -5060,6 +5130,7 @@ function processSessionOptions(options, config = kEmptyObject) {
ongoaway,
onkeylog,
onqlog,
onapplication,
onheaders,
ontrailers,
oninfo,
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/quic/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ class QuicSessionState {
static #LISTENER_SESSION_TICKET = 1 << 3;
static #LISTENER_NEW_TOKEN = 1 << 4;
static #LISTENER_ORIGIN = 1 << 5;
static #LISTENER_APPLICATION = 1 << 6;

#getListenerFlag(flag) {
const handle = this.#handle;
Expand All @@ -367,6 +368,14 @@ class QuicSessionState {
val ? (current | flag) : (current & ~flag), kIsLittleEndian);
}

/** @type {boolean} */
get hasApplicationListener() {
return this.#getListenerFlag(QuicSessionState.#LISTENER_APPLICATION);
}
set hasApplicationListener(val) {
this.#setListenerFlag(QuicSessionState.#LISTENER_APPLICATION, val);
}

/** @type {boolean} */
get hasPathValidationListener() {
return this.#getListenerFlag(QuicSessionState.#LISTENER_PATH_VALIDATION);
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/quic/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const kRemoveSession = Symbol('kRemoveSession');
const kRemoveStream = Symbol('kRemoveStream');
const kReset = Symbol('kReset');
const kSendHeaders = Symbol('kSendHeaders');
const kSessionApplication = Symbol('kSessionApplication');
const kSessionTicket = Symbol('kSessionTicket');
const kTrailers = Symbol('kTrailers');
const kVersionNegotiation = Symbol('kVersionNegotiation');
Expand Down Expand Up @@ -88,6 +89,7 @@ module.exports = {
kRemoveStream,
kReset,
kSendHeaders,
kSessionApplication,
kSessionTicket,
kTrailers,
kVersionNegotiation,
Expand Down
1 change: 1 addition & 0 deletions src/quic/bindingdata.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class SessionManager;
#define QUIC_JS_CALLBACKS(V) \
V(endpoint_close, EndpointClose) \
V(session_close, SessionClose) \
V(session_application, SessionApplication) \
V(session_early_data_rejected, SessionEarlyDataRejected) \
V(session_goaway, SessionGoaway) \
V(session_datagram, SessionDatagram) \
Expand Down
2 changes: 2 additions & 0 deletions src/quic/http3.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,8 @@ class Http3ApplicationImpl final : public Session::Application {
Debug(&session(),
"HTTP/3 application received updated settings: %s",
options_);
// The settings are part of the application
session().EmitApplication();
}

bool started_ = false;
Expand Down
28 changes: 28 additions & 0 deletions src/quic/session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ enum class SessionListenerFlags : uint32_t {
SESSION_TICKET = 1 << 3,
NEW_TOKEN = 1 << 4,
ORIGIN = 1 << 5,
APPLICATION = 1 << 6
};

inline SessionListenerFlags operator|(SessionListenerFlags a,
Expand Down Expand Up @@ -3601,6 +3602,33 @@ void Session::EmitSessionTicket(Store&& ticket) {
}
}

void Session::EmitApplication() {
if (is_destroyed()) return;
if (!env()->can_call_into_js()) return;

if (!has_application()) {
// The application has not yet been selected (ALPN negotiation is not
// yet complete on the server) or the session has been destroyed. In
// either case, the application options are not available.
// Should not happen, but we bail out
return;
}
Comment thread
martenrichter marked this conversation as resolved.

if (!HasListenerFlag(impl_->state()->listener_flags,
SessionListenerFlags::APPLICATION)) [[likely]] {
return;
}

CallbackScope<Session> cb_scope(this);

Local<Value> argv;
auto& options = application().options();
if (options.ToObject(env()).ToLocal(&argv)) {
MakeCallback(
BindingData::Get(env()).session_application_callback(), 1, &argv);
}
}

void Session::DestroyAllStreams(const QuicError& error) {
DCHECK(!is_destroyed());
// Copy the streams map since streams remove themselves during
Expand Down
1 change: 1 addition & 0 deletions src/quic/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
const uint32_t* sv,
size_t nsv);
void EmitApplication();
void DatagramStatus(datagram_id datagramId, DatagramStatus status);
void DatagramReceived(const uint8_t* data,
size_t datalen,
Expand Down
Loading
Loading