diff --git a/src/protocols/ioxide.nghttp3/native/ioxide_nghttp3_shim.c b/src/protocols/ioxide.nghttp3/native/ioxide_nghttp3_shim.c index e23d044f..03bc7e5c 100644 --- a/src/protocols/ioxide.nghttp3/native/ioxide_nghttp3_shim.c +++ b/src/protocols/ioxide.nghttp3/native/ioxide_nghttp3_shim.c @@ -67,6 +67,8 @@ typedef struct ih3_conn { ih3_callbacks cbs; void *user; ih3_stream *streams; + nghttp3_nv *nv; /* the fields being submitted; keeps its size */ + size_t nv_cap; } ih3_conn; static ih3_stream *ih3_stream_find(ih3_conn *c, int64_t id) @@ -305,6 +307,7 @@ void ih3_free(ih3_conn *c) ih3_stream_drop(c, c->streams->id); } nghttp3_conn_del(c->conn); + free(c->nv); free(c); } @@ -326,14 +329,23 @@ int64_t ih3_read_stream(ih3_conn *c, int64_t stream_id, const uint8_t *data, siz return (int64_t)rv; } -#define IH3_MAX_NV 64 - /* headers: [u16 namelen][name][u16 valuelen][value] repeated, little-endian u16 (written and read - * on the same host). Returns the entry count, or -1 on a malformed buffer. */ -static int ih3_unpack_headers(const uint8_t *headers, size_t headers_len, nghttp3_nv *nva) + * on the same host). Unpacks into c->nv, which nghttp3 copies out of during submit. Returns the + * entry count, or a negative nghttp3 error. */ +static int ih3_unpack_headers(ih3_conn *c, const uint8_t *headers, size_t headers_len) { size_t nvlen = 0; - for (size_t off = 0; off + 2 <= headers_len && nvlen < IH3_MAX_NV; nvlen++) { + for (size_t off = 0; off + 2 <= headers_len; nvlen++) { + if (nvlen == c->nv_cap) { + size_t cap = c->nv_cap == 0 ? 64 : c->nv_cap * 2; + nghttp3_nv *grown = realloc(c->nv, cap * sizeof(*grown)); + if (grown == NULL) { + return NGHTTP3_ERR_NOMEM; + } + c->nv = grown; + c->nv_cap = cap; + } + uint16_t namelen; memcpy(&namelen, headers + off, 2); off += 2; @@ -341,7 +353,7 @@ static int ih3_unpack_headers(const uint8_t *headers, size_t headers_len, nghttp off += namelen; if (off + 2 > headers_len) { - return -1; + return NGHTTP3_ERR_INVALID_ARGUMENT; } uint16_t valuelen; memcpy(&valuelen, headers + off, 2); @@ -350,14 +362,14 @@ static int ih3_unpack_headers(const uint8_t *headers, size_t headers_len, nghttp off += valuelen; if (off > headers_len) { - return -1; + return NGHTTP3_ERR_INVALID_ARGUMENT; } - nva[nvlen].name = (uint8_t *)name; - nva[nvlen].namelen = namelen; - nva[nvlen].value = (uint8_t *)value; - nva[nvlen].valuelen = valuelen; - nva[nvlen].flags = NGHTTP3_NV_FLAG_NONE; + c->nv[nvlen].name = (uint8_t *)name; + c->nv[nvlen].namelen = namelen; + c->nv[nvlen].value = (uint8_t *)value; + c->nv[nvlen].valuelen = valuelen; + c->nv[nvlen].flags = NGHTTP3_NV_FLAG_NONE; if (off == headers_len) { return (int)(nvlen + 1); @@ -371,10 +383,9 @@ int ih3_submit_response(ih3_conn *c, int64_t stream_id, const uint8_t *headers, size_t headers_len, const uint8_t *body, size_t body_len) { - nghttp3_nv nva[IH3_MAX_NV]; - int nvlen = ih3_unpack_headers(headers, headers_len, nva); + int nvlen = ih3_unpack_headers(c, headers, headers_len); if (nvlen < 0) { - return NGHTTP3_ERR_INVALID_ARGUMENT; + return nvlen; } ih3_stream *s = NULL; @@ -396,7 +407,7 @@ int ih3_submit_response(ih3_conn *c, int64_t stream_id, } nghttp3_data_reader dr = { .read_data = ih3_read_body }; - int rv = nghttp3_conn_submit_response(c->conn, stream_id, nva, (size_t)nvlen, + int rv = nghttp3_conn_submit_response(c->conn, stream_id, c->nv, (size_t)nvlen, body_len > 0 ? &dr : NULL); if (rv != 0 && s != NULL) { ih3_stream_drop(c, stream_id); @@ -410,10 +421,9 @@ int ih3_submit_response(ih3_conn *c, int64_t stream_id, int ih3_submit_response_stream(ih3_conn *c, int64_t stream_id, const uint8_t *headers, size_t headers_len) { - nghttp3_nv nva[IH3_MAX_NV]; - int nvlen = ih3_unpack_headers(headers, headers_len, nva); + int nvlen = ih3_unpack_headers(c, headers, headers_len); if (nvlen < 0) { - return NGHTTP3_ERR_INVALID_ARGUMENT; + return nvlen; } ih3_stream *s = calloc(1, sizeof(*s)); @@ -426,7 +436,7 @@ int ih3_submit_response_stream(ih3_conn *c, int64_t stream_id, c->streams = s; nghttp3_data_reader dr = { .read_data = ih3_read_body_streamed }; - int rv = nghttp3_conn_submit_response(c->conn, stream_id, nva, (size_t)nvlen, &dr); + int rv = nghttp3_conn_submit_response(c->conn, stream_id, c->nv, (size_t)nvlen, &dr); if (rv != 0) { ih3_stream_drop(c, stream_id); } @@ -446,10 +456,9 @@ int ih3_submit_request(ih3_conn *c, int64_t stream_id, const uint8_t *headers, size_t headers_len, const uint8_t *body, size_t body_len) { - nghttp3_nv nva[IH3_MAX_NV]; - int nvlen = ih3_unpack_headers(headers, headers_len, nva); + int nvlen = ih3_unpack_headers(c, headers, headers_len); if (nvlen < 0) { - return NGHTTP3_ERR_INVALID_ARGUMENT; + return nvlen; } ih3_stream *s = NULL; @@ -471,7 +480,7 @@ int ih3_submit_request(ih3_conn *c, int64_t stream_id, } nghttp3_data_reader dr = { .read_data = ih3_read_body }; - int rv = nghttp3_conn_submit_request(c->conn, stream_id, nva, (size_t)nvlen, + int rv = nghttp3_conn_submit_request(c->conn, stream_id, c->nv, (size_t)nvlen, body_len > 0 ? &dr : NULL, NULL); if (rv != 0 && s != NULL) { ih3_stream_drop(c, stream_id); diff --git a/src/protocols/ioxide.nghttp3/runtimes/linux-arm64/native/libioxide_nghttp3.so b/src/protocols/ioxide.nghttp3/runtimes/linux-arm64/native/libioxide_nghttp3.so index a1b0a298..740073dd 100755 Binary files a/src/protocols/ioxide.nghttp3/runtimes/linux-arm64/native/libioxide_nghttp3.so and b/src/protocols/ioxide.nghttp3/runtimes/linux-arm64/native/libioxide_nghttp3.so differ diff --git a/src/protocols/ioxide.nghttp3/runtimes/linux-x64/native/libioxide_nghttp3.so b/src/protocols/ioxide.nghttp3/runtimes/linux-x64/native/libioxide_nghttp3.so index cc3af8e5..55aa8547 100755 Binary files a/src/protocols/ioxide.nghttp3/runtimes/linux-x64/native/libioxide_nghttp3.so and b/src/protocols/ioxide.nghttp3/runtimes/linux-x64/native/libioxide_nghttp3.so differ diff --git a/tests/Ioxide.Tests.E2E/Program.cs b/tests/Ioxide.Tests.E2E/Program.cs index e0c353db..6958dce2 100644 --- a/tests/Ioxide.Tests.E2E/Program.cs +++ b/tests/Ioxide.Tests.E2E/Program.cs @@ -56,6 +56,7 @@ private static int Main() H3BodyTruncationTests.Register(runner); H3HeldChunkTests.Register(runner); H3ErrorCodeTests.Register(runner); + Nghttp3ManyFieldsTests.Register(runner); H3CriticalStreamTests.Register(runner); H3AlpnTests.Register(runner); Http2BodyTests.Register(runner); diff --git a/tests/Ioxide.Tests.E2E/Protocols/Nghttp3ManyFieldsTests.cs b/tests/Ioxide.Tests.E2E/Protocols/Nghttp3ManyFieldsTests.cs new file mode 100644 index 00000000..4e82e446 --- /dev/null +++ b/tests/Ioxide.Tests.E2E/Protocols/Nghttp3ManyFieldsTests.cs @@ -0,0 +1,80 @@ +using System.Text; +using ioxide.nghttp3; +using ioxide.ngtcp2; + +namespace Ioxide.Tests; + +/// +/// A message's fields are not capped by the shim: each reaches the peer, however many there are. +/// +internal static class Nghttp3ManyFieldsTests +{ + public static void Register(Runner runner) + { + // 64 and 65 fields with :status are either side of the first growth, and 201 past the second. + foreach (int fields in new[] { 63, 64, 200 }) + { + foreach (bool asStream in new[] { false, true }) + { + int count = fields; + bool streamed = asStream; + runner.Test($"nghttp3: a {(streamed ? "streamed" : "buffered")} response with {count} headers of its own reaches the peer whole", () => + { + var response = new Nghttp3Response { Status = 200 }; + for (int i = 0; i < count; i++) + { + response.Headers.Add(Encoding.ASCII.GetBytes($"x-{i}"), "v"u8.ToArray()); + } + + int udpPort = Start(connection => streamed + ? connection.RunAsync(async (_, writer) => + { + writer.WriteHeaders(response); + await writer.FlushAsync(); + }) + : connection.RunAsync(_ => response)); + + using H3TestClient client = Connect(udpPort); + Assert.Equal(200, client.Get("/", timeoutMs: 5000).Status); + Assert.Equal(count, client.ResponseHeaders.Count(h => h.Name.StartsWith("x-", StringComparison.Ordinal))); + }); + } + } + + runner.Test("nghttp3: a request past 64 fields reaches its handler whole", () => + { + int seen = -1; + var ok = new Nghttp3Response { Status = 200 }; + int udpPort = Start(connection => connection.RunAsync(request => + { + seen = request.Headers.AsSpan().Length; + return ok; + })); + + using H3TestClient client = Connect(udpPort); + (string, string)[] many = [.. Enumerable.Range(0, 70).Select(i => ($"x-{i}", "v"))]; + Assert.Equal(200, client.Request("GET", "/", null, many, timeoutMs: 5000).Status); + Assert.Equal(70, seen); + }); + } + + private static int Start(Func run) + { + (string certPath, string keyPath) = TestCert.Ensure(); + var engine = TestServer.Own(new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"])); + + (_, int udpPort) = TestServer.StartDatagram( + onDatagram: null, + quicFactory: engine.CreateFactory(), + quicHandle: (_, conn) => run(new Nghttp3Connection(conn))); + return udpPort; + } + + private static H3TestClient Connect(int udpPort) + { + var client = new H3TestClient("127.0.0.1", udpPort); + client.Connect(); + Assert.True(client.CompleteHandshake(timeoutMs: 5000), "handshake did not complete"); + return client; + } +}