From: Stefan Eissing Date: Thu, 19 Sep 2024 09:47:29 +0000 (+0200) Subject: url: connection reuse on h3 connections X-Git-Tag: curl-8_11_0~394 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=433d73033e325f7d0f37cecc2ea7fac6b05bcd45;p=thirdparty%2Fcurl.git 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 --- diff --git a/lib/altsvc.c b/lib/altsvc.c index dcedc491c5..cd41fc0014 100644 --- a/lib/altsvc.c +++ b/lib/altsvc.c @@ -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 */ } diff --git a/lib/url.c b/lib/url.c index 8cfe01bec4..93fbdb92a3 100644 --- a/lib/url.c +++ b/lib/url.c @@ -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) diff --git a/tests/http/test_12_reuse.py b/tests/http/test_12_reuse.py index 5896e19619..c04eef70e5 100644 --- a/tests/http/test_12_reuse.py +++ b/tests/http/test_12_reuse.py @@ -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}'