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
This commit is contained in:
Stefan Eissing 2023-03-06 12:44:45 +01:00 committed by Daniel Stenberg
parent 6466071e8e
commit 7c5637b8b4
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
17 changed files with 264 additions and 190 deletions

View File

@ -325,20 +325,6 @@ int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn,
return socket_close(data, conn, FALSE, sock); 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 #ifdef USE_WINSOCK
/* When you run a program that uses the Windows Sockets API, you may /* When you run a program that uses the Windows Sockets API, you may
experience slow performance when you copy data to a TCP server. 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, 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 cf_socket_ctx *ctx = cf->ctx;
struct pollfd pfd[1]; struct pollfd pfd[1];
int r; int r;
*input_pending = FALSE;
(void)data; (void)data;
if(!ctx || ctx->sock == CURL_SOCKET_BAD) if(!ctx || ctx->sock == CURL_SOCKET_BAD)
return FALSE; 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")); DEBUGF(LOG_CF(data, cf, "is_alive: valid events, looks alive"));
*input_pending = TRUE;
return TRUE; return TRUE;
} }

View File

@ -70,13 +70,6 @@ CURLcode Curl_socket_open(struct Curl_easy *data,
int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn, int Curl_socket_close(struct Curl_easy *data, struct connectdata *conn,
curl_socket_t sock); 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. * Determine the curl code for a socket connect() == -1 with errno.
*/ */

View File

@ -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, 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? 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 */ 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]; 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, CURLcode Curl_conn_keep_alive(struct Curl_easy *data,

View File

@ -85,7 +85,8 @@ typedef ssize_t Curl_cft_recv(struct Curl_cfilter *cf,
CURLcode *err); /* error to return */ CURLcode *err); /* error to return */
typedef bool Curl_cft_conn_is_alive(struct Curl_cfilter *cf, 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, typedef CURLcode Curl_cft_conn_keep_alive(struct Curl_cfilter *cf,
struct Curl_easy *data); struct Curl_easy *data);
@ -216,7 +217,8 @@ CURLcode Curl_cf_def_cntrl(struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_easy *data,
int event, int arg1, void *arg2); int event, int arg1, void *arg2);
bool Curl_cf_def_conn_is_alive(struct Curl_cfilter *cf, 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, CURLcode Curl_cf_def_conn_keep_alive(struct Curl_cfilter *cf,
struct Curl_easy *data); struct Curl_easy *data);
CURLcode Curl_cf_def_query(struct Curl_cfilter *cf, 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. * 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. * Try to upkeep the connection filters at sockindex.

View File

@ -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, * 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. * 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 * Check the lower filters first and, if successful, peek at the socket
* and distinguish between closed and data. * 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; struct cf_h2_ctx *ctx = cf->ctx;
int sval; bool alive = TRUE;
bool dead = TRUE;
if(!cf->next || !cf->next->cft->is_alive(cf->next, data)) *input_pending = FALSE;
return TRUE; 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(*input_pending) {
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) {
/* This happens before we've sent off a request and the connection is /* 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, not in use by any other transfer, there shouldn't be any data here,
only "protocol frames" */ only "protocol frames" */
CURLcode result; CURLcode result;
ssize_t nread = -1; ssize_t nread = -1;
*input_pending = FALSE;
Curl_attach_connection(data, cf->conn); Curl_attach_connection(data, cf->conn);
nread = Curl_conn_cf_recv(cf->next, data, nread = Curl_conn_cf_recv(cf->next, data,
ctx->inbuf, H2_BUFSIZE, &result); ctx->inbuf, H2_BUFSIZE, &result);
dead = FALSE;
if(nread != -1) { if(nread != -1) {
DEBUGF(LOG_CF(data, cf, "%d bytes stray data read before trying " DEBUGF(LOG_CF(data, cf, "%d bytes stray data read before trying "
"h2 connection", (int)nread)); "h2 connection", (int)nread));
@ -409,15 +410,19 @@ static bool http2_connisdead(struct Curl_cfilter *cf, struct Curl_easy *data)
ctx->inbuflen = nread; ctx->inbuflen = nread;
if(h2_process_pending_input(cf, data, &result) < 0) if(h2_process_pending_input(cf, data, &result) < 0)
/* immediate error, considered dead */ /* 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 */ /* the read failed so let's say this is dead anyway */
dead = TRUE; alive = FALSE;
}
Curl_detach_connection(data); Curl_detach_connection(data);
} }
return dead; return alive;
} }
static CURLcode http2_send_ping(struct Curl_cfilter *cf, static CURLcode http2_send_ping(struct Curl_cfilter *cf,
@ -1451,15 +1456,6 @@ CURLcode Curl_http2_request_upgrade(struct dynbuf *req,
return result; 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 * h2_process_pending_input() processes pending input left in
* httpc->inbuf. Then, call h2_session_send() to send pending data. * 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, 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; struct cf_h2_ctx *ctx = cf->ctx;
CURLcode result; CURLcode result;
struct cf_call_data save; struct cf_call_data save;
CF_DATA_SAVE(save, cf, data); 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); CF_DATA_RESTORE(cf, save);
return result; return result;
} }

