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
2659 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 == 'h2' and env.curl_uses_lib('mbedtls') and \
118
sys.platform.startswith('darwin') and env.ci_run:
119
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
120
count = 200
121
curl = CurlClient(env=env)
122
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
123
r = curl.http_download(urls=[urln], alpn_proto=proto)
124
r.check_response(http_status=200, count=count)
125
if proto == 'http/1.1':
126
# http/1.1 parallel transfers will open multiple connections
127
assert r.total_connects > 1, r.dump_logs()
128
else:
129
# http2 parallel transfers will use one connection (common limit is 100)
130
assert r.total_connects == 1, r.dump_logs()
131
132
# download 500 files parallel
133
@pytest.mark.parametrize("proto", ['h2', 'h3'])
134
def test_02_06_download_many_parallel(self, env: Env, httpd, nghttpx, proto):
135
if proto == 'h3' and not env.have_h3():
136
pytest.skip("h3 not supported")
137
if proto == 'h2' and env.curl_uses_lib('mbedtls') and \
138
sys.platform.startswith('darwin') and env.ci_run:
139
pytest.skip('mbedtls 3.6.3 fails this test on macOS CI runners')
140
count = 200
141
max_parallel = 50
142
curl = CurlClient(env=env)
143
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[000-{count-1}]'
144
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
145
'--parallel', '--parallel-max', f'{max_parallel}'
146
])
147
r.check_response(http_status=200, count=count, connect_count=1)
148
149
# download files parallel, check connection reuse/multiplex
150
@pytest.mark.parametrize("proto", ['h2', 'h3'])
151
def test_02_07_download_reuse(self, env: Env, httpd, nghttpx, proto):
152
if proto == 'h3' and not env.have_h3():
153
pytest.skip("h3 not supported")
154
count = 200
155
curl = CurlClient(env=env)
156
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
157
r = curl.http_download(urls=[urln], alpn_proto=proto,
158
with_stats=True, extra_args=[
159
'--parallel', '--parallel-max', '200'
160
])
161
r.check_response(http_status=200, count=count)
162
# should have used at most 2 connections only (test servers allow 100 req/conn)
163
# it may be just 1 on slow systems where request are answered faster than
164
# curl can exhaust the capacity or if curl runs with address-sanitizer speed
165
assert r.total_connects <= 2, "h2 should use fewer connections here"
166
167
# download files parallel with http/1.1, check connection not reused
168
@pytest.mark.parametrize("proto", ['http/1.1'])
169
def test_02_07b_download_reuse(self, env: Env, httpd, nghttpx, proto):
170
count = 6
171
curl = CurlClient(env=env)
172
urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
173
r = curl.http_download(urls=[urln], alpn_proto=proto,
174
with_stats=True, extra_args=[
175
'--parallel'
176
])
177
r.check_response(count=count, http_status=200)
178
# http/1.1 should have used count connections
179
assert r.total_connects == count, "http/1.1 should use this many connections"
180
181
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
182
def test_02_08_1MB_serial(self, env: Env, httpd, nghttpx, proto):
183
if proto == 'h3' and not env.have_h3():
184
pytest.skip("h3 not supported")
185
count = 5
186
urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
187
curl = CurlClient(env=env)
188
r = curl.http_download(urls=[urln], alpn_proto=proto)
189
r.check_response(count=count, http_status=200)
190
191
@pytest.mark.parametrize("proto", ['h2', 'h3'])
192
def test_02_09_1MB_parallel(self, env: Env, httpd, nghttpx, proto):
193
if proto == 'h3' and not env.have_h3():
194
pytest.skip("h3 not supported")
195
count = 5
196
urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
197
curl = CurlClient(env=env)
198
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
199
'--parallel'
200
])
201
r.check_response(count=count, http_status=200)
202
203
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
204
@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
205
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
206
def test_02_10_10MB_serial(self, env: Env, httpd, nghttpx, proto):
207
if proto == 'h3' and not env.have_h3():
208
pytest.skip("h3 not supported")
209
count = 3
210
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
211
curl = CurlClient(env=env)
212
r = curl.http_download(urls=[urln], alpn_proto=proto)
213
r.check_response(count=count, http_status=200)
214
215
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
216
@pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
217
@pytest.mark.parametrize("proto", ['h2', 'h3'])
218
def test_02_11_10MB_parallel(self, env: Env, httpd, nghttpx, proto):
219
if proto == 'h3' and not env.have_h3():
220
pytest.skip("h3 not supported")
221
count = 3
222
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
223
curl = CurlClient(env=env)
224
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
225
'--parallel'
226
])
227
r.check_response(count=count, http_status=200)
228
229
@pytest.mark.parametrize("proto", ['h2', 'h3'])
230
def test_02_12_head_serial_https(self, env: Env, httpd, nghttpx, proto):
231
if proto == 'h3' and not env.have_h3():
232
pytest.skip("h3 not supported")
233
count = 5
234
urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
235
curl = CurlClient(env=env)
236
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
237
'--head'
238
])
239
r.check_response(count=count, http_status=200)
240
241
@pytest.mark.parametrize("proto", ['h2'])
242
def test_02_13_head_serial_h2c(self, env: Env, httpd, nghttpx, proto):
243
if proto == 'h3' and not env.have_h3():
244
pytest.skip("h3 not supported")
245
count = 5
246
urln = f'http://{env.domain1}:{env.http_port}/data-10m?[0-{count-1}]'
247
curl = CurlClient(env=env)
248
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
249
'--head', '--http2-prior-knowledge', '--fail-early'
250
])
251
r.check_response(count=count, http_status=200)
252
253
@pytest.mark.parametrize("proto", ['h2', 'h3'])
254
def test_02_14_not_found(self, env: Env, httpd, nghttpx, proto):
255
if proto == 'h3' and not env.have_h3():
256
pytest.skip("h3 not supported")
257
count = 5
258
urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
259
curl = CurlClient(env=env)
260
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
261
'--parallel'
262
])
263
r.check_stats(count=count, http_status=404, exitcode=0,
264
remote_port=env.port_for(alpn_proto=proto),
265
remote_ip='127.0.0.1')
266
267
@pytest.mark.parametrize("proto", ['h2', 'h3'])
268
def test_02_15_fail_not_found(self, env: Env, httpd, nghttpx, proto):
269
if proto == 'h3' and not env.have_h3():
270
pytest.skip("h3 not supported")
271
count = 5
272
urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
273
curl = CurlClient(env=env)
274
r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
275
'--fail'
276
])
277
r.check_stats(count=count, http_status=404, exitcode=22,
278
remote_port=env.port_for(alpn_proto=proto),
279
remote_ip='127.0.0.1')
280
281
@pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
282
def test_02_20_h2_small_frames(self, env: Env, httpd, configures_httpd):
283
# Test case to reproduce content corruption as observed in
284
# https://github.com/curl/curl/issues/10525
285
# To reliably reproduce, we need an Apache httpd that supports
286
# setting smaller frame sizes. This is not released yet, we
287
# test if it works and back out if not.
288
httpd.set_extra_config(env.domain1, lines=[
289
'H2MaxDataFrameLen 1024',
290
])
291
if not httpd.reload_if_config_changed():
292
pytest.skip('H2MaxDataFrameLen not supported')
293
# ok, make 100 downloads with 2 parallel running and they
294
# are expected to stumble into the issue when using `lib/http2.c`
295
# from curl 7.88.0
296
count = 5
297
urln = f'https://{env.authority_for(env.domain1, "h2")}/data-1m?[0-{count-1}]'
298
curl = CurlClient(env=env)
299
r = curl.http_download(urls=[urln], alpn_proto="h2", extra_args=[
300
'--parallel', '--parallel-max', '2'
301
])
302
r.check_response(count=count, http_status=200)
303
srcfile = os.path.join(httpd.docs_dir, 'data-1m')
304
self.check_downloads(curl, srcfile, count)
305
306
# download serial via lib client, pause/resume at different offsets
307
@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
308
@pytest.mark.parametrize("proto", ['http/1.1', 'h3'])
309
def test_02_21_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset):
310
if proto == 'h3' and not env.have_h3():
311
pytest.skip("h3 not supported")
312
count = 2
313
docname = 'data-10m'
314
url = f'https://localhost:{env.https_port}/{docname}'
315
client = LocalClient(name='cli_hx_download', env=env)
316
if not client.exists():
317
pytest.skip(f'example client not built: {client.name}')
318
r = client.run(args=[
319
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
320
])
321
r.check_exit_code(0)
322
srcfile = os.path.join(httpd.docs_dir, docname)
323
self.check_downloads(client, srcfile, count)
324
325
# h2 download parallel via lib client, pause/resume at different offsets
326
# debug-override stream window size to reproduce #16955
327
@pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
328
@pytest.mark.parametrize("swin_max", [0, 10*1024])
329
def test_02_21_h2_lib_serial(self, env: Env, httpd, pause_offset, swin_max):
330
proto = 'h2'
331
count = 2
332
docname = 'data-10m'
333
url = f'https://localhost:{env.https_port}/{docname}'
334
run_env = os.environ.copy()
335
run_env['CURL_DEBUG'] = 'multi,http/2'
336
if swin_max > 0:
337
run_env['CURL_H2_STREAM_WIN_MAX'] = f'{swin_max}'
338
client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)
339
if not client.exists():
340
pytest.skip(f'example client not built: {client.name}')
341
r = client.run(args=[
342
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
343
])
344
r.check_exit_code(0)
345
srcfile = os.path.join(httpd.docs_dir, docname)
346
self.check_downloads(client, srcfile, count)
347
348
# download via lib client, several at a time, pause/resume
349
@pytest.mark.parametrize("pause_offset", [100*1023])
350
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
351
def test_02_22_lib_parallel_resume(self, env: Env, httpd, nghttpx, proto, pause_offset):
352
if proto == 'h3' and not env.have_h3():
353
pytest.skip("h3 not supported")
354
count = 2
355
max_parallel = 5
356
docname = 'data-10m'
357
url = f'https://localhost:{env.https_port}/{docname}'
358
client = LocalClient(name='cli_hx_download', env=env)
359
if not client.exists():
360
pytest.skip(f'example client not built: {client.name}')
361
r = client.run(args=[
362
'-n', f'{count}', '-m', f'{max_parallel}',
363
'-P', f'{pause_offset}', '-V', proto, url
364
])
365
r.check_exit_code(0)
366
srcfile = os.path.join(httpd.docs_dir, docname)
367
self.check_downloads(client, srcfile, count)
368
369
# download, several at a time, pause and abort paused
370
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
371
def test_02_23a_lib_abort_paused(self, env: Env, httpd, nghttpx, proto):
372
if proto == 'h3' and not env.have_h3():
373
pytest.skip("h3 not supported")
374
if proto == 'h3' and env.curl_uses_ossl_quic():
375
pytest.skip('OpenSSL QUIC fails here')
376
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
377
pytest.skip("fails in CI, but works locally for unknown reasons")
378
count = 10
379
max_parallel = 5
380
if proto in ['h2', 'h3']:
381
pause_offset = 64 * 1024
382
else:
383
pause_offset = 12 * 1024
384
docname = 'data-1m'
385
url = f'https://localhost:{env.https_port}/{docname}'
386
client = LocalClient(name='cli_hx_download', env=env)
387
if not client.exists():
388
pytest.skip(f'example client not built: {client.name}')
389
r = client.run(args=[
390
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
391
'-P', f'{pause_offset}', '-V', proto, url
392
])
393
r.check_exit_code(0)
394
srcfile = os.path.join(httpd.docs_dir, docname)
395
# downloads should be there, but not necessarily complete
396
self.check_downloads(client, srcfile, count, complete=False)
397
398
# download, several at a time, abort after n bytes
399
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
400
def test_02_23b_lib_abort_offset(self, env: Env, httpd, nghttpx, proto):
401
if proto == 'h3' and not env.have_h3():
402
pytest.skip("h3 not supported")
403
if proto == 'h3' and env.curl_uses_ossl_quic():
404
pytest.skip('OpenSSL QUIC fails here')
405
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
406
pytest.skip("fails in CI, but works locally for unknown reasons")
407
count = 10
408
max_parallel = 5
409
if proto in ['h2', 'h3']:
410
abort_offset = 64 * 1024
411
else:
412
abort_offset = 12 * 1024
413
docname = 'data-1m'
414
url = f'https://localhost:{env.https_port}/{docname}'
415
client = LocalClient(name='cli_hx_download', env=env)
416
if not client.exists():
417
pytest.skip(f'example client not built: {client.name}')
418
r = client.run(args=[
419
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
420
'-A', f'{abort_offset}', '-V', proto, url
421
])
422
r.check_exit_code(42) # CURLE_ABORTED_BY_CALLBACK
423
srcfile = os.path.join(httpd.docs_dir, docname)
424
# downloads should be there, but not necessarily complete
425
self.check_downloads(client, srcfile, count, complete=False)
426
427
# download, several at a time, abort after n bytes
428
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
429
def test_02_23c_lib_fail_offset(self, env: Env, httpd, nghttpx, proto):
430
if proto == 'h3' and not env.have_h3():
431
pytest.skip("h3 not supported")
432
if proto == 'h3' and env.curl_uses_ossl_quic():
433
pytest.skip('OpenSSL QUIC fails here')
434
if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
435
pytest.skip("fails in CI, but works locally for unknown reasons")
436
count = 10
437
max_parallel = 5
438
if proto in ['h2', 'h3']:
439
fail_offset = 64 * 1024
440
else:
441
fail_offset = 12 * 1024
442
docname = 'data-1m'
443
url = f'https://localhost:{env.https_port}/{docname}'
444
client = LocalClient(name='cli_hx_download', env=env)
445
if not client.exists():
446
pytest.skip(f'example client not built: {client.name}')
447
r = client.run(args=[
448
'-n', f'{count}', '-m', f'{max_parallel}', '-a',
449
'-F', f'{fail_offset}', '-V', proto, url
450
])
451
r.check_exit_code(23) # CURLE_WRITE_ERROR
452
srcfile = os.path.join(httpd.docs_dir, docname)
453
# downloads should be there, but not necessarily complete
454
self.check_downloads(client, srcfile, count, complete=False)
455
456
# speed limited download
457
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
458
def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto):
459
if proto == 'h3' and not env.have_h3():
460
pytest.skip("h3 not supported")
461
count = 1
462
url = f'https://{env.authority_for(env.domain1, proto)}/data-1m'
463
curl = CurlClient(env=env)
464
speed_limit = 384 * 1024
465
min_duration = math.floor((1024 * 1024)/speed_limit)
466
r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
467
'--limit-rate', f'{speed_limit}'
468
])
469
r.check_response(count=count, http_status=200)
470
assert r.duration > timedelta(seconds=min_duration), \
471
f'rate limited transfer should take more than {min_duration}s, '\
472
f'not {r.duration}'
473
474
# make extreme parallel h2 upgrades, check invalid conn reuse
475
# before protocol switch has happened
476
def test_02_25_h2_upgrade_x(self, env: Env, httpd):
477
url = f'http://localhost:{env.http_port}/data-100k'
478
client = LocalClient(name='cli_h2_upgrade_extreme', env=env, timeout=15)
479
if not client.exists():
480
pytest.skip(f'example client not built: {client.name}')
481
r = client.run(args=[url])
482
assert r.exit_code == 0, f'{client.dump_logs()}'
483
484
# Special client that tests TLS session reuse in parallel transfers
485
# TODO: just uses a single connection for h2/h3. Not sure how to prevent that
486
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
487
def test_02_26_session_shared_reuse(self, env: Env, proto, httpd, nghttpx):
488
if proto == 'h3' and not env.have_h3():
489
pytest.skip("h3 not supported")
490
url = f'https://{env.authority_for(env.domain1, proto)}/data-100k'
491
client = LocalClient(name='cli_tls_session_reuse', env=env)
492
if not client.exists():
493
pytest.skip(f'example client not built: {client.name}')
494
r = client.run(args=[url, proto])
495
r.check_exit_code(0)
496
497
# test on paused transfers, based on issue #11982
498
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
499
def test_02_27a_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
500
if proto == 'h3' and not env.have_h3():
501
pytest.skip("h3 not supported")
502
url = f'https://{env.authority_for(env.domain1, proto)}' \
503
'/curltest/tweak/?&chunks=6&chunk_size=8000'
504
client = LocalClient(env=env, name='cli_h2_pausing')
505
r = client.run(args=['-V', proto, url])
506
r.check_exit_code(0)
507
508
# test on paused transfers, based on issue #11982
509
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
510
def test_02_27b_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
511
if proto == 'h3' and not env.have_h3():
512
pytest.skip("h3 not supported")
513
url = f'https://{env.authority_for(env.domain1, proto)}' \
514
'/curltest/tweak/?error=502'
515
client = LocalClient(env=env, name='cli_h2_pausing')
516
r = client.run(args=['-V', proto, url])
517
r.check_exit_code(0)
518
519
# test on paused transfers, based on issue #11982
520
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
521
def test_02_27c_paused_no_cl(self, env: Env, httpd, nghttpx, proto):
522
if proto == 'h3' and not env.have_h3():
523
pytest.skip("h3 not supported")
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='cli_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
if proto == 'h3' and not env.have_h3():
561
pytest.skip("h3 not supported")
562
count = 2
563
docname = 'data-10m'
564
url = f'https://localhost:{env.https_port}/{docname}'
565
client = LocalClient(name='cli_hx_download', env=env)
566
if not client.exists():
567
pytest.skip(f'example client not built: {client.name}')
568
r = client.run(args=[
569
'-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
570
])
571
r.check_exit_code(0)
572
srcfile = os.path.join(httpd.docs_dir, docname)
573
self.check_downloads(client, srcfile, count)
574
575
# download parallel with prior knowledge
576
def test_02_30_parallel_prior_knowledge(self, env: Env, httpd):
577
count = 3
578
curl = CurlClient(env=env)
579
urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
580
r = curl.http_download(urls=[urln], extra_args=[
581
'--parallel', '--http2-prior-knowledge'
582
])
583
r.check_response(http_status=200, count=count)
584
assert r.total_connects == 1, r.dump_logs()
585
586
# download parallel with h2 "Upgrade:"
587
def test_02_31_parallel_upgrade(self, env: Env, httpd, nghttpx):
588
count = 3
589
curl = CurlClient(env=env)
590
urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
591
r = curl.http_download(urls=[urln], extra_args=[
592
'--parallel', '--http2'
593
])
594
r.check_response(http_status=200, count=count)
595
# we see up to 3 connections, because Apache wants to serve only a single
596
# request via Upgrade: and then closes the connection. But if a new
597
# request comes in time, it might still get served.
598
assert r.total_connects <= 3, r.dump_logs()
599
600
# nghttpx is the only server we have that supports TLS early data
601
@pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx")
602
@pytest.mark.skipif(condition=not Env.curl_is_debug(), reason="needs curl debug")
603
@pytest.mark.skipif(condition=not Env.curl_is_verbose(), reason="needs curl verbose strings")
604
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
605
def test_02_32_earlydata(self, env: Env, httpd, nghttpx, proto):
606
if not env.curl_can_early_data():
607
pytest.skip('TLS earlydata not implemented')
608
if proto == 'h3' and \
609
(not env.have_h3() or not env.curl_can_h3_early_data()):
610
pytest.skip("h3 not supported")
611
if proto != 'h3' and sys.platform.startswith('darwin') and env.ci_run:
612
pytest.skip('failing on macOS CI runners')
613
if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('wolfssl'):
614
pytest.skip('h3 wolfssl early data failing on macOS')
615
if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('gnutls'):
616
pytest.skip('h3 gnutls early data failing on macOS')
617
count = 2
618
docname = 'data-10k'
619
# we want this test to always connect to nghttpx, since it is
620
# the only server we have that supports TLS earlydata
621
port = env.port_for(proto)
622
if proto != 'h3':
623
port = env.nghttpx_https_port
624
url = f'https://{env.domain1}:{port}/{docname}'
625
client = LocalClient(name='cli_hx_download', env=env)
626
if not client.exists():
627
pytest.skip(f'example client not built: {client.name}')
628
r = client.run(args=[
629
'-n', f'{count}',
630
'-e', # use TLS earlydata
631
'-f', # forbid reuse of connections
632
'-r', f'{env.domain1}:{port}:127.0.0.1',
633
'-V', proto, url
634
])
635
r.check_exit_code(0)
636
srcfile = os.path.join(httpd.docs_dir, docname)
637
self.check_downloads(client, srcfile, count)
638
# check that TLS earlydata worked as expected
639
earlydata = {}
640
reused_session = False
641
for line in r.trace_lines:
642
m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line)
643
if m:
644
earlydata[int(m.group(1))] = int(m.group(2))
645
continue
646
if re.match(r'\[1-1] \* SSL reusing session.*', line):
647
reused_session = True
648
assert reused_session, 'session was not reused for 2nd transfer'
649
assert earlydata[0] == 0, f'{earlydata}'
650
if proto == 'http/1.1':
651
assert earlydata[1] == 111, f'{earlydata}'
652
elif proto == 'h2':
653
assert earlydata[1] == 127, f'{earlydata}'
654
elif proto == 'h3':
655
assert earlydata[1] == 109, f'{earlydata}'
656
657
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
658
@pytest.mark.parametrize("max_host_conns", [0, 1, 5])
659
def test_02_33_max_host_conns(self, env: Env, httpd, nghttpx, proto, max_host_conns):
660
if not env.curl_is_debug():
661
pytest.skip('only works for curl debug builds')
662
if not env.curl_is_verbose():
663
pytest.skip('only works for curl with verbose strings')
664
if proto == 'h3' and not env.have_h3():
665
pytest.skip("h3 not supported")
666
count = 50
667
max_parallel = 50
668
docname = 'data-10k'
669
port = env.port_for(proto)
670
url = f'https://{env.domain1}:{port}/{docname}'
671
run_env = os.environ.copy()
672
run_env['CURL_DEBUG'] = 'multi'
673
client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)
674
if not client.exists():
675
pytest.skip(f'example client not built: {client.name}')
676
r = client.run(args=[
677
'-n', f'{count}',
678
'-m', f'{max_parallel}',
679
'-x', # always use a fresh connection
680
'-M', str(max_host_conns), # limit conns per host
681
'-r', f'{env.domain1}:{port}:127.0.0.1',
682
'-V', proto, url
683
])
684
r.check_exit_code(0)
685
srcfile = os.path.join(httpd.docs_dir, docname)
686
self.check_downloads(client, srcfile, count)
687
if max_host_conns > 0:
688
matched_lines = 0
689
for line in r.trace_lines:
690
m = re.match(r'.*The cache now contains (\d+) members.*', line)
691
if m:
692
matched_lines += 1
693
n = int(m.group(1))
694
assert n <= max_host_conns
695
assert matched_lines > 0
696
697
@pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
698
@pytest.mark.parametrize("max_total_conns", [0, 1, 5])
699
def test_02_34_max_total_conns(self, env: Env, httpd, nghttpx, proto, max_total_conns):
700
if not env.curl_is_debug():
701
pytest.skip('only works for curl debug builds')
702
if not env.curl_is_verbose():
703
pytest.skip('only works for curl with verbose strings')
704
if proto == 'h3' and not env.have_h3():
705
pytest.skip("h3 not supported")
706
count = 50
707
max_parallel = 50
708
docname = 'data-10k'
709
port = env.port_for(proto)
710
url = f'https://{env.domain1}:{port}/{docname}'
711
run_env = os.environ.copy()
712
run_env['CURL_DEBUG'] = 'multi'
713
client = LocalClient(name='cli_hx_download', env=env, run_env=run_env)
714
if not client.exists():
715
pytest.skip(f'example client not built: {client.name}')
716
r = client.run(args=[
717
'-n', f'{count}',
718
'-m', f'{max_parallel}',
719
'-x', # always use a fresh connection
720
'-T', str(max_total_conns), # limit total connections
721
'-r', f'{env.domain1}:{port}:127.0.0.1',
722
'-V', proto, url
723
])
724
r.check_exit_code(0)
725
srcfile = os.path.join(httpd.docs_dir, docname)
726
self.check_downloads(client, srcfile, count)
727
if max_total_conns > 0:
728
matched_lines = 0
729
for line in r.trace_lines:
730
m = re.match(r'.*The cache now contains (\d+) members.*', line)
731
if m:
732
matched_lines += 1
733
n = int(m.group(1))
734
assert n <= max_total_conns
735
assert matched_lines > 0
736
737
# 2 parallel transers, pause and resume. Load a 100 MB zip bomb from
738
# the server with "Content-Encoding: gzip" that gets exloded during
739
# response writing to the client. Client pauses after 1MB unzipped data
740
# and causes buffers to fill while the server sends more response
741
# data.
742
# * http/1.1: not much buffering is done as curl does no longer
743
# serve the connections that are paused
744
# * h2/h3: server continues sending what the stream window allows and
745
# since the one connection involved unpaused transfers, data continues
746
# to be received, requiring buffering.
747
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
748
def test_02_35_pause_bomb(self, env: Env, httpd, nghttpx, proto):
749
if proto == 'h3' and not env.have_h3():
750
pytest.skip("h3 not supported")
751
count = 2
752
pause_offset = 1024 * 1024
753
docname = 'bomb-100m.txt.var'
754
url = f'https://localhost:{env.https_port}/{docname}'
755
client = LocalClient(name='cli_hx_download', env=env)
756
if not client.exists():
757
pytest.skip(f'example client not built: {client.name}')
758
r = client.run(args=[
759
'-n', f'{count}', '-m', f'{count}',
760
'-P', f'{pause_offset}', '-V', proto, url
761
])
762
r.check_exit_code(0)
763
764
# download with looong urls
765
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
766
@pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024, 80*1024, 96*1024])
767
def test_02_36_looong_urls(self, env: Env, httpd, nghttpx, proto, url_junk):
768
if proto == 'h3' and not env.have_h3():
769
pytest.skip("h3 not supported")
770
if proto == 'h3' and env.curl_uses_lib('quiche'):
771
pytest.skip("quiche fails from 16k onwards")
772
curl = CurlClient(env=env)
773
# url is longer than 'url_len'
774
url = f'https://{env.authority_for(env.domain1, proto)}/data.json?{"x"*(url_junk)}'
775
r = curl.http_download(urls=[url], alpn_proto=proto)
776
if url_junk <= 1024:
777
r.check_exit_code(0)
778
r.check_response(http_status=200)
779
elif url_junk <= 16*1024:
780
r.check_exit_code(0)
781
# server replies with 414, Request URL too long
782
r.check_response(http_status=414)
783
elif url_junk <= 32*1024:
784
r.check_exit_code(0)
785
# server replies with 414, Request URL too long
786
r.check_response(http_status=414)
787
else:
788
# with urls larger than 64k, behaviour differs
789
if proto == 'http/1.1':
790
r.check_exit_code(0)
791
r.check_response(http_status=414)
792
elif proto == 'h2':
793
# h2 is unable to send such large headers (frame limits)
794
r.check_exit_code(55)
795
elif proto == 'h3':
796
if url_junk <= 64*1024:
797
r.check_exit_code(0)
798
# nghttpx reports 431 Request Header Field too Large
799
r.check_response(http_status=431)
800
else:
801
# nghttpx destroys the connection with internal error
802
# ERR_QPACK_HEADER_TOO_LARGE
803
r.check_exit_code(56)
804
805