File curl-handle_user-defined_connection_headers.patch of Package curl
From 061e265502fc2f605f0d87453b8b1167ba16839d Mon Sep 17 00:00:00 2001
From: Stefan Eissing <stefan@eissing.org>
Date: Mon, 22 Sep 2025 15:48:07 +0200
Subject: [PATCH] http: handle user-defined connection headers
When there is more than one user-supplied 'Connection: ' header, add
values that curl needs internally to the first one and emit all
subsequent ones thereafter.
Fixes #18662
Reported-by: Evgeny Grin (Karlson2k)
Closes #18686
---
docs/libcurl/opts/CURLOPT_HTTPHEADER.md | 4 ++
lib/http.c | 64 +++++++++++++++++----
tests/data/Makefile.am | 2 +-
tests/data/test1617 | 75 +++++++++++++++++++++++++
4 files changed, 133 insertions(+), 12 deletions(-)
create mode 100644 tests/data/test1617
diff --git a/docs/libcurl/opts/CURLOPT_HTTPHEADER.md b/docs/libcurl/opts/CURLOPT_HTTPHEADER.md
index 4440b4ec4b61..c7a705bedc82 100644
--- a/docs/libcurl/opts/CURLOPT_HTTPHEADER.md
+++ b/docs/libcurl/opts/CURLOPT_HTTPHEADER.md
@@ -55,6 +55,10 @@ without content (no data on the right side of the colon) as in `Accept:`, the
internally used header is removed. To forcibly add a header without content
(nothing after the colon), use the form `name;` (using a trailing semicolon).
+There are exceptions when suppressing headers. The `Connection:` header in
+HTTP/1.1 cannot be overridden. You can provide values for it, but should a
+request require specific ones, they are always added to your own.
+
The headers included in the linked list **must not** be CRLF-terminated, since
libcurl adds CRLF after each header item itself. Failure to comply with this
might result in strange behavior. libcurl passes on the verbatim strings you
diff --git a/lib/http.c b/lib/http.c
index ce31e6dff005..3479bb4ec976 100644
--- a/lib/http.c
+++ b/lib/http.c
@@ -263,6 +263,19 @@ char *Curl_checkProxyheaders(struct Curl_easy *data,
#define Curl_checkProxyheaders(x,y,z,a) NULL
#endif
+static bool http_header_is_empty(const char *header)
+{
+ struct Curl_str out;
+
+ if(!curlx_str_cspn(&header, &out, ";:") &&
+ (!curlx_str_single(&header, ':') || !curlx_str_single(&header, ';'))) {
+ curlx_str_untilnl(&header, &out, MAX_HTTP_RESP_HEADER_SIZE);
+ curlx_str_trimblanks(&out);
+ return curlx_strlen(&out) == 0;
+ }
+ return TRUE; /* invalid head format, treat as empty */
+}
+
/*
* Strip off leading and trailing whitespace from the value in the given HTTP
* header line and return a strdup()ed copy. Returns NULL in case of
@@ -1702,7 +1715,7 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data,
curlx_str_casecompare(&name, "Content-Length"))
;
else if(curlx_str_casecompare(&name, "Connection"))
- /* Normal Connection: header generation takes care of this */
+ /* Connection headers are handled specially */
;
else if((httpversion >= 20) &&
curlx_str_casecompare(&name, "Transfer-Encoding"))
@@ -2636,17 +2649,29 @@ static CURLcode http_check_new_conn(struct Curl_easy *data)
static CURLcode http_add_connection_hd(struct Curl_easy *data,
struct dynbuf *req)
{
- char *custom = Curl_checkheaders(data, STRCONST("Connection"));
- char *custom_val = custom ? Curl_copy_header_value(custom) : NULL;
- const char *sep = (custom_val && *custom_val) ? ", " : "Connection: ";
+ struct curl_slist *head;
+ const char *sep = "Connection: ";
CURLcode result = CURLE_OK;
size_t rlen = curlx_dyn_len(req);
+ char *value;
+ bool skip;
+
+ /* Add the 1st custom "Connection: " header, if there is one */
+ for(head = data->set.headers; head; head = head->next) {
+ if(curl_strnequal(head->data, "Connection", 10) &&
+ Curl_headersep(head->data[10]) &&
+ !http_header_is_empty(head->data)) {
+ value = Curl_copy_header_value(head->data);
+ if(!value)
+ return CURLE_OUT_OF_MEMORY;
+ result = curlx_dyn_addf(req, "%s%s", sep, value);
+ sep = ", ";
+ free(value);
+ break; /* leave, having added 1st one */
+ }
+ }
- if(custom && !custom_val)
- return CURLE_OUT_OF_MEMORY;
-
- if(custom_val && *custom_val)
- result = curlx_dyn_addf(req, "Connection: %s", custom_val);
+ /* add our internal Connection: header values, if we have any */
if(!result && data->state.http_hd_te) {
result = curlx_dyn_addf(req, "%s%s", sep, "TE");
sep = ", ";
@@ -2660,9 +2685,26 @@ static CURLcode http_add_connection_hd(struct Curl_easy *data,
}
if(!result && (rlen < curlx_dyn_len(req)))
result = curlx_dyn_addn(req, STRCONST("\r\n"));
+ if(result)
+ return result;
- free(custom_val);
- return result;
+ /* Add all user-defined Connection: headers after the first */
+ skip = TRUE;
+ for(head = data->set.headers; head; head = head->next) {
+ if(curl_strnequal(head->data, "Connection", 10) &&
+ Curl_headersep(head->data[10]) &&
+ !http_header_is_empty(head->data)) {
+ if(skip) {
+ skip = FALSE;
+ continue;
+ }
+ result = curlx_dyn_addf(req, "%s\r\n", head->data);
+ if(result)
+ return result;
+ }
+ }
+
+ return CURLE_OK;
}
/* Header identifier in order we send them by default */
diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am
index 854791fba0e9..5063e0210f42 100644
--- a/tests/data/Makefile.am
+++ b/tests/data/Makefile.am
@@ -214,7 +214,7 @@ test1580 test1581 \
test1590 test1591 test1592 test1593 test1594 test1595 test1596 test1597 \
test1598 test1599 test1600 test1601 test1602 test1603 test1604 test1605 \
test1606 test1607 test1608 test1609 test1610 test1611 test1612 test1613 \
-test1614 test1615 test1616 \
+test1614 test1615 test1616 test1617 \
test1620 test1621 \
\
test1630 test1631 test1632 test1633 test1634 test1635 \
diff --git a/tests/data/test1617 b/tests/data/test1617
new file mode 100644
index 000000000000..0fe967bd27a7
--- /dev/null
+++ b/tests/data/test1617
@@ -0,0 +1,75 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+HTTP GET
+compressed
+Transfer-Encoding
+</keywords>
+</info>
+#
+# Server-side
+<reply>
+<data>
+HTTP/1.1 200 OK
+Date: Mon, 29 Nov 2004 21:56:53 GMT
+Server: Apache/1.3.31 (Debian GNU/Linux) mod_gzip/1.3.26.1a PHP/4.3.9-1 mod_ssl/2.8.20 OpenSSL/0.9.7d mod_perl/1.29
+Vary: Accept-Encoding
+Content-Type: text/html; charset=ISO-8859-1
+Transfer-Encoding: gzip, chunked
+
+2c
+%hex[%1f%8b%08%08%79%9e%ab%41%00%03%6c%61%6c%61%6c%61%00%cb%c9%cc%4b%55%30%e4%52%c8%01%d1%46%5c]hex%
+%hex[%10%86%31%17%00]hex%
+%hex[%02%71%60%18%00%00%00]hex%
+0
+
+</data>
+
+<datacheck>
+HTTP/1.1 200 OK
+Date: Mon, 29 Nov 2004 21:56:53 GMT
+Server: Apache/1.3.31 (Debian GNU/Linux) mod_gzip/1.3.26.1a PHP/4.3.9-1 mod_ssl/2.8.20 OpenSSL/0.9.7d mod_perl/1.29
+Vary: Accept-Encoding
+Content-Type: text/html; charset=ISO-8859-1
+Transfer-Encoding: gzip, chunked
+
+line 1
+ line 2
+ line 3
+</datacheck>
+
+</reply>
+
+#
+# Client-side
+<client>
+<features>
+libz
+</features>
+<server>
+http
+</server>
+<name>
+HTTP GET transfer-encoding with two user Connection: headers
+</name>
+<command>
+http://%HOSTIP:%HTTPPORT/%TESTNUMBER --tr-encoding -H "Connection: this" -H "Connection: that"
+</command>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+<protocol>
+GET /%TESTNUMBER HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+User-Agent: curl/%VERSION
+Accept: */*
+TE: gzip
+Connection: this, TE
+Connection: that
+
+</protocol>
+</verify>
+</testcase>