condition=not Env.have_nghttpx(), reason="no nghttpx available"
)
-H3_PROXY_COMMON_MARKS = [
- MARK_NEEDS_HTTPS_PROXY,
- MARK_NEEDS_HTTP3,
- MARK_NEEDS_PROXY_HTTP3,
- MARK_NEEDS_NGHTTP3,
-]
-
-NGTCP2_ONLY_MSG = "only supported with the ngtcp2 quic stack"
UNSUPPORTED_OPT_MSG = "does not support this"
H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"'
return xargs
-class TestH3ProxySuccess:
- """Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP."""
+@MARK_NEEDS_HTTPS_PROXY
+@MARK_NEEDS_HTTP3
+@MARK_NEEDS_NGHTTP3
+class TestH3Proxy:
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
+ # Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP.
+ @MARK_NEEDS_PROXY_HTTP3
+ @MARK_NEEDS_H2O
@pytest.mark.parametrize(
["alpn_proto", "proxy_proto"],
[
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.
-class TestH3ProxyFailure:
- """Failure matrix when proxy side does not support requested mode."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
-
+ @MARK_NEEDS_PROXY_HTTP3
+ @MARK_NEEDS_NGHTTPX
@pytest.mark.parametrize(
["alpn_proto", "proxy_proto", "exp_err"],
[
f"Expected protocol/proxy error but got: {r.dump_logs()}"
)
+ # Behavior checks for tunnel vs non-tunnel proxy mode selection.
-class TestH3ProxyModeSelection:
- """Behavior checks for tunnel vs non-tunnel proxy mode selection."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
-
+ @MARK_NEEDS_PROXY_HTTP3
+ @MARK_NEEDS_NGHTTPX
@pytest.mark.parametrize(
["proxy_proto"],
[
f"expected CONNECT-UDP attempt in output, got: {r.dump_logs()}"
)
-
-class TestH3ProxyRuntimeGuards:
- """Guard checks for unsupported HTTP/3 proxy options."""
-
- pytestmark = [
- MARK_NEEDS_HTTPS_PROXY,
- MARK_NEEDS_PROXY_HTTP3,
- pytest.mark.skipif(
- condition=Env.curl_uses_lib("ngtcp2"),
- reason="guard only applies to non-ngtcp2 builds",
- ),
- ]
+ # Guard checks for unsupported HTTP/3 proxy options.
- @pytest.mark.skipif(
- condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
- )
@pytest.mark.skipif(
condition=Env.curl_has_feature("proxy-HTTP3"), reason="curl has h3 proxy support"
)
f"Expected unsupported option failure but got: {r.stderr}"
)
+ # Robustness checks for shutdown and proxy loss during transfer.
-class TestH3ProxyRobustness:
- """Robustness checks for shutdown and proxy loss during transfer."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
- @pytest.fixture(autouse=True, scope="class")
- def _class_scope(self, env, h2o_server):
- if not env.have_h2o():
- pytest.skip("h2o not available")
- env.make_data_file(
- indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024
- )
-
- def test_60_05_graceful_shutdown(
- self, env: Env, h2o_server, h2o_proxy
- ):
+ @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():
]
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")
proc.wait(timeout=5)
assert h2o_proxy.start(), "failed to restart h2o proxy"
+ # Large file transfers and multiplexing through HTTP/3 proxy.
-class TestH3ProxyDataTransfer:
- """Large file transfers and multiplexing through HTTP/3 proxy."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
- @pytest.fixture(autouse=True, scope="class")
- def _class_scope(self, env, h2o_server):
- if not env.have_h2o():
- pytest.skip("h2o not available")
- env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
- env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
- env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024)
-
+ @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.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]"
)
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}]"
)
r.check_response(count=count, http_status=200)
-
-class TestH3ProxyConnectionManagement:
- """Proxy authentication, connection reuse, and session resumption."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
+ @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)
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)
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):
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.
-class TestH3ProxyUdpTunnel:
- """CONNECT-UDP tunnel payload size and capsule-protocol tests."""
-
- pytestmark = H3_PROXY_COMMON_MARKS
-
- @pytest.fixture(autouse=True, scope="class")
- def _class_scope(self, env, h2o_server):
- if not env.have_h2o():
- return
- env.make_data_file(indir=h2o_server.docs_dir, fname="download-1400", fsize=1400)
- env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
- env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
-
+ @MARK_NEEDS_PROXY_HTTP3
@MARK_NEEDS_H2O
@pytest.mark.parametrize(
"fname,fsize",
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.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
"expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol"
)
+ # Timeout and protocol-mismatch edge cases.
-class TestH3ProxyEdgeCases:
- """Timeout and protocol-mismatch edge cases."""
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
+ #@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)
# 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)
)
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.
-class TestH3ProxyHappyEyeballs:
- """
- 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.
- """
-
- pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
- def test_60_17_h3_proxy_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy):
+ @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")
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_h3_proxy_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy):
+ 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"]: