From 7c5637b8b4b8a5a125ba1556e50e4b092075a6a7 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Mon, 6 Mar 2023 12:44:45 +0100 Subject: [PATCH] url: fix logic in connection reuse to deny reuse on "unclean" connections - add parameter to `conn_is_alive()` cfilter method that returns if there is input data waiting on the connection - refrain from re-using connnection from the cache that have input pending - adapt http/2 and http/3 alive checks to digest pending input to check the connection state - remove check_cxn method from openssl as that was just doing what the socket filter now does. - add tests for connection reuse with special server configs Closes #10690 --- lib/cf-socket.c | 19 ++------ lib/cf-socket.h | 7 --- lib/cfilters.c | 11 +++-- lib/cfilters.h | 9 ++-- lib/http2.c | 61 ++++++++++++----------- lib/rtsp.c | 3 +- lib/url.c | 15 +++++- lib/vquic/curl_msh3.c | 4 +- lib/vquic/curl_ngtcp2.c | 28 ++++++++++- lib/vquic/curl_quiche.c | 28 ++++++++++- lib/vtls/nss.c | 32 +------------ lib/vtls/openssl.c | 59 +---------------------- lib/vtls/sectransp.c | 31 +----------- lib/vtls/vtls.c | 17 ++++--- tests/http/test_03_goaway.py | 32 +++++++++++++ tests/http/test_12_reuse.py | 93 ++++++++++++++++++++++++++++++++++++ tests/http/testenv/httpd.py | 5 ++ 17 files changed, 264 insertions(+), 190 deletions(-) create mode 100644 tests/http/test_12_reuse.py diff --git a/lib/cf-socket.c b/lib/cf-socket.c index 006c22d2cd..86b024ac0d 100644 --- a/lib/cf-socket.c +++ b/lib/cf-socket.c @@ -325,20 +325,6 @@ int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn, return socket_close(data, conn, FALSE, sock); } -bool Curl_socket_is_dead(curl_socket_t sock) -{ - int sval; - bool ret_val = TRUE; - - sval = SOCKET_READABLE(sock, 0); - if(sval == 0) - /* timeout */ - ret_val = FALSE; - - return ret_val; -} - - #ifdef USE_WINSOCK /* When you run a program that uses the Windows Sockets API, you may experience slow performance when you copy data to a TCP server. @@ -1449,12 +1435,14 @@ static CURLcode cf_socket_cntrl(struct Curl_cfilter *cf, } static bool cf_socket_conn_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data, + bool *input_pending) { struct cf_socket_ctx *ctx = cf->ctx; struct pollfd pfd[1]; int r; + *input_pending = FALSE; (void)data; if(!ctx || ctx->sock == CURL_SOCKET_BAD) return FALSE; @@ -1479,6 +1467,7 @@ static bool cf_socket_conn_is_alive(struct Curl_cfilter *cf, } DEBUGF(LOG_CF(data, cf, "is_alive: valid events, looks alive")); + *input_pending = TRUE; return TRUE; } diff --git a/lib/cf-socket.h b/lib/cf-socket.h index f6eb810a52..0eec61adb7 100644 --- a/lib/cf-socket.h +++ b/lib/cf-socket.h @@ -70,13 +70,6 @@ CURLcode Curl_socket_open(struct Curl_easy *data, int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn, curl_socket_t sock); -/* - * This function should return TRUE if the socket is to be assumed to - * be dead. Most commonly this happens when the server has closed the - * connection due to inactivity. - */ -bool Curl_socket_is_dead(curl_socket_t sock); - /** * Determine the curl code for a socket connect() == -1 with errno. */ diff --git a/lib/cfilters.c b/lib/cfilters.c index c9932afcd1..e60d1386e6 100644 --- a/lib/cfilters.c +++ b/lib/cfilters.c @@ -124,10 +124,11 @@ ssize_t Curl_cf_def_recv(struct Curl_cfilter *cf, struct Curl_easy *data, } bool Curl_cf_def_conn_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data, + bool *input_pending) { return cf->next? - cf->next->cft->is_alive(cf->next, data) : + cf->next->cft->is_alive(cf->next, data, input_pending) : FALSE; /* pessimistic in absence of data */ } @@ -631,10 +632,12 @@ void Curl_conn_report_connect_stats(struct Curl_easy *data, } } -bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn) +bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn, + bool *input_pending) { struct Curl_cfilter *cf = conn->cfilter[FIRSTSOCKET]; - return cf && !cf->conn->bits.close && cf->cft->is_alive(cf, data); + return cf && !cf->conn->bits.close && + cf->cft->is_alive(cf, data, input_pending); } CURLcode Curl_conn_keep_alive(struct Curl_easy *data, diff --git a/lib/cfilters.h b/lib/cfilters.h index b70770350d..317f2bb191 100644 --- a/lib/cfilters.h +++ b/lib/cfilters.h @@ -85,7 +85,8 @@ typedef ssize_t Curl_cft_recv(struct Curl_cfilter *cf, CURLcode *err); /* error to return */ typedef bool Curl_cft_conn_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data); + struct Curl_easy *data, + bool *input_pending); typedef CURLcode Curl_cft_conn_keep_alive(struct Curl_cfilter *cf, struct Curl_easy *data); @@ -216,7 +217,8 @@ CURLcode Curl_cf_def_cntrl(struct Curl_cfilter *cf, struct Curl_easy *data, int event, int arg1, void *arg2); bool Curl_cf_def_conn_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data); + struct Curl_easy *data, + bool *input_pending); CURLcode Curl_cf_def_conn_keep_alive(struct Curl_cfilter *cf, struct Curl_easy *data); CURLcode Curl_cf_def_query(struct Curl_cfilter *cf, @@ -443,7 +445,8 @@ void Curl_conn_report_connect_stats(struct Curl_easy *data, /** * Check if FIRSTSOCKET's cfilter chain deems connection alive. */ -bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn); +bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn, + bool *input_pending); /** * Try to upkeep the connection filters at sockindex. diff --git a/lib/http2.c b/lib/http2.c index aad8166d2d..89932f6942 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -366,6 +366,15 @@ static void http2_stream_free(struct HTTP *stream) } } +/* + * Returns nonzero if current HTTP/2 session should be closed. + */ +static int should_close_session(struct cf_h2_ctx *ctx) +{ + return ctx->drain_total == 0 && !nghttp2_session_want_read(ctx->h2) && + !nghttp2_session_want_write(ctx->h2); +} + /* * The server may send us data at any point (e.g. PING frames). Therefore, * we cannot assume that an HTTP/2 socket is dead just because it is readable. @@ -373,35 +382,27 @@ static void http2_stream_free(struct HTTP *stream) * Check the lower filters first and, if successful, peek at the socket * and distinguish between closed and data. */ -static bool http2_connisdead(struct Curl_cfilter *cf, struct Curl_easy *data) +static bool http2_connisalive(struct Curl_cfilter *cf, struct Curl_easy *data, + bool *input_pending) { struct cf_h2_ctx *ctx = cf->ctx; - int sval; - bool dead = TRUE; + bool alive = TRUE; - if(!cf->next || !cf->next->cft->is_alive(cf->next, data)) - return TRUE; + *input_pending = FALSE; + if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending)) + return FALSE; - sval = SOCKET_READABLE(Curl_conn_cf_get_socket(cf, data), 0); - if(sval == 0) { - /* timeout */ - dead = FALSE; - } - else if(sval & CURL_CSELECT_ERR) { - /* socket is in an error state */ - dead = TRUE; - } - else if(sval & CURL_CSELECT_IN) { + if(*input_pending) { /* This happens before we've sent off a request and the connection is not in use by any other transfer, there shouldn't be any data here, only "protocol frames" */ CURLcode result; ssize_t nread = -1; + *input_pending = FALSE; Curl_attach_connection(data, cf->conn); nread = Curl_conn_cf_recv(cf->next, data, ctx->inbuf, H2_BUFSIZE, &result); - dead = FALSE; if(nread != -1) { DEBUGF(LOG_CF(data, cf, "%d bytes stray data read before trying " "h2 connection", (int)nread)); @@ -409,15 +410,19 @@ static bool http2_connisdead(struct Curl_cfilter *cf, struct Curl_easy *data) ctx->inbuflen = nread; if(h2_process_pending_input(cf, data, &result) < 0) /* immediate error, considered dead */ - dead = TRUE; + alive = FALSE; + else { + alive = !should_close_session(ctx); + } } - else + else { /* the read failed so let's say this is dead anyway */ - dead = TRUE; + alive = FALSE; + } Curl_detach_connection(data); } - return dead; + return alive; } static CURLcode http2_send_ping(struct Curl_cfilter *cf, @@ -1451,15 +1456,6 @@ CURLcode Curl_http2_request_upgrade(struct dynbuf *req, return result; } -/* - * Returns nonzero if current HTTP/2 session should be closed. - */ -static int should_close_session(struct cf_h2_ctx *ctx) -{ - return ctx->drain_total == 0 && !nghttp2_session_want_read(ctx->h2) && - !nghttp2_session_want_write(ctx->h2); -} - /* * h2_process_pending_input() processes pending input left in * httpc->inbuf. Then, call h2_session_send() to send pending data. @@ -2359,14 +2355,17 @@ static bool cf_h2_data_pending(struct Curl_cfilter *cf, } static bool cf_h2_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data, + bool *input_pending) { struct cf_h2_ctx *ctx = cf->ctx; CURLcode result; struct cf_call_data save; CF_DATA_SAVE(save, cf, data); - result = (ctx && ctx->h2 && !http2_connisdead(cf, data)); + result = (ctx && ctx->h2 && http2_connisalive(cf, data, input_pending)); + DEBUGF(LOG_CF(data, cf, "conn alive -> %d, input_pending=%d", + result, *input_pending)); CF_DATA_RESTORE(cf, save); return result; } diff --git a/lib/rtsp.c b/lib/rtsp.c index 8e37fee381..aef3560a9a 100644 --- a/lib/rtsp.c +++ b/lib/rtsp.c @@ -145,7 +145,8 @@ static unsigned int rtsp_conncheck(struct Curl_easy *data, (void)data; if(checks_to_perform & CONNCHECK_ISDEAD) { - if(!Curl_conn_is_alive(data, conn)) + bool input_pending; + if(!Curl_conn_is_alive(data, conn, &input_pending)) ret_val |= CONNRESULT_DEAD; } diff --git a/lib/url.c b/lib/url.c index 1dc2a4b8fd..35e870a367 100644 --- a/lib/url.c +++ b/lib/url.c @@ -965,7 +965,20 @@ static bool extract_if_dead(struct connectdata *conn, } else { - dead = !Curl_conn_is_alive(data, conn); + bool input_pending; + + dead = !Curl_conn_is_alive(data, conn, &input_pending); + if(input_pending) { + /* For reuse, we want a "clean" connection state. The includes + * that we expect - in general - no waiting input data. Input + * waiting might be a TLS Notify Close, for example. We reject + * that. + * For protocols where data from other other end may arrive at + * any time (HTTP/2 PING for example), the protocol handler needs + * to install its own `connection_check` callback. + */ + dead = TRUE; + } } if(dead) { diff --git a/lib/vquic/curl_msh3.c b/lib/vquic/curl_msh3.c index a74f7ceaa7..530899977d 100644 --- a/lib/vquic/curl_msh3.c +++ b/lib/vquic/curl_msh3.c @@ -769,11 +769,13 @@ static CURLcode cf_msh3_query(struct Curl_cfilter *cf, } static bool cf_msh3_conn_is_alive(struct Curl_cfilter *cf, - struct Curl_easy *data) + struct Curl_easy *data, + bool *input_pending) { struct cf_msh3_ctx *ctx = cf->ctx; (void)data; + *input_pending = FALSE; return ctx && ctx->sock[SP_LOCAL] != CURL_SOCKET_BAD && ctx->qconn && ctx->connected; } diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 7296792856..a52766890e 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -2444,6 +2444,32 @@ static CURLcode cf_ngtcp2_query(struct Curl_cfilter *cf, CURLE_UNKNOWN_OPTION; } +static bool cf_ngtcp2_conn_is_alive(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *input_pending) +{ + bool alive = TRUE; + + *input_pending = FALSE; + if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending)) + return FALSE; + + if(*input_pending) { + /* This happens before we've sent off a request and the connection is + not in use by any other transfer, there shouldn't be any data here, + only "protocol frames" */ + *input_pending = FALSE; + Curl_attach_connection(data, cf->conn); + if(cf_process_ingress(cf, data)) + alive = FALSE; + else { + alive = TRUE; + } + Curl_detach_connection(data); + } + + return alive; +} struct Curl_cftype Curl_cft_http3 = { "HTTP/3", @@ -2458,7 +2484,7 @@ struct Curl_cftype Curl_cft_http3 = { cf_ngtcp2_send, cf_ngtcp2_recv, cf_ngtcp2_data_event, - Curl_cf_def_conn_is_alive, + cf_ngtcp2_conn_is_alive, Curl_cf_def_conn_keep_alive, cf_ngtcp2_query, }; diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index 6df9b85a10..0c48c1c2eb 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -1359,6 +1359,32 @@ static CURLcode cf_quiche_query(struct Curl_cfilter *cf, CURLE_UNKNOWN_OPTION; } +static bool cf_quiche_conn_is_alive(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool *input_pending) +{ + bool alive = TRUE; + + *input_pending = FALSE; + if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending)) + return FALSE; + + if(*input_pending) { + /* This happens before we've sent off a request and the connection is + not in use by any other transfer, there shouldn't be any data here, + only "protocol frames" */ + *input_pending = FALSE; + Curl_attach_connection(data, cf->conn); + if(cf_process_ingress(cf, data)) + alive = FALSE; + else { + alive = TRUE; + } + Curl_detach_connection(data); + } + + return alive; +} struct Curl_cftype Curl_cft_http3 = { "HTTP/3", @@ -1373,7 +1399,7 @@ struct Curl_cftype Curl_cft_http3 = { cf_quiche_send, cf_quiche_recv, cf_quiche_data_event, - Curl_cf_def_conn_is_alive, + cf_quiche_conn_is_alive, Curl_cf_def_conn_keep_alive, cf_quiche_query, }; diff --git a/lib/vtls/nss.c b/lib/vtls/nss.c index 774cbdd7ba..12c03900d3 100644 --- a/lib/vtls/nss.c +++ b/lib/vtls/nss.c @@ -1536,36 +1536,6 @@ static void nss_cleanup(void) initialized = 0; } -/* - * This function uses SSL_peek to determine connection status. - * - * Return codes: - * 1 means the connection is still in place - * 0 means the connection has been closed - * -1 means the connection status is unknown - */ -static int nss_check_cxn(struct Curl_cfilter *cf, struct Curl_easy *data) -{ - struct ssl_connect_data *connssl = cf->ctx; - struct ssl_backend_data *backend = connssl->backend; - int rc; - char buf; - - (void)data; - DEBUGASSERT(backend); - - rc = - PR_Recv(backend->handle, (void *)&buf, 1, PR_MSG_PEEK, - PR_SecondsToInterval(1)); - if(rc > 0) - return 1; /* connection still in place */ - - if(rc == 0) - return 0; /* connection has been closed */ - - return -1; /* connection status unknown */ -} - static void close_one(struct ssl_connect_data *connssl) { /* before the cleanup, check whether we are using a client certificate */ @@ -2524,7 +2494,7 @@ const struct Curl_ssl Curl_ssl_nss = { nss_init, /* init */ nss_cleanup, /* cleanup */ nss_version, /* version */ - nss_check_cxn, /* check_cxn */ + Curl_none_check_cxn, /* check_cxn */ /* NSS has no shutdown function provided and thus always fail */ Curl_none_shutdown, /* shutdown */ nss_data_pending, /* data_pending */ diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 6557783a7f..46e3d51ed1 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -1780,63 +1780,6 @@ static void ossl_cleanup(void) Curl_tls_keylog_close(); } -/* - * This function is used to determine connection status. - * - * Return codes: - * 1 means the connection is still in place - * 0 means the connection has been closed - * -1 means the connection status is unknown - */ -static int ossl_check_cxn(struct Curl_cfilter *cf, struct Curl_easy *data) -{ - /* SSL_peek takes data out of the raw recv buffer without peeking so we use - recv MSG_PEEK instead. Bug #795 */ -#ifdef MSG_PEEK - char buf; - ssize_t nread; - curl_socket_t sock = Curl_conn_cf_get_socket(cf, data); - if(sock == CURL_SOCKET_BAD) - return 0; /* no socket, consider closed */ - nread = recv((RECV_TYPE_ARG1)sock, - (RECV_TYPE_ARG2)&buf, (RECV_TYPE_ARG3)1, - (RECV_TYPE_ARG4)MSG_PEEK); - if(nread == 0) - return 0; /* connection has been closed */ - if(nread == 1) - return 1; /* connection still in place */ - else if(nread == -1) { - int err = SOCKERRNO; - if(err == EINPROGRESS || -#if defined(EAGAIN) && (EAGAIN != EWOULDBLOCK) - err == EAGAIN || -#endif - err == EWOULDBLOCK) - return 1; /* connection still in place */ - if(err == ECONNRESET || -#ifdef ECONNABORTED - err == ECONNABORTED || -#endif -#ifdef ENETDOWN - err == ENETDOWN || -#endif -#ifdef ENETRESET - err == ENETRESET || -#endif -#ifdef ESHUTDOWN - err == ESHUTDOWN || -#endif -#ifdef ETIMEDOUT - err == ETIMEDOUT || -#endif - err == ENOTCONN) - return 0; /* connection has been closed */ - } -#endif - (void)data; - return -1; /* connection status unknown */ -} - /* Selects an OpenSSL crypto engine */ static CURLcode ossl_set_engine(struct Curl_easy *data, const char *engine) @@ -4820,7 +4763,7 @@ const struct Curl_ssl Curl_ssl_openssl = { ossl_init, /* init */ ossl_cleanup, /* cleanup */ ossl_version, /* version */ - ossl_check_cxn, /* check_cxn */ + Curl_none_check_cxn, /* check_cxn */ ossl_shutdown, /* shutdown */ ossl_data_pending, /* data_pending */ ossl_random, /* random */ diff --git a/lib/vtls/sectransp.c b/lib/vtls/sectransp.c index 0e1b06187f..8e9198f1aa 100644 --- a/lib/vtls/sectransp.c +++ b/lib/vtls/sectransp.c @@ -3233,35 +3233,6 @@ static size_t sectransp_version(char *buffer, size_t size) return msnprintf(buffer, size, "SecureTransport"); } -/* - * This function uses SSLGetSessionState to determine connection status. - * - * Return codes: - * 1 means the connection is still in place - * 0 means the connection has been closed - * -1 means the connection status is unknown - */ -static int sectransp_check_cxn(struct Curl_cfilter *cf, - struct Curl_easy *data) -{ - struct ssl_connect_data *connssl = cf->ctx; - struct ssl_backend_data *backend = connssl->backend; - OSStatus err; - SSLSessionState state; - - (void)data; - DEBUGASSERT(backend); - - if(backend->ssl_ctx) { - DEBUGF(LOG_CF(data, cf, "check connection")); - err = SSLGetSessionState(backend->ssl_ctx, &state); - if(err == noErr) - return state == kSSLConnected || state == kSSLHandshake; - return -1; - } - return 0; -} - static bool sectransp_data_pending(struct Curl_cfilter *cf, const struct Curl_easy *data) { @@ -3473,7 +3444,7 @@ const struct Curl_ssl Curl_ssl_sectransp = { Curl_none_init, /* init */ Curl_none_cleanup, /* cleanup */ sectransp_version, /* version */ - sectransp_check_cxn, /* check_cxn */ + Curl_none_check_cxn, /* check_cxn */ sectransp_shutdown, /* shutdown */ sectransp_data_pending, /* data_pending */ sectransp_random, /* random */ diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 108ac68d15..144e6ee5b8 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -1650,10 +1650,11 @@ static CURLcode ssl_cf_query(struct Curl_cfilter *cf, CURLE_UNKNOWN_OPTION; } -static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data) +static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data, + bool *input_pending) { struct cf_call_data save; - bool result; + int result; /* * This function tries to determine connection status. * @@ -1663,15 +1664,19 @@ static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data) * -1 means the connection status is unknown */ CF_DATA_SAVE(save, cf, data); - result = Curl_ssl->check_cxn(cf, data) != 0; + result = Curl_ssl->check_cxn(cf, data); CF_DATA_RESTORE(cf, save); - if(result > 0) + if(result > 0) { + *input_pending = TRUE; return TRUE; - if(result == 0) + } + if(result == 0) { + *input_pending = FALSE; return FALSE; + } /* ssl backend does not know */ return cf->next? - cf->next->cft->is_alive(cf->next, data) : + cf->next->cft->is_alive(cf->next, data, input_pending) : FALSE; /* pessimistic in absence of data */ } diff --git a/tests/http/test_03_goaway.py b/tests/http/test_03_goaway.py index 6444ce1cb9..c6bf8ed26c 100644 --- a/tests/http/test_03_goaway.py +++ b/tests/http/test_03_goaway.py @@ -110,4 +110,36 @@ class TestGoAway: assert r.duration >= timedelta(seconds=count) r.check_stats(count=count, exp_status=200, exp_exitcode=0) + # download files sequentially with delay, reload server for GOAWAY + def test_03_03_h1_goaway(self, env: Env, httpd, nghttpx, repeat): + proto = 'http/1.1' + count = 3 + self.r = None + def long_run(): + curl = CurlClient(env=env) + # send 10 chunks of 1024 bytes in a response body with 100ms delay in between + urln = f'https://{env.authority_for(env.domain1, proto)}' \ + f'/curltest/tweak?id=[0-{count - 1}]'\ + '&chunks=10&chunk_size=1024&chunk_delay=100ms' + self.r = curl.http_download(urls=[urln], alpn_proto=proto) + + t = Thread(target=long_run) + t.start() + # each request will take a second, reload the server in the middle + # of the first one. + time.sleep(1.5) + assert httpd.reload() + t.join() + r: ExecResult = self.r + assert r.exit_code == 0, f'{r}' + r.check_stats(count=count, exp_status=200) + # reload will shut down the connection gracefully with GOAWAY + # we expect to see a second connection opened afterwards + assert r.total_connects == 2 + for idx, s in enumerate(r.stats): + if s['num_connects'] > 0: + log.debug(f'request {idx} connected') + # this should take `count` seconds to retrieve + assert r.duration >= timedelta(seconds=count) + diff --git a/tests/http/test_12_reuse.py b/tests/http/test_12_reuse.py new file mode 100644 index 0000000000..0e4bfadc5d --- /dev/null +++ b/tests/http/test_12_reuse.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#*************************************************************************** +# _ _ ____ _ +# Project ___| | | | _ \| | +# / __| | | | |_) | | +# | (__| |_| | _ <| |___ +# \___|\___/|_| \_\_____| +# +# Copyright (C) 2008 - 2022, Daniel Stenberg, , et al. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at https://curl.se/docs/copyright.html. +# +# You may opt to use, copy, modify, merge, publish, distribute and/or sell +# copies of the Software, and permit persons to whom the Software is +# furnished to do so, under the terms of the COPYING file. +# +# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +# KIND, either express or implied. +# +# SPDX-License-Identifier: curl +# +########################################################################### +# +import difflib +import filecmp +import logging +import os +import pytest + +from testenv import Env, CurlClient + + +log = logging.getLogger(__name__) + + +@pytest.mark.skipif(condition=Env.setup_incomplete(), + reason=f"missing: {Env.incomplete_reason()}") +class TestReuse: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env, httpd, nghttpx): + env.make_data_file(indir=httpd.docs_dir, fname="data-100k", fsize=100*1024) + env.make_data_file(indir=httpd.docs_dir, fname="data-1m", fsize=1024*1024) + env.make_data_file(indir=httpd.docs_dir, fname="data-10m", fsize=10*1024*1024) + yield + # restore default config + httpd.clear_extra_configs() + httpd.reload() + + # check if HTTP/1.1 handles 'Connection: close' correctly + @pytest.mark.parametrize("proto", ['http/1.1']) + def test_12_01_h1_conn_close(self, env: Env, + httpd, nghttpx, repeat, proto): + httpd.clear_extra_configs() + httpd.set_extra_config('base', [ + f'MaxKeepAliveRequests 1', + ]) + httpd.reload() + count = 100 + curl = CurlClient(env=env) + urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]' + r = curl.http_download(urls=[urln], alpn_proto=proto) + assert r.exit_code == 0 + r.check_stats(count=count, exp_status=200) + # Server sends `Connection: close` on every 2nd request, requiring + # a new connection + assert r.total_connects == count/2 + + @pytest.mark.parametrize("proto", ['http/1.1']) + def test_12_02_h1_conn_timeout(self, env: Env, + httpd, nghttpx, repeat, proto): + httpd.clear_extra_configs() + httpd.set_extra_config('base', [ + f'KeepAliveTimeout 1', + ]) + httpd.reload() + count = 5 + curl = CurlClient(env=env) + urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]' + r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ + '--rate', '30/m', + ]) + assert r.exit_code == 0 + r.check_stats(count=count, exp_status=200) + # Connections time out on server before we send another request, + assert r.total_connects == count + # we do not see how often a request was retried in the stats, so + # we cannot check that connection reuse attempted a connection that + # was later detected to be "dead". We would like to + # assert stat['retry_count'] == 0 diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py index fd7be84f31..de058ebe22 100644 --- a/tests/http/testenv/httpd.py +++ b/tests/http/testenv/httpd.py @@ -100,6 +100,9 @@ class Httpd: else: self._extra_configs[domain] = lines + def clear_extra_configs(self): + self._extra_configs = {} + def _run(self, args, intext=''): env = {} for key, val in os.environ.items(): @@ -231,6 +234,8 @@ class Httpd: f'Listen {self.env.proxys_port}', f'TypesConfig "{self._conf_dir}/mime.types', ] + if 'base' in self._extra_configs: + conf.extend(self._extra_configs['base']) conf.extend([ # plain http host for domain1 f'', f' ServerName {domain1}', -- 2.47.3