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) */ 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', 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 diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index 7180f719bc09d3..f21a61f44c92c6 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -33,6 +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.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'); @@ -68,6 +70,8 @@ module.exports = { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 99eeccc3c51d23..cdd02abb600170 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. @@ -193,6 +194,8 @@ const { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, + kWTSessionClose, kOwner, kRemoveSession, kKeylog, @@ -208,6 +211,8 @@ const { kPrivateConstructor, kReset, kSendHeaders, + kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kTrailers, @@ -266,6 +271,8 @@ const { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, @@ -291,7 +298,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,7 +308,12 @@ 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 + * @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 @@ -369,6 +383,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 */ /** @@ -461,6 +476,8 @@ 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 {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 @@ -508,6 +525,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. */ /** @@ -704,6 +725,26 @@ 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 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} + */ + + + /** * Called when trailing headers are received from the peer. * @callback OnTrailersCallback @@ -992,6 +1033,18 @@ setCallbacks({ this[kOwner][kHeaders](headers, kind); }, + onStreamSessionId(sessionId) { + // Called when the stream C++ handle has determined the sessionId + debug(`stream ${this[kOwner].id} sessionid callback`, sessionId); + 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]); @@ -1313,6 +1366,8 @@ function applyCallbacks(session, cbs) { onwanttrailers: cbs.onwanttrailers, }; } + if (cbs.onsessionid) session.onsessionid = cbs.onsessionid; + if (cbs.onwtsessionclose) session.onwtsessionclose = cbs.onwtsessionclose; } /** @@ -1572,6 +1627,8 @@ class QuicStream { onblocked: undefined, onreset: undefined, onheaders: undefined, + onsessionid: undefined, + onwtsessionclose: undefined, ontrailers: undefined, oninfo: undefined, onwanttrailers: undefined, @@ -1796,6 +1853,44 @@ 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 {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 { + validateFunction(fn, 'onwtsessionclose'); + inner.onwtsessionclose = FunctionPrototypeBind(fn, this); + inner.state.wantsWTSessionClose = true; + } + } + /** @type {Function|undefined} */ get oninfo() { assertIsQuicStream(this); @@ -1946,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 @@ -2060,10 +2182,16 @@ 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.', + { terminal, webtransport }); + } 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); } @@ -2500,6 +2628,38 @@ 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 + * @param {QuicStream} session Webtransport session stream + * @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); + } + + /** + * 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(); @@ -2539,6 +2699,8 @@ class QuicStream { inner.onblocked = undefined; inner.onreset = undefined; inner.onheaders = undefined; + inner.onsessionid = undefined; + inner.onwtsessionclose = undefined; inner.onerror = undefined; inner.ontrailers = undefined; inner.oninfo = undefined; @@ -2637,6 +2799,39 @@ 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') { + safeCallbackInvoke(inner.onsessionid, this, sessionId); + } + } + + [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; @@ -3251,9 +3446,13 @@ class QuicSession { body, priority = 'default', incremental = false, + webtransport = false, + webtransportSession = undefined, highWaterMark = kDefaultHighWaterMark, headers, onheaders, + onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -3261,9 +3460,27 @@ 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'); + } + + 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(); @@ -3292,12 +3509,24 @@ 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; 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 (webtransportSession) { + if (!stream[kMakeWebtransportStream](webtransportSession)) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } } if (onSessionOpenStreamChannel.hasSubscribers) { @@ -4065,6 +4294,8 @@ class QuicSession { const scbs = this[kStreamCallbacks]; 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; @@ -4442,6 +4673,8 @@ class QuicEndpoint { onapplication, // Stream-level callbacks applied to each incoming stream. onheaders, + onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -4467,6 +4700,8 @@ class QuicEndpoint { onqlog, onapplication, onheaders, + onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5202,6 +5437,8 @@ function processSessionOptions(options, config = kEmptyObject) { // Application level options changed, e.g. HTTP/3 settings related // Stream-level callbacks. onheaders, + onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5323,6 +5560,8 @@ function processSessionOptions(options, config = kEmptyObject) { onqlog, onapplication, onheaders, + onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 8815b0bb4c32cf..827d04003fb43a 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -100,6 +100,8 @@ 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_WTSESSIONCLOSE, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_TRAILERS, IDX_STATE_STREAM_RECEIVED_EARLY_DATA, @@ -141,6 +143,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 +815,35 @@ 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 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() { const handle = this.#handle; @@ -904,6 +936,8 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -924,6 +958,8 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -961,6 +997,8 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, @@ -981,6 +1019,8 @@ class QuicStreamState { wantsBlock, wantsReset, wantsHeaders, + wantsSessionId, + wantsWTSessionClose, wantsTrailers, early, resetCode, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 665ab7ca1911c2..b612abd90b7110 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -40,6 +40,8 @@ const kHandshake = Symbol('kHandshake'); 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'); @@ -55,6 +57,8 @@ const kRemoveSession = Symbol('kRemoveSession'); 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'); @@ -74,6 +78,7 @@ module.exports = { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, kInspect, kKeylog, kKeyObjectHandle, @@ -91,6 +96,8 @@ module.exports = { kRemoveStream, kReset, kSendHeaders, + kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kTrailers, 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..8bc24341f57af4 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; @@ -205,6 +206,27 @@ 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 + // 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; + } + + // 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/bindingdata.h b/src/quic/bindingdata.h index 7879220e02b482..64497dac5aaf72 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -60,6 +60,8 @@ class SessionManager; V(stream_created, StreamCreated) \ V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ + V(stream_sessionid, StreamSessionId) \ + V(stream_wtsessionclose, StreamWTSessionClose) \ V(stream_reset, StreamReset) \ V(stream_trailers, StreamTrailers) @@ -89,6 +91,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/defs.h b/src/quic/defs.h index 75ae915335be93..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, @@ -309,6 +314,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 bc479f96990577..eadb20155b806f 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; @@ -406,7 +406,9 @@ class Http3ApplicationImpl final : public Session::Application { 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[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 +461,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 +513,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, @@ -585,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; } @@ -629,6 +666,47 @@ 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 + + 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(), + reader_ptr, + const_cast(&stream)) + == 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 { @@ -999,6 +1077,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; @@ -1205,6 +1284,56 @@ 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(); + 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]] { + 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; + } + 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, @@ -1402,7 +1531,10 @@ 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, + on_recv_wt_close_session}; }; std::optional ParseHttp3TicketData(const uv_buf_t& data) { diff --git a/src/quic/session.h b/src/quic/session.h index 0caeb764ba56c8..b258e09bae76fa 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -83,6 +83,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool enable_connect_protocol = true; bool enable_datagrams = true; + // 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..7c4d8378467fa4 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,10 @@ 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 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 */ \ @@ -97,6 +102,8 @@ namespace quic { V(AttachSource, attachSource, false) \ 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) \ @@ -474,6 +481,60 @@ struct Stream::Impl { *stream, kind, headers, flags)); } + // Connects a stream to a webtransport session stream, + // also sends the initial bytes of a stream to signal 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() + )); + } + + // 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. @@ -892,7 +953,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; } @@ -1089,6 +1149,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 +1158,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) { @@ -1134,6 +1197,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. @@ -1191,6 +1255,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 @@ -1251,6 +1316,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. @@ -1284,6 +1350,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()) { @@ -1321,6 +1392,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; } @@ -1329,6 +1405,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; @@ -1574,6 +1654,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) { @@ -1592,6 +1673,19 @@ 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::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; @@ -1934,6 +2028,29 @@ 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); + 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. @@ -1962,7 +2079,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 86cb36b2668985..24866fdaa55b4c 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,14 @@ 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); + + // 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() @@ -423,15 +436,27 @@ 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); + + // 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); // 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 session_id); ArenaSlotBase stats_slot_; ArenaSlotBase state_slot_; @@ -447,6 +472,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; 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-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index b485b5e9b43457..376a152ecdc456 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -34,6 +34,8 @@ const callbacks = { onStreamDrain() {}, onStreamReset() {}, onStreamHeaders() {}, + onStreamSessionId() {}, + onStreamWTSessionClose() {}, onStreamTrailers() {}, }; // Fail if any callback is missing 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;