/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) Daniel Stenberg, , 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" #ifndef CURL_DISABLE_HTTP #include "urldata.h" #include "curl_trc.h" #include "cfilters.h" #include "cf-dns.h" #include "connect.h" #include "hostip.h" #include "httpsrr.h" #include "multiif.h" #include "cf-https-connect.h" #include "http2.h" #include "progress.h" #include "select.h" #include "vquic/vquic.h" typedef enum { CF_HC_RESOLV, CF_HC_INIT, CF_HC_CONNECT, CF_HC_SUCCESS, CF_HC_FAILURE } cf_hc_state; struct cf_hc_baller { const char *name; struct Curl_cfilter *cf; CURLcode result; struct curltime started; int reply_ms; uint8_t transport; enum alpnid alpn_id; BIT(shutdown); }; static void cf_hc_baller_discard(struct cf_hc_baller *b, struct Curl_easy *data) { if(b->cf) { Curl_conn_cf_close(b->cf, data); Curl_conn_cf_discard_chain(&b->cf, data); b->cf = NULL; } } static bool cf_hc_baller_is_connecting(struct cf_hc_baller *b) { return b->cf && !b->result; } static bool cf_hc_baller_has_started(struct cf_hc_baller *b) { return !!b->cf; } static int cf_hc_baller_reply_ms(struct cf_hc_baller *b, struct Curl_easy *data) { if(b->cf && (b->reply_ms < 0)) b->cf->cft->query(b->cf, data, CF_QUERY_CONNECT_REPLY_MS, &b->reply_ms, NULL); return b->reply_ms; } static bool cf_hc_baller_data_pending(struct cf_hc_baller *b, const struct Curl_easy *data) { return b->cf && !b->result && b->cf->cft->has_data_pending(b->cf, data); } static bool cf_hc_baller_needs_flush(struct cf_hc_baller *b, struct Curl_easy *data) { return b->cf && !b->result && Curl_conn_cf_needs_flush(b->cf, data); } static CURLcode cf_hc_baller_cntrl(struct cf_hc_baller *b, struct Curl_easy *data, int event, int arg1, void *arg2) { if(b->cf && !b->result) return Curl_conn_cf_cntrl(b->cf, data, FALSE, event, arg1, arg2); return CURLE_OK; } struct cf_hc_ctx { cf_hc_state state; struct curltime started; /* when connect started */ CURLcode result; /* overall result */ CURLcode check_h3_result; struct cf_hc_baller ballers[2]; size_t baller_count; timediff_t soft_eyeballs_timeout_ms; timediff_t hard_eyeballs_timeout_ms; uint8_t def_transport; BIT(httpsrr_resolved); BIT(checked_h3); BIT(ballers_complete); }; static void cf_hc_ctx_close(struct Curl_easy *data, struct cf_hc_ctx *ctx) { if(ctx) { size_t i; for(i = 0; i < ctx->baller_count; ++i) cf_hc_baller_discard(&ctx->ballers[i], data); } } static void cf_hc_ctx_destroy(struct Curl_easy *data, struct cf_hc_ctx *ctx) { if(ctx) { cf_hc_ctx_close(data, ctx); curlx_free(ctx); } } static void cf_hc_baller_assign(struct cf_hc_baller *b, enum alpnid alpn_id, uint8_t def_transport) { b->alpn_id = alpn_id; b->transport = def_transport; b->cf = NULL; b->result = CURLE_OK; b->reply_ms = -1; b->shutdown = FALSE; switch(b->alpn_id) { case ALPN_h3: b->name = "h3"; b->transport = TRNSPRT_QUIC; break; case ALPN_h2: b->name = "h2"; break; case ALPN_h1: b->name = "h1"; break; case ALPN_none: b->name = "no-alpn"; break; default: b->result = CURLE_FAILED_INIT; break; } } static void cf_hc_baller_init(struct cf_hc_baller *b, struct Curl_cfilter *cf, struct Curl_easy *data) { struct Curl_cfilter *save = cf->next; cf->next = NULL; b->started = *Curl_pgrs_now(data); b->result = Curl_cf_setup_insert_after(cf, data, b->transport, CURL_CF_SSL_ENABLE); b->cf = cf->next; cf->next = save; } static CURLcode cf_hc_baller_connect(struct cf_hc_baller *b, struct Curl_cfilter *cf, struct Curl_easy *data, bool *done) { struct Curl_cfilter *save = cf->next; cf->next = b->cf; b->result = Curl_conn_cf_connect(cf->next, data, done); b->cf = cf->next; /* it might mutate */ cf->next = save; return b->result; } static CURLcode baller_connected(struct Curl_cfilter *cf, struct Curl_easy *data, struct cf_hc_baller *winner) { struct cf_hc_ctx *ctx = cf->ctx; /* Make the winner's connection filter out own sub-filter, check, move, * close all remaining. */ if(cf->next) { DEBUGASSERT(0); return CURLE_FAILED_INIT; } if(!winner->cf) { DEBUGASSERT(0); return CURLE_FAILED_INIT; } cf->next = winner->cf; winner->cf = NULL; ctx->state = CF_HC_SUCCESS; cf->connected = TRUE; cf_hc_ctx_close(data, ctx); /* ballers may have failf()'d, the winner resets it, so our * errorbuf is clean again. */ Curl_reset_fail(data); #ifdef USE_NGHTTP2 { /* For a negotiated HTTP/2 connection insert the h2 filter. */ const char *alpn = Curl_conn_cf_get_alpn_negotiated(cf->next, data); if(alpn && !strcmp("h2", alpn)) { CURLcode result = Curl_http2_switch_at(cf, data); if(result) { ctx->state = CF_HC_FAILURE; ctx->result = result; return result; } } } #endif return CURLE_OK; } static bool time_to_start_baller2(struct Curl_cfilter *cf, struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; timediff_t elapsed_ms; if(ctx->baller_count < 2) return FALSE; else if(cf_hc_baller_has_started(&ctx->ballers[1])) return FALSE; else if(ctx->ballers[0].result) { CURL_TRC_CF(data, cf, "%s baller failed, starting %s", ctx->ballers[0].name, ctx->ballers[1].name); return TRUE; } elapsed_ms = curlx_ptimediff_ms(Curl_pgrs_now(data), &ctx->started); if(elapsed_ms >= ctx->hard_eyeballs_timeout_ms) { CURL_TRC_CF(data, cf, "%s inconclusive after %" FMT_TIMEDIFF_T ", " "starting %s", ctx->ballers[0].name, ctx->hard_eyeballs_timeout_ms, ctx->ballers[1].name); return TRUE; } else if(elapsed_ms >= ctx->soft_eyeballs_timeout_ms) { if(cf_hc_baller_reply_ms(&ctx->ballers[0], data) < 0) { CURL_TRC_CF(data, cf, "%s has not seen any data after %" FMT_TIMEDIFF_T "ms, starting %s", ctx->ballers[0].name, ctx->soft_eyeballs_timeout_ms, ctx->ballers[1].name); return TRUE; } } return FALSE; } static bool cf_hc_may_h3(struct Curl_cfilter *cf, struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; if(!ctx->checked_h3) { ctx->check_h3_result = Curl_conn_may_http3(data, cf->conn, ctx->def_transport); ctx->checked_h3 = TRUE; } return !ctx->check_h3_result; } static enum alpnid cf_hc_get_httpsrr_alpn(struct Curl_cfilter *cf, struct Curl_easy *data, enum alpnid not_this_one) { #ifdef USE_HTTPSRR /* Is there an HTTPSRR use its ALPNs here. * We are here after having selected a connection to a host+port and * can no longer change that. Any HTTPSRR advice for other hosts and ports * we need to ignore. */ const struct Curl_https_rrinfo *rr; size_t i; /* Do we have HTTPS-RR information? */ rr = Curl_conn_dns_get_https(data, cf->sockindex); if(rr && !rr->no_def_alpn && /* ALPNs are defaults */ (!rr->target || /* for same host */ !rr->target[0] || (rr->target[0] == '.' && !rr->target[1])) && (!rr->port_set || /* for same port */ rr->port == cf->conn->remote_port)) { for(i = 0; i < CURL_ARRAYSIZE(rr->alpns); ++i) { enum alpnid alpn_rr = (enum alpnid)rr->alpns[i]; if(alpn_rr == not_this_one) /* don't want this one */ continue; switch(alpn_rr) { case ALPN_h3: if((data->state.http_neg.allowed & CURL_HTTP_V3x) && cf_hc_may_h3(cf, data)) { return alpn_rr; } break; case ALPN_h2: if(data->state.http_neg.allowed & CURL_HTTP_V2x) { return alpn_rr; } break; case ALPN_h1: if(data->state.http_neg.allowed & CURL_HTTP_V1x) { return alpn_rr; } break; default: /* ignore */ break; } } } #else (void)cf; (void)data; (void)not_this_one; #endif return ALPN_none; } static enum alpnid cf_hc_get_pref_alpn(struct Curl_cfilter *cf, struct Curl_easy *data, enum alpnid not_this_one) { if((data->state.http_neg.preferred & data->state.http_neg.allowed)) { switch(data->state.http_neg.preferred) { case CURL_HTTP_V3x: if(cf_hc_may_h3(cf, data) && (ALPN_h3 != not_this_one)) return ALPN_h3; break; case CURL_HTTP_V2x: if(ALPN_h2 != not_this_one) return ALPN_h2; break; case CURL_HTTP_V1x: /* If we are trying h2 already, h1 is already used as fallback */ if((ALPN_h1 != not_this_one) && (ALPN_h2 != not_this_one)) return ALPN_h1; break; default: break; } } return ALPN_none; } static enum alpnid cf_hc_get_first_alpn(struct Curl_cfilter *cf, struct Curl_easy *data, http_majors choices, enum alpnid not_this_one) { if((ALPN_h3 != not_this_one) && (choices & CURL_HTTP_V3x) && cf_hc_may_h3(cf, data)) { return ALPN_h3; } if((ALPN_h2 != not_this_one) && (choices & CURL_HTTP_V2x)) { return ALPN_h2; } /* If we are trying h2 already, h1 is already used as fallback */ if((ALPN_h1 != not_this_one) && (ALPN_h2 != not_this_one) && (choices & CURL_HTTP_V1x)) { return ALPN_h1; } return ALPN_none; } static CURLcode cf_hc_set_baller1(struct Curl_cfilter *cf, struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; enum alpnid alpn1 = ALPN_none; VERBOSE(const char *source = "HTTPS-RR"); DEBUGASSERT(cf->conn->bits.tls_enable_alpn); alpn1 = cf_hc_get_httpsrr_alpn(cf, data, ALPN_none); if(alpn1 == ALPN_none) { /* preference is configured and allowed, can we use it? */ VERBOSE(source = "preferred version"); alpn1 = cf_hc_get_pref_alpn(cf, data, ALPN_none); } if(alpn1 == ALPN_none) { VERBOSE(source = "wanted versions"); alpn1 = cf_hc_get_first_alpn(cf, data, data->state.http_neg.wanted, ALPN_none); } if(alpn1 == ALPN_none) { VERBOSE(source = "allowed versions"); alpn1 = cf_hc_get_first_alpn(cf, data, data->state.http_neg.allowed, ALPN_none); } if(alpn1 == ALPN_none) { /* None of the wanted/allowed HTTP versions could be chosen */ if(ctx->check_h3_result) { CURL_TRC_CF(data, cf, "unable to use HTTP/3"); return ctx->check_h3_result; } CURL_TRC_CF(data, cf, "unable to select HTTP version"); return CURLE_FAILED_INIT; } cf_hc_baller_assign(&ctx->ballers[0], alpn1, ctx->def_transport); ctx->baller_count = 1; CURL_TRC_CF(data, cf, "1st attempt uses %s from %s", ctx->ballers[0].name, source); return CURLE_OK; } static void cf_hc_set_baller2(struct Curl_cfilter *cf, struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; enum alpnid alpn2 = ALPN_none, alpn1 = ctx->ballers[0].alpn_id; VERBOSE(const char *source = "HTTPS-RR"); if(ctx->ballers_complete) return; /* already done */ if(!ctx->httpsrr_resolved) return; /* HTTPS-RR pending */ alpn2 = cf_hc_get_httpsrr_alpn(cf, data, alpn1); if(alpn2 == ALPN_none) { /* preference is configured and allowed, can we use it? */ VERBOSE(source = "preferred version"); alpn2 = cf_hc_get_pref_alpn(cf, data, alpn1); } if(alpn2 == ALPN_none) { VERBOSE(source = "wanted versions"); alpn2 = cf_hc_get_first_alpn(cf, data, data->state.http_neg.wanted, alpn1); } if(alpn2 != ALPN_none) { cf_hc_baller_assign(&ctx->ballers[1], alpn2, ctx->def_transport); ctx->baller_count = 2; CURL_TRC_CF(data, cf, "2nd attempt uses %s from %s", ctx->ballers[1].name, source); } ctx->ballers_complete = TRUE; } static CURLcode cf_hc_connect(struct Curl_cfilter *cf, struct Curl_easy *data, bool *done) { struct cf_hc_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; if(cf->connected) { *done = TRUE; return CURLE_OK; } *done = FALSE; if(!ctx->httpsrr_resolved) ctx->httpsrr_resolved = Curl_conn_dns_resolved_https(data, cf->sockindex); switch(ctx->state) { case CF_HC_RESOLV: /* Without any addressinfo, delay the start of balling. */ if(!Curl_conn_dns_has_any_ai(data, cf->sockindex)) return CURLE_OK; ctx->state = CF_HC_INIT; FALLTHROUGH(); case CF_HC_INIT: DEBUGASSERT(!cf->next); CURL_TRC_CF(data, cf, "connect, init"); result = cf_hc_set_baller1(cf, data); if(result) { ctx->result = result; ctx->state = CF_HC_FAILURE; goto out; } cf_hc_set_baller2(cf, data); ctx->started = *Curl_pgrs_now(data); cf_hc_baller_init(&ctx->ballers[0], cf, data); if((ctx->baller_count > 1) || !ctx->ballers_complete) { Curl_expire(data, ctx->soft_eyeballs_timeout_ms, EXPIRE_ALPN_EYEBALLS); } ctx->state = CF_HC_CONNECT; FALLTHROUGH(); case CF_HC_CONNECT: if(!ctx->ballers_complete) cf_hc_set_baller2(cf, data); if(cf_hc_baller_is_connecting(&ctx->ballers[0])) { result = cf_hc_baller_connect(&ctx->ballers[0], cf, data, done); if(!result && *done) { result = baller_connected(cf, data, &ctx->ballers[0]); goto out; } } if(time_to_start_baller2(cf, data)) { cf_hc_baller_init(&ctx->ballers[1], cf, data); } if(cf_hc_baller_is_connecting(&ctx->ballers[1])) { result = cf_hc_baller_connect(&ctx->ballers[1], cf, data, done); if(!result && *done) { result = baller_connected(cf, data, &ctx->ballers[1]); goto out; } } if(ctx->ballers[0].result && (ctx->ballers[1].result || (ctx->ballers_complete && (ctx->baller_count < 2)))) { /* all have failed. we give up */ CURL_TRC_CF(data, cf, "connect, all attempts failed"); ctx->result = result = ctx->ballers[0].result; ctx->state = CF_HC_FAILURE; goto out; } result = CURLE_OK; *done = FALSE; break; case CF_HC_FAILURE: result = ctx->result; cf->connected = FALSE; *done = FALSE; break; case CF_HC_SUCCESS: result = CURLE_OK; cf->connected = TRUE; *done = TRUE; break; } out: CURL_TRC_CF(data, cf, "connect -> %d, done=%d", result, *done); return result; } static CURLcode cf_hc_shutdown(struct Curl_cfilter *cf, struct Curl_easy *data, bool *done) { struct cf_hc_ctx *ctx = cf->ctx; size_t i; CURLcode result = CURLE_OK; DEBUGASSERT(data); if(cf->connected) { *done = TRUE; return CURLE_OK; } /* shutdown all ballers that have not done so already. If one fails, * continue shutting down others until all are shutdown. */ for(i = 0; i < ctx->baller_count; i++) { struct cf_hc_baller *b = &ctx->ballers[i]; bool bdone = FALSE; if(!cf_hc_baller_is_connecting(b) || b->shutdown) continue; b->result = b->cf->cft->do_shutdown(b->cf, data, &bdone); if(b->result || bdone) b->shutdown = TRUE; /* treat a failed shutdown as done */ } *done = TRUE; for(i = 0; i < ctx->baller_count; i++) { if(!ctx->ballers[i].shutdown) *done = FALSE; } if(*done) { for(i = 0; i < ctx->baller_count; i++) { if(ctx->ballers[i].result) result = ctx->ballers[i].result; } } CURL_TRC_CF(data, cf, "shutdown -> %d, done=%d", result, *done); return result; } static CURLcode cf_hc_adjust_pollset(struct Curl_cfilter *cf, struct Curl_easy *data, struct easy_pollset *ps) { CURLcode result = CURLE_OK; if(!cf->connected) { struct cf_hc_ctx *ctx = cf->ctx; size_t i; for(i = 0; (i < ctx->baller_count) && !result; i++) { struct cf_hc_baller *b = &ctx->ballers[i]; if(!cf_hc_baller_is_connecting(b)) continue; result = Curl_conn_cf_adjust_pollset(b->cf, data, ps); } CURL_TRC_CF(data, cf, "adjust_pollset -> %d, %d socks", result, ps->n); } return result; } static bool cf_hc_data_pending(struct Curl_cfilter *cf, const struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; size_t i; if(cf->connected) return cf->next->cft->has_data_pending(cf->next, data); for(i = 0; i < ctx->baller_count; i++) if(cf_hc_baller_data_pending(&ctx->ballers[i], data)) return TRUE; return FALSE; } static struct curltime cf_get_max_baller_time(struct Curl_cfilter *cf, struct Curl_easy *data, int query) { struct cf_hc_ctx *ctx = cf->ctx; struct curltime t, tmax; size_t i; memset(&tmax, 0, sizeof(tmax)); for(i = 0; i < ctx->baller_count; i++) { struct Curl_cfilter *cfb = ctx->ballers[i].cf; memset(&t, 0, sizeof(t)); if(cfb && !cfb->cft->query(cfb, data, query, NULL, &t)) { if((t.tv_sec || t.tv_usec) && curlx_ptimediff_us(&t, &tmax) > 0) tmax = t; } } return tmax; } static CURLcode cf_hc_query(struct Curl_cfilter *cf, struct Curl_easy *data, int query, int *pres1, void *pres2) { struct cf_hc_ctx *ctx = cf->ctx; size_t i; if(!cf->connected) { switch(query) { case CF_QUERY_TIMER_CONNECT: { struct curltime *when = pres2; *when = cf_get_max_baller_time(cf, data, CF_QUERY_TIMER_CONNECT); return CURLE_OK; } case CF_QUERY_TIMER_APPCONNECT: { struct curltime *when = pres2; *when = cf_get_max_baller_time(cf, data, CF_QUERY_TIMER_APPCONNECT); return CURLE_OK; } case CF_QUERY_NEED_FLUSH: { for(i = 0; i < ctx->baller_count; i++) if(cf_hc_baller_needs_flush(&ctx->ballers[i], data)) { *pres1 = TRUE; return CURLE_OK; } break; } default: break; } } return cf->next ? cf->next->cft->query(cf->next, data, query, pres1, pres2) : CURLE_UNKNOWN_OPTION; } static CURLcode cf_hc_cntrl(struct Curl_cfilter *cf, struct Curl_easy *data, int event, int arg1, void *arg2) { struct cf_hc_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; size_t i; if(!cf->connected) { for(i = 0; i < ctx->baller_count; i++) { result = cf_hc_baller_cntrl(&ctx->ballers[i], data, event, arg1, arg2); if(result && (result != CURLE_AGAIN)) goto out; } result = CURLE_OK; } out: return result; } static void cf_hc_close(struct Curl_cfilter *cf, struct Curl_easy *data) { CURL_TRC_CF(data, cf, "close"); cf_hc_ctx_close(data, cf->ctx); cf->connected = FALSE; if(cf->next) { cf->next->cft->do_close(cf->next, data); Curl_conn_cf_discard_chain(&cf->next, data); } } static void cf_hc_destroy(struct Curl_cfilter *cf, struct Curl_easy *data) { struct cf_hc_ctx *ctx = cf->ctx; CURL_TRC_CF(data, cf, "destroy"); cf_hc_ctx_destroy(data, ctx); } struct Curl_cftype Curl_cft_http_connect = { "HTTPS-CONNECT", CF_TYPE_SETUP, CURL_LOG_LVL_NONE, cf_hc_destroy, cf_hc_connect, cf_hc_close, cf_hc_shutdown, cf_hc_adjust_pollset, cf_hc_data_pending, Curl_cf_def_send, Curl_cf_def_recv, cf_hc_cntrl, Curl_cf_def_conn_is_alive, Curl_cf_def_conn_keep_alive, cf_hc_query, }; static CURLcode cf_hc_create(struct Curl_cfilter **pcf, struct Curl_easy *data, uint8_t def_transport) { struct Curl_cfilter *cf = NULL; struct cf_hc_ctx *ctx; CURLcode result = CURLE_OK; ctx = curlx_calloc(1, sizeof(*ctx)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; goto out; } ctx->def_transport = def_transport; result = Curl_cf_create(&cf, &Curl_cft_http_connect, ctx); if(result) goto out; ctx = NULL; out: *pcf = result ? NULL : cf; cf_hc_ctx_destroy(data, ctx); return result; } static CURLcode cf_hc_add(struct Curl_easy *data, struct connectdata *conn, int sockindex, uint8_t def_transport) { struct Curl_cfilter *cf; CURLcode result = CURLE_OK; DEBUGASSERT(data); result = cf_hc_create(&cf, data, def_transport); if(result) goto out; Curl_conn_cf_add(data, conn, sockindex, cf); out: return result; } CURLcode Curl_cf_https_setup(struct Curl_easy *data, struct connectdata *conn, int sockindex) { CURLcode result = CURLE_OK; DEBUGASSERT(conn->scheme->protocol == CURLPROTO_HTTPS); if((conn->scheme->protocol != CURLPROTO_HTTPS) || !conn->bits.tls_enable_alpn) goto out; result = cf_hc_add(data, conn, sockindex, conn->transport_wanted); out: return result; } #endif /* !CURL_DISABLE_HTTP */