]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: h3: transcode H3 response headers into HTX blocks
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Mon, 16 Jun 2025 14:32:57 +0000 (16:32 +0200)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Mon, 16 Jun 2025 16:11:09 +0000 (18:11 +0200)
Finalize HTTP/3 response transcoding into HTX message. This patch
implements conversion of HTTP/3 headers provided by the server into HTX
blocks.

Special checks have been implemented to reject connection-specific
headers, causing the stream to be shut in error. Also, handling of
content-length requires that the body size is equal to the value
advertized in the header to prevent HTTP desync.

src/h3.c

index f2799066b75f670f06aad20cf0f9b8fa1cc09953..75a2afbe2f6ffaa1149f28699521693119948a48 100644 (file)
--- a/src/h3.c
+++ b/src/h3.c
@@ -152,7 +152,7 @@ DECLARE_STATIC_POOL(pool_head_h3c, "h3c", sizeof(struct h3c));
 
 #define H3_SF_UNI_INIT  0x00000001  /* stream type not parsed for unidirectional stream */
 #define H3_SF_UNI_NO_H3 0x00000002  /* unidirectional stream does not carry H3 frames */
-#define H3_SF_HAVE_CLEN 0x00000004  /* content-length header is present */
+#define H3_SF_HAVE_CLEN 0x00000004  /* content-length header is present; relevant either for request or response depending on the side of the connection */
 
 struct h3s {
        struct h3c *h3c;
@@ -1099,13 +1099,15 @@ static ssize_t h3_resp_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
        struct h3s *h3s = qcs->ctx;
        struct h3c *h3c = h3s->h3c;
        struct buffer *appbuf = NULL;
+       struct buffer *tmp = get_trash_chunk();
        struct htx *htx = NULL;
        struct htx_sl *sl;
        struct http_hdr list[global.tune.max_http_hdr * 2];
        unsigned int flags = HTX_SL_F_NONE;
        struct ist status = IST_NULL;
        unsigned char h, t, u;
-       int hdr_idx;
+       int hdr_idx, ret;
+       int qpack_err;
 
        /* RFC 9114 4.1.2. Malformed Requests and Responses
         *
@@ -1129,6 +1131,20 @@ static ssize_t h3_resp_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
 
        TRACE_ENTER(H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
 
+       /* TODO support buffer wrapping */
+       BUG_ON(b_head(buf) + len >= b_wrap(buf));
+       ret = qpack_decode_fs((const unsigned char *)b_head(buf), len, tmp,
+                           list, sizeof(list) / sizeof(list[0]));
+       if (ret < 0) {
+               TRACE_ERROR("QPACK decoding error", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
+               if ((qpack_err = qpack_err_decode(ret)) >= 0) {
+                       h3c->err = qpack_err;
+                       qcc_report_glitch(qcs->qcc, 1);
+               }
+               len = -1;
+               goto out;
+       }
+
        appbuf = qcc_get_stream_rxbuf(qcs);
        BUG_ON(!appbuf || b_data(appbuf)); /* TODO */
        BUG_ON(!b_size(appbuf)); /* TODO */
@@ -1211,6 +1227,71 @@ static ssize_t h3_resp_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
        }
        sl->info.res.status = h * 100 + t * 10 + u;
 
+       /* now treat standard headers */
+       while (1) {
+               if (isteq(list[hdr_idx].n, ist("")))
+                       break;
+
+               if (_h3_handle_hdr(qcs, &list[hdr_idx])) {
+                       h3s->err = H3_ERR_MESSAGE_ERROR;
+                       qcc_report_glitch(h3c->qcc, 1);
+                       len = -1;
+                       goto out;
+               }
+
+               if (isteq(list[hdr_idx].n, ist("connection")) ||
+                   isteq(list[hdr_idx].n, ist("proxy-connection")) ||
+                   isteq(list[hdr_idx].n, ist("keep-alive")) ||
+                   isteq(list[hdr_idx].n, ist("upgrade")) ||
+                   isteq(list[hdr_idx].n, ist("transfer-encoding"))) {
+                       /* forbidden connection-specific fields */
+                       TRACE_ERROR("invalid connection header", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
+                       h3s->err = H3_ERR_MESSAGE_ERROR;
+                       qcc_report_glitch(h3c->qcc, 1);
+                       len = -1;
+                       goto out;
+               }
+               else if (isteq(list[hdr_idx].n, ist("content-length"))) {
+                       ret = http_parse_cont_len_header(&list[hdr_idx].v,
+                                                        &h3s->body_len,
+                                                        h3s->flags & H3_SF_HAVE_CLEN);
+                       if (ret < 0) {
+                               TRACE_ERROR("invalid content-length", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
+                               h3s->err = H3_ERR_MESSAGE_ERROR;
+                               qcc_report_glitch(h3c->qcc, 1);
+                               len = -1;
+                               goto out;
+                       }
+                       else if (!ret) {
+                               /* Skip duplicated value. */
+                               ++hdr_idx;
+                               continue;
+                       }
+
+                       h3s->flags |= H3_SF_HAVE_CLEN;
+                       sl->flags |= HTX_SL_F_CLEN;
+                       /* This will fail if current frame is the last one and
+                        * content-length is not null.
+                        */
+                       if (h3_check_body_size(qcs, fin)) {
+                               len = -1;
+                               goto out;
+                       }
+               }
+
+               if (!htx_add_header(htx, list[hdr_idx].n, _h3_trim_header(list[hdr_idx].v))) {
+                       len = -1;
+                       goto out;
+               }
+               ++hdr_idx;
+       }
+
+       /* Check the number of blocks against "tune.http.maxhdr" value before adding EOH block */
+       if (htx_nbblks(htx) > global.tune.max_http_hdr) {
+               len = -1;
+               goto out;
+       }
+
        if (!htx_add_endof(htx, HTX_BLK_EOH)) {
                len = -1;
                goto out;