From: Stefan Eissing Date: Tue, 28 Jan 2025 13:11:59 +0000 (+0100) Subject: http: negotiation and room for alt-svc/https rr to navigate X-Git-Tag: curl-8_13_0~417 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=279a4772ae67dd4d9770e11e60040f9113b1c345;p=thirdparty%2Fcurl.git http: negotiation and room for alt-svc/https rr to navigate Add a 'wanted' major HTTP version bitmask next to the 'allowed' bitmask in HTTP version negotiation. This will try connections as specified in 'wanted', but enabled Alt-Svc and HTTPS-RR to redirect to other major HTTP versions, if those are 'allowed'. Changes libcurl internal default to `CURL_HTTP_VERSION_NONE` and removes the code in curl that sets `CURL_HTTP_VERSION_2TLS` if the command line does not say anything else. Closes #16117 --- diff --git a/lib/cf-https-connect.c b/lib/cf-https-connect.c index 812d8b1d91..7b14a01abe 100644 --- a/lib/cf-https-connect.c +++ b/lib/cf-https-connect.c @@ -192,7 +192,7 @@ static void cf_hc_reset(struct Curl_cfilter *cf, struct Curl_easy *data) ctx->state = CF_HC_INIT; ctx->result = CURLE_OK; ctx->hard_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout; - ctx->soft_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout / 2; + ctx->soft_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout / 4; } } @@ -263,8 +263,8 @@ static bool time_to_start_next(struct Curl_cfilter *cf, break; } if(i == idx) { - CURL_TRC_CF(data, cf, "all previous ballers have failed, time to start " - "baller %zu [%s]", idx, ctx->ballers[idx].name); + CURL_TRC_CF(data, cf, "all previous attempts failed, starting %s", + ctx->ballers[idx].name); return TRUE; } elapsed_ms = Curl_timediff(now, ctx->started); @@ -315,7 +315,7 @@ static CURLcode cf_hc_connect(struct Curl_cfilter *cf, cf_hc_baller_init(&ctx->ballers[0], cf, data, cf->conn->transport); if(ctx->baller_count > 1) { Curl_expire(data, ctx->soft_eyeballs_timeout_ms, EXPIRE_ALPN_EYEBALLS); - CURL_TRC_CF(data, cf, "set expire for starting next baller in %ums", + CURL_TRC_CF(data, cf, "set next attempt to start in %ums", ctx->soft_eyeballs_timeout_ms); } ctx->state = CF_HC_CONNECT; @@ -351,7 +351,7 @@ static CURLcode cf_hc_connect(struct Curl_cfilter *cf, if(failed_ballers == ctx->baller_count) { /* all have failed. we give up */ - CURL_TRC_CF(data, cf, "connect, all failed"); + CURL_TRC_CF(data, cf, "connect, all attempts failed"); for(i = 0; i < ctx->baller_count; i++) { if(ctx->ballers[i].result) { result = ctx->ballers[i].result; @@ -450,7 +450,6 @@ static bool cf_hc_data_pending(struct Curl_cfilter *cf, if(cf->connected) return cf->next->cft->has_data_pending(cf->next, data); - CURL_TRC_CF((struct Curl_easy *)data, cf, "data_pending"); for(i = 0; i < ctx->baller_count; i++) if(cf_hc_baller_data_pending(&ctx->ballers[i], data)) return TRUE; @@ -606,8 +605,6 @@ static CURLcode cf_hc_create(struct Curl_cfilter **pcf, ctx->baller_count = alpn_count; result = Curl_cf_create(&cf, &Curl_cft_http_connect, ctx); - CURL_TRC_CF(data, cf, "created with %zu ALPNs -> %d", - ctx->baller_count, result); if(result) goto out; ctx = NULL; @@ -637,6 +634,17 @@ out: return result; } +static bool cf_https_alpns_contain(enum alpnid id, + enum alpnid *list, size_t len) +{ + size_t i; + for(i = 0; i < len; ++i) { + if(id == list[i]) + return TRUE; + } + return FALSE; +} + CURLcode Curl_cf_https_setup(struct Curl_easy *data, struct connectdata *conn, int sockindex, @@ -645,41 +653,55 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data, enum alpnid alpn_ids[2]; size_t alpn_count = 0; CURLcode result = CURLE_OK; + struct Curl_cfilter cf_fake, *cf = NULL; (void)sockindex; (void)remotehost; + /* we want to log for the filter before we create it, fake it. */ + memset(&cf_fake, 0, sizeof(cf_fake)); + cf_fake.cft = &Curl_cft_http_connect; + cf = &cf_fake; if(conn->bits.tls_enable_alpn) { #ifdef USE_HTTPSRR + /* Is there a HTTPSRR and if so, do its ALPNs it apply 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. */ if(conn->dns_entry && conn->dns_entry->hinfo && - !conn->dns_entry->hinfo->no_def_alpn) { - size_t i, j; + !conn->dns_entry->hinfo->no_def_alpn && /* ALPNs are defaults */ + (!conn->dns_entry->hinfo->target || /* for same host */ + !conn->dns_entry->hinfo->target[0] || + (conn->dns_entry->hinfo->target[0] == '.' && + !conn->dns_entry->hinfo->target[0])) && + (conn->dns_entry->hinfo->port < 0 || /* for same port */ + conn->dns_entry->hinfo->port == conn->remote_port)) { + size_t i; 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) + if(cf_https_alpns_contain(alpn, alpn_ids, alpn_count)) 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) + if(data->state.http_neg.allowed & CURL_HTTP_V3x) { + CURL_TRC_CF(data, cf, "adding h3 via HTTPS-RR"); alpn_ids[alpn_count++] = alpn; + } break; case ALPN_h2: - if(data->state.http_neg.allowed & CURL_HTTP_V2x) + if(data->state.http_neg.allowed & CURL_HTTP_V2x) { + CURL_TRC_CF(data, cf, "adding h2 via HTTPS-RR"); alpn_ids[alpn_count++] = alpn; + } break; case ALPN_h1: - if(data->state.http_neg.allowed & CURL_HTTP_V1x) + if(data->state.http_neg.allowed & CURL_HTTP_V1x) { + CURL_TRC_CF(data, cf, "adding h1 via HTTPS-RR"); alpn_ids[alpn_count++] = alpn; + } break; default: /* ignore */ break; @@ -688,18 +710,28 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data, } #endif - 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((alpn_count < CURL_ARRAYSIZE(alpn_ids)) && + (data->state.http_neg.wanted & CURL_HTTP_V3x) && + !cf_https_alpns_contain(ALPN_h3, alpn_ids, alpn_count)) { + result = Curl_conn_may_http3(data, conn); + if(!result) { + CURL_TRC_CF(data, cf, "adding wanted h3"); + alpn_ids[alpn_count++] = ALPN_h3; } - if(data->state.http_neg.allowed & CURL_HTTP_V2x) - alpn_ids[alpn_count++] = ALPN_h2; - else if(data->state.http_neg.allowed & CURL_HTTP_V1x) - alpn_ids[alpn_count++] = ALPN_h1; + else if(data->state.http_neg.wanted == CURL_HTTP_V3x) + goto out; /* only h3 allowed, not possible, error out */ + } + if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) && + (data->state.http_neg.wanted & CURL_HTTP_V2x) && + !cf_https_alpns_contain(ALPN_h2, alpn_ids, alpn_count)) { + CURL_TRC_CF(data, cf, "adding wanted h2"); + alpn_ids[alpn_count++] = ALPN_h2; + } + else if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) && + (data->state.http_neg.wanted & CURL_HTTP_V1x) && + !cf_https_alpns_contain(ALPN_h1, alpn_ids, alpn_count)) { + CURL_TRC_CF(data, cf, "adding wanted h1"); + alpn_ids[alpn_count++] = ALPN_h1; } } diff --git a/lib/http.c b/lib/http.c index 52bb368d5a..21510a34a7 100644 --- a/lib/http.c +++ b/lib/http.c @@ -193,31 +193,33 @@ void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg) neg->accept_09 = data->set.http09_allowed; switch(data->set.httpwant) { case CURL_HTTP_VERSION_1_0: - neg->allowed = (CURL_HTTP_V1x); + neg->wanted = neg->allowed = (CURL_HTTP_V1x); neg->only_10 = TRUE; break; case CURL_HTTP_VERSION_1_1: - neg->allowed = (CURL_HTTP_V1x); + neg->wanted = neg->allowed = (CURL_HTTP_V1x); break; case CURL_HTTP_VERSION_2_0: - neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x); + neg->wanted = 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); + neg->wanted = neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x); break; case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE: - neg->allowed = (CURL_HTTP_V2x); + neg->wanted = 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); + neg->wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x); + neg->allowed = neg->wanted; break; case CURL_HTTP_VERSION_3ONLY: - neg->allowed = (CURL_HTTP_V3x); + neg->wanted = neg->allowed = (CURL_HTTP_V3x); break; case CURL_HTTP_VERSION_NONE: default: + neg->wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x); neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x); break; } @@ -229,7 +231,7 @@ CURLcode Curl_http_setup_conn(struct Curl_easy *data, /* allocate the HTTP-specific struct for the Curl_easy, only to survive during this request */ connkeep(conn, "HTTP default"); - if(data->state.http_neg.allowed == CURL_HTTP_V3x) { + if(data->state.http_neg.wanted == CURL_HTTP_V3x) { /* only HTTP/3, needs to work */ CURLcode result = Curl_conn_may_http3(data, conn); if(result) @@ -573,6 +575,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.http_neg.wanted = CURL_HTTP_V1x; data->state.http_neg.allowed = CURL_HTTP_V1x; } } @@ -2849,7 +2852,7 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done) } if(!Curl_conn_is_ssl(conn, FIRSTSOCKET) && (httpversion < 20) && - (data->state.http_neg.allowed & CURL_HTTP_V2x) && + (data->state.http_neg.wanted & 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 */ diff --git a/lib/http.h b/lib/http.h index 61237b3bf8..c1bd457470 100644 --- a/lib/http.h +++ b/lib/http.h @@ -76,6 +76,7 @@ struct dynhds; struct http_negotiation { unsigned char rcvd_min; /* minimum version seen in responses, 09, 10, 11 */ + http_majors wanted; /* wanted major versions when talking to server */ 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 */ diff --git a/lib/http2.c b/lib/http2.c index cd7512871e..d809031cff 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -2795,7 +2795,7 @@ out: bool Curl_http2_may_switch(struct Curl_easy *data) { if(Curl_conn_http_version(data, data->conn) < 20 && - (data->state.http_neg.allowed & CURL_HTTP_V2x) && + (data->state.http_neg.wanted & CURL_HTTP_V2x) && data->state.http_neg.h2_prior_knowledge) { #ifndef CURL_DISABLE_PROXY if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy) { diff --git a/lib/multi.c b/lib/multi.c index 449420dc86..c232f69544 100644 --- a/lib/multi.c +++ b/lib/multi.c @@ -1918,6 +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.http_neg.wanted = CURL_HTTP_V1x; 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; diff --git a/lib/setopt.c b/lib/setopt.c index 8a87d24446..a6be0c7706 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -600,11 +600,6 @@ static CURLcode setopt_long(struct Curl_easy *data, CURLoption option, */ switch(arg) { case CURL_HTTP_VERSION_NONE: -#ifdef USE_HTTP2 - /* This seems an undesirable quirk to force a behaviour on lower - * implementations that they should recognize independently? */ - arg = CURL_HTTP_VERSION_2TLS; -#endif /* accepted */ break; case CURL_HTTP_VERSION_1_0: diff --git a/lib/url.c b/lib/url.c index b348834c22..4efebc9563 100644 --- a/lib/url.c +++ b/lib/url.c @@ -473,11 +473,7 @@ CURLcode Curl_init_userdefined(struct Curl_easy *data) set->maxage_conn = 118; set->maxlifetime_conn = 0; set->http09_allowed = FALSE; -#ifdef USE_HTTP2 - set->httpwant = CURL_HTTP_VERSION_2TLS -#else - set->httpwant = CURL_HTTP_VERSION_1_1 -#endif + set->httpwant = CURL_HTTP_VERSION_NONE ; #if defined(USE_HTTP2) || defined(USE_HTTP3) memset(&set->priority, 0, sizeof(set->priority)); @@ -1034,9 +1030,7 @@ static bool url_match_conn(struct connectdata *conn, void *userdata) #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. */ + * the HTTP version of conn, continue looking. */ if((needle->handler->protocol & PROTO_FAMILY_HTTP)) { switch(Curl_conn_http_version(data, conn)) { case 30: @@ -3074,44 +3068,47 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, enum alpnid srcalpnid = ALPN_none; bool hit = FALSE; struct altsvc *as = NULL; - int allowed_versions = ALPN_none; - - 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; + int allowed_alpns = ALPN_none; + struct http_negotiation *neg = &data->state.http_neg; + + DEBUGF(infof(data, "Alt-svc check wanted=%x, allowed=%x", + neg->wanted, neg->allowed)); + if(neg->allowed & CURL_HTTP_V3x) + allowed_alpns |= ALPN_h3; + if(neg->allowed & CURL_HTTP_V2x) + allowed_alpns |= ALPN_h2; + if(neg->allowed & CURL_HTTP_V1x) + allowed_alpns |= ALPN_h1; + allowed_alpns &= (int)data->asi->flags; host = conn->host.rawalloc; DEBUGF(infof(data, "check Alt-Svc for host %s", host)); #ifdef USE_HTTP3 - if(!hit && (allowed_versions & ALPN_h3)) { + if(!hit && (neg->wanted & CURL_HTTP_V3x)) { srcalpnid = ALPN_h3; hit = Curl_altsvc_lookup(data->asi, ALPN_h3, host, conn->remote_port, /* from */ &as /* to */, - allowed_versions); + allowed_alpns); } #endif #ifdef USE_HTTP2 - if(!hit && (allowed_versions & ALPN_h2) && - !data->state.http_neg.h2_prior_knowledge) { + if(!hit && (neg->wanted & CURL_HTTP_V2x) && + !neg->h2_prior_knowledge) { srcalpnid = ALPN_h2; hit = Curl_altsvc_lookup(data->asi, ALPN_h2, host, conn->remote_port, /* from */ &as /* to */, - allowed_versions); + allowed_alpns); } #endif - if(!hit && (allowed_versions & ALPN_h1) && - !data->state.http_neg.only_10) { + if(!hit && (neg->wanted & CURL_HTTP_V1x) && + !neg->only_10) { srcalpnid = ALPN_h1; hit = Curl_altsvc_lookup(data->asi, ALPN_h1, host, conn->remote_port, /* from */ &as /* to */, - allowed_versions); + allowed_alpns); } if(hit) { @@ -3131,15 +3128,15 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, /* protocol version switch */ switch(as->dst.alpnid) { case ALPN_h1: - data->state.http_neg.allowed = CURL_HTTP_V1x; - data->state.http_neg.only_10 = FALSE; + neg->wanted = neg->allowed = CURL_HTTP_V1x; + neg->only_10 = FALSE; break; case ALPN_h2: - data->state.http_neg.allowed = CURL_HTTP_V2x; + neg->wanted = neg->allowed = CURL_HTTP_V2x; break; case ALPN_h3: conn->transport = TRNSPRT_QUIC; - data->state.http_neg.allowed = CURL_HTTP_V3x; + neg->wanted = neg->allowed = CURL_HTTP_V3x; break; default: /* should not be possible */ break; diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index a6587c072e..ae4afbf74d 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -1541,7 +1541,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf, DEBUGASSERT(data->conn); - ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.allowed, + ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.wanted, conn->bits.tls_enable_alpn)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; diff --git a/lib/ws.c b/lib/ws.c index 9510622683..2ce0b48455 100644 --- a/lib/ws.c +++ b/lib/ws.c @@ -1310,6 +1310,7 @@ static CURLcode ws_setup_conn(struct Curl_easy *data, /* WebSockets is 1.1 only (for now) */ data->state.http_neg.accept_09 = FALSE; data->state.http_neg.only_10 = FALSE; + data->state.http_neg.wanted = CURL_HTTP_V1x; data->state.http_neg.allowed = CURL_HTTP_V1x; return Curl_http_setup_conn(data, conn); } diff --git a/src/tool_operate.c b/src/tool_operate.c index 20a8d87b1a..54cc47cd83 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -1087,8 +1087,6 @@ static CURLcode config2setopts(struct GlobalConfig *global, if(config->httpversion) my_setopt_enum(curl, CURLOPT_HTTP_VERSION, config->httpversion); - else if(feature_http2) - my_setopt_enum(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); /* curl 7.19.1 (the 301 version existed in 7.18.2), 303 was added in 7.26.0 */ diff --git a/tests/http/test_12_reuse.py b/tests/http/test_12_reuse.py index 468fcfbb71..747e31eb58 100644 --- a/tests/http/test_12_reuse.py +++ b/tests/http/test_12_reuse.py @@ -76,30 +76,24 @@ class TestReuse: assert r.total_connects == count @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") - def test_12_03_alt_svc_h2h3(self, env: Env, httpd, nghttpx): + def test_12_03_as_follow_h2h3(self, env: Env, httpd, nghttpx): + # Without '--http*` an Alt-Svc redirection from h2 to h3 is allowed httpd.clear_extra_configs() httpd.reload() - count = 2 - # write a alt-svc file the advises h3 instead of h2 + # write a alt-svc file that advises h3 instead of h2 asfile = os.path.join(env.gen_dir, 'alt-svc-12_03.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'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.https_port} "{expires}" 0 0') - log.info(f'altscv: {open(asfile).readlines()}') + self.create_asfile(asfile, f'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.h3_port}') 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, "h2")}/data.json' r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ - '--alt-svc', f'{asfile}', '--http3', + '--alt-svc', f'{asfile}', ]) - r.check_response(count=count, http_status=200) - # We expect the connection to be reused - assert r.total_connects == 1 - for s in r.stats: - assert s['http_version'] == '3', f'{s}' + r.check_response(count=1, http_status=200) + assert r.stats[0]['http_version'] == '3', f'{r.stats}' @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") - def test_12_04_alt_svc_h3h2(self, env: Env, httpd, nghttpx): + def test_12_04_as_follow_h3h2(self, env: Env, httpd, nghttpx): + # With '--http3` an Alt-Svc redirection from h3 to h2 is allowed httpd.clear_extra_configs() httpd.reload() count = 2 @@ -122,7 +116,8 @@ class TestReuse: assert s['http_version'] == '2', f'{s}' @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") - def test_12_05_alt_svc_h3h1(self, env: Env, httpd, nghttpx): + def test_12_05_as_follow_h3h1(self, env: Env, httpd, nghttpx): + # With '--http3` an Alt-Svc redirection from h3 to h1 is allowed httpd.clear_extra_configs() httpd.reload() count = 2 @@ -145,7 +140,8 @@ class TestReuse: 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): + def test_12_06_as_ignore_h3h1(self, env: Env, httpd, nghttpx): + # With '--http3-only` an Alt-Svc redirection from h3 to h1 is ignored httpd.clear_extra_configs() httpd.reload() count = 2 @@ -166,3 +162,26 @@ class TestReuse: assert r.total_connects == 1 for s in r.stats: assert s['http_version'] == '3', f'{s}' + + @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") + def test_12_07_as_ignore_h2h3(self, env: Env, httpd, nghttpx): + # With '--http2` an Alt-Svc redirection from h2 to h3 is ignored + httpd.clear_extra_configs() + httpd.reload() + # write a alt-svc file that advises h3 instead of h2 + asfile = os.path.join(env.gen_dir, 'alt-svc-12_03.txt') + self.create_asfile(asfile, f'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.h3_port}') + curl = CurlClient(env=env) + urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json' + r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ + '--alt-svc', f'{asfile}', '--http2' + ]) + r.check_response(count=1, http_status=200) + assert r.stats[0]['http_version'] == '2', f'{r.stats}' + + def create_asfile(self, fpath, line): + 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(fpath, 'w') as fd: + fd.write(f'{line} "{expires}" 0 0') + log.info(f'altscv: {open(fpath).readlines()}')