mirror of
https://github.com/curl/curl.git
synced 2026-06-02 00:14:18 +03:00
Restructure the code in cf-setup connect to make it better readable what is happening for establishing the connection's filter chain. Closes #21827
626 lines
23 KiB
Python
626 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# _ _ ____ _
|
|
# Project ___| | | | _ \| |
|
|
# / __| | | | |_) | |
|
|
# | (__| |_| | _ <| |___
|
|
# \___|\___/|_| \_\_____|
|
|
#
|
|
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
|
|
#
|
|
# This software is licensed as described in the file COPYING, which
|
|
# you should have received as part of this distribution. The terms
|
|
# are also available at https://curl.se/docs/copyright.html.
|
|
#
|
|
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
|
# copies of the Software, and permit persons to whom the Software is
|
|
# furnished to do so, under the terms of the COPYING file.
|
|
#
|
|
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
# KIND, either express or implied.
|
|
#
|
|
# SPDX-License-Identifier: curl
|
|
#
|
|
###########################################################################
|
|
#
|
|
import os
|
|
import subprocess
|
|
import time
|
|
|
|
import pytest
|
|
from testenv import CurlClient, Env
|
|
|
|
MARK_NEEDS_HTTPS_PROXY = pytest.mark.skipif(
|
|
condition=not Env.curl_has_feature("HTTPS-proxy"),
|
|
reason="curl lacks HTTPS-proxy support"
|
|
)
|
|
MARK_NEEDS_HTTP3 = pytest.mark.skipif(
|
|
condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
|
|
)
|
|
MARK_NEEDS_PROXY_HTTP3 = pytest.mark.skipif(
|
|
condition=not Env.curl_has_feature("proxy-HTTP3"),
|
|
reason="curl lacks experimental HTTP/3 proxy support"
|
|
)
|
|
MARK_NEEDS_NGHTTP3 = pytest.mark.skipif(
|
|
condition=not Env.curl_uses_lib("nghttp3"), reason="only supported with nghttp3"
|
|
)
|
|
MARK_NEEDS_NGHTTP2 = pytest.mark.skipif(
|
|
condition=not Env.curl_uses_lib("nghttp2"), reason="only supported with nghttp2"
|
|
)
|
|
MARK_NEEDS_H2O = pytest.mark.skipif(
|
|
condition=not Env.have_h2o(), reason="no h2o available"
|
|
)
|
|
MARK_NEEDS_NGHTTPX = pytest.mark.skipif(
|
|
condition=not Env.have_nghttpx(), reason="no nghttpx available"
|
|
)
|
|
|
|
UNSUPPORTED_OPT_MSG = "does not support this"
|
|
H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"'
|
|
|
|
|
|
def _require_available(**items):
|
|
missing = [name for name, value in items.items() if not value]
|
|
if missing:
|
|
pytest.skip(f"{' or '.join(missing)} not available")
|
|
|
|
|
|
def _download_path(curl: CurlClient) -> str:
|
|
return os.path.join(curl.run_dir, "download_#1.data")
|
|
|
|
|
|
def _check_download_message(curl: CurlClient, expected: str):
|
|
dpath = _download_path(curl)
|
|
assert os.path.exists(dpath), f"Download file not found: {dpath}"
|
|
with open(dpath, "r") as fd:
|
|
content = fd.read()
|
|
assert expected in content, f"Unexpected response content: {content}"
|
|
|
|
|
|
def _check_download_size(curl: CurlClient, expected_size: int):
|
|
dpath = _download_path(curl)
|
|
assert os.path.exists(dpath), f"Download file not found: {dpath}"
|
|
actual = os.path.getsize(dpath)
|
|
assert actual == expected_size, f"expected {expected_size}B download, got {actual}B"
|
|
|
|
|
|
def _nghttpx_proxy_args(
|
|
env: Env,
|
|
nghttpx,
|
|
nghttpx_fwd,
|
|
proxy_proto: str,
|
|
tunnel: bool,
|
|
):
|
|
port = env.pts_port(proxy_proto)
|
|
domain = env.proxy_domain
|
|
xxarg = None
|
|
if proxy_proto == "h3":
|
|
port = nghttpx.port
|
|
domain = env.domain1
|
|
xxarg = "--proxy-http3"
|
|
elif proxy_proto == "h2":
|
|
xxarg = "--proxy-http2"
|
|
|
|
xargs = [
|
|
"--proxy", f"https://{domain}:{port}/",
|
|
"--resolve", f"{domain}:{port}:127.0.0.1",
|
|
"--proxy-cacert", env.ca.cert_file
|
|
]
|
|
if xxarg:
|
|
xargs.append(xxarg)
|
|
if tunnel:
|
|
xargs.append("--proxytunnel")
|
|
return xargs
|
|
|
|
|
|
def _h2o_proxy_args(
|
|
env: Env,
|
|
h2o_proxy,
|
|
proxy_proto: str,
|
|
tunnel: bool,
|
|
):
|
|
pport = env.pts_port(proxy_proto, use_h2o=True)
|
|
xargs = [
|
|
"--proxy", f"https://{env.proxy_domain}:{pport}/",
|
|
"--resolve", f"{env.proxy_domain}:{pport}:127.0.0.1",
|
|
"--proxy-cacert", env.ca.cert_file,
|
|
"--cacert", env.ca.cert_file,
|
|
]
|
|
if proxy_proto == "h2":
|
|
xargs.append("--proxy-http2")
|
|
elif proxy_proto == "h3":
|
|
xargs.append("--proxy-http3")
|
|
|
|
if tunnel:
|
|
xargs.append("--proxytunnel")
|
|
|
|
return xargs
|
|
|
|
|
|
@MARK_NEEDS_HTTPS_PROXY
|
|
@MARK_NEEDS_HTTP3
|
|
@MARK_NEEDS_NGHTTP3
|
|
class TestH3Proxy:
|
|
|
|
# Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
@pytest.mark.parametrize(
|
|
["alpn_proto", "proxy_proto"],
|
|
[
|
|
pytest.param("http/1.1", "h3", id="h1_over_h3_proxytunnel"),
|
|
pytest.param(
|
|
"h2",
|
|
"h3",
|
|
marks=MARK_NEEDS_NGHTTP2,
|
|
id="h2_over_h3_proxytunnel",
|
|
),
|
|
pytest.param("h3", "h3", id="h3_over_h3_proxytunnel"),
|
|
pytest.param(
|
|
"h3",
|
|
"h2",
|
|
marks=MARK_NEEDS_NGHTTP2,
|
|
id="h3_over_h2_proxytunnel",
|
|
),
|
|
pytest.param("h3", "http/1.1", id="h3_over_h1_proxytunnel"),
|
|
],
|
|
)
|
|
def test_60_01_connect_tunnel(
|
|
self,
|
|
env: Env,
|
|
h2o_server,
|
|
h2o_proxy,
|
|
alpn_proto,
|
|
proxy_proto,
|
|
):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
proxy_args = _h2o_proxy_args(
|
|
env, h2o_proxy, proxy_proto, tunnel=True
|
|
)
|
|
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
_check_download_message(curl, H2O_HELLO_MSG)
|
|
|
|
# Failure matrix when proxy side does not support requested mode.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_NGHTTPX
|
|
@pytest.mark.parametrize(
|
|
["alpn_proto", "proxy_proto", "exp_err"],
|
|
[
|
|
pytest.param(
|
|
"http/1.1",
|
|
"h3",
|
|
"could not connect to server",
|
|
id="fail_h1_over_h3_proxytunnel",
|
|
),
|
|
pytest.param(
|
|
"h2",
|
|
"h3",
|
|
"could not connect to server",
|
|
marks=MARK_NEEDS_NGHTTP2,
|
|
id="fail_h2_over_h3_proxytunnel",
|
|
),
|
|
pytest.param(
|
|
"h3",
|
|
"h3",
|
|
"could not connect to server",
|
|
id="fail_h3_over_h3_proxytunnel",
|
|
),
|
|
#pytest.param(
|
|
# "h3",
|
|
# "h2",
|
|
# "proxy closed connection",
|
|
# marks=MARK_NEEDS_NGHTTP2,
|
|
# id="fail_h3_over_h2_proxytunnel",
|
|
#),
|
|
pytest.param(
|
|
"h3",
|
|
"http/1.1",
|
|
"connect-udp tunnel failed",
|
|
id="fail_h3_over_h1_proxytunnel",
|
|
),
|
|
],
|
|
)
|
|
def test_60_02_connect_tunnel_fail(
|
|
self,
|
|
env: Env,
|
|
httpd,
|
|
nghttpx,
|
|
nghttpx_fwd,
|
|
h2o_proxy,
|
|
alpn_proto,
|
|
proxy_proto,
|
|
exp_err,
|
|
):
|
|
_require_available(httpd=httpd, nghttpx=nghttpx, nghttpx_fwd=nghttpx_fwd,
|
|
h2o_proxy=h2o_proxy)
|
|
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{env.https_port}/data.json"
|
|
proxy_args = _nghttpx_proxy_args(
|
|
env, nghttpx, nghttpx_fwd, proxy_proto, tunnel=True
|
|
)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args
|
|
)
|
|
assert r.exit_code != 0, f"Expected failure but curl succeeded: {r.dump_logs()}"
|
|
assert exp_err in r.stderr.lower(), (
|
|
f"Expected protocol/proxy error but got: {r.dump_logs()}"
|
|
)
|
|
|
|
# Behavior checks for tunnel vs non-tunnel proxy mode selection.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_NGHTTPX
|
|
@pytest.mark.parametrize(
|
|
["proxy_proto"],
|
|
[
|
|
#pytest.param("h3", id="proxy_h3"),
|
|
pytest.param("h2", marks=MARK_NEEDS_NGHTTP2, id="proxy_h2"),
|
|
pytest.param("http/1.1", id="proxy_h1"),
|
|
],
|
|
)
|
|
def test_60_03_h3_target_auto_connect_udp(
|
|
self, env: Env, httpd, nghttpx, nghttpx_fwd, proxy_proto
|
|
):
|
|
_require_available(
|
|
httpd=httpd, nghttpx=nghttpx, nghttpx_fwd=nghttpx_fwd
|
|
)
|
|
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{httpd.ports['https']}/data.json"
|
|
proxy_args = _nghttpx_proxy_args(
|
|
env, nghttpx, nghttpx_fwd, proxy_proto, tunnel=False
|
|
)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
|
|
)
|
|
|
|
# An HTTP/3 target auto-triggers CONNECT-UDP even without --proxytunnel,
|
|
# just as HTTPS targets auto-trigger CONNECT. nghttpx does not support
|
|
# CONNECT-UDP so this fails, which confirms auto-CONNECT-UDP is active.
|
|
assert r.exit_code != 0, (
|
|
"expected failure: h3 target auto-triggers CONNECT-UDP "
|
|
"which nghttpx does not support"
|
|
)
|
|
assert "connect-udp" in r.stderr.lower(), (
|
|
f"expected CONNECT-UDP attempt in output, got: {r.dump_logs()}"
|
|
)
|
|
|
|
# Guard checks for unsupported HTTP/3 proxy options.
|
|
|
|
@pytest.mark.skipif(
|
|
condition=Env.curl_has_feature("proxy-HTTP3"), reason="curl has h3 proxy support"
|
|
)
|
|
def test_60_04_guard_proxy_http3_unsupported(self, env: Env, httpd):
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{httpd.ports['https']}/data.json"
|
|
proxy_args = [
|
|
"--proxy",
|
|
"https://127.0.0.1:1/",
|
|
"--proxy-http3",
|
|
"--proxytunnel",
|
|
"--cacert",
|
|
env.ca.cert_file,
|
|
]
|
|
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_exit_code(2)
|
|
assert UNSUPPORTED_OPT_MSG in r.stderr.lower(), (
|
|
f"Expected unsupported option failure but got: {r.stderr}"
|
|
)
|
|
|
|
# Robustness checks for shutdown and proxy loss during transfer.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_05_graceful_shutdown(self, env: Env, h2o_server, h2o_proxy):
|
|
if not env.curl_is_debug():
|
|
pytest.skip("needs debug curl for shutdown trace lines")
|
|
if not env.curl_is_verbose():
|
|
pytest.skip("needs verbose-strings curl build")
|
|
|
|
curl = CurlClient(env=env, run_env={"CURL_DEBUG": "all"})
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
proxy_args = curl.get_proxy_args(proto="h3", tunnel=True)
|
|
proxy_args.extend(["--cacert", env.ca.cert_file, "--insecure"])
|
|
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
|
|
shutdown_lines = [
|
|
line
|
|
for line in r.trace_lines
|
|
if ("start shutdown(" in line.lower())
|
|
or ("shutdown completely sent off" in line.lower())
|
|
]
|
|
assert shutdown_lines, f"No shutdown trace lines found:\n{r.stderr}"
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_06_proxy_drop_mid_transfer(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
|
|
env.make_data_file(indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024)
|
|
proxy_port = h2o_proxy.port
|
|
url = f"https://localhost:{h2o_server.port}/proxy-drop-20m"
|
|
out_path = os.path.join(env.gen_dir, "proxy-drop.out")
|
|
if os.path.exists(out_path):
|
|
os.remove(out_path)
|
|
args = [
|
|
env.curl,
|
|
"--http1.1",
|
|
"--proxy", f"https://{env.proxy_domain}:{proxy_port}/",
|
|
"--resolve", f"{env.proxy_domain}:{proxy_port}:127.0.0.1",
|
|
"--proxy-http3",
|
|
"--proxytunnel",
|
|
"--proxy-cacert", env.ca.cert_file,
|
|
"--cacert", env.ca.cert_file,
|
|
"--limit-rate", "10k",
|
|
"--max-time", "20",
|
|
"-o", out_path,
|
|
"-v",
|
|
url,
|
|
]
|
|
|
|
proc = None
|
|
try:
|
|
proc = subprocess.Popen(
|
|
args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
)
|
|
while not os.path.exists(out_path):
|
|
time.sleep(0.1)
|
|
assert h2o_proxy.kill(), "failed to stop h2o proxy"
|
|
_, stderr = proc.communicate(timeout=30)
|
|
assert proc.returncode != 0, (
|
|
"curl should fail when proxy is terminated mid-transfer"
|
|
)
|
|
assert proc.returncode == 56, f'{stderr}'
|
|
finally:
|
|
if proc and (proc.poll() is None):
|
|
proc.kill()
|
|
proc.wait(timeout=5)
|
|
assert h2o_proxy.start(), "failed to restart h2o proxy"
|
|
|
|
# Large file transfers and multiplexing through HTTP/3 proxy.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_07_large_download(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/download-10m"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
_check_download_size(curl, 10 * 1024 * 1024)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_08_large_upload(self, env: Env, httpd, h2o_server, h2o_proxy):
|
|
_require_available(h2o_proxy=h2o_proxy)
|
|
env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024)
|
|
fdata = os.path.join(env.gen_dir, "upload-2m")
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
r = curl.http_upload(
|
|
urls=[url],
|
|
data=f"@{fdata}",
|
|
alpn_proto="http/1.1",
|
|
with_stats=True,
|
|
extra_args=proxy_args,
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_09_parallel_downloads(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
|
|
count = 5
|
|
curl = CurlClient(env=env)
|
|
urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
proxy_args.extend(["--parallel", "--parallel-max", f"{count}"])
|
|
r = curl.http_download(
|
|
urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=count, http_status=200)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_10_proxy_basic_auth(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
proxy_args.extend(["--proxy-user", "testuser:testpass"])
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
_check_download_message(curl, H2O_HELLO_MSG)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_11_connection_reuse(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
curl = CurlClient(env=env)
|
|
urln = f"https://localhost:{h2o_server.port}/data.json?[0-2]"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
r = curl.http_download(
|
|
urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=3, http_status=200)
|
|
assert r.total_connects <= 3, (
|
|
f"expected proxy connection reuse, got {r.total_connects} connects"
|
|
)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
@pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'),
|
|
reason='curl lacks SSL session export support')
|
|
def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
xargs = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
session_file = os.path.join(env.gen_dir, 'test_60_12.sessions')
|
|
if os.path.exists(session_file):
|
|
os.remove(session_file)
|
|
xargs.extend(['--ssl-sessions', session_file])
|
|
# First request establishes QUIC session
|
|
r1 = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs
|
|
)
|
|
r1.check_response(count=1, http_status=200)
|
|
xargs.extend(['--trace-config', 'ssls'])
|
|
r2 = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs
|
|
)
|
|
r2.check_response(count=1, http_status=200)
|
|
reuses = [line for line in r2.trace_lines if '[SSLS] took session for proxy.http.curl.se' in line]
|
|
assert len(reuses), f'{r2.dump_logs()}'
|
|
|
|
# CONNECT-UDP tunnel payload size and capsule-protocol tests.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
@pytest.mark.parametrize(
|
|
"fname,fsize",
|
|
[
|
|
("download-1400", 1400),
|
|
("download-1m", 1 * 1024 * 1024),
|
|
("download-10m", 10 * 1024 * 1024),
|
|
],
|
|
)
|
|
def test_60_13_udp_tunnel_payload_sizes(
|
|
self, env: Env, h2o_server, h2o_proxy, fname, fsize
|
|
):
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
env.make_data_file(indir=h2o_server.docs_dir, fname=fname, fsize=fsize)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/{fname}"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
_check_download_size(curl, fsize)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_NGHTTPX
|
|
def test_60_14_udp_tunnel_capsule_absent(
|
|
self, env: Env, httpd, nghttpx, nghttpx_fwd
|
|
):
|
|
_require_available(
|
|
httpd=httpd, nghttpx=nghttpx, nghttpx_fwd=nghttpx_fwd
|
|
)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{httpd.ports['https']}/data.json"
|
|
proxy_args = _nghttpx_proxy_args(
|
|
env, nghttpx, nghttpx_fwd, "h3", tunnel=True
|
|
)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args
|
|
)
|
|
assert r.exit_code != 0, (
|
|
"expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol"
|
|
)
|
|
|
|
# Timeout and protocol-mismatch edge cases.
|
|
|
|
#@MARK_NEEDS_PROXY_HTTP3
|
|
#@MARK_NEEDS_H2O
|
|
#def test_60_15_connect_timeout(self, env: Env, h2o_proxy):
|
|
# _require_available(h2o_proxy=h2o_proxy)
|
|
# curl = CurlClient(env=env, timeout=15)
|
|
# url = f"https://localhost:{h2o_proxy.port}/data.json"
|
|
# # ipv6 0100::/64 is supposed to go into the void (rfc6666)
|
|
# xargs = [
|
|
# '--proxy', 'https://xxx.invalid/',
|
|
# '--resolve', 'xxx.invalid:443:0100::1,0100::2,0100::3',
|
|
# '--proxy-http3', '--proxytunnel',
|
|
# '--connect-timeout', '1',
|
|
# ]
|
|
# r = curl.http_download(
|
|
# urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs
|
|
# )
|
|
# r.check_exit_code(28) # CURLE_OPERATION_TIMEDOUT
|
|
# assert r.duration.total_seconds() < 10, (
|
|
# f"timeout not respected: took {r.duration.total_seconds():.1f}s"
|
|
# )
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
@MARK_NEEDS_NGHTTP2
|
|
def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy):
|
|
_require_available(httpd=httpd, h2o_proxy=h2o_proxy)
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{env.https_port}/data.json"
|
|
proxy_args = curl.get_proxy_args("h3", tunnel=True)
|
|
# h2 inner traffic always uses CONNECT (TCP), never CONNECT-UDP,
|
|
# even through an HTTP/3 proxy with --proxytunnel. h2o supports
|
|
# CONNECT TCP tunneling, so this request succeeds.
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="h2", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
|
|
# Verify that happy eyeballs is active for HTTP/3 proxy connections.
|
|
#
|
|
# With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address
|
|
# family selection to the proxy is done by happy eyeballs.
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
def test_60_17_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy):
|
|
"""Verbose trace confirms HAPPY-EYEBALLS filter is in the H3 proxy chain."""
|
|
if not env.curl_is_debug():
|
|
pytest.skip("needs debug curl for filter trace")
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
curl = CurlClient(env=env, run_env={"CURL_DEBUG": "HAPPY-EYEBALLS,H3-PROXY"})
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
r = curl.http_download(
|
|
urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args
|
|
)
|
|
r.check_response(count=1, http_status=200)
|
|
assert "happy-eyeballs" in r.stderr.lower(), (
|
|
f"expected HAPPY-EYEBALLS trace for H3 proxy, got: {r.stderr}"
|
|
)
|
|
|
|
@MARK_NEEDS_PROXY_HTTP3
|
|
@MARK_NEEDS_H2O
|
|
@MARK_NEEDS_NGHTTP2
|
|
def test_60_18_happy_eyeballs_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy):
|
|
"""IPv4-forced H3 proxy works for h1/h2/h3 inner protocols."""
|
|
_require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
|
|
for alpn_proto in ["http/1.1", "h2", "h3"]:
|
|
curl = CurlClient(env=env)
|
|
url = f"https://localhost:{h2o_server.port}/data.json"
|
|
proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
|
|
proxy_args.append("--ipv4")
|
|
r = curl.http_download(
|
|
urls=[url],
|
|
alpn_proto=alpn_proto,
|
|
with_stats=True,
|
|
extra_args=proxy_args,
|
|
)
|
|
r.check_response(count=1, http_status=200)
|