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.
}
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;
}
DEBUGF(LOG_CF(data, cf, "is_alive: valid events, looks alive"));
+ *input_pending = TRUE;
return TRUE;
}
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.
*/
}
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 */
}
}
}
-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,
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);
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,
/**
* 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.
}
}
+/*
+ * 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.
* 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));
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,
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.
}
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;
}
(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;
}
}
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) {
}
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;
}
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",
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,
};
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",
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,
};
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 */
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 */
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)
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 */
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)
{
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 */
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.
*
* -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 */
}
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)
+
--- /dev/null
+#!/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
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():
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'<VirtualHost *:{self.env.http_port}>',
f' ServerName {domain1}',