]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http-htx: add a new generic trailers parser
authorWilly Tarreau <w@1wt.eu>
Tue, 8 Sep 2026 06:48:53 +0000 (08:48 +0200)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Tue, 8 Sep 2026 12:50:50 +0000 (14:50 +0200)
http_trailers_to_htx() merges h2_make_htx_trailers() and
h3_trailers_to_htx() into a unified function that takes a fields list,
performs controls and adds them to HTX. It returns http_parser_status
codes so that the caller knows what happens. For now the function is
not ready to handle HTTP/1 because it unconditionally rejects upper
case letters in field names.

The parser uses two modes to detect pseudo-headers:
  - syntax-based (it starts with a colon)
  - number-based (an index number replaces the string)

This is because the former is used by h3, the latter by h2.

include/haproxy/http_htx.h
src/http_htx.c

index ddd210ae2adbaf7a4b46d7ecc8cec6bd29a19626..bfe5f362209e5525aefa1f5d3a568fb2ef77165e 100644 (file)
@@ -74,6 +74,8 @@ int http_scheme_based_normalize(struct htx *htx);
 void http_cookie_register(struct http_hdr *list, int idx, int *first, int *last);
 int http_cookie_merge(struct htx *htx, struct http_hdr *list, int first);
 
+enum http_parser_status http_trailers_to_htx(struct http_hdr *list, struct htx *htx);
+
 struct buffer *http_load_errorfile(const char *file, char **errmsg);
 struct buffer *http_load_errormsg(const char *key, const struct ist msg, char **errmsg);
 struct buffer *http_parse_errorfile(int status, const char *file, char **errmsg);
index f199265b7a88babfabdb743e51eadf09656748b0..829b06c627c173b603fb5053602087b13a9b3ca1 100644 (file)
@@ -1991,6 +1991,134 @@ int http_cookie_merge(struct htx *htx, struct http_hdr *list, int first)
        return 0;
 }
 
+/* Takes an HTTP trailers list <list> terminated by a name being <NULL,0> and
+ * emits the equivalent HTX trailers blocks. The output contents are emitted in
+ * <htx>, and an HTTP_PRS_SUCCESS status code is returned if some bytes were
+ * emitted. In case of error, another code is returned, indicating the nature
+ * of the problem. The caller must have verified that the message in the buffer
+ * is compatible with receipt of trailers.
+ *
+ * The trailers list <list> must be composed of :
+ *   - n.name != NULL, n.len  > 0 : literal trailer name
+ *   - n.name == NULL, n.len  > 0 : reserved for indexed pseudo headers
+ *   - n.name ignored, n.len == 0 : end of list
+ *   - in all cases except the end of list, v.name and v.len must designate a
+ *     valid value.
+ */
+enum http_parser_status http_trailers_to_htx(struct http_hdr *list, struct htx *htx)
+{
+       uint32_t data_ofs = htx->data; /* used to rollback on error */
+       enum http_parser_status ret = HTTP_PRS_SUCCESS;
+       const char *ctl;
+       struct ist v;
+       uint32_t idx;
+       int i;
+
+       for (idx = 0; list[idx].n.len != 0; idx++) {
+               if (!isttest(list[idx].n) ||           // indexed pseudo header
+                   istmatch(list[idx].n, ist(":"))) { // raw pseudo header
+                       /* RFC 9114 4.3. HTTP Control Data
+                        * RFC 9113 8.3. HTTP Control Data
+                        * Pseudo-header fields MUST NOT appear in trailer sections.
+                        */
+                       ret = HTTP_PRS_PHDR_TRL;
+                       goto fail;
+               }
+
+               /* RFC 9113 8.2.1. Field Validity:
+                *
+                *   An implementation that validates fields according to the
+                *   definitions in Sections 5.1 and 5.5 of [HTTP] only needs
+                *   an additional check that field names do not include
+                *   uppercase characters.
+                *
+                * RFC 9110 5.1. Field Names:
+                *   field-name     = token
+                */
+               for (i = 0; i < list[idx].n.len; i++) {
+                       if ((uint8_t)(list[idx].n.ptr[i] - 'A') <= 'Z' - 'A' ||
+                           !HTTP_IS_TOKEN(list[idx].n.ptr[i])) {
+                               ret = HTTP_PRS_INV_HNAME;
+                               goto fail;
+                       }
+               }
+
+               /* All field names affecting decoding or routing are forbidden
+                * in trailers.
+                */
+               if (isteq(list[idx].n, ist("host")) ||
+                   isteq(list[idx].n, ist("content-length")) ||
+                   isteq(list[idx].n, ist("connection")) ||
+                   isteq(list[idx].n, ist("proxy-connection")) ||
+                   isteq(list[idx].n, ist("keep-alive")) ||
+                   isteq(list[idx].n, ist("upgrade")) ||
+                   isteq(list[idx].n, ist("te")) ||
+                   isteq(list[idx].n, ist("transfer-encoding"))) {
+                       ret = HTTP_PRS_FORB_TRL;
+                       goto fail;
+               }
+
+               /* RFC 9113 8.2.1. Field validity
+                *
+                *   A field value MUST NOT contain the zero value (ASCII NUL,
+                *   0x00), line feed (ASCII LF, 0x0a), or carriage return (ASCII
+                *   CR, 0x0d) at any position.
+                *
+                * RFC 9114 10.3 Intermediary-Encapsulation Attacks
+                *
+                *   While most values that can be encoded will not alter field
+                *   parsing, carriage return (ASCII 0x0d), line feed (ASCII 0x0a),
+                *   and the null character (ASCII 0x00) might be exploited by an
+                *   attacker if they are translated verbatim. Any request or
+                *   response that contains a character not permitted in a field
+                *   value MUST be treated as malformed
+                */
+               ctl = ist_find_ctl(list[idx].v);
+               if (unlikely(ctl) && http_header_has_forbidden_char(list[idx].v, ctl)) {
+                       ret = HTTP_PRS_INV_HVAL;
+                       goto fail;
+               }
+
+               /* trim leading/trailing LWS as per RC9113#8.2.1 */
+               for (v = list[idx].v; v.len; v.len--) {
+                       if (unlikely(HTTP_IS_LWS(*v.ptr)))
+                               v.ptr++;
+                       else if (!unlikely(HTTP_IS_LWS(v.ptr[v.len - 1])))
+                               break;
+               }
+
+               if (!htx_add_trailer(htx, list[idx].n, v)) {
+                       ret = HTTP_PRS_TOO_MANY;
+                       goto fail;
+               }
+       }
+
+       /* Check the number of trailers against "tune.http.maxhdr" value
+        * before adding EOT block
+        */
+       if (idx > global.tune.max_http_hdr) {
+               ret = HTTP_PRS_TOO_MANY;
+               goto fail;
+       }
+
+       /* now finalize the trailers block */
+       if (!htx_add_endof(htx, HTX_BLK_EOT)) {
+               ret = HTTP_PRS_TOO_LARGE;
+               goto fail;
+       }
+
+       return HTTP_PRS_SUCCESS;
+
+ fail:
+       /* Rollback the partial conversion. Note that a pointer on the tail
+        * block cannot be used for this because it encodes a block position
+        * and adding blocks may compact the block table, so rely on the
+        * amount of data that was present on entry instead.
+        */
+       htx_truncate(htx, data_ofs);
+       return ret;
+}
+
 /* Parses the "errorloc[302|303]" proxy keyword */
 static int proxy_parse_errorloc(char **args, int section, struct proxy *curpx,
                                  const struct proxy *defpx, const char *file, int line,