You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/01/14 21:00:49 UTC
[trafficserver] 01/02: TLSv1.3 0-RTT support (#5450)
This is an automated email from the ASF dual-hosted git repository.
zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 9ebbd60bdf05404761742acfb90bbdcbd4c33cde
Author: Fei Deng <du...@gmail.com>
AuthorDate: Tue Jan 14 14:58:09 2020 -0600
TLSv1.3 0-RTT support (#5450)
* TLSv1.3 0-RTT support
TLSv1.3 0-RTT test
TLSv1.3 0-RTT anti-replay
TLSv1.3 0-RTT h2
(cherry picked from commit 8236813ef44e3187febe12b7996cb2cc9754f3e7)
---
build/crypto.m4 | 28 +++
configure.ac | 3 +
doc/admin-guide/files/records.config.en.rst | 18 ++
include/tscore/ink_config.h.in | 1 +
iocore/net/P_SSLConfig.h | 6 +
iocore/net/P_SSLNetVConnection.h | 6 +
iocore/net/SSLConfig.cc | 13 +-
iocore/net/SSLNetVConnection.cc | 44 +++++
iocore/net/SSLSessionTicket.cc | 15 +-
iocore/net/SSLStats.cc | 12 +-
iocore/net/SSLStats.h | 1 +
iocore/net/SSLUtils.cc | 146 ++++++++++++++-
mgmt/RecordsConfig.cc | 5 +-
proxy/ProxyTransaction.cc | 4 +-
proxy/ProxyTransaction.h | 2 +-
proxy/hdrs/HTTP.h | 26 +++
proxy/hdrs/HdrToken.cc | 10 +-
proxy/hdrs/MIME.cc | 22 +--
proxy/hdrs/MIME.h | 6 +-
proxy/http/Http1ClientSession.cc | 8 +-
proxy/http/Http1ClientSession.h | 2 +
proxy/http/HttpSM.cc | 24 ++-
proxy/http/HttpSM.h | 3 +-
proxy/http/HttpTransact.cc | 14 ++
proxy/http/HttpTransact.h | 1 +
proxy/http2/Http2ClientSession.cc | 25 ++-
proxy/http2/Http2ClientSession.h | 17 +-
proxy/http2/Http2ConnectionState.cc | 25 ++-
src/traffic_quic/traffic_quic.cc | 2 +-
tests/gold_tests/tls/early_h1_get.txt | 3 +
tests/gold_tests/tls/early_h1_post.txt | 6 +
tests/gold_tests/tls/early_h2_get.txt | Bin 0 -> 77 bytes
tests/gold_tests/tls/early_h2_multi1.txt | Bin 0 -> 172 bytes
tests/gold_tests/tls/early_h2_multi2.txt | Bin 0 -> 170 bytes
tests/gold_tests/tls/early_h2_post.txt | Bin 0 -> 77 bytes
tests/gold_tests/tls/h2_early_decode.py | 257 +++++++++++++++++++++++++++
tests/gold_tests/tls/h2_early_gen.py | 185 +++++++++++++++++++
tests/gold_tests/tls/test-0rtt-s_client.py | 69 +++++++
tests/gold_tests/tls/tls_0rtt_server.test.py | 193 ++++++++++++++++++++
39 files changed, 1160 insertions(+), 42 deletions(-)
diff --git a/build/crypto.m4 b/build/crypto.m4
index 6361955..64787b4 100644
--- a/build/crypto.m4
+++ b/build/crypto.m4
@@ -287,3 +287,31 @@ AC_DEFUN([TS_CHECK_CRYPTO_SET_CIPHERSUITES], [
TS_ARG_ENABLE_VAR([use], [tls-set-ciphersuites])
AC_SUBST(use_tls_set_ciphersuites)
])
+
+dnl
+dnl Since OpenSSL 1.1.1
+dnl
+AC_DEFUN([TS_CHECK_EARLY_DATA], [
+ _set_ciphersuites_saved_LIBS=$LIBS
+
+ TS_ADDTO(LIBS, [$OPENSSL_LIBS])
+ AC_CHECK_HEADERS(openssl/ssl.h)
+ AC_CHECK_FUNCS(
+ SSL_set_max_early_data,
+ [
+ has_tls_early_data=1
+ early_data_check=yes
+ ],
+ [
+ has_tls_early_data=0
+ early_data_check=no
+ ]
+ )
+
+ LIBS=$_set_ciphersuites_saved_LIBS
+
+ AC_MSG_CHECKING([for OpenSSL early data support])
+ AC_MSG_RESULT([$early_data_check])
+
+ AC_SUBST(has_tls_early_data)
+])
diff --git a/configure.ac b/configure.ac
index c03c6df..6b46a1c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1292,6 +1292,9 @@ TS_CHECK_CRYPTO_OCSP
# Check for SSL_CTX_set_ciphersuites call
TS_CHECK_CRYPTO_SET_CIPHERSUITES
+# Check for openssl early data support
+TS_CHECK_EARLY_DATA
+
saved_LIBS="$LIBS"
TS_ADDTO([LIBS], ["$OPENSSL_LIBS"])
diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index 0e8743a..9bef21f 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -3529,6 +3529,24 @@ Client-Related Configuration
engines. This setting assumes an absolute path. An example config file is at
:ts:git:`contrib/openssl/load_engine.cnf`.
+TLS v1.3 0-RTT Configuration
+----------------------------
+
+.. note::
+ TLS v1.3 must be enabled in order to utilize 0-RTT early data.
+
+.. ts:cv:: CONFIG proxy.config.ssl.server.max_early_data INT 0
+
+ Specifies the maximum amount of early data in bytes that is permitted to be sent on a single connection.
+
+ The minimum value that enables early data, and the suggested value for this option are both 16384 (16KB).
+
+ Setting to ``0`` effectively disables 0-RTT.
+
+.. ts:cv:: CONFIG proxy.config.ssl.server.allow_early_data_params INT 0
+
+ Set to ``1`` to allow HTTP parameters on early data requests.
+
OCSP Stapling Configuration
===========================
diff --git a/include/tscore/ink_config.h.in b/include/tscore/ink_config.h.in
index e994ea0..3ff2acb 100644
--- a/include/tscore/ink_config.h.in
+++ b/include/tscore/ink_config.h.in
@@ -78,6 +78,7 @@
#define TS_USE_LINUX_NATIVE_AIO @use_linux_native_aio@
#define TS_USE_REMOTE_UNWINDING @use_remote_unwinding@
#define TS_USE_TLS_OCSP @use_tls_ocsp@
+#define TS_HAS_TLS_EARLY_DATA @has_tls_early_data@
#define TS_HAS_SO_PEERCRED @has_so_peercred@
diff --git a/iocore/net/P_SSLConfig.h b/iocore/net/P_SSLConfig.h
index 54a43dd..628eb63 100644
--- a/iocore/net/P_SSLConfig.h
+++ b/iocore/net/P_SSLConfig.h
@@ -40,6 +40,8 @@
#include "SSLSessionCache.h"
#include "YamlSNIConfig.h"
+#include "P_SSLUtils.h"
+
struct SSLCertLookup;
struct ssl_ticket_key_block;
@@ -100,6 +102,10 @@ struct SSLConfigParams : public ConfigInfo {
char *server_groups_list;
char *client_groups_list;
+ static uint32_t server_max_early_data;
+ static uint32_t server_recv_max_early_data;
+ static bool server_allow_early_data_params;
+
static int ssl_maxrecord;
static bool ssl_allow_client_renegotiation;
diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h
index 3b01405..609d6f1 100644
--- a/iocore/net/P_SSLNetVConnection.h
+++ b/iocore/net/P_SSLNetVConnection.h
@@ -409,6 +409,12 @@ public:
bool protocol_mask_set = false;
unsigned long protocol_mask;
+ // early data related stuff
+ bool early_data_finish = false;
+ MIOBuffer *early_data_buf = nullptr;
+ IOBufferReader *early_data_reader = nullptr;
+ int64_t read_from_early_data = 0;
+
// Only applies during the VERIFY certificate hooks (client and server side)
// Means to give the plugin access to the data structure passed in during the underlying
// openssl callback so the plugin can make more detailed decisions about the
diff --git a/iocore/net/SSLConfig.cc b/iocore/net/SSLConfig.cc
index a87eba3..99b3be2 100644
--- a/iocore/net/SSLConfig.cc
+++ b/iocore/net/SSLConfig.cc
@@ -41,7 +41,6 @@
#include "HttpConfig.h"
#include "P_Net.h"
-#include "P_SSLUtils.h"
#include "P_SSLClientUtils.h"
#include "P_SSLCertLookup.h"
#include "SSLDiags.h"
@@ -66,6 +65,11 @@ init_ssl_ctx_func SSLConfigParams::init_ssl_ctx_cb = nullptr;
load_ssl_file_func SSLConfigParams::load_ssl_file_cb = nullptr;
IpMap *SSLConfigParams::proxy_protocol_ipmap = nullptr;
+const uint32_t EARLY_DATA_DEFAULT_SIZE = 16384;
+uint32_t SSLConfigParams::server_max_early_data = 0;
+uint32_t SSLConfigParams::server_recv_max_early_data = EARLY_DATA_DEFAULT_SIZE;
+bool SSLConfigParams::server_allow_early_data_params = false;
+
int SSLConfigParams::async_handshake_enabled = 0;
char *SSLConfigParams::engine_conf_file = nullptr;
@@ -278,6 +282,13 @@ SSLConfigParams::initialize()
ssl_client_ctx_options |= SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
#endif
+ REC_ReadConfigInteger(server_max_early_data, "proxy.config.ssl.server.max_early_data");
+ REC_ReadConfigInt32(server_allow_early_data_params, "proxy.config.ssl.server.allow_early_data_params");
+
+ // According to OpenSSL the default value is 16384,
+ // we keep it unless "server_max_early_data" is higher.
+ server_recv_max_early_data = std::max(server_max_early_data, EARLY_DATA_DEFAULT_SIZE);
+
REC_ReadConfigStringAlloc(serverCertChainFilename, "proxy.config.ssl.server.cert_chain.filename");
REC_ReadConfigStringAlloc(serverCertRelativePath, "proxy.config.ssl.server.cert.path");
set_paths_helper(serverCertRelativePath, nullptr, &serverCertPathOnly, nullptr);
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index 051c97a..fa59bb4 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -34,6 +34,7 @@
#include "P_Net.h"
#include "P_SSLUtils.h"
+#include "P_SSLNextProtocolSet.h"
#include "P_SSLConfig.h"
#include "P_SSLClientUtils.h"
#include "P_SSLSNI.h"
@@ -175,6 +176,38 @@ make_ssl_connection(SSL_CTX *ctx, SSLNetVConnection *netvc)
BIO *wbio = BIO_new_fd(netvc->get_socket(), BIO_NOCLOSE);
BIO_set_mem_eof_return(wbio, -1);
SSL_set_bio(ssl, rbio, wbio);
+
+#if TS_HAS_TLS_EARLY_DATA
+ // Must disable OpenSSL's internal anti-replay if external cache is used with
+ // 0-rtt, otherwise session reuse will be broken. The freshness check described
+ // in https://tools.ietf.org/html/rfc8446#section-8.3 is still performed. But we
+ // still need to implement something to try to prevent replay atacks.
+ //
+ // We are now also disabling this when using OpenSSL's internal cache, since we
+ // are calling "ssl_accept" non-blocking, it seems to be confusing the anti-replay
+ // mechanism and causing session resumption to fail.
+ SSLConfig::scoped_config params;
+ if (SSL_version(ssl) >= TLS1_3_VERSION && params->server_max_early_data > 0) {
+ bool ret1 = false;
+ bool ret2 = false;
+ if ((ret1 = SSL_set_max_early_data(ssl, params->server_max_early_data)) == 1) {
+ Debug("ssl_early_data", "SSL_set_max_early_data: success");
+ } else {
+ Debug("ssl_early_data", "SSL_set_max_early_data: failed");
+ }
+
+ if ((ret2 = SSL_set_recv_max_early_data(ssl, params->server_recv_max_early_data)) == 1) {
+ Debug("ssl_early_data", "SSL_set_recv_max_early_data: success");
+ } else {
+ Debug("ssl_early_data", "SSL_set_recv_max_early_data: failed");
+ }
+
+ if (ret1 && ret2) {
+ Debug("ssl_early_data", "Must disable anti-replay if 0-rtt is enabled.");
+ SSL_set_options(ssl, SSL_OP_NO_ANTI_REPLAY);
+ }
+ }
+#endif
}
SSLNetVCAttach(ssl, netvc);
@@ -923,6 +956,17 @@ SSLNetVConnection::free(EThread *t)
ats_free(tunnel_host);
+ if (early_data_reader != nullptr) {
+ early_data_reader->dealloc();
+ }
+
+ if (early_data_buf != nullptr) {
+ free_MIOBuffer(early_data_buf);
+ }
+
+ early_data_reader = nullptr;
+ early_data_buf = nullptr;
+
clear();
SET_CONTINUATION_HANDLER(this, (SSLNetVConnHandler)&SSLNetVConnection::startEvent);
ink_assert(con.fd == NO_FD);
diff --git a/iocore/net/SSLSessionTicket.cc b/iocore/net/SSLSessionTicket.cc
index 5610764..de7bce5 100644
--- a/iocore/net/SSLSessionTicket.cc
+++ b/iocore/net/SSLSessionTicket.cc
@@ -55,6 +55,7 @@ int
ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv, EVP_CIPHER_CTX *cipher_ctx, HMAC_CTX *hctx,
int enc)
{
+ SSLConfig::scoped_config config;
SSLCertificateConfig::scoped_config lookup;
SSLTicketKeyConfig::scoped_config params;
SSLNetVConnection &netvc = *SSLNetVCAccess(ssl);
@@ -82,7 +83,7 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
EVP_EncryptInit_ex(cipher_ctx, EVP_aes_128_cbc(), nullptr, most_recent_key.aes_key, iv);
HMAC_Init_ex(hctx, most_recent_key.hmac_secret, sizeof(most_recent_key.hmac_secret), evp_md_func, nullptr);
- Debug("ssl", "create ticket for a new session.");
+ Debug("ssl_session_ticket", "create ticket for a new session.");
SSL_INCREMENT_DYN_STAT(ssl_total_tickets_created_stat);
return 1;
} else if (enc == 0) {
@@ -91,7 +92,7 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
EVP_DecryptInit_ex(cipher_ctx, EVP_aes_128_cbc(), nullptr, keyblock->keys[i].aes_key, iv);
HMAC_Init_ex(hctx, keyblock->keys[i].hmac_secret, sizeof(keyblock->keys[i].hmac_secret), evp_md_func, nullptr);
- Debug("ssl", "verify the ticket for an existing session.");
+ Debug("ssl_session_ticket", "verify the ticket for an existing session.");
// Increase the total number of decrypted tickets.
SSL_INCREMENT_DYN_STAT(ssl_total_tickets_verified_stat);
@@ -100,12 +101,20 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
}
netvc.setSSLSessionCacheHit(true);
+
+#if TS_HAS_TLS_EARLY_DATA
+ if (SSL_version(ssl) >= TLS1_3_VERSION && config->server_max_early_data > 0) {
+ Debug("ssl_session_ticket", "make sure tickets are only used once.");
+ return 2;
+ }
+#endif
+
// When we decrypt with an "older" key, encrypt the ticket again with the most recent key.
return (i == 0) ? 1 : 2;
}
}
- Debug("ssl", "keyname is not consistent.");
+ Debug("ssl_session_ticket", "keyname is not consistent.");
SSL_INCREMENT_DYN_STAT(ssl_total_tickets_not_found_stat);
return 0;
}
diff --git a/iocore/net/SSLStats.cc b/iocore/net/SSLStats.cc
index a29adc3..f9d5304 100644
--- a/iocore/net/SSLStats.cc
+++ b/iocore/net/SSLStats.cc
@@ -173,7 +173,7 @@ SSLInitializeStatistics()
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_session_cache_lock_contention", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_session_cache_lock_contention, RecRawStatSyncCount);
- /* Track dynamic record size */
+ // Track dynamic record size
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.default_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_total_dyn_def_tls_record_count, RecRawStatSyncSum);
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.max_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
@@ -181,7 +181,7 @@ SSLInitializeStatistics()
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.redo_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_total_dyn_redo_tls_record_count, RecRawStatSyncCount);
- /* error stats */
+ // error stats
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_error_syscall", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_error_syscall, RecRawStatSyncCount);
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_error_read_eos", RECD_COUNTER, RECP_PERSISTENT,
@@ -191,7 +191,7 @@ SSLInitializeStatistics()
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_sni_name_set_failure", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_sni_name_set_failure, RecRawStatSyncCount);
- /* ocsp stapling stats */
+ // ocsp stapling stats
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_revoked_cert_stat", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_ocsp_revoked_cert_stat, RecRawStatSyncCount);
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_unknown_cert_stat", RECD_COUNTER, RECP_PERSISTENT,
@@ -201,7 +201,7 @@ SSLInitializeStatistics()
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_refresh_cert_failure", RECD_INT, RECP_PERSISTENT,
(int)ssl_ocsp_refresh_cert_failure_stat, RecRawStatSyncCount);
- /* SSL Version stats */
+ // SSL Version stats
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_sslv3", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_total_sslv3, RecRawStatSyncCount);
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_tlsv1", RECD_COUNTER, RECP_PERSISTENT,
@@ -213,6 +213,10 @@ SSLInitializeStatistics()
RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_tlsv13", RECD_COUNTER, RECP_PERSISTENT,
(int)ssl_total_tlsv13, RecRawStatSyncCount);
+ // TLSv1.3 0-RTT stats
+ RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.early_data_received", RECD_INT, RECP_PERSISTENT,
+ (int)ssl_early_data_received_count, RecRawStatSyncCount);
+
// Get and register the SSL cipher stats. Note that we are using the default SSL context to obtain
// the cipher list. This means that the set of ciphers is fixed by the build configuration and not
// filtered by proxy.config.ssl.server.cipher_suite. This keeps the set of cipher suites stable across
diff --git a/iocore/net/SSLStats.h b/iocore/net/SSLStats.h
index ccd6e93..202aa15 100644
--- a/iocore/net/SSLStats.h
+++ b/iocore/net/SSLStats.h
@@ -84,6 +84,7 @@ enum SSL_Stats {
ssl_session_cache_eviction,
ssl_session_cache_lock_contention,
ssl_session_cache_new_session,
+ ssl_early_data_received_count, // how many times we received early data
/* error stats */
ssl_error_syscall,
diff --git a/iocore/net/SSLUtils.cc b/iocore/net/SSLUtils.cc
index b1994c1..eb28242 100644
--- a/iocore/net/SSLUtils.cc
+++ b/iocore/net/SSLUtils.cc
@@ -1109,7 +1109,8 @@ SSLMultiCertConfigLoader::index_certificate(SSLCertLookup *lookup, SSLCertContex
static void
ssl_callback_info(const SSL *ssl, int where, int ret)
{
- Debug("ssl", "ssl_callback_info ssl: %p where: %d ret: %d State: %s", ssl, where, ret, SSL_state_string_long(ssl));
+ Debug("ssl", "ssl_callback_info ssl: %p, where: %d, ret: %d, State: %s", ssl, where, ret, SSL_state_string_long(ssl));
+
SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
if ((where & SSL_CB_ACCEPT_LOOP) && netvc->getSSLHandShakeComplete() == true &&
@@ -1132,6 +1133,13 @@ ssl_callback_info(const SSL *ssl, int where, int ret)
#endif
#endif
#endif
+#ifdef TLS1_3_VERSION
+ // TLSv1.3 has no renegotiation.
+ if (SSL_version(ssl) >= TLS1_3_VERSION) {
+ Debug("ssl", "TLSv1.3 has no renegotiation.");
+ return;
+ }
+#endif
netvc->setSSLClientRenegotiationAbort(true);
Debug("ssl", "ssl_callback_info trying to renegotiate from the client");
}
@@ -1237,7 +1245,6 @@ SSLMultiCertConfigLoader::init_server_ssl_ctx(std::vector<X509 *> &cert_list, co
SSL_CTX_sess_set_get_cb(ctx, ssl_get_cached_session);
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL | additional_cache_flags);
-
break;
}
}
@@ -1696,7 +1703,26 @@ SSLWriteBuffer(SSL *ssl, const void *buf, int64_t nbytes, int64_t &nwritten)
return SSL_ERROR_NONE;
}
ERR_clear_error();
- int ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+
+ int ret;
+#if TS_HAS_TLS_EARLY_DATA
+ if (SSL_version(ssl) >= TLS1_3_VERSION) {
+ if (SSL_is_init_finished(ssl)) {
+ ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+ } else {
+ size_t nwrite;
+ ret = SSL_write_early_data(ssl, buf, static_cast<size_t>(nbytes), &nwrite);
+ if (ret == 1) {
+ ret = nwrite;
+ }
+ }
+ } else {
+ ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+ }
+#else
+ ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+#endif
+
if (ret > 0) {
nwritten = ret;
BIO *bio = SSL_get_wbio(ssl);
@@ -1724,6 +1750,63 @@ SSLReadBuffer(SSL *ssl, void *buf, int64_t nbytes, int64_t &nread)
return SSL_ERROR_NONE;
}
ERR_clear_error();
+
+#if TS_HAS_TLS_EARLY_DATA
+ if (SSL_version(ssl) >= TLS1_3_VERSION) {
+ SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
+
+ int64_t early_data_len = 0;
+ if (netvc->early_data_reader != nullptr) {
+ early_data_len = netvc->early_data_reader->read_avail();
+ }
+
+ if (early_data_len > 0) {
+ Debug("ssl_early_data", "Reading from early data buffer.");
+ netvc->read_from_early_data += netvc->early_data_reader->read(buf, nbytes < early_data_len ? nbytes : early_data_len);
+
+ if (nbytes < early_data_len) {
+ nread = nbytes;
+ } else {
+ nread = early_data_len;
+ }
+
+ return SSL_ERROR_NONE;
+ }
+
+ if (SSLConfigParams::server_max_early_data > 0 && !netvc->early_data_finish) {
+ Debug("ssl_early_data", "More early data to read.");
+ ssl_error_t ssl_error = SSL_ERROR_NONE;
+ size_t read_bytes = 0;
+
+ int ret = SSL_read_early_data(ssl, buf, static_cast<size_t>(nbytes), &read_bytes);
+
+ if (ret == SSL_READ_EARLY_DATA_ERROR) {
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_ERROR");
+ ssl_error = SSL_get_error(ssl, ret);
+ Debug("ssl_early_data", "Error reading early data: %s", ERR_error_string(ERR_get_error(), nullptr));
+ } else {
+ if ((nread = read_bytes) > 0) {
+ netvc->read_from_early_data += read_bytes;
+ SSL_INCREMENT_DYN_STAT(ssl_early_data_received_count);
+ if (is_debug_tag_set("ssl_early_data_show_received")) {
+ std::string early_data_str(reinterpret_cast<char *>(buf), nread);
+ Debug("ssl_early_data_show_received", "Early data buffer: \n%s", early_data_str.c_str());
+ }
+ }
+
+ if (ret == SSL_READ_EARLY_DATA_FINISH) {
+ netvc->early_data_finish = true;
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_FINISH: size = %lu", nread);
+ } else {
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_SUCCESS: size = %lu", nread);
+ }
+ }
+
+ return ssl_error;
+ }
+ }
+#endif
+
int ret = SSL_read(ssl, buf, static_cast<int>(nbytes));
if (ret > 0) {
nread = ret;
@@ -1744,11 +1827,64 @@ ssl_error_t
SSLAccept(SSL *ssl)
{
ERR_clear_error();
- int ret = SSL_accept(ssl);
+
+ int ret = 0;
+ int ssl_error = SSL_ERROR_NONE;
+
+#if TS_HAS_TLS_EARLY_DATA
+ SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
+ if (SSLConfigParams::server_max_early_data > 0 && !netvc->early_data_finish) {
+ size_t nread;
+ if (netvc->early_data_buf == nullptr) {
+ netvc->early_data_buf = new_MIOBuffer(BUFFER_SIZE_INDEX_16K);
+ netvc->early_data_reader = netvc->early_data_buf->alloc_reader();
+ }
+
+ while (true) {
+ IOBufferBlock *block = new_IOBufferBlock();
+ block->alloc(BUFFER_SIZE_INDEX_16K);
+ ret = SSL_read_early_data(ssl, block->buf(), index_to_buffer_size(BUFFER_SIZE_INDEX_16K), &nread);
+
+ if (ret == SSL_READ_EARLY_DATA_ERROR) {
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_ERROR");
+ break;
+ } else {
+ if (nread > 0) {
+ block->fill(nread);
+ netvc->early_data_buf->append_block(block);
+ SSL_INCREMENT_DYN_STAT(ssl_early_data_received_count);
+
+ if (is_debug_tag_set("ssl_early_data_show_received")) {
+ std::string early_data_str(reinterpret_cast<char *>(block->buf()), nread);
+ Debug("ssl_early_data_show_received", "Early data buffer: \n%s", early_data_str.c_str());
+ }
+ }
+
+ if (ret == SSL_READ_EARLY_DATA_FINISH) {
+ netvc->early_data_finish = true;
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_FINISH: size = %lu", nread);
+
+ if (netvc->early_data_reader->read_avail() == 0) {
+ Debug("ssl_early_data", "no data in early data buffer");
+ ERR_clear_error();
+ ret = SSL_accept(ssl);
+ }
+ break;
+ }
+ Debug("ssl_early_data", "SSL_READ_EARLY_DATA_SUCCESS: size = %lu", nread);
+ }
+ }
+ } else {
+ ret = SSL_accept(ssl);
+ }
+#else
+ ret = SSL_accept(ssl);
+#endif
+
if (ret > 0) {
return SSL_ERROR_NONE;
}
- int ssl_error = SSL_get_error(ssl, ret);
+ ssl_error = SSL_get_error(ssl, ret);
if (ssl_error == SSL_ERROR_SSL && is_debug_tag_set("ssl.error.accept")) {
char buf[512];
unsigned long e = ERR_peek_last_error();
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index 6fa1c63..135e821 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -1150,7 +1150,10 @@ static const RecordElement RecordsConfig[] =
,
{RECT_CONFIG, "proxy.config.ssl.client.groups_list", RECD_STRING, nullptr, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
,
-
+ {RECT_CONFIG, "proxy.config.ssl.server.max_early_data", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ ,
+ {RECT_CONFIG, "proxy.config.ssl.server.allow_early_data_params", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ ,
//##############################################################################
//#
//# OCSP (Online Certificate Status Protocol) Stapling Configuration
diff --git a/proxy/ProxyTransaction.cc b/proxy/ProxyTransaction.cc
index 73843e8..8042c34 100644
--- a/proxy/ProxyTransaction.cc
+++ b/proxy/ProxyTransaction.cc
@@ -30,7 +30,7 @@
ProxyTransaction::ProxyTransaction() : VConnection(nullptr) {}
void
-ProxyTransaction::new_transaction()
+ProxyTransaction::new_transaction(bool from_early_data)
{
ink_assert(_sm == nullptr);
@@ -39,7 +39,7 @@ ProxyTransaction::new_transaction()
ink_release_assert(_proxy_ssn != nullptr);
_sm = HttpSM::allocate();
- _sm->init();
+ _sm->init(from_early_data);
HttpTxnDebug("[%" PRId64 "] Starting transaction %d using sm [%" PRId64 "]", _proxy_ssn->connection_id(),
_proxy_ssn->get_transact_count(), _sm->sm_id);
diff --git a/proxy/ProxyTransaction.h b/proxy/ProxyTransaction.h
index 60a9994..67defe3 100644
--- a/proxy/ProxyTransaction.h
+++ b/proxy/ProxyTransaction.h
@@ -37,7 +37,7 @@ public:
/// Virtual Methods
//
- virtual void new_transaction();
+ virtual void new_transaction(bool from_early_data = false);
virtual void attach_server_session(Http1ServerSession *ssession, bool transaction_done = true);
Action *adjust_thread(Continuation *cont, int event, void *data);
virtual void release(IOBufferReader *r);
diff --git a/proxy/hdrs/HTTP.h b/proxy/hdrs/HTTP.h
index c7c43e1..5acbaef 100644
--- a/proxy/hdrs/HTTP.h
+++ b/proxy/hdrs/HTTP.h
@@ -78,6 +78,7 @@ enum HTTPStatus {
HTTP_STATUS_REQUEST_URI_TOO_LONG = 414,
HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415,
HTTP_STATUS_RANGE_NOT_SATISFIABLE = 416,
+ HTTP_STATUS_TOO_EARLY = 425,
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500,
HTTP_STATUS_NOT_IMPLEMENTED = 501,
@@ -506,6 +507,8 @@ public:
/// also had a port, @c false otherwise.
mutable bool m_port_in_header = false;
+ mutable bool early_data = false;
+
HTTPHdr() = default; // Force the creation of the default constructor
int valid() const;
@@ -629,6 +632,9 @@ public:
const char *reason_get(int *length);
void reason_set(const char *value, int length);
+ void mark_early_data(bool flag = true) const;
+ bool is_early_data() const;
+
ParseResult parse_req(HTTPParser *parser, const char **start, const char *end, bool eof, bool strict_uri_parsing = false,
size_t max_request_line_size = UINT16_MAX, size_t max_hdr_field_size = 131070);
ParseResult parse_resp(HTTPParser *parser, const char **start, const char *end, bool eof);
@@ -1221,6 +1227,26 @@ HTTPHdr::reason_set(const char *value, int length)
/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/
+inline void
+HTTPHdr::mark_early_data(bool flag) const
+{
+ ink_assert(valid());
+ early_data = flag;
+}
+
+/*-------------------------------------------------------------------------
+ -------------------------------------------------------------------------*/
+
+inline bool
+HTTPHdr::is_early_data() const
+{
+ ink_assert(valid());
+ return early_data;
+}
+
+/*-------------------------------------------------------------------------
+ -------------------------------------------------------------------------*/
+
inline ParseResult
HTTPHdr::parse_req(HTTPParser *parser, const char **start, const char *end, bool eof, bool strict_uri_parsing,
size_t max_request_line_size, size_t max_hdr_field_size)
diff --git a/proxy/hdrs/HdrToken.cc b/proxy/hdrs/HdrToken.cc
index a1ddec4..fcfc5c6 100644
--- a/proxy/hdrs/HdrToken.cc
+++ b/proxy/hdrs/HdrToken.cc
@@ -110,7 +110,10 @@ static const char *_hdrtoken_strs[] = {
"X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
// RFC-2739
- "Forwarded"};
+ "Forwarded",
+
+ // RFC-8470
+ "Early-Data"};
static HdrTokenTypeBinding _hdrtoken_strs_type_initializers[] = {
{"file", HDRTOKEN_TYPE_SCHEME},
@@ -359,7 +362,10 @@ static const char *_hdrtoken_commonly_tokenized_strs[] = {
"X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
// RFC-2739
- "Forwarded"};
+ "Forwarded",
+
+ // RFC-8470
+ "Early-Data"};
/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/
diff --git a/proxy/hdrs/MIME.cc b/proxy/hdrs/MIME.cc
index 7107401..75cab70 100644
--- a/proxy/hdrs/MIME.cc
+++ b/proxy/hdrs/MIME.cc
@@ -156,6 +156,7 @@ const char *MIME_FIELD_FORWARDED;
const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
const char *MIME_FIELD_HTTP2_SETTINGS;
+const char *MIME_FIELD_EARLY_DATA;
const char *MIME_VALUE_BYTES;
const char *MIME_VALUE_CHUNKED;
@@ -272,6 +273,7 @@ int MIME_LEN_FORWARDED;
int MIME_LEN_SEC_WEBSOCKET_KEY;
int MIME_LEN_SEC_WEBSOCKET_VERSION;
int MIME_LEN_HTTP2_SETTINGS;
+int MIME_LEN_EARLY_DATA;
int MIME_WKSIDX_ACCEPT;
int MIME_WKSIDX_ACCEPT_CHARSET;
@@ -351,6 +353,7 @@ int MIME_WKSIDX_FORWARDED;
int MIME_WKSIDX_SEC_WEBSOCKET_KEY;
int MIME_WKSIDX_SEC_WEBSOCKET_VERSION;
int MIME_WKSIDX_HTTP2_SETTINGS;
+int MIME_WKSIDX_EARLY_DATA;
/***********************************************************************
* *
@@ -745,11 +748,10 @@ mime_init()
MIME_FIELD_X_ID = hdrtoken_string_to_wks("X-ID");
MIME_FIELD_X_FORWARDED_FOR = hdrtoken_string_to_wks("X-Forwarded-For");
MIME_FIELD_FORWARDED = hdrtoken_string_to_wks("Forwarded");
-
- MIME_FIELD_SEC_WEBSOCKET_KEY = hdrtoken_string_to_wks("Sec-WebSocket-Key");
- MIME_FIELD_SEC_WEBSOCKET_VERSION = hdrtoken_string_to_wks("Sec-WebSocket-Version");
-
- MIME_FIELD_HTTP2_SETTINGS = hdrtoken_string_to_wks("HTTP2-Settings");
+ MIME_FIELD_SEC_WEBSOCKET_KEY = hdrtoken_string_to_wks("Sec-WebSocket-Key");
+ MIME_FIELD_SEC_WEBSOCKET_VERSION = hdrtoken_string_to_wks("Sec-WebSocket-Version");
+ MIME_FIELD_HTTP2_SETTINGS = hdrtoken_string_to_wks("HTTP2-Settings");
+ MIME_FIELD_EARLY_DATA = hdrtoken_string_to_wks("Early-Data");
MIME_LEN_ACCEPT = hdrtoken_wks_to_length(MIME_FIELD_ACCEPT);
MIME_LEN_ACCEPT_CHARSET = hdrtoken_wks_to_length(MIME_FIELD_ACCEPT_CHARSET);
@@ -826,11 +828,10 @@ mime_init()
MIME_LEN_X_ID = hdrtoken_wks_to_length(MIME_FIELD_X_ID);
MIME_LEN_X_FORWARDED_FOR = hdrtoken_wks_to_length(MIME_FIELD_X_FORWARDED_FOR);
MIME_LEN_FORWARDED = hdrtoken_wks_to_length(MIME_FIELD_FORWARDED);
-
- MIME_LEN_SEC_WEBSOCKET_KEY = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_KEY);
- MIME_LEN_SEC_WEBSOCKET_VERSION = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_VERSION);
-
- MIME_LEN_HTTP2_SETTINGS = hdrtoken_wks_to_length(MIME_FIELD_HTTP2_SETTINGS);
+ MIME_LEN_SEC_WEBSOCKET_KEY = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_KEY);
+ MIME_LEN_SEC_WEBSOCKET_VERSION = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_VERSION);
+ MIME_LEN_HTTP2_SETTINGS = hdrtoken_wks_to_length(MIME_FIELD_HTTP2_SETTINGS);
+ MIME_LEN_EARLY_DATA = hdrtoken_wks_to_length(MIME_FIELD_EARLY_DATA);
MIME_WKSIDX_ACCEPT = hdrtoken_wks_to_index(MIME_FIELD_ACCEPT);
MIME_WKSIDX_ACCEPT_CHARSET = hdrtoken_wks_to_index(MIME_FIELD_ACCEPT_CHARSET);
@@ -909,6 +910,7 @@ mime_init()
MIME_WKSIDX_SEC_WEBSOCKET_KEY = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_KEY);
MIME_WKSIDX_SEC_WEBSOCKET_VERSION = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_VERSION);
MIME_WKSIDX_HTTP2_SETTINGS = hdrtoken_wks_to_index(MIME_FIELD_HTTP2_SETTINGS);
+ MIME_WKSIDX_EARLY_DATA = hdrtoken_wks_to_index(MIME_FIELD_EARLY_DATA);
MIME_VALUE_BYTES = hdrtoken_string_to_wks("bytes");
MIME_VALUE_CHUNKED = hdrtoken_string_to_wks("chunked");
diff --git a/proxy/hdrs/MIME.h b/proxy/hdrs/MIME.h
index 1752ad3..a1e9dbc 100644
--- a/proxy/hdrs/MIME.h
+++ b/proxy/hdrs/MIME.h
@@ -458,6 +458,7 @@ extern const char *MIME_FIELD_FORWARDED;
extern const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
extern const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
extern const char *MIME_FIELD_HTTP2_SETTINGS;
+extern const char *MIME_FIELD_EARLY_DATA;
extern const char *MIME_VALUE_BYTES;
extern const char *MIME_VALUE_CHUNKED;
@@ -559,7 +560,6 @@ extern int MIME_LEN_ATS_INTERNAL;
extern int MIME_LEN_X_ID;
extern int MIME_LEN_X_FORWARDED_FOR;
extern int MIME_LEN_FORWARDED;
-
extern int MIME_LEN_BYTES;
extern int MIME_LEN_CHUNKED;
extern int MIME_LEN_CLOSE;
@@ -582,11 +582,10 @@ extern int MIME_LEN_PROXY_REVALIDATE;
extern int MIME_LEN_PUBLIC;
extern int MIME_LEN_S_MAXAGE;
extern int MIME_LEN_NEED_REVALIDATE_ONCE;
-
extern int MIME_LEN_SEC_WEBSOCKET_KEY;
extern int MIME_LEN_SEC_WEBSOCKET_VERSION;
-
extern int MIME_LEN_HTTP2_SETTINGS;
+extern int MIME_LEN_EARLY_DATA;
extern int MIME_WKSIDX_ACCEPT;
extern int MIME_WKSIDX_ACCEPT_CHARSET;
@@ -664,6 +663,7 @@ extern int MIME_WKSIDX_X_ID;
extern int MIME_WKSIDX_SEC_WEBSOCKET_KEY;
extern int MIME_WKSIDX_SEC_WEBSOCKET_VERSION;
extern int MIME_WKSIDX_HTTP2_SETTINGS;
+extern int MIME_WKSIDX_EARLY_DATA;
/***********************************************************************
* *
diff --git a/proxy/http/Http1ClientSession.cc b/proxy/http/Http1ClientSession.cc
index d5b2269..38705df 100644
--- a/proxy/http/Http1ClientSession.cc
+++ b/proxy/http/Http1ClientSession.cc
@@ -139,6 +139,12 @@ Http1ClientSession::new_connection(NetVConnection *new_vc, MIOBuffer *iobuf, IOB
ssn_start_time = Thread::get_hrtime();
in_destroy = false;
+ SSLNetVConnection *ssl_vc = dynamic_cast<SSLNetVConnection *>(new_vc);
+ if (ssl_vc != nullptr) {
+ read_from_early_data = ssl_vc->read_from_early_data;
+ Debug("ssl_early_data", "read_from_early_data = %ld", read_from_early_data);
+ }
+
MUTEX_TRY_LOCK(lock, mutex, this_ethread());
ink_assert(lock.is_locked());
@@ -460,7 +466,7 @@ Http1ClientSession::new_transaction()
transact_count++;
client_vc->add_to_active_queue();
- trans.new_transaction();
+ trans.new_transaction(read_from_early_data > 0 ? true : false);
}
void
diff --git a/proxy/http/Http1ClientSession.h b/proxy/http/Http1ClientSession.h
index fd9afda..4fa61c6 100644
--- a/proxy/http/Http1ClientSession.h
+++ b/proxy/http/Http1ClientSession.h
@@ -124,6 +124,8 @@ private:
int released_transactions = 0;
+ int64_t read_from_early_data = 0;
+
public:
// Link<Http1ClientSession> debug_link;
LINK(Http1ClientSession, debug_link);
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 7f36d63..540b94c 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -296,10 +296,12 @@ HttpSM::destroy()
}
void
-HttpSM::init()
+HttpSM::init(bool from_early_data)
{
milestones[TS_MILESTONE_SM_START] = Thread::get_hrtime();
+ _from_early_data = from_early_data;
+
magic = HTTP_SM_MAGIC_ALIVE;
// Unique state machine identifier
@@ -659,7 +661,7 @@ HttpSM::state_read_client_request_header(int event, void *data)
}
// We need to handle EOS as well as READ_READY because the client
- // may have sent all of the data already followed by a fIN and that
+ // may have sent all of the data already followed by a FIN and that
// should be OK.
if (ua_raw_buffer_reader != nullptr) {
bool do_blind_tunnel = false;
@@ -750,6 +752,24 @@ HttpSM::state_read_client_request_header(int event, void *data)
case PARSE_RESULT_DONE:
SMDebug("http", "[%" PRId64 "] done parsing client request header", sm_id);
+ if (_from_early_data) {
+ // Only allow early data for safe methods defined in RFC7231 Section 4.2.1.
+ // https://tools.ietf.org/html/rfc7231#section-4.2.1
+ SMDebug("ssl_early_data", "%d", t_state.hdr_info.client_request.method_get_wksidx());
+ if (!HttpTransactHeaders::is_method_safe(t_state.hdr_info.client_request.method_get_wksidx())) {
+ SMDebug("http", "client request was from early data but is NOT safe");
+ call_transact_and_set_next_state(HttpTransact::TooEarly);
+ return 0;
+ } else if (!SSLConfigParams::server_allow_early_data_params &&
+ (t_state.hdr_info.client_request.m_http->u.req.m_url_impl->m_len_params > 0 ||
+ t_state.hdr_info.client_request.m_http->u.req.m_url_impl->m_len_query > 0)) {
+ SMDebug("http", "client request was from early data but HAS parameters");
+ call_transact_and_set_next_state(HttpTransact::TooEarly);
+ return 0;
+ }
+ t_state.hdr_info.client_request.mark_early_data();
+ }
+
ua_txn->set_session_active();
if (t_state.hdr_info.client_request.version_get() == HTTPVersion(1, 1) &&
diff --git a/proxy/http/HttpSM.h b/proxy/http/HttpSM.h
index 1f237ef..e357267 100644
--- a/proxy/http/HttpSM.h
+++ b/proxy/http/HttpSM.h
@@ -213,7 +213,7 @@ public:
HttpVCTableEntry *get_ua_entry(); // Added to get the ua_entry pointer - YTS-TEAM
HttpVCTableEntry *get_server_entry(); // Added to get the server_entry pointer
- void init();
+ void init(bool from_early_data = false);
void attach_client_session(ProxyTransaction *client_vc_arg, IOBufferReader *buffer_reader);
@@ -633,6 +633,7 @@ private:
PostDataBuffers _postbuf;
int _client_connection_id = -1, _client_transaction_id = -1;
int _client_transaction_priority_weight = -1, _client_transaction_priority_dependence = -1;
+ bool _from_early_data = false;
};
// Function to get the cache_sm object - YTS Team, yamsat
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index c83c105..34f236e 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -571,6 +571,16 @@ HttpTransact::Forbidden(State *s)
}
void
+HttpTransact::TooEarly(State *s)
+{
+ TxnDebug("http_trans", "[TooEarly]"
+ "Early Data method is not safe");
+ bootstrap_state_variables_from_request(s, &s->hdr_info.client_request);
+ build_error_response(s, HTTP_STATUS_TOO_EARLY, "Too Early", "too#early");
+ TRANSACT_RETURN(SM_ACTION_SEND_ERROR_CACHE_NOOP, nullptr);
+}
+
+void
HttpTransact::HandleBlindTunnel(State *s)
{
URL u;
@@ -7571,6 +7581,10 @@ HttpTransact::build_request(State *s, HTTPHdr *base_request, HTTPHdr *outgoing_r
TxnDebug("http_trans", "[build_request] request expect 100-continue headers removed");
}
+ if (base_request->is_early_data()) {
+ outgoing_request->value_set_int(MIME_FIELD_EARLY_DATA, MIME_LEN_EARLY_DATA, 1);
+ }
+
s->request_sent_time = ink_local_time();
s->current.now = s->request_sent_time;
diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h
index 09ddc29..f181c61 100644
--- a/proxy/http/HttpTransact.h
+++ b/proxy/http/HttpTransact.h
@@ -930,6 +930,7 @@ public:
static void HandleRequestAuthorized(State *s);
static void BadRequest(State *s);
static void Forbidden(State *s);
+ static void TooEarly(State *s);
static void PostActiveTimeoutResponse(State *s);
static void PostInactiveTimeoutResponse(State *s);
static void DecideCacheLookup(State *s);
diff --git a/proxy/http2/Http2ClientSession.cc b/proxy/http2/Http2ClientSession.cc
index 981b5a9..d703925 100644
--- a/proxy/http2/Http2ClientSession.cc
+++ b/proxy/http2/Http2ClientSession.cc
@@ -199,6 +199,12 @@ Http2ClientSession::new_connection(NetVConnection *new_vc, MIOBuffer *iobuf, IOB
this->connection_state.mutex = this->mutex;
+ SSLNetVConnection *ssl_vc = dynamic_cast<SSLNetVConnection *>(new_vc);
+ if (ssl_vc != nullptr) {
+ this->read_from_early_data = ssl_vc->read_from_early_data;
+ Debug("ssl_early_data", "read_from_early_data = %ld", this->read_from_early_data);
+ }
+
Http2SsnDebug("session born, netvc %p", this->client_vc);
this->client_vc->set_tcp_congestion_control(CLIENT_SIDE);
@@ -443,6 +449,11 @@ Http2ClientSession::state_read_connection_preface(int event, void *edata)
return 0;
}
+ // Check whether data is read from early data
+ if (this->read_from_early_data > 0) {
+ this->read_from_early_data -= this->read_from_early_data > nbytes ? nbytes : this->read_from_early_data;
+ }
+
Http2SsnDebug("received connection preface");
this->_reader->consume(nbytes);
HTTP2_SET_SESSION_HANDLER(&Http2ClientSession::state_start_frame_read);
@@ -489,12 +500,19 @@ Http2ClientSession::do_start_frame_read(Http2ErrorCode &ret_error)
Http2SsnDebug("receiving frame header");
nbytes = copy_from_buffer_reader(buf, this->_reader, sizeof(buf));
+ this->cur_frame_from_early_data = false;
if (!http2_parse_frame_header(make_iovec(buf), this->current_hdr)) {
Http2SsnDebug("frame header parse failure");
this->do_io_close();
return -1;
}
+ // Check whether data is read from early data
+ if (this->read_from_early_data > 0) {
+ this->read_from_early_data -= this->read_from_early_data > nbytes ? nbytes : this->read_from_early_data;
+ this->cur_frame_from_early_data = true;
+ }
+
Http2SsnDebug("frame header length=%u, type=%u, flags=0x%x, streamid=%u", (unsigned)this->current_hdr.length,
(unsigned)this->current_hdr.type, (unsigned)this->current_hdr.flags, this->current_hdr.streamid);
@@ -552,8 +570,13 @@ Http2ClientSession::do_complete_frame_read()
// XXX parse the frame and handle it ...
ink_release_assert(this->_reader->read_avail() >= this->current_hdr.length);
- Http2Frame frame(this->current_hdr, this->_reader);
+ Http2Frame frame(this->current_hdr, this->_reader, this->cur_frame_from_early_data);
send_connection_event(&this->connection_state, HTTP2_SESSION_EVENT_RECV, &frame);
+ // Check whether data is read from early data
+ if (this->read_from_early_data > 0) {
+ this->read_from_early_data -=
+ this->read_from_early_data > this->current_hdr.length ? this->current_hdr.length : this->read_from_early_data;
+ }
this->_reader->consume(this->current_hdr.length);
++(this->_n_frame_read);
diff --git a/proxy/http2/Http2ClientSession.h b/proxy/http2/Http2ClientSession.h
index eee6a3b..c3a7209 100644
--- a/proxy/http2/Http2ClientSession.h
+++ b/proxy/http2/Http2ClientSession.h
@@ -80,8 +80,11 @@ struct Http2UpgradeContext {
class Http2Frame
{
public:
- Http2Frame(const Http2FrameHeader &h, IOBufferReader *r) : hdr(h), ioreader(r) {}
- Http2Frame(Http2FrameType type, Http2StreamId streamid, uint8_t flags) : hdr({0, (uint8_t)type, flags, streamid}) {}
+ Http2Frame(const Http2FrameHeader &h, IOBufferReader *r, bool e = false) : hdr(h), ioreader(r), from_early_data(e) {}
+ Http2Frame(Http2FrameType type, Http2StreamId streamid, uint8_t flags, bool e = false)
+ : hdr({0, (uint8_t)type, flags, streamid}), from_early_data(e)
+ {
+ }
IOBufferReader *
reader() const
@@ -147,6 +150,12 @@ public:
}
}
+ bool
+ is_from_early_data() const
+ {
+ return this->from_early_data;
+ }
+
// noncopyable
Http2Frame(Http2Frame &) = delete;
Http2Frame &operator=(const Http2Frame &) = delete;
@@ -155,6 +164,7 @@ private:
Http2FrameHeader hdr; // frame header
Ptr<IOBufferBlock> ioblock; // frame payload
IOBufferReader *ioreader = nullptr;
+ bool from_early_data = false;
};
class Http2ClientSession : public ProxySession
@@ -264,6 +274,9 @@ private:
Event *_reenable_event = nullptr;
int _n_frame_read = 0;
+
+ int64_t read_from_early_data = 0;
+ bool cur_frame_from_early_data = false;
};
extern ClassAllocator<Http2ClientSession> http2ClientSessionAllocator;
diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc
index eb23143..fed9cd2 100644
--- a/proxy/http2/Http2ConnectionState.cc
+++ b/proxy/http2/Http2ConnectionState.cc
@@ -369,7 +369,7 @@ rcv_headers_frame(Http2ConnectionState &cstate, const Http2Frame &frame)
if (!empty_request) {
SCOPED_MUTEX_LOCK(stream_lock, stream->mutex, this_ethread());
stream->mark_milestone(Http2StreamMilestone::START_TXN);
- stream->new_transaction();
+ stream->new_transaction(frame.is_from_early_data());
// Send request header to SM
stream->send_request(cstate);
}
@@ -925,7 +925,9 @@ rcv_continuation_frame(Http2ConnectionState &cstate, const Http2Frame &frame)
// Set up the State Machine
SCOPED_MUTEX_LOCK(stream_lock, stream->mutex, this_ethread());
stream->mark_milestone(Http2StreamMilestone::START_TXN);
- stream->new_transaction();
+ // This should be fine, need to verify whether we need to replace this with the
+ // "from_early_data" flag from the associated HEADERS frame.
+ stream->new_transaction(frame.is_from_early_data());
// Send request header to SM
stream->send_request(cstate);
} else {
@@ -1020,6 +1022,25 @@ Http2ConnectionState::main_event_handler(int event, void *edata)
break;
}
+ // We need to be careful here, certain frame types are not safe over 0-rtt, tentative for now.
+ // DATA: NO
+ // HEADERS: YES (safe http methods only, can only be checked after parsing the payload).
+ // PRIORITY: YES
+ // RST_STREAM: NO
+ // SETTINGS: YES
+ // PUSH_PROMISE: NO
+ // PING: YES
+ // GOAWAY: NO
+ // WINDOW_UPDATE: YES
+ // CONTINUATION: YES (safe http methods only, same as HEADERS frame).
+ if (frame->is_from_early_data() &&
+ (frame->header().type == HTTP2_FRAME_TYPE_DATA || frame->header().type == HTTP2_FRAME_TYPE_RST_STREAM ||
+ frame->header().type == HTTP2_FRAME_TYPE_PUSH_PROMISE || frame->header().type == HTTP2_FRAME_TYPE_GOAWAY)) {
+ Http2StreamDebug(ua_session, stream_id, "Discard a frame which is received from early data and has type=%x",
+ frame->header().type);
+ break;
+ }
+
if (frame_handlers[frame->header().type]) {
error = frame_handlers[frame->header().type](*this, *frame);
} else {
diff --git a/src/traffic_quic/traffic_quic.cc b/src/traffic_quic/traffic_quic.cc
index 1c290eb..69e2350 100644
--- a/src/traffic_quic/traffic_quic.cc
+++ b/src/traffic_quic/traffic_quic.cc
@@ -312,7 +312,7 @@ HttpSM::attach_client_session(ProxyTransaction *, IOBufferReader *)
}
void
-HttpSM::init()
+HttpSM::init(bool from_early_data)
{
ink_abort("do not call stub");
}
diff --git a/tests/gold_tests/tls/early_h1_get.txt b/tests/gold_tests/tls/early_h1_get.txt
new file mode 100644
index 0000000..047f0a3
--- /dev/null
+++ b/tests/gold_tests/tls/early_h1_get.txt
@@ -0,0 +1,3 @@
+GET /early_get HTTP/1.1
+Host: 127.0.0.1
+
diff --git a/tests/gold_tests/tls/early_h1_post.txt b/tests/gold_tests/tls/early_h1_post.txt
new file mode 100644
index 0000000..e85fd17
--- /dev/null
+++ b/tests/gold_tests/tls/early_h1_post.txt
@@ -0,0 +1,6 @@
+POST /early_post HTTP/1.1
+Host: 127.0.0.1
+Content-Length: 11
+
+knock knock
+
diff --git a/tests/gold_tests/tls/early_h2_get.txt b/tests/gold_tests/tls/early_h2_get.txt
new file mode 100644
index 0000000..6f535e8
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_get.txt differ
diff --git a/tests/gold_tests/tls/early_h2_multi1.txt b/tests/gold_tests/tls/early_h2_multi1.txt
new file mode 100644
index 0000000..71c3350
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_multi1.txt differ
diff --git a/tests/gold_tests/tls/early_h2_multi2.txt b/tests/gold_tests/tls/early_h2_multi2.txt
new file mode 100644
index 0000000..cdd633a
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_multi2.txt differ
diff --git a/tests/gold_tests/tls/early_h2_post.txt b/tests/gold_tests/tls/early_h2_post.txt
new file mode 100644
index 0000000..ecee5c7
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_post.txt differ
diff --git a/tests/gold_tests/tls/h2_early_decode.py b/tests/gold_tests/tls/h2_early_decode.py
new file mode 100755
index 0000000..7f87527
--- /dev/null
+++ b/tests/gold_tests/tls/h2_early_decode.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+'''
+A simple tool to decode http2 frames for 0-rtt testing.
+'''
+
+import hpack
+import sys
+
+class Http2FrameDefs:
+
+ RESERVE_BIT_MASK = 0x7fffffff
+
+ DATA_FRAME = 0x00
+ HEADERS_FRAME = 0x01
+ PRIORITY_FRAME = 0x02
+ RST_STREAM_FRAME = 0x03
+ SETTINGS_FRAME = 0x04
+ PUSH_PROMISE_FRAME = 0x05
+ PING_FRAME = 0x06
+ GOAWAY_FRAME = 0x07
+ WINDOW_UPDATE_FRAME = 0x08
+ CONTINUATION_FRAME = 0x09
+
+ FRAME_TYPES = {
+ DATA_FRAME: 'DATA',
+ HEADERS_FRAME: 'HEADERS',
+ PRIORITY_FRAME: 'PRIORITY',
+ RST_STREAM_FRAME: 'RST_STREAM',
+ SETTINGS_FRAME: 'SETTINGS',
+ PUSH_PROMISE_FRAME: 'PUSH_PROMISE',
+ PING_FRAME: 'PING',
+ GOAWAY_FRAME: 'GOAWAY',
+ WINDOW_UPDATE_FRAME: 'WINDOW_UPDATE',
+ CONTINUATION_FRAME: 'CONTINUATION'
+ }
+
+ SETTINGS_HEADER_TABLE_SIZE = 0x01
+ SETTINGS_ENABLE_PUSH = 0x02
+ SETTINGS_MAX_CONCURRENT_STREAMS = 0x03
+ SETTINGS_INITIAL_WINDOW_SIZE = 0x04
+ SETTINGS_MAX_FRAME_SIZE = 0x05
+ SETTINGS_MAX_HEADER_LIST_SIZE = 0x06
+
+ SETTINGS_ID = {
+ SETTINGS_HEADER_TABLE_SIZE: 'HEADER_TABLE_SIZE',
+ SETTINGS_ENABLE_PUSH: 'ENABLE_PUSH',
+ SETTINGS_MAX_CONCURRENT_STREAMS: 'MAX_CONCURRENT_STREAMS',
+ SETTINGS_INITIAL_WINDOW_SIZE: 'INITIAL_WINDOW_SIZE',
+ SETTINGS_MAX_FRAME_SIZE: 'MAX_FRAME_SIZE',
+ SETTINGS_MAX_HEADER_LIST_SIZE: 'MAX_HEADER_LIST_SIZE'
+ }
+
+ RST_STREAM_NO_ERROR = 0x0
+ RST_STREAM_PROTOCOL_ERROR = 0x1
+ RST_STREAM_INTERNAL_ERROR = 0x2
+ RST_STREAM_FLOW_CONTROL_ERROR = 0x3
+ RST_STREAM_SETTINGS_TIMEOUT = 0x4
+ RST_STREAM_STREAM_CLOSED = 0x5
+ RST_STREAM_FRAME_SIZE_ERROR = 0x6
+ RST_STREAM_REFUSED_STREAM = 0x7
+ RST_STREAM_CANCEL = 0x8
+ RST_STREAM_COMPRESSION_ERROR = 0x9
+ RST_STREAM_CONNECT_ERROR = 0xa
+ RST_STREAM_ENHANCE_YOUR_CALM = 0xb
+ RST_STREAM_INADEQUATE_SECURITY = 0xc
+ RST_STREAM_HTTP_1_1_REQUIRED = 0xd
+
+ RST_STREAM_ERROR_CODES = {
+ RST_STREAM_NO_ERROR: 'NO_ERROR',
+ RST_STREAM_PROTOCOL_ERROR: 'PROTOCOL_ERROR',
+ RST_STREAM_INTERNAL_ERROR: 'INTERNAL_ERROR',
+ RST_STREAM_FLOW_CONTROL_ERROR: 'FLOW_CONTROL_ERROR',
+ RST_STREAM_SETTINGS_TIMEOUT: 'SETTINGS_TIMEOUT',
+ RST_STREAM_STREAM_CLOSED: 'STREAM_CLOSED',
+ RST_STREAM_FRAME_SIZE_ERROR: 'FRAME_SIZE_ERROR',
+ RST_STREAM_REFUSED_STREAM: 'REFUSED_STREAM',
+ RST_STREAM_CANCEL: 'CANCEL',
+ RST_STREAM_COMPRESSION_ERROR: 'COMPRESSION_ERROR',
+ RST_STREAM_CONNECT_ERROR: 'CONNECT_ERROR',
+ RST_STREAM_ENHANCE_YOUR_CALM: 'ENHANCE_YOUR_CALM',
+ RST_STREAM_INADEQUATE_SECURITY: 'INADEQUATE_SECURITY',
+ RST_STREAM_HTTP_1_1_REQUIRED: 'HTTP_1_1_REQUIRED'
+ }
+
+
+class Http2Frame:
+ def __init__(self, length, frame_type, flags, stream_id):
+ self.length = length
+ self.frame_type = frame_type
+ self.flags = flags
+ self.stream_id = stream_id
+ self.payload = None
+ self.decode_error = None
+ return
+
+ def add_payload(self, payload):
+ self.payload = payload
+ return
+
+ def read_data(self):
+ if self.frame_type == Http2FrameDefs.DATA_FRAME:
+ return '\n' + self.payload.decode('utf-8')
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def read_headers(self):
+ if self.frame_type == Http2FrameDefs.HEADERS_FRAME:
+ try:
+ decoder = hpack.Decoder()
+ decoded_data = decoder.decode(self.payload)
+ output_str = ''
+ for header in decoded_data:
+ output_str += '\n'
+ for each in header:
+ output_str += each + ' '
+ except hpack.exceptions.InvalidTableIndex:
+ output_str = self.payload.hex()
+ output_str += '\nWarning: Decode failed: Invalid table index (not too important)'
+ return output_str
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def read_rst_stream(self):
+ if self.frame_type == Http2FrameDefs.RST_STREAM_FRAME:
+ error_code = int(self.payload.hex(), 16)
+ return '\nError Code = {0}'.format(Http2FrameDefs.RST_STREAM_ERROR_CODES[error_code])
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def read_settings(self):
+ if self.frame_type == Http2FrameDefs.SETTINGS_FRAME:
+ settings_str = ''
+ for i in range(0, self.length, 6):
+ settings_id = int(self.payload[i:i + 2].hex(), 16)
+ settings_val = int(self.payload[i + 2:i + 6].hex(), 16)
+ settings_str += '\n{0} = {1}'.format(Http2FrameDefs.SETTINGS_ID[settings_id], settings_val)
+ return settings_str
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def read_goaway(self):
+ if self.frame_type == Http2FrameDefs.GOAWAY_FRAME:
+ last_stream_id = int(self.payload[0:4].hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+ error_code = int(self.payload[4:8].hex(), 16)
+ debug_data = self.payload[8:].hex()
+ return '\nLast Stream ID = 0x{0:08x}\nError Code = 0x{1:08x}\nDebug Data = {2}'.format(
+ last_stream_id, error_code, debug_data
+ )
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def read_window_update(self):
+ if self.frame_type == Http2FrameDefs.WINDOW_UPDATE_FRAME:
+ window_size_increment = int(self.payload.hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+ return '\nWindow Size Increment = {0}'.format(window_size_increment)
+ else:
+ return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+ def print_payload(self):
+ if self.frame_type == Http2FrameDefs.DATA_FRAME:
+ return self.read_data()
+ elif self.frame_type == Http2FrameDefs.HEADERS_FRAME:
+ return self.read_headers()
+ elif self.frame_type == Http2FrameDefs.RST_STREAM_FRAME:
+ return self.read_rst_stream()
+ elif self.frame_type == Http2FrameDefs.SETTINGS_FRAME:
+ return self.read_settings()
+ elif self.frame_type == Http2FrameDefs.GOAWAY_FRAME:
+ return self.read_goaway()
+ elif self.frame_type == Http2FrameDefs.WINDOW_UPDATE_FRAME:
+ return self.read_window_update()
+ else:
+ return self.payload.hex()
+
+ def print(self):
+ output = 'Length: {0}\nType: {1}\nFlags: {2}\nStream ID: {3}\nPayload: {4}\n'.format(
+ self.length, Http2FrameDefs.FRAME_TYPES[self.frame_type], self.flags, self.stream_id, self.print_payload()
+ )
+ if self.decode_error is not None:
+ output += self.decode_error + '\n'
+ return output
+
+ def __str__(self):
+ return self.print()
+
+class Decoder:
+ def read_frame_header(self, data):
+ frame = Http2Frame(
+ length=int(data[0:3].hex(), 16),
+ frame_type=int(data[3:4].hex(), 16),
+ flags=int(data[4:5].hex(), 16),
+ stream_id=int(data[5:9].hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+ )
+ return frame
+
+ def decode(self, data):
+ temp_data = data
+ frames = []
+ while len(temp_data) >= 9:
+ frame_header = temp_data[0:9]
+ frame = self.read_frame_header(frame_header)
+ if frame.length > len(temp_data[9:]):
+ frame.decode_error = 'Error: Payload length greater than data: {0} > {1}'.format(frame.length, len(temp_data[9:]))
+ frame.add_payload(temp_data[9:])
+ frames.append(frame)
+ else:
+ frame.add_payload(temp_data[9:9 + frame.length])
+ frames.append(frame)
+ temp_data = temp_data[9 + frame.length:]
+ return frames
+
+def main():
+ # input file is output from openssl s_client.
+ # sample command to get this output:
+ # openssl s_client -bind 127.0.0.1:61991 -connect 127.0.0.1:61992 -tls1_3 -quiet -sess_out /home/duke/Dev/ats-test/sess.dat -sess_in /home/duke/Dev/ats-test/sess.dat -early_data ./gold_tests/tls/early2.txt >! _sandbox/tls_0rtt_server/early2_out.txt 2>&1
+
+ if len(sys.argv) < 2:
+ print('Error: No input file to decode.')
+ exit(1)
+
+ lines = None
+ with open(sys.argv[1], 'rb') as in_file:
+ lines = in_file.readlines()
+
+ data = b''
+ for line in lines:
+ if line.startswith(bytes('SSL_connect:', 'utf-8')) or \
+ line.startswith(bytes('SSL3 alert', 'utf-8')) or \
+ bytes('Can\'t use SSL_get_servername', 'utf-8') in line:
+ continue
+ data += line
+
+ d = Decoder()
+ frames = d.decode(data)
+ for frame in frames:
+ print(frame)
+ exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/gold_tests/tls/h2_early_gen.py b/tests/gold_tests/tls/h2_early_gen.py
new file mode 100755
index 0000000..37e63c2
--- /dev/null
+++ b/tests/gold_tests/tls/h2_early_gen.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+'''
+A simple tool to generate some raw http2 frames for 0-rtt testing.
+'''
+
+# http2 frame format:
+# +-----------------------------------------------+
+# | Length (24) |
+# +---------------+---------------+---------------+
+# | Type (8) | Flags (8) |
+# +-+-------------+---------------+-------------------------------+
+# |R| Stream Identifier (31) |
+# +=+=============================================================+
+# | Frame Payload (0...) ...
+# +---------------------------------------------------------------+
+
+import hpack
+import os
+import sys
+
+H2_PREFACE = bytes.fromhex('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a')
+
+RESERVED_BIT_MASK = 0x7FFFFFFF
+
+TYPE_HEADERS_FRAME = 0x01
+TYPE_SETTINGS_FRAME = 0x04
+TYPE_WINDOW_UPDATE_FRAME = 0x08
+
+SETTINGS_HEADER_TABLE_SIZE = 0x01
+SETTINGS_ENABLE_PUSH = 0x02
+SETTINGS_MAX_CONCURRENT_STREAMS = 0x03
+SETTINGS_INITIAL_WINDOW_SIZE = 0x04
+SETTINGS_MAX_FRAME_SIZE = 0x05
+SETTINGS_MAX_HEADER_LIST_SIZE = 0x06
+
+HEADERS_FLAG_END_STREAM = 0x01
+HEADERS_FLAG_END_HEADERS = 0x04
+HEADERS_FLAG_END_PADDED = 0x08
+HEADERS_FLAG_END_PRIORITY = 0x20
+
+CURRENT_SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
+
+def encode_payload(data):
+ encoder = hpack.Encoder()
+ data_encoded = encoder.encode(data)
+ return data_encoded
+
+def make_frame(frame_length, frame_type, frame_flags, frame_stream_id, frame_payload):
+ frame_length = bytes.fromhex('{0:06x}'.format(frame_length))
+ frame_type = bytes.fromhex('{0:02x}'.format(frame_type))
+ frame_flags = bytes.fromhex('{0:02x}'.format(frame_flags))
+ frame_stream_id = bytes.fromhex('{0:08x}'.format(RESERVED_BIT_MASK & frame_stream_id))
+
+ frame = frame_length + frame_type + frame_flags + frame_stream_id
+
+ if frame_payload is not None:
+ frame += frame_payload
+
+ return frame
+
+def make_settins_frame(ack=False, empty=False):
+ payload = ''
+ if not ack and not empty:
+ payload += '{0:04x}{1:08x}'.format(SETTINGS_ENABLE_PUSH, 0)
+ payload += '{0:04x}{1:08x}'.format(SETTINGS_MAX_CONCURRENT_STREAMS, 100)
+ payload += '{0:04x}{1:08x}'.format(SETTINGS_INITIAL_WINDOW_SIZE, 1073741824)
+ payload = bytes.fromhex(payload)
+
+ frame = make_frame(
+ frame_length = len(payload),
+ frame_type = TYPE_SETTINGS_FRAME,
+ frame_flags = 1 if ack else 0,
+ frame_stream_id = 0,
+ frame_payload = payload
+ )
+
+ return frame
+
+def make_window_update_frame():
+ payload = '{0:08x}'.format(RESERVED_BIT_MASK & 1073676289)
+ payload = bytes.fromhex(payload)
+
+ frame = make_frame(
+ frame_length = len(payload),
+ frame_type = TYPE_WINDOW_UPDATE_FRAME,
+ frame_flags = 0,
+ frame_stream_id = 0,
+ frame_payload = payload
+ )
+ return frame
+
+def make_headers_frame(method, path='', stream_id=0x01):
+ headers = []
+ if method == 'get':
+ headers.append((':method', 'GET'))
+ if path != '':
+ headers.append((':path', path))
+ else:
+ headers.append((':path', '/early_get'))
+ elif method == 'post':
+ headers.append((':method', 'POST'))
+ if path != '':
+ headers.append((':path', path))
+ else:
+ headers.append((':path', '/early_post'))
+
+ headers.extend([
+ (':scheme', 'http'),
+ (':authority', '127.0.0.1'),
+ ('host', '127.0.0.1'),
+ ('accept', '*/*')
+ ])
+
+ headers_encoded = encode_payload(headers)
+
+ frame = make_frame(
+ frame_length = len(headers_encoded),
+ frame_type = TYPE_HEADERS_FRAME,
+ frame_flags = HEADERS_FLAG_END_STREAM | HEADERS_FLAG_END_HEADERS,
+ frame_stream_id = stream_id,
+ frame_payload = headers_encoded
+ )
+
+ return frame
+
+def make_h2_req(test):
+ h2_req = H2_PREFACE
+ if test == 'get' or test == 'post':
+ frames = [
+ make_settins_frame(ack=True),
+ make_headers_frame(test)
+ ]
+ for frame in frames:
+ h2_req += frame
+ elif test == 'multi1':
+ frames = [
+ make_settins_frame(ack=True),
+ make_headers_frame('get', '/early_multi_1', 1),
+ make_headers_frame('get', '/early_multi_2', 3),
+ make_headers_frame('get', '/early_multi_3', 5)
+ ]
+ for frame in frames:
+ h2_req += frame
+ elif test == 'multi2':
+ frames = [
+ make_settins_frame(ack=True),
+ make_headers_frame('get', '/early_multi_1', 1),
+ make_headers_frame('post', stream_id=3),
+ make_headers_frame('get', '/early_multi_3', 5)
+ ]
+ for frame in frames:
+ h2_req += frame
+ else:
+ pass
+ return h2_req
+
+def write_to_file(data, file_name):
+ with open(file_name, 'wb') as out_file:
+ out_file.write(data)
+ return
+
+def main():
+ test = sys.argv[1]
+ write_to_file(make_h2_req(test), os.path.join(CURRENT_SCRIPT_PATH, 'early_h2_{0}.txt'.format(test)))
+ exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/gold_tests/tls/test-0rtt-s_client.py b/tests/gold_tests/tls/test-0rtt-s_client.py
new file mode 100644
index 0000000..05c7165
--- /dev/null
+++ b/tests/gold_tests/tls/test-0rtt-s_client.py
@@ -0,0 +1,69 @@
+'''
+'''
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import sys
+import os
+import shlex
+import h2_early_decode
+
+def main():
+ ats_port = sys.argv[1]
+ http_ver = sys.argv[2]
+ test = sys.argv[3]
+ sess_file_path = os.path.join(sys.argv[4], 'sess.dat')
+ early_data_file_path = os.path.join(sys.argv[4], 'early_{0}_{1}.txt'.format(http_ver, test))
+
+ s_client_cmd_1 = shlex.split('openssl s_client -connect 127.0.0.1:{0} -tls1_3 -quiet -sess_out {1}'.format(ats_port, sess_file_path))
+ s_client_cmd_2 = shlex.split('openssl s_client -connect 127.0.0.1:{0} -tls1_3 -quiet -sess_in {1} -early_data {2}'.format(ats_port, sess_file_path, early_data_file_path))
+
+ create_sess_proc = subprocess.Popen(s_client_cmd_1, env=os.environ.copy(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ try:
+ output = create_sess_proc.communicate(timeout=1)[0]
+ except subprocess.TimeoutExpired:
+ create_sess_proc.kill()
+ output = create_sess_proc.communicate()[0]
+
+ reuse_sess_proc = subprocess.Popen(s_client_cmd_2, env=os.environ.copy(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ try:
+ output = reuse_sess_proc.communicate(timeout=1)[0]
+ except subprocess.TimeoutExpired:
+ reuse_sess_proc.kill()
+ output = reuse_sess_proc.communicate()[0]
+
+ if http_ver == 'h2':
+ lines = output.split(bytes('\n', 'utf-8'))
+ data = b''
+ for line in lines:
+ line += b'\n'
+ if line.startswith(bytes('SSL_connect:', 'utf-8')) or \
+ line.startswith(bytes('SSL3 alert', 'utf-8')) or \
+ bytes('Can\'t use SSL_get_servername', 'utf-8') in line:
+ continue
+ data += line
+ d = h2_early_decode.Decoder()
+ frames = d.decode(data)
+ for frame in frames:
+ print(frame)
+ else:
+ print(output.decode('utf-8'))
+
+ exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/gold_tests/tls/tls_0rtt_server.test.py b/tests/gold_tests/tls/tls_0rtt_server.test.py
new file mode 100644
index 0000000..e438846
--- /dev/null
+++ b/tests/gold_tests/tls/tls_0rtt_server.test.py
@@ -0,0 +1,193 @@
+'''
+'''
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Test.Summary = '''
+Test ATS TLSv1.3 0-RTT support
+'''
+
+Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'))
+
+ts = Test.MakeATSProcess('ts', select_ports=True, enable_tls=True)
+server = Test.MakeOriginServer('server')
+
+request_header1 = {
+ 'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': ''
+}
+response_header1 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'curl test'
+}
+request_header2 = {
+ 'headers': 'GET /early_get HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': ''
+}
+response_header2 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'early data accepted'
+}
+request_header3 = {
+ 'headers': 'POST /early_post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 11\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'knock knock'
+}
+response_header3 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n',
+ 'timestamp': '1415926535.898',
+ 'body': ''
+}
+request_header4 = {
+ 'headers': 'GET /early_multi_1 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': ''
+}
+response_header4 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'early data accepted multi_1'
+}
+request_header5 = {
+ 'headers': 'GET /early_multi_2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': ''
+}
+response_header5 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'early data accepted multi_2'
+}
+request_header6 = {
+ 'headers': 'GET /early_multi_3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': ''
+}
+response_header6 = {
+ 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+ 'timestamp': '1469733493.993',
+ 'body': 'early data accepted multi_3'
+}
+server.addResponse('sessionlog.json', request_header1, response_header1)
+server.addResponse('sessionlog.json', request_header2, response_header2)
+server.addResponse('sessionlog.json', request_header3, response_header3)
+server.addResponse('sessionlog.json', request_header4, response_header4)
+server.addResponse('sessionlog.json', request_header5, response_header5)
+server.addResponse('sessionlog.json', request_header6, response_header6)
+
+ts.addSSLfile('ssl/server.pem')
+ts.addSSLfile('ssl/server.key')
+
+ts.Setup.Copy('test-0rtt-s_client.py')
+ts.Setup.Copy('h2_early_decode.py')
+ts.Setup.Copy('early_h1_get.txt')
+ts.Setup.Copy('early_h1_post.txt')
+ts.Setup.Copy('early_h2_get.txt')
+ts.Setup.Copy('early_h2_post.txt')
+ts.Setup.Copy('early_h2_multi1.txt')
+ts.Setup.Copy('early_h2_multi2.txt')
+
+ts.Disk.records_config.update({
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'http',
+ 'proxy.config.exec_thread.autoconfig': 0,
+ 'proxy.config.exec_thread.limit': 8,
+ 'proxy.config.http.server_ports': '{0}:proto=http2;http:ssl'.format(ts.Variables.ssl_port),
+ 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+ 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+ 'proxy.config.ssl.session_cache': 2,
+ 'proxy.config.ssl.session_cache.size': 512000,
+ 'proxy.config.ssl.session_cache.timeout': 7200,
+ 'proxy.config.ssl.session_cache.num_buckets': 32768,
+ 'proxy.config.ssl.server.session_ticket.enable': 1,
+ 'proxy.config.ssl.server.max_early_data': 16384,
+ 'proxy.config.ssl.server.allow_early_data_params': 0,
+ 'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SH [...]
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+ 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+ 'map / http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+tr = Test.AddTestRun('Basic Curl Test')
+tr.Processes.Default.Command = 'curl https://127.0.0.1:{0} -k'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h1', 'get', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h1', 'post', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('HTTP/1.1 425 Too Early', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 GET)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'get', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'post', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression(':status 425', 'Only safe methods are allowed')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'multi1', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_2', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex with POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'multi2', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression(':status 425', 'Only safe methods are allowed')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')