diff options
author | AlexSm <alex@ydb.tech> | 2024-01-18 11:28:56 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-18 11:28:56 +0100 |
commit | 9d0a3761b3201e0d9db879a7adf91876ebdb0564 (patch) | |
tree | 541d11ac878c18efd7ebca81e35112aa0fef995b /contrib/libs/curl/lib/http2.c | |
parent | 404ef8886ecc9736bc58ade6da2fbd83b486a408 (diff) | |
download | ydb-9d0a3761b3201e0d9db879a7adf91876ebdb0564.tar.gz |
Library import 8 (#1074)
* Library import 8
* Add contrib/libs/cxxsupp/libcxx/include/__verbose_abort
Diffstat (limited to 'contrib/libs/curl/lib/http2.c')
-rw-r--r-- | contrib/libs/curl/lib/http2.c | 3398 |
1 files changed, 1953 insertions, 1445 deletions
diff --git a/contrib/libs/curl/lib/http2.c b/contrib/libs/curl/lib/http2.c index b7409b027d..fd37d3c8b2 100644 --- a/contrib/libs/curl/lib/http2.c +++ b/contrib/libs/curl/lib/http2.c @@ -5,7 +5,7 @@ * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * - * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al. + * Copyright (C) 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 @@ -25,8 +25,11 @@ #include "curl_setup.h" #ifdef USE_NGHTTP2 +#include <stdint.h> #include <nghttp2/nghttp2.h> #include "urldata.h" +#include "bufq.h" +#include "http1.h" #include "http2.h" #include "http.h" #include "sendf.h" @@ -35,20 +38,20 @@ #include "strcase.h" #include "multiif.h" #include "url.h" +#include "urlapi-int.h" +#include "cfilters.h" #include "connect.h" +#include "rand.h" #include "strtoofft.h" #include "strdup.h" #include "transfer.h" #include "dynbuf.h" -#include "h2h3.h" #include "headers.h" /* The last 3 #include files should be in this order */ #include "curl_printf.h" #include "curl_memory.h" #include "memdebug.h" -#define H2_BUFSIZE 32768 - #if (NGHTTP2_VERSION_NUM < 0x010c00) #error too old nghttp2 version, upgrade! #endif @@ -61,304 +64,585 @@ #define NGHTTP2_HAS_SET_LOCAL_WINDOW_SIZE 1 #endif -#define HTTP2_HUGE_WINDOW_SIZE (32 * 1024 * 1024) /* 32 MB */ -#ifdef DEBUG_HTTP2 -#define H2BUGF(x) x -#else -#define H2BUGF(x) do { } while(0) -#endif +/* buffer dimensioning: + * use 16K as chunk size, as that fits H2 DATA frames well */ +#define H2_CHUNK_SIZE (16 * 1024) +/* this is how much we want "in flight" for a stream */ +#define H2_STREAM_WINDOW_SIZE (10 * 1024 * 1024) +/* on receiving from TLS, we prep for holding a full stream window */ +#define H2_NW_RECV_CHUNKS (H2_STREAM_WINDOW_SIZE / H2_CHUNK_SIZE) +/* on send into TLS, we just want to accumulate small frames */ +#define H2_NW_SEND_CHUNKS 1 +/* stream recv/send chunks are a result of window / chunk sizes */ +#define H2_STREAM_RECV_CHUNKS (H2_STREAM_WINDOW_SIZE / H2_CHUNK_SIZE) +/* keep smaller stream upload buffer (default h2 window size) to have + * our progress bars and "upload done" reporting closer to reality */ +#define H2_STREAM_SEND_CHUNKS ((64 * 1024) / H2_CHUNK_SIZE) +/* spare chunks we keep for a full window */ +#define H2_STREAM_POOL_SPARES (H2_STREAM_WINDOW_SIZE / H2_CHUNK_SIZE) + +/* We need to accommodate the max number of streams with their window + * sizes on the overall connection. Streams might become PAUSED which + * will block their received QUOTA in the connection window. And if we + * run out of space, the server is blocked from sending us any data. + * See #10988 for an issue with this. */ +#define HTTP2_HUGE_WINDOW_SIZE (100 * H2_STREAM_WINDOW_SIZE) + +#define H2_SETTINGS_IV_LEN 3 +#define H2_BINSETTINGS_LEN 80 + +static int populate_settings(nghttp2_settings_entry *iv, + struct Curl_easy *data) +{ + iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + iv[0].value = Curl_multi_max_concurrent_streams(data->multi); -static ssize_t http2_recv(struct Curl_easy *data, int sockindex, - char *mem, size_t len, CURLcode *err); -static bool http2_connisdead(struct Curl_easy *data, - struct connectdata *conn); -static int h2_session_send(struct Curl_easy *data, - nghttp2_session *h2); -static int h2_process_pending_input(struct Curl_easy *data, - struct http_conn *httpc, - CURLcode *err); + iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + iv[1].value = H2_STREAM_WINDOW_SIZE; -/* - * Curl_http2_init_state() is called when the easy handle is created and - * allows for HTTP/2 specific init of state. - */ -void Curl_http2_init_state(struct UrlState *state) -{ - state->stream_weight = NGHTTP2_DEFAULT_WEIGHT; -} + iv[2].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + iv[2].value = data->multi->push_cb != NULL; -/* - * Curl_http2_init_userset() is called when the easy handle is created and - * allows for HTTP/2 specific user-set fields. - */ -void Curl_http2_init_userset(struct UserDefined *set) -{ - set->stream_weight = NGHTTP2_DEFAULT_WEIGHT; + return 3; } -static int http2_getsock(struct Curl_easy *data, - struct connectdata *conn, - curl_socket_t *sock) +static ssize_t populate_binsettings(uint8_t *binsettings, + struct Curl_easy *data) { - const struct http_conn *c = &conn->proto.httpc; - struct SingleRequest *k = &data->req; - int bitmap = GETSOCK_BLANK; - struct HTTP *stream = data->req.p.http; + nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN]; + int ivlen; - sock[0] = conn->sock[FIRSTSOCKET]; + ivlen = populate_settings(iv, data); + /* this returns number of bytes it wrote or a negative number on error. */ + return nghttp2_pack_settings_payload(binsettings, H2_BINSETTINGS_LEN, + iv, ivlen); +} - if(!(k->keepon & KEEP_RECV_PAUSE)) - /* Unless paused - in a HTTP/2 connection we can basically always get a - frame so we should always be ready for one */ - bitmap |= GETSOCK_READSOCK(FIRSTSOCKET); +struct cf_h2_ctx { + nghttp2_session *h2; + uint32_t max_concurrent_streams; + /* The easy handle used in the current filter call, cleared at return */ + struct cf_call_data call_data; + + struct bufq inbufq; /* network input */ + struct bufq outbufq; /* network output */ + struct bufc_pool stream_bufcp; /* spares for stream buffers */ + + size_t drain_total; /* sum of all stream's UrlState drain */ + int32_t goaway_error; + int32_t last_stream_id; + BIT(conn_closed); + BIT(goaway); + BIT(enable_push); + BIT(nw_out_blocked); +}; + +/* How to access `call_data` from a cf_h2 filter */ +#undef CF_CTX_CALL_DATA +#define CF_CTX_CALL_DATA(cf) \ + ((struct cf_h2_ctx *)(cf)->ctx)->call_data - /* 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) || - nghttp2_session_want_write(c->h2)) && - (nghttp2_session_get_remote_window_size(c->h2) && - nghttp2_session_get_stream_remote_window_size(c->h2, - stream->stream_id))) - bitmap |= GETSOCK_WRITESOCK(FIRSTSOCKET); +static void cf_h2_ctx_clear(struct cf_h2_ctx *ctx) +{ + struct cf_call_data save = ctx->call_data; - return bitmap; + if(ctx->h2) { + nghttp2_session_del(ctx->h2); + } + Curl_bufq_free(&ctx->inbufq); + Curl_bufq_free(&ctx->outbufq); + Curl_bufcp_free(&ctx->stream_bufcp); + memset(ctx, 0, sizeof(*ctx)); + ctx->call_data = save; } -/* - * http2_stream_free() free HTTP2 stream related data - */ -static void http2_stream_free(struct HTTP *http) +static void cf_h2_ctx_free(struct cf_h2_ctx *ctx) { - if(http) { - Curl_dyn_free(&http->header_recvbuf); - for(; http->push_headers_used > 0; --http->push_headers_used) { - free(http->push_headers[http->push_headers_used - 1]); - } - free(http->push_headers); - http->push_headers = NULL; + if(ctx) { + cf_h2_ctx_clear(ctx); + free(ctx); } } -/* - * Disconnects *a* connection used for HTTP/2. It might be an old one from the - * connection cache and not the "main" one. Don't touch the easy handle! +static CURLcode h2_progress_egress(struct Curl_cfilter *cf, + struct Curl_easy *data); + +/** + * All about the H3 internals of a stream */ +struct stream_ctx { + /*********** for HTTP/2 we store stream-local data here *************/ + int32_t id; /* HTTP/2 protocol identifier for stream */ + struct bufq recvbuf; /* response buffer */ + struct bufq sendbuf; /* request buffer */ + struct h1_req_parser h1; /* parsing the request */ + struct dynhds resp_trailers; /* response trailer fields */ + size_t resp_hds_len; /* amount of response header bytes in recvbuf */ + size_t upload_blocked_len; + curl_off_t upload_left; /* number of request bytes left to upload */ + + char **push_headers; /* allocated array */ + size_t push_headers_used; /* number of entries filled in */ + size_t push_headers_alloc; /* number of entries allocated */ + + int status_code; /* HTTP response status code */ + uint32_t error; /* stream error code */ + uint32_t local_window_size; /* the local recv window size */ + bool resp_hds_complete; /* we have a complete, final response */ + bool closed; /* TRUE on stream close */ + bool reset; /* TRUE on stream reset */ + bool close_handled; /* TRUE if stream closure is handled by libcurl */ + bool bodystarted; + bool send_closed; /* transfer is done sending, we might have still + buffered data in stream->sendbuf to upload. */ +}; + +#define H2_STREAM_CTX(d) ((struct stream_ctx *)(((d) && (d)->req.p.http)? \ + ((struct HTTP *)(d)->req.p.http)->h2_ctx \ + : NULL)) +#define H2_STREAM_LCTX(d) ((struct HTTP *)(d)->req.p.http)->h2_ctx +#define H2_STREAM_ID(d) (H2_STREAM_CTX(d)? \ + H2_STREAM_CTX(d)->id : -2) -static CURLcode http2_disconnect(struct Curl_easy *data, - struct connectdata *conn, - bool dead_connection) +/* + * Mark this transfer to get "drained". + */ +static void drain_stream(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct stream_ctx *stream) { - struct http_conn *c = &conn->proto.httpc; - (void)dead_connection; -#ifndef DEBUG_HTTP2 - (void)data; -#endif + unsigned char bits; + + (void)cf; + bits = CURL_CSELECT_IN; + if(!stream->send_closed && + (stream->upload_left || stream->upload_blocked_len)) + bits |= CURL_CSELECT_OUT; + if(data->state.dselect_bits != bits) { + CURL_TRC_CF(data, cf, "[%d] DRAIN dselect_bits=%x", + stream->id, bits); + data->state.dselect_bits = bits; + Curl_expire(data, 0, EXPIRE_RUN_NOW); + } +} - H2BUGF(infof(data, "HTTP/2 DISCONNECT starts now")); +static CURLcode http2_data_setup(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct stream_ctx **pstream) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream; - nghttp2_session_del(c->h2); - Curl_safefree(c->inbuf); + (void)cf; + DEBUGASSERT(data); + if(!data->req.p.http) { + failf(data, "initialization failure, transfer not http initialized"); + return CURLE_FAILED_INIT; + } + stream = H2_STREAM_CTX(data); + if(stream) { + *pstream = stream; + return CURLE_OK; + } - H2BUGF(infof(data, "HTTP/2 DISCONNECT done")); + stream = calloc(1, sizeof(*stream)); + if(!stream) + return CURLE_OUT_OF_MEMORY; + + stream->id = -1; + Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp, + H2_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); + Curl_bufq_initp(&stream->recvbuf, &ctx->stream_bufcp, + H2_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT); + Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN); + Curl_dynhds_init(&stream->resp_trailers, 0, DYN_HTTP_REQUEST); + stream->resp_hds_len = 0; + stream->bodystarted = FALSE; + stream->status_code = -1; + stream->closed = FALSE; + stream->close_handled = FALSE; + stream->error = NGHTTP2_NO_ERROR; + stream->local_window_size = H2_STREAM_WINDOW_SIZE; + stream->upload_left = 0; + H2_STREAM_LCTX(data) = stream; + *pstream = stream; return CURLE_OK; } -/* - * 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. - * - * Instead, if it is readable, run Curl_connalive() to peek at the socket - * and distinguish between closed and data. - */ -static bool http2_connisdead(struct Curl_easy *data, struct connectdata *conn) +static void http2_data_done(struct Curl_cfilter *cf, + struct Curl_easy *data, bool premature) { - int sval; - bool dead = TRUE; + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); - if(conn->bits.close) - return TRUE; + DEBUGASSERT(ctx); + (void)premature; + if(!stream) + return; - sval = SOCKET_READABLE(conn->sock[FIRSTSOCKET], 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) { - /* readable with no error. could still be closed */ - dead = !Curl_connalive(conn); - if(!dead) { - /* 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; - struct http_conn *httpc = &conn->proto.httpc; - ssize_t nread = -1; - if(httpc->recv_underlying) - /* if called "too early", this pointer isn't setup yet! */ - nread = ((Curl_recv *)httpc->recv_underlying)( - data, FIRSTSOCKET, httpc->inbuf, H2_BUFSIZE, &result); - if(nread != -1) { - H2BUGF(infof(data, - "%d bytes stray data read before trying h2 connection", - (int)nread)); - httpc->nread_inbuf = 0; - httpc->inbuflen = nread; - if(h2_process_pending_input(data, httpc, &result) < 0) - /* immediate error, considered dead */ - dead = TRUE; - } - else - /* the read failed so let's say this is dead anyway */ - dead = TRUE; + if(ctx->h2) { + bool flush_egress = FALSE; + /* returns error if stream not known, which is fine here */ + (void)nghttp2_session_set_stream_user_data(ctx->h2, stream->id, NULL); + + if(!stream->closed && stream->id > 0) { + /* RST_STREAM */ + CURL_TRC_CF(data, cf, "[%d] premature DATA_DONE, RST stream", + stream->id); + stream->closed = TRUE; + stream->reset = TRUE; + stream->send_closed = TRUE; + nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE, + stream->id, NGHTTP2_STREAM_CLOSED); + flush_egress = TRUE; + } + if(!Curl_bufq_is_empty(&stream->recvbuf)) { + /* Anything in the recvbuf is still being counted + * in stream and connection window flow control. Need + * to free that space or the connection window might get + * exhausted eventually. */ + nghttp2_session_consume(ctx->h2, stream->id, + Curl_bufq_len(&stream->recvbuf)); + /* give WINDOW_UPATE a chance to be sent, but ignore any error */ + flush_egress = TRUE; + } + + if(flush_egress) + nghttp2_session_send(ctx->h2); + } + + Curl_bufq_free(&stream->sendbuf); + Curl_bufq_free(&stream->recvbuf); + Curl_h1_req_parse_free(&stream->h1); + Curl_dynhds_free(&stream->resp_trailers); + if(stream->push_headers) { + /* if they weren't used and then freed before */ + for(; stream->push_headers_used > 0; --stream->push_headers_used) { + free(stream->push_headers[stream->push_headers_used - 1]); } + free(stream->push_headers); + stream->push_headers = NULL; } - return dead; + free(stream); + H2_STREAM_LCTX(data) = NULL; } -/* - * Set the transfer that is currently using this HTTP/2 connection. - */ -static void set_transfer(struct http_conn *c, - struct Curl_easy *data) +static int h2_client_new(struct Curl_cfilter *cf, + nghttp2_session_callbacks *cbs) { - c->trnsfr = data; + struct cf_h2_ctx *ctx = cf->ctx; + nghttp2_option *o; + + int rc = nghttp2_option_new(&o); + if(rc) + return rc; + /* We handle window updates ourself to enforce buffer limits */ + nghttp2_option_set_no_auto_window_update(o, 1); +#if NGHTTP2_VERSION_NUM >= 0x013200 + /* with 1.50.0 */ + /* turn off RFC 9113 leading and trailing white spaces validation against + HTTP field value. */ + nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(o, 1); +#endif + rc = nghttp2_session_client_new2(&ctx->h2, cbs, cf, o); + nghttp2_option_del(o); + return rc; } -/* - * Get the transfer that is currently using this HTTP/2 connection. - */ -static struct Curl_easy *get_transfer(struct http_conn *c) +static ssize_t nw_in_reader(void *reader_ctx, + unsigned char *buf, size_t buflen, + CURLcode *err) { - DEBUGASSERT(c && c->trnsfr); - return c->trnsfr; + struct Curl_cfilter *cf = reader_ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + + return Curl_conn_cf_recv(cf->next, data, (char *)buf, buflen, err); } -static unsigned int http2_conncheck(struct Curl_easy *data, - struct connectdata *conn, - unsigned int checks_to_perform) +static ssize_t nw_out_writer(void *writer_ctx, + const unsigned char *buf, size_t buflen, + CURLcode *err) { - unsigned int ret_val = CONNRESULT_NONE; - struct http_conn *c = &conn->proto.httpc; + struct Curl_cfilter *cf = writer_ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + + if(data) { + ssize_t nwritten = Curl_conn_cf_send(cf->next, data, + (const char *)buf, buflen, err); + if(nwritten > 0) + CURL_TRC_CF(data, cf, "[0] egress: wrote %zd bytes", nwritten); + return nwritten; + } + return 0; +} + +static ssize_t send_callback(nghttp2_session *h2, + const uint8_t *mem, size_t length, int flags, + void *userp); +static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, + void *userp); +#ifndef CURL_DISABLE_VERBOSE_STRINGS +static int on_frame_send(nghttp2_session *session, const nghttp2_frame *frame, + void *userp); +#endif +static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags, + int32_t stream_id, + const uint8_t *mem, size_t len, void *userp); +static int on_stream_close(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *userp); +static int on_begin_headers(nghttp2_session *session, + const nghttp2_frame *frame, void *userp); +static int on_header(nghttp2_session *session, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + uint8_t flags, + void *userp); +static int error_callback(nghttp2_session *session, const char *msg, + size_t len, void *userp); + +/* + * Initialize the cfilter context + */ +static CURLcode cf_h2_ctx_init(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool via_h1_upgrade) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream; + CURLcode result = CURLE_OUT_OF_MEMORY; int rc; - bool send_frames = false; + nghttp2_session_callbacks *cbs = NULL; - if(checks_to_perform & CONNCHECK_ISDEAD) { - if(http2_connisdead(data, conn)) - ret_val |= CONNRESULT_DEAD; + DEBUGASSERT(!ctx->h2); + Curl_bufcp_init(&ctx->stream_bufcp, H2_CHUNK_SIZE, H2_STREAM_POOL_SPARES); + Curl_bufq_initp(&ctx->inbufq, &ctx->stream_bufcp, H2_NW_RECV_CHUNKS, 0); + Curl_bufq_initp(&ctx->outbufq, &ctx->stream_bufcp, H2_NW_SEND_CHUNKS, 0); + ctx->last_stream_id = 2147483647; + + rc = nghttp2_session_callbacks_new(&cbs); + if(rc) { + failf(data, "Couldn't initialize nghttp2 callbacks"); + goto out; } - if(checks_to_perform & CONNCHECK_KEEPALIVE) { - struct curltime now = Curl_now(); - timediff_t elapsed = Curl_timediff(now, conn->keepalive); + nghttp2_session_callbacks_set_send_callback(cbs, send_callback); + nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv); +#ifndef CURL_DISABLE_VERBOSE_STRINGS + nghttp2_session_callbacks_set_on_frame_send_callback(cbs, on_frame_send); +#endif + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + cbs, on_data_chunk_recv); + nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close); + nghttp2_session_callbacks_set_on_begin_headers_callback( + cbs, on_begin_headers); + nghttp2_session_callbacks_set_on_header_callback(cbs, on_header); + nghttp2_session_callbacks_set_error_callback(cbs, error_callback); + + /* The nghttp2 session is not yet setup, do it */ + rc = h2_client_new(cf, cbs); + if(rc) { + failf(data, "Couldn't initialize nghttp2"); + goto out; + } + ctx->max_concurrent_streams = DEFAULT_MAX_CONCURRENT_STREAMS; + + if(via_h1_upgrade) { + /* HTTP/1.1 Upgrade issued. H2 Settings have already been submitted + * in the H1 request and we upgrade from there. This stream + * is opened implicitly as #1. */ + uint8_t binsettings[H2_BINSETTINGS_LEN]; + ssize_t binlen; /* length of the binsettings data */ + + binlen = populate_binsettings(binsettings, data); + if(binlen <= 0) { + failf(data, "nghttp2 unexpectedly failed on pack_settings_payload"); + result = CURLE_FAILED_INIT; + goto out; + } - if(elapsed > data->set.upkeep_interval_ms) { - /* Perform an HTTP/2 PING */ - rc = nghttp2_submit_ping(c->h2, 0, ZERO_NULL); - if(!rc) { - /* Successfully added a PING frame to the session. Need to flag this - so the frame is sent. */ - send_frames = true; - } - else { - failf(data, "nghttp2_submit_ping() failed: %s(%d)", - nghttp2_strerror(rc), rc); - } + result = http2_data_setup(cf, data, &stream); + if(result) + goto out; + DEBUGASSERT(stream); + stream->id = 1; + /* queue SETTINGS frame (again) */ + rc = nghttp2_session_upgrade2(ctx->h2, binsettings, binlen, + data->state.httpreq == HTTPREQ_HEAD, + NULL); + if(rc) { + failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", + nghttp2_strerror(rc), rc); + result = CURLE_HTTP2; + goto out; + } - conn->keepalive = now; + rc = nghttp2_session_set_stream_user_data(ctx->h2, stream->id, + data); + if(rc) { + infof(data, "http/2: failed to set user_data for stream %u", + stream->id); + DEBUGASSERT(0); } + CURL_TRC_CF(data, cf, "created session via Upgrade"); } + else { + nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN]; + int ivlen; - if(send_frames) { - set_transfer(c, data); /* set the transfer */ - rc = nghttp2_session_send(c->h2); - if(rc) - failf(data, "nghttp2_session_send() failed: %s(%d)", + ivlen = populate_settings(iv, data); + rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, + iv, ivlen); + if(rc) { + failf(data, "nghttp2_submit_settings() failed: %s(%d)", nghttp2_strerror(rc), rc); + result = CURLE_HTTP2; + goto out; + } } - return ret_val; + rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0, + HTTP2_HUGE_WINDOW_SIZE); + if(rc) { + failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", + nghttp2_strerror(rc), rc); + result = CURLE_HTTP2; + goto out; + } + + /* all set, traffic will be send on connect */ + result = CURLE_OK; + CURL_TRC_CF(data, cf, "[0] created h2 session%s", + via_h1_upgrade? " (via h1 upgrade)" : ""); + +out: + if(cbs) + nghttp2_session_callbacks_del(cbs); + return result; } -/* called from http_setup_conn */ -void Curl_http2_setup_req(struct Curl_easy *data) +/* + * Returns nonzero if current HTTP/2 session should be closed. + */ +static int should_close_session(struct cf_h2_ctx *ctx) { - struct HTTP *http = data->req.p.http; - http->bodystarted = FALSE; - http->status_code = -1; - http->pausedata = NULL; - http->pauselen = 0; - http->closed = FALSE; - http->close_handled = FALSE; - http->mem = NULL; - http->len = 0; - http->memlen = 0; - http->error = NGHTTP2_NO_ERROR; + return ctx->drain_total == 0 && !nghttp2_session_want_read(ctx->h2) && + !nghttp2_session_want_write(ctx->h2); } -/* called from http_setup_conn */ -void Curl_http2_setup_conn(struct connectdata *conn) +/* + * Processes pending input left in network input buffer. + * This function returns 0 if it succeeds, or -1 and error code will + * be assigned to *err. + */ +static int h2_process_pending_input(struct Curl_cfilter *cf, + struct Curl_easy *data, + CURLcode *err) { - conn->proto.httpc.settings.max_concurrent_streams = - DEFAULT_MAX_CONCURRENT_STREAMS; + struct cf_h2_ctx *ctx = cf->ctx; + const unsigned char *buf; + size_t blen; + ssize_t rv; + + while(Curl_bufq_peek(&ctx->inbufq, &buf, &blen)) { + + rv = nghttp2_session_mem_recv(ctx->h2, (const uint8_t *)buf, blen); + if(rv < 0) { + failf(data, + "process_pending_input: nghttp2_session_mem_recv() returned " + "%zd:%s", rv, nghttp2_strerror((int)rv)); + *err = CURLE_RECV_ERROR; + return -1; + } + Curl_bufq_skip(&ctx->inbufq, (size_t)rv); + if(Curl_bufq_is_empty(&ctx->inbufq)) { + break; + } + else { + CURL_TRC_CF(data, cf, "process_pending_input: %zu bytes left " + "in connection buffer", Curl_bufq_len(&ctx->inbufq)); + } + } + + if(nghttp2_session_check_request_allowed(ctx->h2) == 0) { + /* No more requests are allowed in the current session, so + the connection may not be reused. This is set when a + GOAWAY frame has been received or when the limit of stream + identifiers has been reached. */ + connclose(cf->conn, "http/2: No new requests allowed"); + } + + return 0; } /* - * HTTP2 handler interface. This isn't added to the general list of protocols - * but will be used at run-time when the protocol is dynamically switched from - * HTTP to HTTP2. + * 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 const struct Curl_handler Curl_handler_http2 = { - "HTTP", /* scheme */ - ZERO_NULL, /* setup_connection */ - Curl_http, /* do_it */ - Curl_http_done, /* done */ - ZERO_NULL, /* do_more */ - ZERO_NULL, /* connect_it */ - ZERO_NULL, /* connecting */ - ZERO_NULL, /* doing */ - http2_getsock, /* proto_getsock */ - http2_getsock, /* doing_getsock */ - ZERO_NULL, /* domore_getsock */ - http2_getsock, /* perform_getsock */ - http2_disconnect, /* disconnect */ - ZERO_NULL, /* readwrite */ - http2_conncheck, /* connection_check */ - ZERO_NULL, /* attach connection */ - PORT_HTTP, /* defport */ - CURLPROTO_HTTP, /* protocol */ - CURLPROTO_HTTP, /* family */ - PROTOPT_STREAM /* flags */ -}; +static bool http2_connisalive(struct Curl_cfilter *cf, struct Curl_easy *data, + bool *input_pending) +{ + struct cf_h2_ctx *ctx = cf->ctx; + 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" */ + CURLcode result; + ssize_t nread = -1; + + *input_pending = FALSE; + nread = Curl_bufq_slurp(&ctx->inbufq, nw_in_reader, cf, &result); + if(nread != -1) { + CURL_TRC_CF(data, cf, "%zd bytes stray data read before trying " + "h2 connection", nread); + if(h2_process_pending_input(cf, data, &result) < 0) + /* immediate error, considered dead */ + alive = FALSE; + else { + alive = !should_close_session(ctx); + } + } + else if(result != CURLE_AGAIN) { + /* the read failed so let's say this is dead anyway */ + alive = FALSE; + } + } -static const struct Curl_handler Curl_handler_http2_ssl = { - "HTTPS", /* scheme */ - ZERO_NULL, /* setup_connection */ - Curl_http, /* do_it */ - Curl_http_done, /* done */ - ZERO_NULL, /* do_more */ - ZERO_NULL, /* connect_it */ - ZERO_NULL, /* connecting */ - ZERO_NULL, /* doing */ - http2_getsock, /* proto_getsock */ - http2_getsock, /* doing_getsock */ - ZERO_NULL, /* domore_getsock */ - http2_getsock, /* perform_getsock */ - http2_disconnect, /* disconnect */ - ZERO_NULL, /* readwrite */ - http2_conncheck, /* connection_check */ - ZERO_NULL, /* attach connection */ - PORT_HTTP, /* defport */ - CURLPROTO_HTTPS, /* protocol */ - CURLPROTO_HTTP, /* family */ - PROTOPT_SSL | PROTOPT_STREAM /* flags */ -}; + return alive; +} + +static CURLcode http2_send_ping(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; + int rc; + + rc = nghttp2_submit_ping(ctx->h2, 0, ZERO_NULL); + if(rc) { + failf(data, "nghttp2_submit_ping() failed: %s(%d)", + nghttp2_strerror(rc), rc); + return CURLE_HTTP2; + } + + rc = nghttp2_session_send(ctx->h2); + if(rc) { + failf(data, "nghttp2_session_send() failed: %s(%d)", + nghttp2_strerror(rc), rc); + return CURLE_SEND_ERROR; + } + return CURLE_OK; +} /* * Store nghttp2 version info in this buffer. @@ -369,44 +653,64 @@ void Curl_http2_ver(char *p, size_t len) (void)msnprintf(p, len, "nghttp2/%s", h2->version_str); } +static CURLcode nw_out_flush(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; + ssize_t nwritten; + CURLcode result; + + (void)data; + if(Curl_bufq_is_empty(&ctx->outbufq)) + return CURLE_OK; + + nwritten = Curl_bufq_pass(&ctx->outbufq, nw_out_writer, cf, &result); + if(nwritten < 0) { + if(result == CURLE_AGAIN) { + CURL_TRC_CF(data, cf, "flush nw send buffer(%zu) -> EAGAIN", + Curl_bufq_len(&ctx->outbufq)); + ctx->nw_out_blocked = 1; + } + return result; + } + return Curl_bufq_is_empty(&ctx->outbufq)? CURLE_OK: CURLE_AGAIN; +} + /* * The implementation of nghttp2_send_callback type. Here we write |data| with * size |length| to the network and return the number of bytes actually * written. See the documentation of nghttp2_send_callback for the details. */ static ssize_t send_callback(nghttp2_session *h2, - const uint8_t *mem, size_t length, int flags, + const uint8_t *buf, size_t blen, int flags, void *userp) { - struct connectdata *conn = (struct connectdata *)userp; - struct http_conn *c = &conn->proto.httpc; - struct Curl_easy *data = get_transfer(c); - ssize_t written; + struct Curl_cfilter *cf = userp; + struct cf_h2_ctx *ctx = cf->ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + ssize_t nwritten; CURLcode result = CURLE_OK; (void)h2; (void)flags; + DEBUGASSERT(data); - if(!c->send_underlying) - /* called before setup properly! */ - return NGHTTP2_ERR_CALLBACK_FAILURE; - - written = ((Curl_send*)c->send_underlying)(data, FIRSTSOCKET, - mem, length, &result); - - if(result == CURLE_AGAIN) { - return NGHTTP2_ERR_WOULDBLOCK; - } - - if(written == -1) { + nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen, + nw_out_writer, cf, &result); + if(nwritten < 0) { + if(result == CURLE_AGAIN) { + ctx->nw_out_blocked = 1; + return NGHTTP2_ERR_WOULDBLOCK; + } failf(data, "Failed sending HTTP2 data"); return NGHTTP2_ERR_CALLBACK_FAILURE; } - if(!written) + if(!nwritten) { + ctx->nw_out_blocked = 1; return NGHTTP2_ERR_WOULDBLOCK; - - return written; + } + return nwritten; } @@ -427,8 +731,8 @@ char *curl_pushheader_bynum(struct curl_pushheaders *h, size_t num) if(!h || !GOOD_EASY_HANDLE(h->data)) return NULL; else { - struct HTTP *stream = h->data->req.p.http; - if(num < stream->push_headers_used) + struct stream_ctx *stream = H2_STREAM_CTX(h->data); + if(stream && num < stream->push_headers_used) return stream->push_headers[num]; } return NULL; @@ -439,6 +743,9 @@ char *curl_pushheader_bynum(struct curl_pushheaders *h, size_t num) */ char *curl_pushheader_byname(struct curl_pushheaders *h, const char *header) { + struct stream_ctx *stream; + size_t len; + size_t i; /* Verify that we got a good easy handle in the push header struct, mostly to detect rubbish input fast(er). Also empty header name is just a rubbish too. We have to allow ":" at the beginning of @@ -448,45 +755,25 @@ char *curl_pushheader_byname(struct curl_pushheaders *h, const char *header) if(!h || !GOOD_EASY_HANDLE(h->data) || !header || !header[0] || !strcmp(header, ":") || strchr(header + 1, ':')) return NULL; - else { - struct HTTP *stream = h->data->req.p.http; - size_t len = strlen(header); - size_t i; - for(i = 0; i<stream->push_headers_used; i++) { - if(!strncmp(header, stream->push_headers[i], len)) { - /* sub-match, make sure that it is followed by a colon */ - if(stream->push_headers[i][len] != ':') - continue; - return &stream->push_headers[i][len + 1]; - } + + stream = H2_STREAM_CTX(h->data); + if(!stream) + return NULL; + + len = strlen(header); + for(i = 0; i<stream->push_headers_used; i++) { + if(!strncmp(header, stream->push_headers[i], len)) { + /* sub-match, make sure that it is followed by a colon */ + if(stream->push_headers[i][len] != ':') + continue; + return &stream->push_headers[i][len + 1]; } } return NULL; } -/* - * This specific transfer on this connection has been "drained". - */ -static void drained_transfer(struct Curl_easy *data, - struct http_conn *httpc) -{ - DEBUGASSERT(httpc->drain_total >= data->state.drain); - httpc->drain_total -= data->state.drain; - data->state.drain = 0; -} - -/* - * Mark this transfer to get "drained". - */ -static void drain_this(struct Curl_easy *data, - struct http_conn *httpc) -{ - data->state.drain++; - httpc->drain_total++; - DEBUGASSERT(httpc->drain_total >= data->state.drain); -} - -static struct Curl_easy *duphandle(struct Curl_easy *data) +static struct Curl_easy *h2_duphandle(struct Curl_cfilter *cf, + struct Curl_easy *data) { struct Curl_easy *second = curl_easy_duphandle(data); if(second) { @@ -496,10 +783,11 @@ static struct Curl_easy *duphandle(struct Curl_easy *data) (void)Curl_close(&second); } else { + struct stream_ctx *second_stream; + second->req.p.http = http; - Curl_dyn_init(&http->header_recvbuf, DYN_H2_HEADERS); - Curl_http2_setup_req(second); - second->state.stream_weight = data->state.stream_weight; + http2_data_setup(cf, second, &second_stream); + second->state.priority.weight = data->state.priority.weight; } } return second; @@ -517,7 +805,7 @@ static int set_transfer_url(struct Curl_easy *data, if(!u) return 5; - v = curl_pushheader_byname(hp, H2H3_PSEUDO_SCHEME); + v = curl_pushheader_byname(hp, HTTP_PSEUDO_SCHEME); if(v) { uc = curl_url_set(u, CURLUPART_SCHEME, v, 0); if(uc) { @@ -526,16 +814,16 @@ static int set_transfer_url(struct Curl_easy *data, } } - v = curl_pushheader_byname(hp, H2H3_PSEUDO_AUTHORITY); + v = curl_pushheader_byname(hp, HTTP_PSEUDO_AUTHORITY); if(v) { - uc = curl_url_set(u, CURLUPART_HOST, v, 0); + uc = Curl_url_set_authority(u, v, CURLU_DISALLOW_USER); if(uc) { rc = 2; goto fail; } } - v = curl_pushheader_byname(hp, H2H3_PSEUDO_PATH); + v = curl_pushheader_byname(hp, HTTP_PSEUDO_PATH); if(v) { uc = curl_url_set(u, CURLUPART_PATH, v, 0); if(uc) { @@ -547,7 +835,7 @@ static int set_transfer_url(struct Curl_easy *data, uc = curl_url_get(u, CURLUPART_URL, &url, 0); if(uc) rc = 4; - fail: +fail: curl_url_cleanup(u); if(rc) return rc; @@ -559,22 +847,34 @@ static int set_transfer_url(struct Curl_easy *data, return 0; } -static int push_promise(struct Curl_easy *data, - struct connectdata *conn, +static void discard_newhandle(struct Curl_cfilter *cf, + struct Curl_easy *newhandle) +{ + if(!newhandle->req.p.http) { + http2_data_done(cf, newhandle, TRUE); + newhandle->req.p.http = NULL; + } + (void)Curl_close(&newhandle); +} + +static int push_promise(struct Curl_cfilter *cf, + struct Curl_easy *data, const nghttp2_push_promise *frame) { + struct cf_h2_ctx *ctx = cf->ctx; int rv; /* one of the CURL_PUSH_* defines */ - H2BUGF(infof(data, "PUSH_PROMISE received, stream %u", - frame->promised_stream_id)); + + CURL_TRC_CF(data, cf, "[%d] PUSH_PROMISE received", + frame->promised_stream_id); if(data->multi->push_cb) { - struct HTTP *stream; - struct HTTP *newstream; + struct stream_ctx *stream; + struct stream_ctx *newstream; struct curl_pushheaders heads; CURLMcode rc; - struct http_conn *httpc; + CURLcode result; size_t i; /* clone the parent */ - struct Curl_easy *newhandle = duphandle(data); + struct Curl_easy *newhandle = h2_duphandle(cf, data); if(!newhandle) { infof(data, "failed to duplicate handle"); rv = CURL_PUSH_DENY; /* FAIL HARD */ @@ -584,22 +884,31 @@ static int push_promise(struct Curl_easy *data, heads.data = data; heads.frame = frame; /* ask the application */ - H2BUGF(infof(data, "Got PUSH_PROMISE, ask application")); + CURL_TRC_CF(data, cf, "Got PUSH_PROMISE, ask application"); - stream = data->req.p.http; + stream = H2_STREAM_CTX(data); if(!stream) { failf(data, "Internal NULL stream"); - (void)Curl_close(&newhandle); + discard_newhandle(cf, newhandle); rv = CURL_PUSH_DENY; goto fail; } rv = set_transfer_url(newhandle, &heads); if(rv) { - (void)Curl_close(&newhandle); + discard_newhandle(cf, newhandle); + rv = CURL_PUSH_DENY; + goto fail; + } + + result = http2_data_setup(cf, newhandle, &newstream); + if(result) { + failf(data, "error setting up stream: %d", result); + discard_newhandle(cf, newhandle); rv = CURL_PUSH_DENY; goto fail; } + DEBUGASSERT(stream); Curl_set_in_callback(data, true); rv = data->multi->push_cb(data, newhandle, @@ -617,128 +926,110 @@ static int push_promise(struct Curl_easy *data, if(rv) { DEBUGASSERT((rv > CURL_PUSH_OK) && (rv <= CURL_PUSH_ERROROUT)); /* denied, kill off the new handle again */ - http2_stream_free(newhandle->req.p.http); - newhandle->req.p.http = NULL; - (void)Curl_close(&newhandle); + discard_newhandle(cf, newhandle); goto fail; } - newstream = newhandle->req.p.http; - newstream->stream_id = frame->promised_stream_id; + newstream->id = frame->promised_stream_id; newhandle->req.maxdownload = -1; newhandle->req.size = -1; /* approved, add to the multi handle and immediately switch to PERFORM state with the given connection !*/ - rc = Curl_multi_add_perform(data->multi, newhandle, conn); + rc = Curl_multi_add_perform(data->multi, newhandle, cf->conn); if(rc) { infof(data, "failed to add handle to multi"); - http2_stream_free(newhandle->req.p.http); - newhandle->req.p.http = NULL; - Curl_close(&newhandle); + discard_newhandle(cf, newhandle); rv = CURL_PUSH_DENY; goto fail; } - httpc = &conn->proto.httpc; - rv = nghttp2_session_set_stream_user_data(httpc->h2, - frame->promised_stream_id, + rv = nghttp2_session_set_stream_user_data(ctx->h2, + newstream->id, newhandle); if(rv) { infof(data, "failed to set user_data for stream %u", - frame->promised_stream_id); + newstream->id); DEBUGASSERT(0); rv = CURL_PUSH_DENY; goto fail; } - Curl_dyn_init(&newstream->header_recvbuf, DYN_H2_HEADERS); - Curl_dyn_init(&newstream->trailer_recvbuf, DYN_H2_TRAILERS); } else { - H2BUGF(infof(data, "Got PUSH_PROMISE, ignore it")); + CURL_TRC_CF(data, cf, "Got PUSH_PROMISE, ignore it"); rv = CURL_PUSH_DENY; } - fail: +fail: return rv; } -/* - * multi_connchanged() is called to tell that there is a connection in - * this multi handle that has changed state (multiplexing become possible, the - * number of allowed streams changed or similar), and a subsequent use of this - * multi handle should move CONNECT_PEND handles back to CONNECT to have them - * retry. - */ -static void multi_connchanged(struct Curl_multi *multi) +static CURLcode recvbuf_write_hds(struct Curl_cfilter *cf, + struct Curl_easy *data, + const char *buf, size_t blen) { - multi->recheckstate = TRUE; + struct stream_ctx *stream = H2_STREAM_CTX(data); + ssize_t nwritten; + CURLcode result; + + (void)cf; + nwritten = Curl_bufq_write(&stream->recvbuf, + (const unsigned char *)buf, blen, &result); + if(nwritten < 0) + return result; + stream->resp_hds_len += (size_t)nwritten; + DEBUGASSERT((size_t)nwritten == blen); + return CURLE_OK; } -static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, - void *userp) +static CURLcode on_stream_frame(struct Curl_cfilter *cf, + struct Curl_easy *data, + const nghttp2_frame *frame) { - struct connectdata *conn = (struct connectdata *)userp; - struct http_conn *httpc = &conn->proto.httpc; - struct Curl_easy *data_s = NULL; - struct HTTP *stream = NULL; - struct Curl_easy *data = get_transfer(httpc); - int rv; - size_t left, ncopy; + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); int32_t stream_id = frame->hd.stream_id; CURLcode result; + size_t rbuflen; + int rv; - if(!stream_id) { - /* stream ID zero is for connection-oriented stuff */ - if(frame->hd.type == NGHTTP2_SETTINGS) { - uint32_t max_conn = httpc->settings.max_concurrent_streams; - H2BUGF(infof(data, "Got SETTINGS")); - httpc->settings.max_concurrent_streams = - nghttp2_session_get_remote_settings( - session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); - httpc->settings.enable_push = - nghttp2_session_get_remote_settings( - session, NGHTTP2_SETTINGS_ENABLE_PUSH); - H2BUGF(infof(data, "MAX_CONCURRENT_STREAMS == %d", - httpc->settings.max_concurrent_streams)); - H2BUGF(infof(data, "ENABLE_PUSH == %s", - httpc->settings.enable_push?"TRUE":"false")); - if(max_conn != httpc->settings.max_concurrent_streams) { - /* only signal change if the value actually changed */ - infof(data, - "Connection state changed (MAX_CONCURRENT_STREAMS == %u)!", - httpc->settings.max_concurrent_streams); - multi_connchanged(data->multi); - } - } - return 0; - } - data_s = nghttp2_session_get_stream_user_data(session, stream_id); - if(!data_s) { - H2BUGF(infof(data, - "No Curl_easy associated with stream: %u", - stream_id)); - return 0; - } - - stream = data_s->req.p.http; if(!stream) { - H2BUGF(infof(data_s, "No proto pointer for stream: %u", - stream_id)); - return NGHTTP2_ERR_CALLBACK_FAILURE; + CURL_TRC_CF(data, cf, "[%d] No stream_ctx set", stream_id); + return CURLE_FAILED_INIT; } - H2BUGF(infof(data_s, "on_frame_recv() header %x stream %u", - frame->hd.type, stream_id)); - switch(frame->hd.type) { case NGHTTP2_DATA: - /* If body started on this stream, then receiving DATA is illegal. */ + rbuflen = Curl_bufq_len(&stream->recvbuf); + CURL_TRC_CF(data, cf, "[%d] DATA, buffered=%zu, window=%d/%d", + stream_id, rbuflen, + nghttp2_session_get_stream_effective_recv_data_length( + ctx->h2, stream->id), + nghttp2_session_get_stream_effective_local_window_size( + ctx->h2, stream->id)); + /* If !body started on this stream, then receiving DATA is illegal. */ if(!stream->bodystarted) { - rv = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + rv = nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_PROTOCOL_ERROR); if(nghttp2_is_fatal(rv)) { - return NGHTTP2_ERR_CALLBACK_FAILURE; + return CURLE_RECV_ERROR; + } + } + if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + drain_stream(cf, data, stream); + } + else if(rbuflen > stream->local_window_size) { + int32_t wsize = nghttp2_session_get_stream_local_window_size( + ctx->h2, stream->id); + if(wsize > 0 && (uint32_t)wsize != stream->local_window_size) { + /* H2 flow control is not absolute, as the server might not have the + * same view, yet. When we receive more than we want, we enforce + * the local window size again to make nghttp2 send WINDOW_UPATEs + * accordingly. */ + nghttp2_session_set_local_window_size(ctx->h2, + NGHTTP2_FLAG_NONE, + stream->id, + stream->local_window_size); } } break; @@ -753,7 +1044,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, stream->status_code. Fuzzing has proven this can still be reached without status code having been set. */ if(stream->status_code == -1) - return NGHTTP2_ERR_CALLBACK_FAILURE; + return CURLE_RECV_ERROR; /* Only final status code signals the end of header */ if(stream->status_code / 100 != 1) { @@ -761,69 +1052,232 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, stream->status_code = -1; } - result = Curl_dyn_addn(&stream->header_recvbuf, STRCONST("\r\n")); + result = recvbuf_write_hds(cf, data, STRCONST("\r\n")); if(result) - return NGHTTP2_ERR_CALLBACK_FAILURE; - - left = Curl_dyn_len(&stream->header_recvbuf) - - stream->nread_header_recvbuf; - ncopy = CURLMIN(stream->len, left); - - memcpy(&stream->mem[stream->memlen], - Curl_dyn_ptr(&stream->header_recvbuf) + - stream->nread_header_recvbuf, - ncopy); - stream->nread_header_recvbuf += ncopy; - - DEBUGASSERT(stream->mem); - H2BUGF(infof(data_s, "Store %zu bytes headers from stream %u at %p", - ncopy, stream_id, stream->mem)); + return result; - stream->len -= ncopy; - stream->memlen += ncopy; - - drain_this(data_s, httpc); - /* if we receive data for another handle, wake that up */ - if(get_transfer(httpc) != data_s) - Curl_expire(data_s, 0, EXPIRE_RUN_NOW); + if(stream->status_code / 100 != 1) { + stream->resp_hds_complete = TRUE; + } + drain_stream(cf, data, stream); break; case NGHTTP2_PUSH_PROMISE: - rv = push_promise(data_s, conn, &frame->push_promise); + rv = push_promise(cf, data, &frame->push_promise); if(rv) { /* deny! */ - int h2; DEBUGASSERT((rv > CURL_PUSH_OK) && (rv <= CURL_PUSH_ERROROUT)); - h2 = nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + rv = nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE, frame->push_promise.promised_stream_id, NGHTTP2_CANCEL); - if(nghttp2_is_fatal(h2)) - return NGHTTP2_ERR_CALLBACK_FAILURE; + if(nghttp2_is_fatal(rv)) + return CURLE_SEND_ERROR; else if(rv == CURL_PUSH_ERROROUT) { - DEBUGF(infof(data_s, "Fail the parent stream (too)")); - return NGHTTP2_ERR_CALLBACK_FAILURE; + CURL_TRC_CF(data, cf, "[%d] fail in PUSH_PROMISE received", + stream_id); + return CURLE_RECV_ERROR; } } break; + case NGHTTP2_RST_STREAM: + stream->closed = TRUE; + if(frame->rst_stream.error_code) { + stream->reset = TRUE; + } + stream->send_closed = TRUE; + drain_stream(cf, data, stream); + break; + case NGHTTP2_WINDOW_UPDATE: + if(CURL_WANT_SEND(data)) { + drain_stream(cf, data, stream); + } + break; default: - H2BUGF(infof(data_s, "Got frame type %x for stream %u", - frame->hd.type, stream_id)); break; } + return CURLE_OK; +} + +#ifndef CURL_DISABLE_VERBOSE_STRINGS +static int fr_print(const nghttp2_frame *frame, char *buffer, size_t blen) +{ + switch(frame->hd.type) { + case NGHTTP2_DATA: { + return msnprintf(buffer, blen, + "FRAME[DATA, len=%d, eos=%d, padlen=%d]", + (int)frame->hd.length, + !!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM), + (int)frame->data.padlen); + } + case NGHTTP2_HEADERS: { + return msnprintf(buffer, blen, + "FRAME[HEADERS, len=%d, hend=%d, eos=%d]", + (int)frame->hd.length, + !!(frame->hd.flags & NGHTTP2_FLAG_END_HEADERS), + !!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)); + } + case NGHTTP2_PRIORITY: { + return msnprintf(buffer, blen, + "FRAME[PRIORITY, len=%d, flags=%d]", + (int)frame->hd.length, frame->hd.flags); + } + case NGHTTP2_RST_STREAM: { + return msnprintf(buffer, blen, + "FRAME[RST_STREAM, len=%d, flags=%d, error=%u]", + (int)frame->hd.length, frame->hd.flags, + frame->rst_stream.error_code); + } + case NGHTTP2_SETTINGS: { + if(frame->hd.flags & NGHTTP2_FLAG_ACK) { + return msnprintf(buffer, blen, "FRAME[SETTINGS, ack=1]"); + } + return msnprintf(buffer, blen, + "FRAME[SETTINGS, len=%d]", (int)frame->hd.length); + } + case NGHTTP2_PUSH_PROMISE: { + return msnprintf(buffer, blen, + "FRAME[PUSH_PROMISE, len=%d, hend=%d]", + (int)frame->hd.length, + !!(frame->hd.flags & NGHTTP2_FLAG_END_HEADERS)); + } + case NGHTTP2_PING: { + return msnprintf(buffer, blen, + "FRAME[PING, len=%d, ack=%d]", + (int)frame->hd.length, + frame->hd.flags&NGHTTP2_FLAG_ACK); + } + case NGHTTP2_GOAWAY: { + char scratch[128]; + size_t s_len = sizeof(scratch)/sizeof(scratch[0]); + size_t len = (frame->goaway.opaque_data_len < s_len)? + frame->goaway.opaque_data_len : s_len-1; + if(len) + memcpy(scratch, frame->goaway.opaque_data, len); + scratch[len] = '\0'; + return msnprintf(buffer, blen, "FRAME[GOAWAY, error=%d, reason='%s', " + "last_stream=%d]", frame->goaway.error_code, + scratch, frame->goaway.last_stream_id); + } + case NGHTTP2_WINDOW_UPDATE: { + return msnprintf(buffer, blen, + "FRAME[WINDOW_UPDATE, incr=%d]", + frame->window_update.window_size_increment); + } + default: + return msnprintf(buffer, blen, "FRAME[%d, len=%d, flags=%d]", + frame->hd.type, (int)frame->hd.length, + frame->hd.flags); + } +} + +static int on_frame_send(nghttp2_session *session, const nghttp2_frame *frame, + void *userp) +{ + struct Curl_cfilter *cf = userp; + struct Curl_easy *data = CF_DATA_CURRENT(cf); + + (void)session; + DEBUGASSERT(data); + if(data && Curl_trc_cf_is_verbose(cf, data)) { + char buffer[256]; + int len; + len = fr_print(frame, buffer, sizeof(buffer)-1); + buffer[len] = 0; + CURL_TRC_CF(data, cf, "[%d] -> %s", frame->hd.stream_id, buffer); + } return 0; } +#endif /* !CURL_DISABLE_VERBOSE_STRINGS */ + +static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, + void *userp) +{ + struct Curl_cfilter *cf = userp; + struct cf_h2_ctx *ctx = cf->ctx; + struct Curl_easy *data = CF_DATA_CURRENT(cf), *data_s; + int32_t stream_id = frame->hd.stream_id; + + DEBUGASSERT(data); +#ifndef CURL_DISABLE_VERBOSE_STRINGS + if(Curl_trc_cf_is_verbose(cf, data)) { + char buffer[256]; + int len; + len = fr_print(frame, buffer, sizeof(buffer)-1); + buffer[len] = 0; + CURL_TRC_CF(data, cf, "[%d] <- %s",frame->hd.stream_id, buffer); + } +#endif /* !CURL_DISABLE_VERBOSE_STRINGS */ + + if(!stream_id) { + /* stream ID zero is for connection-oriented stuff */ + DEBUGASSERT(data); + switch(frame->hd.type) { + case NGHTTP2_SETTINGS: { + if(!(frame->hd.flags & NGHTTP2_FLAG_ACK)) { + uint32_t max_conn = ctx->max_concurrent_streams; + 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) != 0; + CURL_TRC_CF(data, cf, "[0] MAX_CONCURRENT_STREAMS: %d", + ctx->max_concurrent_streams); + CURL_TRC_CF(data, cf, "[0] ENABLE_PUSH: %s", + ctx->enable_push ? "TRUE" : "false"); + if(data && max_conn != ctx->max_concurrent_streams) { + /* only signal change if the value actually changed */ + CURL_TRC_CF(data, cf, "[0] notify MAX_CONCURRENT_STREAMS: %u", + ctx->max_concurrent_streams); + Curl_multi_connchanged(data->multi); + } + /* Since the initial stream window is 64K, a request might be on HOLD, + * due to exhaustion. The (initial) SETTINGS may announce a much larger + * window and *assume* that we treat this like a WINDOW_UPDATE. Some + * servers send an explicit WINDOW_UPDATE, but not all seem to do that. + * To be safe, we UNHOLD a stream in order not to stall. */ + if(CURL_WANT_SEND(data)) { + struct stream_ctx *stream = H2_STREAM_CTX(data); + if(stream) + drain_stream(cf, data, stream); + } + } + 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, "received GOAWAY, error=%d, last_stream=%u", + ctx->goaway_error, ctx->last_stream_id); + Curl_multi_connchanged(data->multi); + } + break; + default: + break; + } + return 0; + } + + data_s = nghttp2_session_get_stream_user_data(session, stream_id); + if(!data_s) { + CURL_TRC_CF(data, cf, "[%d] No Curl_easy associated", stream_id); + return 0; + } + + return on_stream_frame(cf, data_s, frame)? NGHTTP2_ERR_CALLBACK_FAILURE : 0; +} static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *mem, size_t len, void *userp) { - struct HTTP *stream; + struct Curl_cfilter *cf = userp; + struct stream_ctx *stream; struct Curl_easy *data_s; - size_t nread; - struct connectdata *conn = (struct connectdata *)userp; - struct http_conn *httpc = &conn->proto.httpc; - (void)session; + ssize_t nwritten; + CURLcode result; (void)flags; DEBUGASSERT(stream_id); /* should never be a zero stream ID here */ + DEBUGASSERT(CF_DATA_CURRENT(cf)); /* get the stream from the hash based on Stream ID */ data_s = nghttp2_session_get_stream_user_data(session, stream_id); @@ -831,100 +1285,87 @@ static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags, /* Receiving a Stream ID not in the hash should not happen - unless we have aborted a transfer artificially and there were more data in the pipeline. Silently ignore. */ - H2BUGF(fprintf(stderr, "Data for stream %u but it doesn't exist\n", - stream_id)); + CURL_TRC_CF(CF_DATA_CURRENT(cf), cf, "[%d] Data for unknown", + stream_id); + /* consumed explicitly as no one will read it */ + nghttp2_session_consume(session, stream_id, len); return 0; } - stream = data_s->req.p.http; + stream = H2_STREAM_CTX(data_s); if(!stream) return NGHTTP2_ERR_CALLBACK_FAILURE; - nread = CURLMIN(stream->len, len); - memcpy(&stream->mem[stream->memlen], mem, nread); - - stream->len -= nread; - stream->memlen += nread; - - drain_this(data_s, &conn->proto.httpc); - - /* if we receive data for another handle, wake that up */ - if(get_transfer(httpc) != data_s) - Curl_expire(data_s, 0, EXPIRE_RUN_NOW); - - H2BUGF(infof(data_s, "%zu data received for stream %u " - "(%zu left in buffer %p, total %zu)", - nread, stream_id, - stream->len, stream->mem, - stream->memlen)); - - if(nread < len) { - stream->pausedata = mem + nread; - stream->pauselen = len - nread; - H2BUGF(infof(data_s, "NGHTTP2_ERR_PAUSE - %zu bytes out of buffer" - ", stream %u", - len - nread, stream_id)); - data_s->conn->proto.httpc.pause_stream_id = stream_id; + nwritten = Curl_bufq_write(&stream->recvbuf, mem, len, &result); + if(nwritten < 0) { + if(result != CURLE_AGAIN) + return NGHTTP2_ERR_CALLBACK_FAILURE; - return NGHTTP2_ERR_PAUSE; + nwritten = 0; } - /* pause execution of nghttp2 if we received data for another handle - in order to process them first. */ - if(get_transfer(httpc) != data_s) { - data_s->conn->proto.httpc.pause_stream_id = stream_id; - - return NGHTTP2_ERR_PAUSE; - } + /* if we receive data for another handle, wake that up */ + drain_stream(cf, data_s, stream); + DEBUGASSERT((size_t)nwritten == len); return 0; } static int on_stream_close(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *userp) { - struct Curl_easy *data_s; - struct HTTP *stream; - struct connectdata *conn = (struct connectdata *)userp; + struct Curl_cfilter *cf = userp; + struct Curl_easy *data_s, *call_data = CF_DATA_CURRENT(cf); + struct stream_ctx *stream; int rv; (void)session; - (void)stream_id; - if(stream_id) { - struct http_conn *httpc; - /* 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; - } - H2BUGF(infof(data_s, "on_stream_close(), %s (err %d), stream %u", - nghttp2_http2_strerror(error_code), error_code, stream_id)); - stream = data_s->req.p.http; - if(!stream) - return NGHTTP2_ERR_CALLBACK_FAILURE; + DEBUGASSERT(call_data); + /* 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) { + CURL_TRC_CF(call_data, cf, + "[%d] on_stream_close, no easy set on stream", stream_id); + return 0; + } + if(!GOOD_EASY_HANDLE(data_s)) { + /* nghttp2 still has an easy registered for the stream which has + * been freed be libcurl. This points to a code path that does not + * trigger DONE or DETACH events as it must. */ + CURL_TRC_CF(call_data, cf, + "[%d] on_stream_close, not a GOOD easy on stream", stream_id); + (void)nghttp2_session_set_stream_user_data(session, stream_id, 0); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + stream = H2_STREAM_CTX(data_s); + if(!stream) { + CURL_TRC_CF(data_s, cf, + "[%d] on_stream_close, GOOD easy but no stream", stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } - stream->closed = TRUE; - httpc = &conn->proto.httpc; - drain_this(data_s, httpc); - 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; + stream->send_closed = 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 == httpc->pause_stream_id) { - H2BUGF(infof(data_s, "Stopped the pause stream")); - httpc->pause_stream_id = 0; - } - H2BUGF(infof(data_s, "Removed stream %u hash", stream_id)); - stream->stream_id = 0; /* cleared */ + if(stream->error) + CURL_TRC_CF(data_s, cf, "[%d] RESET: %s (err %d)", + stream_id, nghttp2_http2_strerror(error_code), error_code); + else + CURL_TRC_CF(data_s, cf, "[%d] CLOSED", stream_id); + drain_stream(cf, data_s, stream); + + /* 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); } return 0; } @@ -932,22 +1373,21 @@ static int on_stream_close(nghttp2_session *session, int32_t stream_id, static int on_begin_headers(nghttp2_session *session, const nghttp2_frame *frame, void *userp) { - struct HTTP *stream; + struct Curl_cfilter *cf = userp; + struct stream_ctx *stream; struct Curl_easy *data_s = NULL; - (void)userp; + (void)cf; data_s = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); if(!data_s) { return 0; } - H2BUGF(infof(data_s, "on_begin_headers() was called")); - if(frame->hd.type != NGHTTP2_HEADERS) { return 0; } - stream = data_s->req.p.http; + stream = H2_STREAM_CTX(data_s); if(!stream || !stream->bodystarted) { return 0; } @@ -955,33 +1395,6 @@ static int on_begin_headers(nghttp2_session *session, return 0; } -/* Decode HTTP status code. Returns -1 if no valid status code was - decoded. */ -static int decode_status_code(const uint8_t *value, size_t len) -{ - int i; - int res; - - if(len != 3) { - return -1; - } - - res = 0; - - for(i = 0; i < 3; ++i) { - char c = value[i]; - - if(c < '0' || c > '9') { - return -1; - } - - res *= 10; - res += c - '0'; - } - - return res; -} - /* frame->hd.type is either NGHTTP2_HEADERS or NGHTTP2_PUSH_PROMISE */ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, @@ -989,11 +1402,10 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, uint8_t flags, void *userp) { - struct HTTP *stream; + struct Curl_cfilter *cf = userp; + struct stream_ctx *stream; struct Curl_easy *data_s; int32_t stream_id = frame->hd.stream_id; - struct connectdata *conn = (struct connectdata *)userp; - struct http_conn *httpc = &conn->proto.httpc; CURLcode result; (void)flags; @@ -1006,7 +1418,7 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, internal error more than anything else! */ return NGHTTP2_ERR_CALLBACK_FAILURE; - stream = data_s->req.p.http; + stream = H2_STREAM_CTX(data_s); if(!stream) { failf(data_s, "Internal NULL stream"); return NGHTTP2_ERR_CALLBACK_FAILURE; @@ -1017,16 +1429,17 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, if(frame->hd.type == NGHTTP2_PUSH_PROMISE) { char *h; - if(!strcmp(H2H3_PSEUDO_AUTHORITY, (const char *)name)) { + if(!strcmp(HTTP_PSEUDO_AUTHORITY, (const char *)name)) { /* pseudo headers are lower case */ int rc = 0; - char *check = aprintf("%s:%d", conn->host.name, conn->remote_port); + char *check = aprintf("%s:%d", cf->conn->host.name, + cf->conn->remote_port); if(!check) /* no memory */ return NGHTTP2_ERR_CALLBACK_FAILURE; - if(!Curl_strcasecompare(check, (const char *)value) && - ((conn->remote_port != conn->given->defport) || - !Curl_strcasecompare(conn->host.name, (const char *)value))) { + if(!strcasecompare(check, (const char *)value) && + ((cf->conn->remote_port != cf->conn->given->defport) || + !strcasecompare(cf->conn->host.name, (const char *)value))) { /* This is push is not for the same authority that was asked for in * the URL. RFC 7540 section 8.2 says: "A client MUST treat a * PUSH_PROMISE for which the server is not authoritative as a stream @@ -1075,87 +1488,89 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame, if(stream->bodystarted) { /* This is a trailer */ - H2BUGF(infof(data_s, "h2 trailer: %.*s: %.*s", namelen, name, valuelen, - value)); - result = Curl_dyn_addf(&stream->trailer_recvbuf, - "%.*s: %.*s\r\n", namelen, name, - valuelen, value); + CURL_TRC_CF(data_s, cf, "[%d] trailer: %.*s: %.*s", + stream->id, (int)namelen, name, (int)valuelen, value); + result = Curl_dynhds_add(&stream->resp_trailers, + (const char *)name, namelen, + (const char *)value, valuelen); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; return 0; } - if(namelen == sizeof(H2H3_PSEUDO_STATUS) - 1 && - memcmp(H2H3_PSEUDO_STATUS, name, namelen) == 0) { - /* nghttp2 guarantees :status is received first and only once, and - value is 3 digits status code, and decode_status_code always - succeeds. */ + if(namelen == sizeof(HTTP_PSEUDO_STATUS) - 1 && + memcmp(HTTP_PSEUDO_STATUS, name, namelen) == 0) { + /* nghttp2 guarantees :status is received first and only once. */ char buffer[32]; - stream->status_code = decode_status_code(value, valuelen); - DEBUGASSERT(stream->status_code != -1); - msnprintf(buffer, sizeof(buffer), H2H3_PSEUDO_STATUS ":%u\r", + result = Curl_http_decode_status(&stream->status_code, + (const char *)value, valuelen); + if(result) + return NGHTTP2_ERR_CALLBACK_FAILURE; + msnprintf(buffer, sizeof(buffer), HTTP_PSEUDO_STATUS ":%u\r", stream->status_code); result = Curl_headers_push(data_s, buffer, CURLH_PSEUDO); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; - result = Curl_dyn_addn(&stream->header_recvbuf, STRCONST("HTTP/2 ")); + result = recvbuf_write_hds(cf, data_s, STRCONST("HTTP/2 ")); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; - result = Curl_dyn_addn(&stream->header_recvbuf, value, valuelen); + result = recvbuf_write_hds(cf, data_s, (const char *)value, valuelen); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; /* the space character after the status code is mandatory */ - result = Curl_dyn_addn(&stream->header_recvbuf, STRCONST(" \r\n")); + result = recvbuf_write_hds(cf, data_s, STRCONST(" \r\n")); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; /* if we receive data for another handle, wake that up */ - if(get_transfer(httpc) != data_s) + if(CF_DATA_CURRENT(cf) != data_s) Curl_expire(data_s, 0, EXPIRE_RUN_NOW); - H2BUGF(infof(data_s, "h2 status: HTTP/2 %03d (easy %p)", - stream->status_code, data_s)); + CURL_TRC_CF(data_s, cf, "[%d] status: HTTP/2 %03d", + stream->id, stream->status_code); return 0; } /* nghttp2 guarantees that namelen > 0, and :status was already received, and this is not pseudo-header field . */ - /* convert to a HTTP1-style header */ - result = Curl_dyn_addn(&stream->header_recvbuf, name, namelen); + /* convert to an HTTP1-style header */ + result = recvbuf_write_hds(cf, data_s, (const char *)name, namelen); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; - result = Curl_dyn_addn(&stream->header_recvbuf, STRCONST(": ")); + result = recvbuf_write_hds(cf, data_s, STRCONST(": ")); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; - result = Curl_dyn_addn(&stream->header_recvbuf, value, valuelen); + result = recvbuf_write_hds(cf, data_s, (const char *)value, valuelen); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; - result = Curl_dyn_addn(&stream->header_recvbuf, STRCONST("\r\n")); + result = recvbuf_write_hds(cf, data_s, STRCONST("\r\n")); if(result) return NGHTTP2_ERR_CALLBACK_FAILURE; /* if we receive data for another handle, wake that up */ - if(get_transfer(httpc) != data_s) + if(CF_DATA_CURRENT(cf) != data_s) Curl_expire(data_s, 0, EXPIRE_RUN_NOW); - H2BUGF(infof(data_s, "h2 header: %.*s: %.*s", namelen, name, valuelen, - value)); + CURL_TRC_CF(data_s, cf, "[%d] header: %.*s: %.*s", + stream->id, (int)namelen, name, (int)valuelen, value); return 0; /* 0 is successful */ } -static ssize_t data_source_read_callback(nghttp2_session *session, - int32_t stream_id, - uint8_t *buf, size_t length, - uint32_t *data_flags, - nghttp2_data_source *source, - void *userp) +static ssize_t req_body_read_callback(nghttp2_session *session, + int32_t stream_id, + uint8_t *buf, size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, + void *userp) { + struct Curl_cfilter *cf = userp; struct Curl_easy *data_s; - struct HTTP *stream = NULL; - size_t nread; + struct stream_ctx *stream = NULL; + CURLcode result; + ssize_t nread; (void)source; - (void)userp; + (void)cf; if(stream_id) { /* get the stream from the hash based on Stream ID, stream ID zero is for connection-oriented stuff */ @@ -1165,31 +1580,32 @@ static ssize_t data_source_read_callback(nghttp2_session *session, internal error more than anything else! */ return NGHTTP2_ERR_CALLBACK_FAILURE; - stream = data_s->req.p.http; + stream = H2_STREAM_CTX(data_s); if(!stream) return NGHTTP2_ERR_CALLBACK_FAILURE; } else return NGHTTP2_ERR_INVALID_ARGUMENT; - nread = CURLMIN(stream->upload_len, length); - if(nread > 0) { - memcpy(buf, stream->upload_mem, nread); - stream->upload_mem += nread; - stream->upload_len -= nread; - if(data_s->state.infilesize != -1) - stream->upload_left -= nread; + nread = Curl_bufq_read(&stream->sendbuf, buf, length, &result); + if(nread < 0) { + if(result != CURLE_AGAIN) + return NGHTTP2_ERR_CALLBACK_FAILURE; + nread = 0; } + if(nread > 0 && stream->upload_left != -1) + stream->upload_left -= nread; + + CURL_TRC_CF(data_s, cf, "[%d] req_body_read(len=%zu) left=%" + CURL_FORMAT_CURL_OFF_T " -> %zd, %d", + stream_id, length, stream->upload_left, nread, result); + if(stream->upload_left == 0) *data_flags = NGHTTP2_DATA_FLAG_EOF; else if(nread == 0) return NGHTTP2_ERR_DEFERRED; - H2BUGF(infof(data_s, "data_source_read_callback: " - "returns %zu bytes stream %u", - nread, stream_id)); - return nread; } @@ -1199,184 +1615,33 @@ static int error_callback(nghttp2_session *session, size_t len, void *userp) { + struct Curl_cfilter *cf = userp; + struct Curl_easy *data = CF_DATA_CURRENT(cf); (void)session; - (void)msg; - (void)len; - (void)userp; + failf(data, "%.*s", (int)len, msg); return 0; } #endif -static void populate_settings(struct Curl_easy *data, - struct http_conn *httpc) -{ - nghttp2_settings_entry *iv = httpc->local_settings; - - iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; - iv[0].value = Curl_multi_max_concurrent_streams(data->multi); - - iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; - iv[1].value = HTTP2_HUGE_WINDOW_SIZE; - - iv[2].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; - iv[2].value = data->multi->push_cb != NULL; - - httpc->local_settings_num = 3; -} - -void Curl_http2_done(struct Curl_easy *data, bool premature) -{ - struct HTTP *http = data->req.p.http; - struct http_conn *httpc = &data->conn->proto.httpc; - - /* there might be allocated resources done before this got the 'h2' pointer - setup */ - Curl_dyn_free(&http->header_recvbuf); - Curl_dyn_free(&http->trailer_recvbuf); - if(http->push_headers) { - /* if they weren't used and then freed before */ - for(; http->push_headers_used > 0; --http->push_headers_used) { - free(http->push_headers[http->push_headers_used - 1]); - } - free(http->push_headers); - http->push_headers = NULL; - } - - if(!(data->conn->handler->protocol&PROTO_FAMILY_HTTP) || - !httpc->h2) /* not HTTP/2 ? */ - return; - - /* do this before the reset handling, as that might clear ->stream_id */ - if(http->stream_id == httpc->pause_stream_id) { - H2BUGF(infof(data, "DONE the pause stream (%u)", http->stream_id)); - httpc->pause_stream_id = 0; - } - if(premature || (!http->closed && http->stream_id)) { - /* RST_STREAM */ - set_transfer(httpc, data); /* set the transfer */ - H2BUGF(infof(data, "RST stream %u", http->stream_id)); - if(!nghttp2_submit_rst_stream(httpc->h2, NGHTTP2_FLAG_NONE, - http->stream_id, NGHTTP2_STREAM_CLOSED)) - (void)nghttp2_session_send(httpc->h2); - } - - if(data->state.drain) - drained_transfer(data, httpc); - - /* -1 means unassigned and 0 means cleared */ - if(http->stream_id > 0) { - int rv = nghttp2_session_set_stream_user_data(httpc->h2, - http->stream_id, 0); - if(rv) { - infof(data, "http/2: failed to clear user_data for stream %u", - http->stream_id); - DEBUGASSERT(0); - } - set_transfer(httpc, NULL); - http->stream_id = 0; - } -} - -static int client_new(struct connectdata *conn, - nghttp2_session_callbacks *callbacks) -{ -#if NGHTTP2_VERSION_NUM < 0x013200 - /* before 1.50.0 */ - return nghttp2_session_client_new(&conn->proto.httpc.h2, callbacks, conn); -#else - nghttp2_option *o; - int rc = nghttp2_option_new(&o); - if(rc) - return rc; - /* turn off RFC 9113 leading and trailing white spaces validation against - HTTP field value. */ - nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(o, 1); - rc = nghttp2_session_client_new2(&conn->proto.httpc.h2, callbacks, conn, - o); - nghttp2_option_del(o); - return rc; -#endif -} - /* - * Initialize nghttp2 for a Curl connection - */ -static CURLcode http2_init(struct Curl_easy *data, struct connectdata *conn) -{ - if(!conn->proto.httpc.h2) { - int rc; - nghttp2_session_callbacks *callbacks; - - conn->proto.httpc.inbuf = malloc(H2_BUFSIZE); - if(!conn->proto.httpc.inbuf) - return CURLE_OUT_OF_MEMORY; - - rc = nghttp2_session_callbacks_new(&callbacks); - - if(rc) { - failf(data, "Couldn't initialize nghttp2 callbacks"); - return CURLE_OUT_OF_MEMORY; /* most likely at least */ - } - - /* nghttp2_send_callback */ - nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); - /* nghttp2_on_frame_recv_callback */ - nghttp2_session_callbacks_set_on_frame_recv_callback - (callbacks, on_frame_recv); - /* nghttp2_on_data_chunk_recv_callback */ - nghttp2_session_callbacks_set_on_data_chunk_recv_callback - (callbacks, on_data_chunk_recv); - /* nghttp2_on_stream_close_callback */ - nghttp2_session_callbacks_set_on_stream_close_callback - (callbacks, on_stream_close); - /* nghttp2_on_begin_headers_callback */ - nghttp2_session_callbacks_set_on_begin_headers_callback - (callbacks, on_begin_headers); - /* nghttp2_on_header_callback */ - nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header); - - nghttp2_session_callbacks_set_error_callback(callbacks, error_callback); - - /* The nghttp2 session is not yet setup, do it */ - rc = client_new(conn, callbacks); - - nghttp2_session_callbacks_del(callbacks); - - if(rc) { - failf(data, "Couldn't initialize nghttp2"); - return CURLE_OUT_OF_MEMORY; /* most likely at least */ - } - } - return CURLE_OK; -} - -/* - * Append headers to ask for a HTTP1.1 to HTTP2 upgrade. + * Append headers to ask for an HTTP1.1 to HTTP2 upgrade. */ CURLcode Curl_http2_request_upgrade(struct dynbuf *req, struct Curl_easy *data) { CURLcode result; - ssize_t binlen; char *base64; size_t blen; - struct connectdata *conn = data->conn; struct SingleRequest *k = &data->req; - uint8_t *binsettings = conn->proto.httpc.binsettings; - struct http_conn *httpc = &conn->proto.httpc; - - populate_settings(data, httpc); + uint8_t binsettings[H2_BINSETTINGS_LEN]; + ssize_t binlen; /* length of the binsettings data */ - /* this returns number of bytes it wrote */ - binlen = nghttp2_pack_settings_payload(binsettings, H2_BINSETTINGS_LEN, - httpc->local_settings, - httpc->local_settings_num); + binlen = populate_binsettings(binsettings, data); if(binlen <= 0) { failf(data, "nghttp2 unexpectedly failed on pack_settings_payload"); Curl_dyn_free(req); return CURLE_FAILED_INIT; } - conn->proto.httpc.binlen = binlen; result = Curl_base64url_encode((const char *)binsettings, binlen, &base64, &blen); @@ -1397,208 +1662,119 @@ 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 http_conn *httpc) -{ - return httpc->drain_total == 0 && !nghttp2_session_want_read(httpc->h2) && - !nghttp2_session_want_write(httpc->h2); -} - -/* - * h2_process_pending_input() processes pending input left in - * httpc->inbuf. Then, call h2_session_send() to send pending data. - * This function returns 0 if it succeeds, or -1 and error code will - * be assigned to *err. - */ -static int h2_process_pending_input(struct Curl_easy *data, - struct http_conn *httpc, - CURLcode *err) -{ - ssize_t nread; - char *inbuf; - ssize_t rv; - - nread = httpc->inbuflen - httpc->nread_inbuf; - inbuf = httpc->inbuf + httpc->nread_inbuf; - - set_transfer(httpc, data); /* set the transfer */ - rv = nghttp2_session_mem_recv(httpc->h2, (const uint8_t *)inbuf, nread); - if(rv < 0) { - failf(data, - "h2_process_pending_input: nghttp2_session_mem_recv() returned " - "%zd:%s", rv, nghttp2_strerror((int)rv)); - *err = CURLE_RECV_ERROR; - return -1; - } - - if(nread == rv) { - H2BUGF(infof(data, - "h2_process_pending_input: All data in connection buffer " - "processed")); - httpc->inbuflen = 0; - httpc->nread_inbuf = 0; - } - else { - httpc->nread_inbuf += rv; - H2BUGF(infof(data, - "h2_process_pending_input: %zu bytes left in connection " - "buffer", - httpc->inbuflen - httpc->nread_inbuf)); - } - - rv = h2_session_send(data, httpc->h2); - if(rv) { - *err = CURLE_SEND_ERROR; - return -1; - } - - if(nghttp2_session_check_request_allowed(httpc->h2) == 0) { - /* No more requests are allowed in the current session, so - the connection may not be reused. This is set when a - GOAWAY frame has been received or when the limit of stream - identifiers has been reached. */ - connclose(data->conn, "http/2: No new requests allowed"); - } - - if(should_close_session(httpc)) { - struct HTTP *stream = data->req.p.http; - H2BUGF(infof(data, - "h2_process_pending_input: nothing to do in this session")); - if(stream->error) - *err = CURLE_HTTP2; - else { - /* not an error per se, but should still close the connection */ - connclose(data->conn, "GOAWAY received"); - *err = CURLE_OK; - } - return -1; - } - return 0; -} - -/* - * Called from transfer.c:done_sending when we stop uploading. - */ -CURLcode Curl_http2_done_sending(struct Curl_easy *data, - struct connectdata *conn) +static CURLcode http2_data_done_send(struct Curl_cfilter *cf, + struct Curl_easy *data) { + struct cf_h2_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; + struct stream_ctx *stream = H2_STREAM_CTX(data); - if((conn->handler == &Curl_handler_http2_ssl) || - (conn->handler == &Curl_handler_http2)) { - /* make sure this is only attempted for HTTP/2 transfers */ - struct HTTP *stream = data->req.p.http; - struct http_conn *httpc = &conn->proto.httpc; - nghttp2_session *h2 = httpc->h2; + if(!ctx || !ctx->h2 || !stream) + goto out; + CURL_TRC_CF(data, cf, "[%d] data done send", stream->id); + if(!stream->send_closed) { + stream->send_closed = TRUE; if(stream->upload_left) { - /* If the stream still thinks there's data left to upload. */ - - stream->upload_left = 0; /* DONE! */ - + /* we now know that everything that is buffered is all there is. */ + stream->upload_left = Curl_bufq_len(&stream->sendbuf); /* resume sending here to trigger the callback to get called again so that it can signal EOF to nghttp2 */ - (void)nghttp2_session_resume_data(h2, stream->stream_id); - (void)h2_process_pending_input(data, httpc, &result); - } - - /* If nghttp2 still has pending frames unsent */ - if(nghttp2_session_want_write(h2)) { - struct SingleRequest *k = &data->req; - int rv; - - H2BUGF(infof(data, "HTTP/2 still wants to send data (easy %p)", data)); - - /* and attempt to send the pending frames */ - rv = h2_session_send(data, h2); - if(rv) - result = CURLE_SEND_ERROR; - - if(nghttp2_session_want_write(h2)) { - /* re-set KEEP_SEND to make sure we are called again */ - k->keepon |= KEEP_SEND; - } + (void)nghttp2_session_resume_data(ctx->h2, stream->id); + drain_stream(cf, data, stream); } } + +out: return result; } -static ssize_t http2_handle_stream_close(struct connectdata *conn, +static ssize_t http2_handle_stream_close(struct Curl_cfilter *cf, struct Curl_easy *data, - struct HTTP *stream, CURLcode *err) + struct stream_ctx *stream, + CURLcode *err) { - struct http_conn *httpc = &conn->proto.httpc; - - if(httpc->pause_stream_id == stream->stream_id) { - httpc->pause_stream_id = 0; - } - - drained_transfer(data, httpc); - - if(httpc->pause_stream_id == 0) { - if(h2_process_pending_input(data, httpc, err) != 0) { - return -1; - } - } - - DEBUGASSERT(data->state.drain == 0); + ssize_t rv = 0; - /* Reset to FALSE to prevent infinite loop in readwrite_data function. */ - stream->closed = FALSE; if(stream->error == NGHTTP2_REFUSED_STREAM) { - H2BUGF(infof(data, "REFUSED_STREAM (%u), try again on a new connection", - stream->stream_id)); - connclose(conn, "REFUSED_STREAM"); /* don't use this anymore */ + CURL_TRC_CF(data, cf, "[%d] REFUSED_STREAM, try again on a new " + "connection", stream->id); + connclose(cf->conn, "REFUSED_STREAM"); /* don't use this anymore */ data->state.refused_stream = TRUE; *err = CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ return -1; } else if(stream->error != NGHTTP2_NO_ERROR) { failf(data, "HTTP/2 stream %u was not closed cleanly: %s (err %u)", - stream->stream_id, nghttp2_http2_strerror(stream->error), + stream->id, nghttp2_http2_strerror(stream->error), stream->error); *err = CURLE_HTTP2_STREAM; return -1; } + else if(stream->reset) { + failf(data, "HTTP/2 stream %u was reset", 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 " " all response header fields, treated as error", - stream->stream_id); + stream->id); *err = CURLE_HTTP2_STREAM; return -1; } - if(Curl_dyn_len(&stream->trailer_recvbuf)) { - char *trailp = Curl_dyn_ptr(&stream->trailer_recvbuf); - char *lf; + if(Curl_dynhds_count(&stream->resp_trailers)) { + struct dynhds_entry *e; + struct dynbuf dbuf; + size_t i; - do { - size_t len = 0; - CURLcode result; - /* each trailer line ends with a newline */ - lf = strchr(trailp, '\n'); - if(!lf) + *err = CURLE_OK; + Curl_dyn_init(&dbuf, DYN_TRAILERS); + for(i = 0; i < Curl_dynhds_count(&stream->resp_trailers); ++i) { + e = Curl_dynhds_getn(&stream->resp_trailers, i); + if(!e) break; - len = lf + 1 - trailp; - - Curl_debug(data, CURLINFO_HEADER_IN, trailp, len); - /* pass the trailers one by one to the callback */ - result = Curl_client_write(data, CLIENTWRITE_HEADER, trailp, len); - if(result) { - *err = result; - return -1; - } - trailp = ++lf; - } while(lf); + Curl_dyn_reset(&dbuf); + *err = Curl_dyn_addf(&dbuf, "%.*s: %.*s\x0d\x0a", + (int)e->namelen, e->name, + (int)e->valuelen, e->value); + if(*err) + break; + Curl_debug(data, CURLINFO_HEADER_IN, Curl_dyn_ptr(&dbuf), + Curl_dyn_len(&dbuf)); + *err = Curl_client_write(data, CLIENTWRITE_HEADER|CLIENTWRITE_TRAILER, + Curl_dyn_ptr(&dbuf), Curl_dyn_len(&dbuf)); + if(*err) + break; + } + Curl_dyn_free(&dbuf); + if(*err) + goto out; } stream->close_handled = TRUE; + *err = CURLE_OK; + rv = 0; - H2BUGF(infof(data, "http2_recv returns 0, http2_handle_stream_close")); - return 0; +out: + CURL_TRC_CF(data, cf, "handle_stream_close -> %zd, %d", rv, *err); + return rv; +} + +static int sweight_wanted(const struct Curl_easy *data) +{ + /* 0 weight is not set by user and we take the nghttp2 default one */ + return data->set.priority.weight? + data->set.priority.weight : NGHTTP2_DEFAULT_WEIGHT; +} + +static int sweight_in_effect(const struct Curl_easy *data) +{ + /* 0 weight is not set by user and we take the nghttp2 default one */ + return data->state.priority.weight? + data->state.priority.weight : NGHTTP2_DEFAULT_WEIGHT; } /* @@ -1610,367 +1786,295 @@ static ssize_t http2_handle_stream_close(struct connectdata *conn, static void h2_pri_spec(struct Curl_easy *data, nghttp2_priority_spec *pri_spec) { - struct HTTP *depstream = (data->set.stream_depends_on? - data->set.stream_depends_on->req.p.http:NULL); - int32_t depstream_id = depstream? depstream->stream_id:0; - nghttp2_priority_spec_init(pri_spec, depstream_id, data->set.stream_weight, - data->set.stream_depends_e); - data->state.stream_weight = data->set.stream_weight; - data->state.stream_depends_e = data->set.stream_depends_e; - data->state.stream_depends_on = data->set.stream_depends_on; + struct Curl_data_priority *prio = &data->set.priority; + struct stream_ctx *depstream = H2_STREAM_CTX(prio->parent); + int32_t depstream_id = depstream? depstream->id:0; + nghttp2_priority_spec_init(pri_spec, depstream_id, + sweight_wanted(data), + data->set.priority.exclusive); + data->state.priority = *prio; } /* - * h2_session_send() checks if there's been an update in the priority / + * Check if there's been an update in the priority / * dependency settings and if so it submits a PRIORITY frame with the updated * info. + * Flush any out data pending in the network buffer. */ -static int h2_session_send(struct Curl_easy *data, - nghttp2_session *h2) -{ - struct HTTP *stream = data->req.p.http; - struct http_conn *httpc = &data->conn->proto.httpc; - set_transfer(httpc, data); - if((data->set.stream_weight != data->state.stream_weight) || - (data->set.stream_depends_e != data->state.stream_depends_e) || - (data->set.stream_depends_on != data->state.stream_depends_on) ) { +static CURLcode h2_progress_egress(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); + int rv = 0; + + if(stream && stream->id > 0 && + ((sweight_wanted(data) != sweight_in_effect(data)) || + (data->set.priority.exclusive != data->state.priority.exclusive) || + (data->set.priority.parent != data->state.priority.parent)) ) { /* send new weight and/or dependency */ nghttp2_priority_spec pri_spec; - int rv; h2_pri_spec(data, &pri_spec); - - H2BUGF(infof(data, "Queuing PRIORITY on stream %u (easy %p)", - stream->stream_id, data)); - DEBUGASSERT(stream->stream_id != -1); - rv = nghttp2_submit_priority(h2, NGHTTP2_FLAG_NONE, stream->stream_id, - &pri_spec); + CURL_TRC_CF(data, cf, "[%d] Queuing PRIORITY", stream->id); + DEBUGASSERT(stream->id != -1); + rv = nghttp2_submit_priority(ctx->h2, NGHTTP2_FLAG_NONE, + stream->id, &pri_spec); if(rv) - return rv; + goto out; } - return nghttp2_session_send(h2); + ctx->nw_out_blocked = 0; + while(!rv && !ctx->nw_out_blocked && nghttp2_session_want_write(ctx->h2)) + rv = nghttp2_session_send(ctx->h2); + +out: + if(nghttp2_is_fatal(rv)) { + CURL_TRC_CF(data, cf, "nghttp2_session_send error (%s)%d", + nghttp2_strerror(rv), rv); + return CURLE_SEND_ERROR; + } + return nw_out_flush(cf, data); } -static ssize_t http2_recv(struct Curl_easy *data, int sockindex, - char *mem, size_t len, CURLcode *err) +static ssize_t stream_recv(struct Curl_cfilter *cf, struct Curl_easy *data, + struct stream_ctx *stream, + char *buf, size_t len, CURLcode *err) { - ssize_t nread; - struct connectdata *conn = data->conn; - struct http_conn *httpc = &conn->proto.httpc; - struct HTTP *stream = data->req.p.http; - - (void)sockindex; /* we always do HTTP2 on sockindex 0 */ + struct cf_h2_ctx *ctx = cf->ctx; + ssize_t nread = -1; - if(should_close_session(httpc)) { - H2BUGF(infof(data, - "http2_recv: nothing to do in this session")); - if(conn->bits.close) { - /* already marked for closure, return OK and we're done */ - *err = CURLE_OK; - return 0; + *err = CURLE_AGAIN; + if(!Curl_bufq_is_empty(&stream->recvbuf)) { + nread = Curl_bufq_read(&stream->recvbuf, + (unsigned char *)buf, len, err); + if(nread < 0) + goto out; + DEBUGASSERT(nread > 0); + } + + if(nread < 0) { + if(stream->closed) { + CURL_TRC_CF(data, cf, "[%d] returning CLOSE", stream->id); + nread = http2_handle_stream_close(cf, data, stream, err); + } + else if(stream->reset || + (ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) || + (ctx->goaway && ctx->last_stream_id < stream->id)) { + CURL_TRC_CF(data, cf, "[%d] returning ERR", stream->id); + *err = stream->bodystarted? CURLE_PARTIAL_FILE : CURLE_RECV_ERROR; + nread = -1; } - *err = CURLE_HTTP2; - return -1; } + else if(nread == 0) { + *err = CURLE_AGAIN; + nread = -1; + } + +out: + if(nread < 0 && *err != CURLE_AGAIN) + CURL_TRC_CF(data, cf, "[%d] stream_recv(len=%zu) -> %zd, %d", + stream->id, len, nread, *err); + return nread; +} + +static CURLcode h2_progress_ingress(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream; + CURLcode result = CURLE_OK; + ssize_t nread; - /* Nullify here because we call nghttp2_session_send() and they - might refer to the old buffer. */ - stream->upload_mem = NULL; - stream->upload_len = 0; - - /* - * At this point 'stream' is just in the Curl_easy the connection - * identifies as its owner at this time. - */ - - if(stream->bodystarted && - stream->nread_header_recvbuf < Curl_dyn_len(&stream->header_recvbuf)) { - /* If there is header data pending for this stream to return, do that */ - size_t left = - Curl_dyn_len(&stream->header_recvbuf) - stream->nread_header_recvbuf; - size_t ncopy = CURLMIN(len, left); - memcpy(mem, Curl_dyn_ptr(&stream->header_recvbuf) + - stream->nread_header_recvbuf, ncopy); - stream->nread_header_recvbuf += ncopy; - - H2BUGF(infof(data, "http2_recv: Got %d bytes from header_recvbuf", - (int)ncopy)); - return ncopy; - } - - H2BUGF(infof(data, "http2_recv: easy %p (stream %u) win %u/%u", - data, stream->stream_id, - nghttp2_session_get_local_window_size(httpc->h2), - nghttp2_session_get_stream_local_window_size(httpc->h2, - stream->stream_id) - )); - - if((data->state.drain) && stream->memlen) { - H2BUGF(infof(data, "http2_recv: DRAIN %zu bytes stream %u (%p => %p)", - stream->memlen, stream->stream_id, - stream->mem, mem)); - if(mem != stream->mem) { - /* if we didn't get the same buffer this time, we must move the data to - the beginning */ - memmove(mem, stream->mem, stream->memlen); - stream->len = len - stream->memlen; - stream->mem = mem; - } - if(httpc->pause_stream_id == stream->stream_id && !stream->pausedata) { - /* We have paused nghttp2, but we have no pause data (see - on_data_chunk_recv). */ - httpc->pause_stream_id = 0; - if(h2_process_pending_input(data, httpc, err) != 0) { - return -1; + /* Process network input buffer fist */ + if(!Curl_bufq_is_empty(&ctx->inbufq)) { + CURL_TRC_CF(data, cf, "Process %zu bytes in connection buffer", + Curl_bufq_len(&ctx->inbufq)); + if(h2_process_pending_input(cf, data, &result) < 0) + return result; + } + + /* Receive data from the "lower" filters, e.g. network until + * it is time to stop due to connection close or us not processing + * all network input */ + while(!ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) { + stream = H2_STREAM_CTX(data); + if(stream && (stream->closed || Curl_bufq_is_full(&stream->recvbuf))) { + /* We would like to abort here and stop processing, so that + * the transfer loop can handle the data/close here. However, + * this may leave data in underlying buffers that will not + * be consumed. */ + if(!cf->next || !cf->next->cft->has_data_pending(cf->next, data)) + break; + } + + nread = Curl_bufq_slurp(&ctx->inbufq, nw_in_reader, cf, &result); + if(nread < 0) { + if(result != CURLE_AGAIN) { + failf(data, "Failed receiving HTTP2 data: %d(%s)", result, + curl_easy_strerror(result)); + return result; } + break; } + else if(nread == 0) { + CURL_TRC_CF(data, cf, "[0] ingress: connection closed"); + ctx->conn_closed = TRUE; + break; + } + else { + CURL_TRC_CF(data, cf, "[0] ingress: read %zd bytes", + nread); + } + + if(h2_process_pending_input(cf, data, &result)) + return result; } - else if(stream->pausedata) { - DEBUGASSERT(httpc->pause_stream_id == stream->stream_id); - nread = CURLMIN(len, stream->pauselen); - memcpy(mem, stream->pausedata, nread); - stream->pausedata += nread; - stream->pauselen -= nread; + if(ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) { + connclose(cf->conn, "GOAWAY received"); + } - if(stream->pauselen == 0) { - H2BUGF(infof(data, "Unpaused by stream %u", stream->stream_id)); - DEBUGASSERT(httpc->pause_stream_id == stream->stream_id); - httpc->pause_stream_id = 0; + return CURLE_OK; +} - stream->pausedata = NULL; - stream->pauselen = 0; +static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, + char *buf, size_t len, CURLcode *err) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); + ssize_t nread = -1; + CURLcode result; + struct cf_call_data save; - /* When NGHTTP2_ERR_PAUSE is returned from - data_source_read_callback, we might not process DATA frame - fully. Calling nghttp2_session_mem_recv() again will - continue to process DATA frame, but if there is no incoming - frames, then we have to call it again with 0-length data. - Without this, on_stream_close callback will not be called, - and stream could be hanged. */ - if(h2_process_pending_input(data, httpc, err) != 0) { - return -1; - } - } - H2BUGF(infof(data, "http2_recv: returns unpaused %zd bytes on stream %u", - nread, stream->stream_id)); - return nread; - } - else if(httpc->pause_stream_id) { - /* If a stream paused nghttp2_session_mem_recv previously, and has - not processed all data, it still refers to the buffer in - nghttp2_session. If we call nghttp2_session_mem_recv(), we may - overwrite that buffer. To avoid that situation, just return - here with CURLE_AGAIN. This could be busy loop since data in - socket is not read. But it seems that usually streams are - notified with its drain property, and socket is read again - quickly. */ - if(stream->closed) - /* closed overrides paused */ - return 0; - H2BUGF(infof(data, "stream %u is paused, pause id: %u", - stream->stream_id, httpc->pause_stream_id)); - *err = CURLE_AGAIN; + if(!stream) { + /* Abnormal call sequence: either this transfer has never opened a stream + * (unlikely) or the transfer has been done, cleaned up its resources, but + * a read() is called anyway. It is not clear what the calling sequence + * is for such a case. */ + failf(data, "[%zd-%zd], http/2 recv on a transfer never opened " + "or already cleared", (ssize_t)data->id, + (ssize_t)cf->conn->connection_id); + *err = CURLE_HTTP2; return -1; } - else { - /* remember where to store incoming data for this stream and how big the - buffer is */ - stream->mem = mem; - stream->len = len; - stream->memlen = 0; - - if(httpc->inbuflen == 0) { - nread = ((Curl_recv *)httpc->recv_underlying)( - data, FIRSTSOCKET, httpc->inbuf, H2_BUFSIZE, err); - - if(nread == -1) { - if(*err != CURLE_AGAIN) - failf(data, "Failed receiving HTTP2 data"); - else if(stream->closed) - /* received when the stream was already closed! */ - return http2_handle_stream_close(conn, data, stream, err); - - return -1; - } - 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); - *err = CURLE_HTTP2_STREAM; - return -1; - } + CF_DATA_SAVE(save, cf, data); - H2BUGF(infof(data, "end of stream")); - *err = CURLE_OK; - return 0; - } + nread = stream_recv(cf, data, stream, buf, len, err); + if(nread < 0 && *err != CURLE_AGAIN) + goto out; - H2BUGF(infof(data, "nread=%zd", nread)); + if(nread < 0) { + *err = h2_progress_ingress(cf, data); + if(*err) + goto out; - httpc->inbuflen = nread; + nread = stream_recv(cf, data, stream, buf, len, err); + } - DEBUGASSERT(httpc->nread_inbuf == 0); + if(nread > 0) { + size_t data_consumed = (size_t)nread; + /* Now that we transferred this to the upper layer, we report + * the actual amount of DATA consumed to the H2 session, so + * that it adjusts stream flow control */ + if(stream->resp_hds_len >= data_consumed) { + stream->resp_hds_len -= data_consumed; /* no DATA */ } else { - nread = httpc->inbuflen - httpc->nread_inbuf; - (void)nread; /* silence warning, used in debug */ - H2BUGF(infof(data, "Use data left in connection buffer, nread=%zd", - nread)); + if(stream->resp_hds_len) { + data_consumed -= stream->resp_hds_len; + stream->resp_hds_len = 0; + } + if(data_consumed) { + nghttp2_session_consume(ctx->h2, stream->id, data_consumed); + } } - if(h2_process_pending_input(data, httpc, err)) - return -1; - } - if(stream->memlen) { - ssize_t retlen = stream->memlen; - H2BUGF(infof(data, "http2_recv: returns %zd for stream %u", - retlen, stream->stream_id)); - stream->memlen = 0; - - if(httpc->pause_stream_id == stream->stream_id) { - /* data for this stream is returned now, but this stream caused a pause - already so we need it called again asap */ - H2BUGF(infof(data, "Data returned for PAUSED stream %u", - stream->stream_id)); - } - else if(!stream->closed) { - drained_transfer(data, httpc); + if(stream->closed) { + CURL_TRC_CF(data, cf, "[%d] DRAIN closed stream", stream->id); + drain_stream(cf, data, stream); } - else - /* this stream is closed, trigger a another read ASAP to detect that */ - Curl_expire(data, 0, EXPIRE_RUN_NOW); + } - return retlen; +out: + result = h2_progress_egress(cf, data); + if(result == CURLE_AGAIN) { + /* pending data to send, need to be called again. Ideally, we'd + * monitor the socket for POLLOUT, but we might not be in SENDING + * transfer state any longer and are unable to make this happen. + */ + drain_stream(cf, data, stream); } - if(stream->closed) - return http2_handle_stream_close(conn, data, stream, err); - *err = CURLE_AGAIN; - H2BUGF(infof(data, "http2_recv returns AGAIN for stream %u", - stream->stream_id)); - return -1; + else if(result) { + *err = result; + nread = -1; + } + CURL_TRC_CF(data, cf, "[%d] cf_recv(len=%zu) -> %zd %d, " + "buffered=%zu, window=%d/%d, connection %d/%d", + stream->id, len, nread, *err, + Curl_bufq_len(&stream->recvbuf), + nghttp2_session_get_stream_effective_recv_data_length( + ctx->h2, stream->id), + nghttp2_session_get_stream_effective_local_window_size( + ctx->h2, stream->id), + nghttp2_session_get_local_window_size(ctx->h2), + HTTP2_HUGE_WINDOW_SIZE); + + CF_DATA_RESTORE(cf, save); + return nread; } -static ssize_t http2_send(struct Curl_easy *data, int sockindex, - const void *mem, size_t len, CURLcode *err) +static ssize_t h2_submit(struct stream_ctx **pstream, + struct Curl_cfilter *cf, struct Curl_easy *data, + const void *buf, size_t len, CURLcode *err) { - /* - * Currently, we send request in this function, but this function is also - * used to send request body. It would be nice to add dedicated function for - * request. - */ - int rv; - struct connectdata *conn = data->conn; - struct http_conn *httpc = &conn->proto.httpc; - struct HTTP *stream = data->req.p.http; + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = NULL; + struct dynhds h2_headers; nghttp2_nv *nva = NULL; - size_t nheader; + const void *body = NULL; + size_t nheader, bodylen, i; nghttp2_data_provider data_prd; int32_t stream_id; - nghttp2_session *h2 = httpc->h2; nghttp2_priority_spec pri_spec; - CURLcode result; - struct h2h3req *hreq; - - (void)sockindex; + ssize_t nwritten; - H2BUGF(infof(data, "http2_send len=%zu", len)); + Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST); - if(stream->stream_id != -1) { - if(stream->close_handled) { - infof(data, "stream %u closed", stream->stream_id); - *err = CURLE_HTTP2_STREAM; - return -1; - } - else if(stream->closed) { - return http2_handle_stream_close(conn, data, stream, err); - } - /* If stream_id != -1, we have dispatched request HEADERS, and now - are going to send or sending request body in DATA frame */ - stream->upload_mem = mem; - stream->upload_len = len; - rv = nghttp2_session_resume_data(h2, stream->stream_id); - if(nghttp2_is_fatal(rv)) { - *err = CURLE_SEND_ERROR; - return -1; - } - rv = h2_session_send(data, h2); - if(nghttp2_is_fatal(rv)) { - *err = CURLE_SEND_ERROR; - return -1; - } - len -= stream->upload_len; - - /* Nullify here because we call nghttp2_session_send() and they - might refer to the old buffer. */ - stream->upload_mem = NULL; - stream->upload_len = 0; - - if(should_close_session(httpc)) { - H2BUGF(infof(data, "http2_send: nothing to do in this session")); - *err = CURLE_HTTP2; - return -1; - } - - if(stream->upload_left) { - /* we are sure that we have more data to send here. Calling the - following API will make nghttp2_session_want_write() return - nonzero if remote window allows it, which then libcurl checks - socket is writable or not. See http2_perform_getsock(). */ - nghttp2_session_resume_data(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(httpc->h2), - nghttp2_session_get_stream_remote_window_size(httpc->h2, - stream->stream_id) - ); + *err = http2_data_setup(cf, data, &stream); + if(*err) { + nwritten = -1; + goto out; + } - } - infof(data, "http2_send returns %zu for stream %u", len, - stream->stream_id); -#endif - return len; + nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, 0, err); + if(nwritten < 0) + goto out; + if(!stream->h1.done) { + /* need more data */ + goto out; } + DEBUGASSERT(stream->h1.req); - result = Curl_pseudo_headers(data, mem, len, &hreq); - if(result) { - *err = result; - return -1; + *err = Curl_http_req_to_h2(&h2_headers, stream->h1.req, data); + if(*err) { + nwritten = -1; + goto out; } - nheader = hreq->entries; + /* no longer needed */ + Curl_h1_req_parse_free(&stream->h1); - nva = malloc(sizeof(nghttp2_nv) * nheader); + nva = Curl_dynhds_to_nva(&h2_headers, &nheader); if(!nva) { - Curl_pseudo_free(hreq); *err = CURLE_OUT_OF_MEMORY; - return -1; - } - else { - unsigned int i; - for(i = 0; i < nheader; i++) { - nva[i].name = (unsigned char *)hreq->header[i].name; - nva[i].namelen = hreq->header[i].namelen; - nva[i].value = (unsigned char *)hreq->header[i].value; - nva[i].valuelen = hreq->header[i].valuelen; - nva[i].flags = NGHTTP2_NV_FLAG_NONE; - } - Curl_pseudo_free(hreq); + nwritten = -1; + goto out; } h2_pri_spec(data, &pri_spec); - - H2BUGF(infof(data, "http2_send request allowed %d (easy handle %p)", - nghttp2_session_check_request_allowed(h2), (void *)data)); + if(!nghttp2_session_check_request_allowed(ctx->h2)) + CURL_TRC_CF(data, cf, "send request NOT allowed (via nghttp2)"); switch(data->state.httpreq) { case HTTPREQ_POST: @@ -1981,220 +2085,367 @@ static ssize_t http2_send(struct Curl_easy *data, int sockindex, stream->upload_left = data->state.infilesize; else /* data sending without specifying the data amount up front */ - stream->upload_left = -1; /* unknown, but not zero */ + stream->upload_left = -1; /* unknown */ - data_prd.read_callback = data_source_read_callback; + data_prd.read_callback = req_body_read_callback; data_prd.source.ptr = NULL; - stream_id = nghttp2_submit_request(h2, &pri_spec, nva, nheader, + stream_id = nghttp2_submit_request(ctx->h2, &pri_spec, nva, nheader, &data_prd, data); break; default: - stream_id = nghttp2_submit_request(h2, &pri_spec, nva, nheader, + stream->upload_left = 0; /* no request body */ + stream_id = nghttp2_submit_request(ctx->h2, &pri_spec, nva, nheader, NULL, data); } - Curl_safefree(nva); - if(stream_id < 0) { - H2BUGF(infof(data, - "http2_send() nghttp2_submit_request error (%s)%u", - nghttp2_strerror(stream_id), stream_id)); + CURL_TRC_CF(data, cf, "send: nghttp2_submit_request error (%s)%u", + nghttp2_strerror(stream_id), stream_id); *err = CURLE_SEND_ERROR; - return -1; + nwritten = -1; + goto out; } - infof(data, "Using Stream ID: %u (easy handle %p)", - stream_id, (void *)data); - stream->stream_id = stream_id; +#define MAX_ACC 60000 /* <64KB to account for some overhead */ + if(Curl_trc_is_verbose(data)) { + size_t acc = 0; - rv = h2_session_send(data, h2); - if(rv) { - H2BUGF(infof(data, - "http2_send() nghttp2_session_send error (%s)%d", - nghttp2_strerror(rv), rv)); + infof(data, "[HTTP/2] [%d] OPENED stream for %s", + stream_id, data->state.url); + for(i = 0; i < nheader; ++i) { + acc += nva[i].namelen + nva[i].valuelen; - *err = CURLE_SEND_ERROR; - return -1; + infof(data, "[HTTP/2] [%d] [%.*s: %.*s]", stream_id, + (int)nva[i].namelen, nva[i].name, + (int)nva[i].valuelen, nva[i].value); + } + + if(acc > MAX_ACC) { + infof(data, "[HTTP/2] Warning: The cumulative length of all " + "headers exceeds %d bytes and that could cause the " + "stream to be rejected.", MAX_ACC); + } } - if(should_close_session(httpc)) { - H2BUGF(infof(data, "http2_send: nothing to do in this session")); - *err = CURLE_HTTP2; - return -1; + stream->id = stream_id; + stream->local_window_size = H2_STREAM_WINDOW_SIZE; + if(data->set.max_recv_speed) { + /* We are asked to only receive `max_recv_speed` bytes per second. + * Let's limit our stream window size around that, otherwise the server + * will send in large bursts only. We make the window 50% larger to + * allow for data in flight and avoid stalling. */ + curl_off_t n = (((data->set.max_recv_speed - 1) / H2_CHUNK_SIZE) + 1); + n += CURLMAX((n/2), 1); + if(n < (H2_STREAM_WINDOW_SIZE / H2_CHUNK_SIZE) && + n < (UINT_MAX / H2_CHUNK_SIZE)) { + stream->local_window_size = (uint32_t)n * H2_CHUNK_SIZE; + } } - /* If whole HEADERS frame was sent off to the underlying socket, the nghttp2 - library calls data_source_read_callback. But only it found that no data - available, so it deferred the DATA transmission. Which means that - nghttp2_session_want_write() returns 0 on http2_perform_getsock(), which - results that no writable socket check is performed. To workaround this, - we issue nghttp2_session_resume_data() here to bring back DATA - transmission from deferred state. */ - nghttp2_session_resume_data(h2, stream->stream_id); + body = (const char *)buf + nwritten; + bodylen = len - nwritten; - return len; + if(bodylen) { + /* We have request body to send in DATA frame */ + ssize_t n = Curl_bufq_write(&stream->sendbuf, body, bodylen, err); + if(n < 0) { + *err = CURLE_SEND_ERROR; + nwritten = -1; + goto out; + } + nwritten += n; + } + +out: + CURL_TRC_CF(data, cf, "[%d] submit -> %zd, %d", + stream? stream->id : -1, nwritten, *err); + Curl_safefree(nva); + *pstream = stream; + Curl_dynhds_free(&h2_headers); + return nwritten; } -CURLcode Curl_http2_setup(struct Curl_easy *data, - struct connectdata *conn) +static ssize_t cf_h2_send(struct Curl_cfilter *cf, struct Curl_easy *data, + const void *buf, size_t len, CURLcode *err) { + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); + struct cf_call_data save; + int rv; + ssize_t nwritten; CURLcode result; - struct http_conn *httpc = &conn->proto.httpc; - struct HTTP *stream = data->req.p.http; - - DEBUGASSERT(data->state.buffer); - - stream->stream_id = -1; - - Curl_dyn_init(&stream->header_recvbuf, DYN_H2_HEADERS); - Curl_dyn_init(&stream->trailer_recvbuf, DYN_H2_TRAILERS); - - stream->upload_left = 0; - stream->upload_mem = NULL; - stream->upload_len = 0; - stream->mem = data->state.buffer; - stream->len = data->set.buffer_size; - - multi_connchanged(data->multi); - /* below this point only connection related inits are done, which only needs - to be done once per connection */ - - if((conn->handler == &Curl_handler_http2_ssl) || - (conn->handler == &Curl_handler_http2)) - return CURLE_OK; /* already done */ - - if(conn->handler->flags & PROTOPT_SSL) - conn->handler = &Curl_handler_http2_ssl; - else - conn->handler = &Curl_handler_http2; + int blocked = 0, was_blocked = 0; + + CF_DATA_SAVE(save, cf, data); + + if(stream && stream->id != -1) { + if(stream->upload_blocked_len) { + /* the data in `buf` has already been submitted or added to the + * buffers, but have been EAGAINed on the last invocation. */ + /* TODO: this assertion triggers in OSSFuzz runs and it is not + * clear why. Disable for now to let OSSFuzz continue its tests. */ + DEBUGASSERT(len >= stream->upload_blocked_len); + if(len < stream->upload_blocked_len) { + /* Did we get called again with a smaller `len`? This should not + * happen. We are not prepared to handle that. */ + failf(data, "HTTP/2 send again with decreased length (%zd vs %zd)", + len, stream->upload_blocked_len); + *err = CURLE_HTTP2; + nwritten = -1; + goto out; + } + nwritten = (ssize_t)stream->upload_blocked_len; + stream->upload_blocked_len = 0; + was_blocked = 1; + } + else if(stream->closed) { + if(stream->resp_hds_complete) { + /* Server decided to close the stream after having sent us a findl + * response. This is valid if it is not interested in the request + * body. This happens on 30x or 40x responses. + * We silently discard the data sent, since this is not a transport + * error situation. */ + CURL_TRC_CF(data, cf, "[%d] discarding data" + "on closed stream with response", stream->id); + *err = CURLE_OK; + nwritten = (ssize_t)len; + goto out; + } + infof(data, "stream %u closed", stream->id); + *err = CURLE_SEND_ERROR; + nwritten = -1; + goto out; + } + else { + /* If stream_id != -1, we have dispatched request HEADERS and + * optionally request body, and now are going to send or sending + * more request body in DATA frame */ + nwritten = Curl_bufq_write(&stream->sendbuf, buf, len, err); + if(nwritten < 0 && *err != CURLE_AGAIN) + goto out; + } - result = http2_init(data, conn); - if(result) { - Curl_dyn_free(&stream->header_recvbuf); - return result; + if(!Curl_bufq_is_empty(&stream->sendbuf)) { + /* req body data is buffered, resume the potentially suspended stream */ + rv = nghttp2_session_resume_data(ctx->h2, stream->id); + if(nghttp2_is_fatal(rv)) { + *err = CURLE_SEND_ERROR; + nwritten = -1; + goto out; + } + } + } + else { + nwritten = h2_submit(&stream, cf, data, buf, len, err); + if(nwritten < 0) { + goto out; + } + DEBUGASSERT(stream); } - infof(data, "Using HTTP2, server supports multiplexing"); - - conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ - conn->httpversion = 20; - conn->bundle->multiuse = BUNDLE_MULTIPLEX; - - httpc->inbuflen = 0; - httpc->nread_inbuf = 0; - - httpc->pause_stream_id = 0; - httpc->drain_total = 0; + /* Call the nghttp2 send loop and flush to write ALL buffered data, + * headers and/or request body completely out to the network */ + result = h2_progress_egress(cf, data); + /* if the stream has been closed in egress handling (nghttp2 does that + * when it does not like the headers, for example */ + if(stream && stream->closed && !was_blocked) { + infof(data, "stream %u closed", stream->id); + *err = CURLE_SEND_ERROR; + nwritten = -1; + goto out; + } + else if(result == CURLE_AGAIN) { + blocked = 1; + } + else if(result) { + *err = result; + nwritten = -1; + goto out; + } + else if(stream && !Curl_bufq_is_empty(&stream->sendbuf)) { + /* although we wrote everything that nghttp2 wants to send now, + * there is data left in our stream send buffer unwritten. This may + * be due to the stream's HTTP/2 flow window being exhausted. */ + blocked = 1; + } + + if(stream && blocked && nwritten > 0) { + /* Unable to send all data, due to connection blocked or H2 window + * exhaustion. Data is left in our stream buffer, or nghttp2's internal + * frame buffer or our network out buffer. */ + size_t rwin = nghttp2_session_get_stream_remote_window_size(ctx->h2, + stream->id); + /* Whatever the cause, we need to return CURL_EAGAIN for this call. + * We have unwritten state that needs us being invoked again and EAGAIN + * is the only way to ensure that. */ + stream->upload_blocked_len = nwritten; + CURL_TRC_CF(data, cf, "[%d] cf_send(len=%zu) BLOCK: win %u/%zu " + "blocked_len=%zu", + stream->id, len, + nghttp2_session_get_remote_window_size(ctx->h2), rwin, + nwritten); + *err = CURLE_AGAIN; + nwritten = -1; + goto out; + } + else if(should_close_session(ctx)) { + /* nghttp2 thinks this session is done. If the stream has not been + * closed, this is an error state for out transfer */ + if(stream->closed) { + nwritten = http2_handle_stream_close(cf, data, stream, err); + } + else { + CURL_TRC_CF(data, cf, "send: nothing to do in this session"); + *err = CURLE_HTTP2; + nwritten = -1; + } + } - return CURLE_OK; +out: + if(stream) { + CURL_TRC_CF(data, cf, "[%d] cf_send(len=%zu) -> %zd, %d, " + "upload_left=%" CURL_FORMAT_CURL_OFF_T ", " + "h2 windows %d-%d (stream-conn), " + "buffers %zu-%zu (stream-conn)", + stream->id, len, nwritten, *err, + stream->upload_left, + nghttp2_session_get_stream_remote_window_size( + ctx->h2, stream->id), + nghttp2_session_get_remote_window_size(ctx->h2), + Curl_bufq_len(&stream->sendbuf), + Curl_bufq_len(&ctx->outbufq)); + } + else { + CURL_TRC_CF(data, cf, "cf_send(len=%zu) -> %zd, %d, " + "connection-window=%d, nw_send_buffer(%zu)", + len, nwritten, *err, + nghttp2_session_get_remote_window_size(ctx->h2), + Curl_bufq_len(&ctx->outbufq)); + } + CF_DATA_RESTORE(cf, save); + return nwritten; } -CURLcode Curl_http2_switched(struct Curl_easy *data, - const char *mem, size_t nread) +static void cf_h2_adjust_pollset(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct easy_pollset *ps) { - CURLcode result; - struct connectdata *conn = data->conn; - struct http_conn *httpc = &conn->proto.httpc; - int rv; - struct HTTP *stream = data->req.p.http; - - result = Curl_http2_setup(data, conn); - if(result) - return result; - - httpc->recv_underlying = conn->recv[FIRSTSOCKET]; - httpc->send_underlying = conn->send[FIRSTSOCKET]; - conn->recv[FIRSTSOCKET] = http2_recv; - conn->send[FIRSTSOCKET] = http2_send; + struct cf_h2_ctx *ctx = cf->ctx; + bool want_recv = CURL_WANT_RECV(data); + bool want_send = CURL_WANT_SEND(data); + + if(ctx->h2 && (want_recv || want_send)) { + struct stream_ctx *stream = H2_STREAM_CTX(data); + curl_socket_t sock = Curl_conn_cf_get_socket(cf, data); + struct cf_call_data save; + bool c_exhaust, s_exhaust; + + CF_DATA_SAVE(save, cf, data); + c_exhaust = !nghttp2_session_get_remote_window_size(ctx->h2); + s_exhaust = stream && stream->id >= 0 && + !nghttp2_session_get_stream_remote_window_size(ctx->h2, + stream->id); + want_recv = (want_recv || c_exhaust || s_exhaust); + want_send = (!s_exhaust && want_send) || + (!c_exhaust && nghttp2_session_want_write(ctx->h2)); + + Curl_pollset_set(data, ps, sock, want_recv, want_send); + CF_DATA_RESTORE(cf, save); + } +} - if(data->req.upgr101 == UPGR101_RECEIVED) { - /* stream 1 is opened implicitly on upgrade */ - stream->stream_id = 1; - /* queue SETTINGS frame (again) */ - rv = nghttp2_session_upgrade2(httpc->h2, httpc->binsettings, httpc->binlen, - data->state.httpreq == HTTPREQ_HEAD, NULL); - if(rv) { - failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", - nghttp2_strerror(rv), rv); - return CURLE_HTTP2; - } +static CURLcode cf_h2_connect(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool blocking, bool *done) +{ + struct cf_h2_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + struct cf_call_data save; - rv = nghttp2_session_set_stream_user_data(httpc->h2, - stream->stream_id, - data); - if(rv) { - infof(data, "http/2: failed to set user_data for stream %u", - stream->stream_id); - DEBUGASSERT(0); - } + if(cf->connected) { + *done = TRUE; + return CURLE_OK; } - else { - populate_settings(data, httpc); - /* stream ID is unknown at this point */ - stream->stream_id = -1; - rv = nghttp2_submit_settings(httpc->h2, NGHTTP2_FLAG_NONE, - httpc->local_settings, - httpc->local_settings_num); - if(rv) { - failf(data, "nghttp2_submit_settings() failed: %s(%d)", - nghttp2_strerror(rv), rv); - return CURLE_HTTP2; - } + /* Connect the lower filters first */ + if(!cf->next->connected) { + result = Curl_conn_cf_connect(cf->next, data, blocking, done); + if(result || !*done) + return result; } - rv = nghttp2_session_set_local_window_size(httpc->h2, NGHTTP2_FLAG_NONE, 0, - HTTP2_HUGE_WINDOW_SIZE); - if(rv) { - failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", - nghttp2_strerror(rv), rv); - return CURLE_HTTP2; - } + *done = FALSE; - /* we are going to copy mem to httpc->inbuf. This is required since - mem is part of buffer pointed by stream->mem, and callbacks - called by nghttp2_session_mem_recv() will write stream specific - data into stream->mem, overwriting data already there. */ - if(H2_BUFSIZE < nread) { - failf(data, "connection buffer size is too small to store data following " - "HTTP Upgrade response header: buflen=%d, datalen=%zu", - H2_BUFSIZE, nread); - return CURLE_HTTP2; + CF_DATA_SAVE(save, cf, data); + if(!ctx->h2) { + result = cf_h2_ctx_init(cf, data, FALSE); + if(result) + goto out; } - infof(data, "Copying HTTP/2 data in stream buffer to connection buffer" - " after upgrade: len=%zu", - nread); + result = h2_progress_ingress(cf, data); + if(result) + goto out; + + /* Send out our SETTINGS and ACKs and such. If that blocks, we + * have it buffered and can count this filter as being connected */ + result = h2_progress_egress(cf, data); + if(result == CURLE_AGAIN) + result = CURLE_OK; + else if(result) + goto out; + + *done = TRUE; + cf->connected = TRUE; + result = CURLE_OK; + +out: + CURL_TRC_CF(data, cf, "cf_connect() -> %d, %d, ", result, *done); + CF_DATA_RESTORE(cf, save); + return result; +} - if(nread) - memcpy(httpc->inbuf, mem, nread); +static void cf_h2_close(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; - httpc->inbuflen = nread; + if(ctx) { + struct cf_call_data save; - DEBUGASSERT(httpc->nread_inbuf == 0); + CF_DATA_SAVE(save, cf, data); + cf_h2_ctx_clear(ctx); + CF_DATA_RESTORE(cf, save); + } + if(cf->next) + cf->next->cft->do_close(cf->next, data); +} - if(-1 == h2_process_pending_input(data, httpc, &result)) - return CURLE_HTTP2; +static void cf_h2_destroy(struct Curl_cfilter *cf, struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; - return CURLE_OK; + (void)data; + if(ctx) { + cf_h2_ctx_free(ctx); + cf->ctx = NULL; + } } -CURLcode Curl_http2_stream_pause(struct Curl_easy *data, bool pause) +static CURLcode http2_data_pause(struct Curl_cfilter *cf, + struct Curl_easy *data, + bool pause) { - DEBUGASSERT(data); - DEBUGASSERT(data->conn); - /* if it isn't HTTP/2, we're done */ - if(!(data->conn->handler->protocol & PROTO_FAMILY_HTTP) || - !data->conn->proto.httpc.h2) - return CURLE_OK; #ifdef NGHTTP2_HAS_SET_LOCAL_WINDOW_SIZE - else { - struct HTTP *stream = data->req.p.http; - struct http_conn *httpc = &data->conn->proto.httpc; - uint32_t window = !pause * HTTP2_HUGE_WINDOW_SIZE; - int rv = nghttp2_session_set_local_window_size(httpc->h2, + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); + + DEBUGASSERT(data); + if(ctx && ctx->h2 && stream) { + uint32_t window = pause? 0 : stream->local_window_size; + + int rv = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, - stream->stream_id, + stream->id, window); if(rv) { failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", @@ -2202,22 +2453,32 @@ CURLcode Curl_http2_stream_pause(struct Curl_easy *data, bool pause) return CURLE_HTTP2; } - /* make sure the window update gets sent */ - rv = h2_session_send(data, httpc->h2); - if(rv) - return CURLE_SEND_ERROR; + if(!pause) + drain_stream(cf, data, stream); + + /* attempt to send the window update */ + (void)h2_progress_egress(cf, data); + if(!pause) { + /* Unpausing a h2 transfer, requires it to be run again. The server + * may send new DATA on us increasing the flow window, and it may + * not. We may have already buffered and exhausted the new window + * by operating on things in flight during the handling of other + * transfers. */ + drain_stream(cf, data, stream); + Curl_expire(data, 0, EXPIRE_RUN_NOW); + } DEBUGF(infof(data, "Set HTTP/2 window size to %u for stream %u", - window, stream->stream_id)); + window, stream->id)); #ifdef DEBUGBUILD { /* read out the stream local window again */ uint32_t window2 = - nghttp2_session_get_stream_local_window_size(httpc->h2, - stream->stream_id); + nghttp2_session_get_stream_local_window_size(ctx->h2, + stream->id); DEBUGF(infof(data, "HTTP/2 window size is now %u for stream %u", - window2, stream->stream_id)); + window2, stream->id)); } #endif } @@ -2225,94 +2486,341 @@ CURLcode Curl_http2_stream_pause(struct Curl_easy *data, bool pause) return CURLE_OK; } -CURLcode Curl_http2_add_child(struct Curl_easy *parent, - struct Curl_easy *child, - bool exclusive) +static CURLcode cf_h2_cntrl(struct Curl_cfilter *cf, + struct Curl_easy *data, + int event, int arg1, void *arg2) { - if(parent) { - struct Curl_http2_dep **tail; - struct Curl_http2_dep *dep = calloc(1, sizeof(struct Curl_http2_dep)); - if(!dep) - return CURLE_OUT_OF_MEMORY; - dep->data = child; + CURLcode result = CURLE_OK; + struct cf_call_data save; - if(parent->set.stream_dependents && exclusive) { - struct Curl_http2_dep *node = parent->set.stream_dependents; - while(node) { - node->data->set.stream_depends_on = child; - node = node->next; - } + (void)arg2; - tail = &child->set.stream_dependents; - while(*tail) - tail = &(*tail)->next; + CF_DATA_SAVE(save, cf, data); + switch(event) { + case CF_CTRL_DATA_SETUP: + break; + case CF_CTRL_DATA_PAUSE: + result = http2_data_pause(cf, data, (arg1 != 0)); + break; + case CF_CTRL_DATA_DONE_SEND: + result = http2_data_done_send(cf, data); + break; + case CF_CTRL_DATA_DETACH: + http2_data_done(cf, data, TRUE); + break; + case CF_CTRL_DATA_DONE: + http2_data_done(cf, data, arg1 != 0); + break; + default: + break; + } + CF_DATA_RESTORE(cf, save); + return result; +} - DEBUGASSERT(!*tail); - *tail = parent->set.stream_dependents; - parent->set.stream_dependents = 0; - } +static bool cf_h2_data_pending(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct stream_ctx *stream = H2_STREAM_CTX(data); - tail = &parent->set.stream_dependents; - while(*tail) { - (*tail)->data->set.stream_depends_e = FALSE; - tail = &(*tail)->next; - } + if(ctx && (!Curl_bufq_is_empty(&ctx->inbufq) + || (stream && !Curl_bufq_is_empty(&stream->sendbuf)) + || (stream && !Curl_bufq_is_empty(&stream->recvbuf)))) + return TRUE; + return cf->next? cf->next->cft->has_data_pending(cf->next, data) : FALSE; +} + +static bool cf_h2_is_alive(struct Curl_cfilter *cf, + 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_connisalive(cf, data, input_pending)); + CURL_TRC_CF(data, cf, "conn alive -> %d, input_pending=%d", + result, *input_pending); + CF_DATA_RESTORE(cf, save); + return result; +} + +static CURLcode cf_h2_keep_alive(struct Curl_cfilter *cf, + struct Curl_easy *data) +{ + CURLcode result; + struct cf_call_data save; + + CF_DATA_SAVE(save, cf, data); + result = http2_send_ping(cf, data); + CF_DATA_RESTORE(cf, save); + return result; +} - DEBUGASSERT(!*tail); - *tail = dep; +static CURLcode cf_h2_query(struct Curl_cfilter *cf, + struct Curl_easy *data, + int query, int *pres1, void *pres2) +{ + struct cf_h2_ctx *ctx = cf->ctx; + struct cf_call_data save; + size_t effective_max; + + switch(query) { + case CF_QUERY_MAX_CONCURRENT: + DEBUGASSERT(pres1); + + CF_DATA_SAVE(save, cf, data); + if(nghttp2_session_check_request_allowed(ctx->h2) == 0) { + /* the limit is what we have in use right now */ + effective_max = CONN_INUSE(cf->conn); + } + else { + effective_max = ctx->max_concurrent_streams; + } + *pres1 = (effective_max > INT_MAX)? INT_MAX : (int)effective_max; + CF_DATA_RESTORE(cf, save); + return CURLE_OK; + default: + break; } + return cf->next? + cf->next->cft->query(cf->next, data, query, pres1, pres2) : + CURLE_UNKNOWN_OPTION; +} - child->set.stream_depends_on = parent; - child->set.stream_depends_e = exclusive; - return CURLE_OK; +struct Curl_cftype Curl_cft_nghttp2 = { + "HTTP/2", + CF_TYPE_MULTIPLEX, + CURL_LOG_LVL_NONE, + cf_h2_destroy, + cf_h2_connect, + cf_h2_close, + Curl_cf_def_get_host, + cf_h2_adjust_pollset, + cf_h2_data_pending, + cf_h2_send, + cf_h2_recv, + cf_h2_cntrl, + cf_h2_is_alive, + cf_h2_keep_alive, + cf_h2_query, +}; + +static CURLcode http2_cfilter_add(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn, + int sockindex) +{ + struct Curl_cfilter *cf = NULL; + struct cf_h2_ctx *ctx; + CURLcode result = CURLE_OUT_OF_MEMORY; + + DEBUGASSERT(data->conn); + ctx = calloc(1, sizeof(*ctx)); + if(!ctx) + goto out; + + result = Curl_cf_create(&cf, &Curl_cft_nghttp2, ctx); + if(result) + goto out; + + Curl_conn_cf_add(data, conn, sockindex, cf); + result = CURLE_OK; + +out: + if(result) + cf_h2_ctx_free(ctx); + *pcf = result? NULL : cf; + return result; } -void Curl_http2_remove_child(struct Curl_easy *parent, struct Curl_easy *child) +static CURLcode http2_cfilter_insert_after(struct Curl_cfilter *cf, + struct Curl_easy *data) { - struct Curl_http2_dep *last = 0; - struct Curl_http2_dep *data = parent->set.stream_dependents; - DEBUGASSERT(child->set.stream_depends_on == parent); + struct Curl_cfilter *cf_h2 = NULL; + struct cf_h2_ctx *ctx; + CURLcode result = CURLE_OUT_OF_MEMORY; + + (void)data; + ctx = calloc(1, sizeof(*ctx)); + if(!ctx) + goto out; + + result = Curl_cf_create(&cf_h2, &Curl_cft_nghttp2, ctx); + if(result) + goto out; + + Curl_conn_cf_insert_after(cf, cf_h2); + result = CURLE_OK; + +out: + if(result) + cf_h2_ctx_free(ctx); + return result; +} - while(data && data->data != child) { - last = data; - data = data->next; +static bool Curl_cf_is_http2(struct Curl_cfilter *cf, + const struct Curl_easy *data) +{ + (void)data; + for(; cf; cf = cf->next) { + if(cf->cft == &Curl_cft_nghttp2) + return TRUE; + if(cf->cft->flags & CF_TYPE_IP_CONNECT) + return FALSE; } + return FALSE; +} - DEBUGASSERT(data); +bool Curl_conn_is_http2(const struct Curl_easy *data, + const struct connectdata *conn, + int sockindex) +{ + return conn? Curl_cf_is_http2(conn->cfilter[sockindex], data) : FALSE; +} - if(data) { - if(last) { - last->next = data->next; - } - else { - parent->set.stream_dependents = data->next; +bool Curl_http2_may_switch(struct Curl_easy *data, + struct connectdata *conn, + int sockindex) +{ + (void)sockindex; + 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 + whether or not this setting should apply to HTTP/2 proxies. */ + infof(data, "Ignoring HTTP/2 prior knowledge due to proxy"); + return FALSE; } - free(data); +#endif + return TRUE; } + return FALSE; +} + +CURLcode Curl_http2_switch(struct Curl_easy *data, + struct connectdata *conn, int sockindex) +{ + struct Curl_cfilter *cf; + CURLcode result; + + DEBUGASSERT(!Curl_conn_is_http2(data, conn, sockindex)); + DEBUGF(infof(data, "switching to HTTP/2")); + + result = http2_cfilter_add(&cf, data, conn, sockindex); + if(result) + return result; + + result = cf_h2_ctx_init(cf, data, FALSE); + if(result) + return result; - child->set.stream_depends_on = 0; - child->set.stream_depends_e = FALSE; + conn->httpversion = 20; /* we know we're on HTTP/2 now */ + conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ + conn->bundle->multiuse = BUNDLE_MULTIPLEX; + Curl_multi_connchanged(data->multi); + + if(cf->next) { + bool done; + return Curl_conn_cf_connect(cf, data, FALSE, &done); + } + return CURLE_OK; } -void Curl_http2_cleanup_dependencies(struct Curl_easy *data) +CURLcode Curl_http2_switch_at(struct Curl_cfilter *cf, struct Curl_easy *data) { - while(data->set.stream_dependents) { - struct Curl_easy *tmp = data->set.stream_dependents->data; - Curl_http2_remove_child(data, tmp); - if(data->set.stream_depends_on) - Curl_http2_add_child(data->set.stream_depends_on, tmp, FALSE); + struct Curl_cfilter *cf_h2; + CURLcode result; + + DEBUGASSERT(!Curl_cf_is_http2(cf, data)); + + result = http2_cfilter_insert_after(cf, data); + if(result) + return result; + + cf_h2 = cf->next; + result = cf_h2_ctx_init(cf_h2, data, FALSE); + if(result) + return result; + + cf->conn->httpversion = 20; /* we know we're on HTTP/2 now */ + cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ + cf->conn->bundle->multiuse = BUNDLE_MULTIPLEX; + Curl_multi_connchanged(data->multi); + + if(cf_h2->next) { + bool done; + return Curl_conn_cf_connect(cf_h2, data, FALSE, &done); + } + return CURLE_OK; +} + +CURLcode Curl_http2_upgrade(struct Curl_easy *data, + struct connectdata *conn, int sockindex, + const char *mem, size_t nread) +{ + struct Curl_cfilter *cf; + struct cf_h2_ctx *ctx; + CURLcode result; + + DEBUGASSERT(!Curl_conn_is_http2(data, conn, sockindex)); + DEBUGF(infof(data, "upgrading to HTTP/2")); + DEBUGASSERT(data->req.upgr101 == UPGR101_RECEIVED); + + result = http2_cfilter_add(&cf, data, conn, sockindex); + if(result) + return result; + + DEBUGASSERT(cf->cft == &Curl_cft_nghttp2); + ctx = cf->ctx; + + result = cf_h2_ctx_init(cf, data, TRUE); + if(result) + return result; + + if(nread > 0) { + /* Remaining data from the protocol switch reply is already using + * the switched protocol, ie. HTTP/2. We add that to the network + * inbufq. */ + ssize_t copied; + + copied = Curl_bufq_write(&ctx->inbufq, + (const unsigned char *)mem, nread, &result); + if(copied < 0) { + failf(data, "error on copying HTTP Upgrade response: %d", result); + return CURLE_RECV_ERROR; + } + if((size_t)copied < nread) { + failf(data, "connection buffer size could not take all data " + "from HTTP Upgrade response header: copied=%zd, datalen=%zu", + copied, nread); + return CURLE_HTTP2; + } + infof(data, "Copied HTTP/2 data in stream buffer to connection buffer" + " after upgrade: len=%zu", nread); } - if(data->set.stream_depends_on) - Curl_http2_remove_child(data->set.stream_depends_on, data); + conn->httpversion = 20; /* we know we're on HTTP/2 now */ + conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ + conn->bundle->multiuse = BUNDLE_MULTIPLEX; + Curl_multi_connchanged(data->multi); + + if(cf->next) { + bool done; + return Curl_conn_cf_connect(cf, data, FALSE, &done); + } + return CURLE_OK; } -/* Only call this function for a transfer that already got a HTTP/2 +/* Only call this function for a transfer that already got an HTTP/2 CURLE_HTTP2_STREAM error! */ bool Curl_h2_http_1_1_error(struct Curl_easy *data) { - struct HTTP *stream = data->req.p.http; - return (stream->error == NGHTTP2_HTTP_1_1_REQUIRED); + struct stream_ctx *stream = H2_STREAM_CTX(data); + return (stream && stream->error == NGHTTP2_HTTP_1_1_REQUIRED); } #else /* !USE_NGHTTP2 */ |