http: version negotiation

Translate the `data->set.httpwant` which is one of the consts from the
public API (CURL_HTTP_VERSION_*) into a major version mask plus
additional flags for internal handling.

`Curl_http_neg_init()` does the translation and flags setting in http.c,
using new internal consts CURL_HTTP_V1x, CURL_HTTP_V2x and CURL_HTTP_V3x
for the major versions. The flags are

- only_10: when the application explicity asked fro HTTP/1.0
- h2_upgrade: when the application asks for upgrading 1.1 to 2.
- h2_prior_knowledge: when directly talking h2 without ALPN
- accept_09: when a HTTP/0.9 response is acceptable.

The Alt-Svc and HTTPS RR redirections from one ALPN to another obey the
allowed major versions. If a transfer has only h3 enabled, Alt-Svc
redirection to h2 is ignored.

This is the current implementation. It can be debated if Alt-Svc should
be able to override the allowed major versions. Added test_12_06 to
verify the current restriction.

Closes #16100
This commit is contained in:
Stefan Eissing 2025-01-27 15:39:13 +01:00 committed by Daniel Stenberg
parent f7fcbb8127
commit db72b8d4d0
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
13 changed files with 255 additions and 190 deletions

View File

@ -651,67 +651,56 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data,
(void)remotehost;
if(conn->bits.tls_enable_alpn) {
switch(data->state.httpwant) {
case CURL_HTTP_VERSION_NONE:
/* No preferences by transfer setup. Choose best defaults */
#ifdef USE_HTTPSRR
if(conn->dns_entry && conn->dns_entry->hinfo &&
!conn->dns_entry->hinfo->no_def_alpn) {
size_t i, j;
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) {
switch(alpn) {
case ALPN_h3:
if(Curl_conn_may_http3(data, conn))
break; /* not possible */
FALLTHROUGH();
case ALPN_h2:
case ALPN_h1:
alpn_ids[alpn_count++] = alpn;
break;
default: /* ignore */
break;
}
if(conn->dns_entry && conn->dns_entry->hinfo &&
!conn->dns_entry->hinfo->no_def_alpn) {
size_t i, j;
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)
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)
alpn_ids[alpn_count++] = alpn;
break;
case ALPN_h2:
if(data->state.http_neg.allowed & CURL_HTTP_V2x)
alpn_ids[alpn_count++] = alpn;
break;
case ALPN_h1:
if(data->state.http_neg.allowed & CURL_HTTP_V1x)
alpn_ids[alpn_count++] = alpn;
break;
default: /* ignore */
break;
}
}
}
#endif
if(!alpn_count)
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(data->state.http_neg.allowed & CURL_HTTP_V2x)
alpn_ids[alpn_count++] = ALPN_h2;
break;
case CURL_HTTP_VERSION_3ONLY:
result = Curl_conn_may_http3(data, conn);
if(result) /* cannot do it */
goto out;
alpn_ids[alpn_count++] = ALPN_h3;
break;
case CURL_HTTP_VERSION_3:
/* We assume that silently not even trying H3 is ok here */
if(Curl_conn_may_http3(data, conn) == CURLE_OK)
alpn_ids[alpn_count++] = ALPN_h3;
alpn_ids[alpn_count++] = ALPN_h2;
break;
case CURL_HTTP_VERSION_2_0:
case CURL_HTTP_VERSION_2TLS:
case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE:
alpn_ids[alpn_count++] = ALPN_h2;
break;
case CURL_HTTP_VERSION_1_0:
case CURL_HTTP_VERSION_1_1:
alpn_ids[alpn_count++] = ALPN_h1;
break;
default:
alpn_ids[alpn_count++] = ALPN_h2;
break;
else if(data->state.http_neg.allowed & CURL_HTTP_V1x)
alpn_ids[alpn_count++] = ALPN_h1;
}
}

View File