View File

@ -145,7 +145,8 @@ static unsigned int rtsp_conncheck(struct Curl_easy *data,
(void)data; (void)data;
if(checks_to_perform & CONNCHECK_ISDEAD) { 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; ret_val |= CONNRESULT_DEAD;
} }

View File

@ -965,7 +965,20 @@ static bool extract_if_dead(struct connectdata *conn,
} }
else { 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) { if(dead) {

View File

@ -769,11 +769,13 @@ static CURLcode cf_msh3_query(struct Curl_cfilter *cf,
} }
static bool cf_msh3_conn_is_alive(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; struct cf_msh3_ctx *ctx = cf->ctx;
(void)data; (void)data;
*input_pending = FALSE;
return ctx && ctx->sock[SP_LOCAL] != CURL_SOCKET_BAD && ctx->qconn && return ctx && ctx->sock[SP_LOCAL] != CURL_SOCKET_BAD && ctx->qconn &&
ctx->connected; ctx->connected;
} }

View File

@ -2444,6 +2444,32 @@ static CURLcode cf_ngtcp2_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION; 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 = { struct Curl_cftype Curl_cft_http3 = {
"HTTP/3", "HTTP/3",
@ -2458,7 +2484,7 @@ struct Curl_cftype Curl_cft_http3 = {
cf_ngtcp2_send, cf_ngtcp2_send,
cf_ngtcp2_recv, cf_ngtcp2_recv,
cf_ngtcp2_data_event, cf_ngtcp2_data_event,
Curl_cf_def_conn_is_alive, cf_ngtcp2_conn_is_alive,
Curl_cf_def_conn_keep_alive, Curl_cf_def_conn_keep_alive,
cf_ngtcp2_query, cf_ngtcp2_query,
}; };

View File

@ -1359,6 +1359,32 @@ static CURLcode cf_quiche_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION; 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 = { struct Curl_cftype Curl_cft_http3 = {
"HTTP/3", "HTTP/3",
@ -1373,7 +1399,7 @@ struct Curl_cftype Curl_cft_http3 = {
cf_quiche_send, cf_quiche_send,
cf_quiche_recv, cf_quiche_recv,
cf_quiche_data_event, cf_quiche_data_event,
Curl_cf_def_conn_is_alive, cf_quiche_conn_is_alive,
Curl_cf_def_conn_keep_alive, Curl_cf_def_conn_keep_alive,
cf_quiche_query, cf_quiche_query,
}; };

View File

@ -1536,36 +1536,6 @@ static void nss_cleanup(void)
initialized = 0; 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) static void close_one(struct ssl_connect_data *connssl)
{ {
/* before the cleanup, check whether we are using a client certificate */ /* 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_init, /* init */
nss_cleanup, /* cleanup */ nss_cleanup, /* cleanup */
nss_version, /* version */ nss_version, /* version */
nss_check_cxn, /* check_cxn */ Curl_none_check_cxn, /* check_cxn */
/* NSS has no shutdown function provided and thus always fail */ /* NSS has no shutdown function provided and thus always fail */
Curl_none_shutdown, /* shutdown */ Curl_none_shutdown, /* shutdown */
nss_data_pending, /* data_pending */ nss_data_pending, /* data_pending */

View File

@ -1780,63 +1780,6 @@ static void ossl_cleanup(void)
Curl_tls_keylog_close(); 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 /* Selects an OpenSSL crypto engine
*/ */
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *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_init, /* init */
ossl_cleanup, /* cleanup */ ossl_cleanup, /* cleanup */
ossl_version, /* version */ ossl_version, /* version */
ossl_check_cxn, /* check_cxn */ Curl_none_check_cxn, /* check_cxn */
ossl_shutdown, /* shutdown */ ossl_shutdown, /* shutdown */
ossl_data_pending, /* data_pending */ ossl_data_pending, /* data_pending */
ossl_random, /* random */ ossl_random, /* random */

View File

@ -3233,35 +3233,6 @@ static size_t sectransp_version(char *buffer, size_t size)
return msnprintf(buffer, size, "SecureTransport"); 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, static bool sectransp_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data) const struct Curl_easy *data)
{ {
@ -3473,7 +3444,7 @@ const struct Curl_ssl Curl_ssl_sectransp = {
Curl_none_init, /* init */ Curl_none_init, /* init */
Curl_none_cleanup, /* cleanup */ Curl_none_cleanup, /* cleanup */
sectransp_version, /* version */ sectransp_version, /* version */
sectransp_check_cxn, /* check_cxn */ Curl_none_check_cxn, /* check_cxn */
sectransp_shutdown, /* shutdown */ sectransp_shutdown, /* shutdown */
sectransp_data_pending, /* data_pending */ sectransp_data_pending, /* data_pending */
sectransp_random, /* random */ sectransp_random, /* random */

View File

@ -1650,10 +1650,11 @@ static CURLcode ssl_cf_query(struct Curl_cfilter *cf,
CURLE_UNKNOWN_OPTION; 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; struct cf_call_data save;
bool result; int result;
/* /*
* This function tries to determine connection status. * 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 * -1 means the connection status is unknown
*/ */
CF_DATA_SAVE(save, cf, data); 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); CF_DATA_RESTORE(cf, save);
if(result > 0) if(result > 0) {
*input_pending = TRUE;
return TRUE; return TRUE;
if(result == 0) }
if(result == 0) {
*input_pending = FALSE;
return FALSE; return FALSE;
}
/* ssl backend does not know */ /* ssl backend does not know */
return cf->next? 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 */ FALSE; /* pessimistic in absence of data */
} }

View File

@ -110,4 +110,36 @@ class TestGoAway:
assert r.duration >= timedelta(seconds=count) assert r.duration >= timedelta(seconds=count)
r.check_stats(count=count, exp_status=200, exp_exitcode=0) 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)

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) 2008 - 2022, Daniel Stenberg, <daniel@haxx.se>, 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

View File

@ -100,6 +100,9 @@ class Httpd:
else: else:
self._extra_configs[domain] = lines self._extra_configs[domain] = lines
def clear_extra_configs(self):
self._extra_configs = {}
def _run(self, args, intext=''): def _run(self, args, intext=''):
env = {} env = {}
for key, val in os.environ.items(): for key, val in os.environ.items():
@ -231,6 +234,8 @@ class Httpd:
f'Listen {self.env.proxys_port}', f'Listen {self.env.proxys_port}',
f'TypesConfig "{self._conf_dir}/mime.types', 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 conf.extend([ # plain http host for domain1
f'<VirtualHost *:{self.env.http_port}>', f'<VirtualHost *:{self.env.http_port}>',
f' ServerName {domain1}', f' ServerName {domain1}',