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;
}
}
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);
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;
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;
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;
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;
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,
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;
}
#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;
}
}
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;
}
/* 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)
(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;
}
}
}
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 */
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 */
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) {
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;
*/
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:
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));
#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:
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) {
/* 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;
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;
/* 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);
}
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 */
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
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
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
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()}')