]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
QUIC: 0RTT for gnutls via CURLSSLOPT_EARLYDATA
authorStefan Eissing <stefan@eissing.org>
Mon, 2 Dec 2024 11:50:15 +0000 (12:50 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Mon, 23 Dec 2024 16:07:15 +0000 (17:07 +0100)
When a QUIC TLS session announced early data support and
'CURLSSLOPT_EARLYDATA' is set for the transfer, send initial request and
body (up to the 128k we buffer) as 0RTT when curl is built with
ngtcp2+gnutls.

QUIC 0RTT needs not only the TLS session but the QUIC transport
paramters as well. Store those and the earlydata max value together with
the session in the cache.

Add test case for h3 use of this. Enable quic early data in nghttpx for
testing.

Closes #15667

17 files changed:
lib/vquic/curl_ngtcp2.c
lib/vquic/curl_osslq.c
lib/vquic/curl_quiche.c
lib/vquic/vquic-tls.c
lib/vquic/vquic-tls.h
lib/vtls/bearssl.c
lib/vtls/gtls.c
lib/vtls/gtls.h
lib/vtls/mbedtls.c
lib/vtls/openssl.c
lib/vtls/vtls_scache.c
lib/vtls/vtls_scache.h
lib/vtls/wolfssl.c
tests/http/test_02_download.py
tests/http/test_07_upload.py
tests/http/test_08_caddy.py
tests/http/testenv/nghttpx.py

index 9b2c66e70534e9f316b9ebbdb6efe3251d369de9..0449acdab1adb2929705d752cc7debee9822745b 100644 (file)
@@ -66,6 +66,7 @@
 #include "vquic-tls.h"
 #include "vtls/keylog.h"
 #include "vtls/vtls.h"
+#include "vtls/vtls_scache.h"
 #include "curl_ngtcp2.h"
 
 #include "warnless.h"
@@ -137,8 +138,16 @@ struct cf_ngtcp2_ctx {
   uint64_t max_idle_ms;              /* max idle time for QUIC connection */
   uint64_t used_bidi_streams;        /* bidi streams we have opened */
   uint64_t max_bidi_streams;         /* max bidi streams we can open */
+  size_t earlydata_max;              /* max amount of early data supported by
+                                        server on session reuse */
+  size_t earlydata_skip;            /* sending bytes to skip when earlydata
+                                     * is accepted by peer */
+  CURLcode tls_vrfy_result;          /* result of TLS peer verification */
   int qlogfd;
   BIT(initialized);
+  BIT(tls_handshake_complete);       /* TLS handshake is done */
+  BIT(use_earlydata);                /* Using 0RTT data */
+  BIT(earlydata_accepted);           /* 0RTT was acceptd by server */
   BIT(shutdown_started);             /* queued shutdown packets */
 };
 
@@ -442,12 +451,42 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
   }
 }
 
-static CURLcode init_ngh3_conn(struct Curl_cfilter *cf);
+static CURLcode init_ngh3_conn(struct Curl_cfilter *cf,
+                               struct Curl_easy *data);
 
-static int cb_handshake_completed(ngtcp2_conn *tconn, void *user_data)
+static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data)
 {
-  (void)user_data;
+  struct Curl_cfilter *cf = user_data;
+  struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL;
+  struct Curl_easy *data;
+
   (void)tconn;
+  DEBUGASSERT(ctx);
+  data = CF_DATA_CURRENT(cf);
+  DEBUGASSERT(data);
+  if(!ctx || !data)
+    return NGHTTP3_ERR_CALLBACK_FAILURE;
+
+  ctx->handshake_at = Curl_now();
+  ctx->tls_handshake_complete = TRUE;
+  cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */
+  cf->conn->httpversion = 30;
+
+  ctx->tls_vrfy_result = Curl_vquic_tls_verify_peer(&ctx->tls, cf,
+                                                    data, &ctx->peer);
+  CURL_TRC_CF(data, cf, "handshake complete after %dms",
+             (int)Curl_timediff(ctx->handshake_at, ctx->started_at));
+#ifdef USE_GNUTLS
+  if(ctx->use_earlydata) {
+    int flags = gnutls_session_get_flags(ctx->tls.gtls.session);
+    ctx->earlydata_accepted = !!(flags & GNUTLS_SFLAGS_EARLY_DATA);
+    CURL_TRC_CF(data, cf, "server did%s accept %zu bytes of early data",
+                ctx->earlydata_accepted ? "" : " not", ctx->earlydata_skip);
+    Curl_pgrsEarlyData(data, ctx->earlydata_accepted ?
+                              (curl_off_t)ctx->earlydata_skip :
+                             -(curl_off_t)ctx->earlydata_skip);
+  }
+#endif
   return 0;
 }
 
@@ -717,16 +756,19 @@ static int cb_recv_rx_key(ngtcp2_conn *tconn, ngtcp2_encryption_level level,
                           void *user_data)
 {
   struct Curl_cfilter *cf = user_data;
+  struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL;
+  struct Curl_easy *data = CF_DATA_CURRENT(cf);
   (void)tconn;
 
-  if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT) {
+  if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT)
     return 0;
-  }
 
-  if(init_ngh3_conn(cf) != CURLE_OK) {
-    return NGTCP2_ERR_CALLBACK_FAILURE;
+  DEBUGASSERT(ctx);
+  DEBUGASSERT(data);
+  if(ctx && data && !ctx->h3conn) {
+    if(init_ngh3_conn(cf, data))
+      return NGTCP2_ERR_CALLBACK_FAILURE;
   }
-
   return 0;
 }
 
