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>
openSUSE Build Service is sponsored by