From b56953b340eabd6d05cf733d6579efc59e7da757 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 08:12:08 +0200 Subject: [PATCH 01/19] Import nghttp3 webtransport PR --- .../nghttp3/lib/includes/nghttp3/nghttp3.h | 317 ++++- .../nghttp3/lib/includes/nghttp3/version.h | 2 +- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 1262 ++++++++++++++++- deps/ngtcp2/nghttp3/lib/nghttp3_conn.h | 43 +- deps/ngtcp2/nghttp3/lib/nghttp3_conv.c | 6 + deps/ngtcp2/nghttp3/lib/nghttp3_conv.h | 7 + deps/ngtcp2/nghttp3/lib/nghttp3_err.c | 9 + deps/ngtcp2/nghttp3/lib/nghttp3_frame.c | 38 + deps/ngtcp2/nghttp3/lib/nghttp3_frame.h | 76 + deps/ngtcp2/nghttp3/lib/nghttp3_http.c | 5 + deps/ngtcp2/nghttp3/lib/nghttp3_http.h | 2 + deps/ngtcp2/nghttp3/lib/nghttp3_stream.c | 241 ++++ deps/ngtcp2/nghttp3/lib/nghttp3_stream.h | 49 + deps/ngtcp2/nghttp3/lib/nghttp3_wt.c | 95 ++ deps/ngtcp2/nghttp3/lib/nghttp3_wt.h | 86 ++ 15 files changed, 2194 insertions(+), 44 deletions(-) create mode 100644 deps/ngtcp2/nghttp3/lib/nghttp3_wt.c create mode 100644 deps/ngtcp2/nghttp3/lib/nghttp3_wt.h diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h index 83999a34d17b81..b08ab60061e1b8 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h @@ -335,6 +335,27 @@ typedef uint64_t nghttp3_duration; * that might generating excessive load. */ #define NGHTTP3_ERR_H3_EXCESSIVE_LOAD -610 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_H3_MESSAGE_ERROR` indicates that HTTP message + * was malformed. + */ +#define NGHTTP3_ERR_H3_MESSAGE_ERROR -611 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_WT_SESSION_GONE` indicates that WebTransport + * session was terminated or rejected. + */ +#define NGHTTP3_ERR_WT_SESSION_GONE -612 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED` indicates that + * buffering WebTransport data stream was rejected. + */ +#define NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED -613 /** * @macro * @@ -503,6 +524,38 @@ typedef uint64_t nghttp3_duration; * error code ``QPACK_DECODER_STREAM_ERROR``. */ #define NGHTTP3_QPACK_DECODER_STREAM_ERROR 0x0202 +/** + * @macro + * + * :macro:`NGHTTP3_WT_BUFFERED_STREAM_REJECTED` is WebTransport error + * code ``WT_BUFFERED_STREAM_REJECTED``. + */ +#define NGHTTP3_WT_BUFFERED_STREAM_REJECTED 0x3994BD84 +/** + * @macro + * + * :macro:`NGHTTP3_WT_SESSION_GONE` is WebTransport error code + * ``WT_SESSION_GONE``. + */ +#define NGHTTP3_WT_SESSION_GONE 0x170D7B68 +/** + * @macro + * + * :macro:`NGHTTP3_WT_ALPN_ERROR` is WebTransport error code + * ``WT_ALPN_ERROR``. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ +#define NGHTTP3_WT_ALPN_ERROR 0x0817B3DD +/** + * @macro + * + * :macro:`NGHTTP3_WT_REQUIREMENTS_NOT_MET` is WebTransport error code + * ``WT_REQUIREMENTS_NOT_MET``. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ +#define NGHTTP3_WT_REQUIREMENTS_NOT_MET 0x212C0D48 /** * @functypedef @@ -1899,6 +1952,16 @@ typedef struct nghttp3_settings { * .. version-added:: 1.13.0 */ nghttp3_qpack_indexing_strat qpack_indexing_strat; + /** + * :member:`wt_enabled`, if set to nonzero, enables WebTransport. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + * + * TODO For client, it might be better to always enable + * WebTransport. Only draft version of client needs to send + * SETTINGS_WT_ENABLED. + */ + uint8_t wt_enabled; } nghttp3_settings; #define NGHTTP3_PROTO_SETTINGS_V1 1 @@ -1939,6 +2002,12 @@ typedef struct nghttp3_proto_settings { * Datagrams (see :rfc:`9297`). */ uint8_t h3_datagram; + /** + * :member:`wt_enabled`, if set to nonzero, enables WebTransport. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ + uint8_t wt_enabled; } nghttp3_proto_settings; /** @@ -2240,6 +2309,69 @@ typedef int (*nghttp3_recv_settings2)(nghttp3_conn *conn, const nghttp3_proto_settings *settings, void *conn_user_data); +/** + * @functypedef + * + * :type:`nghttp3_recv_wt_data` is a callback function which is + * invoked when data is received on WebTransport data stream. + * |session_id| is the WebTransport session ID. |stream_id| is the + * stream ID of the WebTransport data stream. |data| points to the + * received data, and its length is |datalen|. + * + * The application is responsible for increasing flow control credit + * (say, increasing by |datalen| bytes). + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_recv_wt_data)(nghttp3_conn *conn, int64_t session_id, + int64_t stream_id, const uint8_t *data, + size_t datalen, void *conn_user_data, + void *stream_user_data); + +/** + * @functypedef + * + * :type:`nghttp3_wt_data_stream_open` is a callback function which is + * invoked when a remote stream denoted by |stream_id| is identified + * as WebTransport data stream that belongs to WebTransport session + * identified by |session_id|. This callback function is called after + * WebTransport session is confirmed. + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_wt_data_stream_open)(nghttp3_conn *conn, + int64_t session_id, + int64_t stream_id, + void *conn_user_data, + void *stream_user_data); + +/** + * @functypedef + * + * :type:`nghttp3_recv_wt_close_session` is a callback function which + * is invoked when WT_CLOSE_SESSION Capsule is received. The + * WebTransport session is identified by |session_id|. + * |wt_error_code| is Application Error Code. The buffer pointed by + * |msg| of length |msglen| contains Application Error Message. + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_recv_wt_close_session)(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, size_t msglen, + void *conn_user_data, + void *stream_user_data); + #define NGHTTP3_CALLBACKS_V1 1 #define NGHTTP3_CALLBACKS_V2 2 #define NGHTTP3_CALLBACKS_V3 3 @@ -2375,6 +2507,22 @@ typedef struct nghttp3_callbacks { * .. version-added:: 1.14.0 */ nghttp3_recv_settings2 recv_settings2; + /** + * :member:`recv_wt_data` is a callback function which is invoked + * when data on WebTransport data stream is received. + */ + nghttp3_recv_wt_data recv_wt_data; + /** + * :member:`wt_data_stream_open` is a callback function which is + * invoked when a remote stream is identified as WebTransport data + * stream. + */ + nghttp3_wt_data_stream_open wt_data_stream_open; + /** + * :member:`recv_wt_close_session` is a callback function which is + * invoked when WT_CLOSE_SESSION Capsule is received. + */ + nghttp3_recv_wt_close_session recv_wt_close_session; } nghttp3_callbacks; /** @@ -3353,6 +3501,165 @@ NGHTTP3_EXTERN int nghttp3_conn_is_drained(nghttp3_conn *conn); */ NGHTTP3_EXTERN int nghttp3_conn_is_drained2(const nghttp3_conn *conn); +/** + * @function + * + * `nghttp3_conn_submit_wt_request` works like + * `nghttp3_conn_submit_request`, but it is specifically tailored for + * WebTransport session establishment. |nva| of length |nvlen| + * specifies HTTP request header fields. They must contain at least + * the following fields: + * + * - :method = "CONNECT" + * - :scheme = "https" + * - :protocol = "webtransport" + * - :authority + * - :path + * + * The application must also set the following settings: + * + * - :member:`nghttp3_settings.h3_datagram` = 1 + * - :member:`nghttp3_settings.wt_enabled` = 1 + * + * It also must send the following QUIC transport parameters: + * + * - max_datagram_frame_size > 0 + * - reset_stream_at + * + * The application should wait for SETTINGS frame from server and make + * sure that it satisfies server-side requirements for WebTransport. + * + * After receiving 2xx response from server, WebTransport session is + * established. `nghttp3_conn_open_wt_data_stream` is used to open + * WebTransport data streams. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_submit_wt_request(nghttp3_conn *conn, + int64_t stream_id, + const nghttp3_nv *nva, + size_t nvlen, + void *stream_user_data); + +/** + * @function + * + * `nghttp3_conn_submit_wt_response` works like + * `nghttp3_conn_submit_response`, but it is specifically tailored for + * WebTransport session establishment. |nva| of length |nvlen| + * specifies HTTP response header fields. It must contain 2xx status + * code in :status field. + * + * The application should make sure that the stream denoted by + * |stream_id| is a request stream that requests WebTransport session + * establishment. If this function is called inside + * :member:`nghttp3_callbacks.end_headers` callback, + * `nghttp3_conn_server_confirm_wt_session` is called internally, and + * it establishes WebTransport session. If this function is called + * outside of the callback, the application must call + * `nghttp3_conn_server_confirm_wt_session` after calling this + * function. + * + * If `nghttp3_conn_submit_response` is used against the WebTransport + * upgrade request, it means refusal of the request regardless of HTTP + * status code. The application is responsible to set non-2xx status + * code when `nghttp3_conn_submit_response` is used. If + * `nghttp3_conn_submit_response` is called from + * :member:`nghttp3_callbacks.end_headers` callback, + * :member:`nghttp3_callbacks.stop_sending` callback is automatically + * called. If `nghttp3_conn_submit_response` is called outside of the + * :member:`nghttp3_callbacks.end_headers` callback, + * :member:`nghttp3_callbacks.stop_sending` is not called + * automatically. The application should tell QUIC stack to send + * STOP_SENDING frame to this stream. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_submit_wt_response(nghttp3_conn *conn, + int64_t stream_id, + const nghttp3_nv *nva, + size_t nvlen); + +/** + * @function + * + * `nghttp3_conn_server_confirm_wt_session` establishes WebTransport + * session. This should be called after + * `nghttp3_conn_submit_wt_response` call if it is not called inside + * `nghttp3_callbacks.end_headers` callback. + * + * Only server can call this function. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_server_confirm_wt_session(nghttp3_conn *conn, + int64_t session_id, + nghttp3_tstamp ts); + +/** + * @function + * + * `nghttp3_conn_open_wt_data_stream` opens WebTransport data stream. + * |session_id| is the stream ID that established WebTransport + * session. |stream_id| is the stream ID to write data, and it can be + * both bidirectional and unidirectional. |dr| must not be NULL, and + * it must have non-NULL callback. + * + * This function can be also used to start writing to the + * bidirectional stream initiated by the remote endpoint. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_open_wt_data_stream( + nghttp3_conn *conn, int64_t session_id, int64_t stream_id, + const nghttp3_data_reader *dr, void *stream_user_data); + +/** + * @function + * + * `nghttp3_conn_close_wt_session` closes WebTransport session denoted + * by |session_id| which is the stream ID that established + * WebTransport session. |wt_error_code| is WebTransport error code. + * Upon calling this function, all existing WebTransport data streams + * are shutdown. |msg| of |msglen| bytes is the application error + * message, which is optional. |msglen| must be less than or equal to + * 1024. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_close_wt_session(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen); + +/** + * @function + * + * `nghttp3_conn_get_stream_wt_session_id` returns the WebTransport + * session ID of a stream denoted by |stream_id| if it is WebTransport + * data stream. If the stream is not found, it is not a WebTransport + * data stream, or it is unable to get session ID, this function + * returns -1. + */ +NGHTTP3_EXTERN int64_t nghttp3_conn_get_stream_wt_session_id( + const nghttp3_conn *conn, int64_t stream_id); + /** * @function * @@ -3447,8 +3754,6 @@ NGHTTP3_EXTERN int nghttp3_err_is_fatal(int liberr); * that contains a valid variable-length unsigned integer. Use * `nghttp3_get_uvarintlen` to get the number of bytes to successfully * decode an integer. - * - * .. version-added:: 1.17.0 */ NGHTTP3_EXTERN const uint8_t *nghttp3_get_uvarint(uint64_t *dest, const uint8_t *p); @@ -3460,8 +3765,6 @@ NGHTTP3_EXTERN const uint8_t *nghttp3_get_uvarint(uint64_t *dest, * read variable-length unsigned integer starting at |p|. |p| must * not be NULL. This function only reads the single byte from the * buffer pointed by |p|, and determines the number of bytes to read. - * - * .. version-added:: 1.17.0 */ NGHTTP3_EXTERN size_t nghttp3_get_uvarintlen(const uint8_t *p); @@ -3475,8 +3778,6 @@ NGHTTP3_EXTERN size_t nghttp3_get_uvarintlen(const uint8_t *p); * that contains a valid variable-length unsigned integer. Use * `nghttp3_get_uvarintlen` to get the number of bytes to successfully * decode an integer. - * - * .. version-added:: 1.17.0 */ NGHTTP3_EXTERN const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p); @@ -3490,8 +3791,6 @@ NGHTTP3_EXTERN const uint8_t *nghttp3_get_varint(int64_t *dest, * the buffer pointed by |p| has sufficient capacity to encode |n|. * To know the required capacity, use `nghttp3_put_uvarintlen`. |n| * must be less than or equal to (1 << 62) - 1. - * - * .. version-added:: 1.17.0 */ NGHTTP3_EXTERN uint8_t *nghttp3_put_uvarint(uint8_t *p, uint64_t n); @@ -3501,8 +3800,6 @@ NGHTTP3_EXTERN uint8_t *nghttp3_put_uvarint(uint8_t *p, uint64_t n); * `nghttp3_put_uvarintlen` returns the required number of bytes to * encode |n| in variable-length unsigned integer encoding. |n| must * be less than or equal to (1 << 62) - 1. - * - * .. version-added:: 1.17.0 */ NGHTTP3_EXTERN size_t nghttp3_put_uvarintlen(uint64_t n); diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h index 6f9f04e7426b95..f846051ac9966f 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h @@ -31,7 +31,7 @@ * * Version number of the nghttp3 library release. */ -#define NGHTTP3_VERSION "1.17.0" +#define NGHTTP3_VERSION "1.17.0-DEV" /** * @macro diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index d1b6355bb7e036..1201a28a300f2a 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -36,9 +36,22 @@ #include "nghttp3_unreachable.h" #include "nghttp3_settings.h" #include "nghttp3_callbacks.h" +#include "nghttp3_wt.h" nghttp3_objalloc_def(chunk, nghttp3_chunk, oplent) +/* + * conn_remote_stream returns nonzero if |stream_id| is a remote + * stream ID. + */ +static int conn_remote_stream(const nghttp3_conn *conn, int64_t stream_id) { + if (conn->server) { + return !(stream_id & 0x1); + } + + return stream_id & 0x1; +} + /* * conn_remote_stream_uni returns nonzero if |stream_id| is remote * unidirectional stream ID. @@ -50,6 +63,30 @@ static int conn_remote_stream_uni(const nghttp3_conn *conn, int64_t stream_id) { return (stream_id & 0x03) == 0x03; } +static int conn_wt_enabled(const nghttp3_conn *conn) { + const nghttp3_settings *local_settings = &conn->local.settings; + const nghttp3_proto_settings *remote_settings = &conn->remote.settings; + + if (!local_settings->wt_enabled || !local_settings->h3_datagram) { + return 0; + } + + if (conn->server) { + return (!(conn->flags & NGHTTP3_CONN_FLAG_SETTINGS_RECVED) || + /* TODO client sends SETTINGS_WT_ENABLED for draft + versions only. But some client implementations do not + send it. For interop purpose, do not require this + remote setting for now. */ + (/* remote_settings->wt_enabled && */ remote_settings + ->h3_datagram)) && + local_settings->enable_connect_protocol; + } + + return remote_settings->wt_enabled && + remote_settings->enable_connect_protocol && + remote_settings->h3_datagram; +} + static int conn_call_begin_headers(nghttp3_conn *conn, nghttp3_stream *stream) { int rv; @@ -251,6 +288,70 @@ static int conn_call_end_origin(nghttp3_conn *conn) { return 0; } +static int conn_call_recv_data(nghttp3_conn *conn, const nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { + int rv; + + if (!conn->callbacks.recv_data) { + return 0; + } + + rv = conn->callbacks.recv_data(conn, stream->node.id, data, datalen, + conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int conn_call_recv_wt_data(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { + nghttp3_wt_session *wt_session; + int rv; + + if (!conn->callbacks.recv_wt_data) { + return 0; + } + + wt_session = stream->wt.session; + + assert(wt_session); + + rv = conn->callbacks.recv_wt_data(conn, wt_session->session_id, + stream->node.id, data, datalen, + conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int conn_call_wt_data_stream_open(nghttp3_conn *conn, + const nghttp3_stream *stream) { + nghttp3_wt_session *wt_session; + int rv; + + if (!conn->callbacks.wt_data_stream_open) { + return 0; + } + + wt_session = stream->wt.session; + + assert(wt_session); + + rv = conn->callbacks.wt_data_stream_open(conn, wt_session->session_id, + stream->node.id, conn->user_data, + stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + static int conn_glitch_ratelim_drain(nghttp3_conn *conn, uint64_t n, nghttp3_tstamp ts) { if (ts == UINT64_MAX) { @@ -401,11 +502,25 @@ int nghttp3_conn_server_new_versioned(nghttp3_conn **pconn, return 0; } +static void remove_wt_session_ref(nghttp3_wt_session *wt_session) { + nghttp3_stream *stream; + + for (stream = wt_session->head; stream; stream = stream->wt.next) { + assert(stream->wt.session == wt_session); + + stream->wt.session = NULL; + } +} + static int free_stream(void *data, void *ptr) { nghttp3_stream *stream = data; (void)ptr; + if (nghttp3_stream_wt_ctrl(stream)) { + remove_wt_session_ref(stream->wt.session); + } + nghttp3_stream_del(stream); return 0; @@ -501,6 +616,10 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, return rv; } + if (conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + if ((conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_QUEUED) && conn->tx.goaway_id <= stream_id) { stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; @@ -527,7 +646,9 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, } stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; - } else if (nghttp3_server_stream_uni(stream_id)) { + } else if (nghttp3_server_stream_uni(stream_id) || + (conn_wt_enabled(conn) && + nghttp3_server_stream_bidi(stream_id))) { if (srclen == 0 && fin) { return 0; } @@ -537,6 +658,10 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, return rv; } + if (!(stream_id & 0x2) && conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + stream->rx.hstate = NGHTTP3_HTTP_STATE_RESP_INITIAL; } else { /* client doesn't expect to receive new bidirectional stream or @@ -545,10 +670,12 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, } } else if (conn->server) { assert(nghttp3_client_stream_bidi(stream_id) || - nghttp3_client_stream_uni(stream_id)); + nghttp3_client_stream_uni(stream_id) || + (conn_wt_enabled(conn) && nghttp3_server_stream_bidi(stream_id))); } else { assert(nghttp3_client_stream_bidi(stream_id) || - nghttp3_server_stream_uni(stream_id)); + nghttp3_server_stream_uni(stream_id) || + (conn_wt_enabled(conn) && nghttp3_server_stream_bidi(stream_id))); } if (srclen == 0 && !fin) { @@ -613,6 +740,12 @@ static nghttp3_ssize conn_read_type(nghttp3_conn *conn, nghttp3_stream *stream, conn->flags |= NGHTTP3_CONN_FLAG_QPACK_DECODER_OPENED; stream->type = NGHTTP3_STREAM_TYPE_QPACK_DECODER; break; + case NGHTTP3_STREAM_TYPE_WT_STREAM: + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_H3_STREAM_CREATION_ERROR; + } + stream->type = NGHTTP3_STREAM_TYPE_WT_STREAM; + break; default: stream->type = NGHTTP3_STREAM_TYPE_UNKNOWN; break; @@ -702,6 +835,10 @@ nghttp3_ssize nghttp3_conn_read_uni(nghttp3_conn *conn, nghttp3_stream *stream, } nconsumed = nghttp3_conn_read_qpack_decoder(conn, src, srclen); break; + case NGHTTP3_STREAM_TYPE_WT_STREAM: + nconsumed = + nghttp3_conn_read_wt_stream_uni(conn, stream, src, srclen, fin, ts); + break; case NGHTTP3_STREAM_TYPE_UNKNOWN: nconsumed = (nghttp3_ssize)srclen; break; @@ -794,7 +931,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, case NGHTTP3_FRAME_SETTINGS: /* SETTINGS frame might be empty. */ if (rstate->left == 0) { - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -892,7 +1029,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, case NGHTTP3_CTRL_STREAM_STATE_SETTINGS: for (;;) { if (rstate->left == 0) { - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -1010,7 +1147,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; } - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -1335,6 +1472,35 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, return (nghttp3_ssize)nconsumed; } +static int conn_unlink_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream) { + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + nghttp3_stream *stream, *next; + int rv; + (void)rv; + + for (stream = wt_session->head; stream;) { + next = stream->wt.next; + + assert(stream->wt.session); + + stream->wt.session = NULL; + stream->wt.prev = stream->wt.next = NULL; + + rv = nghttp3_conn_shutdown_wt_data_stream(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + stream = next; + } + + wt_session->head = NULL; + + return 0; +} + static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream) { int rv; @@ -1361,6 +1527,15 @@ static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream) { } } + if (nghttp3_stream_wt_ctrl(stream)) { + rv = conn_unlink_wt_session(conn, stream); + if (rv != 0) { + return rv; + } + } else if (nghttp3_stream_wt_data(stream)) { + nghttp3_wt_session_remove_stream(stream->wt.session, stream); + } + if (conn->server && nghttp3_client_stream_bidi(stream->node.id)) { assert(conn->remote.bidi.num_streams > 0); @@ -1494,6 +1669,7 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, int rv; nghttp3_stream_read_state *rstate = &stream->rstate; nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_stream *wt_ctrl_stream; nghttp3_ssize nread; size_t nconsumed = 0; int busy = 0; @@ -1505,7 +1681,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return (nghttp3_ssize)srclen; } - if (stream->flags & NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED) { + if (stream->flags & (NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED | + NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED)) { *pnproc = 0; if (srclen == 0) { @@ -1614,6 +1791,42 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, } rstate->state = NGHTTP3_REQ_STREAM_STATE_HEADERS; + break; + case NGHTTP3_EXFR_WT_STREAM_BIDI: + if (!nghttp3_stream_wt_data(stream) && + !(stream->flags & NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA)) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + + if (conn->server) { + if (stream->rx.hstate != NGHTTP3_HTTP_STATE_REQ_INITIAL) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + } else if (stream->rx.hstate != NGHTTP3_HTTP_STATE_RESP_INITIAL) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + /* rstate->left is Session ID */ + rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); + if (rv != 0) { + if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + return rv; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + break; + } + + rstate->state = NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA; + break; case NGHTTP3_FRAME_PUSH_PROMISE: /* We do not support push */ case NGHTTP3_FRAME_CANCEL_PUSH: @@ -1633,6 +1846,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return NGHTTP3_ERR_H3_EXCESSIVE_LOAD; } + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + /* TODO Handle reserved frame type */ busy = 1; rstate->state = NGHTTP3_REQ_STREAM_STATE_IGN_FRAME; @@ -1641,11 +1856,29 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, break; case NGHTTP3_REQ_STREAM_STATE_DATA: len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); - rv = nghttp3_conn_on_data(conn, stream, p, len); - if (rv != 0) { - return rv; + nread = nghttp3_conn_on_data(conn, stream, p, len); + if (nread < 0) { + if (nread != NGHTTP3_ERR_WT_SESSION_GONE) { + return nread; + } + + rv = nghttp3_conn_shutdown_wt_session(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + /* Now that the stream is in + NGHTTP3_REQ_STREAM_STATE_IGN_REST, end_stream callback is + not called. */ + + /* Pretend that all stream data have been consumed */ + nconsumed += len; + + goto almost_done; } p += len; + nconsumed += (size_t)nread; rstate->left -= len; if (rstate->left) { @@ -1717,7 +1950,9 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return rv; } } - /* fall through */ + + rv = conn_call_end_headers(conn, stream, p == end && fin); + break; case NGHTTP3_HTTP_STATE_RESP_HEADERS_BEGIN: rv = conn_call_end_headers(conn, stream, p == end && fin); break; @@ -1739,6 +1974,62 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, nghttp3_stream_read_state_reset(rstate); + if (conn->server) { + if (stream->rx.hstate == NGHTTP3_HTTP_STATE_REQ_HEADERS_END) { + if (stream->wt.session && (stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED)) { + /* Server has submitted WebTransport session. */ + rv = nghttp3_conn_on_wt_session_confirmed(conn, stream, ts); + if (rv != 0) { + return rv; + } + } else if (stream->rx.http.flags & NGHTTP3_HTTP_FLAG_WEBTRANSPORT) { + if (stream->flags & NGHTTP3_STREAM_FLAG_RESP_SUBMITTED) { + /* Server refused WebTransport upgrade request */ + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = conn_call_stop_sending(conn, stream, NGHTTP3_H3_NO_ERROR); + if (rv != 0) { + return rv; + } + } else { + /* Server has not submitted response */ + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + *pnproc = (size_t)(p - src); + + return (nghttp3_ssize)nconsumed; + } + } + } + } else if (stream->rx.hstate == NGHTTP3_HTTP_STATE_RESP_HEADERS_END && + stream->wt.session) { + if (stream->rx.http.status_code / 100 == 2) { + rv = nghttp3_conn_on_wt_session_confirmed(conn, stream, ts); + if (rv != 0) { + return rv; + } + } else { + /* Server refused WebTransport negotiation. Reset the session + stream. This could be a redirect, but client is instructed + not to follow the redirect automatically. Most of the + case, we cannot do anything but just close the stream. */ + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, + NGHTTP3_H3_REQUEST_CANCELLED); + if (rv != 0) { + return rv; + } + } + } + break; case NGHTTP3_REQ_STREAM_STATE_IGN_FRAME: len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); @@ -1752,6 +2043,40 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, nghttp3_stream_read_state_reset(rstate); break; + case NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA: + rstate->state = NGHTTP3_REQ_STREAM_STATE_WT_DATA; + + assert(stream->wt.session); + + wt_ctrl_stream = + nghttp3_conn_find_stream(conn, stream->wt.session->session_id); + + if (!(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_CONFIRMED)) { + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + *pnproc = (size_t)(p - src); + + return (nghttp3_ssize)nconsumed; + } + + break; + case NGHTTP3_REQ_STREAM_STATE_WT_DATA: + rv = conn_call_recv_wt_data(conn, stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + + p = end; + + goto almost_done; case NGHTTP3_REQ_STREAM_STATE_IGN_REST: nconsumed += (size_t)(end - p); *pnproc = (size_t)(end - src); @@ -1771,10 +2096,16 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, if (rv != 0) { return rv; } + + /* Fall through */ + /* When a stream is closed without any data */ + case NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA: + case NGHTTP3_REQ_STREAM_STATE_WT_DATA: rv = conn_call_end_stream(conn, stream); if (rv != 0) { return rv; } + break; case NGHTTP3_REQ_STREAM_STATE_IGN_REST: break; @@ -1787,8 +2118,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return (nghttp3_ssize)nconsumed; } -int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, - const uint8_t *data, size_t datalen) { +nghttp3_ssize nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { int rv; rv = nghttp3_http_on_data_chunk(stream, datalen); @@ -1796,17 +2127,21 @@ int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, return rv; } - if (!conn->callbacks.recv_data) { - return 0; + if (!stream->wt.session) { + return conn_call_recv_data(conn, stream, data, datalen); } - rv = conn->callbacks.recv_data(conn, stream->node.id, data, datalen, - conn->user_data, stream->user_data); + /* The stream data must be buffered until WebTransport session has + been confirmed. */ + assert(stream->wt.session->flags & NGHTTP3_WT_SESSION_FLAG_CONFIRMED); + + rv = nghttp3_conn_read_wt_ctrl_stream(conn, stream, data, datalen); if (rv != 0) { - return NGHTTP3_ERR_CALLBACK_FAILURE; + return rv; } - return 0; + /* WebTransport control stream has consumed all data */ + return (nghttp3_ssize)datalen; } static nghttp3_pq *conn_get_sched_pq(nghttp3_conn *conn, nghttp3_tnode *tnode) { @@ -2001,6 +2336,14 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, dest->h3_datagram = (uint8_t)ent->value; break; + case NGHTTP3_SETTINGS_ID_WT_ENABLED: + /* compat for pre draft-15 */ + case NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS: + case NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7: + /* compat for ancient draft */ + case NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2: + dest->wt_enabled = ent->value != 0; + break; case NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH: case NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS: case NGHTTP3_H2_SETTINGS_ID_INITIAL_WINDOW_SIZE: @@ -2014,6 +2357,37 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, return 0; } +static int abort_wt_session(void *data, void *ptr) { + nghttp3_conn *conn = ptr; + nghttp3_stream *stream = data; + + if (!nghttp3_stream_wt_ctrl(stream)) { + return 0; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return nghttp3_conn_abort_stream(conn, stream, + NGHTTP3_H3_GENERAL_PROTOCOL_ERROR); +} + +int nghttp3_conn_on_settings_received(nghttp3_conn *conn) { + int rv; + + conn->flags |= NGHTTP3_CONN_FLAG_SETTINGS_RECVED; + + rv = conn_call_recv_settings(conn); + if (rv != 0) { + return rv; + } + + if (!conn->local.settings.wt_enabled || conn_wt_enabled(conn)) { + return 0; + } + + return nghttp3_map_each(&conn->streams, abort_wt_session, conn); +} + static int conn_on_priority_update_stream(nghttp3_conn *conn, const nghttp3_frame_priority_update *fr) { @@ -2055,6 +2429,11 @@ conn_on_priority_update_stream(nghttp3_conn *conn, stream->node.pri = fr->pri; stream->flags |= NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED; + + if (conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; return 0; @@ -2323,7 +2702,7 @@ nghttp3_ssize nghttp3_conn_writev_stream(nghttp3_conn *conn, return ncnt; } - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && !nghttp3_stream_require_schedule(stream)) { nghttp3_conn_unschedule_stream(conn, stream); } @@ -2362,7 +2741,7 @@ int nghttp3_conn_add_write_offset(nghttp3_conn *conn, int64_t stream_id, stream->unscheduled_nwrite += n; - if (!nghttp3_client_stream_bidi(stream->node.id)) { + if (!nghttp3_stream_schedulable(stream)) { return 0; } @@ -2404,6 +2783,22 @@ int nghttp3_conn_update_ack_offset(nghttp3_conn *conn, int64_t stream_id, return nghttp3_stream_update_ack_offset(stream, offset); } +static nghttp3_ssize wt_session_read_data(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, + uint32_t *pflags, + void *conn_user_data, + void *stream_user_data) { + (void)conn; + (void)stream_id; + (void)vec; + (void)veccnt; + (void)pflags; + (void)conn_user_data; + (void)stream_user_data; + + return NGHTTP3_ERR_WOULDBLOCK; +} + static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream, const nghttp3_nv *nva, size_t nvlen, const nghttp3_data_reader *dr) { @@ -2428,7 +2823,7 @@ static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream, .nvlen = nvlen, }; - if (dr) { + if (dr && dr->read_data != wt_session_read_data) { rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { return rv; @@ -2555,6 +2950,8 @@ int nghttp3_conn_submit_response(nghttp3_conn *conn, int64_t stream_id, stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; } + stream->flags |= NGHTTP3_STREAM_FLAG_RESP_SUBMITTED; + return conn_submit_headers_data(conn, stream, nva, nvlen, dr); } @@ -2632,14 +3029,27 @@ int nghttp3_conn_shutdown(nghttp3_conn *conn) { } int nghttp3_conn_reject_stream(nghttp3_conn *conn, nghttp3_stream *stream) { + return nghttp3_conn_abort_stream(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); +} + +int nghttp3_conn_abort_stream(nghttp3_conn *conn, nghttp3_stream *stream, + uint64_t error_code) { int rv; + int remote_uni = conn_remote_stream_uni(conn, stream->node.id); + int bidi = !nghttp3_stream_uni(stream->node.id); - rv = conn_call_stop_sending(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); - if (rv != 0) { - return rv; + if (remote_uni || bidi) { + rv = conn_call_stop_sending(conn, stream, error_code); + if (rv != 0) { + return rv; + } + } + + if (remote_uni) { + return 0; } - return conn_call_reset_stream(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); + return conn_call_reset_stream(conn, stream, error_code); } void nghttp3_conn_block_stream(nghttp3_conn *conn, int64_t stream_id) { @@ -2681,7 +3091,7 @@ int nghttp3_conn_unblock_stream(nghttp3_conn *conn, int64_t stream_id) { stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_FC_BLOCKED; - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && nghttp3_stream_require_schedule(stream)) { return nghttp3_conn_ensure_stream_scheduled(conn, stream); } @@ -2715,7 +3125,7 @@ int nghttp3_conn_resume_stream(nghttp3_conn *conn, int64_t stream_id) { stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED; - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && nghttp3_stream_require_schedule(stream)) { return nghttp3_conn_ensure_stream_scheduled(conn, stream); } @@ -2731,8 +3141,7 @@ int nghttp3_conn_close_stream(nghttp3_conn *conn, int64_t stream_id, return NGHTTP3_ERR_STREAM_NOT_FOUND; } - if (nghttp3_stream_uni(stream_id) && - stream->type != NGHTTP3_STREAM_TYPE_UNKNOWN) { + if (nghttp3_stream_critical(stream)) { return NGHTTP3_ERR_H3_CLOSED_CRITICAL_STREAM; } @@ -2760,6 +3169,13 @@ int nghttp3_conn_shutdown_stream_read(nghttp3_conn *conn, int64_t stream_id) { } stream->flags |= NGHTTP3_STREAM_FLAG_SHUT_RD; + + /* If stream is WebTransport data stream, do not send QPACK Stream + Cancellation. */ + if (nghttp3_stream_wt_data(stream) || + (stream->flags & NGHTTP3_STREAM_FLAG_WT_DATA)) { + return 0; + } } return nghttp3_qpack_decoder_cancel_stream(&conn->qdec, stream_id); @@ -2999,5 +3415,789 @@ int nghttp3_conn_is_stream_flushed(const nghttp3_conn *conn, fr = nghttp3_ringbuf_get(&stream->frq, 0); - return fr->hd.type == NGHTTP3_FRAME_DATA; + return fr->hd.type == NGHTTP3_FRAME_DATA || + (fr->hd.type == NGHTTP3_FRAME_EX_WT && + fr->wt.fr.hd.type == NGHTTP3_EXFR_WT_STREAM_DATA); +} + +int nghttp3_conn_submit_wt_request(nghttp3_conn *conn, int64_t stream_id, + const nghttp3_nv *nva, size_t nvlen, + void *stream_user_data) { + int rv; + nghttp3_stream *stream; + + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_INVALID_STATE; + } + + rv = nghttp3_conn_submit_request( + conn, stream_id, nva, nvlen, + &(nghttp3_data_reader){.read_data = wt_session_read_data}, + stream_user_data); + if (rv != 0) { + return rv; + } + + stream = nghttp3_conn_find_stream(conn, stream_id); + + assert(stream); + + return nghttp3_conn_open_wt_session(conn, stream); +} + +int nghttp3_conn_submit_wt_response(nghttp3_conn *conn, int64_t stream_id, + const nghttp3_nv *nva, size_t nvlen) { + int rv; + nghttp3_stream *stream; + + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_INVALID_STATE; + } + + rv = nghttp3_conn_submit_response( + conn, stream_id, nva, nvlen, + &(nghttp3_data_reader){.read_data = wt_session_read_data}); + if (rv != 0) { + return rv; + } + + stream = nghttp3_conn_find_stream(conn, stream_id); + + if (!stream->wt.session) { + rv = nghttp3_conn_open_wt_session(conn, stream); + if (rv != 0) { + return rv; + } + } + + stream->wt.session->flags |= NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED; + + return 0; +} + +int nghttp3_conn_server_confirm_wt_session(nghttp3_conn *conn, + int64_t session_id, + nghttp3_tstamp ts) { + nghttp3_stream *wt_ctrl_stream; + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream) { + return NGHTTP3_ERR_STREAM_NOT_FOUND; + } + + assert(wt_ctrl_stream->wt.session); + assert(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED); + + wt_ctrl_stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + return nghttp3_conn_on_wt_session_confirmed(conn, wt_ctrl_stream, ts); +} + +int nghttp3_conn_open_wt_session(nghttp3_conn *conn, nghttp3_stream *stream) { + int rv; + + rv = nghttp3_wt_session_new(&stream->wt.session, stream->node.id, conn->mem); + if (rv != 0) { + return rv; + } + + return 0; +} + +int nghttp3_conn_open_wt_data_stream(nghttp3_conn *conn, int64_t session_id, + int64_t stream_id, + const nghttp3_data_reader *dr, + void *stream_user_data) { + nghttp3_stream *stream, *wt_ctrl_stream; + nghttp3_wt_session *wt_session; + nghttp3_frame *fr; + uint64_t type; + int rv; + int remote_bidi = 0; + + if (conn->server) { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_server_stream_bidi(stream_id) || + nghttp3_server_stream_uni(stream_id)); + } else { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_server_stream_bidi(stream_id) || + nghttp3_client_stream_uni(stream_id)); + } + + /* TODO Check session flow control */ + + assert(dr); + + if (conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_RECVED) { + return NGHTTP3_ERR_CONN_CLOSING; + } + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream || !wt_ctrl_stream->wt.session) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + wt_session = wt_ctrl_stream->wt.session; + + stream = nghttp3_conn_find_stream(conn, stream_id); + if (stream) { + if (conn->server) { + assert(nghttp3_client_stream_bidi(stream_id)); + } else { + assert(nghttp3_server_stream_bidi(stream_id)); + } + + /* TODO verify that we do not start writing more than once. */ + + /* Normally, stream->wt.session is not NULL because we must + identify WT stream header first. The only exception is a + stream create by priority update on server side. But it must + be client initiated bidi stream, and we must wait for its WT + header. */ + if (!stream->wt.session) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WRITE_END_STREAM) { + return NGHTTP3_ERR_INVALID_STATE; + } + + remote_bidi = 1; + + if (stream_user_data) { + stream->user_data = stream_user_data; + } + + if (conn->server) { + stream->flags |= NGHTTP3_STREAM_FLAG_SERVER_PRIORITY_SET; + } + + assert(!nghttp3_tnode_is_scheduled(&stream->node)); + } else { + if (conn->server) { + assert(nghttp3_server_stream_bidi(stream_id) || + nghttp3_server_stream_uni(stream_id)); + } else { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_client_stream_uni(stream_id)); + } + + rv = nghttp3_conn_create_stream(conn, &stream, stream_id); + if (rv != 0) { + return rv; + } + + nghttp3_wt_session_add_stream(wt_session, stream); + + if (conn->server) { + stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; + } else { + stream->rx.hstate = NGHTTP3_HTTP_STATE_RESP_INITIAL; + } + + stream->user_data = stream_user_data; + + if (stream_id & 0x2) { + stream->flags |= NGHTTP3_STREAM_FLAG_SHUT_RD; + stream->type = NGHTTP3_STREAM_TYPE_WT_STREAM; + } + } + + stream->node.pri = (nghttp3_pri){ + .urgency = NGHTTP3_DEFAULT_URGENCY, + .inc = 1, + }; + + if (stream_id & 0x2) { + type = NGHTTP3_EXFR_WT_STREAM_UNI; + } else if (remote_bidi) { + type = NGHTTP3_EXFR_WT_STREAM_DATA; + } else { + type = NGHTTP3_EXFR_WT_STREAM_BIDI; + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA; + } + + rv = nghttp3_stream_frq_emplace(stream, &fr); + if (rv != 0) { + return rv; + } + + fr->wt = (nghttp3_frame_ex_wt){ + .type = NGHTTP3_FRAME_EX_WT, + .fr.wt_stream = + { + .type = type, + .session_id = session_id, + .dr = *dr, + }, + }; + + if (nghttp3_stream_require_schedule(stream)) { + return nghttp3_conn_ensure_stream_scheduled(conn, stream); + } + + return 0; +} + +int nghttp3_conn_close_wt_session(nghttp3_conn *conn, int64_t session_id, + uint32_t wt_error_code, const uint8_t *msg, + size_t msglen) { + nghttp3_stream *stream; + nghttp3_wt_session *wt_session; + nghttp3_frame *fr; + int rv; + + stream = nghttp3_conn_find_stream(conn, session_id); + if (stream == NULL) { + return NGHTTP3_ERR_STREAM_NOT_FOUND; + } + + if (!nghttp3_stream_wt_ctrl(stream)) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WRITE_END_STREAM) { + return NGHTTP3_ERR_INVALID_STATE; + } + + stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; + + wt_session = stream->wt.session; + + assert(!wt_session->tx.error_msg.base); + + if (msglen) { + wt_session->tx.error_msg.base = nghttp3_mem_malloc(conn->mem, msglen); + if (!wt_session->tx.error_msg.base) { + return NGHTTP3_ERR_NOMEM; + } + + memcpy(wt_session->tx.error_msg.base, msg, msglen); + wt_session->tx.error_msg.len = msglen; + } + + rv = nghttp3_stream_frq_emplace(stream, &fr); + if (rv != 0) { + return rv; + } + + fr->cpsl = (nghttp3_frame_ex_cpsl){ + .type = NGHTTP3_FRAME_EX_CPSL, + .fr.wt_close_session = + { + .type = NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION, + .error_code = wt_error_code, + .error_msg = wt_session->tx.error_msg, + }, + }; + + if (nghttp3_stream_require_schedule(stream)) { + rv = nghttp3_conn_schedule_stream(conn, stream); + if (rv != 0) { + return rv; + } + } + + rv = nghttp3_conn_shutdown_all_wt_data_streams(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return conn_call_stop_sending(conn, stream, NGHTTP3_WT_SESSION_GONE); +} + +int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, + int64_t session_id) { + nghttp3_stream *wt_ctrl_stream; + nghttp3_wt_session *wt_session; + int rv; + + if (!nghttp3_client_stream_bidi(session_id)) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + if (stream->wt.session) { + assert(stream->wt.session->session_id != stream->node.id); + + if (stream->wt.session->session_id != session_id) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + return 0; + } + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream) { + if (!conn->server) { + /* On client's perspective, if session stream is not found, we are + sure that session is gone. */ + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + if (nghttp3_ord_stream_id(session_id) > + conn->remote.bidi.max_client_streams) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + if ((conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_QUEUED) && + conn->tx.goaway_id <= session_id) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + rv = conn_bidi_idtr_open(conn, session_id); + if (rv != 0) { + if (nghttp3_err_is_fatal(rv)) { + return rv; + } + + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + conn->rx.max_stream_id_bidi = + nghttp3_max(conn->rx.max_stream_id_bidi, session_id); + rv = nghttp3_conn_create_stream(conn, &wt_ctrl_stream, session_id); + if (rv != 0) { + return rv; + } + + wt_ctrl_stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; + } + + if (!wt_ctrl_stream->wt.session) { + rv = nghttp3_conn_open_wt_session(conn, wt_ctrl_stream); + if (rv != 0) { + return rv; + } + } + + wt_session = wt_ctrl_stream->wt.session; + + assert(wt_session); + + nghttp3_wt_session_add_stream(wt_session, stream); + + if (wt_ctrl_stream->wt.session->flags & NGHTTP3_WT_SESSION_FLAG_CONFIRMED) { + return conn_call_wt_data_stream_open(conn, stream); + } + + return 0; +} + +int nghttp3_conn_on_wt_session_confirmed(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + nghttp3_tstamp ts) { + nghttp3_stream *stream; + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + int rv; + + wt_session->flags |= NGHTTP3_WT_SESSION_FLAG_CONFIRMED; + + /* TODO Is stream gone during iteration? */ + for (stream = wt_session->head; stream; stream = stream->wt.next) { + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (conn_remote_stream(conn, stream->node.id)) { + rv = conn_call_wt_data_stream_open(conn, stream); + if (rv != 0) { + return rv; + } + } + + rv = nghttp3_conn_process_blocked_wt_stream_data(conn, stream, ts); + if (rv != 0) { + return rv; + } + } + + return nghttp3_conn_process_blocked_wt_stream_data(conn, wt_ctrl_stream, ts); +} + +nghttp3_ssize nghttp3_conn_read_wt_stream_uni(nghttp3_conn *conn, + nghttp3_stream *stream, + const uint8_t *src, size_t srclen, + int fin, nghttp3_tstamp ts) { + const uint8_t *p = src, *end = src ? src + srclen : src; + int rv; + nghttp3_stream_read_state *rstate = &stream->rstate; + nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_ssize nread; + size_t nconsumed = 0; + nghttp3_stream *wt_ctrl_stream; + (void)ts; + + if ((stream->flags & NGHTTP3_STREAM_FLAG_SHUT_RD)) { + return (nghttp3_ssize)srclen; + } + + if (srclen == 0) { + goto almost_done; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED) { + if (srclen == 0) { + return 0; + } + + rv = nghttp3_stream_buffer_data(stream, p, srclen); + if (rv != 0) { + return rv; + } + + return 0; + } + + switch (rstate->state) { + case NGHTTP3_WT_STREAM_STATE_SESSION_ID: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, fin); + if (nread < 0) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + p += nread; + nconsumed += (size_t)nread; + if (rvint->left) { + /* TODO What should we do if unidirectional stream is closed + before reading Session ID? */ + break; + } + + rstate->left = rvint->acc; + nghttp3_varint_read_state_reset(rvint); + + /* rstate->left is Session ID */ + rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); + if (rv != 0) { + if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + return rv; + } + + stream->rstate.state = NGHTTP3_WT_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + nconsumed += (size_t)(end - p); + + return (nghttp3_ssize)nconsumed; + } + + rstate->state = NGHTTP3_WT_STREAM_STATE_DATA; + + wt_ctrl_stream = + nghttp3_conn_find_stream(conn, stream->wt.session->session_id); + + if (!(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_CONFIRMED)) { + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + return (nghttp3_ssize)nconsumed; + } + + if (p == end) { + break; + } + + /* Fall through */ + case NGHTTP3_WT_STREAM_STATE_DATA: + rv = conn_call_recv_wt_data(conn, stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + + break; + case NGHTTP3_WT_STREAM_STATE_IGN_REST: + nconsumed += (size_t)(end - p); + + return (nghttp3_ssize)nconsumed; + } + +almost_done: + if (fin) { + rv = conn_call_end_stream(conn, stream); + if (rv != 0) { + return rv; + } + } + + return (nghttp3_ssize)nconsumed; +} + +int nghttp3_conn_process_blocked_wt_stream_data(nghttp3_conn *conn, + nghttp3_stream *stream, + nghttp3_tstamp ts) { + nghttp3_buf *buf; + nghttp3_ssize nconsumed; + size_t nproc; + int rv; + size_t len; + + for (;;) { + len = nghttp3_ringbuf_len(&stream->inq); + if (len == 0) { + break; + } + + buf = nghttp3_ringbuf_get(&stream->inq, 0); + + if (nghttp3_stream_uni(stream->node.id)) { + nconsumed = nghttp3_conn_read_wt_stream_uni( + conn, stream, buf->pos, nghttp3_buf_len(buf), + len == 1 && (stream->flags & NGHTTP3_STREAM_FLAG_READ_EOF), ts); + } else { + nconsumed = nghttp3_conn_read_bidi( + conn, &nproc, stream, buf->pos, nghttp3_buf_len(buf), + len == 1 && (stream->flags & NGHTTP3_STREAM_FLAG_READ_EOF), ts); + } + + if (nconsumed < 0) { + return (int)nconsumed; + } + + rv = conn_call_deferred_consume(conn, stream, (size_t)nconsumed); + if (rv != 0) { + return rv; + } + + nghttp3_buf_free(buf, stream->mem); + nghttp3_ringbuf_pop_front(&stream->inq); + } + + return 0; +} + +int nghttp3_conn_shutdown_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code) { + int rv; + + rv = + nghttp3_conn_shutdown_all_wt_data_streams(conn, wt_ctrl_stream, error_code); + if (rv != 0) { + return rv; + } + + wt_ctrl_stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return nghttp3_conn_abort_stream(conn, wt_ctrl_stream, error_code); +} + +int nghttp3_conn_shutdown_all_wt_data_streams(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code) { + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + nghttp3_stream *stream; + int rv; + + for (stream = wt_session->head; stream; stream = stream->wt.next) { + rv = nghttp3_conn_shutdown_wt_data_stream(conn, stream, error_code); + if (rv != 0) { + return rv; + } + } + + return 0; +} + +int nghttp3_conn_shutdown_wt_data_stream(nghttp3_conn *conn, + nghttp3_stream *stream, + uint64_t error_code) { + if (stream->node.id & 0x2) { + stream->rstate.state = NGHTTP3_WT_STREAM_STATE_IGN_REST; + } else { + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + } + + return nghttp3_conn_abort_stream(conn, stream, error_code); +} + +int64_t nghttp3_conn_get_stream_wt_session_id(const nghttp3_conn *conn, + int64_t stream_id) { + const nghttp3_stream *stream = nghttp3_conn_find_stream(conn, stream_id); + + if (!stream || !nghttp3_stream_wt_data(stream)) { + return -1; + } + + return stream->wt.session->session_id; +} + +int nghttp3_conn_read_wt_ctrl_stream(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *src, size_t srclen) { + const uint8_t *p, *end; + nghttp3_wt_session *wts = stream->wt.session; + nghttp3_wt_ctrl_read_state *rstate = &wts->rstate; + nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_ssize nread; + nghttp3_exfr_cpsl *cpsl = &rstate->cpsl; + size_t len; + size_t i; + int rv; + + if (srclen == 0) { + return 0; + } + + p = src; + end = src + srclen; + + for (; p != end;) { + switch (rstate->state) { + case NGHTTP3_WT_CTRL_STREAM_STATE_TYPE: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, /* fin = */ 0); + + assert(nread > 0); + + p += nread; + if (rvint->left) { + return 0; + } + + rstate->cpsl.hd.type = rvint->acc; + + nghttp3_varint_read_state_reset(rvint); + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH; + if (p == end) { + return 0; + } + /* Fall through */ + case NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, /* fin = */ 0); + assert(nread > 0); + + p += nread; + if (rvint->left) { + return 0; + } + + rstate->left = rvint->acc; + nghttp3_varint_read_state_reset(rvint); + + switch (rstate->cpsl.hd.type) { + case NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION: + if (rstate->left < sizeof(uint32_t) || + rstate->left > sizeof(uint32_t) + /* largest message size */ 1024) { + /* TODO Find better error code */ + return NGHTTP3_ERR_H3_MESSAGE_ERROR; + } + + rstate->field_left = sizeof(uint32_t); + rstate->state = + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE; + + break; + default: + /* TODO Add rate limit after we implement all supported + capsules. */ + if (rstate->left == 0) { + nghttp3_wt_ctrl_read_state_reset(rstate); + break; + } + + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_IGN; + } + + break; + case NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE: + len = nghttp3_min(rstate->field_left, (size_t)(end - p)); + + for (i = 0; i < len; ++i) { + cpsl->wt_close_session.error_code <<= 8; + cpsl->wt_close_session.error_code += *p++; + } + + rstate->left -= len; + rstate->field_left -= len; + if (rstate->field_left) { + break; + } + + wts->rx.error_code = cpsl->wt_close_session.error_code; + + if (rstate->left == 0) { + if (conn->callbacks.recv_wt_close_session) { + rv = conn->callbacks.recv_wt_close_session( + conn, wts->session_id, wts->rx.error_code, NULL, 0, conn->user_data, + stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG; + + wts->rx.error_msg.base = + nghttp3_mem_malloc(conn->mem, (size_t)rstate->left); + if (!wts->rx.error_msg.base) { + return NGHTTP3_ERR_NOMEM; + } + + if (p == end) { + return 0; + } + + /* Fall through */ + case NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG: + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); + + memcpy(wts->rx.error_msg.base + wts->rx.error_msg.len, p, len); + wts->rx.error_msg.len += len; + + p += len; + rstate->left -= len; + + if (rstate->left) { + break; + } + + if (conn->callbacks.recv_wt_close_session) { + rv = conn->callbacks.recv_wt_close_session( + conn, wts->session_id, wts->rx.error_code, wts->rx.error_msg.base, + wts->rx.error_msg.len, conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + return NGHTTP3_ERR_WT_SESSION_GONE; + case NGHTTP3_WT_CTRL_STREAM_STATE_IGN: + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); + p += len; + rstate->left -= len; + + if (rstate->left) { + return 0; + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + break; + } + } + + return 0; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h index 6841b1c343a305..101def930b75e8 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h @@ -202,8 +202,8 @@ nghttp3_ssize nghttp3_conn_read_qpack_decoder(nghttp3_conn *conn, const uint8_t *src, size_t srclen); -int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, - const uint8_t *data, size_t datalen); +nghttp3_ssize nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, + const uint8_t *data, size_t datalen); int nghttp3_conn_on_priority_update(nghttp3_conn *conn, const nghttp3_frame_priority_update *fr); @@ -216,6 +216,8 @@ nghttp3_ssize nghttp3_conn_on_headers(nghttp3_conn *conn, int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, const nghttp3_frame_settings *fr); +int nghttp3_conn_on_settings_received(nghttp3_conn *conn); + int nghttp3_conn_qpack_blocked_streams_push(nghttp3_conn *conn, nghttp3_stream *stream); @@ -233,10 +235,47 @@ void nghttp3_conn_unschedule_stream(nghttp3_conn *conn, nghttp3_stream *stream); int nghttp3_conn_reject_stream(nghttp3_conn *conn, nghttp3_stream *stream); +int nghttp3_conn_abort_stream(nghttp3_conn *conn, nghttp3_stream *stream, + uint64_t error_code); + /* * nghttp3_conn_get_next_tx_stream returns next stream to send. It * returns NULL if there is no such stream. */ nghttp3_stream *nghttp3_conn_get_next_tx_stream(nghttp3_conn *conn); +int nghttp3_conn_open_wt_session(nghttp3_conn *conn, nghttp3_stream *stream); + +int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, + int64_t session_id); + +nghttp3_ssize nghttp3_conn_read_wt_stream_uni(nghttp3_conn *conn, + nghttp3_stream *stream, + const uint8_t *src, size_t srclen, + int fin, nghttp3_tstamp ts); + +int nghttp3_conn_on_wt_session_confirmed(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + nghttp3_tstamp ts); + +int nghttp3_conn_process_blocked_wt_stream_data(nghttp3_conn *conn, + nghttp3_stream *stream, + nghttp3_tstamp ts); + +int nghttp3_conn_shutdown_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code); + +int nghttp3_conn_shutdown_all_wt_data_streams(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code); + +int nghttp3_conn_shutdown_wt_data_stream(nghttp3_conn *conn, + nghttp3_stream *stream, + uint64_t error_code); + +int nghttp3_conn_read_wt_ctrl_stream(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *src, size_t srclen); + #endif /* !defined(NGHTTP3_CONN_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c index 031ac78d815f85..a90e1d25b70eab 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c @@ -79,6 +79,12 @@ const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { return p; } +const uint8_t *nghttp3_get_uint32be(uint32_t *dest, const uint8_t *p) { + memcpy(dest, p, sizeof(*dest)); + *dest = ntohl(*dest); + return p + sizeof(*dest); +} + uint8_t *nghttp3_put_uint64be(uint8_t *p, uint64_t n) { n = nghttp3_htonl64(n); return nghttp3_cpymem(p, (const uint8_t *)&n, sizeof(n)); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h index bd1c518fa6638d..bd6f29a55db569 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h @@ -92,6 +92,13 @@ # define ntohs(N) _byteswap_ushort(N) #endif /* defined(WIN32) */ +/* + * nghttp3_get_uint32be reads 4 bytes from |p| as 32 bits unsigned + * integer encoded as network byte order, and stores it in the buffer + * pointed by |dest| in host byte order. It returns |p| + 4. + */ +const uint8_t *nghttp3_get_uint32be(uint32_t *dest, const uint8_t *p); + /* * nghttp3_put_uint64be writes |n| in host byte order in |p| in * network byte order. It returns the one beyond of the last written diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c index eff6ea6a63a2f7..e6603c00f041f5 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c @@ -76,6 +76,10 @@ const char *nghttp3_strerror(int liberr) { return "ERR_H3_STREAM_CREATION_ERROR"; case NGHTTP3_ERR_H3_EXCESSIVE_LOAD: return "ERR_H3_EXCESSIVE_LOAD"; + case NGHTTP3_ERR_WT_SESSION_GONE: + return "ERR_WT_SESSION_GONE"; + case NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED: + return "ERR_WT_BUFFERED_STREAM_REJECTED"; case NGHTTP3_ERR_NOMEM: return "ERR_NOMEM"; case NGHTTP3_ERR_CALLBACK_FAILURE: @@ -122,7 +126,12 @@ uint64_t nghttp3_err_infer_quic_app_error_code(int liberr) { return NGHTTP3_H3_EXCESSIVE_LOAD; case NGHTTP3_ERR_MALFORMED_HTTP_HEADER: case NGHTTP3_ERR_MALFORMED_HTTP_MESSAGING: + case NGHTTP3_ERR_H3_MESSAGE_ERROR: return NGHTTP3_H3_MESSAGE_ERROR; + case NGHTTP3_ERR_WT_SESSION_GONE: + return NGHTTP3_WT_SESSION_GONE; + case NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED: + return NGHTTP3_WT_BUFFERED_STREAM_REJECTED; default: return NGHTTP3_H3_GENERAL_PROTOCOL_ERROR; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c index 2efba7472c251f..1742e0756fc665 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c @@ -133,6 +133,44 @@ size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, payloadlen; } +uint8_t *nghttp3_frame_write_wt_stream(uint8_t *p, + const nghttp3_exfr_wt_stream *fr) { + p = nghttp3_put_uvarint(p, fr->type); + return nghttp3_put_uvarint(p, (uint64_t)fr->session_id); +} + +size_t nghttp3_frame_write_wt_stream_len(const nghttp3_exfr_wt_stream *fr) { + return nghttp3_put_uvarintlen(fr->type) + + nghttp3_put_uvarintlen((uint64_t)fr->session_id); +} + +uint8_t *nghttp3_frame_write_cpsl_wt_close_session( + uint8_t *p, const nghttp3_exfr_cpsl_wt_close_session *fr, + uint64_t payloadlen) { + p = nghttp3_frame_write_hd(p, NGHTTP3_FRAME_DATA, payloadlen); + p = nghttp3_frame_write_hd(p, fr->type, + sizeof(fr->error_code) + fr->error_msg.len); + p = nghttp3_put_uint32be(p, fr->error_code); + + if (fr->error_msg.len) { + p = nghttp3_cpymem(p, fr->error_msg.base, fr->error_msg.len); + } + + return p; +} + +size_t nghttp3_frame_write_cpsl_wt_close_session_len( + uint64_t *ppayloadlen, const nghttp3_exfr_cpsl_wt_close_session *fr) { + size_t cpsl_payloadlen = sizeof(fr->error_code) + fr->error_msg.len; + size_t payloadlen = nghttp3_put_uvarintlen(fr->type) + + nghttp3_put_uvarintlen(cpsl_payloadlen) + cpsl_payloadlen; + + *ppayloadlen = payloadlen; + + return nghttp3_put_uvarintlen(NGHTTP3_FRAME_DATA) + + nghttp3_put_uvarintlen(payloadlen) + payloadlen; +} + int nghttp3_nva_copy(nghttp3_nv **pnva, const nghttp3_nv *nva, size_t nvlen, const nghttp3_mem *mem) { size_t i; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h index 7806cadbcf5f5a..77b4d37b8fb293 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h @@ -46,6 +46,24 @@ #define NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID 0x0F0701U /* ORIGIN: https://datatracker.ietf.org/doc/html/rfc9412 */ #define NGHTTP3_FRAME_ORIGIN 0x0CU +/* WebTransport extended frame type */ +#define NGHTTP3_FRAME_EX_WT 0x4000000000000001ULL +/* WT_STREAM: + https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-14 */ +#define NGHTTP3_EXFR_WT_STREAM_BIDI 0x41U +#define NGHTTP3_EXFR_WT_STREAM_UNI 0x54U +#define NGHTTP3_EXFR_WT_STREAM_DATA 0x00U + +/* HTTP Capsule extended frame type */ +#define NGHTTP3_FRAME_EX_CPSL 0x4000000000000002ULL +#define NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION 0x2843U +#define NGHTTP3_EXFR_CPSL_WT_DRAIN_SESSION 0x78AEU +#define NGHTTP3_EXFR_CPSL_WT_MAX_STREAMS_BIDI 0x190B4D3FU +#define NGHTTP3_EXFR_CPSL_WT_MAX_STREAMS_UNI 0x190B4D40U +#define NGHTTP3_EXFR_CPSL_WT_STREAMS_BLOCKED_BIDI 0x190B4D43U +#define NGHTTP3_EXFR_CPSL_WT_STREAMS_BLOCKED_UNI 0x190B4D44U +#define NGHTTP3_EXFR_CPSL_WT_MAX_DATA 0x190B4D3DU +#define NGHTTP3_EXFR_CPSL_WT_DATA_BLOCKED 0x190B4D41U /* Frame types that are reserved for HTTP/2, and must not be used in HTTP/3. */ @@ -76,6 +94,14 @@ typedef struct nghttp3_frame_headers { #define NGHTTP3_SETTINGS_ID_QPACK_BLOCKED_STREAMS 0x07U #define NGHTTP3_SETTINGS_ID_ENABLE_CONNECT_PROTOCOL 0x08U #define NGHTTP3_SETTINGS_ID_H3_DATAGRAM 0x33U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 */ +#define NGHTTP3_SETTINGS_ID_WT_ENABLED 0x2C7CF000U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-14 */ +#define NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS 0x14E9CD29U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-07 */ +#define NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7 0xC671706AU +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-02 */ +#define NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2 0x2B603742U #define NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH 0x2U #define NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS 0x3U @@ -132,6 +158,42 @@ typedef struct nghttp3_frame_origin { nghttp3_vec origin_list; } nghttp3_frame_origin; +typedef struct nghttp3_exfr_hd { + uint64_t type; +} nghttp3_exfr_hd; + +typedef struct nghttp3_exfr_wt_stream { + uint64_t type; + int64_t session_id; + nghttp3_data_reader dr; +} nghttp3_exfr_wt_stream; + +typedef union nghttp3_exfr_wt { + nghttp3_exfr_hd hd; + nghttp3_exfr_wt_stream wt_stream; +} nghttp3_exfr_wt; + +typedef struct nghttp3_frame_ex_wt { + uint64_t type; + nghttp3_exfr_wt fr; +} nghttp3_frame_ex_wt; + +typedef struct nghttp3_exfr_cpsl_wt_close_session { + uint64_t type; + nghttp3_vec error_msg; + uint32_t error_code; +} nghttp3_exfr_cpsl_wt_close_session; + +typedef union nghttp3_exfr_cpsl { + nghttp3_exfr_hd hd; + nghttp3_exfr_cpsl_wt_close_session wt_close_session; +} nghttp3_exfr_cpsl; + +typedef struct nghttp3_frame_ex_cpsl { + uint64_t type; + nghttp3_exfr_cpsl fr; +} nghttp3_frame_ex_cpsl; + typedef union nghttp3_frame { nghttp3_frame_hd hd; nghttp3_frame_data data; @@ -140,6 +202,8 @@ typedef union nghttp3_frame { nghttp3_frame_goaway goaway; nghttp3_frame_priority_update priority_update; nghttp3_frame_origin origin; + nghttp3_frame_ex_wt wt; + nghttp3_frame_ex_cpsl cpsl; } nghttp3_frame; /* @@ -233,6 +297,18 @@ uint8_t *nghttp3_frame_write_origin(uint8_t *dest, size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, const nghttp3_frame_origin *fr); +uint8_t *nghttp3_frame_write_wt_stream(uint8_t *dest, + const nghttp3_exfr_wt_stream *fr); + +size_t nghttp3_frame_write_wt_stream_len(const nghttp3_exfr_wt_stream *fr); + +uint8_t *nghttp3_frame_write_cpsl_wt_close_session( + uint8_t *dest, const nghttp3_exfr_cpsl_wt_close_session *fr, + uint64_t payloadlen); + +size_t nghttp3_frame_write_cpsl_wt_close_session_len( + uint64_t *ppayloadlen, const nghttp3_exfr_cpsl_wt_close_session *fr); + /* * nghttp3_nva_copy copies name/value pairs from |nva|, which contains * |nvlen| pairs, to |*nva_ptr|, which is dynamically allocated so diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c index 4194a404b33f97..9f7a16ac2e4849 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c @@ -384,6 +384,11 @@ static int http_request_on_header(nghttp3_http_state *http, !check_pseudo_header(http, nv, NGHTTP3_HTTP_FLAG__PROTOCOL)) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; } + + if (lstrieq("webtransport", nv->value->base, nv->value->len)) { + http->flags |= NGHTTP3_HTTP_FLAG_WEBTRANSPORT; + } + break; case NGHTTP3_QPACK_TOKEN_HOST: if (!check_authority(nv->value->base, nv->value->len)) { diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h index 2bdf3110027c15..ec32414c1d1d5a 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h @@ -82,6 +82,8 @@ typedef struct nghttp3_http_state nghttp3_http_state; while parsing priority header field. */ #define NGHTTP3_HTTP_FLAG_BAD_PRIORITY 0x010000U +#define NGHTTP3_HTTP_FLAG_WEBTRANSPORT 0x020000U + /* * This function is called when HTTP header field |nv| received for * |http|. This function will validate |nv| against the current state diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c index 76db6fe303da6c..8acf0da817b54a 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c @@ -36,6 +36,7 @@ #include "nghttp3_http.h" #include "nghttp3_vec.h" #include "nghttp3_unreachable.h" +#include "nghttp3_wt.h" /* NGHTTP3_STREAM_MAX_COPY_THRES is the maximum size of buffer which makes a copy to outq. */ @@ -168,6 +169,10 @@ void nghttp3_stream_del(nghttp3_stream *stream) { delete_frq(&stream->frq, stream->mem); nghttp3_tnode_free(&stream->node); + if (nghttp3_stream_wt_ctrl(stream)) { + nghttp3_wt_session_del(stream->wt.session, stream->mem); + } + nghttp3_objalloc_stream_release(stream->stream_objalloc, stream); } @@ -295,6 +300,47 @@ int nghttp3_stream_fill_outq(nghttp3_stream *stream) { return rv; } + break; + case NGHTTP3_FRAME_EX_WT: + switch (fr->wt.fr.hd.type) { + case NGHTTP3_EXFR_WT_STREAM_BIDI: + case NGHTTP3_EXFR_WT_STREAM_UNI: + rv = nghttp3_stream_write_wt_stream(stream, &fr->wt.fr.wt_stream); + if (rv != 0) { + return rv; + } + + fr->wt.fr.wt_stream.type = NGHTTP3_EXFR_WT_STREAM_DATA; + + /* fall through */ + case NGHTTP3_EXFR_WT_STREAM_DATA: + rv = nghttp3_stream_write_wt_stream_data(stream, &data_eof, + &fr->wt.fr.wt_stream); + if (rv != 0) { + return rv; + } + + if ((stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED) || + !data_eof) { + return 0; + } + + break; + } + + break; + case NGHTTP3_FRAME_EX_CPSL: + switch (fr->cpsl.fr.hd.type) { + case NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION: + rv = nghttp3_stream_write_cpsl_wt_close_session( + stream, &fr->cpsl.fr.wt_close_session); + if (rv != 0) { + return rv; + } + + break; + } + break; default: /* TODO Not implemented */ @@ -373,6 +419,31 @@ int nghttp3_stream_write_settings(nghttp3_stream *stream, ++fr.niv; } + if (local_settings->wt_enabled) { + /* For client, only draft version sends SETTINGS_WT_ENABLED. */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_ENABLED, + .value = 1, + }; + + /* compat for pre draft-15 */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS, + .value = 1, + }; + + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7, + .value = 1, + }; + + /* compat for ancient draft */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2, + .value = 1, + }; + } + len = nghttp3_frame_write_settings_len(&payloadlen, &fr); rv = nghttp3_stream_ensure_chunk(stream, len); @@ -479,6 +550,150 @@ int nghttp3_stream_write_origin(nghttp3_stream *stream, return nghttp3_stream_outq_add(stream, &tbuf); } +int nghttp3_stream_write_wt_stream(nghttp3_stream *stream, + const nghttp3_exfr_wt_stream *fr) { + size_t len; + int rv; + nghttp3_buf *chunk; + nghttp3_typed_buf tbuf; + + len = nghttp3_frame_write_wt_stream_len(fr); + + rv = nghttp3_stream_ensure_chunk(stream, len); + if (rv != 0) { + return rv; + } + + chunk = nghttp3_stream_get_chunk(stream); + nghttp3_typed_buf_shared_init(&tbuf, chunk); + + chunk->last = nghttp3_frame_write_wt_stream(chunk->last, fr); + + tbuf.buf.last = chunk->last; + + return nghttp3_stream_outq_add(stream, &tbuf); +} + +int nghttp3_stream_write_wt_stream_data(nghttp3_stream *stream, int *peof, + const nghttp3_exfr_wt_stream *fr) { + int rv; + nghttp3_typed_buf tbuf; + nghttp3_buf buf; + nghttp3_read_data_callback read_data = fr->dr.read_data; + nghttp3_conn *conn = stream->conn; + uint64_t datalen; + uint32_t flags = 0; + nghttp3_vec vec[8]; + nghttp3_vec *v; + nghttp3_ssize sveccnt; + size_t i; + + assert(!(stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED)); + assert(read_data); + assert(conn); + + *peof = 0; + + sveccnt = read_data(conn, stream->node.id, vec, nghttp3_arraylen(vec), &flags, + conn->user_data, stream->user_data); + if (sveccnt < 0) { + if (sveccnt == NGHTTP3_ERR_WOULDBLOCK) { + stream->flags |= NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED; + return 0; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + rv = nghttp3_vec_len_uvarint(&datalen, vec, (size_t)sveccnt); + if (rv != 0) { + return NGHTTP3_ERR_STREAM_DATA_OVERFLOW; + } + + assert(datalen || flags & NGHTTP3_DATA_FLAG_EOF); + + if (flags & NGHTTP3_DATA_FLAG_EOF) { + *peof = 1; + + stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; + if (datalen == 0) { + if (nghttp3_stream_outq_write_done(stream)) { + /* If this is the last data and its is 0 length, we don't need + send data. We rely on the non-emptiness of outq to + schedule stream, so add empty tbuf to outq to just send + fin. */ + nghttp3_buf_init(&buf); + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_PRIVATE); + return nghttp3_stream_outq_add(stream, &tbuf); + } + + /* We are going to send data, but nothing to send this time. */ + + return 0; + } + } + + assert(datalen); + + for (i = 0; i < (size_t)sveccnt; ++i) { + v = &vec[i]; + if (v->len == 0) { + continue; + } + nghttp3_buf_wrap_init(&buf, v->base, v->len); + buf.last = buf.end; + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_ALIEN); + rv = nghttp3_stream_outq_add(stream, &tbuf); + if (rv != 0) { + return rv; + } + } + + return 0; +} + +int nghttp3_stream_write_cpsl_wt_close_session( + nghttp3_stream *stream, const nghttp3_exfr_cpsl_wt_close_session *fr) { + int rv; + nghttp3_buf *chunk; + nghttp3_buf buf; + nghttp3_typed_buf tbuf; + size_t cpsl_payloadlen = sizeof(fr->error_code) + fr->error_msg.len; + size_t fr_hdlen = nghttp3_frame_write_hd_len(fr->type, cpsl_payloadlen); + uint64_t payloadlen = fr_hdlen + cpsl_payloadlen; + + rv = nghttp3_stream_ensure_chunk( + stream, nghttp3_frame_write_hd_len(NGHTTP3_FRAME_DATA, payloadlen) + + fr_hdlen + sizeof(fr->error_code)); + if (rv != 0) { + return rv; + } + + chunk = nghttp3_stream_get_chunk(stream); + nghttp3_typed_buf_shared_init(&tbuf, chunk); + + chunk->last = + nghttp3_frame_write_hd(chunk->last, NGHTTP3_FRAME_DATA, payloadlen); + chunk->last = nghttp3_frame_write_hd(chunk->last, fr->type, cpsl_payloadlen); + chunk->last = nghttp3_put_uint32be(chunk->last, fr->error_code); + + tbuf.buf.last = chunk->last; + + rv = nghttp3_stream_outq_add(stream, &tbuf); + if (rv != 0) { + return rv; + } + + if (fr->error_msg.len == 0) { + return 0; + } + + nghttp3_buf_wrap_init(&buf, fr->error_msg.base, fr->error_msg.len); + buf.last = buf.end; + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_ALIEN_NO_ACK); + + return nghttp3_stream_outq_add(stream, &tbuf); +} + int nghttp3_stream_write_headers(nghttp3_stream *stream, const nghttp3_frame_headers *fr) { nghttp3_conn *conn = stream->conn; @@ -849,6 +1064,11 @@ int nghttp3_stream_require_schedule(const nghttp3_stream *stream) { !(stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED)); } +int nghttp3_stream_schedulable(const nghttp3_stream *stream) { + return !nghttp3_stream_uni(stream->node.id) || + stream->type == NGHTTP3_STREAM_TYPE_WT_STREAM; +} + size_t nghttp3_stream_writev(nghttp3_stream *stream, int *pfin, nghttp3_vec *vec, size_t veccnt) { nghttp3_ringbuf *outq = &stream->outq; @@ -1236,8 +1456,25 @@ int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream) { } } +int nghttp3_stream_critical(const nghttp3_stream *stream) { + return nghttp3_stream_uni(stream->node.id) && + (stream->type == NGHTTP3_STREAM_TYPE_CONTROL || + stream->type == NGHTTP3_STREAM_TYPE_QPACK_ENCODER || + stream->type == NGHTTP3_STREAM_TYPE_QPACK_DECODER); +} + int nghttp3_stream_uni(int64_t stream_id) { return (stream_id & 0x2) != 0; } +int nghttp3_stream_wt_ctrl(const nghttp3_stream *stream) { + return stream->wt.session && + stream->wt.session->session_id == stream->node.id; +} + +int nghttp3_stream_wt_data(const nghttp3_stream *stream) { + return stream->wt.session && + stream->wt.session->session_id != stream->node.id; +} + int nghttp3_client_stream_bidi(int64_t stream_id) { return (stream_id & 0x3) == 0; } @@ -1249,3 +1486,7 @@ int nghttp3_client_stream_uni(int64_t stream_id) { int nghttp3_server_stream_uni(int64_t stream_id) { return (stream_id & 0x3) == 0x3; } + +int nghttp3_server_stream_bidi(int64_t stream_id) { + return (stream_id & 0x3) == 0x1; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h index 61a1f085ac8709..59ff74866267ef 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h @@ -56,6 +56,7 @@ typedef uint64_t nghttp3_stream_type; #define NGHTTP3_STREAM_TYPE_PUSH 0x01U #define NGHTTP3_STREAM_TYPE_QPACK_ENCODER 0x02U #define NGHTTP3_STREAM_TYPE_QPACK_DECODER 0x03U +#define NGHTTP3_STREAM_TYPE_WT_STREAM 0x54U #define NGHTTP3_STREAM_TYPE_UNKNOWN UINT64_MAX typedef enum nghttp3_ctrl_stream_state { @@ -78,10 +79,19 @@ typedef enum nghttp3_req_stream_state { NGHTTP3_REQ_STREAM_STATE_FRAME_LENGTH, NGHTTP3_REQ_STREAM_STATE_DATA, NGHTTP3_REQ_STREAM_STATE_HEADERS, + NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA, + NGHTTP3_REQ_STREAM_STATE_WT_DATA, NGHTTP3_REQ_STREAM_STATE_IGN_FRAME, NGHTTP3_REQ_STREAM_STATE_IGN_REST, } nghttp3_req_stream_state; +/* stream state for WebTransport unidirectional data stream */ +typedef enum nghttp3_wt_stream_state { + NGHTTP3_WT_STREAM_STATE_SESSION_ID, + NGHTTP3_WT_STREAM_STATE_DATA, + NGHTTP3_WT_STREAM_STATE_IGN_REST, +} nghttp3_wt_stream_state; + typedef struct nghttp3_varint_read_state { uint64_t acc; size_t left; @@ -95,6 +105,8 @@ typedef struct nghttp3_stream_read_state { int state; } nghttp3_stream_read_state; +typedef struct nghttp3_wt_session nghttp3_wt_session; + /* NGHTTP3_STREAM_FLAG_NONE indicates that no flag is set. */ #define NGHTTP3_STREAM_FLAG_NONE 0x0000U /* NGHTTP3_STREAM_FLAG_TYPE_IDENTIFIED is set when a unidirectional @@ -128,6 +140,18 @@ typedef struct nghttp3_stream_read_state { /* NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED indicates that server received PRIORITY_UPDATE frame for this stream. */ #define NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED 0x0800U +/* NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA indicates that the stream may be + WebTransport data stream. */ +#define NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA 0x1000U +/* NGHTTP3_STREAM_FLAG_WT_DATA indicates that the stream is + WebTransport data stream. */ +#define NGHTTP3_STREAM_FLAG_WT_DATA 0x2000U +/* NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED indicates that the stream is + blocked because WebTransport session has not been established. */ +#define NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED 0x4000U +/* NGHTTP3_STREAM_FLAG_RESP_SUBMITTED indicates that HTTP/3 response + has been submitted via nghttp3_conn_submit_response. */ +#define NGHTTP3_STREAM_FLAG_RESP_SUBMITTED 0x8000U typedef enum nghttp3_stream_http_state { NGHTTP3_HTTP_STATE_NONE, @@ -237,6 +261,12 @@ struct nghttp3_stream { nghttp3_http_state http; } rx; + struct { + nghttp3_wt_session *session; + nghttp3_stream *prev; + nghttp3_stream *next; + } wt; + uint16_t flags; }; @@ -312,6 +342,15 @@ int nghttp3_stream_write_priority_update( int nghttp3_stream_write_origin(nghttp3_stream *stream, const nghttp3_frame_origin *fr); +int nghttp3_stream_write_wt_stream(nghttp3_stream *stream, + const nghttp3_exfr_wt_stream *fr); + +int nghttp3_stream_write_wt_stream_data(nghttp3_stream *stream, int *peof, + const nghttp3_exfr_wt_stream *fr); + +int nghttp3_stream_write_cpsl_wt_close_session( + nghttp3_stream *stream, const nghttp3_exfr_cpsl_wt_close_session *frent); + int nghttp3_stream_ensure_chunk(nghttp3_stream *stream, size_t need); nghttp3_buf *nghttp3_stream_get_chunk(nghttp3_stream *stream); @@ -346,6 +385,8 @@ int nghttp3_stream_is_active(nghttp3_stream *stream); */ int nghttp3_stream_require_schedule(const nghttp3_stream *stream); +int nghttp3_stream_schedulable(const nghttp3_stream *stream); + int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *src, size_t srclen); @@ -360,6 +401,12 @@ int nghttp3_stream_transit_rx_http_state(nghttp3_stream *stream, int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream); +int nghttp3_stream_wt_ctrl(const nghttp3_stream *stream); + +int nghttp3_stream_wt_data(const nghttp3_stream *stream); + +int nghttp3_stream_critical(const nghttp3_stream *stream); + /* * nghttp3_stream_uni returns nonzero if stream identified by * |stream_id| is unidirectional. @@ -384,4 +431,6 @@ int nghttp3_client_stream_uni(int64_t stream_id); */ int nghttp3_server_stream_uni(int64_t stream_id); +int nghttp3_server_stream_bidi(int64_t stream_id); + #endif /* !defined(NGHTTP3_STREAM_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c new file mode 100644 index 00000000000000..91331206c5c6e6 --- /dev/null +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c @@ -0,0 +1,95 @@ +/* + * nghttp3 + * + * Copyright (c) 2025 nghttp3 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp3_wt.h" + +#include + +#include "nghttp3_mem.h" + +int nghttp3_wt_session_new(nghttp3_wt_session **pwts, int64_t session_id, + const nghttp3_mem *mem) { + *pwts = nghttp3_mem_malloc(mem, sizeof(**pwts)); + if (*pwts == NULL) { + return NGHTTP3_ERR_NOMEM; + } + + **pwts = (nghttp3_wt_session){ + .session_id = session_id, + }; + + return 0; +} + +void nghttp3_wt_session_del(nghttp3_wt_session *wts, const nghttp3_mem *mem) { + if (!wts) { + return; + } + + nghttp3_mem_free(mem, wts->rx.error_msg.base); + nghttp3_mem_free(mem, wts->tx.error_msg.base); + + nghttp3_mem_free(mem, wts); +} + +void nghttp3_wt_session_add_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream) { + assert(!stream->wt.session); + assert(!stream->wt.prev); + assert(!stream->wt.next); + + stream->wt.session = wts; + stream->flags |= NGHTTP3_STREAM_FLAG_WT_DATA; + + if (wts->head) { + stream->wt.next = wts->head; + wts->head->wt.prev = stream; + } + + wts->head = stream; +} + +void nghttp3_wt_session_remove_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream) { + assert(stream->wt.session); + + if (stream->wt.prev) { + stream->wt.prev->wt.next = stream->wt.next; + } + + if (stream->wt.next) { + stream->wt.next->wt.prev = stream->wt.prev; + } + + if (wts->head == stream) { + wts->head = stream->wt.next; + } + + stream->wt.session = NULL; + stream->wt.prev = stream->wt.next = NULL; +} + +void nghttp3_wt_ctrl_read_state_reset(nghttp3_wt_ctrl_read_state *rstate) { + *rstate = (nghttp3_wt_ctrl_read_state){0}; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h new file mode 100644 index 00000000000000..eb4b2df13fc823 --- /dev/null +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h @@ -0,0 +1,86 @@ +/* + * nghttp3 + * + * Copyright (c) 2025 nghttp3 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP3_WT_H +#define NGHTTP3_WT_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include + +#include "nghttp3_stream.h" + +/* NGHTTP3_WT_SESSION_FLAG_CONFIRMED indicates that WebTransport + session has been established. */ +#define NGHTTP3_WT_SESSION_FLAG_CONFIRMED 0x1 +/* NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED indicates that HTTP/3 + response has been submitted via nghttp3_conn_submit_wt_response. */ +#define NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED 0x2 + +typedef enum nghttp3_wt_ctrl_stream_state { + NGHTTP3_WT_CTRL_STREAM_STATE_TYPE, + NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH, + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE, + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG, + NGHTTP3_WT_CTRL_STREAM_STATE_IGN, +} nghttp3_wt_ctrl_stream_state; + +typedef struct nghttp3_wt_ctrl_read_state { + nghttp3_varint_read_state rvint; + nghttp3_exfr_cpsl cpsl; + uint64_t left; + size_t field_left; + int state; +} nghttp3_wt_ctrl_read_state; + +typedef struct nghttp3_wt_session { + nghttp3_wt_ctrl_read_state rstate; + struct { + nghttp3_vec error_msg; + } tx; + struct { + nghttp3_vec error_msg; + uint32_t error_code; + } rx; + int64_t session_id; + nghttp3_stream *head; + uint32_t flags; +} nghttp3_wt_session; + +void nghttp3_wt_session_add_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream); + +void nghttp3_wt_session_remove_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream); + +int nghttp3_wt_session_new(nghttp3_wt_session **pwts, int64_t stream_id, + const nghttp3_mem *mem); + +void nghttp3_wt_session_del(nghttp3_wt_session *wts, const nghttp3_mem *mem); + +void nghttp3_wt_ctrl_read_state_reset(nghttp3_wt_ctrl_read_state *rstate); + +#endif /* !defined(NGHTTP3_WT_H) */ From eab7db27143fc3745bd5013f5896f9f441942a2a Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 09:52:12 +0200 Subject: [PATCH 02/19] nghttp3: add wt to gyp --- deps/ngtcp2/ngtcp2.gyp | 1 + 1 file changed, 1 insertion(+) diff --git a/deps/ngtcp2/ngtcp2.gyp b/deps/ngtcp2/ngtcp2.gyp index dd0a64d5852b1b..043b6e442fcf64 100644 --- a/deps/ngtcp2/ngtcp2.gyp +++ b/deps/ngtcp2/ngtcp2.gyp @@ -90,6 +90,7 @@ 'nghttp3/lib/nghttp3_unreachable.c', 'nghttp3/lib/nghttp3_vec.c', 'nghttp3/lib/nghttp3_version.c', + 'nghttp3/lib/nghttp3_wt.c', ], 'ngtcp2_test_server_sources': [ 'ngtcp2/examples/tls_server_session_ossl.cc', From bf721a4e44f9e7e4eb204fbbafa520d6adfebb32 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 23 May 2026 18:02:24 +0200 Subject: [PATCH 03/19] quic: Implement webtransport settings --- lib/internal/quic/quic.js | 1 + src/quic/application.cc | 10 ++++++-- src/quic/application.h | 1 + src/quic/bindingdata.h | 1 + src/quic/http3.cc | 25 ++++++++++++------- src/quic/session.h | 1 + test/parallel/test-quic-h3-settings.mjs | 1 + .../test-quic-session-application-options.mjs | 2 ++ 8 files changed, 31 insertions(+), 11 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 99eeccc3c51d23..311741f591550b 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -369,6 +369,7 @@ const endpointRegistry = new SafeSet(); * @property {bigint|number} [qpackBlockedStreams] The qpack blocked streams * @property {boolean} [enableConnectProtocol] Enable the connect protocol * @property {boolean} [enableDatagrams] Enable datagrams + * @property {boolean} [enableWebtransport] Enable webtransport */ /** diff --git a/src/quic/application.cc b/src/quic/application.cc index ce5d5e12154d8a..4952b3b71b04a5 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -57,6 +57,7 @@ Session::Application_Options::operator const nghttp3_settings() const { .glitch_ratelim_burst = 1000, .glitch_ratelim_rate = 33, .qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_EAGER, + .wt_enabled = enable_webtransport }; } @@ -78,6 +79,8 @@ std::string Session::Application_Options::ToString() const { (enable_connect_protocol ? std::string("yes") : std::string("no")); res += prefix + "enable datagrams: " + (enable_datagrams ? std::string("yes") : std::string("no")); + res += prefix + "webtransport enabled: " + + (enable_webtransport ? std::string("yes") : std::string("no")); res += indent.Close(); return res; } @@ -107,7 +110,7 @@ Maybe Session::Application_Options::From( !SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) || !SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams) || !SET(enable_connect_protocol) || - !SET(enable_datagrams)) { + !SET(enable_datagrams) || !SET(enable_webtransport)) { // The call to SetOption should have scheduled an exception to be thrown. return Nothing(); } @@ -138,6 +141,7 @@ MaybeLocal Session::Application_Options::ToObject( "qpackBlockedStreams", "enableConnectProtocol", "enableDatagrams", + "enableWebtransport" }; if (tmpl.IsEmpty()) { tmpl = DictionaryTemplate::New(env->isolate(), names); @@ -153,6 +157,7 @@ MaybeLocal Session::Application_Options::ToObject( BigInt::NewFromUnsigned(env->isolate(), qpack_blocked_streams), Boolean::New(env->isolate(), enable_connect_protocol), Boolean::New(env->isolate(), enable_datagrams), + Boolean::New(env->isolate(), enable_webtransport), }; static_assert(std::size(values) == std::size(names)); @@ -248,7 +253,8 @@ bool Session::Application::ValidateTicketData( options.qpack_blocked_streams >= ticket.qpack_blocked_streams && (!ticket.enable_connect_protocol || options.enable_connect_protocol) && - (!ticket.enable_datagrams || options.enable_datagrams); + (!ticket.enable_datagrams || options.enable_datagrams) && + (!ticket.enable_webtransport || options.enable_webtransport); } // DefaultTicketData always validates. return true; diff --git a/src/quic/application.h b/src/quic/application.h index 0df9b9f0a0e68d..5f2d5adfb57c40 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -25,6 +25,7 @@ struct Http3TicketData { uint64_t qpack_blocked_streams; bool enable_connect_protocol; bool enable_datagrams; + bool enable_webtransport; }; using PendingTicketAppData = std::variant; diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 7879220e02b482..539adada9d9bdc 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -89,6 +89,7 @@ class SessionManager; V(enable_connect_protocol, "enableConnectProtocol") \ V(enable_early_data, "enableEarlyData") \ V(enable_datagrams, "enableDatagrams") \ + V(enable_webtransport, "enableWebtransport") \ V(enable_tls_trace, "tlsTrace") \ V(endpoint, "Endpoint") \ V(endpoint_udp, "Endpoint::UDP") \ diff --git a/src/quic/http3.cc b/src/quic/http3.cc index bc479f96990577..fed6795240b0b5 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -28,8 +28,8 @@ namespace quic { namespace { constexpr uint8_t kSessionTicketAppDataVersion = 1; -// Layout: [type(1)][version(1)][crc(4)][payload(34)] = 40 bytes -constexpr size_t kSessionTicketAppDataSize = 40; +// Layout: [type(1)][version(1)][crc(4)][payload(35)] = 41 bytes +constexpr size_t kSessionTicketAppDataSize = 41; constexpr size_t kSessionTicketAppDataHeaderSize = 6; // type + version + crc constexpr size_t kSessionTicketAppDataPayloadSize = kSessionTicketAppDataSize - kSessionTicketAppDataHeaderSize; @@ -405,8 +405,9 @@ class Http3ApplicationImpl final : public Session::Application { WriteBE64(payload + 8, options_.qpack_max_dtable_capacity); WriteBE64(payload + 16, options_.qpack_encoder_max_dtable_capacity); WriteBE64(payload + 24, options_.qpack_blocked_streams); - payload[32] = options_.enable_connect_protocol ? 1 : 0; + payload[32] = options_.enable_connect_protocol ? 1 : 0; // May be bitfield should be used! payload[33] = options_.enable_datagrams ? 1 : 0; + payload[34] = options_.enable_webtransport ? 1 : 0; uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, payload, kSessionTicketAppDataPayloadSize); @@ -459,32 +460,36 @@ class Http3ApplicationImpl final : public Session::Application { uint64_t stored_qpack_blocked_streams = ReadBE64(payload + 24); bool stored_enable_connect_protocol = payload[32] != 0; bool stored_enable_datagrams = payload[33] != 0; + bool stored_enable_webtransport = payload[34] != 0; Debug(&session(), "Ticket app data: stored mfss=%" PRIu64 " qmdc=%" PRIu64 - " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d", + " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d ew=%d", stored_max_field_section_size, stored_qpack_max_dtable_capacity, stored_qpack_encoder_max_dtable_capacity, stored_qpack_blocked_streams, stored_enable_connect_protocol, - stored_enable_datagrams); + stored_enable_datagrams, + stored_enable_webtransport); Debug(&session(), "Current opts: mfss=%" PRIu64 " qmdc=%" PRIu64 " qemdc=%" PRIu64 - " qbs=%" PRIu64 " ecp=%d ed=%d", + " qbs=%" PRIu64 " ecp=%d ed=%d ew %d", options_.max_field_section_size, options_.qpack_max_dtable_capacity, options_.qpack_encoder_max_dtable_capacity, options_.qpack_blocked_streams, options_.enable_connect_protocol, - options_.enable_datagrams); + options_.enable_datagrams, + options_.enable_webtransport); if (options_.max_field_section_size < stored_max_field_section_size || options_.qpack_max_dtable_capacity < stored_qpack_max_dtable_capacity || options_.qpack_encoder_max_dtable_capacity < stored_qpack_encoder_max_dtable_capacity || options_.qpack_blocked_streams < stored_qpack_blocked_streams || (stored_enable_connect_protocol && !options_.enable_connect_protocol) || - (stored_enable_datagrams && !options_.enable_datagrams)) { + (stored_enable_datagrams && !options_.enable_datagrams) || + (stored_enable_webtransport && !options_.enable_webtransport)) { Debug(&session(), "Ticket app data REJECTED"); return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; } @@ -507,7 +512,8 @@ class Http3ApplicationImpl final : public Session::Application { options_.qpack_blocked_streams >= ticket.qpack_blocked_streams && (!ticket.enable_connect_protocol || options_.enable_connect_protocol) && - (!ticket.enable_datagrams || options_.enable_datagrams); + (!ticket.enable_datagrams || options_.enable_datagrams) && + (!ticket.enable_webtransport || options_.enable_webtransport); } void ReceiveStreamClose(Stream* stream, @@ -999,6 +1005,7 @@ class Http3ApplicationImpl final : public Session::Application { void OnReceiveSettings(const nghttp3_proto_settings* settings) { options_.enable_connect_protocol = settings->enable_connect_protocol; options_.enable_datagrams = settings->h3_datagram; + options_.enable_webtransport = settings->wt_enabled; options_.max_field_section_size = settings->max_field_section_size; options_.qpack_blocked_streams = settings->qpack_blocked_streams; options_.qpack_max_dtable_capacity = settings->qpack_max_dtable_capacity; diff --git a/src/quic/session.h b/src/quic/session.h index 0caeb764ba56c8..92d150e97c446a 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -83,6 +83,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool enable_connect_protocol = true; bool enable_datagrams = true; + bool enable_webtransport = false; // for a client always enabling it, may be good operator const nghttp3_settings() const; diff --git a/test/parallel/test-quic-h3-settings.mjs b/test/parallel/test-quic-h3-settings.mjs index d954813c9c2564..ec5a29c116f961 100644 --- a/test/parallel/test-quic-h3-settings.mjs +++ b/test/parallel/test-quic-h3-settings.mjs @@ -5,6 +5,7 @@ // maxHeaderLength enforcement - reject headers exceeding byte length // enableConnectProtocol setting (accepted without error) // enableDatagrams setting (accepted without error) +// enableWebtransport setting (accepted without error) import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; diff --git a/test/parallel/test-quic-session-application-options.mjs b/test/parallel/test-quic-session-application-options.mjs index 1f5c9c0926808c..055983ea10864e 100644 --- a/test/parallel/test-quic-session-application-options.mjs +++ b/test/parallel/test-quic-session-application-options.mjs @@ -25,6 +25,7 @@ const customAppOptions = { qpackBlockedStreams: 50n, enableConnectProtocol: false, enableDatagrams: false, + enableWebtransport: false, }; const serverDone = Promise.withResolvers(); @@ -53,6 +54,7 @@ const serverEndpoint = await listen(mustCall((serverSession) => { strictEqual(opts.enableConnectProtocol, customAppOptions.enableConnectProtocol); strictEqual(opts.enableDatagrams, customAppOptions.enableDatagrams); + strictEqual(opts.enableWebtransport, customAppOptions.enableWebtransport); stream.writer.endSync(); await stream.closed; From 88c1f4f60f200a50bfd72267193049c4c90db990 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 24 May 2026 11:54:50 +0200 Subject: [PATCH 04/19] quic: Implement webtransport response and reply --- lib/internal/quic/quic.js | 33 +++++++++++++++--- src/quic/defs.h | 1 + src/quic/http3.cc | 73 ++++++++++++++++++++++++++++----------- src/quic/session.h | 3 +- src/quic/streams.cc | 3 ++ 5 files changed, 87 insertions(+), 26 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 311741f591550b..c01d8161c2d117 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -76,6 +76,7 @@ const { QUIC_STREAM_HEADERS_KIND_TRAILING: kHeadersKindTrailing, QUIC_STREAM_HEADERS_FLAGS_NONE: kHeadersFlagsNone, QUIC_STREAM_HEADERS_FLAGS_TERMINAL: kHeadersFlagsTerminal, + QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT: kHeadersFlagsWebtransport, } = internalBinding('quic'); // Maps the numeric HeadersKind constants from C++ to user-facing strings. @@ -291,7 +292,9 @@ const endpointRegistry = new SafeSet(); * @property {string|ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob| * FileHandle|AsyncIterable|Iterable|Promise|null} [body] The outbound * body source. See the public docs for `stream.setBody()` for details - * on supported types. When omitted, the stream is closed immediately. + * on supported types. When omitted, the stream is closed immediately, + * except if the stream is a webtransport stream. For a webtransport + * stream setting body is illegal. * @property {object} [headers] Initial request or response headers to * send. Only used when the negotiated application supports headers * (e.g. HTTP/3). @@ -299,6 +302,8 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [highWaterMark] The high water mark for write * backpressure, in bytes. **Default:** `65536`. + * @property {boolean} [webtransport] Indicates that the send headers signal + * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers @@ -509,6 +514,10 @@ const endpointRegistry = new SafeSet(); * @typedef {object} SendHeadersOptions * @property {boolean} [terminal] When true, indicates that no body data will be * sent after these headers. + * @property {boolean} [webtransport] When true, indicates that this is a header + * for a webtransport connection. Note, on the response, if you deny a + * webtransport connection, set it to false. A webtransport header can not be + * terminal. So we throw an error, if webtransport and terminal are both true. */ /** @@ -2061,10 +2070,15 @@ class QuicStream { 'The negotiated QUIC application protocol does not support headers'); } validateObject(headers, 'headers'); - const { terminal = false } = options; + const { terminal = false, webtransport = false } = options; + if (terminal && webtransport) { + throw new ERR_INVALID_ARG_VALUE( + 'webtransport and terminal can not be set simultaenously.'); + } const headerString = buildNgHeaderString( headers, assertValidPseudoHeader, true /* strictSingleValueFields */); - const flags = terminal ? kHeadersFlagsTerminal : kHeadersFlagsNone; + const flags = terminal ? kHeadersFlagsTerminal : + (webtransport ? kHeadersFlagsWebtransport : kHeadersFlagsNone); return this.#handle.sendHeaders(kHeadersKindInitial, headerString, flags); } @@ -3252,6 +3266,7 @@ class QuicSession { body, priority = 'default', incremental = false, + webtransport = false, highWaterMark = kDefaultHighWaterMark, headers, onheaders, @@ -3262,9 +3277,14 @@ class QuicSession { validateOneOf(priority, 'options.priority', ['default', 'low', 'high']); validateBoolean(incremental, 'options.incremental'); + validateBoolean(webtransport, 'options.webtransport'); const validatedBody = validateBody(body); + if (typeof body !== 'undefined' && webtransport) { + throw new ERR_INVALID_ARG_TYPE('options.body', 'A body can not be provided in webtransport mode'); + } + const handle = this.#handle.openStream(direction, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); @@ -3298,7 +3318,12 @@ class QuicSession { if (onwanttrailers) stream.onwanttrailers = onwanttrailers; if (headers !== undefined) { - stream.sendHeaders(headers, { terminal: validatedBody === undefined }); + stream.sendHeaders(headers, + { terminal: validatedBody === undefined && !webtransport, + webtransport }); + } else if (webtransport) { + throw new ERR_INVALID_ARG_VALUE('options.webtransport', + 'Specifying webtransport without a header has no effect'); } if (onSessionOpenStreamChannel.hasSubscribers) { diff --git a/src/quic/defs.h b/src/quic/defs.h index 75ae915335be93..d937469a41915d 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -309,6 +309,7 @@ enum class HeadersKind : uint8_t { enum class HeadersFlags : uint8_t { NONE, TERMINAL, + WEBTRANSPORT }; enum class StreamPriority : uint8_t { diff --git a/src/quic/http3.cc b/src/quic/http3.cc index fed6795240b0b5..a2a215b602bf4f 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -405,7 +405,8 @@ class Http3ApplicationImpl final : public Session::Application { WriteBE64(payload + 8, options_.qpack_max_dtable_capacity); WriteBE64(payload + 16, options_.qpack_encoder_max_dtable_capacity); WriteBE64(payload + 24, options_.qpack_blocked_streams); - payload[32] = options_.enable_connect_protocol ? 1 : 0; // May be bitfield should be used! + payload[32] = options_.enable_connect_protocol ? 1 : 0; + // May be bitfield should be used! payload[33] = options_.enable_datagrams ? 1 : 0; payload[34] = options_.enable_webtransport ? 1 : 0; @@ -591,33 +592,63 @@ class Http3ApplicationImpl final : public Session::Application { // If the terminal flag is set, that means that we know we're only // sending headers and no body and the stream writable side should be // closed immediately because there is no nghttp3_data_reader provided. - if (flags != HeadersFlags::TERMINAL) { + if (flags != HeadersFlags::TERMINAL + && flags != HeadersFlags::WEBTRANSPORT) { reader_ptr = &reader; } if (session().is_server()) { // If this is a server, we're submitting a response... - Debug(&session(), - "Submitting %" PRIu64 " response headers for stream %" PRIu64, - nva.length(), - stream.id()); - return nghttp3_conn_submit_response(*this, - stream.id(), - nva.data(), - nva.length(), - reader_ptr) == 0; + if (flags != HeadersFlags::WEBTRANSPORT) { + Debug(&session(), + "Submitting %" PRIu64 " response headers for stream %" PRIu64, + nva.length(), + stream.id()); + return nghttp3_conn_submit_response(*this, + stream.id(), + nva.data(), + nva.length(), + reader_ptr) == 0; + } else { + Debug(&session(), + "Submitting %" PRIu64 " wt resp. headers for stream %" PRIu64, + nva.length(), + stream.id()); + if (nghttp3_conn_submit_wt_response(*this, + stream.id(), + nva.data(), + nva.length()) != 0) + return false; + return nghttp3_conn_server_confirm_wt_session(*this, + stream.id(), + 0) == 0; + } } else { // Otherwise we're submitting a request... - Debug(&session(), - "Submitting %" PRIu64 " request headers for stream %" PRIu64, - nva.length(), - stream.id()); - return nghttp3_conn_submit_request(*this, - stream.id(), - nva.data(), - nva.length(), - reader_ptr, - const_cast(&stream)) == 0; + if (flags != HeadersFlags::WEBTRANSPORT) { + Debug(&session(), + "Submitting %" PRIu64 " request headers for stream %" PRIu64, + nva.length(), + stream.id()); + return nghttp3_conn_submit_request(*this, + stream.id(), + nva.data(), + nva.length(), + reader_ptr, + const_cast(&stream)) + == 0; + } else { + Debug(&session(), + "Submitting %" PRIu64 " wt req. headers for stream %" PRIu64, + nva.length(), + stream.id()); + return nghttp3_conn_submit_wt_request(*this, + stream.id(), + nva.data(), + nva.length(), + const_cast(&stream)) + == 0; + } } break; } diff --git a/src/quic/session.h b/src/quic/session.h index 92d150e97c446a..b258e09bae76fa 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -83,7 +83,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool enable_connect_protocol = true; bool enable_datagrams = true; - bool enable_webtransport = false; // for a client always enabling it, may be good + // for a client always enabling wt, may be good + bool enable_webtransport = false; operator const nghttp3_settings() const; diff --git a/src/quic/streams.cc b/src/quic/streams.cc index e838392361f946..0bd334d9eab41f 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -1089,6 +1089,8 @@ void Stream::InitPerContext(Realm* realm, Local target) { static_cast(HeadersFlags::NONE); constexpr int QUIC_STREAM_HEADERS_FLAGS_TERMINAL = static_cast(HeadersFlags::TERMINAL); + constexpr int QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT = + static_cast(HeadersFlags::WEBTRANSPORT); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_HINTS); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_INITIAL); @@ -1096,6 +1098,7 @@ void Stream::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_NONE); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_TERMINAL); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT); } Stream* Stream::From(void* stream_user_data) { From ca3830736e381ef8bf61d35ead4434f84b7472b4 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 25 May 2026 09:16:35 +0200 Subject: [PATCH 05/19] quic: Improve webtransport options and lint --- lib/internal/quic/quic.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index c01d8161c2d117..2087a9298106af 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -302,7 +302,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [highWaterMark] The high water mark for write * backpressure, in bytes. **Default:** `65536`. - * @property {boolean} [webtransport] Indicates that the send headers signal + * @property {boolean} [webtransport] Indicates that the headers to send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers @@ -2073,7 +2073,8 @@ class QuicStream { const { terminal = false, webtransport = false } = options; if (terminal && webtransport) { throw new ERR_INVALID_ARG_VALUE( - 'webtransport and terminal can not be set simultaenously.'); + 'webtransport and terminal can not be set simultaenously.', + { terminal, webtransport }); } const headerString = buildNgHeaderString( headers, assertValidPseudoHeader, true /* strictSingleValueFields */); From 428999b1472a312adb9509364f53927486112ec3 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 30 May 2026 19:44:12 +0200 Subject: [PATCH 06/19] quic: Make a webtransport stream --- lib/internal/quic/quic.js | 35 +++++++++++++++++++++++++++++++++++ lib/internal/quic/symbols.js | 2 ++ src/quic/application.h | 8 ++++++++ src/quic/defs.h | 5 +++++ src/quic/http3.cc | 18 ++++++++++++++++++ src/quic/streams.cc | 31 +++++++++++++++++++++++++++++++ src/quic/streams.h | 2 ++ 7 files changed, 101 insertions(+) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 2087a9298106af..0990daccaed702 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -209,6 +209,7 @@ const { kPrivateConstructor, kReset, kSendHeaders, + kMakeWebtransportStream, kSessionApplication, kSessionTicket, kTrailers, @@ -302,6 +303,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [highWaterMark] The high water mark for write * backpressure, in bytes. **Default:** `65536`. + * @property {QuicStream} [webtransportSession] The webtransport session control stream. * @property {boolean} [webtransport] Indicates that the headers to send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers @@ -2516,6 +2518,20 @@ class QuicStream { return this.#handle.sendHeaders(kind, headerString, flags); } + /** + * Attaches a webtransport session to the stream and sends initial bytes + * indicating webtransport stream and the session id + * @returns {boolean} true if it succeeded. + */ + [kMakeWebtransportStream](session) { + if (this.pending) { + debug('pending stream enqueuing makeWebtransportStream for session', session.id); + } else { + debug(`stream ${this.id} makeWebtransportStream for session`, session.id); + } + return this.#handle.makeWebtransportStream(session.#handle); + } + [kFinishClose](error) { const inner = this.#inner; inner.pendingClose ??= PromiseWithResolvers(); @@ -3268,6 +3284,7 @@ class QuicSession { priority = 'default', incremental = false, webtransport = false, + webtransportSession = undefined, highWaterMark = kDefaultHighWaterMark, headers, onheaders, @@ -3286,6 +3303,19 @@ class QuicSession { throw new ERR_INVALID_ARG_TYPE('options.body', 'A body can not be provided in webtransport mode'); } + if (webtransportSession) { + if (webtransport && webtransportSession) { + throw new ERR_INVALID_ARG_TYPE('options.webtransport', 'webtransport can not be set for creating a webtransport stream associated with a webtransport session'); + } + + if (headers && webtransportSession) { + throw new ERR_INVALID_ARG_TYPE('options.headers', 'headers can not be set for creating a webtransport stream associated with a webtransport session'); + } + if (!isQuicStream(webtransportSession)) { + throw new ERR_INVALID_ARG_TYPE('options.webtransportSession', 'webtransportSession was to be of type QuicStream'); + } + } + const handle = this.#handle.openStream(direction, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); @@ -3326,6 +3356,11 @@ class QuicSession { throw new ERR_INVALID_ARG_VALUE('options.webtransport', 'Specifying webtransport without a header has no effect'); } + if (webtransportSession) { + if (!stream[kMakeWebtransportStream](webtransportSession)) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + } if (onSessionOpenStreamChannel.hasSubscribers) { onSessionOpenStreamChannel.publish({ diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 665ab7ca1911c2..2afbe2ad5f80d8 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -55,6 +55,7 @@ const kRemoveSession = Symbol('kRemoveSession'); const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); +const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); const kSessionApplication = Symbol('kSessionApplication'); const kSessionTicket = Symbol('kSessionTicket'); const kTrailers = Symbol('kTrailers'); @@ -91,6 +92,7 @@ module.exports = { kRemoveStream, kReset, kSendHeaders, + kMakeWebtransportStream, kSessionApplication, kSessionTicket, kTrailers, diff --git a/src/quic/application.h b/src/quic/application.h index 5f2d5adfb57c40..54a48e85ce1edb 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -206,6 +206,14 @@ class Session::Application : public MemoryRetainer { return false; } + // connects the webtransport session stream to stream object, + // it also sends some initial bytes to the wire to signal + // the other side, that this is a webtransport stream + virtual bool MakeWebtransportStream(const Stream& stream, + int64_t sessionid) { + return false; + } + // Signals to the Application that it should serialize and transmit any // pending session and stream packets it has accumulated. void SendPendingData(); diff --git a/src/quic/defs.h b/src/quic/defs.h index d937469a41915d..a9607bba497517 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -300,6 +300,11 @@ enum class Direction : uint8_t { UNIDIRECTIONAL, }; +enum class StreamType : uint8_t { + QUICSTREAM, // standard quic stream + WTSTREAM, // quic stream associated with webtransport session +}; + enum class HeadersKind : uint8_t { HINTS, INITIAL, diff --git a/src/quic/http3.cc b/src/quic/http3.cc index a2a215b602bf4f..c7f6fbcc0ad1d5 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -666,6 +666,24 @@ class Http3ApplicationImpl final : public Session::Application { return false; } + bool MakeWebtransportStream(const Stream& stream, int64_t sessionid) override { + Session::SendPendingDataScope send_scope(&session()); + static constexpr nghttp3_data_reader reader = {on_read_data_callback}; + const nghttp3_data_reader* reader_ptr = &reader; // can use the same reader + printf("mws in mark 3\n"); + + Debug(&session(), + "Make stream %" PRIu64 " webtransport stream of session %" PRIu64, + stream.id(), + sessionid); + return nghttp3_conn_open_wt_data_stream(*this, + sessionid, + stream.id(), + reader_ptr, + const_cast(&stream)) + == 0; + } + void SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) override { diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 0bd334d9eab41f..f57f67ff496afd 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -97,6 +97,7 @@ namespace quic { V(AttachSource, attachSource, false) \ V(Destroy, destroy, false) \ V(SendHeaders, sendHeaders, false) \ + V(MakeWebtransportStream, makeWebtransportStream, false) \ V(StopSending, stopSending, false) \ V(ResetStream, resetStream, false) \ V(SetPriority, setPriority, false) \ @@ -474,6 +475,26 @@ struct Stream::Impl { *stream, kind, headers, flags)); } + // Connects a stream to a webtransport session stream, + // also sends the initial bytes of a stream to signel the wt stream + // also connects the readers + JS_METHOD(MakeWebtransportStream) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); + CHECK(args.Length() > 0); + CHECK(args[0]->IsObject()); + Stream* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + if (stream->is_pending()) { + stream->EnqueuePendingWebtransportStream(session->id()); + return args.GetReturnValue().Set(true); + } + args.GetReturnValue().Set(stream->session().application().MakeWebtransportStream( + *stream, + session->id() + )); + } + // Tells the peer to stop sending data for this stream. This has the effect // of shutting down the readable side of the stream for this peer. Any data // that has already been received is still readable. @@ -1287,6 +1308,11 @@ void Stream::NotifyStreamOpened(stream_id id) { headers->flags); } } + if (pending_webtransport_session_ >= 0) { + session().application().MakeWebtransportStream(*this, + pending_webtransport_session_); + pending_webtransport_session_ = 0; + } // If the stream is not a local undirectional stream and is_readable is // false, then we should shutdown the streams readable side now. if (!is_local_unidirectional() && !is_readable()) { @@ -1324,6 +1350,11 @@ void Stream::EnqueuePendingHeaders(HeadersKind kind, kind, Global(env()->isolate(), headers), flags)); } +void Stream::EnqueuePendingWebtransportStream(int64_t sessionid) { + Debug(this, "Enqueing Webtransport Session strean for pending stream"); + pending_webtransport_session_ = sessionid; +} + bool Stream::is_pending() const { return state()->pending; } diff --git a/src/quic/streams.h b/src/quic/streams.h index 86cb36b2668985..c9ce584c94d08c 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -432,6 +432,7 @@ class Stream final : public AsyncWrap, void EnqueuePendingHeaders(HeadersKind kind, v8::Local headers, HeadersFlags flags); + void EnqueuePendingWebtransportStream(int64_t sessionid); ArenaSlotBase stats_slot_; ArenaSlotBase state_slot_; @@ -447,6 +448,7 @@ class Stream final : public AsyncWrap, std::optional> maybe_pending_stream_ = std::nullopt; std::vector> pending_headers_queue_; + int64_t pending_webtransport_session_ = -1; error_code pending_close_read_code_ = 0; error_code pending_close_write_code_ = 0; From adbc6ca24db2614cf5467b0d547875601c5f40af Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Wed, 10 Jun 2026 06:36:59 +0200 Subject: [PATCH 07/19] quic: Implement more webtransport stuff --- lib/internal/quic/diagnostics.js | 2 + lib/internal/quic/quic.js | 68 ++++++++++++++++++++++++++++++++ lib/internal/quic/state.js | 21 ++++++++++ lib/internal/quic/symbols.js | 2 + src/quic/bindingdata.h | 1 + src/quic/http3.cc | 35 +++++++++++++++- src/quic/streams.cc | 31 ++++++++++++++- src/quic/streams.h | 18 ++++++++- 8 files changed, 173 insertions(+), 5 deletions(-) diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index 7180f719bc09d3..c537ed93ae394f 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -33,6 +33,7 @@ const onSessionGoawayChannel = dc.channel('quic.session.goaway'); const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); const onStreamClosedChannel = dc.channel('quic.stream.closed'); const onStreamHeadersChannel = dc.channel('quic.stream.headers'); +const onStreamSessionIdChannel = dc.channel('quic.stream.sessioid'); const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); const onStreamInfoChannel = dc.channel('quic.stream.info'); const onStreamResetChannel = dc.channel('quic.stream.reset'); @@ -68,6 +69,7 @@ module.exports = { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 0990daccaed702..22cffbc401b398 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -194,6 +194,7 @@ const { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, kOwner, kRemoveSession, kKeylog, @@ -268,6 +269,7 @@ const { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, @@ -307,6 +309,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [webtransport] Indicates that the headers to send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers + * @property {OnSessionIdCallback} [onsessionid] Callback for incoming sessionid * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers * @property {OnWantTrailersCallback} [onwanttrailers] Callback fired when the @@ -469,6 +472,7 @@ const endpointRegistry = new SafeSet(); * @property {OnQlogCallback} [onqlog] qlog data callback. * @property {OnApplicationCallback} [onapplication] application options callback. * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. + * @property {OnSessionIdCallback} [onsessionid] Default perstream initial callback for incoming sessionid. * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream @@ -716,6 +720,17 @@ const endpointRegistry = new SafeSet(); * @returns {void} */ +/** + * Called when session id (e.g. for Webtransport streams) is determined + * @callback OnSessionIdCallback + * @this {QuicStream} + * @param {bigint|undefined} sessionid Id of the session stream or undefined + * if no session stream is determined. + * @returns {void} + */ + + + /** * Called when trailing headers are received from the peer. * @callback OnTrailersCallback @@ -1004,6 +1019,13 @@ setCallbacks({ this[kOwner][kHeaders](headers, kind); }, + onStreamSessionId(sessionId) { + // Called when the stream C++ handle has determined the sessionId + console.log('onStreamSessionId', sessionId); + debug(`stream ${this[kOwner].id} sessionid callback`, sessionId); + this[kOwner][kSessionId](sessionId); + }, + onStreamTrailers() { // Called when the stream C++ handle is ready to receive trailing headers. debug('stream want trailers callback', this[kOwner]); @@ -1325,6 +1347,7 @@ function applyCallbacks(session, cbs) { onwanttrailers: cbs.onwanttrailers, }; } + if (cbs.onsessionid) session.onsessionid = cbs.onsessionid; } /** @@ -1584,6 +1607,7 @@ class QuicStream { onblocked: undefined, onreset: undefined, onheaders: undefined, + onsessionid: undefined, ontrailers: undefined, oninfo: undefined, onwanttrailers: undefined, @@ -1808,6 +1832,25 @@ class QuicStream { } } + /** @type {OnSessionIdCallback} */ + get onsessionid() { + assertIsQuicStream(this); + return this.#inner.onsessionid; + } + + set onsessionid(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onsessionid = undefined; + inner.state.wantsSessionId = false; + } else { + validateFunction(fn, 'onsessionid'); + inner.onsessionid = FunctionPrototypeBind(fn, this); + inner.state.wantsSessionId = true; + } + } + /** @type {Function|undefined} */ get oninfo() { assertIsQuicStream(this); @@ -2571,6 +2614,7 @@ class QuicStream { inner.onblocked = undefined; inner.onreset = undefined; inner.onheaders = undefined; + inner.onsessionid = undefined; inner.onerror = undefined; inner.ontrailers = undefined; inner.oninfo = undefined; @@ -2669,6 +2713,23 @@ class QuicStream { } } + [kSessionId](sessionId) { + if (this.destroyed) return; + const inner = this.#inner; + if (onStreamSessionIdChannel.hasSubscribers) { + onStreamSessionIdChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + sessionid, + }); + } + if (typeof inner.onsessionid === 'function') { + console.log('kSessionId pass sess', sessionId); + safeCallbackInvoke(inner.onsessionid, this, sessionId); + } + } + [kTrailers]() { if (this.destroyed) return; const inner = this.#inner; @@ -3288,6 +3349,7 @@ class QuicSession { highWaterMark = kDefaultHighWaterMark, headers, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -3344,6 +3406,7 @@ class QuicSession { // Set stream callbacks before sending headers to avoid missing events. if (onheaders) stream.onheaders = onheaders; + if (onsessionid) stream.onsessionid = onsessionid; if (ontrailers) stream.ontrailers = ontrailers; if (oninfo) stream.oninfo = oninfo; if (onwanttrailers) stream.onwanttrailers = onwanttrailers; @@ -4127,6 +4190,7 @@ class QuicSession { const scbs = this[kStreamCallbacks]; if (scbs) { if (scbs.onheaders) stream.onheaders = scbs.onheaders; + if (scbs.onsessionid) stream.onsessionid = scbs.onsessionid; if (scbs.ontrailers) stream.ontrailers = scbs.ontrailers; if (scbs.oninfo) stream.oninfo = scbs.oninfo; if (scbs.onwanttrailers) stream.onwanttrailers = scbs.onwanttrailers; @@ -4504,6 +4568,7 @@ class QuicEndpoint { onapplication, // Stream-level callbacks applied to each incoming stream. onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -4529,6 +4594,7 @@ class QuicEndpoint { onqlog, onapplication, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -5264,6 +5330,7 @@ function processSessionOptions(options, config = kEmptyObject) { // Application level options changed, e.g. HTTP/3 settings related // Stream-level callbacks. onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -5385,6 +5452,7 @@ function processSessionOptions(options, config = kEmptyObject) { onqlog, onapplication, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 8815b0bb4c32cf..509fd320ae5e72 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -100,6 +100,7 @@ const { IDX_STATE_STREAM_HAS_READER, IDX_STATE_STREAM_WANTS_BLOCK, IDX_STATE_STREAM_WANTS_HEADERS, + IDX_STATE_STREAM_WANTS_SESSIONID, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_TRAILERS, IDX_STATE_STREAM_RECEIVED_EARLY_DATA, @@ -141,6 +142,7 @@ assert(IDX_STATE_STREAM_HAS_OUTBOUND !== undefined); assert(IDX_STATE_STREAM_HAS_READER !== undefined); assert(IDX_STATE_STREAM_WANTS_BLOCK !== undefined); assert(IDX_STATE_STREAM_WANTS_HEADERS !== undefined); +assert(IDX_STATE_STREAM_WANTS_SESSIONID !== undefined); assert(IDX_STATE_STREAM_WANTS_RESET !== undefined); assert(IDX_STATE_STREAM_WANTS_TRAILERS !== undefined); assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined); @@ -812,6 +814,21 @@ class QuicStreamState { DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0); } + /** @type {boolean} */ + get wantsSessionId() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID) !== 0; + } + + /** @type {boolean} */ + set wantsSessionId(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID, val ? 1 : 0); + } + + /** @type {boolean} */ get wantsReset() { const handle = this.#handle; @@ -904,6 +921,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, wantsTrailers, early, resetCode, @@ -924,6 +942,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, wantsTrailers, early, resetCode, @@ -961,6 +980,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, wantsTrailers, early, resetCode, @@ -981,6 +1001,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, wantsTrailers, early, resetCode, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 2afbe2ad5f80d8..4d98fbdc9c188b 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -40,6 +40,7 @@ const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); +const kSessionId = Symbol('kSessionId'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); @@ -75,6 +76,7 @@ module.exports = { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, kInspect, kKeylog, kKeyObjectHandle, diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 539adada9d9bdc..326a0fc73754f7 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -60,6 +60,7 @@ class SessionManager; V(stream_created, StreamCreated) \ V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ + V(stream_sessionid, StreamSessionId) \ V(stream_reset, StreamReset) \ V(stream_trailers, StreamTrailers) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index c7f6fbcc0ad1d5..c7fff54f294c9f 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1261,6 +1261,37 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } + static int on_receive_wt_data(nghttp3_conn *conn, + int64_t session_id, + int64_t stream_id, + const uint8_t *data, + size_t datalen, + void *conn_user_data, + void *stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto& session = app.session(); + printf("on_receive_wt_data %d: %d %d %d\n", stream_id, data[0], data[1], data[2]); + if (auto stream = FindOrCreateStream(conn, &session, stream_id)) [[likely]] { + stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{}); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + static int on_wt_data_stream_open(nghttp3_conn *conn, + int64_t session_id, + int64_t stream_id, + void *conn_user_data, + void *stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto& session = app.session(); + if (auto stream = FindOrCreateStream(conn, &session, stream_id)) [[likely]] { + stream->NotifyWTSession(session_id); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + static int on_deferred_consume(nghttp3_conn* conn, stream_id id, size_t consumed, @@ -1458,7 +1489,9 @@ class Http3ApplicationImpl final : public Session::Application { on_receive_origin, on_end_origin, on_rand, - on_receive_settings}; + on_receive_settings, + on_receive_wt_data, + on_wt_data_stream_open}; }; std::optional ParseHttp3TicketData(const uv_buf_t& data) { diff --git a/src/quic/streams.cc b/src/quic/streams.cc index f57f67ff496afd..a1040454a419b6 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -46,6 +46,7 @@ namespace quic { #define STREAM_STATE(V) \ V(ID, id, stream_id) \ + V(SESSION_ID, session_id, stream_id) \ V(PENDING, pending, uint8_t) \ V(FIN_SENT, fin_sent, uint8_t) \ V(FIN_RECEIVED, fin_received, uint8_t) \ @@ -59,6 +60,8 @@ namespace quic { V(WANTS_BLOCK, wants_block, uint8_t) \ /* Set when the stream has a headers event handler */ \ V(WANTS_HEADERS, wants_headers, uint8_t) \ + /* Set when the stream has a sessionid event handler */ \ + V(WANTS_SESSIONID, wants_sessionid, uint8_t) \ /* Set when the stream has a reset event handler */ \ V(WANTS_RESET, wants_reset, uint8_t) \ /* Set when the stream has a trailers event handler */ \ @@ -913,7 +916,6 @@ class Stream::Outbound final : public MemoryRetainer { PullUncommitted(std::move(next)); return bob::Status::STATUS_CONTINUE; } - std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {}); return bob::Status::STATUS_BLOCK; } @@ -1158,6 +1160,7 @@ Stream::Stream(BaseObjectWeakPtr session, MakeWeak(); DCHECK(id < kMaxStreamId); state()->id = id; + state()->session_id = kMaxStreamId; state()->pending = 0; // Allows us to be notified when data is actually read from the // inbound queue so that we can update the stream flow control. @@ -1215,6 +1218,7 @@ Stream::Stream(BaseObjectWeakPtr session, state_slot_ = GetStreamStateArena(binding).Allocate(env()->isolate()); MakeWeak(); state()->id = kMaxStreamId; + state()->session_id = kMaxStreamId; state()->pending = 1; // Allows us to be notified when data is actually read from the @@ -1275,6 +1279,7 @@ void Stream::NotifyStreamOpened(stream_id id) { Debug(this, "Pending stream opened with id %" PRIi64, id); state()->pending = 0; state()->id = id; + state()->session_id = kMaxStreamId; STAT_RECORD_TIMESTAMP(Stats, opened_at); // Now that the stream is actually opened, add it to the sessions // list of known open streams. @@ -1363,6 +1368,10 @@ stream_id Stream::id() const { return state()->id; } +stream_id Stream::session_id() const { + return state()->session_id; +} + Side Stream::origin() const { CHECK(!is_pending()); return (state()->id & 0b01) ? Side::SERVER : Side::CLIENT; @@ -1608,6 +1617,7 @@ void Stream::BeginHeaders(HeadersKind kind) { headers_length_ = 0; headers_.clear(); set_headers_kind(kind); + state()->session_id = -1; // we know we are not a wt stream } void Stream::set_headers_kind(HeadersKind kind) { @@ -1626,6 +1636,13 @@ bool Stream::AddHeader(std::unique_ptr
header) { return true; } + void Stream::NotifyWTSession(stream_id session_id) { + if (state()->session_id != session_id) { + state()->session_id = session_id; + EmitSessionid(session_id); + } + } + void Stream::Acknowledge(size_t datalen) { if (outbound_ == nullptr) return; @@ -1968,6 +1985,14 @@ void Stream::EmitHeaders() { MakeCallback(binding.stream_headers_callback(), arraysize(argv), argv); } +void Stream::EmitSessionid(stream_id session_id) { + if (!env()->can_call_into_js() || !state()->wants_sessionid) return; + CallbackScope cb_scope(this); + Local sid = BigInt::New(env()->isolate(), session_id); + printf("EmitSessionid mark 2\n"); + MakeCallback(BindingData::Get(env()).stream_sessionid_callback(), 1, &sid); +} + void Stream::EmitReset(const QuicError& error) { // state()->wants_reset will be set from the javascript side if the // stream object has a handler for the reset event. @@ -1996,7 +2021,9 @@ void Stream::EmitWantTrailers() { void Stream::Schedule(Queue* queue) { // If this stream is not already in the queue to send data, add it. Debug(this, "Scheduled"); - if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this); + if (outbound_ && stream_queue_.IsEmpty()) { + queue->PushBack(this); + } } void Stream::Unschedule() { diff --git a/src/quic/streams.h b/src/quic/streams.h index c9ce584c94d08c..5ce4ef82450fda 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -248,9 +248,14 @@ class Stream final : public AsyncWrap, ~Stream() override; // While the stream is still pending, the id will be kMaxStreamId, - // inidicating the maximum possible stream id is kMaxStreamId - 1. + // indicating the maximum possible stream id is kMaxStreamId - 1. stream_id id() const; + // Until is is clear, that this has a session stream, it is kMaxStreamId + // after this it is -1, if it is not a webtransport stream + // and >= 0 it is a webtransport stream. + stream_id session_id() const; + // While the stream is still pending, the origin will be invalid. Side origin() const; @@ -351,6 +356,9 @@ class Stream final : public AsyncWrap, // have already been added, or the maximum total header length is reached. bool AddHeader(std::unique_ptr
header); + // Currently only http/3 can have a session stream in WebTransport + void NotifyWTSession(stream_id session_id); + // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_, // reader_, headers_, and pending_headers_queue_. SET_NO_MEMORY_INFO() @@ -423,16 +431,22 @@ class Stream final : public AsyncWrap, // Delivers the set of inbound headers that have been collected. void EmitHeaders(); + // Delivers the session_id aka the stream that holds e.g. the WT session. + void EmitSessionid(stream_id session_id); + void NotifyReadableEnded(error_code code); void NotifyWritableEnded(error_code code); // When a pending stream is finally opened, the NotifyStreamOpened method // will be called and the id will be assigned. void NotifyStreamOpened(stream_id id); + + // The session id can arrive later + void NotifySessionStream(stream_id session_id); void EnqueuePendingHeaders(HeadersKind kind, v8::Local headers, HeadersFlags flags); - void EnqueuePendingWebtransportStream(int64_t sessionid); + void EnqueuePendingWebtransportStream(int64_t session_id); ArenaSlotBase stats_slot_; ArenaSlotBase state_slot_; From 24be8b86af503a22d87beeee4411ee7ddb180701 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 13 Jun 2026 17:39:43 +0200 Subject: [PATCH 08/19] quic: Webtransport fix incoming streams --- src/quic/http3.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index c7fff54f294c9f..a3e747da2ea374 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1286,6 +1286,10 @@ class Http3ApplicationImpl final : public Session::Application { NGHTTP3_CALLBACK_SCOPE(app); auto& session = app.session(); if (auto stream = FindOrCreateStream(conn, &session, stream_id)) [[likely]] { + if (!app.MakeWebtransportStream(*stream.get(), session_id)) { + stream->Destroy(); // close stream forcefully, TODO may be use an assert instead? + return NGHTTP3_ERR_CALLBACK_FAILURE; + } stream->NotifyWTSession(session_id); return NGTCP2_SUCCESS; } From c17ec37d1b13356cfe78b4f515e2774583b4387a Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 13 Jun 2026 17:42:00 +0200 Subject: [PATCH 09/19] quic: Webtransport remove debug code --- src/quic/http3.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index a3e747da2ea374..e10f6e2a6394ce 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1270,7 +1270,6 @@ class Http3ApplicationImpl final : public Session::Application { void *stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); auto& session = app.session(); - printf("on_receive_wt_data %d: %d %d %d\n", stream_id, data[0], data[1], data[2]); if (auto stream = FindOrCreateStream(conn, &session, stream_id)) [[likely]] { stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{}); return NGTCP2_SUCCESS; From 84c2703d0ab3564907d9edf10d9f88ddcc1abc67 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 13 Jun 2026 18:05:45 +0200 Subject: [PATCH 10/19] quic: Do not try to open WT sending on remote unidirectional stream --- src/quic/application.h | 2 ++ src/quic/http3.cc | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/quic/application.h b/src/quic/application.h index 54a48e85ce1edb..a68020a4138ece 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -209,6 +209,8 @@ class Session::Application : public MemoryRetainer { // connects the webtransport session stream to stream object, // it also sends some initial bytes to the wire to signal // the other side, that this is a webtransport stream + // it is a noop, if we can not send on this stream incoming + // unidirectional stream virtual bool MakeWebtransportStream(const Stream& stream, int64_t sessionid) { return false; diff --git a/src/quic/http3.cc b/src/quic/http3.cc index e10f6e2a6394ce..23b2e0c040be45 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -670,12 +670,14 @@ class Http3ApplicationImpl final : public Session::Application { Session::SendPendingDataScope send_scope(&session()); static constexpr nghttp3_data_reader reader = {on_read_data_callback}; const nghttp3_data_reader* reader_ptr = &reader; // can use the same reader - printf("mws in mark 3\n"); Debug(&session(), "Make stream %" PRIu64 " webtransport stream of session %" PRIu64, stream.id(), sessionid); + // we only need to do this, if we can send data + if (stream.is_remote_unidirectional()) + return true; // so bail out for remote unidirectional streams return nghttp3_conn_open_wt_data_stream(*this, sessionid, stream.id(), From c4c5bb4a945f96fd4784a6f2cddb16748bc0004a Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 14 Jun 2026 16:57:41 +0200 Subject: [PATCH 11/19] quic: Remove debug code --- lib/internal/quic/quic.js | 2 -- src/quic/streams.cc | 1 - 2 files changed, 3 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 22cffbc401b398..729d3cfcb8f93f 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -1021,7 +1021,6 @@ setCallbacks({ onStreamSessionId(sessionId) { // Called when the stream C++ handle has determined the sessionId - console.log('onStreamSessionId', sessionId); debug(`stream ${this[kOwner].id} sessionid callback`, sessionId); this[kOwner][kSessionId](sessionId); }, @@ -2725,7 +2724,6 @@ class QuicStream { }); } if (typeof inner.onsessionid === 'function') { - console.log('kSessionId pass sess', sessionId); safeCallbackInvoke(inner.onsessionid, this, sessionId); } } diff --git a/src/quic/streams.cc b/src/quic/streams.cc index a1040454a419b6..185ffc9ccec2a7 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -1989,7 +1989,6 @@ void Stream::EmitSessionid(stream_id session_id) { if (!env()->can_call_into_js() || !state()->wants_sessionid) return; CallbackScope cb_scope(this); Local sid = BigInt::New(env()->isolate(), session_id); - printf("EmitSessionid mark 2\n"); MakeCallback(BindingData::Get(env()).stream_sessionid_callback(), 1, &sid); } From a3fe2a507b097c7c18e75c1cd493d2e9ca7c1c3d Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 11:39:17 +0200 Subject: [PATCH 12/19] quic: add session id callback to test --- test/parallel/test-quic-internal-setcallbacks.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index b485b5e9b43457..387b6b9f7e885b 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -34,6 +34,7 @@ const callbacks = { onStreamDrain() {}, onStreamReset() {}, onStreamHeaders() {}, + onStreamSessionId() {}, onStreamTrailers() {}, }; // Fail if any callback is missing From 1d5314295fb929649d245eea4dcce872e5aa2f7a Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 17:57:18 +0200 Subject: [PATCH 13/19] quic: WT implement close session callback --- lib/internal/quic/diagnostics.js | 4 +- lib/internal/quic/quic.js | 66 +++++++++++++++++++ lib/internal/quic/state.js | 19 ++++++ lib/internal/quic/symbols.js | 1 + src/quic/bindingdata.h | 1 + src/quic/http3.cc | 19 +++++- src/quic/streams.cc | 28 +++++++- src/quic/streams.h | 10 +++ .../test-quic-internal-setcallbacks.mjs | 1 + 9 files changed, 145 insertions(+), 4 deletions(-) diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index c537ed93ae394f..f21a61f44c92c6 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -33,7 +33,8 @@ const onSessionGoawayChannel = dc.channel('quic.session.goaway'); const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); const onStreamClosedChannel = dc.channel('quic.stream.closed'); const onStreamHeadersChannel = dc.channel('quic.stream.headers'); -const onStreamSessionIdChannel = dc.channel('quic.stream.sessioid'); +const onStreamSessionIdChannel = dc.channel('quic.stream.sessionid'); +const onStreamWTSessionCloseChannel = dc.channel('quic.stream.wtsessionclose') const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); const onStreamInfoChannel = dc.channel('quic.stream.info'); const onStreamResetChannel = dc.channel('quic.stream.reset'); @@ -70,6 +71,7 @@ module.exports = { onStreamClosedChannel, onStreamHeadersChannel, onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 729d3cfcb8f93f..9b9ff49c9f7c9f 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -195,6 +195,7 @@ const { kVerifyPeer, kHeaders, kSessionId, + kWTSessionClose, kOwner, kRemoveSession, kKeylog, @@ -270,6 +271,7 @@ const { onStreamClosedChannel, onStreamHeadersChannel, onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, @@ -310,6 +312,7 @@ const endpointRegistry = new SafeSet(); * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnSessionIdCallback} [onsessionid] Callback for incoming sessionid + * @property {OnWTSessionCloseCallback} [onwtsessionclose] Callback for incoming close capsules * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers * @property {OnWantTrailersCallback} [onwanttrailers] Callback fired when the @@ -473,6 +476,7 @@ const endpointRegistry = new SafeSet(); * @property {OnApplicationCallback} [onapplication] application options callback. * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. * @property {OnSessionIdCallback} [onsessionid] Default perstream initial callback for incoming sessionid. + * @property {OnWTSessionCloseCallback} [onwtsessionclose] Default perstream initial callback for incoming close capsules * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream @@ -729,6 +733,15 @@ const endpointRegistry = new SafeSet(); * @returns {void} */ +/** + * Called when session id (e.g. for Webtransport streams) is determined + * @callback OnWTSessionCloseCallback + * @this {QuicStream} + * @param {number} error code from the webtransport session + * @param {string|undefined} error message from the webtransport session + * @returns {void} + */ + /** @@ -1025,6 +1038,12 @@ setCallbacks({ this[kOwner][kSessionId](sessionId); }, + onStreamWTSessionClose(errorcode, errormessage) { + // Called when the stream C++ handle has received a close capsule + debug(`stream ${this[kOwner].id} wtsessionclose callback`, errorcode, errormessage); + this[kOwner][kWTSessionClose](errorcode, errormessage); + }, + onStreamTrailers() { // Called when the stream C++ handle is ready to receive trailing headers. debug('stream want trailers callback', this[kOwner]); @@ -1347,6 +1366,7 @@ function applyCallbacks(session, cbs) { }; } if (cbs.onsessionid) session.onsessionid = cbs.onsessionid; + if (cbs.onwtsessionclose) session.onwtsessionclose = cbs.onwtsessionclose; } /** @@ -1607,6 +1627,7 @@ class QuicStream { onreset: undefined, onheaders: undefined, onsessionid: undefined, + onwtsessionclose: undefined, ontrailers: undefined, oninfo: undefined, onwanttrailers: undefined, @@ -1850,6 +1871,26 @@ class QuicStream { } } + /** @type {OnWTSessionCloseCallback} */ + get onwtsessionclose() { + assertIsQuicStream(this); + return this.#inner.onwtsessionclose; + } + + set onwtsessionclose(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onwtsessionclose = undefined; + inner.state.wantsWTSessionClose = false; + } else { + console.log('Set wantsWTSessionClose'); + validateFunction(fn, 'onwtsessionclose'); + inner.onwtsessionclose = FunctionPrototypeBind(fn, this); + inner.state.wantsWTSessionClose = true; + } + } + /** @type {Function|undefined} */ get oninfo() { assertIsQuicStream(this); @@ -2614,6 +2655,7 @@ class QuicStream { inner.onreset = undefined; inner.onheaders = undefined; inner.onsessionid = undefined; + inner.onwtsessionclose = undefined; inner.onerror = undefined; inner.ontrailers = undefined; inner.oninfo = undefined; @@ -2728,6 +2770,23 @@ class QuicStream { } } + [kWTSessionClose](errorcode, errormessage) { + if (this.destroyed) return; + const inner = this.#inner; + if (onStreamWTSessionCloseChannel.hasSubscribers) { + onStreamWTSessionCloseChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + errorcode, + errormessage + }); + } + if (typeof inner.onwtsessionclose === 'function') { + safeCallbackInvoke(inner.onwtsessionclose, this, errorcode, errormessage); + } + } + [kTrailers]() { if (this.destroyed) return; const inner = this.#inner; @@ -3348,6 +3407,7 @@ class QuicSession { headers, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -3405,6 +3465,7 @@ class QuicSession { // Set stream callbacks before sending headers to avoid missing events. if (onheaders) stream.onheaders = onheaders; if (onsessionid) stream.onsessionid = onsessionid; + if (onwtsessionclose) stream.onwtsessionclose = onwtsessionclose; if (ontrailers) stream.ontrailers = ontrailers; if (oninfo) stream.oninfo = oninfo; if (onwanttrailers) stream.onwanttrailers = onwanttrailers; @@ -4189,6 +4250,7 @@ class QuicSession { if (scbs) { if (scbs.onheaders) stream.onheaders = scbs.onheaders; if (scbs.onsessionid) stream.onsessionid = scbs.onsessionid; + if (scbs.onwtsessionclose) stream.onwtsessionclose = scbs.onwtsessionclose; if (scbs.ontrailers) stream.ontrailers = scbs.ontrailers; if (scbs.oninfo) stream.oninfo = scbs.oninfo; if (scbs.onwanttrailers) stream.onwanttrailers = scbs.onwanttrailers; @@ -4567,6 +4629,7 @@ class QuicEndpoint { // Stream-level callbacks applied to each incoming stream. onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -4593,6 +4656,7 @@ class QuicEndpoint { onapplication, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5329,6 +5393,7 @@ function processSessionOptions(options, config = kEmptyObject) { // Stream-level callbacks. onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5451,6 +5516,7 @@ function processSessionOptions(options, config = kEmptyObject) { onapplication, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 509fd320ae5e72..827d04003fb43a 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -101,6 +101,7 @@ const { IDX_STATE_STREAM_WANTS_BLOCK, IDX_STATE_STREAM_WANTS_HEADERS, IDX_STATE_STREAM_WANTS_SESSIONID, + IDX_STATE_STREAM_WANTS_WTSESSIONCLOSE, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_TRAILERS, IDX_STATE_STREAM_RECEIVED_EARLY_DATA, @@ -828,6 +829,20 @@ class QuicStreamState { DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID, val ? 1 : 0); } + /** @type {boolean} */ + get wantsWTSessionClose() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_WTSESSIONCLOSE) !== 0; + } + + /** @type {boolean} */ + set wantsWTSessionClose(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_WTSESSIONCLOSE, val ? 1 : 0); + } + /** @type {boolean} */ get wantsReset() { @@ -922,6 +937,7 @@ class QuicStreamState { wantsReset, wantsHeaders, wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -943,6 +959,7 @@ class QuicStreamState { wantsReset, wantsHeaders, wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -981,6 +998,7 @@ class QuicStreamState { wantsReset, wantsHeaders, wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -1002,6 +1020,7 @@ class QuicStreamState { wantsReset, wantsHeaders, wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 4d98fbdc9c188b..8d7037f102ce09 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -41,6 +41,7 @@ const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); const kSessionId = Symbol('kSessionId'); +const kWTSessionClose = Symbol('kWTSessionClose'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 326a0fc73754f7..64497dac5aaf72 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -61,6 +61,7 @@ class SessionManager; V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ V(stream_sessionid, StreamSessionId) \ + V(stream_wtsessionclose, StreamWTSessionClose) \ V(stream_reset, StreamReset) \ V(stream_trailers, StreamTrailers) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 23b2e0c040be45..17d77c42ec1395 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1297,6 +1297,22 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } + static int on_recv_wt_close_session(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen, + void *conn_user_data, + void *stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto& session = app.session(); + if (auto stream = FindOrCreateStream(conn, &session, session_id)) [[likely]] { + stream->NotifyWTSessionClose(wt_error_code, msg, msglen); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + static int on_deferred_consume(nghttp3_conn* conn, stream_id id, size_t consumed, @@ -1496,7 +1512,8 @@ class Http3ApplicationImpl final : public Session::Application { on_rand, on_receive_settings, on_receive_wt_data, - on_wt_data_stream_open}; + on_wt_data_stream_open, + on_recv_wt_close_session}; }; std::optional ParseHttp3TicketData(const uv_buf_t& data) { diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 185ffc9ccec2a7..bdde2a3e2bc217 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -62,6 +62,8 @@ namespace quic { V(WANTS_HEADERS, wants_headers, uint8_t) \ /* Set when the stream has a sessionid event handler */ \ V(WANTS_SESSIONID, wants_sessionid, uint8_t) \ + /* Set when the stream has a event handler for closing a WT session */ \ + V(WANTS_WTSESSIONCLOSE, wants_wtsessionclose, uint8_t) \ /* Set when the stream has a reset event handler */ \ V(WANTS_RESET, wants_reset, uint8_t) \ /* Set when the stream has a trailers event handler */ \ @@ -1636,12 +1638,18 @@ bool Stream::AddHeader(std::unique_ptr
header) { return true; } - void Stream::NotifyWTSession(stream_id session_id) { +void Stream::NotifyWTSession(stream_id session_id) { if (state()->session_id != session_id) { state()->session_id = session_id; EmitSessionid(session_id); } - } +} + +void Stream::NotifyWTSessionClose(uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen) { + EmitWTSessionClose(wt_error_code, msg, msglen); +} void Stream::Acknowledge(size_t datalen) { if (outbound_ == nullptr) return; @@ -1992,6 +2000,22 @@ void Stream::EmitSessionid(stream_id session_id) { MakeCallback(BindingData::Get(env()).stream_sessionid_callback(), 1, &sid); } + +void Stream::EmitWTSessionClose(uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen) { + if (!env()->can_call_into_js() || !state()->wants_wtsessionclose) return; + CallbackScope cb_scope(this); + Local argv[] = { + Integer::NewFromUnsigned(env()->isolate(), + wt_error_code), + String::NewFromUtf8(env()->isolate(), reinterpret_cast(msg), + v8::NewStringType::kNormal, msglen).ToLocalChecked() + }; + MakeCallback(BindingData::Get(env()).stream_wtsessionclose_callback(), + arraysize(argv), argv); +} + void Stream::EmitReset(const QuicError& error) { // state()->wants_reset will be set from the javascript side if the // stream object has a handler for the reset event. diff --git a/src/quic/streams.h b/src/quic/streams.h index 5ce4ef82450fda..24866fdaa55b4c 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -359,6 +359,11 @@ class Stream final : public AsyncWrap, // Currently only http/3 can have a session stream in WebTransport void NotifyWTSession(stream_id session_id); + // Currently only http/3 can have a session stream that receives a close capsule + void NotifyWTSessionClose(uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen); + // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_, // reader_, headers_, and pending_headers_queue_. SET_NO_MEMORY_INFO() @@ -434,6 +439,11 @@ class Stream final : public AsyncWrap, // Delivers the session_id aka the stream that holds e.g. the WT session. void EmitSessionid(stream_id session_id); + // delivers the content of the close capsule + void EmitWTSessionClose(uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen); + void NotifyReadableEnded(error_code code); void NotifyWritableEnded(error_code code); diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index 387b6b9f7e885b..376a152ecdc456 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -35,6 +35,7 @@ const callbacks = { onStreamReset() {}, onStreamHeaders() {}, onStreamSessionId() {}, + onStreamWTSessionClose() {}, onStreamTrailers() {}, }; // Fail if any callback is missing From b60ce094a1dc339df06df409600487be9d4f7b2b Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 18:26:31 +0200 Subject: [PATCH 14/19] quic: Remove debug message --- lib/internal/quic/quic.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 9b9ff49c9f7c9f..c6254f842fa0d7 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -1884,7 +1884,6 @@ class QuicStream { inner.onwtsessionclose = undefined; inner.state.wantsWTSessionClose = false; } else { - console.log('Set wantsWTSessionClose'); validateFunction(fn, 'onwtsessionclose'); inner.onwtsessionclose = FunctionPrototypeBind(fn, this); inner.state.wantsWTSessionClose = true; From 5e9751eb6380d3d9cb7d408dd3563ffce951da54 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 21 Jun 2026 14:04:09 +0200 Subject: [PATCH 15/19] quic: Fix wake up blob --- lib/internal/blob.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index dfaa24b79db5ab..c8d3ca959df600 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -475,9 +475,15 @@ function createBlobReaderStream(reader) { this.pendingPulls = []; // Register a wakeup callback that the C++ side can invoke // when new data is available after a STATUS_BLOCK. + let immediate; reader.setWakeup(() => { - if (this.pendingPulls.length > 0) { - this.readNext(c); + if (this.pendingPulls.length > 0 && + typeof immediate === 'undefined') { + // Postpone the execution to the next steps of the event loop + immediate = setImmediate(() => { + immediate = undefined; + this.readNext(c); + }); } }); }, @@ -564,7 +570,16 @@ const kMaxBatchChunks = 16; async function* createBlobReaderIterable(reader, options = kEmptyObject) { const { getReadError } = options; let wakeup = PromiseWithResolvers(); - reader.setWakeup(wakeup.resolve); + let immediate; + reader.setWakeup(() => { + if (typeof immediate === 'undefined') { + // Postpone the execution to the next steps of the event loop + immediate = setImmediate(() => { + immediate = undefined; + wakeup.resolve?.(); + }); + } + }); try { while (true) { @@ -611,7 +626,6 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { if (blocked) { const fin = await wakeup.promise; wakeup = PromiseWithResolvers(); - reader.setWakeup(wakeup.resolve); // If the wakeup was triggered by FIN (EndReadable), the DataQueue // is capped. Continue the loop to pull again -- the next pull will // return EOS. Without this, a race between the data notification From ba65cd62027ae3499280fd691382517407dff25e Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 27 Jun 2026 10:45:59 +0200 Subject: [PATCH 16/19] quic: Implement webtransport close session stream --- lib/internal/quic/quic.js | 46 ++++++++++++++++++++++++++++++++++++ lib/internal/quic/symbols.js | 2 ++ src/quic/application.h | 11 +++++++++ src/quic/http3.cc | 21 ++++++++++++++++ src/quic/streams.cc | 37 ++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 1 deletion(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index c6254f842fa0d7..cdd02abb600170 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -212,6 +212,7 @@ const { kReset, kSendHeaders, kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kTrailers, @@ -2040,6 +2041,33 @@ class QuicStream { return this.#inner.pendingClose.promise; } + /** + * Only for webtransport session streams + * Closes the webtransport session stream, and also closes + * connected datastream internally. + * @param {number|undefined} code optional error code + * @param {string|undefined} msg optional error message truncated to 1024 bytes + */ + closeWebtransportSessionStream(code, msg) { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.destroying || this.destroyed) return; + + if (msg !== undefined) { + validateString(msg, 'msg'); + } + inner.destroying = true; + const handle = this.#handle; + const error = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'Webtransport error', + 'application', + code ?? 0, + msg ?? ''); + this[kFinishClose](error); + handle.destroy(); + } + /** * Immediately destroys the stream. Any queued data is discarded. If * an error is given, the closed promise will be rejected with that @@ -2603,6 +2631,7 @@ class QuicStream { /** * Attaches a webtransport session to the stream and sends initial bytes * indicating webtransport stream and the session id + * @param {QuicStream} session Webtransport session stream * @returns {boolean} true if it succeeded. */ [kMakeWebtransportStream](session) { @@ -2614,6 +2643,23 @@ class QuicStream { return this.#handle.makeWebtransportStream(session.#handle); } + /** + * Closes a webtransport session stream and also closes associated data streams + * Passing optional error code and error message (limited to 1024 bytes). + * @param {number} code error code + * @param {string} msg error message limited to 1024 bytes + * @returns {boolean} true if it succeeded. + */ + [kCloseWebtransportSessionStream](code, msg) { + if (this.pending) { + debug('pending stream enqueuing closeWebtransportSessionStream with code', code, ' and msg:', msg); + } else { + debug(`stream ${this.id} closeWebtransportSessionStream with code`, code, + ' and msg:', msg); + } + return this.#handle.closeWebtransportSessionStream(session.#handle); + } + [kFinishClose](error) { const inner = this.#inner; inner.pendingClose ??= PromiseWithResolvers(); diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 8d7037f102ce09..b612abd90b7110 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -58,6 +58,7 @@ const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); +const kCloseWebtransportSessionStream = Symbol('kCloseWebtransportSessionStream'); const kSessionApplication = Symbol('kSessionApplication'); const kSessionTicket = Symbol('kSessionTicket'); const kTrailers = Symbol('kTrailers'); @@ -96,6 +97,7 @@ module.exports = { kReset, kSendHeaders, kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kTrailers, diff --git a/src/quic/application.h b/src/quic/application.h index a68020a4138ece..8bc24341f57af4 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -216,6 +216,17 @@ class Session::Application : public MemoryRetainer { return false; } + // closes the webtransort session stream, + // and also closes connect webtransport data streams + virtual bool CloseWebtransportSessionStream( + const Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen + ) { + return false; + } + // Signals to the Application that it should serialize and transmit any // pending session and stream packets it has accumulated. void SendPendingData(); diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 17d77c42ec1395..eadb20155b806f 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -686,6 +686,27 @@ class Http3ApplicationImpl final : public Session::Application { == 0; } + // closes the webtransort session stream, + // and also closes connect webtransport data streams + // msg is optional + // msg length is maximum 1024 + bool CloseWebtransportSessionStream( + const Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen + ) override { + Session::SendPendingDataScope send_scope(&session()); + Debug(&session(), + "Close webtransport session stream %" PRIu64, + stream.id()); + return nghttp3_conn_close_wt_session(*this, + stream.id(), + wt_error_code, + msg, + msglen) == 0; + } + void SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) override { diff --git a/src/quic/streams.cc b/src/quic/streams.cc index bdde2a3e2bc217..7c4d8378467fa4 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -103,6 +103,7 @@ namespace quic { V(Destroy, destroy, false) \ V(SendHeaders, sendHeaders, false) \ V(MakeWebtransportStream, makeWebtransportStream, false) \ + V(CloseWebtransportSessionStream, closeWebtransportSessionStream, false) \ V(StopSending, stopSending, false) \ V(ResetStream, resetStream, false) \ V(SetPriority, setPriority, false) \ @@ -481,7 +482,7 @@ struct Stream::Impl { } // Connects a stream to a webtransport session stream, - // also sends the initial bytes of a stream to signel the wt stream + // also sends the initial bytes of a stream to signal the wt stream // also connects the readers JS_METHOD(MakeWebtransportStream) { Stream* stream; @@ -500,6 +501,40 @@ struct Stream::Impl { )); } + // Closes a webtransport session stream, + // also closes connected data streams + JS_METHOD(CloseWebtransportSessionStream) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); + CHECK(args.Length() > 0); + CHECK(args[0]->IsObject()); + uint32_t wt_error_code = 0; + if (args.Length() > 1) { + CHECK(args[1]->IsUint32()); + wt_error_code = FromV8Value(args[0]); + } + uint8_t * msg = nullptr; + size_t msglen = 0; + if (args.Length() > 2) { + CHECK(args[2]->IsString()); + Local msgstr = args[2].As(); + const size_t length = msgstr->Utf8LengthV2(args.GetIsolate()); + msg = new uint8_t[length]; + msgstr->WriteUtf8V2( + args.GetIsolate(), reinterpret_cast(msg), length, String::WriteFlags::kNone); + msglen = std::min(length, 1024); + } + args.GetReturnValue().Set(stream->session().application().CloseWebtransportSessionStream( + *stream, + wt_error_code, + msg, + msglen + )); + if (msg) { + delete[] msg; + } + } + // Tells the peer to stop sending data for this stream. This has the effect // of shutting down the readable side of the stream for this peer. Any data // that has already been received is still readable. From 9220fa17d01f189b56e714cb4722b7689e4137ec Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 4 Jul 2026 17:50:31 +0200 Subject: [PATCH 17/19] quic: correct http3 callback and fix revealed errs The http3 application had misinterpreted some of nghttp3 callbacks regarding stopSending and ResetStream. Actually, these callbacks asks the application to do the action and not informs about an event from the peer. The fixes lead to some failures of the automated tests, uncovering some problems: First headers, and pendingTrailers were reset, when the internal object went away, though the test wanted to read them. Second, during a graceful session shutdown, the implemented did not waited for all stream to be removed, but only one. Fixes: https://github.com/nodejs/node/issues/63657 Signed-off-by: Marten Richter --- lib/internal/quic/quic.js | 4 +-- src/quic/http3.cc | 26 +++++++++--------- src/quic/session.cc | 3 ++- src/quic/streams.cc | 56 ++++++++++++++++++++++----------------- src/quic/streams.h | 12 +++++++++ 5 files changed, 62 insertions(+), 39 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index cdd02abb600170..9abc7fd945811d 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2705,8 +2705,8 @@ class QuicStream { inner.ontrailers = undefined; inner.oninfo = undefined; inner.onwanttrailers = undefined; - inner.headers = undefined; - inner.pendingTrailers = undefined; + // do not reset headers here, this is still important information + // the same applies for pendingTrailers this.#handle = undefined; if (inner.fileHandle !== undefined) { // Close the FileHandle that was used as a body source. The close diff --git a/src/quic/http3.cc b/src/quic/http3.cc index eadb20155b806f..9107f81625f1b6 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -999,24 +999,24 @@ class Http3ApplicationImpl final : public Session::Application { stream->ReceiveData(nullptr, 0, flags); } - void OnStopSending(stream_id id, error_code app_error_code) { + void OnSendStopSending(stream_id id, error_code app_error_code) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), - "HTTP/3 application received stop sending for stream %" PRIi64, + "HTTP/3 application should send stop sending for stream %" PRIi64, id); - stream->ReceiveStopSending(QuicError::ForApplication(app_error_code)); + stream->SendStopSending(app_error_code); } - void OnResetStream(stream_id id, error_code app_error_code) { + void OnDoResetStream(stream_id id, error_code app_error_code) { auto stream = session().FindStream(id); if (!stream) [[unlikely]] return; Debug(&session(), - "HTTP/3 application received reset stream for stream %" PRIi64, + "HTTP/3 application received a request to reset stream for stream %" PRIi64, id); - stream->ReceiveStreamReset(0, QuicError::ForApplication(app_error_code)); + stream->DoStreamReset(app_error_code); } void OnShutdown(stream_id id) { @@ -1447,29 +1447,31 @@ class Http3ApplicationImpl final : public Session::Application { return NGTCP2_SUCCESS; } - static int on_stop_sending(nghttp3_conn* conn, + static int on_send_stop_sending(nghttp3_conn* conn, stream_id id, error_code app_error_code, void* conn_user_data, void* stream_user_data) { + // this callback asks the app side to send a stop sending NGHTTP3_CALLBACK_SCOPE(app); if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnStopSending(id, app_error_code); + app.OnSendStopSending(id, app_error_code); return NGTCP2_SUCCESS; } - static int on_reset_stream(nghttp3_conn* conn, + static int on_do_reset_stream(nghttp3_conn* conn, stream_id id, error_code app_error_code, void* conn_user_data, void* stream_user_data) { + // this callback ask the app side to do a reset stream NGHTTP3_CALLBACK_SCOPE(app); if (app.is_control_stream(id)) [[unlikely]] { return NGHTTP3_ERR_CALLBACK_FAILURE; } - app.OnResetStream(id, app_error_code); + app.OnDoResetStream(id, app_error_code); return NGTCP2_SUCCESS; } @@ -1523,9 +1525,9 @@ class Http3ApplicationImpl final : public Session::Application { on_begin_trailers, on_receive_trailer, on_end_trailers, - on_stop_sending, + on_send_stop_sending, on_end_stream, - on_reset_stream, + on_do_reset_stream, on_shutdown, nullptr, // recv_settings (deprecated) on_receive_origin, diff --git a/src/quic/session.cc b/src/quic/session.cc index 8380e477c01e80..e3fce1563e826d 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -2797,7 +2797,8 @@ void Session::RemoveStream(stream_id id) { // then we can proceed to finishing the close now. Note that the // expectation is that the session will be destroyed once FinishClose // returns. - if (impl_->state()->closing && impl_->state()->graceful_close) { + if (impl_->state()->closing && impl_->state()->graceful_close + && impl_->streams_.size() == 0) { FinishClose(); CHECK(is_destroyed()); } diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 7c4d8378467fa4..f6800baa524b6e 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -548,15 +548,7 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&unused); } - stream->EndReadable(); - - if (!stream->is_pending()) { - // If the stream is a local unidirectional there's nothing to do here. - if (stream->is_local_unidirectional()) return; - stream->NotifyReadableEnded(code); - } else { - stream->pending_close_read_code_ = code; - } + stream->SendStopSending(code); } // Sends a reset stream to the peer to tell it we will not be sending any @@ -573,21 +565,7 @@ struct Stream::Impl { code = args[0].As()->Uint64Value(&lossless); } - if (stream->state()->reset == 1) return; - - stream->EndWritable(); - // We can release our outbound here now. Since the stream is being reset - // on the ngtcp2 side, we do not need to keep any of the data around - // waiting for acknowledgement that will never come. - stream->outbound_.reset(); - stream->state()->reset = 1; - - if (!stream->is_pending()) { - if (stream->is_remote_unidirectional()) return; - stream->NotifyWritableEnded(code); - } else { - stream->pending_close_write_code_ = code; - } + stream->DoStreamReset(code); } JS_METHOD(SetPriority) { @@ -1917,6 +1895,36 @@ void Stream::ReceiveStreamReset(uint64_t final_size, QuicError error) { EmitReset(error); } +void Stream::DoStreamReset(error_code code) { + if (state()->reset == 1) return; + + EndWritable(); + // We can release our outbound here now. Since the stream is being reset + // on the ngtcp2 side, we do not need to keep any of the data around + // waiting for acknowledgement that will never come. + outbound_.reset(); + state()->reset = 1; + + if (!is_pending()) { + if (is_remote_unidirectional()) return; + NotifyWritableEnded(code); + } else { + pending_close_write_code_ = code; + } +} + +void Stream::SendStopSending(error_code code) { + EndReadable(); + + if (!is_pending()) { + // If the stream is a local unidirectional there's nothing to do here. + if (is_local_unidirectional()) return; + NotifyReadableEnded(code); + } else { + pending_close_read_code_ = code; + } +} + // ============================================================================ void Stream::EmitBlocked() { diff --git a/src/quic/streams.h b/src/quic/streams.h index 24866fdaa55b4c..e2dcf47bdb74cd 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -346,6 +346,18 @@ class Stream final : public AsyncWrap, void ReceiveStopSending(QuicError error); void ReceiveStreamReset(uint64_t final_size, QuicError error); + // Sends a reset stream to the peer to tell it we will not be sending any + // more data for this stream. This has the effect of shutting down the + // writable side of the stream for this peer. Any data that is held in the + // outbound queue will be dropped. The stream may still be readable. + void DoStreamReset(error_code code); + + // Tells the peer to stop sending data for this stream. This has the effect + // of shutting down the readable side of the stream for this peer. Any data + // that has already been received is still readable. + void SendStopSending(error_code code); + + // Currently, only HTTP/3 streams support headers. These methods are here // to support that. They are not used when using any other QUIC application. From 0efa2ed109a3f508eeced73c297b359ccc097606 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 09:24:14 +0200 Subject: [PATCH 18/19] quic: fix stall datagrams, if no pending streams If you are only sending datagrams and so streams, your datagrams could stall. There were two reasons: i. A SendPendingDataScope in SendDatagrams was missing. ii. SendPendingData did not attempt to send datagrams, if there were no stream data. --- src/quic/application.cc | 2 +- src/quic/session.cc | 1 + test/parallel/test-quic-session-preferred-address-ipv6.mjs | 3 +++ test/parallel/test-quic-session-preferred-address.mjs | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/quic/application.cc b/src/quic/application.cc index 4952b3b71b04a5..7322c96fa53d7d 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -546,7 +546,7 @@ void Session::Application::SendPendingData() { // We call Application::ResumeStream directly (not Session::ResumeStream) // to avoid creating a SendPendingDataScope — we're already inside // SendPendingData and re-entering would just hit nwrite=0 again. - if (nwrite == 0) { + if (nwrite == 0 && (stream_data.id >= 0 || !session_->HasPendingDatagrams())) { Debug(session_, "Congestion or not our turn to send"); if (stream_data.id >= 0 && (stream_data.count > 0 || stream_data.fin)) { ResumeStream(stream_data.id); diff --git a/src/quic/session.cc b/src/quic/session.cc index e3fce1563e826d..2758cbd00927bd 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -2593,6 +2593,7 @@ datagram_id Session::SendDatagram(Store&& data) { return did; } } + Session::SendPendingDataScope send_scope(this); // Queue the datagram. It will be serialized into packets by // SendPendingData alongside stream data. diff --git a/test/parallel/test-quic-session-preferred-address-ipv6.mjs b/test/parallel/test-quic-session-preferred-address-ipv6.mjs index e9f23c3bf554d5..35e539848b4aba 100644 --- a/test/parallel/test-quic-session-preferred-address-ipv6.mjs +++ b/test/parallel/test-quic-session-preferred-address-ipv6.mjs @@ -100,6 +100,9 @@ const clientSession = await connect(serverEndpoint.address, { family: 'ipv6', }, }, + maxDatagramSendAttempts: 100, // While the connection is restablished, + // all the acknowledgement packets of ngtcp2 are counted as send attempts + // so either this or a delay, or a change in ngtcp2 interfaces }); await clientSession.opened; diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs index c4a55ee4d42b74..6a526a8c9ca63f 100644 --- a/test/parallel/test-quic-session-preferred-address.mjs +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -78,6 +78,9 @@ const clientSession = await connect(serverEndpoint.address, { strictEqual(oldRemote, null); strictEqual(preferred, true); }), + maxDatagramSendAttempts: 100, // While the connection is restablished, + // all the acknowledgement packets of ngtcp2 are counted as send attempts + // so either this or a delay, or a change in ngtcp2 interfaces }); await clientSession.opened; From 0a8e55f7d3f41060249c5d842b4c4ccf97fa67d3 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 09:34:03 +0200 Subject: [PATCH 19/19] quic: fixes in close webtransport session --- lib/internal/quic/quic.js | 4 ++++ src/quic/streams.cc | 12 +++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 9abc7fd945811d..6a6a0a06f4663d 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2056,8 +2056,12 @@ class QuicStream { if (msg !== undefined) { validateString(msg, 'msg'); } + if (code !== undefined) { + validateInteger(code); + } inner.destroying = true; const handle = this.#handle; + this[kCloseWebtransportSessionStream](code, msg); const error = makeQuicError( 'ERR_QUIC_APPLICATION_ERROR', 'Webtransport error', diff --git a/src/quic/streams.cc b/src/quic/streams.cc index f6800baa524b6e..293dbcf94fce48 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -507,17 +507,15 @@ struct Stream::Impl { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); CHECK(args.Length() > 0); - CHECK(args[0]->IsObject()); uint32_t wt_error_code = 0; - if (args.Length() > 1) { - CHECK(args[1]->IsUint32()); + if (args.Length() > 0) { + CHECK(args[0]->IsUint32()); wt_error_code = FromV8Value(args[0]); - } uint8_t * msg = nullptr; size_t msglen = 0; - if (args.Length() > 2) { - CHECK(args[2]->IsString()); - Local msgstr = args[2].As(); + if (args.Length() > 1) { + CHECK(args[1]->IsString()); + Local msgstr = args[1].As(); const size_t length = msgstr->Utf8LengthV2(args.GetIsolate()); msg = new uint8_t[length]; msgstr->WriteUtf8V2(