@@ -739,7 +781,7 @@ static ngtcp2_callbacks ng_callbacks = {
   ngtcp2_crypto_client_initial_cb,
   NULL, /* recv_client_initial */
   ngtcp2_crypto_recv_crypto_data_cb,
-  cb_handshake_completed,
+  cf_ngtcp2_handshake_completed,
   NULL, /* recv_version_negotiation */
   ngtcp2_crypto_encrypt_cb,
   ngtcp2_crypto_decrypt_cb,
@@ -1128,14 +1170,15 @@ static nghttp3_callbacks ngh3_callbacks = {
   NULL /* recv_settings */
 };
 
-static CURLcode init_ngh3_conn(struct Curl_cfilter *cf)
+static CURLcode init_ngh3_conn(struct Curl_cfilter *cf,
+                               struct Curl_easy *data)
 {
   struct cf_ngtcp2_ctx *ctx = cf->ctx;
-  CURLcode result;
-  int rc;
   int64_t ctrl_stream_id, qpack_enc_stream_id, qpack_dec_stream_id;
+  int rc;
 
   if(ngtcp2_conn_get_streams_uni_left(ctx->qconn) < 3) {
+    failf(data, "QUIC connection lacks 3 uni streams to run HTTP/3");
     return CURLE_QUIC_CONNECT_ERROR;
   }
 
@@ -1147,45 +1190,47 @@ static CURLcode init_ngh3_conn(struct Curl_cfilter *cf)
                                nghttp3_mem_default(),
                                cf);
   if(rc) {
-    result = CURLE_OUT_OF_MEMORY;
-    goto fail;
+    failf(data, "error creating nghttp3 connection instance");
+    return CURLE_OUT_OF_MEMORY;
   }
 
   rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &ctrl_stream_id, NULL);
   if(rc) {
-    result = CURLE_QUIC_CONNECT_ERROR;
-    goto fail;
+    failf(data, "error creating HTTP/3 control stream: %s",
+          ngtcp2_strerror(rc));
+    return CURLE_QUIC_CONNECT_ERROR;
   }
 
   rc = nghttp3_conn_bind_control_stream(ctx->h3conn, ctrl_stream_id);
   if(rc) {
-    result = CURLE_QUIC_CONNECT_ERROR;
-    goto fail;
+    failf(data, "error binding HTTP/3 control stream: %s",
+          ngtcp2_strerror(rc));
+    return CURLE_QUIC_CONNECT_ERROR;
   }
 
   rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_enc_stream_id, NULL);
   if(rc) {
-    result = CURLE_QUIC_CONNECT_ERROR;
-    goto fail;
+    failf(data, "error creating HTTP/3 qpack encoding stream: %s",
+          ngtcp2_strerror(rc));
+    return CURLE_QUIC_CONNECT_ERROR;
   }
 
   rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_dec_stream_id, NULL);
   if(rc) {
-    result = CURLE_QUIC_CONNECT_ERROR;
-    goto fail;
+    failf(data, "error creating HTTP/3 qpack decoding stream: %s",
+          ngtcp2_strerror(rc));
+    return CURLE_QUIC_CONNECT_ERROR;
   }
 
   rc = nghttp3_conn_bind_qpack_streams(ctx->h3conn, qpack_enc_stream_id,
                                        qpack_dec_stream_id);
   if(rc) {
-    result = CURLE_QUIC_CONNECT_ERROR;
-    goto fail;
+    failf(data, "error binding HTTP/3 qpack streams: %s",
+          ngtcp2_strerror(rc));
+    return CURLE_QUIC_CONNECT_ERROR;
   }
 
   return CURLE_OK;
