From: Willy Tarreau Date: Tue, 8 Sep 2026 06:48:53 +0000 (+0200) Subject: MINOR: http-htx: add a new generic trailers parser X-Git-Tag: v3.5-dev7~63 X-Git-Url: https://git.kaiwu.me/$%7BISSUES%7D/static/gitweb.js?a=commitdiff_plain;h=28a00d7d248f1fe5cb03a01c515698c0fb4dbac5;p=haproxy.git MINOR: http-htx: add a new generic trailers parser 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. --- diff --git a/include/haproxy/http_htx.h b/include/haproxy/http_htx.h index ddd210ae2..bfe5f3622 100644 --- a/include/haproxy/http_htx.h +++ b/include/haproxy/http_htx.h @@ -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); diff --git a/src/http_htx.c b/src/http_htx.c index f199265b7..829b06c62 100644 --- a/src/http_htx.c +++ b/src/http_htx.c @@ -1991,6 +1991,134 @@ int http_cookie_merge(struct htx *htx, struct http_hdr *list, int first) return 0; } +/* Takes an HTTP trailers list terminated by a name being and + * emits the equivalent HTX trailers blocks. The output contents are emitted in + * , 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 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,