Skip to content

Commit

Permalink
http: version negotiation
Browse files Browse the repository at this point in the history
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 curl#16100
  • Loading branch information
icing authored and bagder committed Feb 18, 2025
1 parent f7fcbb8 commit db72b8d
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 189 deletions.
99 changes: 44 additions & 55 deletions lib/cf-https-connect.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/cfilters.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion lib/cfilters.h
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
82 changes: 59 additions & 23 deletions lib/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions lib/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);
Expand Down
13 changes: 7 additions & 6 deletions lib/http2.c
Original file line number Diff line number Diff line change
Expand Up @@ -2794,8 +2794,9 @@ static CURLcode http2_cfilter_insert_after(struct Curl_cfilter *cf,

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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit db72b8d

Please sign in to comment.