-fail:
-
-  return result;
 }
 
 static ssize_t recv_closed_stream(struct Curl_cfilter *cf,
@@ -1236,6 +1281,10 @@ static ssize_t cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
   DEBUGASSERT(ctx->h3conn);
   *err = CURLE_OK;
 
+  /* handshake verification failed in callback, do not recv anything */
+  if(ctx->tls_vrfy_result)
+    return ctx->tls_vrfy_result;
+
   pktx_init(&pktx, cf, data);
 
   if(!stream || ctx->shutdown_started) {
@@ -1533,6 +1582,10 @@ static ssize_t cf_ngtcp2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
   pktx_init(&pktx, cf, data);
   *err = CURLE_OK;
 
+  /* handshake verification failed in callback, do not send anything */
+  if(ctx->tls_vrfy_result)
+    return ctx->tls_vrfy_result;
+
   (void)eos; /* TODO: use for stream EOF and block handling */
   result = cf_progress_ingress(cf, data, &pktx);
   if(result) {
@@ -1594,6 +1647,9 @@ static ssize_t cf_ngtcp2_send(struct Curl_cfilter *cf, struct Curl_easy *data,
     (void)nghttp3_conn_resume_stream(ctx->h3conn, stream->id);
   }
 
+  if(sent > 0 && !ctx->tls_handshake_complete && ctx->use_earlydata)
+    ctx->earlydata_skip += sent;
+
   result = cf_progress_egress(cf, data, &pktx);
   if(result) {
     *err = result;
@@ -1612,17 +1668,6 @@ out:
   return sent;
 }
 
-static CURLcode qng_verify_peer(struct Curl_cfilter *cf,
-                                struct Curl_easy *data)
-{
-  struct cf_ngtcp2_ctx *ctx = cf->ctx;
-
-  cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */
-  cf->conn->httpversion = 30;
-
-  return Curl_vquic_tls_verify_peer(&ctx->tls, cf, data, &ctx->peer);
-}
-
 static CURLcode recv_pkt(const unsigned char *pkt, size_t pktlen,
                          struct sockaddr_storage *remote_addr,
                          socklen_t remote_addrlen, int ecn,
@@ -2135,6 +2180,24 @@ static int quic_ossl_new_session_cb(SSL *ssl, SSL_SESSION *ssl_sessionid)
 #endif /* USE_OPENSSL */
 
 #ifdef USE_GNUTLS
+
+static const char *gtls_hs_msg_name(int mtype)
+{
+  switch(mtype) {
+    case 1: return "ClientHello";
+    case 2: return "ServerHello";
+    case 4: return "SessionTicket";
+    case 8: return "EncryptedExtensions";
+    case 11: return "Certificate";
+    case 13: return "CertificateRequest";
+    case 15: return "CertificateVerify";
+    case 20: return "Finished";
+    case 24: return "KeyUpdate";
+    case 254: return "MessageHash";
+  }
+  return "Unknown";
+}
+
 static int quic_gtls_handshake_cb(gnutls_session_t session, unsigned int htype,
                                   unsigned when, unsigned int incoming,
                                   const gnutls_datum_t *msg)
@@ -2148,14 +2211,28 @@ static int quic_gtls_handshake_cb(gnutls_session_t session, unsigned int htype,
   if(when && cf && ctx) { /* after message has been processed */
     struct Curl_easy *data = CF_DATA_CURRENT(cf);
     DEBUGASSERT(data);
-    if(data) {
-      CURL_TRC_CF(data, cf, "handshake: %s message type %d",
-                  incoming ? "incoming" : "outgoing", htype);
-    }
+    if(!data)
+      return 0;
+    CURL_TRC_CF(data, cf, "SSL message: %s %s [%d]",
+                incoming ? "<-" : "->", gtls_hs_msg_name(htype), htype);
     switch(htype) {
     case GNUTLS_HANDSHAKE_NEW_SESSION_TICKET: {
+      ngtcp2_ssize tplen;
+      uint8_t tpbuf[256];
+      unsigned char *quic_tp = NULL;
+      size_t quic_tp_len = 0;
+
+      tplen = ngtcp2_conn_encode_0rtt_transport_params(ctx->qconn, tpbuf,
+                                                       sizeof(tpbuf));
+      if(tplen < 0)
+        CURL_TRC_CF(data, cf, "error encoding 0RTT transport data: %s",
+                    ngtcp2_strerror((int)tplen));
+      else {
+        quic_tp = (unsigned char *)tpbuf;
+        quic_tp_len = (size_t)tplen;
+      }
       (void)Curl_gtls_cache_session(cf, data, ctx->peer.scache_key,
-                                    session, -1, "h3");
+                                    session, -1, "h3", quic_tp, quic_tp_len);
       break;
     }
     default:
@@ -2186,9 +2263,9 @@ static int wssl_quic_new_session_cb(WOLFSSL *ssl, WOLFSSL_SESSION *session)
 }
 #endif /* USE_WOLFSSL */
 
-static CURLcode tls_ctx_setup(struct Curl_cfilter *cf,
-                              struct Curl_easy *data,
-                              void *user_data)
+static CURLcode cf_ngtcp2_tls_ctx_setup(struct Curl_cfilter *cf,
+                                        struct Curl_easy *data,
+                                        void *user_data)
 {
   struct curl_tls_ctx *ctx = user_data;
   struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
@@ -2241,6 +2318,53 @@ static CURLcode tls_ctx_setup(struct Curl_cfilter *cf,
   return CURLE_OK;
 }
 
+static CURLcode cf_ngtcp2_on_session_reuse(struct Curl_cfilter *cf,
+                                           struct Curl_easy *data,
+                                           struct Curl_ssl_session *scs,
+                                           bool *do_early_data)
+{
+  struct cf_ngtcp2_ctx *ctx = cf->ctx;
+  CURLcode result = CURLE_OK;
+
+  *do_early_data = FALSE;
+#ifdef USE_GNUTLS
+  ctx->earlydata_max =
+    gnutls_record_get_max_early_data_size(ctx->tls.gtls.session);
+  if((!ctx->earlydata_max)) {
+    CURL_TRC_CF(data, cf, "SSL session does not allow earlydata");
+  }
+  else if(strcmp("h3", scs->alpn)) {
+    CURL_TRC_CF(data, cf, "SSL session from different ALPN, no early data");
+  }
+  else if(!scs->quic_tp || !scs->quic_tp_len) {
+    CURL_TRC_CF(data, cf, "no 0RTT transport parameters, no early data, ");
+  }
+  else {
+    int rv;
+    rv = ngtcp2_conn_decode_and_set_0rtt_transport_params(
+      ctx->qconn, (uint8_t *)scs->quic_tp, scs->quic_tp_len);
+    if(rv)
+      CURL_TRC_CF(data, cf, "no early data, failed to set 0RTT transport "
+                  "parameters: %s", ngtcp2_strerror(rv));
+    else {
+      infof(data, "SSL session allows %zu bytes of early data, "
+            "reusing ALPN '%s'", ctx->earlydata_max, scs->alpn);
+      result = init_ngh3_conn(cf, data);
+      if(!result) {
+        ctx->use_earlydata = TRUE;
+        cf->connected = TRUE;
+        *do_early_data = TRUE;
+      }
+    }
+  }
+#else /* USE_GNUTLS */
+  (void)data;
+  (void)ctx;
+  (void)scs;
+#endif
+  return result;
+}
+
 /*
  * Might be called twice for happy eyeballs.
  */
@@ -2256,17 +2380,6 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
   int qfd;
 
   DEBUGASSERT(ctx->initialized);
-#define H3_ALPN "\x2h3\x5h3-29"
-  result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer,
-                               H3_ALPN, sizeof(H3_ALPN) - 1,
-                               tls_ctx_setup, &ctx->tls, &ctx->conn_ref);
-  if(result)
-    return result;
-
-#ifdef USE_OPENSSL
-  SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0);
-#endif
-
   ctx->dcid.datalen = NGTCP2_MAX_CIDLEN;
   result = Curl_rand(data, ctx->dcid.data, NGTCP2_MAX_CIDLEN);
   if(result)
@@ -2308,7 +2421,17 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
   if(rc)
     return CURLE_QUIC_CONNECT_ERROR;
 
+#define H3_ALPN "\x2h3\x5h3-29"
+  result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer,
+                               H3_ALPN, sizeof(H3_ALPN) - 1,
+                               cf_ngtcp2_tls_ctx_setup, &ctx->tls,
+                               &ctx->conn_ref,
+                               cf_ngtcp2_on_session_reuse);
+  if(result)
+    return result;
+
 #ifdef USE_OPENSSL
+  SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0);
   ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.ossl.ssl);
 #elif defined(USE_GNUTLS)
   ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.gtls.session);
@@ -2359,6 +2482,11 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf,
     result = cf_connect_start(cf, data, &pktx);
     if(result)
       goto out;
+    if(cf->connected) {
+      cf->conn->alpn = CURL_HTTP_VERSION_3;
+      *done = TRUE;
+      goto out;
+    }
     result = cf_progress_egress(cf, data, &pktx);
     /* we do not expect to be able to recv anything yet */
     goto out;
