test_01_basic.py (13004B)
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 logging 28 import pytest 29 30 from testenv import Env 31 from testenv import CurlClient 32 33 34 log = logging.getLogger(__name__) 35 36 37 class TestBasic: 38 39 # simple http: GET 40 def test_01_01_http_get(self, env: Env, httpd): 41 curl = CurlClient(env=env) 42 url = f'http://{env.domain1}:{env.http_port}/data.json' 43 r = curl.http_get(url=url) 44 r.check_response(http_status=200) 45 assert r.json['server'] == env.domain1 46 47 # simple https: GET, any http version 48 @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 49 def test_01_02_https_get(self, env: Env, httpd): 50 curl = CurlClient(env=env) 51 url = f'https://{env.domain1}:{env.https_port}/data.json' 52 r = curl.http_get(url=url) 53 r.check_response(http_status=200) 54 assert r.json['server'] == env.domain1 55 56 # simple https: GET, h2 wanted and got 57 @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 58 def test_01_03_h2_get(self, env: Env, httpd): 59 curl = CurlClient(env=env) 60 url = f'https://{env.domain1}:{env.https_port}/data.json' 61 r = curl.http_get(url=url, extra_args=['--http2']) 62 r.check_response(http_status=200, protocol='HTTP/2') 63 assert r.json['server'] == env.domain1 64 65 # simple https: GET, h2 unsupported, fallback to h1 66 @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 67 def test_01_04_h2_unsupported(self, env: Env, httpd): 68 curl = CurlClient(env=env) 69 url = f'https://{env.domain2}:{env.https_port}/data.json' 70 r = curl.http_get(url=url, extra_args=['--http2']) 71 r.check_response(http_status=200, protocol='HTTP/1.1') 72 assert r.json['server'] == env.domain2 73 74 # simple h3: GET, want h3 and get it 75 @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") 76 def test_01_05_h3_get(self, env: Env, httpd, nghttpx): 77 curl = CurlClient(env=env) 78 url = f'https://{env.domain1}:{env.h3_port}/data.json' 79 r = curl.http_get(url=url, extra_args=['--http3-only']) 80 r.check_response(http_status=200, protocol='HTTP/3') 81 assert r.json['server'] == env.domain1 82 83 # simple download, check connect/handshake timings 84 @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 85 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 86 def test_01_06_timings(self, env: Env, httpd, nghttpx, proto): 87 if proto == 'h3' and not env.have_h3(): 88 pytest.skip("h3 not supported") 89 curl = CurlClient(env=env) 90 url = f'https://{env.authority_for(env.domain1, proto)}/data.json' 91 r = curl.http_download(urls=[url], alpn_proto=proto, with_stats=True) 92 r.check_stats(http_status=200, count=1, 93 remote_port=env.port_for(alpn_proto=proto), 94 remote_ip='127.0.0.1') 95 # there are cases where time_connect is reported as 0 96 assert r.stats[0]['time_connect'] >= 0, f'{r.stats[0]}' 97 assert r.stats[0]['time_appconnect'] > 0, f'{r.stats[0]}' 98 99 # simple https: HEAD 100 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 101 @pytest.mark.skipif(condition=not Env.have_ssl_curl(), reason="curl without SSL") 102 def test_01_07_head(self, env: Env, httpd, nghttpx, proto): 103 if proto == 'h3' and not env.have_h3(): 104 pytest.skip("h3 not supported") 105 curl = CurlClient(env=env) 106 url = f'https://{env.authority_for(env.domain1, proto)}/data.json' 107 r = curl.http_download(urls=[url], with_stats=True, with_headers=True, 108 extra_args=['-I']) 109 r.check_stats(http_status=200, count=1, exitcode=0, 110 remote_port=env.port_for(alpn_proto=proto), 111 remote_ip='127.0.0.1') 112 # got the Content-Length: header, but did not download anything 113 assert r.responses[0]['header']['content-length'] == '30', f'{r.responses[0]}' 114 assert r.stats[0]['size_download'] == 0, f'{r.stats[0]}' 115 116 # http: GET for HTTP/2, see Upgrade:, 101 switch 117 def test_01_08_h2_upgrade(self, env: Env, httpd): 118 curl = CurlClient(env=env) 119 url = f'http://{env.domain1}:{env.http_port}/data.json' 120 r = curl.http_get(url=url, extra_args=['--http2']) 121 r.check_exit_code(0) 122 assert len(r.responses) == 2, f'{r.responses}' 123 assert r.responses[0]['status'] == 101, f'{r.responses[0]}' 124 assert r.responses[1]['status'] == 200, f'{r.responses[1]}' 125 assert r.responses[1]['protocol'] == 'HTTP/2', f'{r.responses[1]}' 126 assert r.json['server'] == env.domain1 127 128 # http: GET for HTTP/2 with prior knowledge 129 def test_01_09_h2_prior_knowledge(self, env: Env, httpd): 130 curl = CurlClient(env=env) 131 url = f'http://{env.domain1}:{env.http_port}/data.json' 132 r = curl.http_get(url=url, extra_args=['--http2-prior-knowledge']) 133 r.check_exit_code(0) 134 assert len(r.responses) == 1, f'{r.responses}' 135 assert r.response['status'] == 200, f'{r.responsw}' 136 assert r.response['protocol'] == 'HTTP/2', f'{r.response}' 137 assert r.json['server'] == env.domain1 138 139 # http: strip TE header in HTTP/2 requests 140 def test_01_10_te_strip(self, env: Env, httpd): 141 curl = CurlClient(env=env) 142 url = f'https://{env.authority_for(env.domain1, "h2")}/data.json' 143 r = curl.http_get(url=url, extra_args=['--http2', '-H', 'TE: gzip']) 144 r.check_exit_code(0) 145 assert len(r.responses) == 1, f'{r.responses}' 146 assert r.responses[0]['status'] == 200, f'{r.responses[1]}' 147 assert r.responses[0]['protocol'] == 'HTTP/2', f'{r.responses[1]}' 148 149 # http: large response headers 150 # send 48KB+ sized response headers to check we handle that correctly 151 # larger than 64KB headers expose a bug in Apache HTTP/2 that is not 152 # RSTing the stream correctly when its internal limits are exceeded. 153 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 154 def test_01_11_large_resp_headers(self, env: Env, httpd, proto): 155 if proto == 'h3' and not env.have_h3(): 156 pytest.skip("h3 not supported") 157 curl = CurlClient(env=env) 158 url = f'https://{env.authority_for(env.domain1, proto)}' \ 159 f'/curltest/tweak?x-hd={48 * 1024}' 160 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[]) 161 r.check_exit_code(0) 162 assert len(r.responses) == 1, f'{r.responses}' 163 assert r.responses[0]['status'] == 200, f'{r.responses}' 164 165 # http: response headers larger than what curl buffers for 166 @pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'), 167 reason='httpd must be at least 2.4.64') 168 @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) 169 def test_01_12_xlarge_resp_headers(self, env: Env, httpd, configures_httpd, proto): 170 httpd.set_extra_config('base', [ 171 f'H2MaxHeaderBlockLen {130 * 1024}', 172 ]) 173 httpd.reload_if_config_changed() 174 curl = CurlClient(env=env) 175 url = f'https://{env.authority_for(env.domain1, proto)}' \ 176 f'/curltest/tweak?x-hd={128 * 1024}' 177 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[]) 178 r.check_exit_code(0) 179 assert len(r.responses) == 1, f'{r.responses}' 180 assert r.responses[0]['status'] == 200, f'{r.responses}' 181 182 # http: 1 response header larger than what curl buffers for 183 @pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'), 184 reason='httpd must be at least 2.4.64') 185 @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) 186 def test_01_13_megalarge_resp_headers(self, env: Env, httpd, configures_httpd, proto): 187 httpd.set_extra_config('base', [ 188 'LogLevel http2:trace2', 189 f'H2MaxHeaderBlockLen {130 * 1024}', 190 ]) 191 httpd.reload_if_config_changed() 192 curl = CurlClient(env=env) 193 url = f'https://{env.authority_for(env.domain1, proto)}' \ 194 f'/curltest/tweak?x-hd1={128 * 1024}' 195 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[]) 196 if proto == 'h2': 197 r.check_exit_code(16) # CURLE_HTTP2 198 else: 199 r.check_exit_code(100) # CURLE_TOO_LARGE 200 201 # http: several response headers, together > 256 KB 202 # nghttp2 error -905: Too many CONTINUATION frames following a HEADER frame 203 @pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'), 204 reason='httpd must be at least 2.4.64') 205 @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) 206 def test_01_14_gigalarge_resp_headers(self, env: Env, httpd, configures_httpd, proto): 207 httpd.set_extra_config('base', [ 208 'LogLevel http2:trace2', 209 f'H2MaxHeaderBlockLen {1024 * 1024}', 210 ]) 211 httpd.reload_if_config_changed() 212 curl = CurlClient(env=env) 213 url = f'https://{env.authority_for(env.domain1, proto)}' \ 214 f'/curltest/tweak?x-hd={256 * 1024}' 215 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[]) 216 if proto == 'h2': 217 r.check_exit_code(16) # CURLE_HTTP2 218 else: 219 r.check_exit_code(0) # 1.1 can do 220 221 # http: one response header > 256 KB 222 @pytest.mark.skipif(condition=not Env.httpd_is_at_least('2.4.64'), 223 reason='httpd must be at least 2.4.64') 224 @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) 225 def test_01_15_gigalarge_resp_headers(self, env: Env, httpd, configures_httpd, proto): 226 httpd.set_extra_config('base', [ 227 'LogLevel http2:trace2', 228 f'H2MaxHeaderBlockLen {1024 * 1024}', 229 ]) 230 httpd.reload_if_config_changed() 231 curl = CurlClient(env=env) 232 url = f'https://{env.authority_for(env.domain1, proto)}' \ 233 f'/curltest/tweak?x-hd1={256 * 1024}' 234 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[]) 235 if proto == 'h2': 236 r.check_exit_code(16) # CURLE_HTTP2 237 else: 238 r.check_exit_code(100) # CURLE_TOO_LARGE 239 240 # http: invalid request headers, GET, issue #16998 241 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 242 def test_01_16_inv_req_get(self, env: Env, httpd, nghttpx, proto): 243 if proto == 'h3' and not env.have_h3(): 244 pytest.skip("h3 not supported") 245 curl = CurlClient(env=env) 246 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo' 247 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ 248 '-H', "a: a\x0ab" 249 ]) 250 # on h1, request is sent, h2/h3 reject 251 if proto == 'http/1.1': 252 r.check_exit_code(0) 253 else: 254 r.check_exit_code(43) 255 256 # http: special handling of TE request header 257 @pytest.mark.parametrize("te_in, te_out", [ 258 pytest.param('trailers', 'trailers', id='trailers'), 259 pytest.param('chunked', None, id='chunked'), 260 pytest.param('gzip, trailers', 'trailers', id='gzip+trailers'), 261 pytest.param('gzip ;q=0.2;x="y,x", trailers', 'trailers', id='gzip+q+x+trailers'), 262 pytest.param('gzip ;x="trailers", chunks', None, id='gzip+x+chunks'), 263 ]) 264 def test_01_17_TE(self, env: Env, httpd, te_in, te_out): 265 proto = 'h2' 266 curl = CurlClient(env=env) 267 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo' 268 r = curl.http_download(urls=[url], alpn_proto=proto, with_stats=True, 269 with_headers=True, 270 extra_args=['-H', f'TE: {te_in}']) 271 r.check_response(200) 272 if te_out is not None: 273 assert r.responses[0]['header']['request-te'] == te_out, f'{r.responses[0]}' 274 else: 275 assert 'request-te' not in r.responses[0]['header'], f'{r.responses[0]}'