]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
http: version negotiation
authorStefan Eissing <stefan@eissing.org>
Mon, 27 Jan 2025 14:39:13 +0000 (15:39 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 18 Feb 2025 15:10:12 +0000 (16:10 +0100)
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

13 files changed:
lib/cf-https-connect.c
lib/cfilters.c
lib/cfilters.h
lib/http.c
lib/http.h
lib/http2.c
lib/multi.c
lib/transfer.c
lib/url.c
lib/urldata.h
lib/vtls/vtls.c
lib/ws.c
tests/http/test_12_reuse.py

index f073647e558ec07941b298142f1fc8a9ad2b65b2..2a645d415bc5c8ac4d504abb56baef4d1901649b 100644 (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;
     }
   }
 
index 91f7325c3834f2a002270bf03f4c14b7de785caf..fa0abc46d9ebc9e959249ff20668c9333755db21 100644 (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;
index 2d5599a90abf82463f5379b9f981e97a90639b8f..23746edcdebe0819533aa92bcb35447d48a540f0 100644 (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`.
index f82e030a1b22ecb7099cae7eb5587d35bc43944e..e054bf4b69e4b04e52d7e486fd123beb79a434f9 100644 (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;
         }
index a15a982356a31e3c30601836ae93231e5bb4ce69..61237b3bf8a33089a063906ae3083a1745cd6f6d 100644 (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);
index 171de70b2c382ced865966d4437756ff512abf3d..b82b984935bd9c3fbfd825cf441eff495c9b8edf 100644 (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;
   }
index 2b05e94a04686a2389e33e999f37cacd8e480d30..449420dc86592c80beac3fcd2e383a335553a070 100644 (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) {
     /*
index 365005d02ef7477e3128f6eed91f3411da4e272e..19dcad3f04e35b462494a557db89f47b82e2ba62 100644 (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;
index 2826fc2a6863d1f0772ae946aa28a162257aa2bc..b348834c225a1390e14e128d817bed39d49fc3b3 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -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);
-      }
+#ifdef USE_HTTP3
+    if(!hit && (allowed_versions & ALPN_h3)) {
+      srcalpnid = ALPN_h3;
+      hit = Curl_altsvc_lookup(data->asi,
+                               ALPN_h3, host, conn->remote_port, /* from */
+                               &as /* to */,
+                               allowed_versions);
     }
-    else {
-      /* look for a specific alt-svc protocol id */
+ #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,
-                               srcalpnid, host, conn->remote_port, /* from */
+                               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;
index 6744129fa66bf9d697c8b75710840c28e3077e07..a77fdfc3d9dbef824d779ec199419173e5b294db 100644 (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
index 9c59b4c97bbb36db70adbf11f8191e0d1200d87a..6297d82327985a29324bd09f22ded62609704654 100644 (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;
index 5a52c84ade6836fc037597a0f68ef2efc36848fe..23ee016229ea50cf1900c3238cf0add3754d03ea 100644 (file)
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -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);
 }
 
index 18d88596b1c50d2295e49ac485a1002120c96e95..468fcfbb71f0d11d429754f0e5d5fb7e47875e3d 100644 (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}'