@@ -2373,10 +2501,7 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf,
     goto out;
 
   if(ngtcp2_conn_get_handshake_completed(ctx->qconn)) {
-    ctx->handshake_at = now;
-    CURL_TRC_CF(data, cf, "handshake complete after %dms",
-               (int)Curl_timediff(now, ctx->started_at));
-    result = qng_verify_peer(cf, data);
+    result = ctx->tls_vrfy_result;
     if(!result) {
       CURL_TRC_CF(data, cf, "peer verified");
       cf->connected = TRUE;
index d9e5e1da3ae1c2c7a496029092336b8b07ac5848..cf743d197204033ade3c894b1b66252f28acbb3b 100644 (file)
@@ -1166,7 +1166,7 @@ static CURLcode cf_osslq_ctx_start(struct Curl_cfilter *cf,
 #define H3_ALPN "\x2h3"
   result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer,
                                H3_ALPN, sizeof(H3_ALPN) - 1,
-                               NULL, NULL, NULL);
+                               NULL, NULL, NULL, NULL);
   if(result)
     goto out;
 
index 0c294e2bfcd1dbc83af5e523fe81477ac5724009..17025d99067b742e706a57071abddb93f43244bc 100644 (file)
@@ -1309,7 +1309,7 @@ static CURLcode cf_quiche_ctx_open(struct Curl_cfilter *cf,
   result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer,
                                QUICHE_H3_APPLICATION_PROTOCOL,
                                sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1,
-                               NULL, NULL, cf);
+                               NULL, NULL, cf, NULL);
   if(result)
     return result;
 
index e9fda86481ecb5d40209f575f310bf480ed2c79f..6e1ace2e3c2d4a8919b3c1dc493ae6074d7910da 100644 (file)
@@ -235,7 +235,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
                              struct ssl_peer *peer,
                              const char *alpn, size_t alpn_len,
                              Curl_vquic_tls_ctx_setup *cb_setup,
-                             void *cb_user_data, void *ssl_user_data)
+                             void *cb_user_data, void *ssl_user_data,
+                             Curl_vquic_session_reuse_cb *session_reuse_cb)
 {
   char tls_id[80];
   CURLcode result;
@@ -250,6 +251,7 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
 #error "no TLS lib in used, should not happen"
   return CURLE_FAILED_INIT;
 #endif
+  (void)session_reuse_cb;
   result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC);
   if(result)
     return result;
@@ -260,15 +262,16 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
                             (const unsigned char *)alpn, alpn_len,
                             cb_setup, cb_user_data, NULL, ssl_user_data);
 #elif defined(USE_GNUTLS)
-  (void)result;
   return Curl_gtls_ctx_init(&ctx->gtls, cf, data, peer,
-                            (const unsigned char *)alpn, alpn_len, NULL,
-                            cb_setup, cb_user_data, ssl_user_data);
+                            (const unsigned char *)alpn, alpn_len,
+                            cb_setup, cb_user_data, ssl_user_data,
+                            session_reuse_cb);
 #elif defined(USE_WOLFSSL)
   result = wssl_init_ctx(ctx, cf, data, cb_setup, cb_user_data);
   if(result)
     return result;
 
+  (void)session_reuse_cb;
   return wssl_init_ssl(ctx, cf, data, peer, alpn, alpn_len, ssl_user_data);
 #else
 #error "no TLS lib in used, should not happen"
index 8ee6904a549d122e480572d101b30a4c29391aea..c0706b0eb83529b258f37513a5d62849b3e1f730 100644 (file)
@@ -35,6 +35,7 @@
 #include "vtls/wolfssl.h"
 
 struct ssl_peer;
