alt-svc: more flexibility on same destination
No binary (build failed)
—
(log yok)
| @@ -622,7 +622,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi, | |
| enum alpnid srcalpnid, const char *srchost, | |
| int srcport, | |
| struct altsvc **dstentry, | |
| - | const int versions) /* one or more bits */ |
| + | const int versions, /* one or more bits */ |
| + | bool *psame_destination) |
| { | |
| struct Curl_llist_node *e; | |
| struct Curl_llist_node *n; | |
| @@ -631,6 +632,7 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi, | |
| DEBUGASSERT(srchost); | |
| DEBUGASSERT(dstentry); | |
| + | *psame_destination = FALSE; |
| for(e = Curl_llist_head(&asi->list); e; e = n) { | |
| struct altsvc *as = Curl_node_elem(e); | |
| n = Curl_node_next(e); | |
| @@ -646,6 +648,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi, | |
| (versions & (int)as->dst.alpnid)) { | |
| /* match */ | |
| *dstentry = as; | |
| + | *psame_destination = (srcport == as->dst.port) && |
| + | hostcompare(srchost, as->dst.host); |
| return TRUE; | |
| } | |
| } | |
| @@ -65,7 +65,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi, | |
| enum alpnid srcalpnid, const char *srchost, | |
| int srcport, | |
| struct altsvc **dstentry, | |
| - | const int versions); /* CURLALTSVC_H* bits */ |
| + | const int versions, /* CURLALTSVC_H* bits */ |
| + | bool *psame_destination); |
| #else | |
| /* disabled */ | |
| #define Curl_altsvc_save(a, b, c) | |
| @@ -710,6 +710,31 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data, | |
| } | |
| #endif | |
| + | /* Add preferred HTTP version ALPN first */ |
| + | if(data->state.http_neg.preferred && |
| + | (alpn_count < CURL_ARRAYSIZE(alpn_ids)) && |
| + | (data->state.http_neg.preferred & data->state.http_neg.allowed)) { |
| + | enum alpnid alpn_pref = ALPN_none; |
| + | switch(data->state.http_neg.preferred) { |
| + | case CURL_HTTP_V3x: |
| + | if(!Curl_conn_may_http3(data, conn, conn->transport_wanted)) |
| + | alpn_pref = ALPN_h3; |
| + | break; |
| + | case CURL_HTTP_V2x: |
| + | alpn_pref = ALPN_h2; |
| + | break; |
| + | case CURL_HTTP_V1x: |
| + | alpn_pref = ALPN_h1; |
| + | break; |
| + | default: |
| + | break; |
| + | } |
| + | if(alpn_pref && |
| + | !cf_https_alpns_contain(alpn_pref, alpn_ids, alpn_count)) { |
| + | alpn_ids[alpn_count++] = alpn_pref; |
| + | } |
| + | } |
| + | |
| 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)) { | |
| @@ -72,6 +72,7 @@ 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 */ | |
| + | http_majors preferred; /* preferred major version 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 */ | |
| @@ -3060,6 +3060,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, | |
| struct altsvc *as = NULL; | |
| int allowed_alpns = ALPN_none; | |
| struct http_negotiation *neg = &data->state.http_neg; | |
| + | bool same_dest = FALSE; |
| DEBUGF(infof(data, "Alt-svc check wanted=%x, allowed=%x", | |
| neg->wanted, neg->allowed)); | |
| @@ -3083,7 +3084,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, | |
| hit = Curl_altsvc_lookup(data->asi, | |
| ALPN_h3, host, conn->remote_port, /* from */ | |
| &as /* to */, | |
| - | allowed_alpns); |
| + | allowed_alpns, &same_dest); |
| } | |
| #endif | |
| #ifdef USE_HTTP2 | |
| @@ -3093,7 +3094,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, | |
| hit = Curl_altsvc_lookup(data->asi, | |
| ALPN_h2, host, conn->remote_port, /* from */ | |
| &as /* to */, | |
| - | allowed_alpns); |
| + | allowed_alpns, &same_dest); |
| } | |
| #endif | |
| if(!hit && (neg->wanted & CURL_HTTP_V1x) && | |
| @@ -3102,10 +3103,29 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data, | |
| hit = Curl_altsvc_lookup(data->asi, | |
| ALPN_h1, host, conn->remote_port, /* from */ | |
| &as /* to */, | |
| - | allowed_alpns); |
| + | allowed_alpns, &same_dest); |
| } | |
| - | if(hit) { |
| + | if(hit && same_dest) { |
| + | /* same destination, but more HTTPS version options */ |
| + | switch(as->dst.alpnid) { |
| + | case ALPN_h1: |
| + | neg->wanted |= CURL_HTTP_V1x; |
| + | neg->preferred = CURL_HTTP_V1x; |
| + | break; |
| + | case ALPN_h2: |
| + | neg->wanted |= CURL_HTTP_V2x; |
| + | neg->preferred = CURL_HTTP_V2x; |
| + | break; |
| + | case ALPN_h3: |
| + | neg->wanted |= CURL_HTTP_V3x; |
| + | neg->preferred = CURL_HTTP_V3x; |
| + | break; |
| + | default: /* should not be possible */ |
| + | break; |
| + | } |
| + | } |
| + | else if(hit) { |
| char *hostd = curlx_strdup((char *)as->dst.host); | |
| if(!hostd) | |
| return CURLE_OUT_OF_MEMORY; | |
| @@ -145,22 +145,28 @@ static const struct alpn_spec ALPN_SPEC_H2 = { | |
| static const struct alpn_spec ALPN_SPEC_H2_H11 = { | |
| { ALPN_H2, ALPN_HTTP_1_1 }, 2 | |
| }; | |
| + | static const struct alpn_spec ALPN_SPEC_H11_H2 = { |
| + | { ALPN_HTTP_1_1, ALPN_H2 }, 2 |
| + | }; |
| #endif | |
| #if !defined(CURL_DISABLE_HTTP) || !defined(CURL_DISABLE_PROXY) | |
| - | static const struct alpn_spec *alpn_get_spec(http_majors allowed, |
| + | static const struct alpn_spec *alpn_get_spec(http_majors wanted, |
| + | http_majors preferred, |
| bool use_alpn) | |
| { | |
| if(!use_alpn) | |
| return NULL; | |
| #ifdef USE_HTTP2 | |
| - | if(allowed & CURL_HTTP_V2x) { |
| - | if(allowed & CURL_HTTP_V1x) |
| - | return &ALPN_SPEC_H2_H11; |
| + | if(wanted & CURL_HTTP_V2x) { |
| + | if(wanted & CURL_HTTP_V1x) |
| + | return (preferred == CURL_HTTP_V1x) ? |
| + | &ALPN_SPEC_H11_H2 : &ALPN_SPEC_H2_H11; |
| return &ALPN_SPEC_H2; | |
| } | |
| #else | |
| - | (void)allowed; |
| + | (void)wanted; |
| + | (void)preferred; |
| #endif | |
| /* Use the ALPN protocol "http/1.1" for HTTP/1.x. | |
| Avoid "http/1.0" because some servers do not support it. */ | |
| @@ -1718,6 +1724,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf, | |
| ctx = cf_ctx_new(data, NULL); | |
| #else | |
| ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.wanted, | |
| + | data->state.http_neg.preferred, |
| conn->bits.tls_enable_alpn)); | |
| #endif | |
| if(!ctx) { | |
| @@ -1770,17 +1777,17 @@ static CURLcode cf_ssl_proxy_create(struct Curl_cfilter **pcf, | |
| CURLcode result; | |
| /* ALPN is default, but if user explicitly disables it, obey */ | |
| bool use_alpn = data->set.ssl_enable_alpn; | |
| - | http_majors allowed = CURL_HTTP_V1x; |
| + | http_majors wanted = CURL_HTTP_V1x; |
| (void)conn; | |
| #ifdef USE_HTTP2 | |
| if(conn->http_proxy.proxytype == CURLPROXY_HTTPS2) { | |
| use_alpn = TRUE; | |
| - | allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x); |
| + | wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x); |
| } | |
| #endif | |
| - | ctx = cf_ctx_new(data, alpn_get_spec(allowed, use_alpn)); |
| + | ctx = cf_ctx_new(data, alpn_get_spec(wanted, 0, use_alpn)); |
| if(!ctx) { | |
| result = CURLE_OUT_OF_MEMORY; | |
| goto out; | |
| @@ -133,3 +133,19 @@ class TestEyeballs: | |
| he_timers_set = [line for line in r.trace_lines | |
| if re.match(r'.*\[TIMER] \[HAPPY_EYEBALLS] set for', line)] | |
| assert len(he_timers_set) == 2, f'found: {"".join(he_timers_set)}\n{r.dump_logs()}' | |
| + | |
| + | # download using HTTP/3 on missing server with alt-svc pointing there |
| + | @pytest.mark.skipif(condition=not Env.have_h3(), reason="missing HTTP/3 support") |
| + | def test_06_20_h2_altsvc_h3_fallback(self, env: Env, httpd, nghttpx): |
| + | curl = CurlClient(env=env) |
| + | urln = f'https://{env.domain1}:{env.https_only_tcp_port}/data.json' |
| + | altsvc_file = curl.mk_altsvc_file('test_06', |
| + | 'h2', env.domain1, env.https_only_tcp_port, |
| + | 'h3', env.domain1, env.https_only_tcp_port) |
| + | r = curl.http_download(urls=[urln], extra_args=[ |
| + | '--alt-svc', altsvc_file |
| + | ]) |
| + | # Should try a QUIC connection that fails and fallback to h2 |
| + | r.check_exit_code(0) |
| + | r.check_response(count=1, http_status=200) |
| + | assert r.stats[0]['http_version'] == '2' |
| @@ -26,6 +26,7 @@ | |
| # | |
| import logging | |
| import os | |
| + | import re |
| from datetime import datetime, timedelta | |
| import pytest | |
| @@ -77,10 +78,11 @@ class TestReuse: | |
| @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") | |
| def test_12_03_as_follow_h2h3(self, env: Env, httpd, configures_httpd, nghttpx): | |
| - | # write an 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) | |
| + | # write an alt-svc file that advises h3 instead of h2 |
| + | asfile = curl.mk_altsvc_file('test_12', |
| + | 'h2', env.domain1, env.https_port, |
| + | 'h3', env.domain1, env.h3_port) |
| 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}', | |
| @@ -112,54 +114,47 @@ class TestReuse: | |
| @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") | |
| def test_12_05_as_follow_h3h1(self, env: Env, httpd, configures_httpd, nghttpx): | |
| # With '--http3` an Alt-Svc redirection from h3 to h1 is allowed | |
| - | count = 2 |
| # write an 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}]' |
| + | asfile = curl.mk_altsvc_file('test_12', |
| + | 'h3', env.domain1, env.https_port, |
| + | 'http/1.1', env.domain1, env.https_port) |
| + | urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json' |
| r = curl.http_download(urls=[urln], with_stats=True, extra_args=[ | |
| '--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 |
| + | r.check_response(count=1, http_status=200) |
| + | # We expect the connection to be preferring HTTP/1.1 in the ALPN |
| assert r.total_connects == 1 | |
| - | for s in r.stats: |
| - | assert s['http_version'] == '1.1', f'{s}' |
| + | re_m = re.compile(r'.* ALPN: curl offers http/1.1,h2') |
| + | lines = [line for line in r.trace_lines if re_m.match(line)] |
| + | assert len(lines), f'{r.dump_logs()}' |
| @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported") | |
| def test_12_06_as_ignore_h3h1(self, env: Env, httpd, configures_httpd, nghttpx): | |
| # With '--http3-only` an Alt-Svc redirection from h3 to h1 is ignored | |
| - | count = 2 |
| # write an 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}]' |
| + | asfile = curl.mk_altsvc_file('test_12', |
| + | 'h3', env.domain1, env.https_port, |
| + | 'http/1.1', env.domain1, env.https_port) |
| + | urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json' |
| 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) |
| + | r.check_response(count=1, 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}' |
| + | 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_07_as_ignore_h2h3(self, env: Env, httpd, configures_httpd, nghttpx): | |
| # With '--http2` an Alt-Svc redirection from h2 to h3 is ignored | |
| # write an 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) | |
| + | asfile = curl.mk_altsvc_file('test_12', |
| + | 'h2', env.domain1, env.https_port, |
| + | 'h3', env.domain1, env.h3_port) |
| 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' | |
| @@ -167,9 +162,17 @@ class TestReuse: | |
| 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()}') |
| + | # download using HTTP/3 on available server with alt-svc to h2, use h2 |
| + | @pytest.mark.skipif(condition=not Env.have_h3(), reason="missing HTTP/3 support") |
| + | def test_12_08_h3_altsvc_h2_used(self, env: Env, httpd, nghttpx): |
| + | curl = CurlClient(env=env) |
| + | urln = f'https://{env.domain1}:{env.https_port}/data.json' |
| + | altsvc_file = curl.mk_altsvc_file('test_12', |
| + | 'h3', env.domain1, env.https_port, |
| + | 'h2', env.domain1, env.https_port) |
| + | r = curl.http_download(urls=[urln], extra_args=[ |
| + | '--http3', '--alt-svc', altsvc_file |
| + | ]) |
| + | r.check_exit_code(0) |
| + | r.check_response(count=1, http_status=200) |
| + | assert r.stats[0]['http_version'] == '2' |
| @@ -36,7 +36,7 @@ import re | |
| import shutil | |
| import subprocess | |
| from statistics import mean, fmean | |
| - | from datetime import timedelta, datetime |
| + | from datetime import timedelta, datetime, timezone |
| from typing import List, Optional, Dict, Union, Any | |
| from urllib.parse import urlparse | |
| @@ -1211,3 +1211,12 @@ class CurlClient: | |
| rc = p.returncode | |
| if rc != 0: | |
| raise Exception(f'{fg_gen_flame} returned error {rc}') | |
| + | |
| + | def mk_altsvc_file(self, name, src_alpn, src_host, src_port, |
| + | dest_alpn, dest_host, dest_port): |
| + | fpath = os.path.join(self.run_dir, f'{name}.altsvc') |
| + | ts = datetime.now(timezone.utc) + timedelta(hours=1) |
| + | ts = ts.strftime('%Y%m%d %H:%M:%S') |
| + | with open(fpath, 'w') as fd: |
| + | fd.write(f'{src_alpn} {src_host} {src_port} {dest_alpn} {dest_host} {dest_port} "{ts}" 1 0\n') |
| + | return fpath |