Path: blob/main/external/curl/tests/http/test_02_download.py
2659 views
#!/usr/bin/env python31# -*- coding: utf-8 -*-2#***************************************************************************3# _ _ ____ _4# Project ___| | | | _ \| |5# / __| | | | |_) | |6# | (__| |_| | _ <| |___7# \___|\___/|_| \_\_____|8#9# Copyright (C) Daniel Stenberg, <[email protected]>, et al.10#11# This software is licensed as described in the file COPYING, which12# you should have received as part of this distribution. The terms13# are also available at https://curl.se/docs/copyright.html.14#15# You may opt to use, copy, modify, merge, publish, distribute and/or sell16# copies of the Software, and permit persons to whom the Software is17# furnished to do so, under the terms of the COPYING file.18#19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY20# KIND, either express or implied.21#22# SPDX-License-Identifier: curl23#24###########################################################################25#26import difflib27import filecmp28import logging29import math30import os31import re32import sys33from datetime import timedelta34import pytest3536from testenv import Env, CurlClient, LocalClient373839log = logging.getLogger(__name__)404142class TestDownload:4344@pytest.fixture(autouse=True, scope='class')45def _class_scope(self, env, httpd):46indir = httpd.docs_dir47env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)48env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)49env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)50env.make_data_file(indir=indir, fname="data-10m", fsize=10*1024*1024)51env.make_data_file(indir=indir, fname="data-50m", fsize=50*1024*1024)52env.make_data_gzipbomb(indir=indir, fname="bomb-100m.txt", fsize=100*1024*1024)5354# download 1 file55@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])56def test_02_01_download_1(self, env: Env, httpd, nghttpx, proto):57if proto == 'h3' and not env.have_h3():58pytest.skip("h3 not supported")59curl = CurlClient(env=env)60url = f'https://{env.authority_for(env.domain1, proto)}/data.json'61r = curl.http_download(urls=[url], alpn_proto=proto)62r.check_response(http_status=200)6364# download 2 files65@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])66def test_02_02_download_2(self, env: Env, httpd, nghttpx, proto):67if proto == 'h3' and not env.have_h3():68pytest.skip("h3 not supported")69curl = CurlClient(env=env)70url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'71r = curl.http_download(urls=[url], alpn_proto=proto)72r.check_response(http_status=200, count=2)7374# download 100 files sequentially75@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])76def test_02_03_download_sequential(self, env: Env, httpd, nghttpx, proto):77if proto == 'h3' and not env.have_h3():78pytest.skip("h3 not supported")79if (proto == 'http/1.1' or proto == 'h2') and env.curl_uses_lib('mbedtls') and \80sys.platform.startswith('darwin') and env.ci_run:81pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')82count = 1083curl = CurlClient(env=env)84urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'85r = curl.http_download(urls=[urln], alpn_proto=proto)86r.check_response(http_status=200, count=count, connect_count=1)8788# download 100 files parallel89@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])90def test_02_04_download_parallel(self, env: Env, httpd, nghttpx, proto):91if proto == 'h3' and not env.have_h3():92pytest.skip("h3 not supported")93if proto == 'h2' and env.curl_uses_lib('mbedtls') and \94sys.platform.startswith('darwin') and env.ci_run:95pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')96count = 1097max_parallel = 598curl = CurlClient(env=env)99urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'100r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[101'--parallel', '--parallel-max', f'{max_parallel}'102])103r.check_response(http_status=200, count=count)104if proto == 'http/1.1':105# http/1.1 parallel transfers will open multiple connections106assert r.total_connects > 1, r.dump_logs()107else:108# http2 parallel transfers will use one connection (common limit is 100)109assert r.total_connects == 1, r.dump_logs()110111# download 500 files sequential112@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])113def test_02_05_download_many_sequential(self, env: Env, httpd, nghttpx, proto):114if proto == 'h3' and not env.have_h3():115pytest.skip("h3 not supported")116if proto == 'h2' and env.curl_uses_lib('mbedtls') and \117sys.platform.startswith('darwin') and env.ci_run:118pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')119count = 200120curl = CurlClient(env=env)121urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'122r = curl.http_download(urls=[urln], alpn_proto=proto)123r.check_response(http_status=200, count=count)124if proto == 'http/1.1':125# http/1.1 parallel transfers will open multiple connections126assert r.total_connects > 1, r.dump_logs()127else:128# http2 parallel transfers will use one connection (common limit is 100)129assert r.total_connects == 1, r.dump_logs()130131# download 500 files parallel132@pytest.mark.parametrize("proto", ['h2', 'h3'])133def test_02_06_download_many_parallel(self, env: Env, httpd, nghttpx, proto):134if proto == 'h3' and not env.have_h3():135pytest.skip("h3 not supported")136if proto == 'h2' and env.curl_uses_lib('mbedtls') and \137sys.platform.startswith('darwin') and env.ci_run:138pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')139count = 200140max_parallel = 50141curl = CurlClient(env=env)142urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[000-{count-1}]'143r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[144'--parallel', '--parallel-max', f'{max_parallel}'145])146r.check_response(http_status=200, count=count, connect_count=1)147148# download files parallel, check connection reuse/multiplex149@pytest.mark.parametrize("proto", ['h2', 'h3'])150def test_02_07_download_reuse(self, env: Env, httpd, nghttpx, proto):151if proto == 'h3' and not env.have_h3():152pytest.skip("h3 not supported")153count = 200154curl = CurlClient(env=env)155urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'156r = curl.http_download(urls=[urln], alpn_proto=proto,157with_stats=True, extra_args=[158'--parallel', '--parallel-max', '200'159])160r.check_response(http_status=200, count=count)161# should have used at most 2 connections only (test servers allow 100 req/conn)162# it may be just 1 on slow systems where request are answered faster than163# curl can exhaust the capacity or if curl runs with address-sanitizer speed164assert r.total_connects <= 2, "h2 should use fewer connections here"165166# download files parallel with http/1.1, check connection not reused167@pytest.mark.parametrize("proto", ['http/1.1'])168def test_02_07b_download_reuse(self, env: Env, httpd, nghttpx, proto):169count = 6170curl = CurlClient(env=env)171urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'172r = curl.http_download(urls=[urln], alpn_proto=proto,173with_stats=True, extra_args=[174'--parallel'175])176r.check_response(count=count, http_status=200)177# http/1.1 should have used count connections178assert r.total_connects == count, "http/1.1 should use this many connections"179180@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])181def test_02_08_1MB_serial(self, env: Env, httpd, nghttpx, proto):182if proto == 'h3' and not env.have_h3():183pytest.skip("h3 not supported")184count = 5185urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'186curl = CurlClient(env=env)187r = curl.http_download(urls=[urln], alpn_proto=proto)188r.check_response(count=count, http_status=200)189190@pytest.mark.parametrize("proto", ['h2', 'h3'])191def test_02_09_1MB_parallel(self, env: Env, httpd, nghttpx, proto):192if proto == 'h3' and not env.have_h3():193pytest.skip("h3 not supported")194count = 5195urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'196curl = CurlClient(env=env)197r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[198'--parallel'199])200r.check_response(count=count, http_status=200)201202@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")203@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")204@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])205def test_02_10_10MB_serial(self, env: Env, httpd, nghttpx, proto):206if proto == 'h3' and not env.have_h3():207pytest.skip("h3 not supported")208count = 3209urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'210curl = CurlClient(env=env)211r = curl.http_download(urls=[urln], alpn_proto=proto)212r.check_response(count=count, http_status=200)213214@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")215@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")216@pytest.mark.parametrize("proto", ['h2', 'h3'])217def test_02_11_10MB_parallel(self, env: Env, httpd, nghttpx, proto):218if proto == 'h3' and not env.have_h3():219pytest.skip("h3 not supported")220count = 3221urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'222curl = CurlClient(env=env)223r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[224'--parallel'225])226r.check_response(count=count, http_status=200)227228@pytest.mark.parametrize("proto", ['h2', 'h3'])229def test_02_12_head_serial_https(self, env: Env, httpd, nghttpx, proto):230if proto == 'h3' and not env.have_h3():231pytest.skip("h3 not supported")232count = 5233urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'234curl = CurlClient(env=env)235r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[236'--head'237])238r.check_response(count=count, http_status=200)239240@pytest.mark.parametrize("proto", ['h2'])241def test_02_13_head_serial_h2c(self, env: Env, httpd, nghttpx, proto):242if proto == 'h3' and not env.have_h3():243pytest.skip("h3 not supported")244count = 5245urln = f'http://{env.domain1}:{env.http_port}/data-10m?[0-{count-1}]'246curl = CurlClient(env=env)247r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[248'--head', '--http2-prior-knowledge', '--fail-early'249])250r.check_response(count=count, http_status=200)251252@pytest.mark.parametrize("proto", ['h2', 'h3'])253def test_02_14_not_found(self, env: Env, httpd, nghttpx, proto):254if proto == 'h3' and not env.have_h3():255pytest.skip("h3 not supported")256count = 5257urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'258curl = CurlClient(env=env)259r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[260'--parallel'261])262r.check_stats(count=count, http_status=404, exitcode=0,263remote_port=env.port_for(alpn_proto=proto),264remote_ip='127.0.0.1')265266@pytest.mark.parametrize("proto", ['h2', 'h3'])267def test_02_15_fail_not_found(self, env: Env, httpd, nghttpx, proto):268if proto == 'h3' and not env.have_h3():269pytest.skip("h3 not supported")270count = 5271urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'272curl = CurlClient(env=env)273r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[274'--fail'275])276r.check_stats(count=count, http_status=404, exitcode=22,277remote_port=env.port_for(alpn_proto=proto),278remote_ip='127.0.0.1')279280@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")281def test_02_20_h2_small_frames(self, env: Env, httpd, configures_httpd):282# Test case to reproduce content corruption as observed in283# https://github.com/curl/curl/issues/10525284# To reliably reproduce, we need an Apache httpd that supports285# setting smaller frame sizes. This is not released yet, we286# test if it works and back out if not.287httpd.set_extra_config(env.domain1, lines=[288'H2MaxDataFrameLen 1024',289])290if not httpd.reload_if_config_changed():291pytest.skip('H2MaxDataFrameLen not supported')292# ok, make 100 downloads with 2 parallel running and they293# are expected to stumble into the issue when using `lib/http2.c`294# from curl 7.88.0295count = 5296urln = f'https://{env.authority_for(env.domain1, "h2")}/data-1m?[0-{count-1}]'297curl = CurlClient(env=env)298r = curl.http_download(urls=[urln], alpn_proto="h2", extra_args=[299'--parallel', '--parallel-max', '2'300])301r.check_response(count=count, http_status=200)302srcfile = os.path.join(httpd.docs_dir, 'data-1m')303self.check_downloads(curl, srcfile, count)304305# download serial via lib client, pause/resume at different offsets306@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])307@pytest.mark.parametrize("proto", ['http/1.1', 'h3'])308def test_02_21_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset):309if proto == 'h3' and not env.have_h3():310pytest.skip("h3 not supported")311count = 2312docname = 'data-10m'313url = f'https://localhost:{env.https_port}/{docname}'314client = LocalClient(name='cli_hx_download', env=env)315if not client.exists():316pytest.skip(f'example client not built: {client.name}')317r = client.run(args=[318'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url319])320r.check_exit_code(0)321srcfile = os.path.join(httpd.docs_dir, docname)322self.check_downloads(client, srcfile, count)323324# h2 download parallel via lib client, pause/resume at different offsets325# debug-override stream window size to reproduce #16955326@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])327@pytest.mark.parametrize("swin_max", [0, 10*1024])328def test_02_21_h2_lib_serial(self, env: Env, httpd, pause_offset, swin_max):329proto = 'h2'330count = 2331docname = 'data-10m'332url = f'https://localhost:{env.https_port}/{docname}'333run_env = os.environ.copy()334run_env['CURL_DEBUG'] = 'multi,http/2'335if swin_max > 0:336run_env['CURL_H2_STREAM_WIN_MAX'] = f'{swin_max}'337client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)338if not client.exists():339pytest.skip(f'example client not built: {client.name}')340r = client.run(args=[341'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url342])343r.check_exit_code(0)344srcfile = os.path.join(httpd.docs_dir, docname)345self.check_downloads(client, srcfile, count)346347# download via lib client, several at a time, pause/resume348@pytest.mark.parametrize("pause_offset", [100*1023])349@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])350def test_02_22_lib_parallel_resume(self, env: Env, httpd, nghttpx, proto, pause_offset):351if proto == 'h3' and not env.have_h3():352pytest.skip("h3 not supported")353count = 2354max_parallel = 5355docname = 'data-10m'356url = f'https://localhost:{env.https_port}/{docname}'357client = LocalClient(name='cli_hx_download', env=env)358if not client.exists():359pytest.skip(f'example client not built: {client.name}')360r = client.run(args=[361'-n', f'{count}', '-m', f'{max_parallel}',362'-P', f'{pause_offset}', '-V', proto, url363])364r.check_exit_code(0)365srcfile = os.path.join(httpd.docs_dir, docname)366self.check_downloads(client, srcfile, count)367368# download, several at a time, pause and abort paused369@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])370def test_02_23a_lib_abort_paused(self, env: Env, httpd, nghttpx, proto):371if proto == 'h3' and not env.have_h3():372pytest.skip("h3 not supported")373if proto == 'h3' and env.curl_uses_ossl_quic():374pytest.skip('OpenSSL QUIC fails here')375if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):376pytest.skip("fails in CI, but works locally for unknown reasons")377count = 10378max_parallel = 5379if proto in ['h2', 'h3']:380pause_offset = 64 * 1024381else:382pause_offset = 12 * 1024383docname = 'data-1m'384url = f'https://localhost:{env.https_port}/{docname}'385client = LocalClient(name='cli_hx_download', env=env)386if not client.exists():387pytest.skip(f'example client not built: {client.name}')388r = client.run(args=[389'-n', f'{count}', '-m', f'{max_parallel}', '-a',390'-P', f'{pause_offset}', '-V', proto, url391])392r.check_exit_code(0)393srcfile = os.path.join(httpd.docs_dir, docname)394# downloads should be there, but not necessarily complete395self.check_downloads(client, srcfile, count, complete=False)396397# download, several at a time, abort after n bytes398@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])399def test_02_23b_lib_abort_offset(self, env: Env, httpd, nghttpx, proto):400if proto == 'h3' and not env.have_h3():401pytest.skip("h3 not supported")402if proto == 'h3' and env.curl_uses_ossl_quic():403pytest.skip('OpenSSL QUIC fails here')404if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):405pytest.skip("fails in CI, but works locally for unknown reasons")406count = 10407max_parallel = 5408if proto in ['h2', 'h3']:409abort_offset = 64 * 1024410else:411abort_offset = 12 * 1024412docname = 'data-1m'413url = f'https://localhost:{env.https_port}/{docname}'414client = LocalClient(name='cli_hx_download', env=env)415if not client.exists():416pytest.skip(f'example client not built: {client.name}')417r = client.run(args=[418'-n', f'{count}', '-m', f'{max_parallel}', '-a',419'-A', f'{abort_offset}', '-V', proto, url420])421r.check_exit_code(42) # CURLE_ABORTED_BY_CALLBACK422srcfile = os.path.join(httpd.docs_dir, docname)423# downloads should be there, but not necessarily complete424self.check_downloads(client, srcfile, count, complete=False)425426# download, several at a time, abort after n bytes427@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])428def test_02_23c_lib_fail_offset(self, env: Env, httpd, nghttpx, proto):429if proto == 'h3' and not env.have_h3():430pytest.skip("h3 not supported")431if proto == 'h3' and env.curl_uses_ossl_quic():432pytest.skip('OpenSSL QUIC fails here')433if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):434pytest.skip("fails in CI, but works locally for unknown reasons")435count = 10436max_parallel = 5437if proto in ['h2', 'h3']:438fail_offset = 64 * 1024439else:440fail_offset = 12 * 1024441docname = 'data-1m'442url = f'https://localhost:{env.https_port}/{docname}'443client = LocalClient(name='cli_hx_download', env=env)444if not client.exists():445pytest.skip(f'example client not built: {client.name}')446r = client.run(args=[447'-n', f'{count}', '-m', f'{max_parallel}', '-a',448'-F', f'{fail_offset}', '-V', proto, url449])450r.check_exit_code(23) # CURLE_WRITE_ERROR451srcfile = os.path.join(httpd.docs_dir, docname)452# downloads should be there, but not necessarily complete453self.check_downloads(client, srcfile, count, complete=False)454455# speed limited download456@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])457def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto):458if proto == 'h3' and not env.have_h3():459pytest.skip("h3 not supported")460count = 1461url = f'https://{env.authority_for(env.domain1, proto)}/data-1m'462curl = CurlClient(env=env)463speed_limit = 384 * 1024464min_duration = math.floor((1024 * 1024)/speed_limit)465r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[466'--limit-rate', f'{speed_limit}'467])468r.check_response(count=count, http_status=200)469assert r.duration > timedelta(seconds=min_duration), \470f'rate limited transfer should take more than {min_duration}s, '\471f'not {r.duration}'472473# make extreme parallel h2 upgrades, check invalid conn reuse474# before protocol switch has happened475def test_02_25_h2_upgrade_x(self, env: Env, httpd):476url = f'http://localhost:{env.http_port}/data-100k'477client = LocalClient(name='cli_h2_upgrade_extreme', env=env, timeout=15)478if not client.exists():479pytest.skip(f'example client not built: {client.name}')480r = client.run(args=[url])481assert r.exit_code == 0, f'{client.dump_logs()}'482483# Special client that tests TLS session reuse in parallel transfers484# TODO: just uses a single connection for h2/h3. Not sure how to prevent that485@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])486def test_02_26_session_shared_reuse(self, env: Env, proto, httpd, nghttpx):487if proto == 'h3' and not env.have_h3():488pytest.skip("h3 not supported")489url = f'https://{env.authority_for(env.domain1, proto)}/data-100k'490client = LocalClient(name='cli_tls_session_reuse', env=env)491if not client.exists():492pytest.skip(f'example client not built: {client.name}')493r = client.run(args=[url, proto])494r.check_exit_code(0)495496# test on paused transfers, based on issue #11982497@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])498def test_02_27a_paused_no_cl(self, env: Env, httpd, nghttpx, proto):499if proto == 'h3' and not env.have_h3():500pytest.skip("h3 not supported")501url = f'https://{env.authority_for(env.domain1, proto)}' \502'/curltest/tweak/?&chunks=6&chunk_size=8000'503client = LocalClient(env=env, name='cli_h2_pausing')504r = client.run(args=['-V', proto, url])505r.check_exit_code(0)506507# test on paused transfers, based on issue #11982508@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])509def test_02_27b_paused_no_cl(self, env: Env, httpd, nghttpx, proto):510if proto == 'h3' and not env.have_h3():511pytest.skip("h3 not supported")512url = f'https://{env.authority_for(env.domain1, proto)}' \513'/curltest/tweak/?error=502'514client = LocalClient(env=env, name='cli_h2_pausing')515r = client.run(args=['-V', proto, url])516r.check_exit_code(0)517518# test on paused transfers, based on issue #11982519@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])520def test_02_27c_paused_no_cl(self, env: Env, httpd, nghttpx, proto):521if proto == 'h3' and not env.have_h3():522pytest.skip("h3 not supported")523url = f'https://{env.authority_for(env.domain1, proto)}' \524'/curltest/tweak/?status=200&chunks=1&chunk_size=100'525client = LocalClient(env=env, name='cli_h2_pausing')526r = client.run(args=['-V', proto, url])527r.check_exit_code(0)528529@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])530def test_02_28_get_compressed(self, env: Env, httpd, nghttpx, proto):531if proto == 'h3' and not env.have_h3():532pytest.skip("h3 not supported")533count = 1534urln = f'https://{env.authority_for(env.domain1brotli, proto)}/data-100k?[0-{count-1}]'535curl = CurlClient(env=env)536r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[537'--compressed'538])539r.check_exit_code(code=0)540r.check_response(count=count, http_status=200)541542def check_downloads(self, client, srcfile: str, count: int,543complete: bool = True):544for i in range(count):545dfile = client.download_file(i)546assert os.path.exists(dfile)547if complete and not filecmp.cmp(srcfile, dfile, shallow=False):548diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),549b=open(dfile).readlines(),550fromfile=srcfile,551tofile=dfile,552n=1))553assert False, f'download {dfile} differs:\n{diff}'554555# download via lib client, 1 at a time, pause/resume at different offsets556@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])557@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])558def test_02_29_h2_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset):559if proto == 'h3' and not env.have_h3():560pytest.skip("h3 not supported")561count = 2562docname = 'data-10m'563url = f'https://localhost:{env.https_port}/{docname}'564client = LocalClient(name='cli_hx_download', env=env)565if not client.exists():566pytest.skip(f'example client not built: {client.name}')567r = client.run(args=[568'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url569])570r.check_exit_code(0)571srcfile = os.path.join(httpd.docs_dir, docname)572self.check_downloads(client, srcfile, count)573574# download parallel with prior knowledge575def test_02_30_parallel_prior_knowledge(self, env: Env, httpd):576count = 3577curl = CurlClient(env=env)578urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'579r = curl.http_download(urls=[urln], extra_args=[580'--parallel', '--http2-prior-knowledge'581])582r.check_response(http_status=200, count=count)583assert r.total_connects == 1, r.dump_logs()584585# download parallel with h2 "Upgrade:"586def test_02_31_parallel_upgrade(self, env: Env, httpd, nghttpx):587count = 3588curl = CurlClient(env=env)589urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'590r = curl.http_download(urls=[urln], extra_args=[591'--parallel', '--http2'592])593r.check_response(http_status=200, count=count)594# we see up to 3 connections, because Apache wants to serve only a single595# request via Upgrade: and then closes the connection. But if a new596# request comes in time, it might still get served.597assert r.total_connects <= 3, r.dump_logs()598599# nghttpx is the only server we have that supports TLS early data600@pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx")601@pytest.mark.skipif(condition=not Env.curl_is_debug(), reason="needs curl debug")602@pytest.mark.skipif(condition=not Env.curl_is_verbose(), reason="needs curl verbose strings")603@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])604def test_02_32_earlydata(self, env: Env, httpd, nghttpx, proto):605if not env.curl_can_early_data():606pytest.skip('TLS earlydata not implemented')607if proto == 'h3' and \608(not env.have_h3() or not env.curl_can_h3_early_data()):609pytest.skip("h3 not supported")610if proto != 'h3' and sys.platform.startswith('darwin') and env.ci_run:611pytest.skip('failing on macOS CI runners')612if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('wolfssl'):613pytest.skip('h3 wolfssl early data failing on macOS')614if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('gnutls'):615pytest.skip('h3 gnutls early data failing on macOS')616count = 2617docname = 'data-10k'618# we want this test to always connect to nghttpx, since it is619# the only server we have that supports TLS earlydata620port = env.port_for(proto)621if proto != 'h3':622port = env.nghttpx_https_port623url = f'https://{env.domain1}:{port}/{docname}'624client = LocalClient(name='cli_hx_download', env=env)625if not client.exists():626pytest.skip(f'example client not built: {client.name}')627r = client.run(args=[628'-n', f'{count}',629'-e', # use TLS earlydata630'-f', # forbid reuse of connections631'-r', f'{env.domain1}:{port}:127.0.0.1',632'-V', proto, url633])634r.check_exit_code(0)635srcfile = os.path.join(httpd.docs_dir, docname)636self.check_downloads(client, srcfile, count)637# check that TLS earlydata worked as expected638earlydata = {}639reused_session = False640for line in r.trace_lines:641m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line)642if m:643earlydata[int(m.group(1))] = int(m.group(2))644continue645if re.match(r'\[1-1] \* SSL reusing session.*', line):646reused_session = True647assert reused_session, 'session was not reused for 2nd transfer'648assert earlydata[0] == 0, f'{earlydata}'649if proto == 'http/1.1':650assert earlydata[1] == 111, f'{earlydata}'651elif proto == 'h2':652assert earlydata[1] == 127, f'{earlydata}'653elif proto == 'h3':654assert earlydata[1] == 109, f'{earlydata}'655656@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])657@pytest.mark.parametrize("max_host_conns", [0, 1, 5])658def test_02_33_max_host_conns(self, env: Env, httpd, nghttpx, proto, max_host_conns):659if not env.curl_is_debug():660pytest.skip('only works for curl debug builds')661if not env.curl_is_verbose():662pytest.skip('only works for curl with verbose strings')663if proto == 'h3' and not env.have_h3():664pytest.skip("h3 not supported")665count = 50666max_parallel = 50667docname = 'data-10k'668port = env.port_for(proto)669url = f'https://{env.domain1}:{port}/{docname}'670run_env = os.environ.copy()671run_env['CURL_DEBUG'] = 'multi'672client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)673if not client.exists():674pytest.skip(f'example client not built: {client.name}')675r = client.run(args=[676'-n', f'{count}',677'-m', f'{max_parallel}',678'-x', # always use a fresh connection679'-M', str(max_host_conns), # limit conns per host680'-r', f'{env.domain1}:{port}:127.0.0.1',681'-V', proto, url682])683r.check_exit_code(0)684srcfile = os.path.join(httpd.docs_dir, docname)685self.check_downloads(client, srcfile, count)686if max_host_conns > 0:687matched_lines = 0688for line in r.trace_lines:689m = re.match(r'.*The cache now contains (\d+) members.*', line)690if m:691matched_lines += 1692n = int(m.group(1))693assert n <= max_host_conns694assert matched_lines > 0695696@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])697@pytest.mark.parametrize("max_total_conns", [0, 1, 5])698def test_02_34_max_total_conns(self, env: Env, httpd, nghttpx, proto, max_total_conns):699if not env.curl_is_debug():700pytest.skip('only works for curl debug builds')701if not env.curl_is_verbose():702pytest.skip('only works for curl with verbose strings')703if proto == 'h3' and not env.have_h3():704pytest.skip("h3 not supported")705count = 50706max_parallel = 50707docname = 'data-10k'708port = env.port_for(proto)709url = f'https://{env.domain1}:{port}/{docname}'710run_env = os.environ.copy()711run_env['CURL_DEBUG'] = 'multi'712client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)713if not client.exists():714pytest.skip(f'example client not built: {client.name}')715r = client.run(args=[716'-n', f'{count}',717'-m', f'{max_parallel}',718'-x', # always use a fresh connection719'-T', str(max_total_conns), # limit total connections720'-r', f'{env.domain1}:{port}:127.0.0.1',721'-V', proto, url722])723r.check_exit_code(0)724srcfile = os.path.join(httpd.docs_dir, docname)725self.check_downloads(client, srcfile, count)726if max_total_conns > 0:727matched_lines = 0728for line in r.trace_lines:729m = re.match(r'.*The cache now contains (\d+) members.*', line)730if m:731matched_lines += 1732n = int(m.group(1))733assert n <= max_total_conns734assert matched_lines > 0735736# 2 parallel transers, pause and resume. Load a 100 MB zip bomb from737# the server with "Content-Encoding: gzip" that gets exloded during738# response writing to the client. Client pauses after 1MB unzipped data739# and causes buffers to fill while the server sends more response740# data.741# * http/1.1: not much buffering is done as curl does no longer742# serve the connections that are paused743# * h2/h3: server continues sending what the stream window allows and744# since the one connection involved unpaused transfers, data continues745# to be received, requiring buffering.746@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])747def test_02_35_pause_bomb(self, env: Env, httpd, nghttpx, proto):748if proto == 'h3' and not env.have_h3():749pytest.skip("h3 not supported")750count = 2751pause_offset = 1024 * 1024752docname = 'bomb-100m.txt.var'753url = f'https://localhost:{env.https_port}/{docname}'754client = LocalClient(name='cli_hx_download', env=env)755if not client.exists():756pytest.skip(f'example client not built: {client.name}')757r = client.run(args=[758'-n', f'{count}', '-m', f'{count}',759'-P', f'{pause_offset}', '-V', proto, url760])761r.check_exit_code(0)762763# download with looong urls764@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])765@pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024, 80*1024, 96*1024])766def test_02_36_looong_urls(self, env: Env, httpd, nghttpx, proto, url_junk):767if proto == 'h3' and not env.have_h3():768pytest.skip("h3 not supported")769if proto == 'h3' and env.curl_uses_lib('quiche'):770pytest.skip("quiche fails from 16k onwards")771curl = CurlClient(env=env)772# url is longer than 'url_len'773url = f'https://{env.authority_for(env.domain1, proto)}/data.json?{"x"*(url_junk)}'774r = curl.http_download(urls=[url], alpn_proto=proto)775if url_junk <= 1024:776r.check_exit_code(0)777r.check_response(http_status=200)778elif url_junk <= 16*1024:779r.check_exit_code(0)780# server replies with 414, Request URL too long781r.check_response(http_status=414)782elif url_junk <= 32*1024:783r.check_exit_code(0)784# server replies with 414, Request URL too long785r.check_response(http_status=414)786else:787# with urls larger than 64k, behaviour differs788if proto == 'http/1.1':789r.check_exit_code(0)790r.check_response(http_status=414)791elif proto == 'h2':792# h2 is unable to send such large headers (frame limits)793r.check_exit_code(55)794elif proto == 'h3':795if url_junk <= 64*1024:796r.check_exit_code(0)797# nghttpx reports 431 Request Header Field too Large798r.check_response(http_status=431)799else:800# nghttpx destroys the connection with internal error801# ERR_QPACK_HEADER_TOO_LARGE802r.check_exit_code(56)803804805