+struct Curl_ssl_session;
 
 struct curl_tls_ctx {
 #ifdef USE_OPENSSL
@@ -57,6 +58,11 @@ typedef CURLcode Curl_vquic_tls_ctx_setup(struct Curl_cfilter *cf,
                                           struct Curl_easy *data,
                                           void *cb_user_data);
 
+typedef CURLcode Curl_vquic_session_reuse_cb(struct Curl_cfilter *cf,
+                                             struct Curl_easy *data,
+                                             struct Curl_ssl_session *scs,
+                                             bool *do_early_data);
+
 /**
  * Initialize the QUIC TLS instances based of the SSL configurations
  * for the connection filter, transfer and peer.
@@ -68,8 +74,9 @@ typedef CURLcode Curl_vquic_tls_ctx_setup(struct Curl_cfilter *cf,
  *                    may be NULL
  * @param alpn_len    the overall number of bytes in `alpn`
  * @param cb_setup    optional callback for early TLS config
± @param cb_user_data user_data param for callback
* @param cb_user_data user_data param for callback
  * @param ssl_user_data  optional pointer to set in TLS application context
+ * @param session_reuse_cb callback to handle session reuse, signal early data
  */
 CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
                              struct Curl_cfilter *cf,
@@ -78,7 +85,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
                              const char *alpn, size_t alpn_len,
                              Curl_vquic_tls_ctx_setup *cb_setup,
                              void *cb_user_data,
-                             void *ssl_user_data);
+                             void *ssl_user_data,
+                             Curl_vquic_session_reuse_cb *session_reuse_cb);
 
 /**
  * Cleanup all data that has been initialized.
index acd158f027050bded938ea4441ae436e12c482e7..d3326bb45cf5e8d1d3c40df58519e330b1b36dee 100644 (file)
@@ -836,7 +836,7 @@ static CURLcode bearssl_connect_step3(struct Curl_cfilter *cf,
     ret = Curl_ssl_session_create((unsigned char *)session, sizeof(*session),
                                   (int)session->version,
                                   connssl->negotiated.alpn,
-                                  0, -1, &sc_session);
+                                  0, -1, 0, &sc_session);
     if(!ret) {
       ret = Curl_ssl_scache_put(cf, data, connssl->peer.scache_key,
                                 sc_session);
index 451bc92cbe4ebc4b6f6db15ef57452d107f911ce..879c6f3455c26e72d106f19f5dc4321e119a6235 100644 (file)
@@ -54,6 +54,7 @@
 #include "progress.h"
 #include "select.h"
 #include "strcase.h"
+#include "strdup.h"
 #include "warnless.h"
 #include "x509asn1.h"
 #include "multiif.h"
@@ -720,12 +721,15 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf,
                                  const char *ssl_peer_key,
                                  gnutls_session_t session,
                                  int lifetime_secs,
-                                 const char *alpn)
+                                 const char *alpn,
+                                 unsigned char *quic_tp,
+                                 size_t quic_tp_len)
 {
   struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
   struct Curl_ssl_session *sc_session;
-  unsigned char *sdata;
+  unsigned char *sdata, *qtp_clone = NULL;
   size_t sdata_len = 0;
+  size_t earlydata_max = 0;
   CURLcode result = CURLE_OK;
 
   if(!ssl_config->primary.cache_session)
@@ -750,11 +754,21 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf,
 
   CURL_TRC_CF(data, cf, "get session id (len=%zu, alpn=%s) and store in cache",
               sdata_len, alpn ? alpn : "-");
-  result = Curl_ssl_session_create(sdata, sdata_len,
-                                   Curl_glts_get_ietf_proto(session),
-                                   alpn, 0, lifetime_secs,
-                                   &sc_session);
-  /* call took ownership of `sdata`*/
+  earlydata_max = gnutls_record_get_max_early_data_size(session);
+  if(quic_tp && quic_tp_len) {
+    qtp_clone = Curl_memdup0((char *)quic_tp, quic_tp_len);
+    if(!qtp_clone) {
+      free(sdata);
+      return CURLE_OUT_OF_MEMORY;
+    }
+  }
+
+  result = Curl_ssl_session_create2(sdata, sdata_len,
+                                    Curl_glts_get_ietf_proto(session),
+                                    alpn, 0, lifetime_secs, earlydata_max,
+                                    qtp_clone, quic_tp_len,
+                                    &sc_session);
+  /* call took ownership of `sdata` and `qtp_clone` */
   if(!result) {
     result = Curl_ssl_scache_put(cf, data, ssl_peer_key, sc_session);
     /* took ownership of `sc_session` */
@@ -787,7 +801,7 @@ static CURLcode cf_gtls_update_session_id(struct Curl_cfilter *cf,
   struct ssl_connect_data *connssl = cf->ctx;
   return Curl_gtls_cache_session(cf, data, connssl->peer.scache_key,
                                  session, -1,
-                                 connssl->negotiated.alpn);
+                                 connssl->negotiated.alpn, NULL, 0);
 }
 
 static int gtls_handshake_cb(gnutls_session_t session, unsigned int htype,
@@ -819,6 +833,7 @@ static int gtls_handshake_cb(gnutls_session_t session, unsigned int htype,
 static CURLcode gtls_client_init(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  struct ssl_peer *peer,
+                                 size_t earlydata_max,
                                  struct gtls_ctx *gtls)
 {
   struct ssl_primary_config *config = Curl_ssl_cf_get_primary_config(cf);
@@ -872,6 +887,14 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
 
   /* Initialize TLS session as a client */
   init_flags = GNUTLS_CLIENT;
+  if(peer->transport == TRNSPRT_QUIC && earlydata_max > 0)
+    init_flags |= GNUTLS_ENABLE_EARLY_DATA | GNUTLS_NO_END_OF_EARLY_DATA;
+  else if(earlydata_max > 0 && earlydata_max != 0xFFFFFFFFUL)
+    /* See https://gitlab.com/gnutls/gnutls/-/issues/1619
+     * We cannot differentiate between a session announcing no earldata
+     * and one announcing 0xFFFFFFFFUL. On TCP+TLS, this is unlikely, but
+     * on QUIC this is common. */
+    init_flags |= GNUTLS_ENABLE_EARLY_DATA;
 
 #if defined(GNUTLS_FORCE_CLIENT_CERT)
   init_flags |= GNUTLS_FORCE_CLIENT_CERT;
@@ -893,6 +916,8 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
     init_flags |= GNUTLS_NO_STATUS_REQUEST;
 #endif
 
+  CURL_TRC_CF(data, cf, "gnutls_init(flags=%x), earlydata=%zu",
+              init_flags, earlydata_max);
   rc = gnutls_init(&gtls->session, init_flags);
   if(rc != GNUTLS_E_SUCCESS) {
     failf(data, "gnutls_init() failed: %d", rc);
@@ -1065,46 +1090,62 @@ static int keylog_callback(gnutls_session_t session, const char *label,
   return 0;
 }
 
+static CURLcode gtls_on_session_reuse(struct Curl_cfilter *cf,
+                                      struct Curl_easy *data,
+                                      struct Curl_ssl_session *scs,
+                                      bool *do_early_data)
+{
+  struct ssl_connect_data *connssl = cf->ctx;
+  struct gtls_ssl_backend_data *backend =
+    (struct gtls_ssl_backend_data *)connssl->backend;
+  CURLcode result = CURLE_OK;
+
+  *do_early_data = FALSE;
+  connssl->earlydata_max =
+    gnutls_record_get_max_early_data_size(backend->gtls.session);
+  if((!connssl->earlydata_max || connssl->earlydata_max == 0xFFFFFFFFUL)) {
+    /* Seems to be GnuTLS way to signal no EarlyData in session */
+    CURL_TRC_CF(data, cf, "SSL session does not allow earlydata");
+  }
+  else if(!Curl_alpn_contains_proto(connssl->alpn, scs->alpn)) {
+    CURL_TRC_CF(data, cf, "SSL session has different ALPN, no early data");
+  }
+  else {
+    infof(data, "SSL session allows %zu bytes of early data, "
+          "reusing ALPN '%s'", connssl->earlydata_max, scs->alpn);
+    connssl->earlydata_state = ssl_earlydata_use;
+    connssl->state = ssl_connection_deferred;
+    result = Curl_alpn_set_negotiated(cf, data, connssl,
+                    (const unsigned char *)scs->alpn,
+                    scs->alpn ? strlen(scs->alpn) : 0);
+    *do_early_data = !result;
+  }
+  return result;
+}
+
 CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx,
                             struct Curl_cfilter *cf,
                             struct Curl_easy *data,
                             struct ssl_peer *peer,
                             const unsigned char *alpn, size_t alpn_len,
-                            struct ssl_connect_data *connssl,
                             Curl_gtls_ctx_setup_cb *cb_setup,
                             void *cb_user_data,
-                            void *ssl_user_data)
+                            void *ssl_user_data,
+                            Curl_gtls_init_session_reuse_cb *sess_reuse_cb)
 {
   struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
   struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
   struct Curl_ssl_session *scs = NULL;
   gnutls_datum_t gtls_alpns[5];
   size_t gtls_alpns_count = 0;
+  bool gtls_session_setup = FALSE;
   CURLcode result;
   int rc;
 
   DEBUGASSERT(gctx);
-
-  result = gtls_client_init(cf, data, peer, gctx);
-  if(result)
-    return result;
-
-  gnutls_session_set_ptr(gctx->session, ssl_user_data);
-
-  if(cb_setup) {
-    result = cb_setup(cf, data, cb_user_data);
-    if(result)
-      return result;
-  }
-
-  /* Open the file if a TLS or QUIC backend has not done this before. */
-  Curl_tls_keylog_open();
-  if(Curl_tls_keylog_enabled()) {
-    gnutls_session_set_keylog_function(gctx->session, keylog_callback);
-  }
-
   /* This might be a reconnect, so we check for a session ID in the cache
-     to speed up things */
+     to speed up things. We need to do this before constructing the gnutls
+     session since we need to set flags depending on the kind of reuse. */
   if(conn_config->cache_session) {
     result = Curl_ssl_scache_take(cf, data, peer->scache_key, &scs);
     if(result)
@@ -1112,35 +1153,28 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx,
 
     if(scs && scs->sdata && scs->sdata_len) {
       /* we got a cached session, use it! */
+
+      result = gtls_client_init(cf, data, peer, scs->earlydata_max, gctx);
+      if(result)
+        goto out;
+      gtls_session_setup = TRUE;
+
       rc = gnutls_session_set_data(gctx->session, scs->sdata, scs->sdata_len);
-      if(rc < 0) {
+      if(rc < 0)
         infof(data, "SSL session not accepted by GnuTLS, continuing without");
-      }
       else {
         infof(data, "SSL reusing session with ALPN '%s'",
               scs->alpn ? scs->alpn : "-");
         if(ssl_config->earlydata &&
-           !cf->conn->connect_only && connssl &&
-           (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3) &&
-           Curl_alpn_contains_proto(connssl->alpn, scs->alpn)) {
-          connssl->earlydata_max =
-            gnutls_record_get_max_early_data_size(gctx->session);
-          if((!connssl->earlydata_max ||
-              connssl->earlydata_max == 0xFFFFFFFFUL)) {
-            /* Seems to be GnuTLS way to signal no EarlyData in session */
-            CURL_TRC_CF(data, cf, "TLS session does not allow earlydata");
-          }
-          else {
-            CURL_TRC_CF(data, cf, "TLS session allows %zu earlydata bytes, "
-                        "reusing ALPN '%s'",
-                        connssl->earlydata_max, scs->alpn);
-            connssl->earlydata_state = ssl_earlydata_use;
-            connssl->state = ssl_connection_deferred;
-            result = Curl_alpn_set_negotiated(cf, data, connssl,
-                            (const unsigned char *)scs->alpn,
-                            scs->alpn ? strlen(scs->alpn) : 0);
+           !cf->conn->connect_only &&
+           (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3)) {
+          bool do_early_data = FALSE;
+          if(sess_reuse_cb) {
+            result = sess_reuse_cb(cf, data, scs, &do_early_data);
             if(result)
-              goto out;
+              goto  out;
+          }
+          if(do_early_data) {
             /* We only try the ALPN protocol the session used before,
              * otherwise we might send early data for the wrong protocol */
             gtls_alpns[0].data = (unsigned char *)scs->alpn;
@@ -1161,6 +1195,26 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx,
     }
   }
 
+  if(!gtls_session_setup) {
+    result = gtls_client_init(cf, data, peer, 0, gctx);
+    if(result)
+      goto out;
+  }
+
+  gnutls_session_set_ptr(gctx->session, ssl_user_data);
+
+  if(cb_setup) {
+    result = cb_setup(cf, data, cb_user_data);
+    if(result)
+      goto out;
+  }
+
+  /* Open the file if a TLS or QUIC backend has not done this before. */
+  Curl_tls_keylog_open();
+  if(Curl_tls_keylog_enabled()) {
+    gnutls_session_set_keylog_function(gctx->session, keylog_callback);
+  }
+
   /* convert the ALPN string from our arguments to a list of strings that
    * gnutls wants and will convert internally back to this string for sending
    * to the server. nice. */
@@ -1225,7 +1279,7 @@ gtls_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data)
 
   result = Curl_gtls_ctx_init(&backend->gtls, cf, data, &connssl->peer,
                               proto.data, proto.len,
-                              connssl, NULL, NULL, cf);
+                              NULL, NULL, cf, gtls_on_session_reuse);
   if(result)
     return result;
 
index 1b7c098cf0020e1d59307c8c07dd68903b936419..a40e68097ca0d71084883fbcb2527494d7794c6b 100644 (file)
@@ -46,6 +46,7 @@ struct ssl_primary_config;
 struct ssl_config_data;
 struct ssl_peer;
 struct ssl_connect_data;
+struct Curl_ssl_session;
 
 int Curl_glts_get_ietf_proto(gnutls_session_t session);
 
@@ -78,15 +79,20 @@ typedef CURLcode Curl_gtls_ctx_setup_cb(struct Curl_cfilter *cf,
                                         struct Curl_easy *data,
                                         void *user_data);
 
+typedef CURLcode Curl_gtls_init_session_reuse_cb(struct Curl_cfilter *cf,
+                                                 struct Curl_easy *data,
+                                                 struct Curl_ssl_session *scs,
+                                                 bool *do_early_data);
+
 CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx,
                             struct Curl_cfilter *cf,
                             struct Curl_easy *data,
                             struct ssl_peer *peer,
                             const unsigned char *alpn, size_t alpn_len,
-                            struct ssl_connect_data *connssl,
                             Curl_gtls_ctx_setup_cb *cb_setup,
                             void *cb_user_data,
-                            void *ssl_user_data);
+                            void *ssl_user_data,
+                            Curl_gtls_init_session_reuse_cb *sess_reuse_cb);
 
 CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
                                       struct Curl_easy *data,
@@ -105,7 +111,9 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf,
                                  const char *ssl_peer_key,
                                  gnutls_session_t session,
                                  int lifetime_secs,
-                                 const char *alpn);
+                                 const char *alpn,
+                                 unsigned char *quic_tp,
+                                 size_t quic_tp_len);
 
 extern const struct Curl_ssl Curl_ssl_gnutls;
 
index 1739084c9ac7d1eadfdb812a9b67fa4a46a1d423..e5d162df8370966d7ac37db9ffe0f48b66d4cc5d 100644 (file)
@@ -1171,7 +1171,7 @@ mbed_new_session(struct Curl_cfilter *cf, struct Curl_easy *data)
 #endif
   result = Curl_ssl_session_create(sdata, slen,
                                    ietf_tls_id,
-                                   connssl->negotiated.alpn, 0, -1,
+                                   connssl->negotiated.alpn, 0, -1, 0,
                                    &sc_session);
   sdata = NULL;  /* call took ownership */
   if(!result)
index 955f2bc743571a8eb29f7636056b6cb63a13f07d..a6549d272fa054c23bce16ac78c1c079967b8b84 100644 (file)
@@ -2903,7 +2903,7 @@ CURLcode Curl_ossl_add_session(struct Curl_cfilter *cf,
 
     result = Curl_ssl_session_create(der_session_buf, der_session_size,
                                      ietf_tls_id, alpn, 0,
-                                     SSL_SESSION_get_timeout(session),
+                                     SSL_SESSION_get_timeout(session), 0,
                                      &sc_session);
     der_session_buf = NULL;  /* took ownership of sdata */
     if(!result) {
index a9215223e7c8885e26a1821782cef2ef6ea0e650..b06a4ca805399bb8be290e149044ebe1d825f9b7 100644 (file)
@@ -103,6 +103,11 @@ static void cf_ssl_scache_clear_session(struct Curl_ssl_session *s)
     s->sdata = NULL;
   }
   s->sdata_len = 0;
+  if(s->quic_tp) {
+    free((void *)s->quic_tp);
+    s->quic_tp = NULL;
+  }
+  s->quic_tp_len = 0;
   s->ietf_tls_id = 0;
   s->time_received = 0;
   s->lifetime_secs = 0;
@@ -120,7 +125,21 @@ CURLcode
 Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len,
                         int ietf_tls_id, const char *alpn,
                         curl_off_t time_received, long lifetime_secs,
+                        size_t earlydata_max,
                         struct Curl_ssl_session **psession)
+{
+  return Curl_ssl_session_create2(sdata, sdata_len, ietf_tls_id, alpn,
+                                  time_received, lifetime_secs,
+                                  earlydata_max, NULL, 0, psession);
+}
+
+CURLcode
+Curl_ssl_session_create2(unsigned char *sdata, size_t sdata_len,
+                         int ietf_tls_id, const char *alpn,
+                         curl_off_t time_received, long lifetime_secs,
+                         size_t earlydata_max,
+                         unsigned char *quic_tp, size_t quic_tp_len,
+                         struct Curl_ssl_session **psession)
 {
   struct Curl_ssl_session *s;
 
@@ -133,6 +152,7 @@ Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len,
   s = calloc(1, sizeof(*s));
   if(!s) {
     free(sdata);
+    free(quic_tp);
     return CURLE_OUT_OF_MEMORY;
   }
 
@@ -147,8 +167,11 @@ Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len,
     lifetime_secs = CURL_SCACHE_MAX_12_LIFETIME_SEC;
 
   s->lifetime_secs = (int)lifetime_secs;
+  s->earlydata_max = earlydata_max;
   s->sdata = sdata;
   s->sdata_len = sdata_len;
+  s->quic_tp = quic_tp;
+  s->quic_tp_len = quic_tp_len;
   if(alpn) {
     s->alpn = strdup(alpn);
     if(!s->alpn) {
@@ -738,8 +761,10 @@ out:
   }
   else
     CURL_TRC_CF(data, cf, "[SCACHE] added session for %s [proto=0x%x, "
-                "lifetime=%d, alpn=%s], peer has %zu sessions now",
+                "lifetime=%d, alpn=%s, earlydata=%zu, quic_tp=%s], "
+                "peer has %zu sessions now",
                 ssl_peer_key, s->ietf_tls_id, s->lifetime_secs, s->alpn,
+                s->earlydata_max, s->quic_tp ? "yes" : "no",
                 Curl_llist_count(&peer->sessions));
   return result;
 }
@@ -779,6 +804,7 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf,
   struct Curl_ssl_scache *scache = data->state.ssl_scache;
   struct Curl_ssl_scache_peer *peer = NULL;
   struct Curl_llist_node *n;
+  struct Curl_ssl_session *s = NULL;
   CURLcode result;
 
   *ps = NULL;
@@ -791,15 +817,24 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf,
     cf_scache_peer_remove_expired(peer, (curl_off_t)time(NULL));
     n = Curl_llist_head(&peer->sessions);
     if(n) {
-      *ps = Curl_node_take_elem(n);
+      s = Curl_node_take_elem(n);
       (scache->age)++;            /* increase general age */
       peer->age = scache->age; /* set this as used in this age */
     }
   }
   Curl_ssl_scache_unlock(data);
-
-  CURL_TRC_CF(data, cf, "[SCACHE] %s cached session for '%s'",
-              *ps ? "Found" : "No", ssl_peer_key);
+  if(s) {
+    *ps = s;
+    CURL_TRC_CF(data, cf, "[SCACHE] took session for %s [proto=0x%x, "
+                "lifetime=%d, alpn=%s, earlydata=%zu, quic_tp=%s], "
+                "%zu sessions remain",
+                ssl_peer_key, s->ietf_tls_id, s->lifetime_secs, s->alpn,
+                s->earlydata_max, s->quic_tp ? "yes" : "no",
+                Curl_llist_count(&peer->sessions));
+  }
+  else {
+    CURL_TRC_CF(data, cf, "[SCACHE] no cached session for %s", ssl_peer_key);
+  }
   return result;
 }
 
