| /*************************************************************************** |
| * _ _ ____ _ |
| * Project ___| | | | _ \| | |
| * / __| | | | |_) | | |
| * | (__| |_| | _ <| |___ |
| * \___|\___/|_| \_\_____| |
| * |
| * 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 |
| * are also available at https://curl.se/docs/copyright.html. |
| * |
| * You may opt to use, copy, modify, merge, publish, distribute and/or sell |
| * copies of the Software, and permit persons to whom the Software is |
| * furnished to do so, under the terms of the COPYING file. |
| * |
| * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
| * KIND, either express or implied. |
| * |
| * SPDX-License-Identifier: curl |
| * |
| ***************************************************************************/ |
| |
| #include "curl_setup.h" |
| |
| #if defined(USE_NGHTTP2) && !defined(CURL_DISABLE_PROXY) |
| |
| #include <nghttp2/nghttp2.h> |
| #include "urldata.h" |
| #include "cfilters.h" |
| #include "connect.h" |
| #include "curl_trc.h" |
| #include "bufq.h" |
| #include "dynbuf.h" |
| #include "dynhds.h" |
| #include "http1.h" |
| #include "http2.h" |
| #include "http_proxy.h" |
| #include "multiif.h" |
| #include "sendf.h" |
| #include "cf-h2-proxy.h" |
| |
| /* The last 3 #include files should be in this order */ |
| #include "curl_printf.h" |
| #include "curl_memory.h" |
| #include "memdebug.h" |
| |
| #define PROXY_H2_CHUNK_SIZE (16*1024) |
| |
| #define PROXY_HTTP2_HUGE_WINDOW_SIZE (100 * 1024 * 1024) |
| #define H2_TUNNEL_WINDOW_SIZE (10 * 1024 * 1024) |
| |
| #define PROXY_H2_NW_RECV_CHUNKS (H2_TUNNEL_WINDOW_SIZE / PROXY_H2_CHUNK_SIZE) |
| #define PROXY_H2_NW_SEND_CHUNKS 1 |
| |
| #define H2_TUNNEL_RECV_CHUNKS (H2_TUNNEL_WINDOW_SIZE / PROXY_H2_CHUNK_SIZE) |
| #define H2_TUNNEL_SEND_CHUNKS ((128 * 1024) / PROXY_H2_CHUNK_SIZE) |
| |
| |
| typedef enum { |
| H2_TUNNEL_INIT, /* init/default/no tunnel state */ |
| H2_TUNNEL_CONNECT, /* CONNECT request is being send */ |
| H2_TUNNEL_RESPONSE, /* CONNECT response received completely */ |
| H2_TUNNEL_ESTABLISHED, |
| H2_TUNNEL_FAILED |
| } h2_tunnel_state; |
| |
| struct tunnel_stream { |
| struct http_resp *resp; |
| struct bufq recvbuf; |
| struct bufq sendbuf; |
| char *authority; |
| int32_t stream_id; |
| uint32_t error; |
| size_t upload_blocked_len; |
| h2_tunnel_state state; |
| BIT(has_final_response); |
| BIT(closed); |
| BIT(reset); |
| }; |
| |
| static CURLcode tunnel_stream_init(struct Curl_cfilter *cf, |
| struct tunnel_stream *ts) |
| { |
| const char *hostname; |
| int port; |
| bool ipv6_ip; |
| CURLcode result; |
| |
| ts->state = H2_TUNNEL_INIT; |
| ts->stream_id = -1; |
| Curl_bufq_init2(&ts->recvbuf, PROXY_H2_CHUNK_SIZE, H2_TUNNEL_RECV_CHUNKS, |
| BUFQ_OPT_SOFT_LIMIT); |
| Curl_bufq_init(&ts->sendbuf, PROXY_H2_CHUNK_SIZE, H2_TUNNEL_SEND_CHUNKS); |
| |
| result = Curl_http_proxy_get_destination(cf, &hostname, &port, &ipv6_ip); |
| if(result) |
| return result; |
| |
| ts->authority = /* host:port with IPv6 support */ |
| aprintf("%s%s%s:%d", ipv6_ip?"[":"", hostname, ipv6_ip?"]":"", port); |
| if(!ts->authority) |
| return CURLE_OUT_OF_MEMORY; |
| |
| return CURLE_OK; |
| } |
| |
| static void tunnel_stream_clear(struct tunnel_stream *ts) |
| { |
| Curl_http_resp_free(ts->resp); |
| Curl_bufq_free(&ts->recvbuf); |
| Curl_bufq_free(&ts->sendbuf); |
| Curl_safefree(ts->authority); |
| memset(ts, 0, sizeof(*ts)); |
| ts->state = H2_TUNNEL_INIT; |
| } |
| |
| static void h2_tunnel_go_state(struct Curl_cfilter *cf, |
| struct tunnel_stream *ts, |
| h2_tunnel_state new_state, |
| struct Curl_easy *data) |
| { |
| (void)cf; |
| |
| if(ts->state == new_state) |
| return; |
| /* leaving this one */ |
| switch(ts->state) { |
| case H2_TUNNEL_CONNECT: |
| data->req.ignorebody = FALSE; |
| break; |
| default: |
| break; |
| } |
| /* entering this one */ |
| switch(new_state) { |
| case H2_TUNNEL_INIT: |
| CURL_TRC_CF(data, cf, "[%d] new tunnel state 'init'", ts->stream_id); |
| tunnel_stream_clear(ts); |
| break; |
| |
| case H2_TUNNEL_CONNECT: |
| CURL_TRC_CF(data, cf, "[%d] new tunnel state 'connect'", ts->stream_id); |
| ts->state = H2_TUNNEL_CONNECT; |
| break; |
| |
| case H2_TUNNEL_RESPONSE: |
| CURL_TRC_CF(data, cf, "[%d] new tunnel state 'response'", ts->stream_id); |
| ts->state = H2_TUNNEL_RESPONSE; |
| break; |
| |
| case H2_TUNNEL_ESTABLISHED: |
| CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'", |
| ts->stream_id); |
| infof(data, "CONNECT phase completed"); |
| data->state.authproxy.done = TRUE; |
| data->state.authproxy.multipass = FALSE; |
| FALLTHROUGH(); |
| case H2_TUNNEL_FAILED: |
| if(new_state == H2_TUNNEL_FAILED) |
| CURL_TRC_CF(data, cf, "[%d] new tunnel state 'failed'", ts->stream_id); |
| ts->state = new_state; |
| /* If a proxy-authorization header was used for the proxy, then we should |
| make sure that it isn't accidentally used for the document request |
| after we've connected. So let's free and clear it here. */ |
| Curl_safefree(data->state.aptr.proxyuserpwd); |
| break; |
| } |
| } |
| |
| struct cf_h2_proxy_ctx { |
| nghttp2_session *h2; |
| /* The easy handle used in the current filter call, cleared at return */ |
| struct cf_call_data call_data; |
| |
| struct bufq inbufq; /* network receive buffer */ |
| struct bufq outbufq; /* network send buffer */ |
| |
| struct tunnel_stream tunnel; /* our tunnel CONNECT stream */ |
| int32_t goaway_error; |
| int32_t last_stream_id; |
| BIT(conn_closed); |
| BIT(goaway); |
| 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_proxy_ctx *)(cf)->ctx)->call_data |
| |
| static void cf_h2_proxy_ctx_clear(struct cf_h2_proxy_ctx *ctx) |
| { |
| struct cf_call_data save = ctx->call_data; |
| |
| if(ctx->h2) { |
| nghttp2_session_del(ctx->h2); |
| } |
| Curl_bufq_free(&ctx->inbufq); |
| Curl_bufq_free(&ctx->outbufq); |
| tunnel_stream_clear(&ctx->tunnel); |
| memset(ctx, 0, sizeof(*ctx)); |
| ctx->call_data = save; |
| } |
| |
| static void cf_h2_proxy_ctx_free(struct cf_h2_proxy_ctx *ctx) |
| { |
| if(ctx) { |
| cf_h2_proxy_ctx_clear(ctx); |
| free(ctx); |
| } |
| } |
| |
| static void drain_tunnel(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| struct tunnel_stream *tunnel) |
| { |
| unsigned char bits; |
| |
| (void)cf; |
| bits = CURL_CSELECT_IN; |
| if(!tunnel->closed && !tunnel->reset && tunnel->upload_blocked_len) |
| bits |= CURL_CSELECT_OUT; |
| if(data->state.select_bits != bits) { |
| CURL_TRC_CF(data, cf, "[%d] DRAIN select_bits=%x", |
| tunnel->stream_id, bits); |
| data->state.select_bits = bits; |
| Curl_expire(data, 0, EXPIRE_RUN_NOW); |
| } |
| } |
| |
| static ssize_t proxy_nw_in_reader(void *reader_ctx, |
| unsigned char *buf, size_t buflen, |
| CURLcode *err) |
| { |
| struct Curl_cfilter *cf = reader_ctx; |
| ssize_t nread; |
| |
| if(cf) { |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| nread = Curl_conn_cf_recv(cf->next, data, (char *)buf, buflen, err); |
| CURL_TRC_CF(data, cf, "[0] nw_in_reader(len=%zu) -> %zd, %d", |
| buflen, nread, *err); |
| } |
| else { |
| nread = 0; |
| } |
| return nread; |
| } |
| |
| static ssize_t proxy_h2_nw_out_writer(void *writer_ctx, |
| const unsigned char *buf, size_t buflen, |
| CURLcode *err) |
| { |
| struct Curl_cfilter *cf = writer_ctx; |
| ssize_t nwritten; |
| |
| if(cf) { |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| nwritten = Curl_conn_cf_send(cf->next, data, (const char *)buf, buflen, |
| err); |
| CURL_TRC_CF(data, cf, "[0] nw_out_writer(len=%zu) -> %zd, %d", |
| buflen, nwritten, *err); |
| } |
| else { |
| nwritten = 0; |
| } |
| return nwritten; |
| } |
| |
| static int proxy_h2_client_new(struct Curl_cfilter *cf, |
| nghttp2_session_callbacks *cbs) |
| { |
| struct cf_h2_proxy_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; |
| } |
| |
| static ssize_t on_session_send(nghttp2_session *h2, |
| const uint8_t *buf, size_t blen, |
| int flags, void *userp); |
| static int proxy_h2_on_frame_recv(nghttp2_session *session, |
| const nghttp2_frame *frame, |
| void *userp); |
| #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| static int proxy_h2_on_frame_send(nghttp2_session *session, |
| const nghttp2_frame *frame, |
| void *userp); |
| #endif |
| static int proxy_h2_on_stream_close(nghttp2_session *session, |
| int32_t stream_id, |
| uint32_t error_code, void *userp); |
| static int proxy_h2_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 tunnel_recv_callback(nghttp2_session *session, uint8_t flags, |
| int32_t stream_id, |
| const uint8_t *mem, size_t len, void *userp); |
| |
| /* |
| * Initialize the cfilter context |
| */ |
| static CURLcode cf_h2_proxy_ctx_init(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result = CURLE_OUT_OF_MEMORY; |
| nghttp2_session_callbacks *cbs = NULL; |
| int rc; |
| |
| DEBUGASSERT(!ctx->h2); |
| memset(&ctx->tunnel, 0, sizeof(ctx->tunnel)); |
| |
| Curl_bufq_init(&ctx->inbufq, PROXY_H2_CHUNK_SIZE, PROXY_H2_NW_RECV_CHUNKS); |
| Curl_bufq_init(&ctx->outbufq, PROXY_H2_CHUNK_SIZE, PROXY_H2_NW_SEND_CHUNKS); |
| |
| if(tunnel_stream_init(cf, &ctx->tunnel)) |
| goto out; |
| |
| rc = nghttp2_session_callbacks_new(&cbs); |
| if(rc) { |
| failf(data, "Couldn't initialize nghttp2 callbacks"); |
| goto out; |
| } |
| |
| nghttp2_session_callbacks_set_send_callback(cbs, on_session_send); |
| nghttp2_session_callbacks_set_on_frame_recv_callback( |
| cbs, proxy_h2_on_frame_recv); |
| #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| nghttp2_session_callbacks_set_on_frame_send_callback(cbs, |
| proxy_h2_on_frame_send); |
| #endif |
| nghttp2_session_callbacks_set_on_data_chunk_recv_callback( |
| cbs, tunnel_recv_callback); |
| nghttp2_session_callbacks_set_on_stream_close_callback( |
| cbs, proxy_h2_on_stream_close); |
| nghttp2_session_callbacks_set_on_header_callback(cbs, proxy_h2_on_header); |
| |
| /* The nghttp2 session is not yet setup, do it */ |
| rc = proxy_h2_client_new(cf, cbs); |
| if(rc) { |
| failf(data, "Couldn't initialize nghttp2"); |
| goto out; |
| } |
| |
| { |
| nghttp2_settings_entry iv[3]; |
| |
| 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 = H2_TUNNEL_WINDOW_SIZE; |
| iv[2].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; |
| iv[2].value = 0; |
| rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, iv, 3); |
| if(rc) { |
| failf(data, "nghttp2_submit_settings() failed: %s(%d)", |
| nghttp2_strerror(rc), rc); |
| result = CURLE_HTTP2; |
| goto out; |
| } |
| } |
| |
| rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0, |
| PROXY_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; |
| |
| out: |
| if(cbs) |
| nghttp2_session_callbacks_del(cbs); |
| CURL_TRC_CF(data, cf, "[0] init proxy ctx -> %d", result); |
| return result; |
| } |
| |
| static int proxy_h2_should_close_session(struct cf_h2_proxy_ctx *ctx) |
| { |
| return !nghttp2_session_want_read(ctx->h2) && |
| !nghttp2_session_want_write(ctx->h2); |
| } |
| |
| static CURLcode proxy_h2_nw_out_flush(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_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, proxy_h2_nw_out_writer, cf, |
| &result); |
| if(nwritten < 0) { |
| if(result == CURLE_AGAIN) { |
| CURL_TRC_CF(data, cf, "[0] flush nw send buffer(%zu) -> EAGAIN", |
| Curl_bufq_len(&ctx->outbufq)); |
| ctx->nw_out_blocked = 1; |
| } |
| return result; |
| } |
| CURL_TRC_CF(data, cf, "[0] nw send buffer flushed"); |
| return Curl_bufq_is_empty(&ctx->outbufq)? CURLE_OK: CURLE_AGAIN; |
| } |
| |
| /* |
| * 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 proxy_h2_process_pending_input(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| CURLcode *err) |
| { |
| struct cf_h2_proxy_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); |
| CURL_TRC_CF(data, cf, "[0] %zu bytes to nghttp2 -> %zd", blen, rv); |
| 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)) { |
| CURL_TRC_CF(data, cf, "[0] all data in connection buffer processed"); |
| break; |
| } |
| else { |
| CURL_TRC_CF(data, cf, "[0] process_pending_input: %zu bytes left " |
| "in connection buffer", Curl_bufq_len(&ctx->inbufq)); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static CURLcode proxy_h2_progress_ingress(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result = CURLE_OK; |
| ssize_t nread; |
| |
| /* Process network input buffer fist */ |
| if(!Curl_bufq_is_empty(&ctx->inbufq)) { |
| CURL_TRC_CF(data, cf, "[0] process %zu bytes in connection buffer", |
| Curl_bufq_len(&ctx->inbufq)); |
| if(proxy_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 or we have enough data for this stream */ |
| while(!ctx->conn_closed && /* not closed the connection */ |
| !ctx->tunnel.closed && /* nor the tunnel */ |
| Curl_bufq_is_empty(&ctx->inbufq) && /* and we consumed our input */ |
| !Curl_bufq_is_full(&ctx->tunnel.recvbuf)) { |
| |
| nread = Curl_bufq_slurp(&ctx->inbufq, proxy_nw_in_reader, cf, &result); |
| CURL_TRC_CF(data, cf, "[0] read %zu bytes nw data -> %zd, %d", |
| Curl_bufq_len(&ctx->inbufq), nread, result); |
| if(nread < 0) { |
| if(result != CURLE_AGAIN) { |
| failf(data, "Failed receiving HTTP2 data"); |
| return result; |
| } |
| break; |
| } |
| else if(nread == 0) { |
| ctx->conn_closed = TRUE; |
| break; |
| } |
| |
| if(proxy_h2_process_pending_input(cf, data, &result)) |
| return result; |
| } |
| |
| if(ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) { |
| connclose(cf->conn, "GOAWAY received"); |
| } |
| |
| return CURLE_OK; |
| } |
| |
| static CURLcode proxy_h2_progress_egress(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| int rv = 0; |
| |
| ctx->nw_out_blocked = 0; |
| while(!rv && !ctx->nw_out_blocked && nghttp2_session_want_write(ctx->h2)) |
| rv = nghttp2_session_send(ctx->h2); |
| |
| if(nghttp2_is_fatal(rv)) { |
| CURL_TRC_CF(data, cf, "[0] nghttp2_session_send error (%s)%d", |
| nghttp2_strerror(rv), rv); |
| return CURLE_SEND_ERROR; |
| } |
| return proxy_h2_nw_out_flush(cf, data); |
| } |
| |
| static ssize_t on_session_send(nghttp2_session *h2, |
| const uint8_t *buf, size_t blen, int flags, |
| void *userp) |
| { |
| struct Curl_cfilter *cf = userp; |
| struct cf_h2_proxy_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); |
| |
| nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen, |
| proxy_h2_nw_out_writer, cf, &result); |
| if(nwritten < 0) { |
| if(result == CURLE_AGAIN) { |
| return NGHTTP2_ERR_WOULDBLOCK; |
| } |
| failf(data, "Failed sending HTTP2 data"); |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| } |
| |
| if(!nwritten) |
| return NGHTTP2_ERR_WOULDBLOCK; |
| |
| return nwritten; |
| } |
| |
| #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| static int proxy_h2_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 proxy_h2_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 = proxy_h2_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 proxy_h2_on_frame_recv(nghttp2_session *session, |
| const nghttp2_frame *frame, |
| void *userp) |
| { |
| struct Curl_cfilter *cf = userp; |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| int32_t stream_id = frame->hd.stream_id; |
| |
| (void)session; |
| DEBUGASSERT(data); |
| #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| if(Curl_trc_cf_is_verbose(cf, data)) { |
| char buffer[256]; |
| int len; |
| len = proxy_h2_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: |
| /* 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)) { |
| drain_tunnel(cf, data, &ctx->tunnel); |
| } |
| break; |
| case NGHTTP2_GOAWAY: |
| ctx->goaway = TRUE; |
| break; |
| default: |
| break; |
| } |
| return 0; |
| } |
| |
| if(stream_id != ctx->tunnel.stream_id) { |
| CURL_TRC_CF(data, cf, "[%d] rcvd FRAME not for tunnel", stream_id); |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| } |
| |
| switch(frame->hd.type) { |
| case NGHTTP2_HEADERS: |
| /* nghttp2 guarantees that :status is received, and we store it to |
| stream->status_code. Fuzzing has proven this can still be reached |
| without status code having been set. */ |
| if(!ctx->tunnel.resp) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| /* Only final status code signals the end of header */ |
| CURL_TRC_CF(data, cf, "[%d] got http status: %d", |
| stream_id, ctx->tunnel.resp->status); |
| if(!ctx->tunnel.has_final_response) { |
| if(ctx->tunnel.resp->status / 100 != 1) { |
| ctx->tunnel.has_final_response = TRUE; |
| } |
| } |
| break; |
| case NGHTTP2_WINDOW_UPDATE: |
| if(CURL_WANT_SEND(data)) { |
| drain_tunnel(cf, data, &ctx->tunnel); |
| } |
| break; |
| default: |
| break; |
| } |
| return 0; |
| } |
| |
| static int proxy_h2_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) |
| { |
| struct Curl_cfilter *cf = userp; |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| int32_t stream_id = frame->hd.stream_id; |
| CURLcode result; |
| |
| (void)flags; |
| (void)data; |
| (void)session; |
| DEBUGASSERT(stream_id); /* should never be a zero stream ID here */ |
| if(stream_id != ctx->tunnel.stream_id) { |
| CURL_TRC_CF(data, cf, "[%d] header for non-tunnel stream: " |
| "%.*s: %.*s", stream_id, |
| (int)namelen, name, (int)valuelen, value); |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| } |
| |
| if(frame->hd.type == NGHTTP2_PUSH_PROMISE) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| |
| if(ctx->tunnel.has_final_response) { |
| /* we do not do anything with trailers for tunnel streams */ |
| return 0; |
| } |
| |
| if(namelen == sizeof(HTTP_PSEUDO_STATUS) - 1 && |
| memcmp(HTTP_PSEUDO_STATUS, name, namelen) == 0) { |
| int http_status; |
| struct http_resp *resp; |
| |
| /* status: always comes first, we might get more than one response, |
| * link the previous ones for keepers */ |
| result = Curl_http_decode_status(&http_status, |
| (const char *)value, valuelen); |
| if(result) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| result = Curl_http_resp_make(&resp, http_status, NULL); |
| if(result) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| resp->prev = ctx->tunnel.resp; |
| ctx->tunnel.resp = resp; |
| CURL_TRC_CF(data, cf, "[%d] status: HTTP/2 %03d", |
| stream_id, ctx->tunnel.resp->status); |
| return 0; |
| } |
| |
| if(!ctx->tunnel.resp) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| |
| result = Curl_dynhds_add(&ctx->tunnel.resp->headers, |
| (const char *)name, namelen, |
| (const char *)value, valuelen); |
| if(result) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| |
| CURL_TRC_CF(data, cf, "[%d] header: %.*s: %.*s", |
| stream_id, (int)namelen, name, (int)valuelen, value); |
| |
| return 0; /* 0 is successful */ |
| } |
| |
| static ssize_t tunnel_send_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 cf_h2_proxy_ctx *ctx = cf->ctx; |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| struct tunnel_stream *ts; |
| CURLcode result; |
| ssize_t nread; |
| |
| (void)source; |
| (void)data; |
| (void)ctx; |
| |
| if(!stream_id) |
| return NGHTTP2_ERR_INVALID_ARGUMENT; |
| |
| ts = nghttp2_session_get_stream_user_data(session, stream_id); |
| if(!ts) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| DEBUGASSERT(ts == &ctx->tunnel); |
| |
| nread = Curl_bufq_read(&ts->sendbuf, buf, length, &result); |
| if(nread < 0) { |
| if(result != CURLE_AGAIN) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| return NGHTTP2_ERR_DEFERRED; |
| } |
| if(ts->closed && Curl_bufq_is_empty(&ts->sendbuf)) |
| *data_flags = NGHTTP2_DATA_FLAG_EOF; |
| |
| CURL_TRC_CF(data, cf, "[%d] tunnel_send_callback -> %zd", |
| ts->stream_id, nread); |
| return nread; |
| } |
| |
| static int tunnel_recv_callback(nghttp2_session *session, uint8_t flags, |
| int32_t stream_id, |
| const uint8_t *mem, size_t len, void *userp) |
| { |
| struct Curl_cfilter *cf = userp; |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| ssize_t nwritten; |
| CURLcode result; |
| |
| (void)flags; |
| (void)session; |
| DEBUGASSERT(stream_id); /* should never be a zero stream ID here */ |
| |
| if(stream_id != ctx->tunnel.stream_id) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| |
| nwritten = Curl_bufq_write(&ctx->tunnel.recvbuf, mem, len, &result); |
| if(nwritten < 0) { |
| if(result != CURLE_AGAIN) |
| return NGHTTP2_ERR_CALLBACK_FAILURE; |
| nwritten = 0; |
| } |
| DEBUGASSERT((size_t)nwritten == len); |
| return 0; |
| } |
| |
| static int proxy_h2_on_stream_close(nghttp2_session *session, |
| int32_t stream_id, |
| uint32_t error_code, void *userp) |
| { |
| struct Curl_cfilter *cf = userp; |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| struct Curl_easy *data = CF_DATA_CURRENT(cf); |
| |
| (void)session; |
| (void)data; |
| |
| if(stream_id != ctx->tunnel.stream_id) |
| return 0; |
| |
| CURL_TRC_CF(data, cf, "[%d] proxy_h2_on_stream_close, %s (err %d)", |
| stream_id, nghttp2_http2_strerror(error_code), error_code); |
| ctx->tunnel.closed = TRUE; |
| ctx->tunnel.error = error_code; |
| |
| return 0; |
| } |
| |
| static CURLcode proxy_h2_submit(int32_t *pstream_id, |
| struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| nghttp2_session *h2, |
| struct httpreq *req, |
| const nghttp2_priority_spec *pri_spec, |
| void *stream_user_data, |
| nghttp2_data_source_read_callback read_callback, |
| void *read_ctx) |
| { |
| struct dynhds h2_headers; |
| nghttp2_nv *nva = NULL; |
| int32_t stream_id = -1; |
| size_t nheader; |
| CURLcode result; |
| |
| (void)cf; |
| Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST); |
| result = Curl_http_req_to_h2(&h2_headers, req, data); |
| if(result) |
| goto out; |
| |
| nva = Curl_dynhds_to_nva(&h2_headers, &nheader); |
| if(!nva) { |
| result = CURLE_OUT_OF_MEMORY; |
| goto out; |
| } |
| |
| if(read_callback) { |
| nghttp2_data_provider data_prd; |
| |
| data_prd.read_callback = read_callback; |
| data_prd.source.ptr = read_ctx; |
| stream_id = nghttp2_submit_request(h2, pri_spec, nva, nheader, |
| &data_prd, stream_user_data); |
| } |
| else { |
| stream_id = nghttp2_submit_request(h2, pri_spec, nva, nheader, |
| NULL, stream_user_data); |
| } |
| |
| if(stream_id < 0) { |
| failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", |
| nghttp2_strerror(stream_id), stream_id); |
| result = CURLE_SEND_ERROR; |
| goto out; |
| } |
| result = CURLE_OK; |
| |
| out: |
| free(nva); |
| Curl_dynhds_free(&h2_headers); |
| *pstream_id = stream_id; |
| return result; |
| } |
| |
| static CURLcode submit_CONNECT(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| struct tunnel_stream *ts) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result; |
| struct httpreq *req = NULL; |
| |
| result = Curl_http_proxy_create_CONNECT(&req, cf, data, 2); |
| if(result) |
| goto out; |
| result = Curl_creader_set_null(data); |
| if(result) |
| goto out; |
| |
| infof(data, "Establish HTTP/2 proxy tunnel to %s", req->authority); |
| |
| result = proxy_h2_submit(&ts->stream_id, cf, data, ctx->h2, req, |
| NULL, ts, tunnel_send_callback, cf); |
| if(result) { |
| CURL_TRC_CF(data, cf, "[%d] send, nghttp2_submit_request error: %s", |
| ts->stream_id, nghttp2_strerror(ts->stream_id)); |
| } |
| |
| out: |
| if(req) |
| Curl_http_req_free(req); |
| if(result) |
| failf(data, "Failed sending CONNECT to proxy"); |
| return result; |
| } |
| |
| static CURLcode inspect_response(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| struct tunnel_stream *ts) |
| { |
| CURLcode result = CURLE_OK; |
| struct dynhds_entry *auth_reply = NULL; |
| (void)cf; |
| |
| DEBUGASSERT(ts->resp); |
| if(ts->resp->status/100 == 2) { |
| infof(data, "CONNECT tunnel established, response %d", ts->resp->status); |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data); |
| return CURLE_OK; |
| } |
| |
| if(ts->resp->status == 401) { |
| auth_reply = Curl_dynhds_cget(&ts->resp->headers, "WWW-Authenticate"); |
| } |
| else if(ts->resp->status == 407) { |
| auth_reply = Curl_dynhds_cget(&ts->resp->headers, "Proxy-Authenticate"); |
| } |
| |
| if(auth_reply) { |
| CURL_TRC_CF(data, cf, "[0] CONNECT: fwd auth header '%s'", |
| auth_reply->value); |
| result = Curl_http_input_auth(data, ts->resp->status == 407, |
| auth_reply->value); |
| if(result) |
| return result; |
| if(data->req.newurl) { |
| /* Indicator that we should try again */ |
| Curl_safefree(data->req.newurl); |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data); |
| return CURLE_OK; |
| } |
| } |
| |
| /* Seems to have failed */ |
| return CURLE_RECV_ERROR; |
| } |
| |
| static CURLcode H2_CONNECT(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| struct tunnel_stream *ts) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result = CURLE_OK; |
| |
| DEBUGASSERT(ts); |
| DEBUGASSERT(ts->authority); |
| do { |
| switch(ts->state) { |
| case H2_TUNNEL_INIT: |
| /* Prepare the CONNECT request and make a first attempt to send. */ |
| CURL_TRC_CF(data, cf, "[0] CONNECT start for %s", ts->authority); |
| result = submit_CONNECT(cf, data, ts); |
| if(result) |
| goto out; |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_CONNECT, data); |
| FALLTHROUGH(); |
| |
| case H2_TUNNEL_CONNECT: |
| /* see that the request is completely sent */ |
| result = proxy_h2_progress_ingress(cf, data); |
| if(!result) |
| result = proxy_h2_progress_egress(cf, data); |
| if(result && result != CURLE_AGAIN) { |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data); |
| break; |
| } |
| |
| if(ts->has_final_response) { |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_RESPONSE, data); |
| } |
| else { |
| result = CURLE_OK; |
| goto out; |
| } |
| FALLTHROUGH(); |
| |
| case H2_TUNNEL_RESPONSE: |
| DEBUGASSERT(ts->has_final_response); |
| result = inspect_response(cf, data, ts); |
| if(result) |
| goto out; |
| break; |
| |
| case H2_TUNNEL_ESTABLISHED: |
| return CURLE_OK; |
| |
| case H2_TUNNEL_FAILED: |
| return CURLE_RECV_ERROR; |
| |
| default: |
| break; |
| } |
| |
| } while(ts->state == H2_TUNNEL_INIT); |
| |
| out: |
| if(result || ctx->tunnel.closed) |
| h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data); |
| return result; |
| } |
| |
| static CURLcode cf_h2_proxy_connect(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| bool blocking, bool *done) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result = CURLE_OK; |
| struct cf_call_data save; |
| timediff_t check; |
| struct tunnel_stream *ts = &ctx->tunnel; |
| |
| if(cf->connected) { |
| *done = TRUE; |
| return CURLE_OK; |
| } |
| |
| /* Connect the lower filters first */ |
| if(!cf->next->connected) { |
| result = Curl_conn_cf_connect(cf->next, data, blocking, done); |
| if(result || !*done) |
| return result; |
| } |
| |
| *done = FALSE; |
| |
| CF_DATA_SAVE(save, cf, data); |
| if(!ctx->h2) { |
| result = cf_h2_proxy_ctx_init(cf, data); |
| if(result) |
| goto out; |
| } |
| DEBUGASSERT(ts->authority); |
| |
| check = Curl_timeleft(data, NULL, TRUE); |
| if(check <= 0) { |
| failf(data, "Proxy CONNECT aborted due to timeout"); |
| result = CURLE_OPERATION_TIMEDOUT; |
| goto out; |
| } |
| |
| /* for the secondary socket (FTP), use the "connect to host" |
| * but ignore the "connect to port" (use the secondary port) |
| */ |
| result = H2_CONNECT(cf, data, ts); |
| |
| out: |
| *done = (result == CURLE_OK) && (ts->state == H2_TUNNEL_ESTABLISHED); |
| if(*done) { |
| cf->connected = TRUE; |
| /* The real request will follow the CONNECT, reset request partially */ |
| Curl_req_soft_reset(&data->req, data); |
| Curl_client_reset(data); |
| } |
| CF_DATA_RESTORE(cf, save); |
| return result; |
| } |
| |
| static void cf_h2_proxy_close(struct Curl_cfilter *cf, struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| |
| if(ctx) { |
| struct cf_call_data save; |
| |
| CF_DATA_SAVE(save, cf, data); |
| cf_h2_proxy_ctx_clear(ctx); |
| CF_DATA_RESTORE(cf, save); |
| } |
| if(cf->next) |
| cf->next->cft->do_close(cf->next, data); |
| } |
| |
| static void cf_h2_proxy_destroy(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| |
| (void)data; |
| if(ctx) { |
| cf_h2_proxy_ctx_free(ctx); |
| cf->ctx = NULL; |
| } |
| } |
| |
| static bool cf_h2_proxy_data_pending(struct Curl_cfilter *cf, |
| const struct Curl_easy *data) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| if((ctx && !Curl_bufq_is_empty(&ctx->inbufq)) || |
| (ctx && ctx->tunnel.state == H2_TUNNEL_ESTABLISHED && |
| !Curl_bufq_is_empty(&ctx->tunnel.recvbuf))) |
| return TRUE; |
| return cf->next? cf->next->cft->has_data_pending(cf->next, data) : FALSE; |
| } |
| |
| static void cf_h2_proxy_adjust_pollset(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| struct easy_pollset *ps) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| curl_socket_t sock = Curl_conn_cf_get_socket(cf, data); |
| bool want_recv, want_send; |
| |
| Curl_pollset_check(data, ps, sock, &want_recv, &want_send); |
| if(ctx->h2 && (want_recv || want_send)) { |
| 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 = ctx->tunnel.stream_id >= 0 && |
| !nghttp2_session_get_stream_remote_window_size( |
| ctx->h2, ctx->tunnel.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); |
| } |
| } |
| |
| static ssize_t h2_handle_tunnel_close(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| CURLcode *err) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| ssize_t rv = 0; |
| |
| if(ctx->tunnel.error == NGHTTP2_REFUSED_STREAM) { |
| CURL_TRC_CF(data, cf, "[%d] REFUSED_STREAM, try again on a new " |
| "connection", ctx->tunnel.stream_id); |
| connclose(cf->conn, "REFUSED_STREAM"); /* don't use this anymore */ |
| *err = CURLE_RECV_ERROR; /* trigger Curl_retry_request() later */ |
| return -1; |
| } |
| else if(ctx->tunnel.error != NGHTTP2_NO_ERROR) { |
| failf(data, "HTTP/2 stream %u was not closed cleanly: %s (err %u)", |
| ctx->tunnel.stream_id, nghttp2_http2_strerror(ctx->tunnel.error), |
| ctx->tunnel.error); |
| *err = CURLE_HTTP2_STREAM; |
| return -1; |
| } |
| else if(ctx->tunnel.reset) { |
| failf(data, "HTTP/2 stream %u was reset", ctx->tunnel.stream_id); |
| *err = CURLE_RECV_ERROR; |
| return -1; |
| } |
| |
| *err = CURLE_OK; |
| rv = 0; |
| CURL_TRC_CF(data, cf, "[%d] handle_tunnel_close -> %zd, %d", |
| ctx->tunnel.stream_id, rv, *err); |
| return rv; |
| } |
| |
| static ssize_t tunnel_recv(struct Curl_cfilter *cf, struct Curl_easy *data, |
| char *buf, size_t len, CURLcode *err) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| ssize_t nread = -1; |
| |
| *err = CURLE_AGAIN; |
| if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf)) { |
| nread = Curl_bufq_read(&ctx->tunnel.recvbuf, |
| (unsigned char *)buf, len, err); |
| if(nread < 0) |
| goto out; |
| DEBUGASSERT(nread > 0); |
| } |
| |
| if(nread < 0) { |
| if(ctx->tunnel.closed) { |
| nread = h2_handle_tunnel_close(cf, data, err); |
| } |
| else if(ctx->tunnel.reset || |
| (ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) || |
| (ctx->goaway && ctx->last_stream_id < ctx->tunnel.stream_id)) { |
| *err = CURLE_RECV_ERROR; |
| nread = -1; |
| } |
| } |
| else if(nread == 0) { |
| *err = CURLE_AGAIN; |
| nread = -1; |
| } |
| |
| out: |
| CURL_TRC_CF(data, cf, "[%d] tunnel_recv(len=%zu) -> %zd, %d", |
| ctx->tunnel.stream_id, len, nread, *err); |
| return nread; |
| } |
| |
| static ssize_t cf_h2_proxy_recv(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| char *buf, size_t len, CURLcode *err) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| ssize_t nread = -1; |
| struct cf_call_data save; |
| CURLcode result; |
| |
| if(ctx->tunnel.state != H2_TUNNEL_ESTABLISHED) { |
| *err = CURLE_RECV_ERROR; |
| return -1; |
| } |
| CF_DATA_SAVE(save, cf, data); |
| |
| if(Curl_bufq_is_empty(&ctx->tunnel.recvbuf)) { |
| *err = proxy_h2_progress_ingress(cf, data); |
| if(*err) |
| goto out; |
| } |
| |
| nread = tunnel_recv(cf, data, buf, len, err); |
| |
| if(nread > 0) { |
| CURL_TRC_CF(data, cf, "[%d] increase window by %zd", |
| ctx->tunnel.stream_id, nread); |
| nghttp2_session_consume(ctx->h2, ctx->tunnel.stream_id, (size_t)nread); |
| } |
| |
| result = proxy_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. |
| */ |
| CURL_TRC_CF(data, cf, "[%d] egress blocked, DRAIN", |
| ctx->tunnel.stream_id); |
| drain_tunnel(cf, data, &ctx->tunnel); |
| } |
| else if(result) { |
| *err = result; |
| nread = -1; |
| } |
| |
| out: |
| if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) && |
| (nread >= 0 || *err == CURLE_AGAIN)) { |
| /* data pending and no fatal error to report. Need to trigger |
| * draining to avoid stalling when no socket events happen. */ |
| drain_tunnel(cf, data, &ctx->tunnel); |
| } |
| CURL_TRC_CF(data, cf, "[%d] cf_recv(len=%zu) -> %zd %d", |
| ctx->tunnel.stream_id, len, nread, *err); |
| CF_DATA_RESTORE(cf, save); |
| return nread; |
| } |
| |
| static ssize_t cf_h2_proxy_send(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| const void *buf, size_t len, CURLcode *err) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| struct cf_call_data save; |
| int rv; |
| ssize_t nwritten; |
| CURLcode result; |
| int blocked = 0; |
| |
| if(ctx->tunnel.state != H2_TUNNEL_ESTABLISHED) { |
| *err = CURLE_SEND_ERROR; |
| return -1; |
| } |
| CF_DATA_SAVE(save, cf, data); |
| |
| if(ctx->tunnel.closed) { |
| nwritten = -1; |
| *err = CURLE_SEND_ERROR; |
| goto out; |
| } |
| else if(ctx->tunnel.upload_blocked_len) { |
| /* the data in `buf` has already been submitted or added to the |
| * buffers, but have been EAGAINed on the last invocation. */ |
| DEBUGASSERT(len >= ctx->tunnel.upload_blocked_len); |
| if(len < ctx->tunnel.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 proxy, send again with decreased length"); |
| *err = CURLE_HTTP2; |
| nwritten = -1; |
| goto out; |
| } |
| nwritten = (ssize_t)ctx->tunnel.upload_blocked_len; |
| ctx->tunnel.upload_blocked_len = 0; |
| *err = CURLE_OK; |
| } |
| else { |
| nwritten = Curl_bufq_write(&ctx->tunnel.sendbuf, buf, len, err); |
| if(nwritten < 0) { |
| if(*err != CURLE_AGAIN) |
| goto out; |
| nwritten = 0; |
| } |
| } |
| |
| if(!Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) { |
| /* req body data is buffered, resume the potentially suspended stream */ |
| rv = nghttp2_session_resume_data(ctx->h2, ctx->tunnel.stream_id); |
| if(nghttp2_is_fatal(rv)) { |
| *err = CURLE_SEND_ERROR; |
| nwritten = -1; |
| goto out; |
| } |
| } |
| |
| result = proxy_h2_progress_ingress(cf, data); |
| if(result) { |
| *err = result; |
| nwritten = -1; |
| goto out; |
| } |
| |
| /* Call the nghttp2 send loop and flush to write ALL buffered data, |
| * headers and/or request body completely out to the network */ |
| result = proxy_h2_progress_egress(cf, data); |
| if(result == CURLE_AGAIN) { |
| blocked = 1; |
| } |
| else if(result) { |
| *err = result; |
| nwritten = -1; |
| goto out; |
| } |
| else if(!Curl_bufq_is_empty(&ctx->tunnel.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(blocked) { |
| /* 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, ctx->tunnel.stream_id); |
| if(rwin == 0) { |
| /* H2 flow window exhaustion. |
| * FIXME: there is no way to HOLD all transfers that use this |
| * proxy connection AND to UNHOLD all of them again when the |
| * window increases. |
| * We *could* iterate over all data on this conn maybe? */ |
| CURL_TRC_CF(data, cf, "[%d] remote flow " |
| "window is exhausted", ctx->tunnel.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. */ |
| ctx->tunnel.upload_blocked_len = nwritten; |
| CURL_TRC_CF(data, cf, "[%d] cf_send(len=%zu) BLOCK: win %u/%zu " |
| "blocked_len=%zu", |
| ctx->tunnel.stream_id, len, |
| nghttp2_session_get_remote_window_size(ctx->h2), rwin, |
| nwritten); |
| drain_tunnel(cf, data, &ctx->tunnel); |
| *err = CURLE_AGAIN; |
| nwritten = -1; |
| goto out; |
| } |
| else if(proxy_h2_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(ctx->tunnel.closed) { |
| *err = CURLE_SEND_ERROR; |
| nwritten = -1; |
| } |
| else { |
| CURL_TRC_CF(data, cf, "[0] send: nothing to do in this session"); |
| *err = CURLE_HTTP2; |
| nwritten = -1; |
| } |
| } |
| |
| out: |
| if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) && |
| (nwritten >= 0 || *err == CURLE_AGAIN)) { |
| /* data pending and no fatal error to report. Need to trigger |
| * draining to avoid stalling when no socket events happen. */ |
| drain_tunnel(cf, data, &ctx->tunnel); |
| } |
| CURL_TRC_CF(data, cf, "[%d] cf_send(len=%zu) -> %zd, %d, " |
| "h2 windows %d-%d (stream-conn), buffers %zu-%zu (stream-conn)", |
| ctx->tunnel.stream_id, len, nwritten, *err, |
| nghttp2_session_get_stream_remote_window_size( |
| ctx->h2, ctx->tunnel.stream_id), |
| nghttp2_session_get_remote_window_size(ctx->h2), |
| Curl_bufq_len(&ctx->tunnel.sendbuf), |
| Curl_bufq_len(&ctx->outbufq)); |
| CF_DATA_RESTORE(cf, save); |
| return nwritten; |
| } |
| |
| static bool proxy_h2_connisalive(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| bool *input_pending) |
| { |
| struct cf_h2_proxy_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, proxy_nw_in_reader, cf, &result); |
| if(nread != -1) { |
| if(proxy_h2_process_pending_input(cf, data, &result) < 0) |
| /* immediate error, considered dead */ |
| alive = FALSE; |
| else { |
| alive = !proxy_h2_should_close_session(ctx); |
| } |
| } |
| else if(result != CURLE_AGAIN) { |
| /* the read failed so let's say this is dead anyway */ |
| alive = FALSE; |
| } |
| } |
| |
| return alive; |
| } |
| |
| static bool cf_h2_proxy_is_alive(struct Curl_cfilter *cf, |
| struct Curl_easy *data, |
| bool *input_pending) |
| { |
| struct cf_h2_proxy_ctx *ctx = cf->ctx; |
| CURLcode result; |
| struct cf_call_data save; |
| |
| CF_DATA_SAVE(save, cf, data); |
| result = (ctx && ctx->h2 && proxy_h2_connisalive(cf, data, input_pending)); |
| CURL_TRC_CF(data, cf, "[0] conn alive -> %d, input_pending=%d", |
| result, *input_pending); |
| CF_DATA_RESTORE(cf, save); |
| return result; |
| } |
| |
| struct Curl_cftype Curl_cft_h2_proxy = { |
| "H2-PROXY", |
| CF_TYPE_IP_CONNECT, |
| CURL_LOG_LVL_NONE, |
| cf_h2_proxy_destroy, |
| cf_h2_proxy_connect, |
| cf_h2_proxy_close, |
| Curl_cf_http_proxy_get_host, |
| cf_h2_proxy_adjust_pollset, |
| cf_h2_proxy_data_pending, |
| cf_h2_proxy_send, |
| cf_h2_proxy_recv, |
| Curl_cf_def_cntrl, |
| cf_h2_proxy_is_alive, |
| Curl_cf_def_conn_keep_alive, |
| Curl_cf_def_query, |
| }; |
| |
| CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf, |
| struct Curl_easy *data) |
| { |
| struct Curl_cfilter *cf_h2_proxy = NULL; |
| struct cf_h2_proxy_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_proxy, &Curl_cft_h2_proxy, ctx); |
| if(result) |
| goto out; |
| |
| Curl_conn_cf_insert_after(cf, cf_h2_proxy); |
| result = CURLE_OK; |
| |
| out: |
| if(result) |
| cf_h2_proxy_ctx_free(ctx); |
| return result; |
| } |
| |
| #endif /* defined(USE_NGHTTP2) && !defined(CURL_DISABLE_PROXY) */ |