diff --git a/lib/includes/nghttp3/nghttp3.h b/lib/includes/nghttp3/nghttp3.h index b9748199..13795be0 100644 --- a/lib/includes/nghttp3/nghttp3.h +++ b/lib/includes/nghttp3/nghttp3.h @@ -2240,10 +2240,34 @@ typedef int (*nghttp3_recv_settings2)(nghttp3_conn *conn, const nghttp3_proto_settings *settings, void *conn_user_data); +/** + * @functypedef + * + * :type:`nghttp3_recv_datagram` is a callback function which is + * invoked when an HTTP/3 Datagram (see :rfc:`9297`) associated with a + * request stream identified by |stream_id| is received. |data| + * points to the HTTP Datagram payload (that is, the bytes that follow + * the Quarter Stream ID field), and its length is |datalen|. The + * payload may be zero length. This callback is invoked from + * `nghttp3_conn_read_datagram`. + * + * 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`. + * + * .. version-added:: 1.17.0 + */ +typedef int (*nghttp3_recv_datagram)(nghttp3_conn *conn, int64_t stream_id, + const uint8_t *data, size_t datalen, + void *conn_user_data, + void *stream_user_data); + #define NGHTTP3_CALLBACKS_V1 1 #define NGHTTP3_CALLBACKS_V2 2 #define NGHTTP3_CALLBACKS_V3 3 -#define NGHTTP3_CALLBACKS_VERSION NGHTTP3_CALLBACKS_V3 +#define NGHTTP3_CALLBACKS_V4 4 +#define NGHTTP3_CALLBACKS_VERSION NGHTTP3_CALLBACKS_V4 /** * @struct @@ -2375,6 +2399,15 @@ typedef struct nghttp3_callbacks { * .. version-added:: 1.14.0 */ nghttp3_recv_settings2 recv_settings2; + /* The following fields have been added since + NGHTTP3_CALLBACKS_V3. */ + /** + * :member:`recv_datagram` is a callback function which is invoked + * when an HTTP/3 Datagram (see :rfc:`9297`) is received. + * + * .. version-added:: 1.17.0 + */ + nghttp3_recv_datagram recv_datagram; } nghttp3_callbacks; /** @@ -2562,6 +2595,68 @@ NGHTTP3_EXTERN nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, size_t srclen, int fin, nghttp3_tstamp ts); +/** + * @function + * + * `nghttp3_conn_read_datagram` processes a single HTTP/3 Datagram (see + * :rfc:`9297`). |data| of length |datalen| is the payload of a QUIC + * DATAGRAM frame as received from the underlying QUIC stack; that is, + * the Quarter Stream ID field followed by the HTTP Datagram payload. + * + * The function decodes the Quarter Stream ID, maps it to the + * associated request stream, and, if the datagram is valid, invokes + * :type:`nghttp3_recv_datagram` with the request stream ID and the + * remaining payload. + * + * Because HTTP/3 Datagrams are unreliable, a datagram that cannot be + * delivered is silently dropped rather than treated as a connection + * error. This includes the cases where |datalen| is 0, the Quarter + * Stream ID field is truncated or maps to an invalid stream ID, the + * referenced request stream is not open, HTTP/3 Datagrams have not + * been enabled by both endpoints, or no :type:`nghttp3_recv_datagram` + * callback has been installed. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` + * User callback failed. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN int nghttp3_conn_read_datagram(nghttp3_conn *conn, + const uint8_t *data, + size_t datalen); + +/** + * @function + * + * `nghttp3_conn_write_datagram_prefix` writes the Quarter Stream ID + * field that prefixes an HTTP/3 Datagram (see :rfc:`9297`) for the + * request stream identified by |stream_id| into the buffer |dest| of + * length |destlen|. This is a framing helper: an application sends an + * HTTP/3 Datagram by passing the written prefix together with its own + * payload to the QUIC stack as the payload of a single QUIC DATAGRAM + * frame (e.g. as two iovecs to avoid copying the payload). + * + * |dest| should be able to hold at least 8 bytes, which is the maximum + * length of the Quarter Stream ID field. + * + * This function returns the number of bytes written to |dest|, or one + * of the following negative error codes: + * + * :macro:`NGHTTP3_ERR_INVALID_ARGUMENT` + * |stream_id| does not identify a client-initiated bidirectional + * (request) stream, or |destlen| is too small. + * :macro:`NGHTTP3_ERR_INVALID_STATE` + * HTTP/3 Datagrams have not been enabled by both endpoints; sending + * an HTTP/3 Datagram is not allowed. + * + * .. version-added:: 1.17.0 + */ +NGHTTP3_EXTERN nghttp3_ssize nghttp3_conn_write_datagram_prefix( + nghttp3_conn *conn, int64_t stream_id, uint8_t *dest, size_t destlen); + /** * @function * diff --git a/lib/nghttp3_callbacks.c b/lib/nghttp3_callbacks.c index 283f479b..441c707a 100644 --- a/lib/nghttp3_callbacks.c +++ b/lib/nghttp3_callbacks.c @@ -66,6 +66,9 @@ size_t nghttp3_callbackslen_version(int callbacks_version) { switch (callbacks_version) { case NGHTTP3_CALLBACKS_VERSION: return sizeof(callbacks); + case NGHTTP3_CALLBACKS_V3: + return offsetof(nghttp3_callbacks, recv_settings2) + + sizeof(callbacks.recv_settings2); case NGHTTP3_CALLBACKS_V2: return offsetof(nghttp3_callbacks, rand) + sizeof(callbacks.rand); case NGHTTP3_CALLBACKS_V1: diff --git a/lib/nghttp3_conn.c b/lib/nghttp3_conn.c index d1b6355b..3e05c6c4 100644 --- a/lib/nghttp3_conn.c +++ b/lib/nghttp3_conn.c @@ -566,6 +566,89 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, ts); } +/* conn_h3_datagram_negotiated returns nonzero if HTTP/3 Datagrams have + been enabled by both endpoints. RFC 9297, Section 2.1.1 requires the + H3_DATAGRAM setting to have been both sent and received with a value + of 1 before HTTP/3 Datagrams may be sent or received. */ +static int conn_h3_datagram_negotiated(const nghttp3_conn *conn) { + return conn->local.settings.h3_datagram && conn->remote.settings.h3_datagram; +} + +int nghttp3_conn_read_datagram(nghttp3_conn *conn, const uint8_t *data, + size_t datalen) { + nghttp3_stream *stream; + uint64_t qstream_id; + int64_t stream_id; + size_t prefixlen; + int rv; + + /* HTTP/3 Datagrams are unreliable, so anything that cannot be + delivered is dropped rather than treated as a connection error. */ + if (!conn_h3_datagram_negotiated(conn) || !conn->callbacks.recv_datagram || + datalen == 0) { + return 0; + } + + prefixlen = nghttp3_get_uvarintlen(data); + if (prefixlen > datalen) { + return 0; + } + + nghttp3_get_uvarint(&qstream_id, data); + + /* Quarter Stream ID is the request stream ID divided by four; reject + values that would map to a stream ID greater than the maximum. */ + if (qstream_id > NGHTTP3_MAX_VARINT >> 2) { + return 0; + } + + stream_id = (int64_t)(qstream_id << 2); + + stream = nghttp3_conn_find_stream(conn, stream_id); + if (stream == NULL) { + return 0; + } + + rv = conn->callbacks.recv_datagram(conn, stream_id, data + prefixlen, + datalen - prefixlen, conn->user_data, + stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +nghttp3_ssize nghttp3_conn_write_datagram_prefix(nghttp3_conn *conn, + int64_t stream_id, + uint8_t *dest, + size_t destlen) { + uint64_t qstream_id; + size_t prefixlen; + + assert(stream_id >= 0); + assert(stream_id <= (int64_t)NGHTTP3_MAX_VARINT); + + if (!nghttp3_client_stream_bidi(stream_id)) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + if (!conn_h3_datagram_negotiated(conn)) { + return NGHTTP3_ERR_INVALID_STATE; + } + + qstream_id = (uint64_t)stream_id >> 2; + + prefixlen = nghttp3_put_uvarintlen(qstream_id); + if (prefixlen > destlen) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + nghttp3_put_uvarint(dest, qstream_id); + + return (nghttp3_ssize)prefixlen; +} + static nghttp3_ssize conn_read_type(nghttp3_conn *conn, nghttp3_stream *stream, const uint8_t *src, size_t srclen, int fin) { diff --git a/tests/nghttp3_conn_test.c b/tests/nghttp3_conn_test.c index cc80bc87..1db780d0 100644 --- a/tests/nghttp3_conn_test.c +++ b/tests/nghttp3_conn_test.c @@ -75,6 +75,8 @@ static const MunitTest tests[] = { munit_void_test(test_nghttp3_conn_recv_unknown_frame), munit_void_test(test_nghttp3_conn_get_stream_user_data), munit_void_test(test_nghttp3_conn_is_stream_flushed), + munit_void_test(test_nghttp3_conn_recv_datagram), + munit_void_test(test_nghttp3_conn_write_datagram_prefix), munit_test_end(), }; @@ -140,6 +142,13 @@ typedef struct { size_t origin_listlen; size_t offset; } recv_origin_cb; + struct { + size_t ncalled; + int64_t stream_id; + void *stream_user_data; + uint8_t data[256]; + size_t datalen; + } recv_datagram_cb; } userdata; typedef struct { @@ -463,6 +472,24 @@ static void rand_cb(uint8_t *data, size_t datalen) { memset(data, 0xFE, datalen); } +static int recv_datagram(nghttp3_conn *conn, int64_t stream_id, + const uint8_t *data, size_t datalen, + void *conn_user_data, void *stream_user_data) { + userdata *ud = conn_user_data; + (void)conn; + + ++ud->recv_datagram_cb.ncalled; + ud->recv_datagram_cb.stream_id = stream_id; + ud->recv_datagram_cb.stream_user_data = stream_user_data; + + assert(datalen <= sizeof(ud->recv_datagram_cb.data)); + + memcpy(ud->recv_datagram_cb.data, data, datalen); + ud->recv_datagram_cb.datalen = datalen; + + return 0; +} + typedef struct conn_options { const nghttp3_callbacks *callbacks; const nghttp3_settings *settings; @@ -6923,3 +6950,259 @@ void test_nghttp3_conn_is_stream_flushed(void) { nghttp3_conn_del(conn); } + +void test_nghttp3_conn_recv_datagram(void) { + nghttp3_conn *conn; + userdata ud; + nghttp3_callbacks callbacks = { + .recv_datagram = recv_datagram, + }; + nghttp3_settings settings; + conn_options opts; + uint8_t buf[256]; + uint8_t *p; + int stream_user_data; + int rv; + + nghttp3_settings_default(&settings); + settings.h3_datagram = 1; + + opts = (conn_options){ + .callbacks = &callbacks, + .settings = &settings, + .user_data = &ud, + }; + + /* HTTP/3 Datagram is delivered to the associated request stream */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + conn->remote.settings.h3_datagram = 1; + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, &stream_user_data); + + assert_int(0, ==, rv); + + p = buf; + /* Quarter Stream ID of stream 0 is 0 */ + p = nghttp3_put_uvarint(p, 0); + memcpy(p, "hello", 5); + p += 5; + + rv = nghttp3_conn_read_datagram(conn, buf, (size_t)(p - buf)); + + assert_int(0, ==, rv); + assert_size(1, ==, ud.recv_datagram_cb.ncalled); + assert_int64(0, ==, ud.recv_datagram_cb.stream_id); + assert_ptr_equal(&stream_user_data, ud.recv_datagram_cb.stream_user_data); + assert_memn_equal((const uint8_t *)"hello", 5, ud.recv_datagram_cb.data, + ud.recv_datagram_cb.datalen); + + nghttp3_conn_del(conn); + + /* Zero-length HTTP Datagram payload is delivered */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + conn->remote.settings.h3_datagram = 1; + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, NULL); + + assert_int(0, ==, rv); + + p = buf; + p = nghttp3_put_uvarint(p, 0); + + rv = nghttp3_conn_read_datagram(conn, buf, (size_t)(p - buf)); + + assert_int(0, ==, rv); + assert_size(1, ==, ud.recv_datagram_cb.ncalled); + assert_size(0, ==, ud.recv_datagram_cb.datalen); + + nghttp3_conn_del(conn); + + /* HTTP/3 Datagram for an unopened stream is silently dropped */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + conn->remote.settings.h3_datagram = 1; + + p = buf; + p = nghttp3_put_uvarint(p, 0); + memcpy(p, "hello", 5); + p += 5; + + rv = nghttp3_conn_read_datagram(conn, buf, (size_t)(p - buf)); + + assert_int(0, ==, rv); + assert_size(0, ==, ud.recv_datagram_cb.ncalled); + + nghttp3_conn_del(conn); + + /* Empty QUIC DATAGRAM payload is silently dropped */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + conn->remote.settings.h3_datagram = 1; + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, NULL); + + assert_int(0, ==, rv); + + rv = nghttp3_conn_read_datagram(conn, buf, 0); + + assert_int(0, ==, rv); + assert_size(0, ==, ud.recv_datagram_cb.ncalled); + + nghttp3_conn_del(conn); + + /* Truncated Quarter Stream ID is silently dropped */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + conn->remote.settings.h3_datagram = 1; + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, NULL); + + assert_int(0, ==, rv); + + /* 0x40 encodes a 2-byte varint, but only 1 byte is provided */ + buf[0] = 0x40; + + rv = nghttp3_conn_read_datagram(conn, buf, 1); + + assert_int(0, ==, rv); + assert_size(0, ==, ud.recv_datagram_cb.ncalled); + + nghttp3_conn_del(conn); + + /* HTTP/3 Datagram is dropped when not enabled in local settings */ + memset(&ud, 0, sizeof(ud)); + + { + nghttp3_settings settings_off; + conn_options opts_off; + + nghttp3_settings_default(&settings_off); + + opts_off = (conn_options){ + .callbacks = &callbacks, + .settings = &settings_off, + .user_data = &ud, + }; + + setup_default_client_with_options(&conn, opts_off); + } + + conn->remote.settings.h3_datagram = 1; + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, NULL); + + assert_int(0, ==, rv); + + p = buf; + p = nghttp3_put_uvarint(p, 0); + memcpy(p, "hello", 5); + p += 5; + + rv = nghttp3_conn_read_datagram(conn, buf, (size_t)(p - buf)); + + assert_int(0, ==, rv); + assert_size(0, ==, ud.recv_datagram_cb.ncalled); + + nghttp3_conn_del(conn); + + /* HTTP/3 Datagram is dropped when not enabled remotely */ + memset(&ud, 0, sizeof(ud)); + setup_default_client_with_options(&conn, opts); + + rv = nghttp3_conn_submit_request(conn, 0, req_nva, nghttp3_arraylen(req_nva), + NULL, NULL); + + assert_int(0, ==, rv); + + p = buf; + p = nghttp3_put_uvarint(p, 0); + memcpy(p, "hello", 5); + p += 5; + + rv = nghttp3_conn_read_datagram(conn, buf, (size_t)(p - buf)); + + assert_int(0, ==, rv); + assert_size(0, ==, ud.recv_datagram_cb.ncalled); + + nghttp3_conn_del(conn); +} + +void test_nghttp3_conn_write_datagram_prefix(void) { + nghttp3_conn *conn; + nghttp3_settings settings; + conn_options opts; + uint8_t buf[8]; + nghttp3_ssize n; + int64_t qstream_id; + + nghttp3_settings_default(&settings); + settings.h3_datagram = 1; + + opts = (conn_options){ + .settings = &settings, + }; + + setup_default_client_with_options(&conn, opts); + + /* Sending is not allowed until the remote endpoint enables it */ + n = nghttp3_conn_write_datagram_prefix(conn, 0, buf, sizeof(buf)); + + assert_ptrdiff(NGHTTP3_ERR_INVALID_STATE, ==, n); + + conn->remote.settings.h3_datagram = 1; + + /* Quarter Stream ID of stream 0 is 0 (1 byte) */ + n = nghttp3_conn_write_datagram_prefix(conn, 0, buf, sizeof(buf)); + + assert_ptrdiff(1, ==, n); + nghttp3_get_varint(&qstream_id, buf); + assert_int64(0, ==, qstream_id); + + /* Quarter Stream ID of stream 400 is 100 (2 bytes) */ + n = nghttp3_conn_write_datagram_prefix(conn, 400, buf, sizeof(buf)); + + assert_ptrdiff(2, ==, n); + nghttp3_get_varint(&qstream_id, buf); + assert_int64(100, ==, qstream_id); + + /* Non-request stream is rejected */ + n = nghttp3_conn_write_datagram_prefix(conn, 3, buf, sizeof(buf)); + + assert_ptrdiff(NGHTTP3_ERR_INVALID_ARGUMENT, ==, n); + + /* Destination buffer too small is rejected */ + n = nghttp3_conn_write_datagram_prefix(conn, 400, buf, 1); + + assert_ptrdiff(NGHTTP3_ERR_INVALID_ARGUMENT, ==, n); + + nghttp3_conn_del(conn); + + /* Sending is not allowed when not enabled by the local endpoint */ + { + nghttp3_settings settings_off; + conn_options opts_off; + + nghttp3_settings_default(&settings_off); + + opts_off = (conn_options){ + .settings = &settings_off, + }; + + setup_default_client_with_options(&conn, opts_off); + } + + conn->remote.settings.h3_datagram = 1; + + n = nghttp3_conn_write_datagram_prefix(conn, 0, buf, sizeof(buf)); + + assert_ptrdiff(NGHTTP3_ERR_INVALID_STATE, ==, n); + + nghttp3_conn_del(conn); +} diff --git a/tests/nghttp3_conn_test.h b/tests/nghttp3_conn_test.h index 3e7177db..e8133451 100644 --- a/tests/nghttp3_conn_test.h +++ b/tests/nghttp3_conn_test.h @@ -72,5 +72,7 @@ munit_void_test_decl(test_nghttp3_conn_write_origin) munit_void_test_decl(test_nghttp3_conn_recv_unknown_frame) munit_void_test_decl(test_nghttp3_conn_get_stream_user_data) munit_void_test_decl(test_nghttp3_conn_is_stream_flushed) +munit_void_test_decl(test_nghttp3_conn_recv_datagram) +munit_void_test_decl(test_nghttp3_conn_write_datagram_prefix) #endif /* !defined(NGHTTP3_CONN_TEST_H) */