index a3e3cac3d25d25f76663d13c87791a8536183307..33d426a38e7e8045d2a358df02daedbe53887237 100644 (file)
@@ -122,6 +122,9 @@ struct Curl_ssl_session {
   int lifetime_secs;           /* ticket lifetime (-1 unknown) */
   int ietf_tls_id;             /* TLS protocol identifier negotiated */
   char *alpn;                  /* APLN TLS negotiated protocol string */
+  size_t earlydata_max;        /* max 0-RTT data supported by peer */
+  const unsigned char *quic_tp; /* Optional QUIC transport param bytes */
+  size_t quic_tp_len;          /* number of bytes in quic_tp */
   struct Curl_llist_node list; /*  internal storage handling */
 };
 
@@ -142,8 +145,19 @@ CURLcode
 Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len,
                         int ietf_tls_id, const char *alpn,
                         curl_off_t time_received, long lifetime_secs,
+                        size_t earlydata_max,
                         struct Curl_ssl_session **psession);
 
+/* Variation of session creation with quic transport parameter bytes,
+ * Takes ownership of `quic_tp` regardless of return code. */
+CURLcode
+Curl_ssl_session_create2(unsigned char *sdata, size_t sdata_len,
+                         int ietf_tls_id, const char *alpn,
+                         curl_off_t time_received, long lifetime_secs,
+                         size_t earlydata_max,
+                         unsigned char *quic_tp, size_t quic_tp_len,
+                         struct Curl_ssl_session **psession);
+
 /* Destroy a `session` instance. Can be called with NULL.
  * Does NOT need locking. */
 void Curl_ssl_session_destroy(struct Curl_ssl_session *s);
