tftp-blksize.md \
tftp-no-options.md \
time-cond.md \
+ tls-earlydata.md \
tls-max.md \
tls13-ciphers.md \
tlsauthtype.md \
--- /dev/null
+---
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+SPDX-License-Identifier: curl
+Long: tls-earlydata
+Help: Allow use of TLSv1.3 early data (0RTT)
+Protocols: TLS
+Added: 8.11.0
+Category: tls
+Multi: boolean
+See-also:
+ - tlsv1.3
+ - tls-max
+Example:
+ - --tls-earlydata $URL
+---
+
+# `--tls-earlydata`
+
+Enable the use of TLSv1.3 early data, also known as '0RTT' where possible.
+This has security implications for the requests sent that way.
+
+This option is used when curl is built to use GnuTLS.
+
+If a server supports this TLSv1.3 feature, and to what extent, is announced
+as part of the TLS "session" sent back to curl. Until curl has seen such
+a session in a previous request, early data cannot be used.
+
+When a new connection is initiated with a known TLSv1.3 session, and that
+session announced early data support, the first request on this connection is
+sent *before* the TLS handshake is complete. While the early data is also
+encrypted, it is not protected against replays. An attacker can send
+your early data to the server again and the server would accept it.
+
+If your request contacts a public server and only retrieves a file, there
+may be no harm in that. If the first request orders a refrigerator
+for you, it is probably not a good idea to use early data for it. curl
+cannot deduce what the security implications of your requests actually
+are and make this decision for you.
+
+**WARNING**: this option has security implications. See above for more
+details.
List of all known cookies. See CURLINFO_COOKIELIST(3)
+## CURLINFO_EARLYDATA_SENT_T
+
+Amount of TLS early data sent (in number of bytes) when
+CURLSSLOPT_EARLYDATA is enabled.
+
## CURLINFO_EFFECTIVE_METHOD
Last used HTTP method. See CURLINFO_EFFECTIVE_METHOD(3)
--- /dev/null
+---
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+SPDX-License-Identifier: curl
+Title: CURLINFO_EARLYDATA_SENT_T
+Section: 3
+Source: libcurl
+See-also:
+ - curl_easy_getinfo (3)
+ - curl_easy_setopt (3)
+Protocol:
+ - TLS
+TLS-backend:
+ - GnuTLS
+Added-in: 8.11.0
+---
+
+# NAME
+
+CURLINFO_EARLYDATA_SENT_T - get the number of bytes sent as TLS early data
+
+# SYNOPSIS
+
+~~~c
+#include <curl/curl.h>
+
+CURLcode curl_easy_getinfo(CURL *handle, CURLINFO_EARLYDATA_SENT_T,
+ curl_off_t *amount);
+~~~
+
+# DESCRIPTION
+
+Pass a pointer to an *curl_off_t* to receive the total amount of bytes that
+were sent to the server as TLSv1.3 early data. When no TLS early
+data is used, this reports 0.
+
+TLS early data is only attempted when CURLSSLOPT_EARLYDATA is set for the
+transfer. In addition, it is only used by libcurl when a TLS session exists
+that announces support.
+
+The amount is **negative** when the sent data was rejected
+by the server. TLS allows a server that announces support for early data to
+reject any attempt to use it at its own discretion. When for example 127
+bytes had been sent, but were rejected, it reports -127 as the amount "sent".
+
+# %PROTOCOLS%
+
+# EXAMPLE
+
+~~~c
+int main(void)
+{
+ CURL *curl = curl_easy_init();
+ if(curl) {
+ CURLcode res;
+ curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
+
+ /* Perform the request */
+ res = curl_easy_perform(curl);
+
+ if(!res) {
+ curl_off_t amount;
+ res = curl_easy_getinfo(curl, CURLINFO_EARLYDATA_SENT_T, &amount);
+ if(!res) {
+ printf("TLS earlydata: %" CURL_FORMAT_CURL_OFF_T " bytes\n", amount);
+ }
+ }
+ }
+}
+~~~
+
+# %AVAILABILITY%
+
+# RETURN VALUE
+
+Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not.
could be a privacy violation and unexpected.
(Added in 7.77.0)
+## CURLSSLOPT_EARLYDATA
+
+Tell libcurl to try sending application data as TLS1.3 early data. This option
+is only supported for GnuTLS. This option works on a best effort basis,
+in cases when it wasn't possible to send early data the request is resent
+normally post-handshake.
+This option does not work when using QUIC.
+(Added in 8.11.0)
+
# DEFAULT
0
CURLOPT_DOH_SSL_VERIFYPEER.3 \
CURLOPT_DOH_SSL_VERIFYSTATUS.3 \
CURLOPT_DOH_URL.3 \
+ CURLINFO_EARLYDATA_SENT_T.3 \
CURLOPT_ECH.3 \
CURLOPT_EGDSOCKET.3 \
CURLOPT_ERRORBUFFER.3 \
CURLINFO_DATA_IN 7.9.6
CURLINFO_DATA_OUT 7.9.6
CURLINFO_DOUBLE 7.4.1
+CURLINFO_EARLYDATA_SENT_T 8.11.0
CURLINFO_EFFECTIVE_METHOD 7.72.0
CURLINFO_EFFECTIVE_URL 7.4
CURLINFO_END 7.9.6
CURLSSLOPT_NO_PARTIALCHAIN 7.68.0
CURLSSLOPT_NO_REVOKE 7.44.0
CURLSSLOPT_REVOKE_BEST_EFFORT 7.70.0
+CURLSSLOPT_EARLYDATA 8.11.0
CURLSSLSET_NO_BACKENDS 7.56.0
CURLSSLSET_OK 7.56.0
CURLSSLSET_TOO_LATE 7.56.0
--tftp-blksize 7.20.0
--tftp-no-options 7.48.0
--time-cond (-z) 5.8
+--tls-earlydata 8.11.0
--tls-max 7.54.0
--tls13-ciphers 7.61.0
--tlsauthtype 7.21.4
a client certificate for authentication. (Schannel) */
#define CURLSSLOPT_AUTO_CLIENT_CERT (1<<5)
+/* If possible, send data using TLS 1.3 early data */
+#define CURLSSLOPT_EARLYDATA (1<<6)
+
/* The default connection attempt delay in milliseconds for happy eyeballs.
CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.3 and happy-eyeballs-timeout-ms.d document
this value, keep them in sync. */
CURLINFO_QUEUE_TIME_T = CURLINFO_OFF_T + 65,
CURLINFO_USED_PROXY = CURLINFO_LONG + 66,
CURLINFO_POSTTRANSFER_TIME_T = CURLINFO_OFF_T + 67,
- CURLINFO_LASTONE = 67
+ CURLINFO_EARLYDATA_SENT_T = CURLINFO_OFF_T + 68,
+ CURLINFO_LASTONE = 68
} CURLINFO;
/* CURLINFO_RESPONSE_CODE is the new name for the option previously known as
{
struct cf_hc_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
+ int reply_ms;
DEBUGASSERT(winner->cf);
if(winner != &ctx->h3_baller)
if(winner != &ctx->h21_baller)
cf_hc_baller_reset(&ctx->h21_baller, data);
- CURL_TRC_CF(data, cf, "connect+handshake %s: %dms, 1st data: %dms",
- winner->name, (int)Curl_timediff(Curl_now(), winner->started),
- cf_hc_baller_reply_ms(winner, data));
+ reply_ms = cf_hc_baller_reply_ms(winner, data);
+ if(reply_ms >= 0)
+ CURL_TRC_CF(data, cf, "connect+handshake %s: %dms, 1st data: %dms",
+ winner->name, (int)Curl_timediff(Curl_now(), winner->started),
+ reply_ms);
+ else
+ CURL_TRC_CF(data, cf, "deferred handshake %s: %dms",
+ winner->name, (int)Curl_timediff(Curl_now(), winner->started));
+
cf->next = winner->cf;
winner->cf = NULL;
*param_offt = data->conn ?
data->conn->connection_id : data->state.recent_conn_id;
break;
+ case CURLINFO_EARLYDATA_SENT_T:
+ *param_offt = data->progress.earlydata_sent;
+ break;
default:
return CURLE_UNKNOWN_OPTION;
}
(void)flags;
DEBUGASSERT(data);
- nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen,
- nw_out_writer, cf, &result);
+ if(!cf->connected)
+ nwritten = Curl_bufq_write(&ctx->outbufq, buf, blen, &result);
+ else
+ nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen,
+ nw_out_writer, cf, &result);
if(nwritten < 0) {
if(result == CURLE_AGAIN) {
ctx->nw_out_blocked = 1;
nghttp2_strerror(rv), rv);
return CURLE_SEND_ERROR;
}
+ /* Defer flushing during the connect phase so that the SETTINGS and
+ * other initial frames are sent together with the first request.
+ * Unless we are 'connect_only' where the request will never come. */
+ if(!cf->connected && !cf->conn->connect_only)
+ return CURLE_OK;
return nw_out_flush(cf, data);
}
struct cf_h2_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
struct cf_call_data save;
+ bool first_time = FALSE;
if(cf->connected) {
*done = TRUE;
result = cf_h2_ctx_open(cf, data);
if(result)
goto out;
+ first_time = TRUE;
}
- result = h2_progress_ingress(cf, data, H2_CHUNK_SIZE);
- if(result)
- goto out;
+ if(!first_time) {
+ result = h2_progress_ingress(cf, data, H2_CHUNK_SIZE);
+ if(result)
+ goto out;
+ }
/* Send out our SETTINGS and ACKs and such. If that blocks, we
* have it buffered and can count this filter as being connected */
}
}
+void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent)
+{
+ data->progress.earlydata_sent = sent;
+}
+
/* returns the average speed in bytes / second */
static curl_off_t trspeed(curl_off_t size, /* number of bytes */
curl_off_t us) /* microseconds */
void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer,
struct curltime timestamp);
+void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent);
+
#define PGRS_HIDE (1<<4)
#define PGRS_UL_SIZE_KNOWN (1<<5)
#define PGRS_DL_SIZE_KNOWN (1<<6)
data->set.ssl.revoke_best_effort = !!(arg & CURLSSLOPT_REVOKE_BEST_EFFORT);
data->set.ssl.native_ca_store = !!(arg & CURLSSLOPT_NATIVE_CA);
data->set.ssl.auto_client_cert = !!(arg & CURLSSLOPT_AUTO_CLIENT_CERT);
+ data->set.ssl.earlydata = !!(arg & CURLSSLOPT_EARLYDATA);
/* If a setting is added here it should also be added in dohprobe()
which sets its own CURLOPT_SSL_OPTIONS based on these settings. */
break;
char *key_passwd; /* plain text private key password */
BIT(certinfo); /* gather lots of certificate info */
BIT(falsestart);
+ BIT(earlydata); /* use tls1.3 early data */
BIT(enable_beast); /* allow this flaw for interoperability's sake */
BIT(no_revoke); /* disable SSL certificate revocation checks */
BIT(no_partialchain); /* do not accept partial certificate chains */
char *name; /* hostname for which this ID was used */
char *conn_to_host; /* hostname for the connection (may be NULL) */
const char *scheme; /* protocol scheme used */
+ char *alpn; /* APLN TLS negotiated protocol string */
void *sessionid; /* as returned from the SSL layer */
size_t idsize; /* if known, otherwise 0 */
Curl_ssl_sessionid_dtor *sessionid_free; /* free `sessionid` callback */
struct pgrs_dir dl;
curl_off_t current_speed; /* uses the currently fastest transfer */
+ curl_off_t earlydata_sent;
int width; /* screen width at download start */
int flags; /* see progress.h */
CURL_TRC_CF(data, cf, "connect_step1, check session cache");
Curl_ssl_sessionid_lock(data);
- if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, &session, NULL)) {
+ if(!Curl_ssl_getsessionid(cf, data, &connssl->peer,
+ &session, NULL, NULL)) {
br_ssl_engine_set_session_parameters(&backend->ctx.eng, session);
session_set = 1;
infof(data, "BearSSL: reusing session ID");
const char *proto;
proto = br_ssl_engine_get_selected_protocol(&backend->ctx.eng);
- Curl_alpn_set_negotiated(cf, data, (const unsigned char *)proto,
+ Curl_alpn_set_negotiated(cf, data, connssl, (const unsigned char *)proto,
proto ? strlen(proto) : 0);
}
return CURLE_OUT_OF_MEMORY;
br_ssl_engine_get_session_parameters(&backend->ctx.eng, session);
Curl_ssl_sessionid_lock(data);
- ret = Curl_ssl_set_sessionid(cf, data, &connssl->peer, session, 0,
+ ret = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, session, 0,
bearssl_session_free);
Curl_ssl_sessionid_unlock(data);
if(ret)
#include "vauth/vauth.h"
#include "parsedate.h"
#include "connect.h" /* for the connect timeout */
+#include "progress.h"
#include "select.h"
#include "strcase.h"
#include "warnless.h"
}
else if(0 == what) {
if(nonblocking)
- return CURLE_OK;
+ return CURLE_AGAIN;
else if(timeout_ms) {
/* timeout */
failf(data, "SSL connection timeout at %ld", (long)timeout_ms);
/* get the session ID data size */
gnutls_session_get_data(session, NULL, &connect_idsize);
+ if(!connect_idsize) /* gnutls does this for some version combinations */
+ return CURLE_OK;
+
connect_sessionid = malloc(connect_idsize); /* get a buffer for it */
if(!connect_sessionid) {
return CURLE_OUT_OF_MEMORY;
Curl_ssl_sessionid_lock(data);
/* store this session id, takes ownership */
result = Curl_ssl_set_sessionid(cf, data, &connssl->peer,
+ connssl->alpn_negotiated,
connect_sessionid, connect_idsize,
gtls_sessionid_free);
Curl_ssl_sessionid_unlock(data);
init_flags |= GNUTLS_FORCE_CLIENT_CERT;
#endif
-#if defined(GNUTLS_NO_TICKETS)
- /* Disable TLS session tickets */
- init_flags |= GNUTLS_NO_TICKETS;
+#if defined(GNUTLS_NO_TICKETS_TLS12)
+ init_flags |= GNUTLS_NO_TICKETS_TLS12;
+#elif defined(GNUTLS_NO_TICKETS)
+ /* Disable TLS session tickets for non 1.3 connections */
+ if((config->version != CURL_SSLVERSION_TLSv1_3) &&
+ (config->version != CURL_SSLVERSION_DEFAULT))
+ init_flags |= GNUTLS_NO_TICKETS;
#endif
#if defined(GNUTLS_NO_STATUS_REQUEST)
void *ssl_user_data)
{
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 ssl_connect_data *connssl = cf->ctx;
+ gnutls_datum_t gtls_alpns[5];
+ size_t gtls_alpns_count = 0;
CURLcode result;
DEBUGASSERT(gctx);
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 */
+ if(conn_config->cache_session) {
+ void *ssl_sessionid;
+ size_t ssl_idsize;
+ char *session_alpn;
+ Curl_ssl_sessionid_lock(data);
+ if(!Curl_ssl_getsessionid(cf, data, peer,
+ &ssl_sessionid, &ssl_idsize, &session_alpn)) {
+ /* we got a session id, use it! */
+ int rc;
+
+ rc = gnutls_session_set_data(gctx->session, ssl_sessionid, ssl_idsize);
+ if(rc < 0)
+ infof(data, "SSL failed to set session ID");
+ else {
+ infof(data, "SSL reusing session ID (size=%zu)", ssl_idsize);
+#ifdef DEBUGBUILD
+ if((ssl_config->earlydata || !!getenv("CURL_USE_EARLYDATA")) &&
+#else
+ if(ssl_config->earlydata &&
+#endif
+ !cf->conn->connect_only &&
+ (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3) &&
+ Curl_alpn_contains_proto(connssl->alpn, session_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, session_alpn);
+ connssl->earlydata_state = ssl_earlydata_use;
+ connssl->state = ssl_connection_deferred;
+ result = Curl_alpn_set_negotiated(cf, data, connssl,
+ (const unsigned char *)session_alpn,
+ session_alpn ? strlen(session_alpn) : 0);
+ if(result)
+ return result;
+ /* 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 *)session_alpn;
+ gtls_alpns[0].size = (unsigned)strlen(session_alpn);
+ gtls_alpns_count = 1;
+ }
+ }
+ }
+ }
+ Curl_ssl_sessionid_unlock(data);
+ }
+
/* convert the ALPN string from our arguments to a list of strings
* that gnutls wants and will convert internally back to this very
* string for sending to the server. nice. */
- if(alpn && alpn_len) {
- gnutls_datum_t alpns[5];
+ if(!gtls_alpns_count && alpn && alpn_len) {
size_t i, alen = alpn_len;
unsigned char *s = (unsigned char *)alpn;
unsigned char slen;
- for(i = 0; (i < ARRAYSIZE(alpns)) && alen; ++i) {
+ for(i = 0; (i < ARRAYSIZE(gtls_alpns)) && alen; ++i) {
slen = s[0];
if(slen >= alen)
return CURLE_FAILED_INIT;
- alpns[i].data = s + 1;
- alpns[i].size = slen;
+ gtls_alpns[i].data = s + 1;
+ gtls_alpns[i].size = slen;
s += slen + 1;
alen -= (size_t)slen + 1;
}
if(alen) /* not all alpn chars used, wrong format or too many */
return CURLE_FAILED_INIT;
- if(i && gnutls_alpn_set_protocols(gctx->session,
- alpns, (unsigned int)i,
- GNUTLS_ALPN_MANDATORY)) {
- failf(data, "failed setting ALPN");
- return CURLE_SSL_CONNECT_ERROR;
- }
+ gtls_alpns_count = i;
}
- /* This might be a reconnect, so we check for a session ID in the cache
- to speed up things */
- if(conn_config->cache_session) {
- void *ssl_sessionid;
- size_t ssl_idsize;
-
- Curl_ssl_sessionid_lock(data);
- if(!Curl_ssl_getsessionid(cf, data, peer, &ssl_sessionid, &ssl_idsize)) {
- /* we got a session id, use it! */
- int rc;
-
- rc = gnutls_session_set_data(gctx->session, ssl_sessionid, ssl_idsize);
- if(rc < 0)
- infof(data, "SSL failed to set session ID");
- else
- infof(data, "SSL reusing session ID (size=%zu)", ssl_idsize);
- }
- Curl_ssl_sessionid_unlock(data);
+ if(gtls_alpns_count &&
+ gnutls_alpn_set_protocols(gctx->session,
+ gtls_alpns, (unsigned int)gtls_alpns_count,
+ GNUTLS_ALPN_MANDATORY)) {
+ failf(data, "failed setting ALPN");
+ return CURLE_SSL_CONNECT_ERROR;
}
+
return CURLE_OK;
}
if(result)
return result;
+ if(connssl->alpn && (connssl->state != ssl_connection_deferred)) {
+ Curl_alpn_to_proto_str(&proto, connssl->alpn);
+ infof(data, VTLS_INFOF_ALPN_OFFER_1STR, proto.data);
+ }
+
gnutls_handshake_set_hook_function(backend->gtls.session,
GNUTLS_HANDSHAKE_ANY, GNUTLS_HOOK_POST,
gtls_handshake_cb);
if(result)
goto out;
- if(connssl->alpn) {
- gnutls_datum_t proto;
- int rc;
-
- rc = gnutls_alpn_get_selected_protocol(session, &proto);
- if(rc == 0)
- Curl_alpn_set_negotiated(cf, data, proto.data, proto.size);
- else
- Curl_alpn_set_negotiated(cf, data, NULL, 0);
- }
-
/* Only on TLSv1.2 or lower do we have the session id now. For
* TLSv1.3 we get it via a SESSION_TICKET message that arrives later. */
if(gnutls_protocol_get_version(session) < GNUTLS_TLS1_3)
return result;
}
+static CURLcode gtls_set_earlydata(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ const void *buf, size_t blen)
+{
+ struct ssl_connect_data *connssl = cf->ctx;
+ ssize_t nwritten = 0;
+ CURLcode result = CURLE_OK;
+
+ DEBUGASSERT(connssl->earlydata_state == ssl_earlydata_use);
+ DEBUGASSERT(Curl_bufq_is_empty(&connssl->earlydata));
+ if(blen) {
+ if(blen > connssl->earlydata_max)
+ blen = connssl->earlydata_max;
+ nwritten = Curl_bufq_write(&connssl->earlydata, buf, blen, &result);
+ CURL_TRC_CF(data, cf, "gtls_set_earlydata(len=%zu) -> %zd",
+ blen, nwritten);
+ if(nwritten < 0)
+ return result;
+ }
+ connssl->earlydata_state = ssl_earlydata_sending;
+ connssl->earlydata_skip = Curl_bufq_len(&connssl->earlydata);
+ return CURLE_OK;
+}
+
+static CURLcode gtls_send_earlydata(struct Curl_cfilter *cf,
+ struct Curl_easy *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;
+ const unsigned char *buf;
+ size_t blen;
+ ssize_t n;
+
+ DEBUGASSERT(connssl->earlydata_state == ssl_earlydata_sending);
+ backend->gtls.io_result = CURLE_OK;
+ while(Curl_bufq_peek(&connssl->earlydata, &buf, &blen)) {
+ n = gnutls_record_send_early_data(backend->gtls.session, buf, blen);
+ CURL_TRC_CF(data, cf, "gtls_send_earlydata(len=%zu) -> %zd",
+ blen, n);
+ if(n < 0) {
+ if(n == GNUTLS_E_AGAIN)
+ result = CURLE_AGAIN;
+ else
+ result = backend->gtls.io_result ?
+ backend->gtls.io_result : CURLE_SEND_ERROR;
+ goto out;
+ }
+ else if(!n) {
+ /* gnutls is buggy, it *SHOULD* return the amount of bytes it took in.
+ * Instead it returns 0 if everything was written. */
+ n = (ssize_t)blen;
+ }
+
+ Curl_bufq_skip(&connssl->earlydata, (size_t)n);
+ }
+ /* sent everything there was */
+ infof(data, "SSL sending %" FMT_OFF_T " bytes of early data",
+ connssl->earlydata_skip);
+out:
+ return result;
+}
+
/*
* This function is called after the TCP connect has completed. Setup the TLS
* layer and do all necessary magic.
gtls_connect_common(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool nonblocking,
- bool *done)
-{
+ bool *done) {
struct ssl_connect_data *connssl = cf->ctx;
- CURLcode rc;
+ struct gtls_ssl_backend_data *backend =
+ (struct gtls_ssl_backend_data *)connssl->backend;
CURLcode result = CURLE_OK;
+ DEBUGASSERT(backend);
+
/* Initiate the connection, if not already done */
- if(ssl_connect_1 == connssl->connecting_state) {
- rc = gtls_connect_step1(cf, data);
- if(rc) {
- result = rc;
+ if(connssl->connecting_state == ssl_connect_1) {
+ result = gtls_connect_step1(cf, data);
+ if(result)
goto out;
- }
+ connssl->connecting_state = ssl_connect_2;
}
- rc = handshake(cf, data, TRUE, nonblocking);
- if(rc) {
- /* handshake() sets its own error message with failf() */
- result = rc;
- goto out;
+ if(connssl->connecting_state == ssl_connect_2) {
+ if(connssl->earlydata_state == ssl_earlydata_use) {
+ goto out;
+ }
+ else if(connssl->earlydata_state == ssl_earlydata_sending) {
+ result = gtls_send_earlydata(cf, data);
+ if(result)
+ goto out;
+ connssl->earlydata_state = ssl_earlydata_sent;
+ if(!Curl_ssl_cf_is_proxy(cf))
+ Curl_pgrsEarlyData(data, (curl_off_t)connssl->earlydata_skip);
+ }
+ DEBUGASSERT((connssl->earlydata_state == ssl_earlydata_none) ||
+ (connssl->earlydata_state == ssl_earlydata_sent));
+
+ result = handshake(cf, data, TRUE, nonblocking);
+ if(result)
+ goto out;
+ connssl->connecting_state = ssl_connect_3;
}
/* Finish connecting once the handshake is done */
- if(ssl_connect_1 == connssl->connecting_state) {
- struct gtls_ssl_backend_data *backend =
- (struct gtls_ssl_backend_data *)connssl->backend;
- gnutls_session_t session;
- DEBUGASSERT(backend);
- session = backend->gtls.session;
- rc = gtls_verifyserver(cf, data, session);
- if(rc) {
- result = rc;
+ if(connssl->connecting_state == ssl_connect_3) {
+ gnutls_datum_t proto;
+ int rc;
+ result = gtls_verifyserver(cf, data, backend->gtls.session);
+ if(result)
goto out;
- }
+
connssl->state = ssl_connection_complete;
+ connssl->connecting_state = ssl_connect_1;
+
+ rc = gnutls_alpn_get_selected_protocol(backend->gtls.session, &proto);
+ if(rc) { /* No ALPN from server */
+ proto.data = NULL;
+ proto.size = 0;
+ }
+
+ result = Curl_alpn_set_negotiated(cf, data, connssl,
+ proto.data, proto.size);
+ if(result)
+ goto out;
+
+ if(connssl->earlydata_state == ssl_earlydata_sent) {
+ if(gnutls_session_get_flags(backend->gtls.session) &
+ GNUTLS_SFLAGS_EARLY_DATA) {
+ connssl->earlydata_state = ssl_earlydata_accepted;
+ infof(data, "Server accepted %zu bytes of TLS early data.",
+ connssl->earlydata_skip);
+ }
+ else {
+ connssl->earlydata_state = ssl_earlydata_rejected;
+ if(!Curl_ssl_cf_is_proxy(cf))
+ Curl_pgrsEarlyData(data, -(curl_off_t)connssl->earlydata_skip);
+ infof(data, "Server rejected TLS early data.");
+ connssl->earlydata_skip = 0;
+ }
+ }
}
out:
- *done = ssl_connect_1 == connssl->connecting_state;
-
+ if(result == CURLE_AGAIN) {
+ *done = FALSE;
+ return CURLE_OK;
+ }
+ *done = ((connssl->connecting_state == ssl_connect_1) ||
+ (connssl->state == ssl_connection_deferred));
return result;
}
struct Curl_easy *data,
bool *done)
{
+ struct ssl_connect_data *connssl = cf->ctx;
+ if(connssl->state == ssl_connection_deferred) {
+ /* We refuse to be pushed, we are waiting for someone to send/recv. */
+ *done = TRUE;
+ return CURLE_OK;
+ }
return gtls_connect_common(cf, data, TRUE, done);
}
return CURLE_OK;
}
+static CURLcode gtls_connect_deferred(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ const void *buf,
+ size_t blen,
+ bool *done)
+{
+ struct ssl_connect_data *connssl = cf->ctx;
+ CURLcode result = CURLE_OK;
+
+ DEBUGASSERT(connssl->state == ssl_connection_deferred);
+ *done = FALSE;
+ if(connssl->earlydata_state == ssl_earlydata_use) {
+ result = gtls_set_earlydata(cf, data, buf, blen);
+ if(result)
+ return result;
+ }
+
+ return gtls_connect_common(cf, data, TRUE, done);
+}
+
static bool gtls_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
ssize_t rc;
size_t nwritten, total_written = 0;
- (void)data;
DEBUGASSERT(backend);
+
+ if(connssl->state == ssl_connection_deferred) {
+ bool done = FALSE;
+ *curlcode = gtls_connect_deferred(cf, data, buf, blen, &done);
+ if(*curlcode) {
+ rc = -1;
+ goto out;
+ }
+ else if(!done) {
+ *curlcode = CURLE_AGAIN;
+ rc = -1;
+ goto out;
+ }
+ DEBUGASSERT(connssl->state == ssl_connection_complete);
+ }
+
+ if(connssl->earlydata_skip) {
+ if(connssl->earlydata_skip >= blen) {
+ connssl->earlydata_skip -= blen;
+ *curlcode = CURLE_OK;
+ rc = (ssize_t)blen;
+ goto out;
+ }
+ else {
+ total_written += connssl->earlydata_skip;
+ buf = ((const char *)buf) + connssl->earlydata_skip;
+ blen -= connssl->earlydata_skip;
+ connssl->earlydata_skip = 0;
+ }
+ }
+
while(blen) {
backend->gtls.io_result = CURLE_OK;
rc = gnutls_record_send(backend->gtls.session, buf, blen);
size_t i;
DEBUGASSERT(backend);
- if(!backend->gtls.session || cf->shutdown) {
+ /* If we have no handshaked connection or already shut down */
+ if(!backend->gtls.session || cf->shutdown ||
+ connssl->state != ssl_connection_complete) {
*done = TRUE;
goto out;
}
(void)data;
DEBUGASSERT(backend);
- backend->gtls.io_result = CURLE_OK;
+ if(connssl->state == ssl_connection_deferred) {
+ bool done = FALSE;
+ *curlcode = gtls_connect_deferred(cf, data, NULL, 0, &done);
+ if(*curlcode) {
+ ret = -1;
+ goto out;
+ }
+ else if(!done) {
+ *curlcode = CURLE_AGAIN;
+ ret = -1;
+ goto out;
+ }
+ DEBUGASSERT(connssl->state == ssl_connection_complete);
+ }
+
ret = gnutls_record_recv(backend->gtls.session, buf, buffersize);
if((ret == GNUTLS_E_AGAIN) || (ret == GNUTLS_E_INTERRUPTED)) {
*curlcode = CURLE_AGAIN;
void *old_session = NULL;
Curl_ssl_sessionid_lock(data);
- if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, &old_session, NULL)) {
+ if(!Curl_ssl_getsessionid(cf, data, &connssl->peer,
+ &old_session, NULL, NULL)) {
ret = mbedtls_ssl_set_session(&backend->ssl, old_session);
if(ret) {
Curl_ssl_sessionid_unlock(data);
if(connssl->alpn) {
const char *proto = mbedtls_ssl_get_alpn_protocol(&backend->ssl);
- Curl_alpn_set_negotiated(cf, data, (const unsigned char *)proto,
+ Curl_alpn_set_negotiated(cf, data, connssl, (const unsigned char *)proto,
proto ? strlen(proto) : 0);
}
#endif
/* If there is already a matching session in the cache, delete it */
Curl_ssl_sessionid_lock(data);
- retcode = Curl_ssl_set_sessionid(cf, data, &connssl->peer,
+ retcode = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL,
our_ssl_sessionid, 0,
mbedtls_session_free);
Curl_ssl_sessionid_unlock(data);
}
Curl_ssl_sessionid_lock(data);
- result = Curl_ssl_set_sessionid(cf, data, peer, der_session_buf,
+ result = Curl_ssl_set_sessionid(cf, data, peer, NULL, der_session_buf,
der_session_size, ossl_session_free);
Curl_ssl_sessionid_unlock(data);
}
if(ssl_config->primary.cache_session && transport == TRNSPRT_TCP) {
Curl_ssl_sessionid_lock(data);
if(!Curl_ssl_getsessionid(cf, data, peer, (void **)&der_sessionid,
- &der_sessionid_size)) {
+ &der_sessionid_size, NULL)) {
/* we got a session id, use it! */
ssl_session = d2i_SSL_SESSION(NULL, &der_sessionid,
(long)der_sessionid_size);
unsigned int len;
SSL_get0_alpn_selected(octx->ssl, &neg_protocol, &len);
- return Curl_alpn_set_negotiated(cf, data, neg_protocol, len);
+ return Curl_alpn_set_negotiated(cf, data, connssl, neg_protocol, len);
}
#endif
bool incache;
Curl_ssl_sessionid_lock(data);
incache = !(Curl_ssl_getsessionid(cf, data, peer,
- &old_ssl_sessionid, NULL));
+ &old_ssl_sessionid, NULL, NULL));
if(incache) {
infof(data, "Remove session ID again from cache");
Curl_ssl_delsessionid(data, old_ssl_sessionid);
cr_set_negotiated_alpn(struct Curl_cfilter *cf, struct Curl_easy *data,
const struct rustls_connection *rconn)
{
+ struct ssl_connect_data *const connssl = cf->ctx;
const uint8_t *protocol = NULL;
size_t len = 0;
rustls_connection_get_alpn_protocol(rconn, &protocol, &len);
- Curl_alpn_set_negotiated(cf, data, protocol, len);
+ Curl_alpn_set_negotiated(cf, data, connssl, protocol, len);
}
/* Given an established network connection, do a TLS handshake.
if(ssl_config->primary.cache_session) {
Curl_ssl_sessionid_lock(data);
if(!Curl_ssl_getsessionid(cf, data, &connssl->peer,
- (void **)&old_cred, NULL)) {
+ (void **)&old_cred, NULL, NULL)) {
backend->cred = old_cred;
DEBUGF(infof(data, "schannel: reusing existing credential handle"));
SecApplicationProtocolNegotiationStatus_Success) {
unsigned char prev_alpn = cf->conn->alpn;
- Curl_alpn_set_negotiated(cf, data, alpn_result.ProtocolId,
+ Curl_alpn_set_negotiated(cf, data, connssl, alpn_result.ProtocolId,
alpn_result.ProtocolIdSize);
if(backend->recv_renegotiating) {
if(prev_alpn != cf->conn->alpn &&
}
else {
if(!backend->recv_renegotiating)
- Curl_alpn_set_negotiated(cf, data, NULL, 0);
+ Curl_alpn_set_negotiated(cf, data, connssl, NULL, 0);
}
}
#endif
Curl_ssl_sessionid_lock(data);
/* Up ref count since call takes ownership */
backend->cred->refcount++;
- result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, backend->cred,
+ result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL,
+ backend->cred,
sizeof(struct Curl_schannel_cred),
schannel_session_free);
Curl_ssl_sessionid_unlock(data);
Curl_ssl_sessionid_lock(data);
if(!Curl_ssl_getsessionid(cf, data, &connssl->peer,
- (void **)&ssl_sessionid, &ssl_sessionid_len)) {
+ (void **)&ssl_sessionid, &ssl_sessionid_len,
+ NULL)) {
/* we got a session id, use it! */
err = SSLSetPeerID(backend->ssl_ctx, ssl_sessionid, ssl_sessionid_len);
Curl_ssl_sessionid_unlock(data);
return CURLE_SSL_CONNECT_ERROR;
}
- result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, ssl_sessionid,
- ssl_sessionid_len,
+ result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL,
+ ssl_sessionid, ssl_sessionid_len,
sectransp_session_free);
Curl_ssl_sessionid_unlock(data);
if(result)
return NULL;
ctx->alpn = alpn;
+ Curl_bufq_init2(&ctx->earlydata, CURL_SSL_EARLY_MAX, 1, BUFQ_OPT_NO_SPARES);
ctx->backend = calloc(1, Curl_ssl->sizeof_ssl_backend_data);
if(!ctx->backend) {
free(ctx);
static void cf_ctx_free(struct ssl_connect_data *ctx)
{
if(ctx) {
+ Curl_safefree(ctx->alpn_negotiated);
+ Curl_bufq_free(&ctx->earlydata);
free(ctx->backend);
free(ctx);
}
struct Curl_easy *data,
const struct ssl_peer *peer,
void **ssl_sessionid,
- size_t *idsize) /* set 0 if unknown */
+ size_t *idsize, /* set 0 if unknown */
+ char **palpn)
{
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);
bool no_match = TRUE;
*ssl_sessionid = NULL;
+ if(palpn)
+ *palpn = NULL;
if(!ssl_config)
return TRUE;
*ssl_sessionid = check->sessionid;
if(idsize)
*idsize = check->idsize;
+ if(palpn)
+ *palpn = check->alpn;
no_match = FALSE;
break;
}
Curl_safefree(session->name);
Curl_safefree(session->conn_to_host);
+ Curl_safefree(session->alpn);
}
}
CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf,
struct Curl_easy *data,
const struct ssl_peer *peer,
+ const char *alpn,
void *ssl_sessionid,
size_t idsize,
Curl_ssl_sessionid_dtor *sessionid_free_cb)
long oldest_age;
char *clone_host = NULL;
char *clone_conn_to_host = NULL;
+ char *clone_alpn = NULL;
int conn_to_port;
long *general_age;
void *old_sessionid;
return CURLE_OK;
}
- if(!Curl_ssl_getsessionid(cf, data, peer, &old_sessionid, &old_size)) {
+ if(!Curl_ssl_getsessionid(cf, data, peer, &old_sessionid, &old_size, NULL)) {
if((old_size == idsize) &&
((old_sessionid == ssl_sessionid) ||
(idsize && !memcmp(old_sessionid, ssl_sessionid, idsize)))) {
goto out;
}
+ clone_alpn = alpn ? strdup(alpn) : NULL;
+ if(alpn && !clone_alpn)
+ goto out;
+
if(cf->conn->bits.conn_to_port)
conn_to_port = cf->conn->conn_to_port;
else
store->conn_to_host = clone_conn_to_host; /* clone connect to hostname */
clone_conn_to_host = NULL;
store->conn_to_port = conn_to_port; /* connect to port number */
+ store->alpn = clone_alpn;
+ clone_alpn = NULL;
/* port number */
store->remote_port = peer->port;
store->scheme = cf->conn->handler->scheme;
out:
free(clone_host);
free(clone_conn_to_host);
+ free(clone_alpn);
if(result) {
failf(data, "Failed to add Session ID to cache for %s://%s:%d [%s]",
store->scheme, store->name, store->remote_port,
if(!result && *done) {
cf->connected = TRUE;
connssl->handshake_done = Curl_now();
- DEBUGASSERT(connssl->state == ssl_connection_complete);
+ /* Connection can be deferred when sending early data */
+ DEBUGASSERT(connssl->state == ssl_connection_complete ||
+ connssl->state == ssl_connection_deferred);
}
out:
CURL_TRC_CF(data, cf, "cf_connect() -> %d, done=%d", result, *done);
return CURLE_OK;
}
+bool Curl_alpn_contains_proto(const struct alpn_spec *spec,
+ const char *proto)
+{
+ size_t i, plen = proto ? strlen(proto) : 0;
+ for(i = 0; spec && plen && i < spec->count; ++i) {
+ size_t slen = strlen(spec->entries[i]);
+ if((slen == plen) && !memcmp(proto, spec->entries[i], plen))
+ return TRUE;
+ }
+ return FALSE;
+}
+
CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf,
struct Curl_easy *data,
+ struct ssl_connect_data *connssl,
const unsigned char *proto,
size_t proto_len)
{
+ CURLcode result = CURLE_OK;
unsigned char *palpn =
#ifndef CURL_DISABLE_PROXY
(cf->conn->bits.tunnel_proxy && Curl_ssl_cf_is_proxy(cf)) ?
#endif
;
+ if(connssl->alpn_negotiated) {
+ /* When we ask for a specific ALPN protocol, we need the confirmation
+ * of it by the server, as we have installed protocol handler and
+ * connection filter chain for exactly this protocol. */
+ if(!proto_len) {
+ failf(data, "ALPN: asked for '%s' from previous session, "
+ "but server did not confirm it. Refusing to continue.",
+ connssl->alpn_negotiated);
+ result = CURLE_SSL_CONNECT_ERROR;
+ goto out;
+ }
+ else if((strlen(connssl->alpn_negotiated) != proto_len) ||
+ memcmp(connssl->alpn_negotiated, proto, proto_len)) {
+ failf(data, "ALPN: asked for '%s' from previous session, but server "
+ "selected '%.*s'. Refusing to continue.",
+ connssl->alpn_negotiated, (int)proto_len, proto);
+ result = CURLE_SSL_CONNECT_ERROR;
+ goto out;
+ }
+ /* ALPN is exactly what we asked for, done. */
+ infof(data, "ALPN: server confirmed to use '%s'",
+ connssl->alpn_negotiated);
+ goto out;
+ }
+
+ if(proto && proto_len) {
+ if(memchr(proto, '\0', proto_len)) {
+ failf(data, "ALPN: server selected protocol contains NUL. "
+ "Refusing to continue.");
+ result = CURLE_SSL_CONNECT_ERROR;
+ goto out;
+ }
+ connssl->alpn_negotiated = malloc(proto_len + 1);
+ if(!connssl->alpn_negotiated)
+ return CURLE_OUT_OF_MEMORY;
+ memcpy(connssl->alpn_negotiated, proto, proto_len);
+ connssl->alpn_negotiated[proto_len] = 0;
+ }
+
if(proto && proto_len) {
if(proto_len == ALPN_HTTP_1_1_LENGTH &&
!memcmp(ALPN_HTTP_1_1, proto, ALPN_HTTP_1_1_LENGTH)) {
/* return CURLE_NOT_BUILT_IN; */
goto out;
}
- infof(data, VTLS_INFOF_ALPN_ACCEPTED_LEN_1STR, (int)proto_len, proto);
+
+ if(connssl->state == ssl_connection_deferred)
+ infof(data, VTLS_INFOF_ALPN_DEFERRED, (int)proto_len, proto);
+ else
+ infof(data, VTLS_INFOF_ALPN_ACCEPTED, (int)proto_len, proto);
}
else {
*palpn = CURL_HTTP_VERSION_NONE;
- infof(data, VTLS_INFOF_NO_ALPN);
+ if(connssl->state == ssl_connection_deferred)
+ infof(data, VTLS_INFOF_NO_ALPN_DEFERRED);
+ else
+ infof(data, VTLS_INFOF_NO_ALPN);
}
out:
- return CURLE_OK;
+ return result;
}
#endif /* USE_SSL */
#define ALPN_ACCEPTED "ALPN: server accepted "
-#define VTLS_INFOF_NO_ALPN \
+#define VTLS_INFOF_NO_ALPN \
"ALPN: server did not agree on a protocol. Uses default."
-#define VTLS_INFOF_ALPN_OFFER_1STR \
+#define VTLS_INFOF_ALPN_OFFER_1STR \
"ALPN: curl offers %s"
-#define VTLS_INFOF_ALPN_ACCEPTED_1STR \
- ALPN_ACCEPTED "%s"
-#define VTLS_INFOF_ALPN_ACCEPTED_LEN_1STR \
+#define VTLS_INFOF_ALPN_ACCEPTED \
ALPN_ACCEPTED "%.*s"
+#define VTLS_INFOF_NO_ALPN_DEFERRED \
+ "ALPN: deferred handshake for early data without specific protocol."
+#define VTLS_INFOF_ALPN_DEFERRED \
+ "ALPN: deferred handshake for early data using '%.*s'."
+
/* Curl_multi SSL backend-specific data; declared differently by each SSL
backend */
struct Curl_cfilter;
#ifdef USE_SSL
+struct ssl_connect_data;
+
/* see https://www.iana.org/assignments/tls-extensiontype-values/ */
#define ALPN_HTTP_1_1_LENGTH 8
#define ALPN_HTTP_1_1 "http/1.1"
CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf,
struct Curl_easy *data,
+ struct ssl_connect_data *connssl,
const unsigned char *proto,
size_t proto_len);
+bool Curl_alpn_contains_proto(const struct alpn_spec *spec,
+ const char *proto);
+
/* enum for the nonblocking SSL connection state machine */
typedef enum {
ssl_connect_1,
typedef enum {
ssl_connection_none,
+ ssl_connection_deferred,
ssl_connection_negotiating,
ssl_connection_complete
} ssl_connection_state;
+typedef enum {
+ ssl_earlydata_none,
+ ssl_earlydata_use,
+ ssl_earlydata_sending,
+ ssl_earlydata_sent,
+ ssl_earlydata_accepted,
+ ssl_earlydata_rejected
+} ssl_earlydata_state;
+
#define CURL_SSL_IO_NEED_NONE (0)
#define CURL_SSL_IO_NEED_RECV (1<<0)
#define CURL_SSL_IO_NEED_SEND (1<<1)
+/* Max earlydata payload we want to send */
+#define CURL_SSL_EARLY_MAX (64*1024)
+
/* Information in each SSL cfilter context: cf->ctx */
struct ssl_connect_data {
struct ssl_peer peer;
void *backend; /* vtls backend specific props */
struct cf_call_data call_data; /* data handle used in current call */
struct curltime handshake_done; /* time when handshake finished */
+ char *alpn_negotiated; /* negotiated ALPN value or NULL */
+ struct bufq earlydata; /* earlydata to be send to peer */
+ size_t earlydata_max; /* max earlydata allowed by peer */
+ size_t earlydata_skip; /* sending bytes to skip when earlydata
+ * is accepted by peer */
ssl_connection_state state;
ssl_connect_state connecting_state;
+ ssl_earlydata_state earlydata_state;
int io_need; /* TLS signals special SEND/RECV needs */
BIT(use_alpn); /* if ALPN shall be used in handshake */
BIT(peer_closed); /* peer has closed connection */
* Caller must make sure that the ownership of returned sessionid object
* is properly taken (e.g. its refcount is incremented
* under sessionid mutex).
+ * @param cf the connection filter wanting to use it
+ * @param data the transfer involved
+ * @param peer the peer the filter wants to talk to
+ * @param sessionid on return the TLS session
+ * @param idsize on return the size of the TLS session data
+ * @param palpn on return the ALPN string used by the session,
+ * set to NULL when not interested
*/
bool Curl_ssl_getsessionid(struct Curl_cfilter *cf,
struct Curl_easy *data,
const struct ssl_peer *peer,
void **ssl_sessionid,
- size_t *idsize); /* set 0 if unknown */
+ size_t *idsize, /* set 0 if unknown */
+ char **palpn);
/* Set a TLS session ID for `peer`. Replaces an existing session ID if
* not already the very same.
CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf,
struct Curl_easy *data,
const struct ssl_peer *peer,
+ const char *alpn,
void *sessionid,
size_t sessionid_size,
Curl_ssl_sessionid_dtor *sessionid_free_cb);
Curl_ssl_sessionid_lock(data);
if(!Curl_ssl_getsessionid(cf, data, &connssl->peer,
- &ssl_sessionid, NULL)) {
+ &ssl_sessionid, NULL, NULL)) {
/* we got a session id, use it! */
if(!SSL_set_session(backend->handle, ssl_sessionid)) {
Curl_ssl_delsessionid(data, ssl_sessionid);
rc = wolfSSL_ALPN_GetProtocol(backend->handle, &protocol, &protocol_len);
if(rc == SSL_SUCCESS) {
- Curl_alpn_set_negotiated(cf, data, (const unsigned char *)protocol,
- protocol_len);
+ Curl_alpn_set_negotiated(cf, data, connssl,
+ (const unsigned char *)protocol, protocol_len);
}
else if(rc == SSL_ALPN_NOT_FOUND)
- Curl_alpn_set_negotiated(cf, data, NULL, 0);
+ Curl_alpn_set_negotiated(cf, data, connssl, NULL, 0);
else {
failf(data, "ALPN, failure getting protocol, error %d", rc);
return CURLE_SSL_CONNECT_ERROR;
if(our_ssl_sessionid) {
Curl_ssl_sessionid_lock(data);
/* call takes ownership of `our_ssl_sessionid` */
- result = Curl_ssl_set_sessionid(cf, data, &connssl->peer,
+ result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL,
our_ssl_sessionid, 0,
wolfssl_session_free);
Curl_ssl_sessionid_unlock(data);
bool xattr; /* store metadata in extended attributes */
long gssapi_delegation;
bool ssl_allow_beast; /* allow this SSL vulnerability */
+ bool ssl_allow_earlydata; /* allow use of TLSv1.3 early data */
bool proxy_ssl_allow_beast; /* allow this SSL vulnerability for proxy */
bool ssl_no_revoke; /* disable SSL certificate revocation checks */
bool ssl_revoke_best_effort; /* ignore SSL revocation offline/missing
{"tftp-blksize", ARG_STRG, ' ', C_TFTP_BLKSIZE},
{"tftp-no-options", ARG_BOOL, ' ', C_TFTP_NO_OPTIONS},
{"time-cond", ARG_STRG, 'z', C_TIME_COND},
+ {"tls-earlydata", ARG_BOOL, ' ', C_TLS_EARLYDATA},
{"tls-max", ARG_STRG, ' ', C_TLS_MAX},
{"tls13-ciphers", ARG_STRG, ' ', C_TLS13_CIPHERS},
{"tlsauthtype", ARG_STRG, ' ', C_TLSAUTHTYPE},
config->abstract_unix_socket = TRUE;
err = getstr(&config->unix_socket_path, nextarg, DENY_BLANK);
break;
+ case C_TLS_EARLYDATA: /* --tls-earlydata */
+ if(feature_ssl)
+ config->ssl_allow_earlydata = toggle;
+ break;
case C_TLS_MAX: /* --tls-max */
err = str2tls_max(&config->ssl_version_max, nextarg);
break;
C_TFTP_BLKSIZE,
C_TFTP_NO_OPTIONS,
C_TIME_COND,
+ C_TLS_EARLYDATA,
C_TLS_MAX,
C_TLS13_CIPHERS,
C_TLSAUTHTYPE,
{"-z, --time-cond <time>",
"Transfer based on a time condition",
CURLHELP_HTTP | CURLHELP_FTP},
+ {" --tls-earlydata",
+ "Allow use of TLSv1.3 early data (0RTT)",
+ CURLHELP_TLS},
{" --tls-max <VERSION>",
"Maximum allowed TLS version",
CURLHELP_TLS},
long mask =
(config->ssl_allow_beast ?
CURLSSLOPT_ALLOW_BEAST : 0) |
+ (config->ssl_allow_earlydata ?
+ CURLSSLOPT_EARLYDATA : 0) |
(config->ssl_no_revoke ?
CURLSSLOPT_NO_REVOKE : 0) |
(config->ssl_revoke_best_effort ?
}
static int setup(CURL *hnd, const char *url, struct transfer *t,
- int http_version)
+ int http_version, struct curl_slist *host,
+ CURLSH *share, int use_earlydata)
{
+ curl_easy_setopt(hnd, CURLOPT_SHARE, share);
curl_easy_setopt(hnd, CURLOPT_URL, url);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, http_version);
curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(hnd, CURLOPT_XFERINFOFUNCTION, my_progress_cb);
curl_easy_setopt(hnd, CURLOPT_XFERINFODATA, t);
+ if(use_earlydata)
+ curl_easy_setopt(hnd, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_EARLYDATA);
if(forbid_reuse)
curl_easy_setopt(hnd, CURLOPT_FORBID_REUSE, 1L);
+ if(host)
+ curl_easy_setopt(hnd, CURLOPT_RESOLVE, host);
/* please be verbose */
if(verbose) {
" download a url with following options:\n"
" -a abort paused transfer\n"
" -m number max parallel downloads\n"
- " -n number total downloads\n"
+ " -e use TLS early data when possible\n"
+ " -f forbid connection reuse\n"
+ " -n number total downloads\n");
+ fprintf(stderr,
" -A number abort transfer after `number` response bytes\n"
" -F number fail writing response after `number` response bytes\n"
" -P number pause transfer after `number` response bytes\n"
+ " -r <host>:<port>:<addr> resolve information\n"
" -V http_version (http/1.1, h2, h3) http version to use\n"
);
}
#ifndef _MSC_VER
CURLM *multi_handle;
struct CURLMsg *m;
+ CURLSH *share;
const char *url;
size_t i, n, max_parallel = 1;
size_t active_transfers;
size_t pause_offset = 0;
size_t abort_offset = 0;
size_t fail_offset = 0;
- int abort_paused = 0;
+ int abort_paused = 0, use_earlydata = 0;
struct transfer *t;
int http_version = CURL_HTTP_VERSION_2_0;
int ch;
+ struct curl_slist *host = NULL;
+ const char *resolve = NULL;
- while((ch = getopt(argc, argv, "afhm:n:A:F:P:V:")) != -1) {
+ while((ch = getopt(argc, argv, "aefhm:n:A:F:P:r:V:")) != -1) {
switch(ch) {
case 'h':
usage(NULL);
case 'a':
abort_paused = 1;
break;
+ case 'e':
+ use_earlydata = 1;
+ break;
case 'f':
forbid_reuse = 1;
break;
case 'P':
pause_offset = (size_t)strtol(optarg, NULL, 10);
break;
+ case 'r':
+ resolve = optarg;
+ break;
case 'V': {
if(!strcmp("http/1.1", optarg))
http_version = CURL_HTTP_VERSION_1_1;
}
url = argv[0];
+ if(resolve)
+ host = curl_slist_append(NULL, resolve);
+
+ share = curl_share_init();
+ if(!share) {
+ fprintf(stderr, "error allocating share\n");
+ return 1;
+ }
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
+
transfers = calloc(transfer_count, sizeof(*transfers));
if(!transfers) {
fprintf(stderr, "error allocating transfer structs\n");
for(i = 0; i < n; ++i) {
t = &transfers[i];
t->easy = curl_easy_init();
- if(!t->easy || setup(t->easy, url, t, http_version)) {
+ if(!t->easy ||
+ setup(t->easy, url, t, http_version, host, share, use_earlydata)) {
fprintf(stderr, "[t-%d] FAILED setup\n", (int)i);
return 1;
}
if(t) {
t->done = 1;
fprintf(stderr, "[t-%d] FINISHED\n", t->idx);
+ if(use_earlydata) {
+ curl_off_t sent;
+ curl_easy_getinfo(e, CURLINFO_EARLYDATA_SENT_T, &sent);
+ fprintf(stderr, "[t-%d] EarlyData: %ld\n", t->idx, (long)sent);
+ }
}
else {
curl_easy_cleanup(e);
t = &transfers[i];
if(!t->started) {
t->easy = curl_easy_init();
- if(!t->easy || setup(t->easy, url, t, http_version)) {
+ if(!t->easy ||
+ setup(t->easy, url, t, http_version, host, share,
+ use_earlydata)) {
fprintf(stderr, "[t-%d] FAILED setup\n", (int)i);
return 1;
}
} while(active_transfers); /* as long as we have transfers going */
+ curl_multi_cleanup(multi_handle);
+
for(i = 0; i < transfer_count; ++i) {
t = &transfers[i];
if(t->out) {
}
free(transfers);
- curl_multi_cleanup(multi_handle);
+ curl_share_cleanup(share);
return 0;
#else
}
static int setup(CURL *hnd, const char *url, struct transfer *t,
- int http_version)
+ int http_version, struct curl_slist *host,
+ CURLSH *share, int use_earlydata, int announce_length)
{
+ curl_easy_setopt(hnd, CURLOPT_SHARE, share);
curl_easy_setopt(hnd, CURLOPT_URL, url);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, http_version);
curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, (long)(128 * 1024));
curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, my_write_cb);
curl_easy_setopt(hnd, CURLOPT_WRITEDATA, t);
+ if(use_earlydata)
+ curl_easy_setopt(hnd, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_EARLYDATA);
if(!t->method || !strcmp("PUT", t->method))
curl_easy_setopt(hnd, CURLOPT_UPLOAD, 1L);
}
curl_easy_setopt(hnd, CURLOPT_READFUNCTION, my_read_cb);
curl_easy_setopt(hnd, CURLOPT_READDATA, t);
+ if(announce_length)
+ curl_easy_setopt(hnd, CURLOPT_INFILESIZE_LARGE, t->send_total);
+
curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(hnd, CURLOPT_XFERINFOFUNCTION, my_progress_cb);
curl_easy_setopt(hnd, CURLOPT_XFERINFODATA, t);
if(forbid_reuse)
curl_easy_setopt(hnd, CURLOPT_FORBID_REUSE, 1L);
+ if(host)
+ curl_easy_setopt(hnd, CURLOPT_RESOLVE, host);
/* please be verbose */
if(verbose) {
"usage: [options] url\n"
" upload to a url with following options:\n"
" -a abort paused transfer\n"
+ " -e use TLS earlydata\n"
" -m number max parallel uploads\n"
" -n number total uploads\n"
" -A number abort transfer after `number` request body bytes\n"
" -F number fail reading request body after `number` of bytes\n"
" -P number pause transfer after `number` request body bytes\n"
+ " -r <host>:<port>:<addr> resolve information\n"
" -S number size to upload\n"
" -V http_version (http/1.1, h2, h3) http version to use\n"
);
#ifndef _MSC_VER
CURLM *multi_handle;
struct CURLMsg *m;
+ CURLSH *share;
const char *url;
const char *method = "PUT";
size_t i, n, max_parallel = 1;
size_t send_total = (128 * 1024);
int abort_paused = 0;
int reuse_easy = 0;
+ int use_earlydata = 0;
+ int announce_length = 0;
struct transfer *t;
int http_version = CURL_HTTP_VERSION_2_0;
+ struct curl_slist *host = NULL;
+ const char *resolve = NULL;
int ch;
- while((ch = getopt(argc, argv, "afhm:n:A:F:M:P:RS:V:")) != -1) {
+ while((ch = getopt(argc, argv, "aefhlm:n:A:F:M:P:r:RS:V:")) != -1) {
switch(ch) {
case 'h':
usage(NULL);
case 'a':
abort_paused = 1;
break;
+ case 'e':
+ use_earlydata = 1;
+ break;
case 'f':
forbid_reuse = 1;
break;
+ case 'l':
+ announce_length = 1;
+ break;
case 'm':
max_parallel = (size_t)strtol(optarg, NULL, 10);
break;
case 'P':
pause_offset = (size_t)strtol(optarg, NULL, 10);
break;
+ case 'r':
+ resolve = optarg;
+ break;
case 'R':
reuse_easy = 1;
break;
}
url = argv[0];
+ if(resolve)
+ host = curl_slist_append(NULL, resolve);
+
+ share = curl_share_init();
+ if(!share) {
+ fprintf(stderr, "error allocating share\n");
+ return 1;
+ }
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
+ curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
+
transfers = calloc(transfer_count, sizeof(*transfers));
if(!transfers) {
fprintf(stderr, "error allocating transfer structs\n");
for(i = 0; i < transfer_count; ++i) {
t = &transfers[i];
t->easy = easy;
- if(setup(t->easy, url, t, http_version)) {
+ if(setup(t->easy, url, t, http_version, host, share, use_earlydata,
+ announce_length)) {
fprintf(stderr, "[t-%d] FAILED setup\n", (int)i);
return 1;
}
for(i = 0; i < n; ++i) {
t = &transfers[i];
t->easy = curl_easy_init();
- if(!t->easy || setup(t->easy, url, t, http_version)) {
+ if(!t->easy || setup(t->easy, url, t, http_version, host, share,
+ use_earlydata, announce_length)) {
fprintf(stderr, "[t-%d] FAILED setup\n", (int)i);
return 1;
}
if(t) {
t->done = 1;
fprintf(stderr, "[t-%d] FINISHED\n", t->idx);
+ if(use_earlydata) {
+ curl_off_t sent;
+ curl_easy_getinfo(e, CURLINFO_EARLYDATA_SENT_T, &sent);
+ fprintf(stderr, "[t-%d] EarlyData: %ld\n", t->idx, (long)sent);
+ }
}
else {
curl_easy_cleanup(e);
t = &transfers[i];
if(!t->started) {
t->easy = curl_easy_init();
- if(!t->easy || setup(t->easy, url, t, http_version)) {
+ if(!t->easy || setup(t->easy, url, t, http_version, host,
+ share, use_earlydata, announce_length)) {
fprintf(stderr, "[t-%d] FAILED setup\n", (int)i);
return 1;
}
}
}
free(transfers);
+ curl_share_cleanup(share);
return 0;
#else
import logging
import math
import os
+import re
from datetime import timedelta
import pytest
# we see 3 connections, because Apache only every serves a single
# request via Upgrade: and then closed the connection.
assert r.total_connects == 3, r.dump_logs()
+
+ # nghttpx is the only server we have that supports TLS early data
+ @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx")
+ @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
+ def test_02_32_earlydata(self, env: Env, httpd, nghttpx, proto):
+ if not env.curl_uses_lib('gnutls'):
+ pytest.skip('TLS earlydata only implemented in GnuTLS')
+ if proto == 'h3' and not env.have_h3():
+ pytest.skip("h3 not supported")
+ count = 2
+ docname = 'data-10k'
+ # we want this test to always connect to nghttpx, since it is
+ # the only server we have that supports TLS earlydata
+ port = env.port_for(proto)
+ if proto != 'h3':
+ port = env.nghttpx_https_port
+ # url = f'https://{env.domain1}:{env.port_for(proto)}/{docname}'
+ url = f'https://{env.domain1}:{port}/{docname}'
+ client = LocalClient(name='hx-download', env=env)
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ r = client.run(args=[
+ '-n', f'{count}',
+ '-e', # use TLS earlydata
+ '-f', # forbid reuse of connections
+ '-r', f'{env.domain1}:{port}:127.0.0.1',
+ '-V', proto, url
+ ])
+ r.check_exit_code(0)
+ srcfile = os.path.join(httpd.docs_dir, docname)
+ self.check_downloads(client, srcfile, count)
+ # check that TLS earlydata worked as expected
+ earlydata = {}
+ for line in r.trace_lines:
+ 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}'
+ if proto == 'http/1.1':
+ assert earlydata[1] == 69, f'{earlydata}'
+ elif proto == 'h2':
+ assert earlydata[1] == 107, f'{earlydata}'
+ elif proto == 'h3':
+ # not implemented
+ assert earlydata[1] == 0, f'{earlydata}'
import filecmp
import logging
import os
+import re
import pytest
from typing import List
def _class_scope(self, env, httpd, nghttpx):
if env.have_h3():
nghttpx.start_if_needed()
+ env.make_data_file(indir=env.gen_dir, fname="data-10k", fsize=10*1024)
env.make_data_file(indir=env.gen_dir, fname="data-63k", fsize=63*1024)
env.make_data_file(indir=env.gen_dir, fname="data-64k", fsize=64*1024)
env.make_data_file(indir=env.gen_dir, fname="data-100k", fsize=100*1024)
])
r.check_stats(count=1, http_status=200, exitcode=0)
+ # nghttpx is the only server we have that supports TLS early data and
+ # has a limit of 16k it announces
+ @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx")
+ @pytest.mark.parametrize("proto,upload_size,exp_early", [
+ ['http/1.1', 100, 203], # headers+body
+ ['http/1.1', 10*1024, 10345], # headers+body
+ ['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
+ ])
+ def test_07_70_put_earlydata(self, env: Env, httpd, nghttpx, proto, upload_size, exp_early):
+ if not env.curl_uses_lib('gnutls'):
+ pytest.skip('TLS earlydata only implemented in GnuTLS')
+ if proto == 'h3' and not env.have_h3():
+ pytest.skip("h3 not supported")
+ count = 2
+ # we want this test to always connect to nghttpx, since it is
+ # the only server we have that supports TLS earlydata
+ port = env.port_for(proto)
+ if proto != 'h3':
+ port = env.nghttpx_https_port
+ url = f'https://{env.domain1}:{port}/curltest/put?id=[0-{count-1}]'
+ client = LocalClient(name='hx-upload', env=env)
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ r = client.run(args=[
+ '-n', f'{count}',
+ '-e', # use TLS earlydata
+ '-f', # forbid reuse of connections
+ '-l', # announce upload length, no 'Expect: 100'
+ '-S', f'{upload_size}',
+ '-r', f'{env.domain1}:{port}:127.0.0.1',
+ '-V', proto, url
+ ])
+ r.check_exit_code(0)
+ self.check_downloads(client, [f"{upload_size}"], count)
+ earlydata = {}
+ for line in r.trace_lines:
+ 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}'
+ assert earlydata[1] == exp_early, f'{earlydata}'
+
def check_downloads(self, client, source: List[str], count: int,
complete: bool = True):
for i in range(count):
#
###########################################################################
#
+import difflib
+import filecmp
import logging
import os
+import re
import pytest
-from testenv import Env, CurlClient, Caddy
+from testenv import Env, CurlClient, Caddy, LocalClient
log = logging.getLogger(__name__)
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, caddy):
+ self._make_docs_file(docs_dir=caddy.docs_dir, fname='data10k.data', fsize=10*1024)
self._make_docs_file(docs_dir=caddy.docs_dir, fname='data1.data', fsize=1024*1024)
self._make_docs_file(docs_dir=caddy.docs_dir, fname='data5.data', fsize=5*1024*1024)
self._make_docs_file(docs_dir=caddy.docs_dir, fname='data10.data', fsize=10*1024*1024)
for i in range(count):
respdata = open(curl.response_file(i)).readlines()
assert respdata == exp_data
+
+ @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
+ def test_08_08_earlydata(self, env: Env, httpd, caddy, proto):
+ count = 2
+ docname = 'data10k.data'
+ url = f'https://{env.domain1}:{caddy.port}/{docname}'
+ client = LocalClient(name='hx-download', env=env)
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ r = client.run(args=[
+ '-n', f'{count}',
+ '-e', # use TLS earlydata
+ '-f', # forbid reuse of connections
+ '-r', f'{env.domain1}:{caddy.port}:127.0.0.1',
+ '-V', proto, url
+ ])
+ r.check_exit_code(0)
+ srcfile = os.path.join(caddy.docs_dir, docname)
+ self.check_downloads(client, srcfile, count)
+ earlydata = {}
+ for line in r.trace_lines:
+ 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}'
+
+ def check_downloads(self, client, srcfile: str, count: int,
+ complete: bool = True):
+ for i in range(count):
+ dfile = client.download_file(i)
+ assert os.path.exists(dfile)
+ if complete and not filecmp.cmp(srcfile, dfile, shallow=False):
+ diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
+ b=open(dfile).readlines(),
+ fromfile=srcfile,
+ tofile=dfile,
+ n=1))
+ assert False, f'download {dfile} differs:\n{diff}'
count = 3
exp_resumed = 'Resumed'
xargs = ['--sessionid', '--tls-max', tls_max, f'--tlsv{tls_max}']
- if env.curl_uses_lib('gnutls'):
- if tls_max == '1.3':
- exp_resumed = 'Initial' # 1.2 works in GnuTLS, but 1.3 does not, TODO
if env.curl_uses_lib('libressl'):
if tls_max == '1.3':
exp_resumed = 'Initial' # 1.2 works in LibreSSL, but 1.3 does not, TODO
])
httpd.reload_if_config_changed()
proto = 'http/1.1'
- curl = CurlClient(env=env)
+ run_env = os.environ.copy()
+ run_env['CURL_USE_EARLYDATA'] = '1'
+ curl = CurlClient(env=env, run_env=run_env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
# SSL backend specifics
if env.curl_uses_lib('bearssl'):
'ftps': socket.SOCK_STREAM,
'http': socket.SOCK_STREAM,
'https': socket.SOCK_STREAM,
+ 'nghttpx_https': socket.SOCK_STREAM,
'proxy': socket.SOCK_STREAM,
'proxys': socket.SOCK_STREAM,
'h2proxys': socket.SOCK_STREAM,
def https_port(self) -> int:
return self.CONFIG.ports['https']
+ @property
+ def nghttpx_https_port(self) -> int:
+ return self.CONFIG.ports['nghttpx_https']
+
@property
def h3_port(self) -> int:
return self.https_port
args = [
self._cmd,
f'--frontend=*,{self.env.h3_port};quic',
+ 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}',
'--log-level=INFO',