tests: pin connection reuse rules for partial, host, and credentials
No binary (build failed)
—
(log yok)
| @@ -284,6 +284,7 @@ test3100 test3101 test3102 test3103 test3104 test3105 test3106 \ | |
| test3200 test3201 test3202 test3203 test3204 test3205 test3206 test3207 \ | |
| test3208 test3209 test3210 test3211 test3212 test3213 test3214 test3215 \ | |
| test3216 test3217 test3218 test3219 test3220 test3221 test3222 \ | |
| + | test3223 test3224 test3225 test3226 \ |
| \ | |
| test3300 test3301 test3302 test3303 test3304 test3305 \ | |
| \ | |
| @@ -0,0 +1,67 @@ | |
| + | <?xml version="1.0" encoding="US-ASCII"?> |
| + | <testcase> |
| + | # A partial/aborted HTTP/1.1 response marks the transfer premature. |
| + | # multi_conn_should_close() then closes non-multiplexed connections so they |
| + | # cannot be reused by a later transfer in the same process. |
| + | <info> |
| + | <keywords> |
| + | HTTP |
| + | HTTP GET |
| + | CURLE_PARTIAL_FILE |
| + | connection reuse |
| + | --next |
| + | </keywords> |
| + | </info> |
| + | <reply> |
| + | <data nocheck="yes"> |
| + | HTTP/1.1 200 OK swsclose |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 100 |
| + | Content-Type: text/plain |
| + | |
| + | 0123456789 |
| + | </data> |
| + | <data1 nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | OK |
| + | </data1> |
| + | <servercmd> |
| + | connection-monitor |
| + | </servercmd> |
| + | </reply> |
| + | <client> |
| + | <server> |
| + | http |
| + | </server> |
| + | <name> |
| + | HTTP connection not reused after partial/aborted response |
| + | </name> |
| + | <command> |
| + | http://%HOSTIP:%HTTPPORT/%TESTNUMBER -o %LOGDIR/3223-1 -w "connects=%{num_connects}/" --next http://%HOSTIP:%HTTPPORT/%TESTNUMBER0001 -o %LOGDIR/3223-2 -w "connects=%{num_connects}/" |
| + | </command> |
| + | </client> |
| + | <verify> |
| + | <protocol crlf="headers"> |
| + | GET /%TESTNUMBER HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | [DISCONNECT] |
| + | GET /%TESTNUMBER0001 HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | [DISCONNECT] |
| + | </protocol> |
| + | # Second transfer must open a new connection (cannot reuse after premature). |
| + | <stdout nonewline="yes"> |
| + | connects=1/connects=1/ |
| + | </stdout> |
| + | </verify> |
| + | </testcase> |
| @@ -0,0 +1,69 @@ | |
| + | <?xml version="1.0" encoding="US-ASCII"?> |
| + | <testcase> |
| + | # HTTP has PROTOPT_CREDSPERREQUEST: Basic credentials are attached to the |
| + | # request, not to the connection for reuse matching. Different -u values |
| + | # therefore still reuse the idle connection (see also test1134). Protocols |
| + | # without CREDSPERREQUEST (and NTLM/Negotiate mid-handshake) refuse reuse. |
| + | # This test pins the HTTP Basic behavior with an explicit num_connects check. |
| + | <info> |
| + | <keywords> |
| + | HTTP |
| + | HTTP GET |
| + | HTTP Basic auth |
| + | connection reuse |
| + | --next |
| + | </keywords> |
| + | </info> |
| + | <reply> |
| + | <data nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | A1 |
| + | </data> |
| + | <data1 nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | B2 |
| + | </data1> |
| + | <servercmd> |
| + | connection-monitor |
| + | </servercmd> |
| + | </reply> |
| + | <client> |
| + | <server> |
| + | http |
| + | </server> |
| + | <name> |
| + | HTTP connection reused across different Basic auth credentials |
| + | </name> |
| + | <command> |
| + | http://%HOSTIP:%HTTPPORT/%TESTNUMBER -u user1:password1 -o %LOGDIR/3224-1 -w "connects=%{num_connects}/" --next http://%HOSTIP:%HTTPPORT/%TESTNUMBER0001 -u user2:password2 -o %LOGDIR/3224-2 -w "connects=%{num_connects}/" |
| + | </command> |
| + | </client> |
| + | <verify> |
| + | <protocol crlf="headers"> |
| + | GET /%TESTNUMBER HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | Authorization: Basic %b64[user1:password1]b64% |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | GET /%TESTNUMBER0001 HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | Authorization: Basic %b64[user2:password2]b64% |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | [DISCONNECT] |
| + | </protocol> |
| + | <stdout nonewline="yes"> |
| + | connects=1/connects=0/ |
| + | </stdout> |
| + | </verify> |
| + | </testcase> |
| @@ -0,0 +1,69 @@ | |
| + | <?xml version="1.0" encoding="US-ASCII"?> |
| + | <testcase> |
| + | # url_match_destination() / Curl_peer_same_destination() require matching |
| + | # origin hostname and port. Different Host names that resolve to the same |
| + | # address must not share a connection. Both connections may stay in the |
| + | # pool until process exit, so the server may see two DISCONNECT events only |
| + | # at the end; num_connects proves a new connection was opened. |
| + | <info> |
| + | <keywords> |
| + | HTTP |
| + | HTTP GET |
| + | --resolve |
| + | connection reuse |
| + | --next |
| + | </keywords> |
| + | </info> |
| + | <reply> |
| + | <data nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | H1 |
| + | </data> |
| + | <data1 nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | H2 |
| + | </data1> |
| + | <servercmd> |
| + | connection-monitor |
| + | </servercmd> |
| + | </reply> |
| + | <client> |
| + | <server> |
| + | http |
| + | </server> |
| + | <name> |
| + | HTTP connection not reused when target host changes |
| + | </name> |
| + | <command> |
| + | http://host-a.example.test:%HTTPPORT/%TESTNUMBER --resolve host-a.example.test:%HTTPPORT:%HOSTIP -o %LOGDIR/3225-1 -w "connects=%{num_connects}/" --next http://host-b.example.test:%HTTPPORT/%TESTNUMBER0001 --resolve host-b.example.test:%HTTPPORT:%HOSTIP -o %LOGDIR/3225-2 -w "connects=%{num_connects}/" |
| + | </command> |
| + | </client> |
| + | <verify> |
| + | <protocol crlf="headers"> |
| + | GET /%TESTNUMBER HTTP/1.1 |
| + | Host: host-a.example.test:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | GET /%TESTNUMBER0001 HTTP/1.1 |
| + | Host: host-b.example.test:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | [DISCONNECT] |
| + | [DISCONNECT] |
| + | </protocol> |
| + | # Same IP via --resolve, different Host names => new connection each time. |
| + | <stdout nonewline="yes"> |
| + | connects=1/connects=1/ |
| + | </stdout> |
| + | </verify> |
| + | </testcase> |
| @@ -0,0 +1,63 @@ | |
| + | <?xml version="1.0" encoding="US-ASCII"?> |
| + | <testcase> |
| + | # Positive control: same host reuses the idle connection |
| + | # (second transfer reports num_connects=0). |
| + | <info> |
| + | <keywords> |
| + | HTTP |
| + | HTTP GET |
| + | connection reuse |
| + | --next |
| + | </keywords> |
| + | </info> |
| + | <reply> |
| + | <data nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | OK |
| + | </data> |
| + | <data1 nocheck="yes"> |
| + | HTTP/1.1 200 OK |
| + | Date: Tue, 09 Nov 2010 14:49:00 GMT |
| + | Content-Length: 3 |
| + | Content-Type: text/plain |
| + | |
| + | OK |
| + | </data1> |
| + | <servercmd> |
| + | connection-monitor |
| + | </servercmd> |
| + | </reply> |
| + | <client> |
| + | <server> |
| + | http |
| + | </server> |
| + | <name> |
| + | HTTP connection is reused for same host |
| + | </name> |
| + | <command> |
| + | http://%HOSTIP:%HTTPPORT/%TESTNUMBER -o %LOGDIR/3226-1 -w "connects=%{num_connects}/" --next http://%HOSTIP:%HTTPPORT/%TESTNUMBER0001 -o %LOGDIR/3226-2 -w "connects=%{num_connects}/" |
| + | </command> |
| + | </client> |
| + | <verify> |
| + | <protocol crlf="headers"> |
| + | GET /%TESTNUMBER HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | GET /%TESTNUMBER0001 HTTP/1.1 |
| + | Host: %HOSTIP:%HTTPPORT |
| + | User-Agent: curl/%VERSION |
| + | Accept: */* |
| + | |
| + | [DISCONNECT] |
| + | </protocol> |
| + | <stdout nonewline="yes"> |
| + | connects=1/connects=0/ |
| + | </stdout> |
| + | </verify> |
| + | </testcase> |
| @@ -71,3 +71,71 @@ class TestReuse: | |
| r.check_response(count=count, http_status=200) | |
| # Connections time out on server before we send another request, | |
| assert r.total_connects == count | |
| + | |
| + | # After a partial/aborted HTTP/1.1 response the connection must not be |
| + | # reused (multi_conn_should_close with premature on non-multiplexed conn). |
| + | @pytest.mark.parametrize("proto", ['http/1.1']) |
| + | def test_12_03_no_reuse_after_partial(self, env: Env, httpd, nghttpx, proto): |
| + | curl = CurlClient(env=env) |
| + | auth = env.authority_for(env.domain1, proto) |
| + | # Server promises more bytes than it sends, then resets. |
| + | partial = f'https://{auth}/curltest/tweak?id=0&chunks=1&chunk_size=100&body_error=reset' |
| + | ok = f'https://{auth}/data.json' |
| + | r = curl.http_download(urls=[partial, ok], alpn_proto=proto, extra_args=[ |
| + | '--retry', '0', |
| + | ]) |
| + | # First transfer fails (partial/reset); second succeeds on a new connection. |
| + | assert len(r.stats) == 2, r.dump_logs() |
| + | assert r.stats[0]['exitcode'] != 0, r.dump_logs() |
| + | assert r.stats[1].get('http_code') == 200, r.dump_logs() |
| + | # Both transfers must open their own connection. |
| + | assert r.stats[0]['num_connects'] == 1, r.dump_logs() |
| + | assert r.stats[1]['num_connects'] == 1, r.dump_logs() |
| + | assert r.total_connects == 2, r.dump_logs() |
| + | |
| + | # HTTP uses PROTOPT_CREDSPERREQUEST: Basic credentials are per request, so |
| + | # different -u values still reuse the idle connection (unlike NTLM/Negotiate |
| + | # which bind credentials onto the connection). |
| + | @pytest.mark.parametrize("proto", ['http/1.1']) |
| + | def test_12_04_reuse_different_basic_credentials(self, env: Env, httpd, nghttpx, proto): |
| + | curl = CurlClient(env=env) |
| + | url1 = f'https://{env.authority_for(env.domain1, proto)}/data.json?cred=1' |
| + | url2 = f'https://{env.authority_for(env.domain1, proto)}/data.json?cred=2' |
| + | r = curl.http_download(urls=[url1, url2], alpn_proto=proto, url_options={ |
| + | url1: ['-u', 'user1:password1'], |
| + | url2: ['-u', 'user2:password2'], |
| + | }) |
| + | assert len(r.stats) == 2, r.dump_logs() |
| + | assert r.stats[0].get('http_code') == 200, r.dump_logs() |
| + | assert r.stats[1].get('http_code') == 200, r.dump_logs() |
| + | assert r.stats[0]['num_connects'] == 1, r.dump_logs() |
| + | assert r.stats[1]['num_connects'] == 0, r.dump_logs() |
| + | assert r.total_connects == 1, r.dump_logs() |
| + | |
| + | # Different target hostnames must not reuse even if they resolve to the |
| + | # same address (Curl_peer_same_destination matches hostname + port). |
| + | @pytest.mark.parametrize("proto", ['http/1.1']) |
| + | def test_12_05_no_reuse_different_host(self, env: Env, httpd, nghttpx, proto): |
| + | curl = CurlClient(env=env) |
| + | # domain1 and domain2 are both served by the same httpd instance. |
| + | url1 = f'https://{env.authority_for(env.domain1, proto)}/data.json' |
| + | url2 = f'https://{env.authority_for(env.domain2, proto)}/data.json' |
| + | r = curl.http_download(urls=[url1, url2], alpn_proto=proto) |
| + | r.check_response(count=2, http_status=200) |
| + | assert r.stats[0]['num_connects'] == 1, r.dump_logs() |
| + | assert r.stats[1]['num_connects'] == 1, r.dump_logs() |
| + | assert r.total_connects == 2, r.dump_logs() |
| + | |
| + | # Positive control: same host reuses one connection. |
| + | @pytest.mark.parametrize("proto", ['http/1.1']) |
| + | def test_12_06_reuse_same_host(self, env: Env, httpd, nghttpx, proto): |
| + | curl = CurlClient(env=env) |
| + | url1 = f'https://{env.authority_for(env.domain1, proto)}/data.json?a=1' |
| + | url2 = f'https://{env.authority_for(env.domain1, proto)}/data.json?a=2' |
| + | r = curl.http_download(urls=[url1, url2], alpn_proto=proto) |
| + | assert len(r.stats) == 2, r.dump_logs() |
| + | assert r.stats[0].get('http_code') == 200, r.dump_logs() |
| + | assert r.stats[1].get('http_code') == 200, r.dump_logs() |
| + | assert r.stats[0]['num_connects'] == 1, r.dump_logs() |
| + | assert r.stats[1]['num_connects'] == 0, r.dump_logs() |
| + | assert r.total_connects == 1, r.dump_logs() |