index 800aa2ecb4c58e48101b6d4926e3c86da2b88f6a..a17f1cfddeafdb5c9e354f971871128810650649 100644 (file)
@@ -432,7 +432,7 @@ CURLcode Curl_wssl_cache_session(struct Curl_cfilter *cf,
 
   result = Curl_ssl_session_create(sdata, sdata_len,
                                    ietf_tls_id, alpn, 0,
-                                   wolfSSL_SESSION_get_timeout(session),
+                                   wolfSSL_SESSION_get_timeout(session), 0,
                                    &sc_session);
   sdata = NULL;  /* took ownership of sdata */
   if(!result) {
index b2acaca7b62aebbb889df8cb1571c56229bcdd49..21908c1882a593e4fa081f22320ae58515611b17 100644 (file)
@@ -631,8 +631,7 @@ class TestDownload:
         elif proto == 'h2':
             assert earlydata[1] == 107, f'{earlydata}'
         elif proto == 'h3':
-            # not implemented
-            assert earlydata[1] == 0, f'{earlydata}'
+            assert earlydata[1] == 67, f'{earlydata}'
 
     @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
     @pytest.mark.parametrize("max_host_conns", [0, 1, 5])
index dcafe7c064e7c99780d1b3edb84267797862ac64..a814bf043adaf43c11042011fdb3eb97a5383d06 100644 (file)
@@ -697,7 +697,10 @@ class TestUpload:
         ['http/1.1', 32*1024, 16384],  # headers+body, limited by server max
         ['h2', 10*1024, 10378],        # headers+body
         ['h2', 32*1024, 16384],        # headers+body, limited by server max
-        ['h3', 1024, 0],               # earlydata not supported
+        ['h3', 1024, 1126],            # headers+body (app data)
+        ['h3', 1024 * 1024, 131177],   # headers+body (long app data). The 0RTT
+                                       # size is limited by our sendbuf size
+                                       # of 128K.
     ])
     def test_07_70_put_earlydata(self, env: Env, httpd, nghttpx, proto, upload_size, exp_early):
         if not env.curl_uses_lib('gnutls'):
