From: Stefan Eissing Date: Mon, 27 Jan 2025 14:39:13 +0000 (+0100) Subject: http: version negotiation X-Git-Tag: curl-8_13_0~432 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=db72b8d4d0a5982590998b5db45414f02a4f04c7;p=thirdparty%2Fcurl.git http: version negotiation Translate the `data->set.httpwant` which is one of the consts from the public API (CURL_HTTP_VERSION_*) into a major version mask plus additional flags for internal handling. `Curl_http_neg_init()` does the translation and flags setting in http.c, using new internal consts CURL_HTTP_V1x, CURL_HTTP_V2x and CURL_HTTP_V3x for the major versions. The flags are - only_10: when the application explicity asked fro HTTP/1.0 - h2_upgrade: when the application asks for upgrading 1.1 to 2. - h2_prior_knowledge: when directly talking h2 without ALPN - accept_09: when a HTTP/0.9 response is acceptable. The Alt-Svc and HTTPS RR redirections from one ALPN to another obey the allowed major versions. If a transfer has only h3 enabled, Alt-Svc redirection to h2 is ignored. This is the current implementation. It can be debated if Alt-Svc should be able to override the allowed major versions. Added test_12_06 to verify the current restriction. Closes #16100 --- diff --git a/lib/cf-https-connect.c b/lib/cf-https-connect.c index f073647e55..2a645d415b 100644 --- a/lib/cf-https-connect.c +++ b/lib/cf-https-connect.c @@ -651,67 +651,56 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data, (void)remotehost; if(conn->bits.tls_enable_alpn) { - switch(data->state.httpwant) { - case CURL_HTTP_VERSION_NONE: - /* No preferences by transfer setup. Choose best defaults */ #ifdef USE_HTTPSRR - if(conn->dns_entry && conn->dns_entry->hinfo && - !conn->dns_entry->hinfo->no_def_alpn) { - size_t i, j; - for(i = 0; i < CURL_ARRAYSIZE(conn->dns_entry->hinfo->alpns) && - alpn_count < CURL_ARRAYSIZE(alpn_ids); ++i) { - bool present = FALSE; - enum alpnid alpn = conn->dns_entry->hinfo->alpns[i]; - for(j = 0; j < alpn_count; ++j) { - if(alpn == alpn_ids[j]) { - present = TRUE; - break; - } - } - if(!present) { - switch(alpn) { - case ALPN_h3: - if(Curl_conn_may_http3(data, conn)) - break; /* not possible */ - FALLTHROUGH(); - case ALPN_h2: - case ALPN_h1: - alpn_ids[alpn_count++] = alpn; - break; - default: /* ignore */ - break; - } + if(conn->dns_entry && conn->dns_entry->hinfo && + !conn->dns_entry->hinfo->no_def_alpn) { + size_t i, j; + for(i = 0; i < CURL_ARRAYSIZE(conn->dns_entry->hinfo->alpns) && + alpn_count < CURL_ARRAYSIZE(alpn_ids); ++i) { + bool present = FALSE; + enum alpnid alpn = conn->dns_entry->hinfo->alpns[i]; + for(j = 0; j < alpn_count; ++j) { + if(alpn == alpn_ids[j]) { + present = TRUE; + break; } } + if(present) + continue; + switch(alpn) { + case ALPN_h3: + if(Curl_conn_may_http3(data, conn)) + break; /* not possible */ + if(data->state.http_neg.allowed & CURL_HTTP_V3x) + alpn_ids[alpn_count++] = alpn; + break; + case ALPN_h2: + if(data->state.http_neg.allowed & CURL_HTTP_V2x) + alpn_ids[alpn_count++] = alpn; + break; + case ALPN_h1: + if(data->state.http_neg.allowed & CURL_HTTP_V1x) + alpn_ids[alpn_count++] = alpn; + break; + default: /* ignore */ + break; + } } + } #endif - if(!alpn_count) + + if(!alpn_count) { + if(data->state.http_neg.allowed & CURL_HTTP_V3x) { + result = Curl_conn_may_http3(data, conn); + if(!result) + alpn_ids[alpn_count++] = ALPN_h3; + else if(data->state.http_neg.allowed == CURL_HTTP_V3x) + goto out; /* only h3 allowed, not possible, error out */ + } + if(data->state.http_neg.allowed & CURL_HTTP_V2x) alpn_ids[alpn_count++] = ALPN_h2; - break; - case CURL_HTTP_VERSION_3ONLY: - result = Curl_conn_may_http3(data, conn); - if(result) /* cannot do it */ - goto out; - alpn_ids[alpn_count++] = ALPN_h3; - break; - case CURL_HTTP_VERSION_3: - /* We assume that silently not even trying H3 is ok here */ - if(Curl_conn_may_http3(data, conn) == CURLE_OK) - alpn_ids[alpn_count++] = ALPN_h3; - alpn_ids[alpn_count++] = ALPN_h2; - break; - case CURL_HTTP_VERSION_2_0: - case CURL_HTTP_VERSION_2TLS: - case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: - alpn_ids[alpn_count++] = ALPN_h2; - break; - case CURL_HTTP_VERSION_1_0: - case CURL_HTTP_VERSION_1_1: - alpn_ids[alpn_count++] = ALPN_h1; - break; - default: - alpn_ids[alpn_count++] = ALPN_h2; - break; + else if(data->state.http_neg.allowed & CURL_HTTP_V1x) + alpn_ids[alpn_count++] = ALPN_h1; } } diff --git a/lib/cfilters.c b/lib/cfilters.c index 91f7325c38..fa0abc46d9 100644 --- a/lib/cfilters.c +++ b/lib/cfilters.c @@ -497,13 +497,14 @@ bool Curl_conn_is_multiplex(struct connectdata *conn, int sockindex) return FALSE; } -unsigned char Curl_conn_http_version(struct Curl_easy *data) +unsigned char Curl_conn_http_version(struct Curl_easy *data, + struct connectdata *conn) { struct Curl_cfilter *cf; CURLcode result = CURLE_UNKNOWN_OPTION; unsigned char v = 0; - cf = data->conn ? data->conn->cfilter[FIRSTSOCKET] : NULL; + cf = conn->cfilter[FIRSTSOCKET]; for(; cf; cf = cf->next) { if(cf->cft->flags & CF_TYPE_HTTP) { int value = 0; diff --git a/lib/cfilters.h b/lib/cfilters.h index 2d5599a90a..23746edcde 100644 --- a/lib/cfilters.h +++ b/lib/cfilters.h @@ -399,7 +399,8 @@ bool Curl_conn_is_multiplex(struct connectdata *conn, int sockindex); * Return the HTTP version used on the FIRSTSOCKET connection filters * or 0 if unknown. Value otherwise is 09, 10, 11, etc. */ -unsigned char Curl_conn_http_version(struct Curl_easy *data); +unsigned char Curl_conn_http_version(struct Curl_easy *data, + struct connectdata *conn); /** * Close the filter chain at `sockindex` for connection `data->conn`. diff --git a/lib/http.c b/lib/http.c index f82e030a1b..e054bf4b69 100644 --- a/lib/http.c +++ b/lib/http.c @@ -187,19 +187,54 @@ const struct Curl_handler Curl_handler_https = { #endif +void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg) +{ + memset(neg, 0, sizeof(*neg)); + neg->accept_09 = data->set.http09_allowed; + switch(data->set.httpwant) { + case CURL_HTTP_VERSION_1_0: + neg->allowed = (CURL_HTTP_V1x); + neg->only_10 = TRUE; + break; + case CURL_HTTP_VERSION_1_1: + neg->allowed = (CURL_HTTP_V1x); + break; + case CURL_HTTP_VERSION_2_0: + neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x); + neg->h2_upgrade = TRUE; + break; + case CURL_HTTP_VERSION_2TLS: + neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x); + break; + case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: + neg->allowed = (CURL_HTTP_V2x); + data->state.http_neg.h2_prior_knowledge = TRUE; + break; + case CURL_HTTP_VERSION_3: + neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x); + break; + case CURL_HTTP_VERSION_3ONLY: + neg->allowed = (CURL_HTTP_V3x); + break; + case CURL_HTTP_VERSION_NONE: + default: + neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x); + break; + } +} + CURLcode Curl_http_setup_conn(struct Curl_easy *data, struct connectdata *conn) { /* allocate the HTTP-specific struct for the Curl_easy, only to survive during this request */ connkeep(conn, "HTTP default"); - - if(data->state.httpwant == CURL_HTTP_VERSION_3ONLY) { + if(data->state.http_neg.allowed == CURL_HTTP_V3x) { + /* only HTTP/3, needs to work */ CURLcode result = Curl_conn_may_http3(data, conn); if(result) return result; } - return CURLE_OK; } @@ -538,7 +573,7 @@ CURLcode Curl_http_auth_act(struct Curl_easy *data) (data->req.httpversion_sent > 11)) { infof(data, "Forcing HTTP/1.1 for NTLM"); connclose(conn, "Force HTTP/1.1 connection"); - data->state.httpwant = CURL_HTTP_VERSION_1_1; + data->state.http_neg.allowed = CURL_HTTP_V1x; } } #ifndef CURL_DISABLE_PROXY @@ -1502,29 +1537,28 @@ static bool http_may_use_1_1(const struct Curl_easy *data) const struct connectdata *conn = data->conn; /* We have seen a previous response for *this* transfer with 1.0, * on another connection or the same one. */ - if(data->state.httpversion == 10) + if(data->state.http_neg.rcvd_min == 10) return FALSE; /* We have seen a previous response on *this* connection with 1.0. */ - if(conn->httpversion_seen == 10) + if(conn && conn->httpversion_seen == 10) return FALSE; /* We want 1.0 and have seen no previous response on *this* connection with a higher version (maybe no response at all yet). */ - if((data->state.httpwant == CURL_HTTP_VERSION_1_0) && - (conn->httpversion_seen <= 10)) + if((data->state.http_neg.only_10) && + (!conn || conn->httpversion_seen <= 10)) return FALSE; - /* We want something newer than 1.0 or have no preferences. */ - return (data->state.httpwant == CURL_HTTP_VERSION_NONE) || - (data->state.httpwant >= CURL_HTTP_VERSION_1_1); + /* We are not restricted to use 1.0 only. */ + return !data->state.http_neg.only_10; } static unsigned char http_request_version(struct Curl_easy *data) { - unsigned char httpversion = Curl_conn_http_version(data); - if(!httpversion) { + unsigned char v = Curl_conn_http_version(data, data->conn); + if(!v) { /* No specific HTTP connection filter installed. */ - httpversion = http_may_use_1_1(data) ? 11 : 10; + v = http_may_use_1_1(data) ? 11 : 10; } - return httpversion; + return v; } static const char *get_http_string(int httpversion) @@ -2621,11 +2655,11 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done) switch(conn->alpn) { case CURL_HTTP_VERSION_3: - DEBUGASSERT(Curl_conn_http_version(data) == 30); + DEBUGASSERT(Curl_conn_http_version(data, conn) == 30); break; case CURL_HTTP_VERSION_2: #ifndef CURL_DISABLE_PROXY - if((Curl_conn_http_version(data) != 20) && + if((Curl_conn_http_version(data, conn) != 20) && conn->bits.proxy && !conn->bits.tunnel_proxy ) { result = Curl_http2_switch(data); @@ -2634,7 +2668,7 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done) } else #endif - DEBUGASSERT(Curl_conn_http_version(data) == 20); + DEBUGASSERT(Curl_conn_http_version(data, conn) == 20); break; case CURL_HTTP_VERSION_1_1: /* continue with HTTP/1.x when explicitly requested */ @@ -2815,7 +2849,8 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done) } if(!Curl_conn_is_ssl(conn, FIRSTSOCKET) && (httpversion < 20) && - (data->state.httpwant == CURL_HTTP_VERSION_2)) { + (data->state.http_neg.allowed & CURL_HTTP_V2x) && + data->state.http_neg.h2_upgrade) { /* append HTTP2 upgrade magic stuff to the HTTP request if it is not done over SSL */ result = Curl_http2_request_upgrade(&req, data); @@ -3346,9 +3381,10 @@ static CURLcode http_statusline(struct Curl_easy *data, data->info.httpversion = k->httpversion; conn->httpversion_seen = (unsigned char)k->httpversion; - if(!data->state.httpversion || data->state.httpversion > k->httpversion) + if(!data->state.http_neg.rcvd_min || + data->state.http_neg.rcvd_min > k->httpversion) /* store the lowest server version we encounter */ - data->state.httpversion = (unsigned char)k->httpversion; + data->state.http_neg.rcvd_min = (unsigned char)k->httpversion; /* * This code executes as part of processing the header. As a @@ -4014,7 +4050,7 @@ static CURLcode http_parse_headers(struct Curl_easy *data, failf(data, "Invalid status line"); return CURLE_WEIRD_SERVER_REPLY; } - if(!data->set.http09_allowed) { + if(!data->state.http_neg.accept_09) { failf(data, "Received HTTP/0.9 when not allowed"); return CURLE_UNSUPPORTED_PROTOCOL; } @@ -4051,7 +4087,7 @@ static CURLcode http_parse_headers(struct Curl_easy *data, failf(data, "Invalid status line"); return CURLE_WEIRD_SERVER_REPLY; } - if(!data->set.http09_allowed) { + if(!data->state.http_neg.accept_09) { failf(data, "Received HTTP/0.9 when not allowed"); return CURLE_UNSUPPORTED_PROTOCOL; } diff --git a/lib/http.h b/lib/http.h index a15a982356..61237b3bf8 100644 --- a/lib/http.h +++ b/lib/http.h @@ -53,6 +53,12 @@ typedef enum { FOLLOW_REDIR /* a full true redirect */ } followtype; +#define CURL_HTTP_V1x (1 << 0) +#define CURL_HTTP_V2x (1 << 1) +#define CURL_HTTP_V3x (1 << 2) +/* bitmask of CURL_HTTP_V* values */ +typedef unsigned char http_majors; + #ifndef CURL_DISABLE_HTTP @@ -68,6 +74,17 @@ extern const struct Curl_handler Curl_handler_https; struct dynhds; +struct http_negotiation { + unsigned char rcvd_min; /* minimum version seen in responses, 09, 10, 11 */ + http_majors allowed; /* allowed major versions when talking to server */ + BIT(h2_upgrade); /* Do HTTP Upgrade from 1.1 to 2 */ + BIT(h2_prior_knowledge); /* Directly do HTTP/2 without ALPN/SSL */ + BIT(accept_09); /* Accept an HTTP/0.9 response */ + BIT(only_10); /* When using major version 1x, use only 1.0 */ +}; + +void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg); + CURLcode Curl_bump_headersize(struct Curl_easy *data, size_t delta, bool connect_only); diff --git a/lib/http2.c b/lib/http2.c index 171de70b2c..b82b984935 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -2794,8 +2794,9 @@ out: bool Curl_http2_may_switch(struct Curl_easy *data) { - if(Curl_conn_http_version(data) < 20 && - data->state.httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE) { + if(Curl_conn_http_version(data, data->conn) < 20 && + (data->state.http_neg.allowed & CURL_HTTP_V2x) && + data->state.http_neg.h2_prior_knowledge) { #ifndef CURL_DISABLE_PROXY if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy) { /* We do not support HTTP/2 proxies yet. Also it is debatable @@ -2814,7 +2815,7 @@ CURLcode Curl_http2_switch(struct Curl_easy *data) struct Curl_cfilter *cf; CURLcode result; - DEBUGASSERT(Curl_conn_http_version(data) < 20); + DEBUGASSERT(Curl_conn_http_version(data, data->conn) < 20); result = http2_cfilter_add(&cf, data, data->conn, FIRSTSOCKET, FALSE); if(result) @@ -2836,7 +2837,7 @@ CURLcode Curl_http2_switch_at(struct Curl_cfilter *cf, struct Curl_easy *data) struct Curl_cfilter *cf_h2; CURLcode result; - DEBUGASSERT(Curl_conn_http_version(data) < 20); + DEBUGASSERT(Curl_conn_http_version(data, data->conn) < 20); result = http2_cfilter_insert_after(cf, data, FALSE); if(result) @@ -2861,7 +2862,7 @@ CURLcode Curl_http2_upgrade(struct Curl_easy *data, struct cf_h2_ctx *ctx; CURLcode result; - DEBUGASSERT(Curl_conn_http_version(data) < 20); + DEBUGASSERT(Curl_conn_http_version(data, conn) < 20); DEBUGASSERT(data->req.upgr101 == UPGR101_RECEIVED); result = http2_cfilter_add(&cf, data, conn, sockindex, TRUE); @@ -2908,7 +2909,7 @@ CURLcode Curl_http2_upgrade(struct Curl_easy *data, CURLE_HTTP2_STREAM error! */ bool Curl_h2_http_1_1_error(struct Curl_easy *data) { - if(Curl_conn_http_version(data) == 20) { + if(Curl_conn_http_version(data, data->conn) == 20) { int err = Curl_conn_get_stream_error(data, data->conn, FIRSTSOCKET); return err == NGHTTP2_HTTP_1_1_REQUIRED; } diff --git a/lib/multi.c b/lib/multi.c index 2b05e94a04..449420dc86 100644 --- a/lib/multi.c +++ b/lib/multi.c @@ -1910,6 +1910,7 @@ static CURLMcode state_performing(struct Curl_easy *data, data->req.done = TRUE; } } +#ifndef CURL_DISABLE_HTTP else if((CURLE_HTTP2_STREAM == result) && Curl_h2_http_1_1_error(data)) { CURLcode ret = Curl_retry_request(data, &newurl); @@ -1917,7 +1918,7 @@ static CURLMcode state_performing(struct Curl_easy *data, if(!ret) { infof(data, "Downgrades to HTTP/1.1"); streamclose(data->conn, "Disconnect HTTP/2 for HTTP/1"); - data->state.httpwant = CURL_HTTP_VERSION_1_1; + data->state.http_neg.allowed = CURL_HTTP_V1x; /* clear the error message bit too as we ignore the one we got */ data->state.errorbuf = FALSE; if(!newurl) @@ -1932,6 +1933,7 @@ static CURLMcode state_performing(struct Curl_easy *data, else result = ret; } +#endif if(result) { /* diff --git a/lib/transfer.c b/lib/transfer.c index 365005d02e..19dcad3f04 100644 --- a/lib/transfer.c +++ b/lib/transfer.c @@ -570,8 +570,9 @@ CURLcode Curl_pretransfer(struct Curl_easy *data) data->state.followlocation = 0; /* reset the location-follow counter */ data->state.this_is_a_follow = FALSE; /* reset this */ data->state.errorbuf = FALSE; /* no error has occurred */ - data->state.httpwant = data->set.httpwant; - data->state.httpversion = 0; +#ifndef CURL_DISABLE_HTTP + Curl_http_neg_init(data, &data->state.http_neg); +#endif data->state.authproblem = FALSE; data->state.authhost.want = data->set.httpauth; data->state.authproxy.want = data->set.proxyauth; diff --git a/lib/url.c b/lib/url.c index 2826fc2a68..b348834c22 100644 --- a/lib/url.c +++ b/lib/url.c @@ -652,15 +652,20 @@ bool Curl_on_disconnect(struct Curl_easy *data, static bool xfer_may_multiplex(const struct Curl_easy *data, const struct connectdata *conn) { +#ifndef CURL_DISABLE_HTTP /* If an HTTP protocol and multiplexing is enabled */ if((conn->handler->protocol & PROTO_FAMILY_HTTP) && (!conn->bits.protoconnstart || !conn->bits.close)) { if(Curl_multiplex_wanted(data->multi) && - (data->state.httpwant >= CURL_HTTP_VERSION_2)) + (data->state.http_neg.allowed & (CURL_HTTP_V2x|CURL_HTTP_V3x))) /* allows HTTP/2 or newer */ return TRUE; } +#else + (void)data; + (void)conn; +#endif return FALSE; } @@ -992,8 +997,9 @@ static bool url_match_conn(struct connectdata *conn, void *userdata) } #endif +#ifndef CURL_DISABLE_HTTP if(match->may_multiplex && - (data->state.httpwant == CURL_HTTP_VERSION_2_0) && + (data->state.http_neg.allowed & (CURL_HTTP_V2x|CURL_HTTP_V3x)) && (needle->handler->protocol & CURLPROTO_HTTP) && !conn->httpversion_seen) { if(data->set.pipewait) { @@ -1005,6 +1011,7 @@ static bool url_match_conn(struct connectdata *conn, void *userdata) infof(data, "Server upgrade cannot be used"); return FALSE; } +#endif if(!(needle->handler->flags & PROTOPT_CREDSPERREQUEST)) { /* This protocol requires credentials per connection, @@ -1025,27 +1032,38 @@ static bool url_match_conn(struct connectdata *conn, void *userdata) return FALSE; #endif - /* If looking for HTTP and the HTTP version we want is less - * than the HTTP version of conn, continue looking. +#ifndef CURL_DISABLE_HTTP + /* If looking for HTTP and the HTTP versions allowed do not include + * the HTTP version of conn, continue looking. * CURL_HTTP_VERSION_2TLS is default which indicates no preference, * so we take any existing connection. */ - if((needle->handler->protocol & PROTO_FAMILY_HTTP) && - (data->state.httpwant != CURL_HTTP_VERSION_2TLS)) { - unsigned char httpversion = Curl_conn_http_version(data); - if((httpversion >= 20) && - (data->state.httpwant < CURL_HTTP_VERSION_2_0)) { - DEBUGF(infof(data, "nor reusing conn #%" CURL_FORMAT_CURL_OFF_T - " with httpversion=%d, we want a version less than h2", - conn->connection_id, httpversion)); - } - if((httpversion >= 30) && - (data->state.httpwant < CURL_HTTP_VERSION_3)) { - DEBUGF(infof(data, "nor reusing conn #%" CURL_FORMAT_CURL_OFF_T - " with httpversion=%d, we want a version less than h3", - conn->connection_id, httpversion)); - return FALSE; + if((needle->handler->protocol & PROTO_FAMILY_HTTP)) { + switch(Curl_conn_http_version(data, conn)) { + case 30: + if(!(data->state.http_neg.allowed & CURL_HTTP_V3x)) { + DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T + ", we do not want h3", conn->connection_id)); + return FALSE; + } + break; + case 20: + if(!(data->state.http_neg.allowed & CURL_HTTP_V2x)) { + DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T + ", we do not want h2", conn->connection_id)); + return FALSE; + } + break; + default: + if(!(data->state.http_neg.allowed & CURL_HTTP_V1x)) { + DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T + ", we do not want h1", conn->connection_id)); + return FALSE; + } + break; } } +#endif + #ifdef USE_SSH else if(get_protocol_family(needle->handler) & PROTO_FAMILY_SSH) { if(!ssh_config_matches(needle, conn)) @@ -3054,75 +3072,47 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, )) { /* no connect_to match, try alt-svc! */ enum alpnid srcalpnid = ALPN_none; - bool use_alt_svc = FALSE; bool hit = FALSE; struct altsvc *as = NULL; - const int allowed_versions = ( ALPN_h1 -#ifdef USE_HTTP2 - | ALPN_h2 -#endif -#ifdef USE_HTTP3 - | ALPN_h3 -#endif - ) & data->asi->flags; - static enum alpnid alpn_ids[] = { -#ifdef USE_HTTP3 - ALPN_h3, -#endif -#ifdef USE_HTTP2 - ALPN_h2, -#endif - ALPN_h1, - }; - size_t i; + int allowed_versions = ALPN_none; - switch(data->state.httpwant) { - case CURL_HTTP_VERSION_1_0: - break; - case CURL_HTTP_VERSION_1_1: - use_alt_svc = TRUE; - srcalpnid = ALPN_h1; /* only regard alt-svc advice for http/1.1 */ - break; - case CURL_HTTP_VERSION_2_0: - use_alt_svc = TRUE; - srcalpnid = ALPN_h2; /* only regard alt-svc advice for h2 */ - break; - case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: - break; - case CURL_HTTP_VERSION_3: - use_alt_svc = TRUE; - srcalpnid = ALPN_h3; /* only regard alt-svc advice for h3 */ - break; - case CURL_HTTP_VERSION_3ONLY: - break; - default: /* no specific HTTP version wanted, look at all of alt-svc */ - use_alt_svc = TRUE; - srcalpnid = ALPN_none; - break; - } - if(!use_alt_svc) - return CURLE_OK; + if(data->state.http_neg.allowed & CURL_HTTP_V3x) + allowed_versions |= ALPN_h3; + if(data->state.http_neg.allowed & CURL_HTTP_V2x) + allowed_versions |= ALPN_h2; + if(data->state.http_neg.allowed & CURL_HTTP_V1x) + allowed_versions |= ALPN_h1; + allowed_versions &= (int)data->asi->flags; host = conn->host.rawalloc; DEBUGF(infof(data, "check Alt-Svc for host %s", host)); - if(srcalpnid == ALPN_none) { - /* scan all alt-svc protocol ids in order or relevance */ - for(i = 0; !hit && (i < CURL_ARRAYSIZE(alpn_ids)); ++i) { - srcalpnid = alpn_ids[i]; - hit = Curl_altsvc_lookup(data->asi, - srcalpnid, host, conn->remote_port, /* from */ - &as /* to */, - allowed_versions); - } +#ifdef USE_HTTP3 + if(!hit && (allowed_versions & ALPN_h3)) { + srcalpnid = ALPN_h3; + hit = Curl_altsvc_lookup(data->asi, + ALPN_h3, host, conn->remote_port, /* from */ + &as /* to */, + allowed_versions); } - else { - /* look for a specific alt-svc protocol id */ + #endif + #ifdef USE_HTTP2 + if(!hit && (allowed_versions & ALPN_h2) && + !data->state.http_neg.h2_prior_knowledge) { + srcalpnid = ALPN_h2; hit = Curl_altsvc_lookup(data->asi, - srcalpnid, host, conn->remote_port, /* from */ + ALPN_h2, host, conn->remote_port, /* from */ + &as /* to */, + allowed_versions); + } + #endif + if(!hit && (allowed_versions & ALPN_h1) && + !data->state.http_neg.only_10) { + srcalpnid = ALPN_h1; + hit = Curl_altsvc_lookup(data->asi, + ALPN_h1, host, conn->remote_port, /* from */ &as /* to */, allowed_versions); } - if(hit) { char *hostd = strdup((char *)as->dst.host); @@ -3141,14 +3131,15 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, /* protocol version switch */ switch(as->dst.alpnid) { case ALPN_h1: - data->state.httpwant = CURL_HTTP_VERSION_1_1; + data->state.http_neg.allowed = CURL_HTTP_V1x; + data->state.http_neg.only_10 = FALSE; break; case ALPN_h2: - data->state.httpwant = CURL_HTTP_VERSION_2_0; + data->state.http_neg.allowed = CURL_HTTP_V2x; break; case ALPN_h3: conn->transport = TRNSPRT_QUIC; - data->state.httpwant = CURL_HTTP_VERSION_3; + data->state.http_neg.allowed = CURL_HTTP_V3x; break; default: /* should not be possible */ break; diff --git a/lib/urldata.h b/lib/urldata.h index 6744129fa6..a77fdfc3d9 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -1313,10 +1313,9 @@ struct UrlState { char *proxypasswd; #endif } aptr; - unsigned char httpwant; /* when non-zero, a specific HTTP version requested - to be used in the library's request(s) */ - unsigned char httpversion; /* the lowest HTTP version*10 reported by any - server involved in this request */ +#ifndef CURL_DISABLE_HTTP + struct http_negotiation http_neg; +#endif unsigned char httpreq; /* Curl_HttpReq; what kind of HTTP request (if any) is this */ unsigned char select_bits; /* != 0 -> bitmask of socket events for this diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 9c59b4c97b..6297d82327 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -154,17 +154,19 @@ static const struct alpn_spec ALPN_SPEC_H2_H11 = { }; #endif -static const struct alpn_spec *alpn_get_spec(int httpwant, bool use_alpn) +static const struct alpn_spec * +alpn_get_spec(http_majors allowed, bool use_alpn) { if(!use_alpn) return NULL; #ifdef USE_HTTP2 - if(httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE) + if(allowed & CURL_HTTP_V2x) { + if(allowed & CURL_HTTP_V1x) + return &ALPN_SPEC_H2_H11; return &ALPN_SPEC_H2; - if(httpwant >= CURL_HTTP_VERSION_2) - return &ALPN_SPEC_H2_H11; + } #else - (void)httpwant; + (void)allowed; #endif /* Use the ALPN protocol "http/1.1" for HTTP/1.x. Avoid "http/1.0" because some servers do not support it. */ @@ -1576,7 +1578,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf, DEBUGASSERT(data->conn); - ctx = cf_ctx_new(data, alpn_get_spec(data->state.httpwant, + ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.allowed, conn->bits.tls_enable_alpn)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; @@ -1627,16 +1629,16 @@ static CURLcode cf_ssl_proxy_create(struct Curl_cfilter **pcf, struct ssl_connect_data *ctx; CURLcode result; bool use_alpn = conn->bits.tls_enable_alpn; - int httpwant = CURL_HTTP_VERSION_1_1; + http_majors allowed = CURL_HTTP_V1x; #ifdef USE_HTTP2 if(conn->http_proxy.proxytype == CURLPROXY_HTTPS2) { use_alpn = TRUE; - httpwant = CURL_HTTP_VERSION_2; + allowed = (CURL_HTTP_V1x|CURL_HTTP_V2x); } #endif - ctx = cf_ctx_new(data, alpn_get_spec(httpwant, use_alpn)); + ctx = cf_ctx_new(data, alpn_get_spec(allowed, use_alpn)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; goto out; diff --git a/lib/ws.c b/lib/ws.c index 5a52c84ade..23ee016229 100644 --- a/lib/ws.c +++ b/lib/ws.c @@ -1308,7 +1308,9 @@ static CURLcode ws_setup_conn(struct Curl_easy *data, struct connectdata *conn) { /* WebSockets is 1.1 only (for now) */ - data->state.httpwant = CURL_HTTP_VERSION_1_1; + data->state.http_neg.accept_09 = FALSE; + data->state.http_neg.only_10 = FALSE; + data->state.http_neg.allowed = CURL_HTTP_V1x; return Curl_http_setup_conn(data, conn); } diff --git a/tests/http/test_12_reuse.py b/tests/http/test_12_reuse.py index 18d88596b1..468fcfbb71 100644 --- a/tests/http/test_12_reuse.py +++ b/tests/http/test_12_reuse.py @@ -90,7 +90,7 @@ class TestReuse: curl = CurlClient(env=env) urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json?[0-{count-1}]' r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ - '--alt-svc', f'{asfile}', + '--alt-svc', f'{asfile}', '--http3', ]) r.check_response(count=count, http_status=200) # We expect the connection to be reused @@ -111,9 +111,9 @@ class TestReuse: fd.write(f'h3 {env.domain1} {env.https_port} h2 {env.domain1} {env.https_port} "{expires}" 0 0') log.info(f'altscv: {open(asfile).readlines()}') curl = CurlClient(env=env) - urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json?[0-{count-1}]' + urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json?[0-{count-1}]' r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ - '--alt-svc', f'{asfile}', + '--alt-svc', f'{asfile}', '--http3' ]) r.check_response(count=count, http_status=200) # We expect the connection to be reused and use HTTP/2 @@ -134,12 +134,35 @@ class TestReuse: fd.write(f'h3 {env.domain1} {env.https_port} http/1.1 {env.domain1} {env.https_port} "{expires}" 0 0') log.info(f'altscv: {open(asfile).readlines()}') curl = CurlClient(env=env) - urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json?[0-{count-1}]' + urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json?[0-{count-1}]' r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ - '--alt-svc', f'{asfile}', + '--alt-svc', f'{asfile}', '--http3' ]) r.check_response(count=count, http_status=200) # We expect the connection to be reused and use HTTP/1.1 assert r.total_connects == 1 for s in r.stats: assert s['http_version'] == '1.1', f'{s}' + + @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") + def test_12_06_alt_svc_h3h1_h3only(self, env: Env, httpd, nghttpx): + httpd.clear_extra_configs() + httpd.reload() + count = 2 + # write a alt-svc file the advises h1 instead of h3 + asfile = os.path.join(env.gen_dir, 'alt-svc-12_05.txt') + ts = datetime.now() + timedelta(hours=24) + expires = f'{ts.year:04}{ts.month:02}{ts.day:02} {ts.hour:02}:{ts.minute:02}:{ts.second:02}' + with open(asfile, 'w') as fd: + fd.write(f'h3 {env.domain1} {env.https_port} http/1.1 {env.domain1} {env.https_port} "{expires}" 0 0') + log.info(f'altscv: {open(asfile).readlines()}') + curl = CurlClient(env=env) + urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json?[0-{count-1}]' + r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ + '--alt-svc', f'{asfile}', '--http3-only' + ]) + r.check_response(count=count, http_status=200) + # We expect the connection to be stay on h3, since we used --http3-only + assert r.total_connects == 1 + for s in r.stats: + assert s['http_version'] == '3', f'{s}'