@ -497,13 +497,14 @@ bool Curl_conn_is_multiplex(struct connectdata *conn, int sockindex)
return FALSE;
}
unsigned char Curl_conn_http_version(struct Curl_easy *data)
unsigned char Curl_conn_http_version(struct Curl_easy *data,
struct connectdata *conn)
{
struct Curl_cfilter *cf;
CURLcode result = CURLE_UNKNOWN_OPTION;
unsigned char v = 0;
cf = data->conn ? data->conn->cfilter[FIRSTSOCKET] : NULL;
cf = conn->cfilter[FIRSTSOCKET];
for(; cf; cf = cf->next) {
if(cf->cft->flags & CF_TYPE_HTTP) {
int value = 0;

View File

@ -399,7 +399,8 @@ bool Curl_conn_is_multiplex(struct connectdata *conn, int sockindex);
* Return the HTTP version used on the FIRSTSOCKET connection filters
* or 0 if unknown. Value otherwise is 09, 10, 11, etc.
*/
unsigned char Curl_conn_http_version(struct Curl_easy *data);
unsigned char Curl_conn_http_version(struct Curl_easy *data,
struct connectdata *conn);
/**
* Close the filter chain at `sockindex` for connection `data->conn`.

View File

@ -187,19 +187,54 @@ const struct Curl_handler Curl_handler_https = {
#endif
void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg)
{
memset(neg, 0, sizeof(*neg));
neg->accept_09 = data->set.http09_allowed;
switch(data->set.httpwant) {
case CURL_HTTP_VERSION_1_0:
neg->allowed = (CURL_HTTP_V1x);
neg->only_10 = TRUE;
break;
case CURL_HTTP_VERSION_1_1:
neg->allowed = (CURL_HTTP_V1x);
break;
case CURL_HTTP_VERSION_2_0:
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);
break;
case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE:
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);
break;
case CURL_HTTP_VERSION_3ONLY:
neg->allowed = (CURL_HTTP_V3x);
break;
case CURL_HTTP_VERSION_NONE:
default:
neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x);
break;
}
}
CURLcode Curl_http_setup_conn(struct Curl_easy *data,
struct connectdata *conn)
{
/* allocate the HTTP-specific struct for the Curl_easy, only to survive
during this request */
connkeep(conn, "HTTP default");
if(data->state.httpwant == CURL_HTTP_VERSION_3ONLY) {
if(data->state.http_neg.allowed == CURL_HTTP_V3x) {
/* only HTTP/3, needs to work */
CURLcode result = Curl_conn_may_http3(data, conn);
if(result)
return result;
}
return CURLE_OK;
}
@ -538,7 +573,7 @@ CURLcode Curl_http_auth_act(struct Curl_easy *data)
(data->req.httpversion_sent > 11)) {
infof(data, "Forcing HTTP/1.1 for NTLM");
connclose(conn, "Force HTTP/1.1 connection");
data->state.httpwant = CURL_HTTP_VERSION_1_1;
data->state.http_neg.allowed = CURL_HTTP_V1x;
}
}
#ifndef CURL_DISABLE_PROXY
@ -1502,29 +1537,28 @@ static bool http_may_use_1_1(const struct Curl_easy *data)
const struct connectdata *conn = data->conn;
/* We have seen a previous response for *this* transfer with 1.0,
* on another connection or the same one. */
if(data->state.httpversion == 10)
if(data->state.http_neg.rcvd_min == 10)
return FALSE;
/* We have seen a previous response on *this* connection with 1.0. */
if(conn->httpversion_seen == 10)
if(conn && conn->httpversion_seen == 10)
return FALSE;
/* We want 1.0 and have seen no previous response on *this* connection
with a higher version (maybe no response at all yet). */
if((data->state.httpwant == CURL_HTTP_VERSION_1_0) &&
(conn->httpversion_seen <= 10))
if((data->state.http_neg.only_10) &&
(!conn || conn->httpversion_seen <= 10))
return FALSE;
/* We want something newer than 1.0 or have no preferences. */
return (data->state.httpwant == CURL_HTTP_VERSION_NONE) ||
(data->state.httpwant >= CURL_HTTP_VERSION_1_1);
/* We are not restricted to use 1.0 only. */
return !data->state.http_neg.only_10;
}
static unsigned char http_request_version(struct Curl_easy *data)
{
unsigned char httpversion = Curl_conn_http_version(data);
if(!httpversion) {
unsigned char v = Curl_conn_http_version(data, data->conn);
if(!v) {
/* No specific HTTP connection filter installed. */
httpversion = http_may_use_1_1(data) ? 11 : 10;
v = http_may_use_1_1(data) ? 11 : 10;
}
return httpversion;
return v;
}
static const char *get_http_string(int httpversion)
@ -2621,11 +2655,11 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
switch(conn->alpn) {
case CURL_HTTP_VERSION_3:
DEBUGASSERT(Curl_conn_http_version(data) == 30);
DEBUGASSERT(Curl_conn_http_version(data, conn) == 30);
break;
case CURL_HTTP_VERSION_2:
#ifndef CURL_DISABLE_PROXY
if((Curl_conn_http_version(data) != 20) &&
if((Curl_conn_http_version(data, conn) != 20) &&
conn->bits.proxy && !conn->bits.tunnel_proxy
) {
result = Curl_http2_switch(data);
@ -2634,7 +2668,7 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
}
else
#endif
DEBUGASSERT(Curl_conn_http_version(data) == 20);
DEBUGASSERT(Curl_conn_http_version(data, conn) == 20);
break;
case CURL_HTTP_VERSION_1_1:
/* continue with HTTP/1.x when explicitly requested */
@ -2815,7 +2849,8 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
}
if(!Curl_conn_is_ssl(conn, FIRSTSOCKET) && (httpversion < 20) &&
(data->state.httpwant == CURL_HTTP_VERSION_2)) {
(data->state.http_neg.allowed & 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 */
result = Curl_http2_request_upgrade(&req, data);
@ -3346,9 +3381,10 @@ static CURLcode http_statusline(struct Curl_easy *data,
data->info.httpversion = k->httpversion;
conn->httpversion_seen = (unsigned char)k->httpversion;
if(!data->state.httpversion || data->state.httpversion > k->httpversion)
if(!data->state.http_neg.rcvd_min ||
data->state.http_neg.rcvd_min > k->httpversion)
/* store the lowest server version we encounter */
data->state.httpversion = (unsigned char)k->httpversion;
data->state.http_neg.rcvd_min = (unsigned char)k->httpversion;
/*
* This code executes as part of processing the header. As a
@ -4014,7 +4050,7 @@ static CURLcode http_parse_headers(struct Curl_easy *data,
failf(data, "Invalid status line");
return CURLE_WEIRD_SERVER_REPLY;
}
if(!data->set.http09_allowed) {
if(!data->state.http_neg.accept_09) {
failf(data, "Received HTTP/0.9 when not allowed");
return CURLE_UNSUPPORTED_PROTOCOL;
}
@ -4051,7 +4087,7 @@ static CURLcode http_parse_headers(struct Curl_easy *data,
failf(data, "Invalid status line");
return CURLE_WEIRD_SERVER_REPLY;
}
if(!data->set.http09_allowed) {
if(!data->state.http_neg.accept_09) {
failf(data, "Received HTTP/0.9 when not allowed");
return CURLE_UNSUPPORTED_PROTOCOL;
}

View File

@ -53,6 +53,12 @@ typedef enum {
FOLLOW_REDIR /* a full true redirect */
} followtype;
#define CURL_HTTP_V1x (1 << 0)
#define CURL_HTTP_V2x (1 << 1)
#define CURL_HTTP_V3x (1 << 2)
/* bitmask of CURL_HTTP_V* values */
typedef unsigned char http_majors;
#ifndef CURL_DISABLE_HTTP
@ -68,6 +74,17 @@ extern const struct Curl_handler Curl_handler_https;
struct dynhds;
struct http_negotiation {
unsigned char rcvd_min; /* minimum version seen in responses, 09, 10, 11 */
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 */
BIT(accept_09); /* Accept an HTTP/0.9 response */
BIT(only_10); /* When using major version 1x, use only 1.0 */
};
void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg);
CURLcode Curl_bump_headersize(struct Curl_easy *data,
size_t delta,
bool connect_only);

View File

@ -2794,8 +2794,9 @@ out:
bool Curl_http2_may_switch(struct Curl_easy *data)
{
if(Curl_conn_http_version(data) < 20 &&
data->state.httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE) {
if(Curl_conn_http_version(data, data->conn) < 20 &&
(data->state.http_neg.allowed & CURL_HTTP_V2x) &&
data->state.http_neg.h2_prior_knowledge) {
#ifndef CURL_DISABLE_PROXY
if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy) {
/* We do not support HTTP/2 proxies yet. Also it is debatable
@ -2814,7 +2815,7 @@ CURLcode Curl_http2_switch(struct Curl_easy *data)
struct Curl_cfilter *cf;
CURLcode result;
DEBUGASSERT(Curl_conn_http_version(data) < 20);
DEBUGASSERT(Curl_conn_http_version(data, data->conn) < 20);
result = http2_cfilter_add(&cf, data, data->conn, FIRSTSOCKET, FALSE);
if(result)
@ -2836,7 +2837,7 @@ CURLcode Curl_http2_switch_at(struct Curl_cfilter *cf, struct Curl_easy *data)
struct Curl_cfilter *cf_h2;
CURLcode result;
DEBUGASSERT(Curl_conn_http_version(data) < 20);
DEBUGASSERT(Curl_conn_http_version(data, data->conn) < 20);
result = http2_cfilter_insert_after(cf, data, FALSE);
if(result)
@ -2861,7 +2862,7 @@ CURLcode Curl_http2_upgrade(struct Curl_easy *data,
struct cf_h2_ctx *ctx;
CURLcode result;
DEBUGASSERT(Curl_conn_http_version(data) < 20);
DEBUGASSERT(Curl_conn_http_version(data, conn) < 20);
DEBUGASSERT(data->req.upgr101 == UPGR101_RECEIVED);
result = http2_cfilter_add(&cf, data, conn, sockindex, TRUE);
@ -2908,7 +2909,7 @@ CURLcode Curl_http2_upgrade(struct Curl_easy *data,
CURLE_HTTP2_STREAM error! */
bool Curl_h2_http_1_1_error(struct Curl_easy *data)
{
if(Curl_conn_http_version(data) == 20) {
if(Curl_conn_http_version(data, data->conn) == 20) {
int err = Curl_conn_get_stream_error(data, data->conn, FIRSTSOCKET);
return err == NGHTTP2_HTTP_1_1_REQUIRED;
}

View File

@ -1910,6 +1910,7 @@ static CURLMcode state_performing(struct Curl_easy *data,
data->req.done = TRUE;
}
}
#ifndef CURL_DISABLE_HTTP
else if((CURLE_HTTP2_STREAM == result) &&
Curl_h2_http_1_1_error(data)) {
CURLcode ret = Curl_retry_request(data, &newurl);
@ -1917,7 +1918,7 @@ static CURLMcode state_performing(struct Curl_easy *data,
if(!ret) {
infof(data, "Downgrades to HTTP/1.1");
streamclose(data->conn, "Disconnect HTTP/2 for HTTP/1");
data->state.httpwant = CURL_HTTP_VERSION_1_1;
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;
if(!newurl)
@ -1932,6 +1933,7 @@ static CURLMcode state_performing(struct Curl_easy *data,
else
result = ret;
}
#endif
if(result) {
/*

View File

@ -570,8 +570,9 @@ CURLcode Curl_pretransfer(struct Curl_easy *data)
data->state.followlocation = 0; /* reset the location-follow counter */
data->state.this_is_a_follow = FALSE; /* reset this */
data->state.errorbuf = FALSE; /* no error has occurred */
data->state.httpwant = data->set.httpwant;
data->state.httpversion = 0;
#ifndef CURL_DISABLE_HTTP
Curl_http_neg_init(data, &data->state.http_neg);
#endif
data->state.authproblem = FALSE;
data->state.authhost.want = data->set.httpauth;
data->state.authproxy.want = data->set.proxyauth;

153
lib/url.c
View File

@ -652,15 +652,20 @@ bool Curl_on_disconnect(struct Curl_easy *data,
static bool xfer_may_multiplex(const struct Curl_easy *data,
const struct connectdata *conn)
{
#ifndef CURL_DISABLE_HTTP
/* If an HTTP protocol and multiplexing is enabled */
if((conn->handler->protocol & PROTO_FAMILY_HTTP) &&
(!conn->bits.protoconnstart || !conn->bits.close)) {
if(Curl_multiplex_wanted(data->multi) &&
(data->state.httpwant >= CURL_HTTP_VERSION_2))
(data->state.http_neg.allowed & (CURL_HTTP_V2x|CURL_HTTP_V3x)))
/* allows HTTP/2 or newer */
return TRUE;
}
#else
(void)data;
(void)conn;
#endif
return FALSE;
}
@ -992,8 +997,9 @@ static bool url_match_conn(struct connectdata *conn, void *userdata)
}
#endif
#ifndef CURL_DISABLE_HTTP
if(match->may_multiplex &&
(data->state.httpwant == CURL_HTTP_VERSION_2_0) &&
(data->state.http_neg.allowed & (CURL_HTTP_V2x|CURL_HTTP_V3x)) &&
(needle->handler->protocol & CURLPROTO_HTTP) &&
!conn->httpversion_seen) {
if(data->set.pipewait) {
@ -1005,6 +1011,7 @@ static bool url_match_conn(struct connectdata *conn, void *userdata)
infof(data, "Server upgrade cannot be used");
return FALSE;
}
#endif
if(!(needle->handler->flags & PROTOPT_CREDSPERREQUEST)) {
/* This protocol requires credentials per connection,
@ -1025,27 +1032,38 @@ static bool url_match_conn(struct connectdata *conn, void *userdata)
return FALSE;
#endif
/* If looking for HTTP and the HTTP version we want is less
* than the HTTP version of conn, continue looking.
#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. */
if((needle->handler->protocol & PROTO_FAMILY_HTTP) &&
(data->state.httpwant != CURL_HTTP_VERSION_2TLS)) {
unsigned char httpversion = Curl_conn_http_version(data);
if((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, httpversion));
}
if((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, httpversion));
return FALSE;
if((needle->handler->protocol & PROTO_FAMILY_HTTP)) {
switch(Curl_conn_http_version(data, conn)) {
case 30:
if(!(data->state.http_neg.allowed & CURL_HTTP_V3x)) {
DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
", we do not want h3", conn->connection_id));
return FALSE;
}
break;
case 20:
if(!(data->state.http_neg.allowed & CURL_HTTP_V2x)) {
DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
", we do not want h2", conn->connection_id));
return FALSE;
}
break;
default:
if(!(data->state.http_neg.allowed & CURL_HTTP_V1x)) {
DEBUGF(infof(data, "not reusing conn #%" CURL_FORMAT_CURL_OFF_T
", we do not want h1", conn->connection_id));
return FALSE;
}
break;
}
}
#endif
#ifdef USE_SSH
else if(get_protocol_family(needle->handler) & PROTO_FAMILY_SSH) {
if(!ssh_config_matches(needle, conn))
@ -3054,75 +3072,47 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
)) {
/* no connect_to match, try alt-svc! */
enum alpnid srcalpnid = ALPN_none;
bool use_alt_svc = FALSE;
bool hit = FALSE;
struct altsvc *as = NULL;
const int allowed_versions = ( ALPN_h1
#ifdef USE_HTTP2
| ALPN_h2
#endif
#ifdef USE_HTTP3
| ALPN_h3
#endif
) & data->asi->flags;
static enum alpnid alpn_ids[] = {
#ifdef USE_HTTP3
ALPN_h3,
#endif
#ifdef USE_HTTP2
ALPN_h2,
#endif
ALPN_h1,
};
size_t i;
int allowed_versions = ALPN_none;
switch(data->state.httpwant) {
case CURL_HTTP_VERSION_1_0:
break;
case CURL_HTTP_VERSION_1_1:
use_alt_svc = TRUE;
srcalpnid = ALPN_h1; /* only regard alt-svc advice for http/1.1 */
break;
case CURL_HTTP_VERSION_2_0:
use_alt_svc = TRUE;
srcalpnid = ALPN_h2; /* only regard alt-svc advice for h2 */
break;
case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE:
break;
case CURL_HTTP_VERSION_3:
use_alt_svc = TRUE;
srcalpnid = ALPN_h3; /* only regard alt-svc advice for h3 */
break;
case CURL_HTTP_VERSION_3ONLY:
break;
default: /* no specific HTTP version wanted, look at all of alt-svc */
use_alt_svc = TRUE;
srcalpnid = ALPN_none;
break;
}
if(!use_alt_svc)
return CURLE_OK;
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;
host = conn->host.rawalloc;
DEBUGF(infof(data, "check Alt-Svc for host %s", host));
if(srcalpnid == ALPN_none) {
/* scan all alt-svc protocol ids in order or relevance */
for(i = 0; !hit && (i < CURL_ARRAYSIZE(alpn_ids)); ++i) {
srcalpnid = alpn_ids[i];
hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
}
}
else {
/* look for a specific alt-svc protocol id */
#ifdef USE_HTTP3
if(!hit && (allowed_versions & ALPN_h3)) {
srcalpnid = ALPN_h3;
hit = Curl_altsvc_lookup(data->asi,
srcalpnid, host, conn->remote_port, /* from */
ALPN_h3, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
}
#endif
#ifdef USE_HTTP2
if(!hit && (allowed_versions & ALPN_h2) &&
!data->state.http_neg.h2_prior_knowledge) {
srcalpnid = ALPN_h2;
hit = Curl_altsvc_lookup(data->asi,
ALPN_h2, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
}
#endif
if(!hit && (allowed_versions & ALPN_h1) &&
!data->state.http_neg.only_10) {
srcalpnid = ALPN_h1;
hit = Curl_altsvc_lookup(data->asi,
ALPN_h1, host, conn->remote_port, /* from */
&as /* to */,
allowed_versions);
}
if(hit) {
char *hostd = strdup((char *)as->dst.host);
@ -3141,14 +3131,15 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
/* protocol version switch */
switch(as->dst.alpnid) {
case ALPN_h1:
data->state.httpwant = CURL_HTTP_VERSION_1_1;
data->state.http_neg.allowed = CURL_HTTP_V1x;
data->state.http_neg.only_10 = FALSE;
break;
case ALPN_h2:
data->state.httpwant = CURL_HTTP_VERSION_2_0;
data->state.http_neg.allowed = CURL_HTTP_V2x;
break;
case ALPN_h3:
conn->transport = TRNSPRT_QUIC;
data->state.httpwant = CURL_HTTP_VERSION_3;
data->state.http_neg.allowed = CURL_HTTP_V3x;
break;
default: /* should not be possible */
break;

View File

@ -1313,10 +1313,9 @@ struct UrlState {
char *proxypasswd;
#endif
} aptr;
unsigned char httpwant; /* when non-zero, a specific HTTP version requested
to be used in the library's request(s) */
unsigned char httpversion; /* the lowest HTTP version*10 reported by any
server involved in this request */
#ifndef CURL_DISABLE_HTTP
struct http_negotiation http_neg;
#endif
unsigned char httpreq; /* Curl_HttpReq; what kind of HTTP request (if any)
is this */
unsigned char select_bits; /* != 0 -> bitmask of socket events for this

View File

@ -154,17 +154,19 @@ static const struct alpn_spec ALPN_SPEC_H2_H11 = {
};
#endif
static const struct alpn_spec *alpn_get_spec(int httpwant, bool use_alpn)
static const struct alpn_spec *
alpn_get_spec(http_majors allowed, bool use_alpn)
{
if(!use_alpn)
return NULL;
#ifdef USE_HTTP2
if(httpwant == CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE)
if(allowed & CURL_HTTP_V2x) {
if(allowed & CURL_HTTP_V1x)
return &ALPN_SPEC_H2_H11;
return &ALPN_SPEC_H2;
if(httpwant >= CURL_HTTP_VERSION_2)
return &ALPN_SPEC_H2_H11;
}
#else
(void)httpwant;
(void)allowed;
#endif
/* Use the ALPN protocol "http/1.1" for HTTP/1.x.
Avoid "http/1.0" because some servers do not support it. */
@ -1576,7 +1578,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf,
DEBUGASSERT(data->conn);
ctx = cf_ctx_new(data, alpn_get_spec(data->state.httpwant,
ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.allowed,
conn->bits.tls_enable_alpn));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
@ -1627,16 +1629,16 @@ static CURLcode cf_ssl_proxy_create(struct Curl_cfilter **pcf,
struct ssl_connect_data *ctx;
CURLcode result;
bool use_alpn = conn->bits.tls_enable_alpn;
int httpwant = CURL_HTTP_VERSION_1_1;
http_majors allowed = CURL_HTTP_V1x;
#ifdef USE_HTTP2
if(conn->http_proxy.proxytype == CURLPROXY_HTTPS2) {
use_alpn = TRUE;
httpwant = CURL_HTTP_VERSION_2;
allowed = (CURL_HTTP_V1x|CURL_HTTP_V2x);
}
#endif
ctx = cf_ctx_new(data, alpn_get_spec(httpwant, use_alpn));
ctx = cf_ctx_new(data, alpn_get_spec(allowed, use_alpn));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;

View File

@ -1308,7 +1308,9 @@ static CURLcode ws_setup_conn(struct Curl_easy *data,
struct connectdata *conn)
{
/* WebSockets is 1.1 only (for now) */
data->state.httpwant = CURL_HTTP_VERSION_1_1;
data->state.http_neg.accept_09 = FALSE;
data->state.http_neg.only_10 = FALSE;
data->state.http_neg.allowed = CURL_HTTP_V1x;
return Curl_http_setup_conn(data, conn);
}

View File

@ -90,7 +90,7 @@ class TestReuse:
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}',
'--alt-svc', f'{asfile}', '--http3',
])
r.check_response(count=count, http_status=200)
# We expect the connection to be reused
@ -111,9 +111,9 @@ class TestReuse:
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}]'
urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json?[0-{count-1}]'
r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
'--alt-svc', f'{asfile}',
'--alt-svc', f'{asfile}', '--http3'
])
r.check_response(count=count, http_status=200)
# We expect the connection to be reused and use HTTP/2
@ -134,12 +134,35 @@ class TestReuse:
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}]'
urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json?[0-{count-1}]'
r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
'--alt-svc', f'{asfile}',
'--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
assert r.total_connects == 1
for s in r.stats:
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):
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, "h3")}/data.json?[0-{count-1}]'
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)
# 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}'