@@ -727,7 +730,7 @@ class TestUpload:
         self.check_downloads(client, [f"{upload_size}"], count)
         earlydata = {}
         for line in r.trace_lines:
-            m = re.match(r'^\[t-(\d+)] EarlyData: (\d+)', line)
+            m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line)
             if m:
                 earlydata[int(m.group(1))] = int(m.group(2))
         assert earlydata[0] == 0, f'{earlydata}'
index 335f76e6fcec357b85bd265cf67dc6dea474bb12..2f7f649c082a80ecc11ce24201cb165ca87458d0 100644 (file)
@@ -210,7 +210,7 @@ class TestCaddy:
             respdata = open(curl.response_file(i)).readlines()
             assert respdata == exp_data
 
-    @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
+    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
     def test_08_08_earlydata(self, env: Env, httpd, caddy, proto):
         count = 2
         docname = 'data10k.data'
@@ -230,12 +230,15 @@ class TestCaddy:
         self.check_downloads(client, srcfile, count)
         earlydata = {}
         for line in r.trace_lines:
-            m = re.match(r'^\[t-(\d+)] EarlyData: (\d+)', line)
+            m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line)
             if m:
                 earlydata[int(m.group(1))] = int(m.group(2))
-        # Caddy does not support early data
         assert earlydata[0] == 0, f'{earlydata}'
-        assert earlydata[1] == 0, f'{earlydata}'
+        if proto == 'h3':
+            assert earlydata[1] == 71, f'{earlydata}'
+        else:
+            # Caddy does not support early data on TCP
+            assert earlydata[1] == 0, f'{earlydata}'
 
     def check_downloads(self, client, srcfile: str, count: int,
                         complete: bool = True):
index 801d9a63ad0bb160d3c3028ee7d0e0606cf88dd4..03200beba94ecb5cc5b9026badbc0fbf9d4d2de0 100644 (file)
@@ -205,6 +205,7 @@ class NghttpxQuic(Nghttpx):
         args = [
             self._cmd,
             f'--frontend=*,{self.env.h3_port};quic',
+            '--frontend-quic-early-data',
             f'--frontend=*,{self.env.nghttpx_https_port};tls',
             f'--backend=127.0.0.1,{self.env.https_port};{self.env.domain1};sni={self.env.domain1};proto=h2;tls',
             f'--backend=127.0.0.1,{self.env.http_port}',