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:
parent
6466071e8e
commit
7c5637b8b4
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
61
lib/http2.c
61
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,
|
* 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
lib/url.c
15
lib/url.c
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
93
tests/http/test_12_reuse.py
Normal file
93
tests/http/test_12_reuse.py
Normal 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
|
||||||
@ -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}',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user