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;
if(strcasecompare(name, H3VERSION))
return ALPN_h3;
if(strcasecompare(name, "http/1.1"))
return ALPN_h1;
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;
/* 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) &&
(((conn->httpversion >= 20) &&
(data->state.httpwant < CURL_HTTP_VERSION_2_0))
|| ((conn->httpversion >= 30) &&
(data->state.httpwant < CURL_HTTP_VERSION_3))))
return FALSE;
(data->state.httpwant != CURL_HTTP_VERSION_2TLS)) {
if((conn->httpversion >= 20) &&
(data->state.httpwant < CURL_HTTP_VERSION_2_0)) {
DEBUGF(infof(data, "nor reusing conn #%" CURL_FORMAT_CURL_OFF_T
" 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
else if(get_protocol_family(needle->handler) & PROTO_FAMILY_SSH) {
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! */
enum alpnid srcalpnid;
bool hit;
bool hit = FALSE;
struct altsvc *as;
const int allowed_versions = ( ALPN_h1
#ifdef USE_HTTP2
@ -3026,24 +3038,27 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
| ALPN_h3
#endif
) & 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;
#ifdef USE_HTTP2
/* with h2 support, check that first */
srcalpnid = ALPN_h2;
hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
if(!hit)
#endif
{
srcalpnid = ALPN_h1;
DEBUGF(infof(data, "check Alt-Svc for host %s", host));
for(i = 0; !hit && (i < ARRAYSIZE(alpn_ids)); ++i) {
srcalpnid = alpn_ids[i];
hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
}
if(hit) {
char *hostd = strdup((char *)as->dst.host);
if(!hostd)

View File

@ -28,6 +28,7 @@ import difflib
import filecmp
import logging
import os
from datetime import datetime, timedelta
import pytest
from testenv import Env, CurlClient
@ -78,3 +79,72 @@ class TestReuse:
r.check_response(count=count, http_status=200)
# Connections time out on server before we send another request,
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}'