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;
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);
(versions & (int)as->dst.alpnid)) {
/* match */
*dstentry = as;
+ *psame_destination = (srcport == as->dst.port) &&
+ hostcompare(srchost, as->dst.host);
return TRUE;
}
}
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)
}
#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)) {
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 */
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));
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
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) &&
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;
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. */
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) {
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;
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'
#
import logging
import os
+import re
from datetime import datetime, timedelta
import pytest
@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}',
@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'
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'
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
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