1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3#*************************************************************************** 4# _ _ ____ _ 5# Project ___| | | | _ \| | 6# / __| | | | |_) | | 7# | (__| |_| | _ <| |___ 8# \___|\___/|_| \_\_____| 9# 10# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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# 27import difflib 28import filecmp 29import logging 30import os 31import re 32import pytest 33from typing import List 34 35from testenv import Env, CurlClient, LocalClient 36 37 38log = logging.getLogger(__name__) 39 40 41class TestUpload: 42 43 @pytest.fixture(autouse=True, scope='class') 44 def _class_scope(self, env, httpd, nghttpx): 45 if env.have_h3(): 46 nghttpx.start_if_needed() 47 env.make_data_file(indir=env.gen_dir, fname="data-10k", fsize=10*1024) 48 env.make_data_file(indir=env.gen_dir, fname="data-63k", fsize=63*1024) 49 env.make_data_file(indir=env.gen_dir, fname="data-64k", fsize=64*1024) 50 env.make_data_file(indir=env.gen_dir, fname="data-100k", fsize=100*1024) 51 env.make_data_file(indir=env.gen_dir, fname="data-1m+", fsize=(1024*1024)+1) 52 env.make_data_file(indir=env.gen_dir, fname="data-10m", fsize=10*1024*1024) 53 httpd.clear_extra_configs() 54 httpd.reload() 55 56 # upload small data, check that this is what was echoed 57 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 58 def test_07_01_upload_1_small(self, env: Env, httpd, nghttpx, repeat, proto): 59 if proto == 'h3' and not env.have_h3(): 60 pytest.skip("h3 not supported") 61 if proto == 'h3' and env.curl_uses_lib('msh3'): 62 pytest.skip("msh3 fails here") 63 data = '0123456789' 64 curl = CurlClient(env=env) 65 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 66 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto) 67 r.check_stats(count=1, http_status=200, exitcode=0) 68 respdata = open(curl.response_file(0)).readlines() 69 assert respdata == [data] 70 71 # upload large data, check that this is what was echoed 72 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 73 def test_07_02_upload_1_large(self, env: Env, httpd, nghttpx, repeat, proto): 74 if proto == 'h3' and not env.have_h3(): 75 pytest.skip("h3 not supported") 76 if proto == 'h3' and env.curl_uses_lib('msh3'): 77 pytest.skip("msh3 fails here") 78 fdata = os.path.join(env.gen_dir, 'data-100k') 79 curl = CurlClient(env=env) 80 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 81 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto) 82 r.check_stats(count=1, http_status=200, exitcode=0) 83 indata = open(fdata).readlines() 84 respdata = open(curl.response_file(0)).readlines() 85 assert respdata == indata 86 87 # upload data sequentially, check that they were echoed 88 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 89 def test_07_10_upload_sequential(self, env: Env, httpd, nghttpx, repeat, proto): 90 if proto == 'h3' and not env.have_h3(): 91 pytest.skip("h3 not supported") 92 if proto == 'h3' and env.curl_uses_lib('msh3'): 93 pytest.skip("msh3 stalls here") 94 count = 20 95 data = '0123456789' 96 curl = CurlClient(env=env) 97 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 98 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto) 99 r.check_stats(count=count, http_status=200, exitcode=0) 100 for i in range(count): 101 respdata = open(curl.response_file(i)).readlines() 102 assert respdata == [data] 103 104 # upload data parallel, check that they were echoed 105 @pytest.mark.parametrize("proto", ['h2', 'h3']) 106 def test_07_11_upload_parallel(self, env: Env, httpd, nghttpx, repeat, proto): 107 if proto == 'h3' and not env.have_h3(): 108 pytest.skip("h3 not supported") 109 if proto == 'h3' and env.curl_uses_lib('msh3'): 110 pytest.skip("msh3 stalls here") 111 # limit since we use a separate connection in h1 112 count = 20 113 data = '0123456789' 114 curl = CurlClient(env=env) 115 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 116 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, 117 extra_args=['--parallel']) 118 r.check_stats(count=count, http_status=200, exitcode=0) 119 for i in range(count): 120 respdata = open(curl.response_file(i)).readlines() 121 assert respdata == [data] 122 123 # upload large data sequentially, check that this is what was echoed 124 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 125 def test_07_12_upload_seq_large(self, env: Env, httpd, nghttpx, repeat, proto): 126 if proto == 'h3' and not env.have_h3(): 127 pytest.skip("h3 not supported") 128 if proto == 'h3' and env.curl_uses_lib('msh3'): 129 pytest.skip("msh3 stalls here") 130 fdata = os.path.join(env.gen_dir, 'data-100k') 131 count = 10 132 curl = CurlClient(env=env) 133 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 134 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto) 135 r.check_response(count=count, http_status=200) 136 indata = open(fdata).readlines() 137 r.check_stats(count=count, http_status=200, exitcode=0) 138 for i in range(count): 139 respdata = open(curl.response_file(i)).readlines() 140 assert respdata == indata 141 142 # upload very large data sequentially, check that this is what was echoed 143 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 144 def test_07_13_upload_seq_large(self, env: Env, httpd, nghttpx, repeat, proto): 145 if proto == 'h3' and not env.have_h3(): 146 pytest.skip("h3 not supported") 147 if proto == 'h3' and env.curl_uses_lib('msh3'): 148 pytest.skip("msh3 stalls here") 149 fdata = os.path.join(env.gen_dir, 'data-10m') 150 count = 2 151 curl = CurlClient(env=env) 152 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 153 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto) 154 r.check_stats(count=count, http_status=200, exitcode=0) 155 indata = open(fdata).readlines() 156 for i in range(count): 157 respdata = open(curl.response_file(i)).readlines() 158 assert respdata == indata 159 160 # upload from stdin, issue #14870 161 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 162 @pytest.mark.parametrize("indata", [ 163 '', '1', '123\n456andsomething\n\n' 164 ]) 165 def test_07_14_upload_stdin(self, env: Env, httpd, nghttpx, proto, indata): 166 if proto == 'h3' and not env.have_h3(): 167 pytest.skip("h3 not supported") 168 if proto == 'h3' and env.curl_uses_lib('msh3'): 169 pytest.skip("msh3 stalls here") 170 count = 1 171 curl = CurlClient(env=env) 172 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]' 173 r = curl.http_put(urls=[url], data=indata, alpn_proto=proto) 174 r.check_stats(count=count, http_status=200, exitcode=0) 175 for i in range(count): 176 respdata = open(curl.response_file(i)).readlines() 177 assert respdata == [f'{len(indata)}'] 178 179 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 180 def test_07_15_hx_put(self, env: Env, httpd, nghttpx, proto): 181 if proto == 'h3' and not env.have_h3(): 182 pytest.skip("h3 not supported") 183 count = 2 184 upload_size = 128*1024 185 url = f'https://localhost:{env.https_port}/curltest/put?id=[0-{count-1}]' 186 client = LocalClient(name='hx-upload', env=env) 187 if not client.exists(): 188 pytest.skip(f'example client not built: {client.name}') 189 r = client.run(args=[ 190 '-n', f'{count}', '-S', f'{upload_size}', '-V', proto, url 191 ]) 192 r.check_exit_code(0) 193 self.check_downloads(client, [f"{upload_size}"], count) 194 195 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 196 def test_07_16_hx_put_reuse(self, env: Env, httpd, nghttpx, proto): 197 if proto == 'h3' and not env.have_h3(): 198 pytest.skip("h3 not supported") 199 count = 2 200 upload_size = 128*1024 201 url = f'https://localhost:{env.https_port}/curltest/put?id=[0-{count-1}]' 202 client = LocalClient(name='hx-upload', env=env) 203 if not client.exists(): 204 pytest.skip(f'example client not built: {client.name}') 205 r = client.run(args=[ 206 '-n', f'{count}', '-S', f'{upload_size}', '-R', '-V', proto, url 207 ]) 208 r.check_exit_code(0) 209 self.check_downloads(client, [f"{upload_size}"], count) 210 211 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 212 def test_07_17_hx_post_reuse(self, env: Env, httpd, nghttpx, proto): 213 if proto == 'h3' and not env.have_h3(): 214 pytest.skip("h3 not supported") 215 count = 2 216 upload_size = 128*1024 217 url = f'https://localhost:{env.https_port}/curltest/echo?id=[0-{count-1}]' 218 client = LocalClient(name='hx-upload', env=env) 219 if not client.exists(): 220 pytest.skip(f'example client not built: {client.name}') 221 r = client.run(args=[ 222 '-n', f'{count}', '-M', 'POST', '-S', f'{upload_size}', '-R', '-V', proto, url 223 ]) 224 r.check_exit_code(0) 225 self.check_downloads(client, ["x" * upload_size], count) 226 227 # upload data parallel, check that they were echoed 228 @pytest.mark.parametrize("proto", ['h2', 'h3']) 229 def test_07_20_upload_parallel(self, env: Env, httpd, nghttpx, repeat, proto): 230 if proto == 'h3' and not env.have_h3(): 231 pytest.skip("h3 not supported") 232 if proto == 'h3' and env.curl_uses_lib('msh3'): 233 pytest.skip("msh3 stalls here") 234 # limit since we use a separate connection in h1 235 count = 10 236 data = '0123456789' 237 curl = CurlClient(env=env) 238 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 239 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, 240 extra_args=['--parallel']) 241 r.check_stats(count=count, http_status=200, exitcode=0) 242 for i in range(count): 243 respdata = open(curl.response_file(i)).readlines() 244 assert respdata == [data] 245 246 # upload large data parallel, check that this is what was echoed 247 @pytest.mark.parametrize("proto", ['h2', 'h3']) 248 def test_07_21_upload_parallel_large(self, env: Env, httpd, nghttpx, repeat, proto): 249 if proto == 'h3' and not env.have_h3(): 250 pytest.skip("h3 not supported") 251 if proto == 'h3' and env.curl_uses_lib('msh3'): 252 pytest.skip("msh3 stalls here") 253 fdata = os.path.join(env.gen_dir, 'data-100k') 254 # limit since we use a separate connection in h1 255 count = 10 256 curl = CurlClient(env=env) 257 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-{count-1}]' 258 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, 259 extra_args=['--parallel']) 260 r.check_response(count=count, http_status=200) 261 self.check_download(count, fdata, curl) 262 263 # upload large data parallel to a URL that denies uploads 264 @pytest.mark.parametrize("proto", ['h2', 'h3']) 265 def test_07_22_upload_parallel_fail(self, env: Env, httpd, nghttpx, repeat, proto): 266 if proto == 'h3' and not env.have_h3(): 267 pytest.skip("h3 not supported") 268 if proto == 'h3' and env.curl_uses_lib('msh3'): 269 pytest.skip("msh3 stalls here") 270 fdata = os.path.join(env.gen_dir, 'data-10m') 271 count = 20 272 curl = CurlClient(env=env) 273 url = f'https://{env.authority_for(env.domain1, proto)}'\ 274 f'/curltest/tweak?status=400&delay=5ms&chunks=1&body_error=reset&id=[0-{count-1}]' 275 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, 276 extra_args=['--parallel']) 277 exp_exit = 92 if proto == 'h2' else 95 278 r.check_stats(count=count, exitcode=exp_exit) 279 280 # PUT 100k 281 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 282 def test_07_30_put_100k(self, env: Env, httpd, nghttpx, repeat, proto): 283 if proto == 'h3' and not env.have_h3(): 284 pytest.skip("h3 not supported") 285 if proto == 'h3' and env.curl_uses_lib('msh3'): 286 pytest.skip("msh3 fails here") 287 fdata = os.path.join(env.gen_dir, 'data-100k') 288 count = 1 289 curl = CurlClient(env=env) 290 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]' 291 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, 292 extra_args=['--parallel']) 293 r.check_stats(count=count, http_status=200, exitcode=0) 294 exp_data = [f'{os.path.getsize(fdata)}'] 295 r.check_response(count=count, http_status=200) 296 for i in range(count): 297 respdata = open(curl.response_file(i)).readlines() 298 assert respdata == exp_data 299 300 # PUT 10m 301 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 302 def test_07_31_put_10m(self, env: Env, httpd, nghttpx, repeat, proto): 303 if proto == 'h3' and not env.have_h3(): 304 pytest.skip("h3 not supported") 305 if proto == 'h3' and env.curl_uses_lib('msh3'): 306 pytest.skip("msh3 fails here") 307 fdata = os.path.join(env.gen_dir, 'data-10m') 308 count = 1 309 curl = CurlClient(env=env) 310 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]&chunk_delay=2ms' 311 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, 312 extra_args=['--parallel']) 313 r.check_stats(count=count, http_status=200, exitcode=0) 314 exp_data = [f'{os.path.getsize(fdata)}'] 315 r.check_response(count=count, http_status=200) 316 for i in range(count): 317 respdata = open(curl.response_file(i)).readlines() 318 assert respdata == exp_data 319 320 # issue #10591 321 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 322 def test_07_32_issue_10591(self, env: Env, httpd, nghttpx, repeat, proto): 323 if proto == 'h3' and not env.have_h3(): 324 pytest.skip("h3 not supported") 325 if proto == 'h3' and env.curl_uses_lib('msh3'): 326 pytest.skip("msh3 fails here") 327 fdata = os.path.join(env.gen_dir, 'data-10m') 328 count = 1 329 curl = CurlClient(env=env) 330 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]' 331 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto) 332 r.check_stats(count=count, http_status=200, exitcode=0) 333 334 # issue #11157, upload that is 404'ed by server, needs to terminate 335 # correctly and not time out on sending 336 def test_07_33_issue_11157a(self, env: Env, httpd, nghttpx, repeat): 337 proto = 'h2' 338 fdata = os.path.join(env.gen_dir, 'data-10m') 339 # send a POST to our PUT handler which will send immediately a 404 back 340 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put' 341 curl = CurlClient(env=env) 342 r = curl.run_direct(with_stats=True, args=[ 343 '--resolve', f'{env.authority_for(env.domain1, proto)}:127.0.0.1', 344 '--cacert', env.ca.cert_file, 345 '--request', 'POST', 346 '--max-time', '5', '-v', 347 '--url', url, 348 '--form', 'idList=12345678', 349 '--form', 'pos=top', 350 '--form', 'name=mr_test', 351 '--form', f'fileSource=@{fdata};type=application/pdf', 352 ]) 353 assert r.exit_code == 0, f'{r}' 354 r.check_stats(1, 404) 355 356 # issue #11157, send upload that is slowly read in 357 def test_07_33_issue_11157b(self, env: Env, httpd, nghttpx, repeat): 358 proto = 'h2' 359 fdata = os.path.join(env.gen_dir, 'data-10m') 360 # tell our test PUT handler to read the upload more slowly, so 361 # that the send buffering and transfer loop needs to wait 362 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?chunk_delay=2ms' 363 curl = CurlClient(env=env) 364 r = curl.run_direct(with_stats=True, args=[ 365 '--verbose', '--trace-config', 'ids,time', 366 '--resolve', f'{env.authority_for(env.domain1, proto)}:127.0.0.1', 367 '--cacert', env.ca.cert_file, 368 '--request', 'PUT', 369 '--max-time', '10', '-v', 370 '--url', url, 371 '--form', 'idList=12345678', 372 '--form', 'pos=top', 373 '--form', 'name=mr_test', 374 '--form', f'fileSource=@{fdata};type=application/pdf', 375 ]) 376 assert r.exit_code == 0, r.dump_logs() 377 r.check_stats(1, 200) 378 379 def test_07_34_issue_11194(self, env: Env, httpd, nghttpx, repeat): 380 proto = 'h2' 381 # tell our test PUT handler to read the upload more slowly, so 382 # that the send buffering and transfer loop needs to wait 383 fdata = os.path.join(env.gen_dir, 'data-100k') 384 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put' 385 curl = CurlClient(env=env) 386 r = curl.run_direct(with_stats=True, args=[ 387 '--verbose', '--trace-config', 'ids,time', 388 '--resolve', f'{env.authority_for(env.domain1, proto)}:127.0.0.1', 389 '--cacert', env.ca.cert_file, 390 '--request', 'PUT', 391 '--digest', '--user', 'test:test', 392 '--data-binary', f'@{fdata}', 393 '--url', url, 394 ]) 395 assert r.exit_code == 0, r.dump_logs() 396 r.check_stats(1, 200) 397 398 # upload large data on a h1 to h2 upgrade 399 def test_07_35_h1_h2_upgrade_upload(self, env: Env, httpd, nghttpx, repeat): 400 fdata = os.path.join(env.gen_dir, 'data-100k') 401 curl = CurlClient(env=env) 402 url = f'http://{env.domain1}:{env.http_port}/curltest/echo?id=[0-0]' 403 r = curl.http_upload(urls=[url], data=f'@{fdata}', extra_args=[ 404 '--http2' 405 ]) 406 r.check_response(count=1, http_status=200) 407 # apache does not Upgrade on request with a body 408 assert r.stats[0]['http_version'] == '1.1', f'{r}' 409 indata = open(fdata).readlines() 410 respdata = open(curl.response_file(0)).readlines() 411 assert respdata == indata 412 413 # upload to a 301,302,303 response 414 @pytest.mark.parametrize("redir", ['301', '302', '303']) 415 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 416 def test_07_36_upload_30x(self, env: Env, httpd, nghttpx, repeat, redir, proto): 417 if proto == 'h3' and not env.have_h3(): 418 pytest.skip("h3 not supported") 419 if proto == 'h3' and env.curl_uses_lib('msh3'): 420 pytest.skip("msh3 fails here") 421 data = '0123456789' * 10 422 curl = CurlClient(env=env) 423 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo{redir}?id=[0-0]' 424 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, extra_args=[ 425 '-L', '--trace-config', 'http/2,http/3' 426 ]) 427 r.check_response(count=1, http_status=200) 428 respdata = open(curl.response_file(0)).readlines() 429 assert respdata == [] # was transformed to a GET 430 431 # upload to a 307 response 432 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 433 def test_07_37_upload_307(self, env: Env, httpd, nghttpx, repeat, proto): 434 if proto == 'h3' and not env.have_h3(): 435 pytest.skip("h3 not supported") 436 if proto == 'h3' and env.curl_uses_lib('msh3'): 437 pytest.skip("msh3 fails here") 438 data = '0123456789' * 10 439 curl = CurlClient(env=env) 440 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo307?id=[0-0]' 441 r = curl.http_upload(urls=[url], data=data, alpn_proto=proto, extra_args=[ 442 '-L', '--trace-config', 'http/2,http/3' 443 ]) 444 r.check_response(count=1, http_status=200) 445 respdata = open(curl.response_file(0)).readlines() 446 assert respdata == [data] # was POST again 447 448 # POST form data, yet another code path in transfer 449 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 450 def test_07_38_form_small(self, env: Env, httpd, nghttpx, repeat, proto): 451 if proto == 'h3' and not env.have_h3(): 452 pytest.skip("h3 not supported") 453 if proto == 'h3' and env.curl_uses_lib('msh3'): 454 pytest.skip("msh3 fails here") 455 curl = CurlClient(env=env) 456 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 457 r = curl.http_form(urls=[url], alpn_proto=proto, form={ 458 'name1': 'value1', 459 }) 460 r.check_stats(count=1, http_status=200, exitcode=0) 461 462 # POST data urlencoded, small enough to be sent with request headers 463 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 464 def test_07_39_post_urlenc_small(self, env: Env, httpd, nghttpx, repeat, proto): 465 if proto == 'h3' and not env.have_h3(): 466 pytest.skip("h3 not supported") 467 if proto == 'h3' and env.curl_uses_lib('msh3'): 468 pytest.skip("msh3 fails here") 469 fdata = os.path.join(env.gen_dir, 'data-63k') 470 curl = CurlClient(env=env) 471 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 472 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, extra_args=[ 473 '--trace-config', 'http/2,http/3' 474 ]) 475 r.check_stats(count=1, http_status=200, exitcode=0) 476 indata = open(fdata).readlines() 477 respdata = open(curl.response_file(0)).readlines() 478 assert respdata == indata 479 480 # POST data urlencoded, large enough to be sent separate from request headers 481 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 482 def test_07_40_post_urlenc_large(self, env: Env, httpd, nghttpx, repeat, proto): 483 if proto == 'h3' and not env.have_h3(): 484 pytest.skip("h3 not supported") 485 if proto == 'h3' and env.curl_uses_lib('msh3'): 486 pytest.skip("msh3 fails here") 487 fdata = os.path.join(env.gen_dir, 'data-64k') 488 curl = CurlClient(env=env) 489 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 490 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, extra_args=[ 491 '--trace-config', 'http/2,http/3' 492 ]) 493 r.check_stats(count=1, http_status=200, exitcode=0) 494 indata = open(fdata).readlines() 495 respdata = open(curl.response_file(0)).readlines() 496 assert respdata == indata 497 498 # POST data urlencoded, small enough to be sent with request headers 499 # and request headers are so large that the first send is larger 500 # than our default upload buffer length (64KB). 501 # Unfixed, this will fail when run with CURL_DBG_SOCK_WBLOCK=80 most 502 # of the time 503 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 504 def test_07_41_post_urlenc_small(self, env: Env, httpd, nghttpx, repeat, proto): 505 if proto == 'h3' and not env.have_h3(): 506 pytest.skip("h3 not supported") 507 if proto == 'h3' and env.curl_uses_lib('msh3'): 508 pytest.skip("msh3 fails here") 509 if proto == 'h3' and env.curl_uses_lib('quiche'): 510 pytest.skip("quiche has CWND issues with large requests") 511 fdata = os.path.join(env.gen_dir, 'data-63k') 512 curl = CurlClient(env=env) 513 extra_args = ['--trace-config', 'http/2,http/3'] 514 # add enough headers so that the first send chunk is > 64KB 515 for i in range(63): 516 extra_args.extend(['-H', f'x{i:02d}: {"y"*1019}']) 517 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 518 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, extra_args=extra_args) 519 r.check_stats(count=1, http_status=200, exitcode=0) 520 indata = open(fdata).readlines() 521 respdata = open(curl.response_file(0)).readlines() 522 assert respdata == indata 523 524 def check_download(self, count, srcfile, curl): 525 for i in range(count): 526 dfile = curl.download_file(i) 527 assert os.path.exists(dfile) 528 if not filecmp.cmp(srcfile, dfile, shallow=False): 529 diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(), 530 b=open(dfile).readlines(), 531 fromfile=srcfile, 532 tofile=dfile, 533 n=1)) 534 assert False, f'download {dfile} differs:\n{diff}' 535 536 # upload data, pause, let connection die with an incomplete response 537 # issues #11769 #13260 538 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 539 def test_07_42a_upload_disconnect(self, env: Env, httpd, nghttpx, repeat, proto): 540 if proto == 'h3' and not env.have_h3(): 541 pytest.skip("h3 not supported") 542 if proto == 'h3' and env.curl_uses_lib('msh3'): 543 pytest.skip("msh3 fails here") 544 client = LocalClient(name='upload-pausing', env=env, timeout=60) 545 if not client.exists(): 546 pytest.skip(f'example client not built: {client.name}') 547 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]&die_after=0' 548 r = client.run(['-V', proto, url]) 549 if r.exit_code == 18: # PARTIAL_FILE is always ok 550 pass 551 elif proto == 'h2': 552 r.check_exit_code(92) # CURLE_HTTP2_STREAM also ok 553 elif proto == 'h3': 554 r.check_exit_code(95) # CURLE_HTTP3 also ok 555 else: 556 r.check_exit_code(18) # will fail as it should 557 558 # upload data, pause, let connection die without any response at all 559 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 560 def test_07_42b_upload_disconnect(self, env: Env, httpd, nghttpx, repeat, proto): 561 if proto == 'h3' and not env.have_h3(): 562 pytest.skip("h3 not supported") 563 if proto == 'h3' and env.curl_uses_lib('msh3'): 564 pytest.skip("msh3 fails here") 565 client = LocalClient(name='upload-pausing', env=env, timeout=60) 566 if not client.exists(): 567 pytest.skip(f'example client not built: {client.name}') 568 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]&just_die=1' 569 r = client.run(['-V', proto, url]) 570 exp_code = 52 # GOT_NOTHING 571 if proto == 'h2' or proto == 'h3': 572 exp_code = 0 # we get a 500 from the server 573 r.check_exit_code(exp_code) # GOT_NOTHING 574 575 # upload data, pause, let connection die after 100 continue 576 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 577 def test_07_42c_upload_disconnect(self, env: Env, httpd, nghttpx, repeat, proto): 578 if proto == 'h3' and not env.have_h3(): 579 pytest.skip("h3 not supported") 580 if proto == 'h3' and env.curl_uses_lib('msh3'): 581 pytest.skip("msh3 fails here") 582 client = LocalClient(name='upload-pausing', env=env, timeout=60) 583 if not client.exists(): 584 pytest.skip(f'example client not built: {client.name}') 585 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]&die_after_100=1' 586 r = client.run(['-V', proto, url]) 587 exp_code = 52 # GOT_NOTHING 588 if proto == 'h2' or proto == 'h3': 589 exp_code = 0 # we get a 500 from the server 590 r.check_exit_code(exp_code) # GOT_NOTHING 591 592 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 593 def test_07_43_upload_denied(self, env: Env, httpd, nghttpx, repeat, proto): 594 if proto == 'h3' and not env.have_h3(): 595 pytest.skip("h3 not supported") 596 if proto == 'h3' and env.curl_uses_lib('msh3'): 597 pytest.skip("msh3 fails here") 598 fdata = os.path.join(env.gen_dir, 'data-10m') 599 count = 1 600 max_upload = 128 * 1024 601 curl = CurlClient(env=env) 602 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?'\ 603 f'id=[0-{count-1}]&max_upload={max_upload}' 604 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, 605 extra_args=['--trace-config', 'all']) 606 r.check_stats(count=count, http_status=413, exitcode=0) 607 608 # speed limited on put handler 609 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 610 def test_07_50_put_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat): 611 if proto == 'h3' and not env.have_h3(): 612 pytest.skip("h3 not supported") 613 count = 1 614 fdata = os.path.join(env.gen_dir, 'data-100k') 615 up_len = 100 * 1024 616 speed_limit = 50 * 1024 617 curl = CurlClient(env=env) 618 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]' 619 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, 620 with_headers=True, extra_args=[ 621 '--limit-rate', f'{speed_limit}' 622 ]) 623 r.check_response(count=count, http_status=200) 624 assert r.responses[0]['header']['received-length'] == f'{up_len}', f'{r.responses[0]}' 625 up_speed = r.stats[0]['speed_upload'] 626 assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}' 627 628 # speed limited on echo handler 629 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 630 def test_07_51_echo_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat): 631 if proto == 'h3' and not env.have_h3(): 632 pytest.skip("h3 not supported") 633 count = 1 634 fdata = os.path.join(env.gen_dir, 'data-100k') 635 speed_limit = 50 * 1024 636 curl = CurlClient(env=env) 637 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]' 638 r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto, 639 with_headers=True, extra_args=[ 640 '--limit-rate', f'{speed_limit}' 641 ]) 642 r.check_response(count=count, http_status=200) 643 up_speed = r.stats[0]['speed_upload'] 644 assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}' 645 646 # upload larger data, triggering "Expect: 100-continue" code paths 647 @pytest.mark.parametrize("proto", ['http/1.1']) 648 def test_07_60_upload_exp100(self, env: Env, httpd, nghttpx, repeat, proto): 649 fdata = os.path.join(env.gen_dir, 'data-1m+') 650 read_delay = 1 651 curl = CurlClient(env=env) 652 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]'\ 653 f'&read_delay={read_delay}s' 654 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, extra_args=[ 655 '--expect100-timeout', f'{read_delay+1}' 656 ]) 657 r.check_stats(count=1, http_status=200, exitcode=0) 658 659 # upload larger data, triggering "Expect: 100-continue" code paths 660 @pytest.mark.parametrize("proto", ['http/1.1']) 661 def test_07_61_upload_exp100_timeout(self, env: Env, httpd, nghttpx, repeat, proto): 662 fdata = os.path.join(env.gen_dir, 'data-1m+') 663 read_delay = 2 664 curl = CurlClient(env=env) 665 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]'\ 666 f'&read_delay={read_delay}s' 667 r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, extra_args=[ 668 '--expect100-timeout', f'{read_delay-1}' 669 ]) 670 r.check_stats(count=1, http_status=200, exitcode=0) 671 672 # nghttpx is the only server we have that supports TLS early data and 673 # has a limit of 16k it announces 674 @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx") 675 @pytest.mark.parametrize("proto,upload_size,exp_early", [ 676 ['http/1.1', 100, 203], # headers+body 677 ['http/1.1', 10*1024, 10345], # headers+body 678 ['http/1.1', 32*1024, 16384], # headers+body, limited by server max 679 ['h2', 10*1024, 10378], # headers+body 680 ['h2', 32*1024, 16384], # headers+body, limited by server max 681 ['h3', 1024, 0], # earlydata not supported 682 ]) 683 def test_07_70_put_earlydata(self, env: Env, httpd, nghttpx, proto, upload_size, exp_early): 684 if not env.curl_uses_lib('gnutls'): 685 pytest.skip('TLS earlydata only implemented in GnuTLS') 686 if proto == 'h3' and not env.have_h3(): 687 pytest.skip("h3 not supported") 688 count = 2 689 # we want this test to always connect to nghttpx, since it is 690 # the only server we have that supports TLS earlydata 691 port = env.port_for(proto) 692 if proto != 'h3': 693 port = env.nghttpx_https_port 694 url = f'https://{env.domain1}:{port}/curltest/put?id=[0-{count-1}]' 695 client = LocalClient(name='hx-upload', env=env) 696 if not client.exists(): 697 pytest.skip(f'example client not built: {client.name}') 698 r = client.run(args=[ 699 '-n', f'{count}', 700 '-e', # use TLS earlydata 701 '-f', # forbid reuse of connections 702 '-l', # announce upload length, no 'Expect: 100' 703 '-S', f'{upload_size}', 704 '-r', f'{env.domain1}:{port}:127.0.0.1', 705 '-V', proto, url 706 ]) 707 r.check_exit_code(0) 708 self.check_downloads(client, [f"{upload_size}"], count) 709 earlydata = {} 710 for line in r.trace_lines: 711 m = re.match(r'^\[t-(\d+)] EarlyData: (\d+)', line) 712 if m: 713 earlydata[int(m.group(1))] = int(m.group(2)) 714 assert earlydata[0] == 0, f'{earlydata}' 715 assert earlydata[1] == exp_early, f'{earlydata}' 716 717 def check_downloads(self, client, source: List[str], count: int, 718 complete: bool = True): 719 for i in range(count): 720 dfile = client.download_file(i) 721 assert os.path.exists(dfile) 722 if complete: 723 diff = "".join(difflib.unified_diff(a=source, 724 b=open(dfile).readlines(), 725 fromfile='-', 726 tofile=dfile, 727 n=1)) 728 assert not diff, f'download {dfile} differs:\n{diff}' 729