MEDIUM: h1: better handle transfer-encoding vs content-length
The transfer-encoding header processing was a bit lenient in this part
because it was made to read messages already validated by haproxy. We
absolutely need to reinstate the strict processing defined in RFC7230
as is currently being done in proto_http.c. That is, transfer-encoding
presence alone is enough to cancel content-length, and must be
terminated by the "chunked" token, except in the response where we
can fall back to the close mode if it's not last.
For this we now use a specific parsing function which updates the
flags and we introduce a new flag H1_MF_XFER_ENC indicating that the
transfer-encoding header is present.
Last, if such a header is found, we delete all content-length header
fields found in the message.
diff --git a/include/types/h1.h b/include/types/h1.h
index 2abf472..c6645fa 100644
--- a/include/types/h1.h
+++ b/include/types/h1.h
@@ -145,6 +145,7 @@
#define H1_MF_CONN_KAL 0x00000040 // message contains "connection: keep-alive"
#define H1_MF_CONN_UPG 0x00000080 // message contains "connection: upgrade"
#define H1_MF_XFER_LEN 0x00000100 // message xfer size can be determined
+#define H1_MF_XFER_ENC 0x00000200 // transfer-encoding is present
/* Note: for a connection to be persistent, we need this for the request :
* - one of CLEN or CHNK
diff --git a/src/h1.c b/src/h1.c
index 65e7495..d947661 100644
--- a/src/h1.c
+++ b/src/h1.c
@@ -660,6 +660,44 @@
}
+/* Parse the Transfer-Encoding: header field of an HTTP/1 request, looking for
+ * "chunked" being the last value, and setting H1_MF_CHNK in h1m->flags only in
+ * this case. Any other token found or any empty header field found will reset
+ * this flag, so that it accurately represents the token's presence at the last
+ * position. The H1_MF_XFER_ENC flag is always set. Note that transfer codings
+ * are case-insensitive (cf RFC7230#4).
+ */
+void h1_parse_xfer_enc_header(struct h1m *h1m, struct ist value)
+{
+ char *e, *n;
+ struct ist word;
+
+ h1m->flags |= H1_MF_XFER_ENC;
+ h1m->flags &= ~H1_MF_CHNK;
+
+ word.ptr = value.ptr - 1; // -1 for next loop's pre-increment
+ e = value.ptr + value.len;
+
+ while (++word.ptr < e) {
+ /* skip leading delimitor and blanks */
+ if (HTTP_IS_LWS(*word.ptr))
+ continue;
+
+ n = http_find_hdr_value_end(word.ptr, e); // next comma or end of line
+ word.len = n - word.ptr;
+
+ /* trim trailing blanks */
+ while (word.len && HTTP_IS_LWS(word.ptr[word.len-1]))
+ word.len--;
+
+ h1m->flags &= ~H1_MF_CHNK;
+ if (isteqi(word, ist("chunked")))
+ h1m->flags |= H1_MF_CHNK;
+
+ word.ptr = n;
+ }
+}
+
/* Parse the Connection: header of an HTTP/1 request, looking for "close",
* "keep-alive", and "upgrade" values, and updating h1m->flags according to
* what was found there. Note that flags are only added, not removed, so the
@@ -1272,8 +1310,7 @@
}
if (isteqi(n, ist("transfer-encoding"))) {
- h1m->flags &= ~H1_MF_CLEN;
- h1m->flags |= H1_MF_CHNK;
+ h1_parse_xfer_enc_header(h1m, v);
}
else if (isteqi(n, ist("content-length")) && !(h1m->flags & H1_MF_CHNK)) {
h1m->flags |= H1_MF_CLEN;
@@ -1321,10 +1358,23 @@
if (restarting)
goto restart;
+ state = H1_MSG_DATA;
+ if (h1m->flags & H1_MF_XFER_ENC) {
+ if (h1m->flags & H1_MF_CLEN) {
+ h1m->flags &= ~H1_MF_CLEN;
+ hdr_count = http_del_hdr(hdr, ist("content-length"));
+ }
+
+ if (h1m->flags & H1_MF_CHNK)
+ state = H1_MSG_CHUNK_SIZE;
+ else if (!(h1m->flags & H1_MF_RESP)) {
+ /* cf RFC7230#3.3.3 : transfer-encoding in
+ * request without chunked encoding is invalid.
+ */
+ goto http_msg_invalid;
+ }
+ }
+
- if (h1m->flags & H1_MF_CHNK)
- state = H1_MSG_CHUNK_SIZE;
- else
- state = H1_MSG_DATA;
break;
default: