|
@@ -98,7 +98,6 @@ static size_t populate_binsettings(uint8_t *binsettings,
|
|
|
struct cf_h2_ctx {
|
|
|
nghttp2_session *h2;
|
|
|
uint32_t max_concurrent_streams;
|
|
|
- bool enable_push;
|
|
|
/* The easy handle used in the current filter call, cleared at return */
|
|
|
struct cf_call_data call_data;
|
|
|
|
|
@@ -116,6 +115,10 @@ struct cf_h2_ctx {
|
|
|
int32_t pause_stream_id; /* stream ID which paused
|
|
|
nghttp2_session_mem_recv */
|
|
|
size_t drain_total; /* sum of all stream's UrlState.drain */
|
|
|
+ int32_t goaway_error;
|
|
|
+ int32_t last_stream_id;
|
|
|
+ BIT(goaway);
|
|
|
+ BIT(enable_push);
|
|
|
};
|
|
|
|
|
|
/* How to access `call_data` from a cf_h2 filter */
|
|
@@ -363,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.
|
|
@@ -370,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));
|
|
@@ -406,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,
|
|
@@ -815,7 +823,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
|
|
|
ctx->max_concurrent_streams = nghttp2_session_get_remote_settings(
|
|
|
session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
|
|
|
ctx->enable_push = nghttp2_session_get_remote_settings(
|
|
|
- session, NGHTTP2_SETTINGS_ENABLE_PUSH);
|
|
|
+ session, NGHTTP2_SETTINGS_ENABLE_PUSH) != 0;
|
|
|
DEBUGF(LOG_CF(data, cf, "MAX_CONCURRENT_STREAMS == %d",
|
|
|
ctx->max_concurrent_streams));
|
|
|
DEBUGF(LOG_CF(data, cf, "ENABLE_PUSH == %s",
|
|
@@ -829,9 +837,12 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
|
|
|
break;
|
|
|
}
|
|
|
case NGHTTP2_GOAWAY:
|
|
|
+ ctx->goaway = TRUE;
|
|
|
+ ctx->goaway_error = frame->goaway.error_code;
|
|
|
+ ctx->last_stream_id = frame->goaway.last_stream_id;
|
|
|
if(data) {
|
|
|
infof(data, "recveived GOAWAY, error=%d, last_stream=%u",
|
|
|
- frame->goaway.error_code, frame->goaway.last_stream_id);
|
|
|
+ ctx->goaway_error, ctx->last_stream_id);
|
|
|
multi_connchanged(data->multi);
|
|
|
}
|
|
|
break;
|
|
@@ -858,7 +869,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
|
|
|
|
|
|
switch(frame->hd.type) {
|
|
|
case NGHTTP2_DATA:
|
|
|
- /* If body started on this stream, then receiving DATA is illegal. */
|
|
|
+ /* If !body started on this stream, then receiving DATA is illegal. */
|
|
|
DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] recv frame DATA", stream_id));
|
|
|
if(!stream->bodystarted) {
|
|
|
rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE,
|
|
@@ -940,7 +951,21 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
|
|
|
break;
|
|
|
case NGHTTP2_RST_STREAM:
|
|
|
DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] recv RST", stream_id));
|
|
|
+ stream->closed = TRUE;
|
|
|
stream->reset = TRUE;
|
|
|
+ drain_this(cf, data);
|
|
|
+ Curl_expire(data, 0, EXPIRE_RUN_NOW);
|
|
|
+ break;
|
|
|
+ case NGHTTP2_WINDOW_UPDATE:
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] recv WINDOW_UPDATE", stream_id));
|
|
|
+ if((data_s->req.keepon & KEEP_SEND_HOLD) &&
|
|
|
+ (data_s->req.keepon & KEEP_SEND)) {
|
|
|
+ data_s->req.keepon &= ~KEEP_SEND_HOLD;
|
|
|
+ drain_this(cf, data_s);
|
|
|
+ Curl_expire(data_s, 0, EXPIRE_RUN_NOW);
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] un-holding after win update",
|
|
|
+ stream_id));
|
|
|
+ }
|
|
|
break;
|
|
|
default:
|
|
|
DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] recv frame %x",
|
|
@@ -1006,18 +1031,6 @@ static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags,
|
|
|
return NGHTTP2_ERR_PAUSE;
|
|
|
}
|
|
|
|
|
|
-#if 0
|
|
|
- /* pause execution of nghttp2 if we received data for another handle
|
|
|
- in order to process them first. */
|
|
|
- if(CF_DATA_CURRENT(cf) != data_s) {
|
|
|
- ctx->pause_stream_id = stream_id;
|
|
|
- DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] not call_data -> NGHTTP2_ERR_PAUSE",
|
|
|
- stream_id));
|
|
|
- drain_this(cf, data_s);
|
|
|
- return NGHTTP2_ERR_PAUSE;
|
|
|
- }
|
|
|
-#endif
|
|
|
-
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
@@ -1030,44 +1043,43 @@ static int on_stream_close(nghttp2_session *session, int32_t stream_id,
|
|
|
struct HTTP *stream;
|
|
|
int rv;
|
|
|
(void)session;
|
|
|
- (void)stream_id;
|
|
|
|
|
|
- if(stream_id) {
|
|
|
- /* get the stream from the hash based on Stream ID, stream ID zero is for
|
|
|
- connection-oriented stuff */
|
|
|
- data_s = nghttp2_session_get_stream_user_data(session, stream_id);
|
|
|
- if(!data_s) {
|
|
|
- /* We could get stream ID not in the hash. For example, if we
|
|
|
- decided to reject stream (e.g., PUSH_PROMISE). */
|
|
|
- return 0;
|
|
|
- }
|
|
|
- DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] on_stream_close(), %s (err %d)",
|
|
|
- stream_id, nghttp2_http2_strerror(error_code), error_code));
|
|
|
- stream = data_s->req.p.http;
|
|
|
- if(!stream)
|
|
|
- return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
|
+ /* get the stream from the hash based on Stream ID, stream ID zero is for
|
|
|
+ connection-oriented stuff */
|
|
|
+ data_s = stream_id?
|
|
|
+ nghttp2_session_get_stream_user_data(session, stream_id) : NULL;
|
|
|
+ if(!data_s) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ stream = data_s->req.p.http;
|
|
|
+ DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] on_stream_close(), %s (err %d)",
|
|
|
+ stream_id, nghttp2_http2_strerror(error_code), error_code));
|
|
|
+ if(!stream)
|
|
|
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
|
|
|
|
|
|
- stream->closed = TRUE;
|
|
|
- if(CF_DATA_CURRENT(cf) != data_s) {
|
|
|
- drain_this(cf, data_s);
|
|
|
- Curl_expire(data_s, 0, EXPIRE_RUN_NOW);
|
|
|
- }
|
|
|
- stream->error = error_code;
|
|
|
+ stream->closed = TRUE;
|
|
|
+ stream->error = error_code;
|
|
|
+ if(stream->error)
|
|
|
+ stream->reset = TRUE;
|
|
|
|
|
|
- /* remove the entry from the hash as the stream is now gone */
|
|
|
- rv = nghttp2_session_set_stream_user_data(session, stream_id, 0);
|
|
|
- if(rv) {
|
|
|
- infof(data_s, "http/2: failed to clear user_data for stream %u",
|
|
|
- stream_id);
|
|
|
- DEBUGASSERT(0);
|
|
|
- }
|
|
|
- if(stream_id == ctx->pause_stream_id) {
|
|
|
- DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] closed the pause stream",
|
|
|
- stream_id));
|
|
|
- ctx->pause_stream_id = 0;
|
|
|
- }
|
|
|
- DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] closed, cleared", stream_id));
|
|
|
+ if(CF_DATA_CURRENT(cf) != data_s) {
|
|
|
+ drain_this(cf, data_s);
|
|
|
+ Curl_expire(data_s, 0, EXPIRE_RUN_NOW);
|
|
|
}
|
|
|
+
|
|
|
+ /* remove `data_s` from the nghttp2 stream */
|
|
|
+ rv = nghttp2_session_set_stream_user_data(session, stream_id, 0);
|
|
|
+ if(rv) {
|
|
|
+ infof(data_s, "http/2: failed to clear user_data for stream %u",
|
|
|
+ stream_id);
|
|
|
+ DEBUGASSERT(0);
|
|
|
+ }
|
|
|
+ if(stream_id == ctx->pause_stream_id) {
|
|
|
+ DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] closed the pause stream",
|
|
|
+ stream_id));
|
|
|
+ ctx->pause_stream_id = 0;
|
|
|
+ }
|
|
|
+ DEBUGF(LOG_CF(data_s, cf, "[h2sid=%u] closed now", stream_id));
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
@@ -1383,7 +1395,8 @@ static void http2_data_done(struct Curl_cfilter *cf,
|
|
|
ctx->pause_stream_id = 0;
|
|
|
}
|
|
|
|
|
|
- if(premature || (!stream->closed && stream->stream_id)) {
|
|
|
+ (void)premature;
|
|
|
+ if(!stream->closed && stream->stream_id) {
|
|
|
/* RST_STREAM */
|
|
|
DEBUGF(LOG_CF(data, cf, "[h2sid=%u] RST", stream->stream_id));
|
|
|
if(!nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE,
|
|
@@ -1445,15 +1458,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.
|
|
@@ -1586,8 +1590,6 @@ static ssize_t http2_handle_stream_close(struct Curl_cfilter *cf,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /* Reset to FALSE to prevent infinite loop in readwrite_data function. */
|
|
|
- stream->closed = FALSE;
|
|
|
if(stream->error == NGHTTP2_REFUSED_STREAM) {
|
|
|
DEBUGF(LOG_CF(data, cf, "[h2sid=%u] REFUSED_STREAM, try again on a new "
|
|
|
"connection", stream->stream_id));
|
|
@@ -1603,6 +1605,11 @@ static ssize_t http2_handle_stream_close(struct Curl_cfilter *cf,
|
|
|
*err = CURLE_HTTP2_STREAM;
|
|
|
return -1;
|
|
|
}
|
|
|
+ else if(stream->reset) {
|
|
|
+ failf(data, "HTTP/2 stream %u was reset", stream->stream_id);
|
|
|
+ *err = stream->bodystarted? CURLE_PARTIAL_FILE : CURLE_RECV_ERROR;
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
|
|
|
if(!stream->bodystarted) {
|
|
|
failf(data, "HTTP/2 stream %u was closed cleanly, but before getting "
|
|
@@ -1638,7 +1645,7 @@ static ssize_t http2_handle_stream_close(struct Curl_cfilter *cf,
|
|
|
|
|
|
stream->close_handled = TRUE;
|
|
|
|
|
|
- DEBUGF(LOG_CF(data, cf, "http2_recv returns 0, http2_handle_stream_close"));
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] closed cleanly", stream->stream_id));
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
@@ -1720,9 +1727,29 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
struct HTTP *stream = data->req.p.http;
|
|
|
ssize_t nread = -1;
|
|
|
struct cf_call_data save;
|
|
|
+ bool conn_is_closed = FALSE;
|
|
|
|
|
|
CF_DATA_SAVE(save, cf, data);
|
|
|
|
|
|
+ /* If the h2 session has told us to GOAWAY with an error AND
|
|
|
+ * indicated the highest stream id it has processes AND
|
|
|
+ * the stream we are trying to read has a higher id, this
|
|
|
+ * means we will most likely not receive any more for it.
|
|
|
+ * Treat this as if the server explicitly had RST the stream */
|
|
|
+ if((ctx->goaway && ctx->goaway_error &&
|
|
|
+ ctx->last_stream_id > 0 &&
|
|
|
+ ctx->last_stream_id < stream->stream_id)) {
|
|
|
+ stream->reset = TRUE;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* If a stream is RST, it does not matter what state the h2 session
|
|
|
+ * is in, our answer to receiving data is always the same. */
|
|
|
+ if(stream->reset) {
|
|
|
+ *err = stream->bodystarted? CURLE_PARTIAL_FILE : CURLE_RECV_ERROR;
|
|
|
+ nread = -1;
|
|
|
+ goto out;
|
|
|
+ }
|
|
|
+
|
|
|
if(should_close_session(ctx)) {
|
|
|
DEBUGF(LOG_CF(data, cf, "http2_recv: nothing to do in this session"));
|
|
|
if(cf->conn->bits.close) {
|
|
@@ -1763,7 +1790,7 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
|
- DEBUGF(LOG_CF(data, cf, "[h2sid=%u] recv: win %u/%u",
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] cf_recv: win %u/%u",
|
|
|
stream->stream_id,
|
|
|
nghttp2_session_get_local_window_size(ctx->h2),
|
|
|
nghttp2_session_get_stream_local_window_size(ctx->h2,
|
|
@@ -1846,57 +1873,40 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
stream->memlen = 0;
|
|
|
|
|
|
if(ctx->inbuflen > 0) {
|
|
|
- DEBUGF(LOG_CF(data, cf, "Use data left in connection buffer, nread=%zd",
|
|
|
- ctx->inbuflen - ctx->nread_inbuf));
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] %zd bytes in inbuf",
|
|
|
+ stream->stream_id, ctx->inbuflen - ctx->nread_inbuf));
|
|
|
if(h2_process_pending_input(cf, data, err))
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
- while(stream->memlen == 0 /* have no data for this stream */
|
|
|
- && !ctx->pause_stream_id /* we are not paused either */
|
|
|
- && ctx->inbuflen == 0) { /* and out input buffer is empty */
|
|
|
+ while(stream->memlen == 0 && /* have no data for this stream */
|
|
|
+ !stream->closed && /* and it is not closed/reset */
|
|
|
+ !ctx->pause_stream_id && /* we are not paused either */
|
|
|
+ ctx->inbuflen == 0 && /* and out input buffer is empty */
|
|
|
+ !conn_is_closed) { /* and connection is not closed */
|
|
|
/* Receive data from the "lower" filters */
|
|
|
nread = Curl_conn_cf_recv(cf->next, data, ctx->inbuf, H2_BUFSIZE, err);
|
|
|
if(nread < 0) {
|
|
|
- if(*err != CURLE_AGAIN)
|
|
|
- failf(data, "Failed receiving HTTP2 data");
|
|
|
- else if(stream->closed) {
|
|
|
- /* received when the stream was already closed! */
|
|
|
- nread = http2_handle_stream_close(cf, data, stream, err);
|
|
|
- goto out;
|
|
|
+ DEBUGASSERT(*err);
|
|
|
+ if(*err == CURLE_AGAIN) {
|
|
|
+ break;
|
|
|
}
|
|
|
-
|
|
|
- /* nothing to read from the lower layers, clear drain */
|
|
|
- drained_transfer(cf, data);
|
|
|
- nread = -1;
|
|
|
- goto out;
|
|
|
+ failf(data, "Failed receiving HTTP2 data");
|
|
|
+ conn_is_closed = TRUE;
|
|
|
}
|
|
|
else if(nread == 0) {
|
|
|
- if(!stream->closed) {
|
|
|
- /* This will happen when the server or proxy server is SIGKILLed
|
|
|
- during data transfer. We should emit an error since our data
|
|
|
- received may be incomplete. */
|
|
|
- failf(data, "HTTP/2 stream %u was not closed cleanly before"
|
|
|
- " end of the underlying stream",
|
|
|
- stream->stream_id);
|
|
|
- drained_transfer(cf, data);
|
|
|
- *err = CURLE_PARTIAL_FILE;
|
|
|
- nread = -1;
|
|
|
- goto out;
|
|
|
- }
|
|
|
-
|
|
|
- DEBUGF(LOG_CF(data, cf, "[h2sid=%u] end of stream",
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] underlying connection is closed",
|
|
|
stream->stream_id));
|
|
|
- *err = CURLE_OK;
|
|
|
- nread = 0;
|
|
|
- goto out;
|
|
|
+ conn_is_closed = TRUE;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] read %zd from connection",
|
|
|
+ stream->stream_id, nread));
|
|
|
+ ctx->inbuflen = nread;
|
|
|
+ DEBUGASSERT(ctx->nread_inbuf == 0);
|
|
|
+ if(h2_process_pending_input(cf, data, err))
|
|
|
+ return -1;
|
|
|
}
|
|
|
-
|
|
|
- DEBUGF(LOG_CF(data, cf, "read %zd from connection", nread));
|
|
|
- ctx->inbuflen = nread;
|
|
|
- DEBUGASSERT(ctx->nread_inbuf == 0);
|
|
|
- if(h2_process_pending_input(cf, data, err))
|
|
|
- return -1;
|
|
|
}
|
|
|
|
|
|
}
|
|
@@ -1933,11 +1943,18 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
|
|
|
*err = CURLE_OK;
|
|
|
nread = retlen;
|
|
|
- DEBUGF(LOG_CF(data, cf, "[h2sid=%u] cf_h2_recv -> %zd",
|
|
|
- stream->stream_id, nread));
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
|
+ if(conn_is_closed && !stream->closed) {
|
|
|
+ /* underlying connection is closed and we have nothing for the stream.
|
|
|
+ * Treat this as a RST */
|
|
|
+ stream->closed = stream->reset = TRUE;
|
|
|
+ failf(data, "HTTP/2 stream %u was not closed cleanly before"
|
|
|
+ " end of the underlying connection",
|
|
|
+ stream->stream_id);
|
|
|
+ }
|
|
|
+
|
|
|
if(stream->closed) {
|
|
|
nread = http2_handle_stream_close(cf, data, stream, err);
|
|
|
goto out;
|
|
@@ -1950,9 +1967,9 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
}
|
|
|
*err = CURLE_AGAIN;
|
|
|
nread = -1;
|
|
|
- DEBUGF(LOG_CF(data, cf, "[h2sid=%u] recv -> AGAIN",
|
|
|
- stream->stream_id));
|
|
|
out:
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] cf_recv -> %zd, %d",
|
|
|
+ stream->stream_id, nread, *err));
|
|
|
CF_DATA_RESTORE(cf, save);
|
|
|
return nread;
|
|
|
}
|
|
@@ -1976,19 +1993,20 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
CURLcode result;
|
|
|
struct h2h3req *hreq;
|
|
|
struct cf_call_data save;
|
|
|
+ ssize_t nwritten;
|
|
|
|
|
|
CF_DATA_SAVE(save, cf, data);
|
|
|
- DEBUGF(LOG_CF(data, cf, "send len=%zu", len));
|
|
|
+ DEBUGF(LOG_CF(data, cf, "cf_send(len=%zu) start", len));
|
|
|
|
|
|
if(stream->stream_id != -1) {
|
|
|
if(stream->close_handled) {
|
|
|
infof(data, "stream %u closed", stream->stream_id);
|
|
|
*err = CURLE_HTTP2_STREAM;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
else if(stream->closed) {
|
|
|
- len = http2_handle_stream_close(cf, data, stream, err);
|
|
|
+ nwritten = http2_handle_stream_close(cf, data, stream, err);
|
|
|
goto out;
|
|
|
}
|
|
|
/* If stream_id != -1, we have dispatched request HEADERS, and now
|
|
@@ -1998,26 +2016,24 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
rv = nghttp2_session_resume_data(ctx->h2, stream->stream_id);
|
|
|
if(nghttp2_is_fatal(rv)) {
|
|
|
*err = CURLE_SEND_ERROR;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
result = h2_session_send(cf, data);
|
|
|
if(result) {
|
|
|
*err = result;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
- len -= stream->upload_len;
|
|
|
|
|
|
- /* Nullify here because we call nghttp2_session_send() and they
|
|
|
- might refer to the old buffer. */
|
|
|
+ nwritten = (ssize_t)len - (ssize_t)stream->upload_len;
|
|
|
stream->upload_mem = NULL;
|
|
|
stream->upload_len = 0;
|
|
|
|
|
|
if(should_close_session(ctx)) {
|
|
|
DEBUGF(LOG_CF(data, cf, "send: nothing to do in this session"));
|
|
|
*err = CURLE_HTTP2;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
@@ -2029,26 +2045,36 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
nghttp2_session_resume_data(ctx->h2, stream->stream_id);
|
|
|
}
|
|
|
|
|
|
-#ifdef DEBUG_HTTP2
|
|
|
- if(!len) {
|
|
|
- infof(data, "http2_send: easy %p (stream %u) win %u/%u",
|
|
|
- data, stream->stream_id,
|
|
|
- nghttp2_session_get_remote_window_size(ctx->h2),
|
|
|
- nghttp2_session_get_stream_remote_window_size(ctx->h2,
|
|
|
- stream->stream_id)
|
|
|
- );
|
|
|
-
|
|
|
+ if(!nwritten) {
|
|
|
+ size_t rwin = nghttp2_session_get_stream_remote_window_size(ctx->h2,
|
|
|
+ stream->stream_id);
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] cf_send: win %u/%zu",
|
|
|
+ stream->stream_id,
|
|
|
+ nghttp2_session_get_remote_window_size(ctx->h2), rwin));
|
|
|
+ if(rwin == 0) {
|
|
|
+ /* We cannot upload more as the stream's remote window size
|
|
|
+ * is 0. We need to receive WIN_UPDATEs before we can continue.
|
|
|
+ */
|
|
|
+ data->req.keepon |= KEEP_SEND_HOLD;
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] holding send as remote flow "
|
|
|
+ "window is exhausted", stream->stream_id));
|
|
|
+ }
|
|
|
}
|
|
|
- infof(data, "http2_send returns %zu for stream %u", len,
|
|
|
- stream->stream_id);
|
|
|
-#endif
|
|
|
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%u] cf_send returns %zd ",
|
|
|
+ stream->stream_id, nwritten));
|
|
|
+
|
|
|
+ /* handled writing BODY for open stream. */
|
|
|
goto out;
|
|
|
}
|
|
|
-
|
|
|
+ /* Stream has not been opened yet. `buf` is expected to contain
|
|
|
+ * request headers. */
|
|
|
+ /* TODO: this assumes that the `buf` and `len` we are called with
|
|
|
+ * is *all* HEADERs and no body. We have no way to determine here
|
|
|
+ * if that is indeed the case. */
|
|
|
result = Curl_pseudo_headers(data, buf, len, NULL, &hreq);
|
|
|
if(result) {
|
|
|
*err = result;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
nheader = hreq->entries;
|
|
@@ -2057,7 +2083,7 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
if(!nva) {
|
|
|
Curl_pseudo_free(hreq);
|
|
|
*err = CURLE_OUT_OF_MEMORY;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
else {
|
|
@@ -2104,25 +2130,28 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
DEBUGF(LOG_CF(data, cf, "send: nghttp2_submit_request error (%s)%u",
|
|
|
nghttp2_strerror(stream_id), stream_id));
|
|
|
*err = CURLE_SEND_ERROR;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
|
infof(data, "Using Stream ID: %u (easy handle %p)",
|
|
|
stream_id, (void *)data);
|
|
|
stream->stream_id = stream_id;
|
|
|
+ /* See TODO above. We assume that the whole buf was consumed by
|
|
|
+ * generating the request headers. */
|
|
|
+ nwritten = len;
|
|
|
|
|
|
result = h2_session_send(cf, data);
|
|
|
if(result) {
|
|
|
*err = result;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
|
if(should_close_session(ctx)) {
|
|
|
DEBUGF(LOG_CF(data, cf, "send: nothing to do in this session"));
|
|
|
*err = CURLE_HTTP2;
|
|
|
- len = -1;
|
|
|
+ nwritten = -1;
|
|
|
goto out;
|
|
|
}
|
|
|
|
|
@@ -2137,7 +2166,7 @@ static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
|
|
|
|
|
|
out:
|
|
|
CF_DATA_RESTORE(cf, save);
|
|
|
- return len;
|
|
|
+ return nwritten;
|
|
|
}
|
|
|
|
|
|
static int cf_h2_get_select_socks(struct Curl_cfilter *cf,
|
|
@@ -2160,7 +2189,7 @@ static int cf_h2_get_select_socks(struct Curl_cfilter *cf,
|
|
|
|
|
|
/* we're (still uploading OR the HTTP/2 layer wants to send data) AND
|
|
|
there's a window to send data in */
|
|
|
- if((((k->keepon & (KEEP_SEND|KEEP_SEND_PAUSE)) == KEEP_SEND) ||
|
|
|
+ if((((k->keepon & KEEP_SENDBITS) == KEEP_SEND) ||
|
|
|
nghttp2_session_want_write(ctx->h2)) &&
|
|
|
(nghttp2_session_get_remote_window_size(ctx->h2) &&
|
|
|
nghttp2_session_get_stream_remote_window_size(ctx->h2,
|
|
@@ -2329,14 +2358,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;
|
|
|
}
|
|
@@ -2479,7 +2511,8 @@ bool Curl_http2_may_switch(struct Curl_easy *data,
|
|
|
int sockindex)
|
|
|
{
|
|
|
(void)sockindex;
|
|
|
- if(data->state.httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE) {
|
|
|
+ if(!Curl_conn_is_http2(data, conn, sockindex) &&
|
|
|
+ data->state.httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE) {
|
|
|
#ifndef CURL_DISABLE_PROXY
|
|
|
if(conn->bits.httpproxy && !conn->bits.tunnel_proxy) {
|
|
|
/* We don't support HTTP/2 proxies yet. Also it's debatable
|