Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/pkg
Path: blob/main/external/curl/tests/http/test_02_download.py
2066 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
#***************************************************************************
4
# _ _ ____ _
5
# Project ___| | | | _ \| |
6
# / __| | | | |_) | |
7
# | (__| |_| | _ <| |___
8
# \___|\___/|_| \_\_____|
9
#
10
# Copyright (C) Daniel Stenberg, <[email protected]>, et al.
11
#
12
# This software is licensed as described in the file COPYING, which
13
# you should have received as part of this distribution. The terms
14
# are also available at https://curl.se/docs/copyright.html.
15
#
16
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
17
# copies of the Software, and permit persons to whom the Software is
18
# furnished to do so, under the terms of the COPYING file.
19
#
20
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
21
# KIND, either express or implied.
22
#
23
# SPDX-License-Identifier: curl
24
#
25
###########################################################################
26
#
27
import difflib
28
import filecmp
29
import logging
30
import math
31
import os
32
import re
33
import sys
34
from datetime import timedelta
35
import pytest
36
37
from testenv import Env, CurlClient, LocalClient
38
39
40
log = logging.getLogger(__name__)
41
42
43
class TestDownload:
44
45
@pytest.fixture(autouse=True, scope='class')
46
def _class_scope(self, env, httpd):
47
indir = httpd.docs_dir
48
env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
49
env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
50
env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)
51
env.make_data_file(indir=indir, fname="data-10m", fsize=10*1024*1024)
52
env.make_data_file(indir=indir, fname="data-50m", fsize=50*1024*1024)
53
env.make_data_gzipbomb(indir=indir, fname="bomb-100m.txt", fsize=100*1024*1024)
54
55
# download 1 file
56
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
57
def test_02_01_download_1(self, env: Env, httpd, nghttpx, proto):
58
if proto == 'h3' and not env.have_h3():
59
pytest.skip("h3 not supported")
60
curl = CurlClient(env=env)
61
url = f'https://{env.authority_for(env.domain1, proto)}/data.json'
62
r = curl.http_download(urls=[url], alpn_proto=proto)
63
r.check_response(http_status=200)
64
65
# download 2 files
66
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
67
def test_02_02_download_2(self, env: Env, httpd, nghttpx, proto):
68
if proto == 'h3' and not env.have_h3():
69
pytest.skip("h3 not supported")
70
curl = CurlClient(env=env)
71
url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
72
r = curl.http_download(urls=[url], alpn_proto=proto)
73
r.check_response(http_status=200, count=2)
74
75
# download 100 files sequentially
76
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
77
def test_02_03_download_sequential(self, env: Env, httpd, nghttpx, proto):
78
if proto == 'h3' and not env.have_h3():
79
pytest.skip("h3 not supported")
80
if (proto == 'http/1.1' or proto == 'h2') and env.curl_uses_lib('mbedtls') and \
81
sys.platform.startswith('darwin') and env.ci_run:
82
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
83
count = 10
84
curl = CurlClient(env=env)
85
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
86
r = curl.http_download(urls=[urln], alpn_proto=proto)
87
r.check_response(http_status=200, count=count, connect_count=1)
88
89
# download 100 files parallel
90
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
91
def test_02_04_download_parallel(self, env: Env, httpd, nghttpx, proto):
92
if proto == 'h3' and not env.have_h3():
93
pytest.skip("h3 not supported")
94
if proto == 'h2' and env.curl_uses_lib('mbedtls') and \
95
sys.platform.startswith('darwin') and env.ci_run:
96
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
97
count = 10
98
max_parallel = 5
99
curl = CurlClient(env=env)
100
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
101
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
102
'--parallel', '--parallel-max', f'{max_parallel}'
103
])
104
r.check_response(http_status=200, count=count)
105
if proto == 'http/1.1':
106
# http/1.1 parallel transfers will open multiple connections
107
assert r.total_connects > 1, r.dump_logs()
108
else:
109
# http2 parallel transfers will use one connection (common limit is 100)
110
assert r.total_connects == 1, r.dump_logs()
111
112
# download 500 files sequential
113
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
114
def test_02_05_download_many_sequential(self, env: Env, httpd, nghttpx, proto):
115
if proto == 'h3' and not env.have_h3():
116
pytest.skip("h3 not supported")
117
if proto == 'h3' and env.curl_uses_lib('msh3'):
118
pytest.skip("msh3 shaky here")
119
if proto == 'h2' and env.curl_uses_lib('mbedtls') and \
120
sys.platform.startswith('darwin') and env.ci_run:
121
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
122
count = 200
123
curl = CurlClient(env=env)
124
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
125
r = curl.http_download(urls=[urln], alpn_proto=proto)
126
r.check_response(http_status=200, count=count)
127
if proto == 'http/1.1':
128
# http/1.1 parallel transfers will open multiple connections
129
assert r.total_connects > 1, r.dump_logs()
130
else:
131
# http2 parallel transfers will use one connection (common limit is 100)
132
assert r.total_connects == 1, r.dump_logs()
133
134
# download 500 files parallel
135
@pytest.mark.parametrize("proto", ['h2', 'h3'])
136
def test_02_06_download_many_parallel(self, env: Env, httpd, nghttpx, proto):
137
if proto == 'h3' and not env.have_h3():
138
pytest.skip("h3 not supported")
139
if proto == 'h2' and env.curl_uses_lib('mbedtls') and \
140
sys.platform.startswith('darwin') and env.ci_run:
141
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
142
count = 200
143
max_parallel = 50
144
curl = CurlClient(env=env)
145
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[000-{count-1}]'
146
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
147
'--parallel', '--parallel-max', f'{max_parallel}'
148
])
149
r.check_response(http_status=200, count=count, connect_count=1)
150
151
# download files parallel, check connection reuse/multiplex
152
@pytest.mark.parametrize("proto", ['h2', 'h3'])
153
def test_02_07_download_reuse(self, env: Env, httpd, nghttpx, proto):
154
if proto == 'h3' and not env.have_h3():
155
pytest.skip("h3 not supported")
156
count = 200
157
curl = CurlClient(env=env)
158
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
159
r = curl.http_download(urls=[urln], alpn_proto=proto,
160
with_stats=True, extra_args=[
161
'--parallel', '--parallel-max', '200'
162
])
163
r.check_response(http_status=200, count=count)
164
# should have used at most 2 connections only (test servers allow 100 req/conn)
165
# it may be just 1 on slow systems where request are answered faster than
166
# curl can exhaust the capacity or if curl runs with address-sanitizer speed
167
assert r.total_connects <= 2, "h2 should use fewer connections here"
168
169
# download files parallel with http/1.1, check connection not reused
170
@pytest.mark.parametrize("proto", ['http/1.1'])
171
def test_02_07b_download_reuse(self, env: Env, httpd, nghttpx, proto):
172
count = 6
173
curl = CurlClient(env=env)
174
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
175
r = curl.http_download(urls=[urln], alpn_proto=proto,
176
with_stats=True, extra_args=[
177
'--parallel'
178
])
179
r.check_response(count=count, http_status=200)
180
# http/1.1 should have used count connections
181
assert r.total_connects == count, "http/1.1 should use this many connections"
182
183
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
184
def test_02_08_1MB_serial(self, env: Env, httpd, nghttpx, proto):
185
if proto == 'h3' and not env.have_h3():
186
pytest.skip("h3 not supported")
187
count = 5
188
urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
189
curl = CurlClient(env=env)
190
r = curl.http_download(urls=[urln], alpn_proto=proto)
191
r.check_response(count=count, http_status=200)
192
193
@pytest.mark.parametrize("proto", ['h2', 'h3'])
194
def test_02_09_1MB_parallel(self, env: Env, httpd, nghttpx, proto):
195
if proto == 'h3' and not env.have_h3():
196
pytest.skip("h3 not supported")
197
count = 5
198
urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
199
curl = CurlClient(env=env)
200
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
201
'--parallel'
202
])
203
r.check_response(count=count, http_status=200)
204
205
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
206
@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
207
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
208
def test_02_10_10MB_serial(self, env: Env, httpd, nghttpx, proto):
209
if proto == 'h3' and not env.have_h3():
210
pytest.skip("h3 not supported")
211
count = 3
212
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
213
curl = CurlClient(env=env)
214
r = curl.http_download(urls=[urln], alpn_proto=proto)
215
r.check_response(count=count, http_status=200)
216
217
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
218
@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
219
@pytest.mark.parametrize("proto", ['h2', 'h3'])
220
def test_02_11_10MB_parallel(self, env: Env, httpd, nghttpx, proto):
221
if proto == 'h3' and not env.have_h3():
222
pytest.skip("h3 not supported")
223
if proto == 'h3' and env.curl_uses_lib('msh3'):
224
pytest.skip("msh3 stalls here")
225
count = 3
226
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
227
curl = CurlClient(env=env)
228
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
229
'--parallel'
230
])
231
r.check_response(count=count, http_status=200)
232
233
@pytest.mark.parametrize("proto", ['h2', 'h3'])
234
def test_02_12_head_serial_https(self, env: Env, httpd, nghttpx, proto):
235
if proto == 'h3' and not env.have_h3():
236
pytest.skip("h3 not supported")
237
count = 5
238
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
239
curl = CurlClient(env=env)
240
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
241
'--head'
242
])
243
r.check_response(count=count, http_status=200)
244
245
@pytest.mark.parametrize("proto", ['h2'])
246
def test_02_13_head_serial_h2c(self, env: Env, httpd, nghttpx, proto):
247
if proto == 'h3' and not env.have_h3():
248
pytest.skip("h3 not supported")
249
count = 5
250
urln = f'http://{env.domain1}:{env.http_port}/data-10m?[0-{count-1}]'
251
curl = CurlClient(env=env)
252
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
253
'--head', '--http2-prior-knowledge', '--fail-early'
254
])
255
r.check_response(count=count, http_status=200)
256
257
@pytest.mark.parametrize("proto", ['h2', 'h3'])
258
def test_02_14_not_found(self, env: Env, httpd, nghttpx, proto):
259
if proto == 'h3' and not env.have_h3():
260
pytest.skip("h3 not supported")
261
if proto == 'h3' and env.curl_uses_lib('msh3'):
262
pytest.skip("msh3 stalls here")
263
count = 5
264
urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
265
curl = CurlClient(env=env)
266
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
267
'--parallel'
268
])
269
r.check_stats(count=count, http_status=404, exitcode=0,
270
remote_port=env.port_for(alpn_proto=proto),
271
remote_ip='127.0.0.1')
272
273
@pytest.mark.parametrize("proto", ['h2', 'h3'])
274
def test_02_15_fail_not_found(self, env: Env, httpd, nghttpx, proto):
275
if proto == 'h3' and not env.have_h3():
276
pytest.skip("h3 not supported")
277
if proto == 'h3' and env.curl_uses_lib('msh3'):
278
pytest.skip("msh3 stalls here")
279
count = 5
280
urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
281
curl = CurlClient(env=env)
282
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
283
'--fail'
284
])
285
r.check_stats(count=count, http_status=404, exitcode=22,
286
remote_port=env.port_for(alpn_proto=proto),
287
remote_ip='127.0.0.1')
288
289
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
290
def test_02_20_h2_small_frames(self, env: Env, httpd, configures_httpd):
291
# Test case to reproduce content corruption as observed in
292
# https://github.com/curl/curl/issues/10525
293
# To reliably reproduce, we need an Apache httpd that supports
294
# setting smaller frame sizes. This is not released yet, we
295
# test if it works and back out if not.
296
httpd.set_extra_config(env.domain1, lines=[
297
'H2MaxDataFrameLen 1024',
298
])
299
if not httpd.reload_if_config_changed():
300
pytest.skip('H2MaxDataFrameLen not supported')
301
# ok, make 100 downloads with 2 parallel running and they
302
# are expected to stumble into the issue when using `lib/http2.c`
303
# from curl 7.88.0
304
count = 5
305
urln = f'https://{env.authority_for(env.domain1, "h2")}/data-1m?[0-{count-1}]'
306
curl = CurlClient(env=env)
307
r = curl.http_download(urls=[urln], alpn_proto="h2", extra_args=[
308
'--parallel', '--parallel-max', '2'
309
])
310
r.check_response(count=count, http_status=200)
311
srcfile = os.path.join(httpd.docs_dir, 'data-1m')
312
self.check_downloads(curl, srcfile, count)
313
314
# download serial via lib client, pause/resume at different offsets
315
@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
316
@pytest.mark.parametrize("proto", ['http/1.1', 'h3'])
317
def test_02_21_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset):
318
if proto == 'h3' and not env.have_h3():
319
pytest.skip("h3 not supported")
320
count = 2
321
docname = 'data-10m'
322
url = f'https://localhost:{env.https_port}/{docname}'
323
client = LocalClient(name='hx-download', env=env)
324
if not client.exists():
325
pytest.skip(f'example client not built: {client.name}')
326
r = client.run(args=[
327
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
328
])
329
r.check_exit_code(0)
330
srcfile = os.path.join(httpd.docs_dir, docname)
331
self.check_downloads(client, srcfile, count)
332
333
# h2 download parallel via lib client, pause/resume at different offsets
334
# debug-override stream window size to reproduce #16955
335
@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
336
@pytest.mark.parametrize("swin_max", [0, 10*1024])
337
def test_02_21_h2_lib_serial(self, env: Env, httpd, pause_offset, swin_max):
338
proto = 'h2'
339
count = 2
340
docname = 'data-10m'
341
url = f'https://localhost:{env.https_port}/{docname}'
342
run_env = os.environ.copy()
343
run_env['CURL_DEBUG'] = 'multi,http/2'
344
if swin_max > 0:
345
run_env['CURL_H2_STREAM_WIN_MAX'] = f'{swin_max}'
346
client = LocalClient(name='hx-download', env=env, run_env=run_env)
347
if not client.exists():
348
pytest.skip(f'example client not built: {client.name}')
349
r = client.run(args=[
350
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
351
])
352
r.check_exit_code(0)
353
srcfile = os.path.join(httpd.docs_dir, docname)
354
self.check_downloads(client, srcfile, count)
355
356
# download via lib client, several at a time, pause/resume
357
@pytest.mark.parametrize("pause_offset", [100*1023])
358
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
359
def test_02_22_lib_parallel_resume(self, env: Env, httpd, nghttpx, proto, pause_offset):
360
if proto == 'h3' and not env.have_h3():
361
pytest.skip("h3 not supported")
362
count = 2
363
max_parallel = 5
364
docname = 'data-10m'
365
url = f'https://localhost:{env.https_port}/{docname}'
366
client = LocalClient(name='hx-download', env=env)
367
if not client.exists():
368
pytest.skip(f'example client not built: {client.name}')
369
r = client.run(args=[
370
'-n', f'{count}', '-m', f'{max_parallel}',
371
'-P', f'{pause_offset}', '-V', proto, url
372
])
373
r.check_exit_code(0)
374
srcfile = os.path.join(httpd.docs_dir, docname)
375
self.check_downloads(client, srcfile, count)
376
377
# download, several at a time, pause and abort paused
378
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
379
def test_02_23a_lib_abort_paused(self, env: Env, httpd, nghttpx, proto):
380
if proto == 'h3' and not env.have_h3():
381
pytest.skip("h3 not supported")
382
if proto == 'h3' and env.curl_uses_ossl_quic():
383
pytest.skip('OpenSSL QUIC fails here')
384
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
385
pytest.skip("fails in CI, but works locally for unknown reasons")
386
count = 10
387
max_parallel = 5
388
if proto in ['h2', 'h3']:
389
pause_offset = 64 * 1024
390
else:
391
pause_offset = 12 * 1024
392
docname = 'data-1m'
393
url = f'https://localhost:{env.https_port}/{docname}'
394
client = LocalClient(name='hx-download', env=env)
395
if not client.exists():
396
pytest.skip(f'example client not built: {client.name}')
397
r = client.run(args=[
398
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
399
'-P', f'{pause_offset}', '-V', proto, url
400
])
401
r.check_exit_code(0)
402
srcfile = os.path.join(httpd.docs_dir, docname)
403
# downloads should be there, but not necessarily complete
404
self.check_downloads(client, srcfile, count, complete=False)
405
406
# download, several at a time, abort after n bytes
407
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
408
def test_02_23b_lib_abort_offset(self, env: Env, httpd, nghttpx, proto):
409
if proto == 'h3' and not env.have_h3():
410
pytest.skip("h3 not supported")
411
if proto == 'h3' and env.curl_uses_ossl_quic():
412
pytest.skip('OpenSSL QUIC fails here')
413
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
414
pytest.skip("fails in CI, but works locally for unknown reasons")
415
count = 10
416
max_parallel = 5
417
if proto in ['h2', 'h3']:
418
abort_offset = 64 * 1024
419
else:
420
abort_offset = 12 * 1024
421
docname = 'data-1m'
422
url = f'https://localhost:{env.https_port}/{docname}'
423
client = LocalClient(name='hx-download', env=env)
424
if not client.exists():
425
pytest.skip(f'example client not built: {client.name}')
426
r = client.run(args=[
427
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
428
'-A', f'{abort_offset}', '-V', proto, url
429
])
430
r.check_exit_code(42) # CURLE_ABORTED_BY_CALLBACK
431
srcfile = os.path.join(httpd.docs_dir, docname)
432
# downloads should be there, but not necessarily complete
433
self.check_downloads(client, srcfile, count, complete=False)
434
435
# download, several at a time, abort after n bytes
436
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
437
def test_02_23c_lib_fail_offset(self, env: Env, httpd, nghttpx, proto):
438
if proto == 'h3' and not env.have_h3():
439
pytest.skip("h3 not supported")
440
if proto == 'h3' and env.curl_uses_ossl_quic():
441
pytest.skip('OpenSSL QUIC fails here')
442
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
443
pytest.skip("fails in CI, but works locally for unknown reasons")
444
count = 10
445
max_parallel = 5
446
if proto in ['h2', 'h3']:
447
fail_offset = 64 * 1024
448
else:
449
fail_offset = 12 * 1024
450
docname = 'data-1m'
451
url = f'https://localhost:{env.https_port}/{docname}'
452
client = LocalClient(name='hx-download', env=env)
453
if not client.exists():
454
pytest.skip(f'example client not built: {client.name}')
455
r = client.run(args=[
456
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
457
'-F', f'{fail_offset}', '-V', proto, url
458
])
459
r.check_exit_code(23) # CURLE_WRITE_ERROR
460
srcfile = os.path.join(httpd.docs_dir, docname)
461
# downloads should be there, but not necessarily complete
462
self.check_downloads(client, srcfile, count, complete=False)
463
464
# speed limited download
465
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
466
def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto):
467
if proto == 'h3' and not env.have_h3():
468
pytest.skip("h3 not supported")
469
count = 1
470
url = f'https://{env.authority_for(env.domain1, proto)}/data-1m'
471
curl = CurlClient(env=env)
472
speed_limit = 384 * 1024
473
min_duration = math.floor((1024 * 1024)/speed_limit)
474
r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
475
'--limit-rate', f'{speed_limit}'
476
])
477
r.check_response(count=count, http_status=200)
478
assert r.duration > timedelta(seconds=min_duration), \
479
f'rate limited transfer should take more than {min_duration}s, '\
480
f'not {r.duration}'
481
482
# make extreme parallel h2 upgrades, check invalid conn reuse
483
# before protocol switch has happened
484
def test_02_25_h2_upgrade_x(self, env: Env, httpd):
485
url = f'http://localhost:{env.http_port}/data-100k'
486
client = LocalClient(name='h2-upgrade-extreme', env=env, timeout=15)
487
if not client.exists():
488
pytest.skip(f'example client not built: {client.name}')
489
r = client.run(args=[url])
490
assert r.exit_code == 0, f'{client.dump_logs()}'
491
492
# Special client that tests TLS session reuse in parallel transfers
493
# TODO: just uses a single connection for h2/h3. Not sure how to prevent that
494
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
495
def test_02_26_session_shared_reuse(self, env: Env, proto, httpd, nghttpx):
496
url = f'https://{env.authority_for(env.domain1, proto)}/data-100k'
497
client = LocalClient(name='tls-session-reuse', env=env)
498
if not client.exists():
499
pytest.skip(f'example client not built: {client.name}')
500
r = client.run(args=[proto, url])
501
r.check_exit_code(0)
502
503
# test on paused transfers, based on issue #11982
504
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
505
def test_02_27a_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
506
url = f'https://{env.authority_for(env.domain1, proto)}' \
507
'/curltest/tweak/?&chunks=6&chunk_size=8000'
508
client = LocalClient(env=env, name='h2-pausing')
509
r = client.run(args=['-V', proto, url])
510
r.check_exit_code(0)
511
512
# test on paused transfers, based on issue #11982
513
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
514
def test_02_27b_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
515
url = f'https://{env.authority_for(env.domain1, proto)}' \
516
'/curltest/tweak/?error=502'
517
client = LocalClient(env=env, name='h2-pausing')
518
r = client.run(args=['-V', proto, url])
519
r.check_exit_code(0)
520
521
# test on paused transfers, based on issue #11982
522
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
523
def test_02_27c_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
524
url = f'https://{env.authority_for(env.domain1, proto)}' \
525
'/curltest/tweak/?status=200&chunks=1&chunk_size=100'
526
client = LocalClient(env=env, name='h2-pausing')
527
r = client.run(args=['-V', proto, url])
528
r.check_exit_code(0)
529
530
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
531
def test_02_28_get_compressed(self, env: Env, httpd, nghttpx, proto):
532
if proto == 'h3' and not env.have_h3():
533
pytest.skip("h3 not supported")
534
count = 1
535
urln = f'https://{env.authority_for(env.domain1brotli, proto)}/data-100k?[0-{count-1}]'
536
curl = CurlClient(env=env)
537
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
538
'--compressed'
539
])
540
r.check_exit_code(code=0)
541
r.check_response(count=count, http_status=200)
542
543
def check_downloads(self, client, srcfile: str, count: int,
544
complete: bool = True):
545
for i in range(count):
546
dfile = client.download_file(i)
547
assert os.path.exists(dfile)
548
if complete and not filecmp.cmp(srcfile, dfile, shallow=False):
549
diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
550
b=open(dfile).readlines(),
551
fromfile=srcfile,
552
tofile=dfile,
553
n=1))
554
assert False, f'download {dfile} differs:\n{diff}'
555
556
# download via lib client, 1 at a time, pause/resume at different offsets
557
@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
558
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
559
def test_02_29_h2_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset):
560
count = 2
561
docname = 'data-10m'
562
url = f'https://localhost:{env.https_port}/{docname}'
563
client = LocalClient(name='hx-download', env=env)
564
if not client.exists():
565
pytest.skip(f'example client not built: {client.name}')
566
r = client.run(args=[
567
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
568
])
569
r.check_exit_code(0)
570
srcfile = os.path.join(httpd.docs_dir, docname)
571
self.check_downloads(client, srcfile, count)
572
573
# download parallel with prior knowledge
574
def test_02_30_parallel_prior_knowledge(self, env: Env, httpd):
575
count = 3
576
curl = CurlClient(env=env)
577
urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
578
r = curl.http_download(urls=[urln], extra_args=[
579
'--parallel', '--http2-prior-knowledge'
580
])
581
r.check_response(http_status=200, count=count)
582
assert r.total_connects == 1, r.dump_logs()
583
584
# download parallel with h2 "Upgrade:"
585
def test_02_31_parallel_upgrade(self, env: Env, httpd, nghttpx):
586
count = 3
587
curl = CurlClient(env=env)
588
urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
589
r = curl.http_download(urls=[urln], extra_args=[
590
'--parallel', '--http2'
591
])
592
r.check_response(http_status=200, count=count)
593
# we see up to 3 connections, because Apache wants to serve only a single
594
# request via Upgrade: and then closes the connection. But if a new
595
# request comes in time, it might still get served.
596
assert r.total_connects <= 3, r.dump_logs()
597
598
# nghttpx is the only server we have that supports TLS early data
599
@pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx")
600
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
601
def test_02_32_earlydata(self, env: Env, httpd, nghttpx, proto):
602
if not env.curl_can_early_data():
603
pytest.skip('TLS earlydata not implemented')
604
if proto == 'h3' and \
605
(not env.have_h3() or not env.curl_can_h3_early_data()):
606
pytest.skip("h3 not supported")
607
if proto != 'h3' and sys.platform.startswith('darwin') and env.ci_run:
608
pytest.skip('failing on macOS CI runners')
609
count = 2
610
docname = 'data-10k'
611
# we want this test to always connect to nghttpx, since it is
612
# the only server we have that supports TLS earlydata
613
port = env.port_for(proto)
614
if proto != 'h3':
615
port = env.nghttpx_https_port
616
url = f'https://{env.domain1}:{port}/{docname}'
617
client = LocalClient(name='hx-download', env=env)
618
if not client.exists():
619
pytest.skip(f'example client not built: {client.name}')
620
r = client.run(args=[
621
'-n', f'{count}',
622
'-e', # use TLS earlydata
623
'-f', # forbid reuse of connections
624
'-r', f'{env.domain1}:{port}:127.0.0.1',
625
'-V', proto, url
626
])
627
r.check_exit_code(0)
628
srcfile = os.path.join(httpd.docs_dir, docname)
629
self.check_downloads(client, srcfile, count)
630
# check that TLS earlydata worked as expected
631
earlydata = {}
632
reused_session = False
633
for line in r.trace_lines:
634
m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line)
635
if m:
636
earlydata[int(m.group(1))] = int(m.group(2))
637
continue
638
m = re.match(r'\[1-1] \* SSL reusing session.*', line)
639
if m:
640
reused_session = True
641
assert reused_session, 'session was not reused for 2nd transfer'
642
assert earlydata[0] == 0, f'{earlydata}'
643
if proto == 'http/1.1':
644
assert earlydata[1] == 111, f'{earlydata}'
645
elif proto == 'h2':
646
assert earlydata[1] == 127, f'{earlydata}'
647
elif proto == 'h3':
648
assert earlydata[1] == 109, f'{earlydata}'
649
650
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
651
@pytest.mark.parametrize("max_host_conns", [0, 1, 5])
652
def test_02_33_max_host_conns(self, env: Env, httpd, nghttpx, proto, max_host_conns):
653
if not env.curl_is_debug():
654
pytest.skip('only works for curl debug builds')
655
if proto == 'h3' and not env.have_h3():
656
pytest.skip("h3 not supported")
657
count = 50
658
max_parallel = 50
659
docname = 'data-10k'
660
port = env.port_for(proto)
661
url = f'https://{env.domain1}:{port}/{docname}'
662
run_env = os.environ.copy()
663
run_env['CURL_DEBUG'] = 'multi'
664
client = LocalClient(name='hx-download', env=env, run_env=run_env)
665
if not client.exists():
666
pytest.skip(f'example client not built: {client.name}')
667
r = client.run(args=[
668
'-n', f'{count}',
669
'-m', f'{max_parallel}',
670
'-x', # always use a fresh connection
671
'-M', str(max_host_conns), # limit conns per host
672
'-r', f'{env.domain1}:{port}:127.0.0.1',
673
'-V', proto, url
674
])
675
r.check_exit_code(0)
676
srcfile = os.path.join(httpd.docs_dir, docname)
677
self.check_downloads(client, srcfile, count)
678
if max_host_conns > 0:
679
matched_lines = 0
680
for line in r.trace_lines:
681
m = re.match(r'.*The cache now contains (\d+) members.*', line)
682
if m:
683
matched_lines += 1
684
n = int(m.group(1))
685
assert n <= max_host_conns
686
assert matched_lines > 0
687
688
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
689
@pytest.mark.parametrize("max_total_conns", [0, 1, 5])
690
def test_02_34_max_total_conns(self, env: Env, httpd, nghttpx, proto, max_total_conns):
691
if not env.curl_is_debug():
692
pytest.skip('only works for curl debug builds')
693
if proto == 'h3' and not env.have_h3():
694
pytest.skip("h3 not supported")
695
count = 50
696
max_parallel = 50
697
docname = 'data-10k'
698
port = env.port_for(proto)
699
url = f'https://{env.domain1}:{port}/{docname}'
700
run_env = os.environ.copy()
701
run_env['CURL_DEBUG'] = 'multi'
702
client = LocalClient(name='hx-download', env=env, run_env=run_env)
703
if not client.exists():
704
pytest.skip(f'example client not built: {client.name}')
705
r = client.run(args=[
706
'-n', f'{count}',
707
'-m', f'{max_parallel}',
708
'-x', # always use a fresh connection
709
'-T', str(max_total_conns), # limit total connections
710
'-r', f'{env.domain1}:{port}:127.0.0.1',
711
'-V', proto, url
712
])
713
r.check_exit_code(0)
714
srcfile = os.path.join(httpd.docs_dir, docname)
715
self.check_downloads(client, srcfile, count)
716
if max_total_conns > 0:
717
matched_lines = 0
718
for line in r.trace_lines:
719
m = re.match(r'.*The cache now contains (\d+) members.*', line)
720
if m:
721
matched_lines += 1
722
n = int(m.group(1))
723
assert n <= max_total_conns
724
assert matched_lines > 0
725
726
# 2 parallel transers, pause and resume. Load a 100 MB zip bomb from
727
# the server with "Content-Encoding: gzip" that gets exloded during
728
# response writing to the client. Client pauses after 1MB unzipped data
729
# and causes buffers to fill while the server sends more response
730
# data.
731
# * http/1.1: not much buffering is done as curl does no longer
732
# serve the connections that are paused
733
# * h2/h3: server continues sending what the stream window allows and
734
# since the one connection involved unpaused transfers, data continues
735
# to be received, requiring buffering.
736
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
737
def test_02_35_pause_bomb(self, env: Env, httpd, nghttpx, proto):
738
if proto == 'h3' and not env.have_h3():
739
pytest.skip("h3 not supported")
740
count = 2
741
pause_offset = 1024 * 1024
742
docname = 'bomb-100m.txt.var'
743
url = f'https://localhost:{env.https_port}/{docname}'
744
client = LocalClient(name='hx-download', env=env)
745
if not client.exists():
746
pytest.skip(f'example client not built: {client.name}')
747
r = client.run(args=[
748
'-n', f'{count}', '-m', f'{count}',
749
'-P', f'{pause_offset}', '-V', proto, url
750
])
751
r.check_exit_code(0)
752
753