From 5f13a7645e565c5c1a06f3ef86e97afb856fb364 Mon Sep 17 00:00:00 2001
From: Stefan Eissing <stefan@eissing.org>
Date: Fri, 6 Mar 2026 14:54:09 +0100
Subject: [PATCH] proxy-auth: additional tests

Also eliminate the special handling for socks proxy match.

Closes #20837
---
 lib/url.c                        | 28 +++++++---------------------
 tests/http/test_13_proxy_auth.py | 20 ++++++++++++++++++++
 tests/http/testenv/curl.py       | 18 +++++++++++++++---
 3 files changed, 42 insertions(+), 24 deletions(-)

diff --git a/lib/url.c b/lib/url.c
index eabeb776ab..bdc183b45b 100644
--- a/lib/url.c
+++ b/lib/url.c
@@ -590,30 +590,15 @@ static bool proxy_info_matches(const struct proxy_info *data,
 {
   if((data->proxytype == needle->proxytype) &&
      (data->port == needle->port) &&
-     strcasecompare(data->host.name, needle->host.name))
-    return TRUE;
+     strcasecompare(data->host.name, needle->host.name)) {
 
+    if(Curl_timestrcmp(data->user, needle->user) ||
+       Curl_timestrcmp(data->passwd, needle->passwd))
+      return FALSE;
+    return TRUE;
+  }
   return FALSE;
 }
-
-static bool
-socks_proxy_info_matches(const struct proxy_info *data,
-                         const struct proxy_info *needle)
-{
-  if(!proxy_info_matches(data, needle))
-    return FALSE;
-
-  /* the user information is case-sensitive
-     or at least it is not defined as case-insensitive
-     see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 */
-
-  /* curl_strequal does a case insensitive comparison,
-     so do not use it here! */
-  if(Curl_timestrcmp(data->user, needle->user) ||
-     Curl_timestrcmp(data->passwd, needle->passwd))
-    return FALSE;
-  return TRUE;
-}
 #else
 /* disabled, will not get called */
 #define proxy_info_matches(x,y) FALSE
@@ -923,8 +909,7 @@ static bool url_match_proxy_use(struct connectdata *conn,
     return FALSE;
 
   if(m->needle->bits.socksproxy &&
-    !socks_proxy_info_matches(&m->needle->socks_proxy,
-                              &conn->socks_proxy))
+     !proxy_info_matches(&m->needle->socks_proxy, &conn->socks_proxy))
     return FALSE;
 
   if(m->needle->bits.httpproxy) {
diff --git a/tests/http/test_13_proxy_auth.py b/tests/http/test_13_proxy_auth.py
index 080adef187..33fb211e99 100644
--- a/tests/http/test_13_proxy_auth.py
+++ b/tests/http/test_13_proxy_auth.py
@@ -169,3 +169,23 @@ class TestProxyAuth:
             '--negotiate', '--proxy-user', 'proxy:proxy'
         ])
         r1.check_response(count=1, http_status=200)
+
+    def test_13_10_tunnels_mixed_auth(self, env: Env, httpd, configures_httpd):
+        self.httpd_configure(env, httpd)
+        curl = CurlClient(env=env)
+        url1 = f'http://localhost:{env.http_port}/data.json?1'
+        url2 = f'http://localhost:{env.http_port}/data.json?2'
+        url3 = f'http://localhost:{env.http_port}/data.json?3'
+        xargs1 = curl.get_proxy_args(proxys=False, tunnel=True)
+        xargs1.extend(['--proxy-user', 'proxy:proxy']) # good auth
+        xargs2 = curl.get_proxy_args(proxys=False, tunnel=True)
+        xargs2.extend(['--proxy-user', 'ungood:ungood']) # bad auth
+        xargs3 = curl.get_proxy_args(proxys=False, tunnel=True)
+        # no auth
+        r = curl.http_download(urls=[url1, url2, url3], alpn_proto='http/1.1', with_stats=True,
+                               url_options={url1: xargs1, url2: xargs2, url3: xargs3})
+        # only url1 succeeds, others fail, no connection reuse
+        assert r.stats[0]['http_code'] == 200, f'{r.dump_logs()}'
+        assert r.stats[1]['http_code'] == 0, f'{r.dump_logs()}'
+        assert r.stats[2]['http_code'] == 0, f'{r.dump_logs()}'
+        assert r.total_connects == 3, f'{r.dump_logs()}'
diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py
index 4fc11c7923..1f812a1c2e 100644
--- a/tests/http/testenv/curl.py
+++ b/tests/http/testenv/curl.py
@@ -543,7 +543,8 @@ class CurlClient:
                       with_profile: bool = False,
                       with_tcpdump: bool = False,
                       no_save: bool = False,
-                      extra_args: Optional[List[str]] = None):
+                      extra_args: Optional[List[str]] = None,
+                      url_options: Optional[Dict[str,List[str]]] = None):
         if extra_args is None:
             extra_args = []
         if no_save:
@@ -563,6 +564,7 @@ class CurlClient:
             ])
         return self._raw(urls, alpn_proto=alpn_proto, options=extra_args,
                          with_stats=with_stats,
+                         url_options=url_options,
                          with_headers=with_headers,
                          with_profile=with_profile,
                          with_tcpdump=with_tcpdump)
@@ -1083,6 +1085,7 @@ class CurlClient:
 
     def _raw(self, urls, intext='', timeout=None, options=None, insecure=False,
              alpn_proto: Optional[str] = None,
+             url_options=None,
              force_resolve=True,
              with_stats=False,
              with_headers=True,
@@ -1092,7 +1095,8 @@ class CurlClient:
         args = self._complete_args(
             urls=urls, timeout=timeout, options=options, insecure=insecure,
             alpn_proto=alpn_proto, force_resolve=force_resolve,
-            with_headers=with_headers, def_tracing=def_tracing)
+            with_headers=with_headers, def_tracing=def_tracing,
+            url_options=url_options)
         r = self._run(args, intext=intext, with_stats=with_stats,
                       with_profile=with_profile, with_tcpdump=with_tcpdump)
         if r.exit_code == 0 and with_headers:
@@ -1102,8 +1106,10 @@ class CurlClient:
     def _complete_args(self, urls, timeout=None, options=None,
                        insecure=False, force_resolve=True,
                        alpn_proto: Optional[str] = None,
+                       url_options=None,
                        with_headers: bool = True,
                        def_tracing: bool = True):
+        url_sep = []
         if not isinstance(urls, list):
             urls = [urls]
 
@@ -1129,7 +1135,13 @@ class CurlClient:
             active_options = options[options.index('--next') + 1:]
 
         for url in urls:
-            u = urlparse(urls[0])
+            args.extend(url_sep)
+            if url_options is not None:
+                url_sep = ['--next']
+
+            u = urlparse(url)
+            if url_options is not None and url in url_options:
+                args.extend(url_options[url])
             if options:
                 args.extend(options)
             if alpn_proto is not None:
-- 
2.53.0

