url: connection reuse on h3 connections

- When searching for existing connections, interpret the
  default CURL_HTTP_VERSION_2TLS as "anything goes". This
  will allow us to reuse HTTP/3 connections better
- add 'http/1.1' as allowed protocol identifier in Alt-Svc
  files
- add test_02_0[345] for testing protocol selection on
  provided alt-svc files

Fixes #14890
Reported-by: MacKenzie
Closes #14966
This commit is contained in:
Stefan Eissing 2024-09-19 11:47:29 +02:00 committed by Daniel Stenberg
parent c91c37b6e8
commit 433d73033e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
3 changed files with 105 additions and 18 deletions

View file

@ -64,6 +64,8 @@ static enum alpnid alpn2alpnid(char *name)
return ALPN_h2; return ALPN_h2;
if(strcasecompare(name, H3VERSION)) if(strcasecompare(name, H3VERSION))
return ALPN_h3; return ALPN_h3;
if(strcasecompare(name, "http/1.1"))
return ALPN_h1;
return ALPN_none; /* unknown, probably rubbish input */ return ALPN_none; /* unknown, probably rubbish input */
} }

View file

@ -1031,13 +1031,25 @@ static bool url_match_conn(struct connectdata *conn, void *userdata)
return FALSE; return FALSE;
/* If looking for HTTP and the HTTP version we want is less /* If looking for HTTP and the HTTP version we want is less
* than the HTTP version of conn, continue looking */ * than 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) && if((needle->handler->protocol & PROTO_FAMILY_HTTP) &&
(((conn->httpversion >= 20) && (data->state.httpwant != CURL_HTTP_VERSION_2TLS)) {
(data->state.httpwant < CURL_HTTP_VERSION_2_0)) if((conn->httpversion >= 20) &&
|| ((conn->httpversion >= 30) && (data->state.httpwant < CURL_HTTP_VERSION_2_0)) {
(data->state.httpwant < CURL_HTTP_VERSION_3)))) DEBUGF(infof(data, "nor reusing conn #%" CURL_FORMAT_CURL_OFF_T
return FALSE; " with httpversion=%d, we want a version less than h2",
conn->connection_id, conn->httpversion));
}
if((conn->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, conn->httpversion));
return FALSE;
}
}
#ifdef USE_SSH #ifdef USE_SSH
else if(get_protocol_family(needle->handler) & PROTO_FAMILY_SSH) { else if(get_protocol_family(needle->handler) & PROTO_FAMILY_SSH) {
if(!ssh_config_matches(needle, conn)) if(!ssh_config_matches(needle, conn))
@ -3016,7 +3028,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
)) { )) {
/* no connect_to match, try alt-svc! */ /* no connect_to match, try alt-svc! */
enum alpnid srcalpnid; enum alpnid srcalpnid;
bool hit; bool hit = FALSE;
struct altsvc *as; struct altsvc *as;
const int allowed_versions = ( ALPN_h1 const int allowed_versions = ( ALPN_h1
#ifdef USE_HTTP2 #ifdef USE_HTTP2
@ -3026,24 +3038,27 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
| ALPN_h3 | ALPN_h3
#endif #endif
) & data->asi->flags; ) & data->asi->flags;
static int alpn_ids[] = {
#ifdef USE_HTTP3
ALPN_h3,
#endif
#ifdef USE_HTTP2
ALPN_h2,
#endif
ALPN_h1,
};
size_t i;
host = conn->host.rawalloc; host = conn->host.rawalloc;
#ifdef USE_HTTP2 DEBUGF(infof(data, "check Alt-Svc for host %s", host));
/* with h2 support, check that first */ for(i = 0; !hit && (i < ARRAYSIZE(alpn_ids)); ++i) {
srcalpnid = ALPN_h2; srcalpnid = alpn_ids[i];
hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
if(!hit)
#endif
{
srcalpnid = ALPN_h1;
hit = Curl_altsvc_lookup(data->asi, hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */ srcalpnid, host, conn->remote_port, /* from */
&as /* to */, &as /* to */,
allowed_versions); allowed_versions);
} }
if(hit) { if(hit) {
char *hostd = strdup((char *)as->dst.host); char *hostd = strdup((char *)as->dst.host);
if(!hostd) if(!hostd)

View file

@ -28,6 +28,7 @@ import difflib
import filecmp import filecmp
import logging import logging
import os import os
from datetime import datetime, timedelta
import pytest import pytest
from testenv import Env, CurlClient from testenv import Env, CurlClient
@ -78,3 +79,72 @@ class TestReuse:
r.check_response(count=count, http_status=200) r.check_response(count=count, http_status=200)
# Connections time out on server before we send another request, # Connections time out on server before we send another request,
assert r.total_connects == count 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):
httpd.clear_extra_configs()
httpd.reload()
count = 2
# write a alt-svc file the 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()}')
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}',
])
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}'
def test_12_04_alt_svc_h3h2(self, env: Env, httpd, nghttpx):
httpd.clear_extra_configs()
httpd.reload()
count = 2
# write a alt-svc file the advises h2 instead of h3
asfile = os.path.join(env.gen_dir, 'alt-svc-12_04.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} 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}]'
r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
'--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'] == '2', f'{s}'
def test_12_05_alt_svc_h3h1(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, "h2")}/data.json?[0-{count-1}]'
r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
'--alt-svc', f'{asfile}',
])
r.check_response(count=count, http_status=200)
# We expect the connection to be reused
assert r.total_connects == 1
# When using http/1.1 from alt-svc, we ALPN-negotiate 'h2,http/1.1' anyway
# which means our server gives us h2
for s in r.stats:
assert s['http_version'] == '2', f'{s}'