test_07_upload.py 35 KB

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