]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
url: proxy ssl connection reuse fix
authorStefan Eissing <stefan@eissing.org>
Fri, 3 Nov 2023 10:46:14 +0000 (11:46 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 3 Nov 2023 15:43:45 +0000 (16:43 +0100)
- tunnel https proxy used for http: transfers does
  no check if proxy-ssl configuration matches
- test cases added, test_10_12 fails on 8.4.0

Closes #12255

lib/url.c
tests/http/test_10_proxy.py
tests/http/testenv/curl.py

index 7002b942498c60449cec8c3f5aacfa8ee878701a..f0d04e8857bd46b2b461abb98fbfae481b2a5613 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -1203,17 +1203,19 @@ ConnectionExists(struct Curl_easy *data,
           continue;
 
         if(IS_HTTPS_PROXY(needle->http_proxy.proxytype)) {
-          /* use https proxy */
-          if(needle->http_proxy.proxytype !=
-             check->http_proxy.proxytype)
+          /* https proxies come in different types, http/1.1, h2, ... */
+          if(needle->http_proxy.proxytype != check->http_proxy.proxytype)
             continue;
-          else if(needle->handler->flags&PROTOPT_SSL) {
-            /* use double layer ssl */
-            if(!Curl_ssl_conn_config_match(data, check, TRUE))
-              continue;
-          }
-          else if(!Curl_ssl_conn_config_match(data, check, FALSE))
+          /* match SSL config to proxy */
+          if(!Curl_ssl_conn_config_match(data, check, TRUE)) {
+            DEBUGF(infof(data,
+              "Connection #%" CURL_FORMAT_CURL_OFF_T
+              " has different SSL proxy parameters, can't reuse",
+              check->connection_id));
             continue;
+          }
+          /* the SSL config to the server, which may apply here is checked
+           * further below */
         }
       }
 #endif
index df234013c15d0c44a1d11b3e84d137491e739052..0e4060b67c4823eb0896c8aaf9311c527ddcd08c 100644 (file)
@@ -247,3 +247,105 @@ class TestProxy:
             assert r.total_connects == 2
         else:
             assert r.total_connects == 2
+
+    @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason=f"curl without SSL")
+    @pytest.mark.parametrize("tunnel", ['http/1.1', 'h2'])
+    @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx available")
+    def test_10_10_reuse_proxy(self, env: Env, httpd, nghttpx_fwd, tunnel, repeat):
+        # url twice via https: proxy separated with '--next', will reuse
+        if tunnel == 'h2' and not env.curl_uses_lib('nghttp2'):
+            pytest.skip('only supported with nghttp2')
+        curl = CurlClient(env=env)
+        url = f'https://localhost:{env.https_port}/data.json'
+        proxy_args = curl.get_proxy_args(tunnel=True, proto=tunnel)
+        r1 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=proxy_args)
+        r1.check_response(count=1, http_status=200)
+        assert self.get_tunnel_proto_used(r1) == 'HTTP/2' \
+            if tunnel == 'h2' else 'HTTP/1.1'
+        # get the args, duplicate separated with '--next'
+        x2_args = r1.args[1:]
+        x2_args.append('--next')
+        x2_args.extend(proxy_args)
+        r2 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=x2_args)
+        r2.check_response(count=2, http_status=200)
+        assert r2.total_connects == 1
+
+    @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason=f"curl without SSL")
+    @pytest.mark.parametrize("tunnel", ['http/1.1', 'h2'])
+    @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx available")
+    @pytest.mark.skipif(condition=not Env.curl_uses_lib('openssl'), reason="tls13-ciphers not supported")
+    def test_10_11_noreuse_proxy_https(self, env: Env, httpd, nghttpx_fwd, tunnel, repeat):
+        # different --proxy-tls13-ciphers, no reuse of connection for https:
+        curl = CurlClient(env=env)
+        if tunnel == 'h2' and not env.curl_uses_lib('nghttp2'):
+            pytest.skip('only supported with nghttp2')
+        url = f'https://localhost:{env.https_port}/data.json'
+        proxy_args = curl.get_proxy_args(tunnel=True, proto=tunnel)
+        r1 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=proxy_args)
+        r1.check_response(count=1, http_status=200)
+        assert self.get_tunnel_proto_used(r1) == 'HTTP/2' \
+            if tunnel == 'h2' else 'HTTP/1.1'
+        # get the args, duplicate separated with '--next'
+        x2_args = r1.args[1:]
+        x2_args.append('--next')
+        x2_args.extend(proxy_args)
+        x2_args.extend(['--proxy-tls13-ciphers', 'TLS_AES_128_GCM_SHA256'])
+        r2 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=x2_args)
+        r2.check_response(count=2, http_status=200)
+        assert r2.total_connects == 2
+
+    @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason=f"curl without SSL")
+    @pytest.mark.parametrize("tunnel", ['http/1.1', 'h2'])
+    @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx available")
+    @pytest.mark.skipif(condition=not Env.curl_uses_lib('openssl'), reason="tls13-ciphers not supported")
+    def test_10_12_noreuse_proxy_http(self, env: Env, httpd, nghttpx_fwd, tunnel, repeat):
+        # different --proxy-tls13-ciphers, no reuse of connection for http:
+        if tunnel == 'h2' and not env.curl_uses_lib('nghttp2'):
+            pytest.skip('only supported with nghttp2')
+        curl = CurlClient(env=env)
+        url = f'http://localhost:{env.http_port}/data.json'
+        proxy_args = curl.get_proxy_args(tunnel=True, proto=tunnel)
+        r1 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=proxy_args)
+        r1.check_response(count=1, http_status=200)
+        assert self.get_tunnel_proto_used(r1) == 'HTTP/2' \
+            if tunnel == 'h2' else 'HTTP/1.1'
+        # get the args, duplicate separated with '--next'
+        x2_args = r1.args[1:]
+        x2_args.append('--next')
+        x2_args.extend(proxy_args)
+        x2_args.extend(['--proxy-tls13-ciphers', 'TLS_AES_128_GCM_SHA256'])
+        r2 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=x2_args)
+        r2.check_response(count=2, http_status=200)
+        assert r2.total_connects == 2
+
+    @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason=f"curl without SSL")
+    @pytest.mark.parametrize("tunnel", ['http/1.1', 'h2'])
+    @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx available")
+    @pytest.mark.skipif(condition=not Env.curl_uses_lib('openssl'), reason="tls13-ciphers not supported")
+    def test_10_13_noreuse_https(self, env: Env, httpd, nghttpx_fwd, tunnel, repeat):
+        # different --tls13-ciphers on https: same proxy config
+        if tunnel == 'h2' and not env.curl_uses_lib('nghttp2'):
+            pytest.skip('only supported with nghttp2')
+        curl = CurlClient(env=env)
+        url = f'https://localhost:{env.https_port}/data.json'
+        proxy_args = curl.get_proxy_args(tunnel=True, proto=tunnel)
+        r1 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=proxy_args)
+        r1.check_response(count=1, http_status=200)
+        assert self.get_tunnel_proto_used(r1) == 'HTTP/2' \
+            if tunnel == 'h2' else 'HTTP/1.1'
+        # get the args, duplicate separated with '--next'
+        x2_args = r1.args[1:]
+        x2_args.append('--next')
+        x2_args.extend(proxy_args)
+        x2_args.extend(['--tls13-ciphers', 'TLS_AES_128_GCM_SHA256'])
+        r2 = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True,
+                               extra_args=x2_args)
+        r2.check_response(count=2, http_status=200)
+        assert r2.total_connects == 2
index ed05b79db2f739d6bacd8ea73bcb00fa7b1a4441..bc5b4881c7320d817bf86ef386b34f6a4e2f8e24 100644 (file)
@@ -510,8 +510,14 @@ class CurlClient:
                 args.extend(['--trace-config', 'http/2,http/3,h2-proxy,h1-proxy'])
                 pass
 
+        active_options = options
+        if options is not None and '--next' in options:
+            active_options = options[options.index('--next') + 1:]
+
         for url in urls:
             u = urlparse(urls[0])
+            if options:
+                args.extend(options)
             if alpn_proto is not None:
                 if alpn_proto not in self.ALPN_ARG:
                     raise Exception(f'unknown ALPN protocol: "{alpn_proto}"')
@@ -521,7 +527,7 @@ class CurlClient:
                 pass
             elif insecure:
                 args.append('--insecure')
-            elif options and "--cacert" in options:
+            elif active_options and "--cacert" in active_options:
                 pass
             elif u.hostname:
                 args.extend(["--cacert", self.env.ca.cert_file])
@@ -532,8 +538,6 @@ class CurlClient:
                 args.extend(["--resolve", f"{u.hostname}:{port}:127.0.0.1"])
             if timeout is not None and int(timeout) > 0:
                 args.extend(["--connect-timeout", str(int(timeout))])
-            if options:
-                args.extend(options)
             args.append(url)
         return args