From 59263855dce15e6cf271bf4be8b50ee1171e8024 Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Mon, 13 Jul 2020 10:10:17 -0500 Subject: [PATCH] Traffic Dump: dump server-side protocol stack (#6972) This also adds some TLS information in the protocol stack, such as the verify_mode applied to the SSL session and whether the server requested a client certificate. Co-authored-by: bneradt --- .../functions/TSClientProtocolStack.en.rst | 21 +- .../api/functions/TSVConn.en.rst | 6 +- .../functions/TSVConnProvidedSslCert.en.rst | 38 +++ include/ts/ts.h | 16 +- include/tscore/ink_inet.h | 13 +- .../experimental/traffic_dump/session_data.cc | 282 ++++++++++-------- .../experimental/traffic_dump/session_data.h | 18 +- .../traffic_dump/transaction_data.cc | 31 +- .../traffic_dump/transaction_data.h | 3 + proxy/http/HttpSM.cc | 47 ++- proxy/http/HttpSM.h | 7 + src/traffic_server/InkAPI.cc | 65 +++- .../pluginTest/traffic_dump/gold/200.gold | 2 +- .../traffic_dump/gold/explicit_target.gold | 2 +- .../traffic_dump/gold/post_with_body.gold | 2 +- .../traffic_dump/gold/two_transactions.gold | 4 +- .../traffic_dump/traffic_dump.test.py | 218 ++++++++++++-- .../traffic_dump_sni_filter.test.py | 15 +- .../pluginTest/traffic_dump/verify_replay.py | 126 ++++++-- tests/tools/lib/replay_schema.json | 47 ++- 20 files changed, 735 insertions(+), 228 deletions(-) create mode 100644 doc/developer-guide/api/functions/TSVConnProvidedSslCert.en.rst diff --git a/doc/developer-guide/api/functions/TSClientProtocolStack.en.rst b/doc/developer-guide/api/functions/TSClientProtocolStack.en.rst index aaff54e1c19..762afcd3387 100644 --- a/doc/developer-guide/api/functions/TSClientProtocolStack.en.rst +++ b/doc/developer-guide/api/functions/TSClientProtocolStack.en.rst @@ -31,10 +31,14 @@ Synopsis .. function:: TSReturnCode TSHttpTxnClientProtocolStackGet(TSHttpTxn txnp, int n, char const** result, int* actual) +.. function:: TSReturnCode TSHttpTxnServerProtocolStackGet(TSHttpTxn txnp, int n, const char** result, int* actual) + .. function:: TSReturnCode TSHttpSsnClientProtocolStackGet(TSHttpSsn ssnp, int n, char const** result, int* actual) .. function:: char const* TSHttpTxnClientProtocolStackContains(TSHttpTxn txnp) +.. function:: const char* TSHttpTxnServerProtocolStackContains(TSHttpTxn txnp, char const* tag) + .. function:: char const* TSHttpSsnClientProtocolStackContains(TSHttpSsn ssnp) .. function:: char const* TSNormalizedProtocolTag(char const* tag) @@ -44,12 +48,15 @@ Synopsis Description =========== -These functions are used to explore the protocol stack of the client (user agent) connection to +These functions are used to explore the protocol stack of either the client (user agent) or origin server connection to |TS|. The functions :func:`TSHttpTxnClientProtocolStackGet` and :func:`TSHttpSsnClientProtocolStackGet` can be used to retrieve the entire protocol stack for the -user agent connection. :func:`TSHttpTxnClientProtocolStackContains` and -:func:`TSHttpSsnClientProtocolStackContains` will check for a specific protocol :arg:`tag` being -present in the stack. +user agent connection. The :func:`TSHttpTxnServerProtocolStackGet` can be used +to retrieve the entire protocol stack for +the origin server connection. :func:`TSHttpTxnClientProtocolStackContains`, +:func:`TSHttpSsnClientProtocolStackContains`, and +:func:`TSHttpTxnServerProtocolStackContains` will check for a specific +protocol :arg:`tag` being present in the stack. Each protocol is represented by tag which is a null terminated string. A particular tag will always be returned as the same character pointer and so protocols can be reliably checked with pointer @@ -60,18 +67,18 @@ normalized value. This is useful for plugins that provide custom protocols for u The protocols are ordered from higher level protocols to the lower level ones on which the higher operate. For instance a stack might look like "http/1.1,tls/1.2,tcp,ipv4". For -:func:`TSHttpTxnClientProtocolStackGet` and :func:`TSHttpSsnClientProtocolStackGet` these values +:func:`TSHttpTxnClientProtocolStackGet`, :func:`TSHttpSsnClientProtocolStackGet`, and :func:`TSHttpTxnServerProtocolStackGet` these values are placed in the array :arg:`result`. :arg:`count` is the maximum number of elements of :arg:`result` that may be modified by the function call. If :arg:`actual` is not :const:`NULL` then the actual number of elements in the protocol stack will be returned. If this is equal or less than :arg:`count` then all elements were returned. If it is larger then some layers were omitted from :arg:`result`. If the full stack is required :arg:`actual` can be used to resize :arg:`result` to be sufficient to hold all of the elements and the function called again with updated :arg:`count` -and :arg:`result`. In practice the maximum number of elements will is almost certain to be less +and :arg:`result`. In practice the maximum number of elements is almost certain to be less than 10 which therefore should suffice. These functions return :const:`TS_SUCCESS` on success and :const:`TS_ERROR` on failure which should only occurr if :arg:`txnp` or :arg:`ssnp` are invalid. -The :func:`TSHttpTxnClientProtocolStackContains` and :func:`TSHttpSsnClientProtocolStackContains` +The :func:`TSHttpTxnClientProtocolStackContains`, :func:`TSHttpSsnClientProtocolStackContains`, and :func:`TSHttpTxnServerProtocolStackContains` functions are provided for the convenience when only the presence of a protocol is of interest, not its location or the presence of other protocols. These functions return :const:`NULL` if the protocol :arg:`tag` is not present, and a pointer to the normalized tag if it is present. The strings are diff --git a/doc/developer-guide/api/functions/TSVConn.en.rst b/doc/developer-guide/api/functions/TSVConn.en.rst index e73b7872e72..249e1104a77 100644 --- a/doc/developer-guide/api/functions/TSVConn.en.rst +++ b/doc/developer-guide/api/functions/TSVConn.en.rst @@ -21,7 +21,7 @@ TSVConn ******* -Traffic Server APIs to get :type:`TSVConn` from :type:`TSHttpSsn` object +Traffic Server APIs to get :type:`TSVConn` from :type:`TSHttpSsn` or :type:`TSHttpTxn` object. Synopsis ======== @@ -32,11 +32,13 @@ Synopsis .. function:: TSVConn TSHttpSsnClientVConnGet(TSHttpSsn ssnp) .. function:: TSVConn TSHttpSsnServerVConnGet(TSHttpSsn ssnp) +.. function:: TSVConn TSHttpTxnServerVConnGet(TSHttpTxn txnp) Description =========== -These APIs allow the developer to get the NetVconnection (represented by :type:`TSVConn`) from the Http session (:type:`TSHttpSsn`) object. +These APIs allow the developer to get the NetVconnection (represented by :type:`TSVConn`) from the Http session (:type:`TSHttpSsn`) or transaction (:type:`TSHttpTxn`) object. :func:`TSHttpSsnClientVConnGet` returns the :type:`TSVConn` associated with the client side :type:`TSHttpSsn` object. :func:`TSHttpSsnServerVConnGet` returns the same associated with the server side :type:`TSHttpSsn`. +:func:`TSHttpTxnServerVConnGet` returns the same associated with a :type:`TSHttpTxn`. diff --git a/doc/developer-guide/api/functions/TSVConnProvidedSslCert.en.rst b/doc/developer-guide/api/functions/TSVConnProvidedSslCert.en.rst new file mode 100644 index 00000000000..6d4a1c90536 --- /dev/null +++ b/doc/developer-guide/api/functions/TSVConnProvidedSslCert.en.rst @@ -0,0 +1,38 @@ +.. 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. + +.. include:: ../../../common.defs + +.. default-domain:: c + +TSVConnProvidedSslCert +******************************** + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: int TSVConnProvidedSslCert(TSVConn svc) + +Description +=========== + +Determines whether the connection associated with :arg:`svc` was an SSL +connection on which a server certificate was provided in the SSL handshake. +Returns :literal:`1` if it was and :literal:`0` otherwise. diff --git a/include/ts/ts.h b/include/ts/ts.h index 21c3ec6be30..8efa2514187 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1216,9 +1216,11 @@ tsapi void TSHttpHookAdd(TSHttpHookID id, TSCont contp); tsapi void TSHttpSsnHookAdd(TSHttpSsn ssnp, TSHttpHookID id, TSCont contp); tsapi void TSHttpSsnReenable(TSHttpSsn ssnp, TSEvent event); tsapi int TSHttpSsnTransactionCount(TSHttpSsn ssnp); -/* get TSVConn from session */ +/* Get the TSVConn from a session. */ tsapi TSVConn TSHttpSsnClientVConnGet(TSHttpSsn ssnp); tsapi TSVConn TSHttpSsnServerVConnGet(TSHttpSsn ssnp); +/* Get the TSVConn from a transaction. */ +tsapi TSVConn TSHttpTxnServerVConnGet(TSHttpTxn txnp); /* -------------------------------------------------------------------------- SSL connections */ @@ -1258,6 +1260,10 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); /* Returns 1 if the sslp argument refers to a SSL connection */ tsapi int TSVConnIsSsl(TSVConn sslp); +/* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. + */ +tsapi int TSVConnProvidedSslCert(TSVConn sslp); + tsapi TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); tsapi int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); tsapi TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); @@ -2512,7 +2518,7 @@ tsapi TSUuid TSProcessUuidGet(void); tsapi const char *TSHttpTxnPluginTagGet(TSHttpTxn txnp); /* - * Return information about the client protocols + * Return information about the client protocols. */ tsapi TSReturnCode TSHttpTxnClientProtocolStackGet(TSHttpTxn txnp, int n, const char **result, int *actual); tsapi TSReturnCode TSHttpSsnClientProtocolStackGet(TSHttpSsn ssnp, int n, const char **result, int *actual); @@ -2521,6 +2527,12 @@ tsapi const char *TSHttpSsnClientProtocolStackContains(TSHttpSsn ssnp, char cons tsapi const char *TSNormalizedProtocolTag(char const *tag); tsapi const char *TSRegisterProtocolTag(char const *tag); +/* + * Return information about the server protocols. + */ +tsapi TSReturnCode TSHttpTxnServerProtocolStackGet(TSHttpTxn txnp, int n, const char **result, int *actual); +tsapi const char *TSHttpTxnServerProtocolStackContains(TSHttpTxn txnp, char const *tag); + // If, for the given transaction, the URL has been remapped, this function puts the memory location of the "from" URL object in // the variable pointed to by urlLocp, and returns TS_SUCCESS. (The URL object will be within memory allocated to the // transaction object.) Otherwise, the function returns TS_ERROR. diff --git a/include/tscore/ink_inet.h b/include/tscore/ink_inet.h index efd67c04b93..aa6ddf057dc 100644 --- a/include/tscore/ink_inet.h +++ b/include/tscore/ink_inet.h @@ -45,7 +45,18 @@ IN6_IS_ADDR_UNSPECIFIED(in6_addr const *addr) } #endif -// IP protocol stack tags. +/* + * IP protocol stack tags. + * + * When adding support for an additional protocol, the following minimum steps + * should be done: + * + * 1. This set of string_views should be updated with the new tag. + * 2. A populate_protocol function overload should be implemented for the + * appropriate VConnection or ProxySession virtual function. + * 3. Traffic Dump should be updated to handle the new tag in: + * plugins/experimental/traffic_dump/session_data.cc + */ extern const std::string_view IP_PROTO_TAG_IPV4; extern const std::string_view IP_PROTO_TAG_IPV6; extern const std::string_view IP_PROTO_TAG_UDP; diff --git a/plugins/experimental/traffic_dump/session_data.cc b/plugins/experimental/traffic_dump/session_data.cc index e77c788c39e..281b78df818 100644 --- a/plugins/experimental/traffic_dump/session_data.cc +++ b/plugins/experimental/traffic_dump/session_data.cc @@ -20,11 +20,14 @@ #include #include #include -#include #include +#include #include #include #include +#include + +#include #include "session_data.h" #include "global_variables.h" @@ -34,6 +37,148 @@ namespace { /** The final string used to close a JSON session. */ char const constexpr *const json_closing = "]}]}"; + +/** + * A mapping from IP_PROTO_TAG to the string describing the JSON protocol node. + */ +std::unordered_map tag_to_node = { + {std::string(IP_PROTO_TAG_IPV4), R"("name":"ip","version":"4")"}, + {std::string(IP_PROTO_TAG_IPV6), R"("name":"ip","version":"6")"}, + + {std::string(IP_PROTO_TAG_TCP), R"("name":"tcp")"}, + {std::string(IP_PROTO_TAG_UDP), R"("name":"udp")"}, + + {std::string(IP_PROTO_TAG_QUIC), R"("name:":"quic")"}, + + {std::string(IP_PROTO_TAG_TLS_1_0), R"("name":"tls","version":"1.0")"}, + {std::string(IP_PROTO_TAG_TLS_1_1), R"("name":"tls","version":"1.1")"}, + {std::string(IP_PROTO_TAG_TLS_1_2), R"("name":"tls","version":"1.2")"}, + {std::string(IP_PROTO_TAG_TLS_1_3), R"("name":"tls","version":"1.3")"}, + + {std::string(IP_PROTO_TAG_HTTP_0_9), R"("name":"http","version":"0.9")"}, + {std::string(IP_PROTO_TAG_HTTP_1_0), R"("name":"http","version":"1.0")"}, + {std::string(IP_PROTO_TAG_HTTP_1_1), R"("name":"http","version":"1.1")"}, + {std::string(IP_PROTO_TAG_HTTP_2_0), R"("name":"http","version":"2")"}, + + {std::string(IP_PROTO_TAG_HTTP_QUIC), R"("name":"http","version":"0.9")"}, + {std::string(IP_PROTO_TAG_HTTP_3), R"("name":"http","version":"3")"}, +}; + +/** Create a TLS characteristics node. + * + * This function encapsulates the logic common between the client-side and + * server-side logic for populating a "tls" node. + * + * @param[in] vconn The virtual connection for the session. + * + * @return The node describing the TLS properties of this session. + */ +std::string +get_tls_description_helper(TSVConn vconn) +{ + if (vconn == nullptr) { + return ""; + } + SSL *ssl_obj = reinterpret_cast(TSVConnSslConnectionGet(vconn)); + if (ssl_obj == nullptr) { + return ""; + } + std::ostringstream tls_description; + tls_description << R"("name":"tls",)"; + char const *version_ptr = SSL_get_version(ssl_obj); + if (version_ptr != nullptr) { + std::string_view version{version_ptr}; + if (!version.empty()) { + tls_description << R"("version":")" << version << R"(",)"; + } + } + char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name); + if (sni_ptr != nullptr) { + std::string_view sni{sni_ptr}; + if (!sni.empty()) { + tls_description << R"("sni":")" << sni << R"(",)"; + } + } + + int verify_mode = SSL_get_verify_mode(ssl_obj); + tls_description << R"("proxy-verify-mode":)" << std::to_string(verify_mode) << ","; + bool provided_cert = TSVConnProvidedSslCert(vconn); + tls_description << R"("proxy-provided-cert":)" << (provided_cert ? "true" : "false"); + return tls_description.str(); +} + +/** Create a client-side TLS characteristics node. + * + * @param[in] ssnp The pointer for this session. + * + * @return The node describing the TLS properties of this session. + */ +std::string +get_client_tls_description(TSHttpSsn ssnp) +{ + TSVConn client_ssn_vc = TSHttpSsnClientVConnGet(ssnp); + return get_tls_description_helper(client_ssn_vc); +} + +/** Create a server-side TLS characteristics node. + * + * @param[in] ssnp The pointer for this session. + * + * @return The node describing the TLS properties of this session. + */ +std::string +get_server_tls_description(TSHttpTxn txnp) +{ + TSVConn server_ssn_vc = TSHttpTxnServerVConnGet(txnp); + return get_tls_description_helper(server_ssn_vc); +} + +using get_protocol_stack_f = std::function; +using get_tls_description_f = std::function; + +/** Create the protocol stack for a session. + * + * This function encapsulates the logic common between the client-side and + * server-side logic for populating a protocol stack. + * + * @param[in] get_protocol_stack The function to use to populate a protocol + * stack. + * + * @return The description of the protocol stack. + */ +std::string +get_protocol_stack_helper(get_protocol_stack_f get_protocol_stack, get_tls_description_f get_tls_node) +{ + std::ostringstream protocol_description; + protocol_description << R"("protocol":[)"; + char const *protocol[10]; + int count = -1; + TSAssert(TS_SUCCESS == get_protocol_stack(10, protocol, &count)); + bool is_first_printed_protocol = true; + for (int i = 0; i < count; ++i) { + std::string_view protocol_string(protocol[i]); + if (!is_first_printed_protocol) { + protocol_description << ","; + } + is_first_printed_protocol = false; + if (protocol_string.find("tls") != std::string::npos) { + protocol_description << '{' << get_tls_node() << '}'; + } else { + auto search = tag_to_node.find(std::string(protocol_string)); + if (search == tag_to_node.end()) { + // If the tag from get_protocol_stack is not in our list, then our + // tag_to_node has not been updated with the new tag. Update tag_to_node. + TSError("[%s] Missing tag node description: '%.*s'", traffic_dump::debug_tag, static_cast(protocol_string.length()), + protocol_string.data()); + protocol_description << R"({"name":")" << protocol_string << R"("})"; + } else { + protocol_description << '{' << search->second << '}'; + } + } + } + protocol_description << "]"; // Close the "protocol" sequence. + return protocol_description.str(); +} } // namespace namespace traffic_dump @@ -104,130 +249,24 @@ SessionData::init(std::string_view log_directory, int64_t max_disk_usage, int64_ return true; } -/** Create a TLS characteristics node. - * - * This function encapsulates the logic common between the client-side and - * server-side logic for populating the TLS node. - * - * @param[in] ssnp The pointer for this session. - * - * @return The node describing the TLS properties of this session. - */ std::string -get_tls_description_helper(TSVConn ssn_vc) +SessionData::get_client_protocol_description(TSHttpSsn client_ssnp) { - TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc); - SSL *ssl_obj = (SSL *)ssl_conn; - if (ssl_obj == nullptr) { - return ""; - } - std::ostringstream tls_description; - tls_description << R"("tls":{)"; - char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name); - if (sni_ptr != nullptr) { - std::string_view sni{sni_ptr}; - if (!sni.empty()) { - tls_description << R"("sni":")" << sni << R"(",)"; - } - } - tls_description << R"("verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")"; - tls_description << "}"; - return tls_description.str(); + return get_protocol_stack_helper( + [&client_ssnp](int n, const char **result, int *actual) { + return TSHttpSsnClientProtocolStackGet(client_ssnp, n, result, actual); + }, + [&client_ssnp]() { return get_client_tls_description(client_ssnp); }); } -/** Create a server-side TLS characteristics node. - * - * @param[in] ssnp The pointer for this session. - * - * @return The node describing the TLS properties of this session. - */ std::string -get_server_tls_description(TSHttpSsn ssnp) +SessionData::get_server_protocol_description(TSHttpTxn server_txnp) { - TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp); - return get_tls_description_helper(ssn_vc); -} - -/** Create a client-side TLS characteristics node. - * - * @param[in] ssnp The pointer for this session. - * - * @return The node describing the TLS properties of this session. - */ -std::string -get_client_tls_description(TSHttpSsn ssnp) -{ - TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp); - return get_tls_description_helper(ssn_vc); -} - -/// A named boolean for callers who pass the is_client parameter. -constexpr bool IS_CLIENT = true; - -/** Create the nodes that describe the session's sub-HTTP protocols. - * - * This function encapsulates the logic common between the client-side and - * server-side logic for describing the session's characteristics. - * - * This will create the string representing the "protocol" and "tls" nodes. The - * "tls" node will only be present if the connection is over SSL/TLS. - * - * @param[in] ssnp The pointer for this session. - * - * @return The description of the protocol stack and certain TLS attributes. - */ -std::string -get_protocol_description_helper(TSHttpSsn ssnp, bool is_client) -{ - std::ostringstream protocol_description; - protocol_description << R"("protocol":[)"; - - char const *protocol[10]; - int count = -1; - if (is_client) { - TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count)); - } else { - // See the TODO below in the commented out defintion of get_server_protocol_description. - // TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count)); - } - for (int i = 0; i < count; i++) { - if (i > 0) { - protocol_description << ","; - } - protocol_description << '"' << std::string(protocol[i]) << '"'; - } - - protocol_description << "]"; - std::string tls_description; - if (is_client) { - tls_description = get_client_tls_description(ssnp); - } else { - tls_description = get_server_tls_description(ssnp); - } - if (!tls_description.empty()) { - protocol_description << "," << tls_description; - } - return protocol_description.str(); -} - -#if 0 -// See the TODO above the get_server_protocol_description declaration. -// -// It will be important to add this eventually, but -// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other -// mechanism for getting the server side stack) is implemented, we will call -// this as a part of writing the server-response node. -std::string -SessionData::get_server_protocol_description(TSHttpSsn ssnp) -{ - return get_protocol_description_helper(ssnp, !IS_CLIENT); -} -#endif - -std::string -SessionData::get_client_protocol_description(TSHttpSsn ssnp) -{ - return get_protocol_description_helper(ssnp, IS_CLIENT); + return get_protocol_stack_helper( + [&server_txnp](int n, const char **result, int *actual) { + return TSHttpTxnServerProtocolStackGet(server_txnp, n, result, actual); + }, + [&server_txnp]() { return get_server_tls_description(server_txnp); }); } SessionData::SessionData() @@ -295,7 +334,6 @@ SessionData::write_transaction_to_disk(std::string_view content) result = write_to_disk_no_lock(content); has_written_first_transaction = true; } - return result; } @@ -392,8 +430,8 @@ SessionData::global_session_handler(TSCont contp, TSEvent event, void *edata) TSContDataSet(ssnData->aio_cont, ssnData); - // "protocol":(string),"tls":(string) - // The "tls" node will only be present if the session is over SSL/TLS. + // "protocol": + // This is the protocol stack for the client side of the session. std::string protocol_description = get_client_protocol_description(ssnp); std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" + diff --git a/plugins/experimental/traffic_dump/session_data.h b/plugins/experimental/traffic_dump/session_data.h index 7551501e933..31bc9d3bdd5 100644 --- a/plugins/experimental/traffic_dump/session_data.h +++ b/plugins/experimental/traffic_dump/session_data.h @@ -124,19 +124,19 @@ class SessionData */ static void set_max_disk_usage(int64_t new_max_disk_usage); -#if 0 - // TODO: This will eventually be used by TransactionData to dump - // the server protocol description in the "server-response" node, - // but the TS API does not yet support this. - /** Get the JSON string that describes the server session stack. * - * @param[in] ssnp The reference to the server session. + * The server side protocol description may change on a per-transaction + * basis. Therefore we print this for each transaction and take an TSHttpTxn + * instead of a TSHttpSsn that the analogous get_client_protocol_description + * receives. + * + * @param[in] txnp The reference to the transaction. * - * @return A JSON description of the server protocol stack. + * @return A JSON description of the server protocol stack for the + * transaction. */ - static std::string get_server_protocol_description(TSHttpSsn ssnp); -#endif + static std::string get_server_protocol_description(TSHttpTxn txnp); /** Write the string to the session's dump file. * diff --git a/plugins/experimental/traffic_dump/transaction_data.cc b/plugins/experimental/traffic_dump/transaction_data.cc index b87a8c60f67..5e952294306 100644 --- a/plugins/experimental/traffic_dump/transaction_data.cc +++ b/plugins/experimental/traffic_dump/transaction_data.cc @@ -113,10 +113,10 @@ TransactionData::write_content_node(int64_t num_body_bytes) std::string TransactionData::write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc) { - std::string result = "{"; - int len = 0; - char const *cp = nullptr; - TSMLoc url_loc = nullptr; + std::string result; + int len = 0; + char const *cp = nullptr; + TSMLoc url_loc = nullptr; // 1. "version" // Note that we print this for both requests and responses, so the first @@ -228,6 +228,7 @@ TransactionData::init_helper() // processed before session and transaction ones.) TSCont txn_cont = TSContCreate(global_transaction_handler, nullptr); TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txn_cont); + TSHttpHookAdd(TS_HTTP_READ_RESPONSE_HDR_HOOK, txn_cont); return true; } @@ -287,7 +288,7 @@ TransactionData::global_transaction_handler(TSCont contp, TSEvent event, void *e } // This hook is registered globally, not at TS_EVENT_HTTP_SSN_START in // global_session_handler(). As such, this handler will be called with every - // transaction. However, we know that we are dumping this transaction + // transaction. However, we infer that we are dumping this transaction // because there is a ssnData associated with it. // We must grab the client request information before remap happens because @@ -298,13 +299,23 @@ TransactionData::global_transaction_handler(TSCont contp, TSEvent event, void *e TSDebug(debug_tag, "Found client request"); // We don't have an accurate view of the body size until TXN_CLOSE so we hold // off on writing the content:size node until then. - txnData->txn_json += R"(,"client-request":)" + txnData->write_message_node_no_content(buffer, hdr_loc); + txnData->txn_json += R"(,"client-request":{)" + txnData->write_message_node_no_content(buffer, hdr_loc); TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc); buffer = nullptr; } break; } + case TS_EVENT_HTTP_READ_RESPONSE_HDR: { + TransactionData *txnData = static_cast(TSUserArgGet(txnp, transaction_arg_index)); + if (!txnData) { + TSError("[%s] No transaction data found for the header hook we registered for.", traffic_dump::debug_tag); + break; + } + txnData->server_protocol_description = ssnData->get_server_protocol_description(txnp); + break; + } + case TS_EVENT_HTTP_TXN_CLOSE: { TransactionData *txnData = static_cast(TSUserArgGet(txnp, SessionData::get_session_arg_index())); if (!txnData) { @@ -321,22 +332,22 @@ TransactionData::global_transaction_handler(TSCont contp, TSEvent event, void *e } if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &buffer, &hdr_loc)) { TSDebug(debug_tag, "Found proxy request"); - txnData->txn_json += - R"(,"proxy-request":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp)); + txnData->txn_json += R"(,"proxy-request":{)" + txnData->server_protocol_description + "," + + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp)); TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc); buffer = nullptr; } if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &buffer, &hdr_loc)) { TSDebug(debug_tag, "Found server response"); txnData->txn_json += - R"(,"server-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp)); + R"(,"server-response":{)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp)); TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc); buffer = nullptr; } if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &buffer, &hdr_loc)) { TSDebug(debug_tag, "Found proxy response"); txnData->txn_json += - R"(,"proxy-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp)); + R"(,"proxy-response":{)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp)); TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc); buffer = nullptr; } diff --git a/plugins/experimental/traffic_dump/transaction_data.h b/plugins/experimental/traffic_dump/transaction_data.h index c5b5139119a..3541d7a275a 100644 --- a/plugins/experimental/traffic_dump/transaction_data.h +++ b/plugins/experimental/traffic_dump/transaction_data.h @@ -37,6 +37,9 @@ class TransactionData /** The string for the JSON content of this transaction. */ std::string txn_json; + /** The '"protocol" node for this transaction's server-side conection. */ + std::string server_protocol_description; + // The index to be used for the TS API for storing this TransactionData on a // per-transaction basis. static int transaction_arg_index; diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc index 143527a9a4d..c4d68d77c89 100644 --- a/proxy/http/HttpSM.cc +++ b/proxy/http/HttpSM.cc @@ -8128,7 +8128,7 @@ HttpSM::is_redirect_required() return redirect_required; } -// Fill in the client protocols used. Return the number of entries returned +// Fill in the client protocols used. Return the number of entries populated. int HttpSM::populate_client_protocol(std::string_view *result, int n) const { @@ -8145,7 +8145,7 @@ HttpSM::populate_client_protocol(std::string_view *result, int n) const return retval; } -// Look for a specific protocol +// Look for a specific client protocol const char * HttpSM::client_protocol_contains(std::string_view tag_prefix) const { @@ -8162,10 +8162,51 @@ HttpSM::client_protocol_contains(std::string_view tag_prefix) const return retval; } +// Fill in the server protocols used. Return the number of entries populated. +int +HttpSM::populate_server_protocol(std::string_view *result, int n) const +{ + int retval = 0; + if (!t_state.hdr_info.server_request.valid()) { + return retval; + } + if (n > 0) { + std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.server_request.version_get()); + if (!proto.empty()) { + result[retval++] = proto; + if (n > retval && server_session) { + retval += server_session->populate_protocol(result + retval, n - retval); + } + } + } + return retval; +} + +// Look for a specific server protocol +const char * +HttpSM::server_protocol_contains(std::string_view tag_prefix) const +{ + const char *retval = nullptr; + std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.server_request.version_get()); + if (!proto.empty()) { + std::string_view prefix(tag_prefix); + if (prefix.size() <= proto.size() && 0 == strncmp(proto.data(), prefix.data(), prefix.size())) { + retval = proto.data(); + } else { + if (server_session) { + retval = server_session->protocol_contains(prefix); + } + } + } + return retval; +} + std::string_view HttpSM::find_proto_string(HTTPVersion version) const { - if (version == HTTPVersion(1, 1)) { + if (version == HTTPVersion(2, 0)) { + return IP_PROTO_TAG_HTTP_2_0; + } else if (version == HTTPVersion(1, 1)) { return IP_PROTO_TAG_HTTP_1_1; } else if (version == HTTPVersion(1, 0)) { return IP_PROTO_TAG_HTTP_1_0; diff --git a/proxy/http/HttpSM.h b/proxy/http/HttpSM.h index befb4e5d828..627d30a6c52 100644 --- a/proxy/http/HttpSM.h +++ b/proxy/http/HttpSM.h @@ -303,6 +303,13 @@ class HttpSM : public Continuation, public PluginUserArgs /// @arg n [in] Size of the array @a result. int populate_client_protocol(std::string_view *result, int n) const; const char *client_protocol_contains(std::string_view tag_prefix) const; + + /// Get the protocol stack for the outbound (origin server) connection. + /// @arg result [out] Array to store the results + /// @arg n [in] Size of the array @a result. + int populate_server_protocol(std::string_view *result, int n) const; + const char *server_protocol_contains(std::string_view tag_prefix) const; + std::string_view find_proto_string(HTTPVersion version) const; int64_t sm_id = -1; diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc index 9861ff0c589..eae2cf878e4 100644 --- a/src/traffic_server/InkAPI.cc +++ b/src/traffic_server/InkAPI.cc @@ -4833,8 +4833,27 @@ TSHttpSsnClientVConnGet(TSHttpSsn ssnp) TSVConn TSHttpSsnServerVConnGet(TSHttpSsn ssnp) { + TSVConn vconn = nullptr; Http1ServerSession *ss = reinterpret_cast(ssnp); - return reinterpret_cast(ss->get_netvc()); + if (ss != nullptr) { + vconn = reinterpret_cast(ss->get_netvc()); + } + return vconn; +} + +TSVConn +TSHttpTxnServerVConnGet(TSHttpTxn txnp) +{ + TSVConn vconn = nullptr; + sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS); + HttpSM *sm = reinterpret_cast(txnp); + if (sm != nullptr) { + Http1ServerSession *ss = sm->get_server_session(); + if (ss != nullptr) { + vconn = reinterpret_cast(ss->get_netvc()); + } + } + return vconn; } class TSHttpSsnCallback : public Continuation @@ -9426,6 +9445,13 @@ TSVConnIsSsl(TSVConn sslp) return ssl_vc != nullptr; } +tsapi int +TSVConnProvidedSslCert(TSVConn sslp) +{ + NetVConnection *vc = reinterpret_cast(sslp); + return vc->provided_cert(); +} + void TSVConnReenable(TSVConn vconn) { @@ -9625,7 +9651,7 @@ int64_t TSHttpSsnIdGet(TSHttpSsn ssnp) { sdk_assert(sdk_sanity_check_http_ssn(ssnp) == TS_SUCCESS); - ProxySession *cs = reinterpret_cast(ssnp); + ProxySession const *cs = reinterpret_cast(ssnp); return cs->connection_id(); } @@ -9655,8 +9681,8 @@ TSHttpSsnClientProtocolStackGet(TSHttpSsn ssnp, int n, const char **result, int { sdk_assert(sdk_sanity_check_http_ssn(ssnp) == TS_SUCCESS); sdk_assert(n == 0 || result != nullptr); - ProxySession *cs = reinterpret_cast(ssnp); - int count = 0; + auto const *cs = reinterpret_cast(ssnp); + int count = 0; if (cs && n > 0) { auto mem = static_cast(alloca(sizeof(std::string_view) * n)); count = cs->populate_protocol(mem, n); @@ -9670,6 +9696,27 @@ TSHttpSsnClientProtocolStackGet(TSHttpSsn ssnp, int n, const char **result, int return TS_SUCCESS; } +// Return information about the protocols used by the server +TSReturnCode +TSHttpTxnServerProtocolStackGet(TSHttpTxn txnp, int n, const char **result, int *actual) +{ + sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS); + sdk_assert(n == 0 || result != nullptr); + HttpSM *sm = reinterpret_cast(txnp); + int count = 0; + if (sm && n > 0) { + auto mem = static_cast(alloca(sizeof(std::string_view) * n)); + count = sm->populate_server_protocol(mem, n); + for (int i = 0; i < count; ++i) { + result[i] = mem[i].data(); + } + } + if (actual) { + *actual = count; + } + return TS_SUCCESS; +} + const char * TSNormalizedProtocolTag(const char *tag) { @@ -9680,7 +9727,7 @@ const char * TSHttpTxnClientProtocolStackContains(TSHttpTxn txnp, const char *tag) { sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS); - HttpSM *sm = (HttpSM *)txnp; + HttpSM *sm = reinterpret_cast(txnp); return sm->client_protocol_contains(std::string_view{tag}); } @@ -9692,6 +9739,14 @@ TSHttpSsnClientProtocolStackContains(TSHttpSsn ssnp, const char *tag) return cs->protocol_contains(std::string_view{tag}); } +const char * +TSHttpTxnServerProtocolStackContains(TSHttpTxn txnp, const char *tag) +{ + sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS); + HttpSM *sm = reinterpret_cast(txnp); + return sm->server_protocol_contains(std::string_view{tag}); +} + const char * TSRegisterProtocolTag(const char *tag) { diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold index 1fe8660bbd7..b853f944735 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold +++ b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold @@ -1,6 +1,6 @@ `` > GET /`` HTTP/1.1 -> Host: www.example.com`` +> Host: www.notls.com`` > User-Agent: curl/`` > Accept: */* `` diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold index 40e21cb7abf..d56cb3626d4 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold +++ b/tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold @@ -1,6 +1,6 @@ `` > GET http://localhost:``/candy HTTP/1.1 -> Host: www.example.com`` +> Host: www.notls.com`` > User-Agent: curl/`` > Accept: */* `` diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold index 1dad3b38c75..904f2552047 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold +++ b/tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold @@ -1,6 +1,6 @@ `` > POST http://localhost:``/post_with_body HTTP/1.1 -> Host: www.example.com`` +> Host: www.notls.com`` > User-Agent: curl/`` > Accept: */* `` diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold index 9ac048cb49e..842ba2ea3bf 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold +++ b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold @@ -1,11 +1,11 @@ `` > GET /first HTTP/1.1 -> Host: www.example.com +> Host: www.notls.com `` < HTTP/1.1 200 OK `` > GET /second HTTP/1.1 -> Host: www.example.com +> Host: www.notls.com `` < HTTP/1.1 200 OK `` diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py index dc8ae25e0b5..60431b2be9b 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py +++ b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py @@ -27,10 +27,10 @@ ) # Configure the origin server. -server = Test.MakeOriginServer("server") +server = Test.MakeOriginServer("server", both=True) request_header = {"headers": "GET /one HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} response_header = {"headers": "HTTP/1.1 200 OK" "\r\nConnection: close\r\nContent-Length: 0" @@ -38,7 +38,7 @@ "timestamp": "1469733493.993", "body": ""} server.addResponse("sessionfile.log", request_header, response_header) request_header = {"headers": "GET /two HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} response_header = {"headers": "HTTP/1.1 200 OK" "\r\nConnection: close\r\nContent-Length: 0" @@ -46,7 +46,7 @@ "timestamp": "1469733493.993", "body": ""} server.addResponse("sessionfile.log", request_header, response_header) request_header = {"headers": "GET /three HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} response_header = {"headers": "HTTP/1.1 200 OK" "\r\nConnection: close\r\nContent-Length: 0" @@ -54,14 +54,14 @@ "timestamp": "1469733493.993", "body": ""} server.addResponse("sessionfile.log", request_header, response_header) request_header = {"headers": "GET /post_with_body HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK" - "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) +response_200 = {"headers": "HTTP/1.1 200 OK" + "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionfile.log", request_header, response_200) request_header = {"headers": "GET /cache_test HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} response_header = {"headers": "HTTP/1.1 200 OK" "\r\nConnection: close\r\nCache-Control: max-age=300\r\n" @@ -69,31 +69,66 @@ "timestamp": "1469733493.993", "body": "1234"} server.addResponse("sessionfile.log", request_header, response_header) request_header = {"headers": "GET /first HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK" - "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) +server.addResponse("sessionfile.log", request_header, response_200) request_header = {"headers": "GET /second HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: www.notls.com\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK" - "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) +server.addResponse("sessionfile.log", request_header, response_200) +request_header = {"headers": "GET /tls HTTP/1.1\r\n" + "Host: www.tls.com\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionfile.log", request_header, response_200) +request_header = {"headers": "GET /h2 HTTP/1.1\r\n" + "Host: www.tls.com\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionfile.log", request_header, response_200) +request_header = {"headers": "GET /client_only_tls HTTP/1.1\r\n" + "Host: www.client_only_tls.com\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionfile.log", request_header, response_200) # Define ATS and configure it. -ts = Test.MakeATSProcess("ts") +ts = Test.MakeATSProcess("ts", enable_tls=True) replay_dir = os.path.join(ts.RunDirectory, "ts", "log") + +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") +ts.addSSLfile("ssl/signer.pem") + +ts.Setup.Copy("ssl/signed-foo.pem") +ts.Setup.Copy("ssl/signed-foo.key") + ts.Disk.records_config.update({ 'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'traffic_dump', 'proxy.config.http.insert_age_in_response': 0, + + '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.client.verify.server': 0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.TLSv1_3': 0, }) + +ts.Disk.ssl_multicert_config.AddLine( + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +) + +ts.Disk.remap_config.AddLine( + 'map https://www.client_only_tls.com/ http://127.0.0.1:{0}'.format(server.Variables.Port) +) +ts.Disk.remap_config.AddLine( + 'map https://www.tls.com/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port) +) ts.Disk.remap_config.AddLine( 'map / http://127.0.0.1:{0}'.format(server.Variables.Port) ) + # Configure traffic_dump. ts.Disk.plugin_config.AddLine( 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 ' @@ -140,6 +175,12 @@ ts.Disk.File(replay_file_session_6, exists=True) replay_file_session_7 = os.path.join(replay_dir, "127", "0000000000000006") ts.Disk.File(replay_file_session_7, exists=True) +replay_file_session_8 = os.path.join(replay_dir, "127", "0000000000000007") +ts.Disk.File(replay_file_session_8, exists=True) +replay_file_session_9 = os.path.join(replay_dir, "127", "0000000000000008") +ts.Disk.File(replay_file_session_9, exists=True) +replay_file_session_10 = os.path.join(replay_dir, "127", "0000000000000009") +ts.Disk.File(replay_file_session_10, exists=True) # # Test 1: Verify the correct behavior of two transactions across two sessions. @@ -152,18 +193,18 @@ tr.Processes.Default.StartBefore(Test.Processes.ts) tr.Processes.Default.Command = \ ('curl --http1.1 http://127.0.0.1:{0}/one -H"Cookie: donotlogthis" ' - '-H"Host: www.example.com" -H"X-Request-1: ultra_sensitive" --verbose'.format( + '-H"Host: www.notls.com" -H"X-Request-1: ultra_sensitive" --verbose'.format( ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/200.gold" tr.StillRunningAfter = server tr.StillRunningAfter = ts -http_protocols = "tcp,ipv4" +http_protocols = "tcp,ip" # Execute the second transaction. tr = Test.AddTestRun("Second transaction") tr.Processes.Default.Command = \ - ('curl http://127.0.0.1:{0}/two -H"Host: www.example.com" ' + ('curl http://127.0.0.1:{0}/two -H"Host: www.notls.com" ' '-H"X-Request-2: also_very_sensitive" --verbose'.format( ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 @@ -211,7 +252,7 @@ request_target = "http://localhost:{0}/candy".format(ts.Variables.port) tr.Processes.Default.Command = ( 'curl --request-target "{0}" ' - 'http://127.0.0.1:{1}/three -H"Host: www.example.com" --verbose'.format( + 'http://127.0.0.1:{1}/three -H"Host: www.notls.com" --verbose'.format( request_target, ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/explicit_target.gold" @@ -242,7 +283,7 @@ # in the test run directory. tr.Processes.Default.Command = ( 'curl --data-binary @{0} --request-target "{1}" ' - 'http://127.0.0.1:{2} -H"Host: www.example.com" --verbose'.format( + 'http://127.0.0.1:{2} -H"Host: www.notls.com" --verbose'.format( verify_replay, request_target, ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/post_with_body.gold" @@ -269,7 +310,7 @@ # tr = Test.AddTestRun("Make a request for an uncached object.") tr.Processes.Default.Command = \ - ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format( + ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.notls.com" --verbose'.format( ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold" @@ -278,7 +319,7 @@ tr = Test.AddTestRun("Repeat the previous request: should be cached now.") tr.Processes.Default.Command = \ - ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format( + ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.notls.com" --verbose'.format( ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold" @@ -301,8 +342,8 @@ # tr = Test.AddTestRun("Conduct two transactions in the same session.") tr.Processes.Default.Command = \ - ('curl --http1.1 http://127.0.0.1:{0}/first -H"Host: www.example.com" --verbose --next ' - 'curl --http1.1 http://127.0.0.1:{0}/second -H"Host: www.example.com" --verbose' + ('curl --http1.1 http://127.0.0.1:{0}/first -H"Host: www.notls.com" --verbose --next ' + 'curl --http1.1 http://127.0.0.1:{0}/second -H"Host: www.notls.com" --verbose' .format(ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/two_transactions.gold" @@ -319,3 +360,120 @@ tr.Processes.Default.ReturnCode = 0 tr.StillRunningAfter = server tr.StillRunningAfter = ts + +# +# Test 6: Verify correct protcol dumping of a TLS connection. +# +tr = Test.AddTestRun("Perform an HTTP/1 transaction over a TLS connection.") +tr.Processes.Default.Command = \ + ('curl --http1.1 -k -H"Host: www.tls.com" --resolve "www.tls.com:{0}:127.0.0.1" ' + '--cert ./signed-foo.pem --key ./signed-foo.key --verbose https://www.tls.com:{0}/tls'.format( + ts.Variables.ssl_port)) + +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the client protocol stack.") +https_protocols = "tls,tcp,ip" +client_tls_features = "sni:www.tls.com,proxy-verify-mode:0,proxy-provided-cert:true" +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}" --client-tls-features "{4}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_8, + https_protocols, + client_tls_features) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the server protocol stack.") +https_server_stack = "http,tls,tcp,ip" +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +server_tls_features = 'proxy-provided-cert:false,sni:www.tls.com,proxy-verify-mode:1' +tr.Processes.Default.Command = 'python3 {0} {1} {2} --server-protocols "{3}" --server-tls-features "{4}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_8, + https_server_stack, + server_tls_features) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# +# Test 7: Verify correct protcol dumping of TLS and HTTP/2 connections. +# +tr = Test.AddTestRun("Conduct an HTTP/2 transaction over a TLS connection.") +tr.Processes.Default.Command = \ + ('curl --http2 -k -H"Host: www.tls.com" --resolve "www.tls.com:{0}:127.0.0.1" ' + '--cert ./signed-foo.pem --key ./signed-foo.key --verbose https://www.tls.com:{0}/h2'.format( + ts.Variables.ssl_port)) + +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the client protocol stack.") +h2_protocols = "http,tls,tcp,ip" +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}" --client-tls-features "{4}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_9, + h2_protocols, + client_tls_features) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the server protocol stack.") +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +tr.Processes.Default.Command = 'python3 {0} {1} {2} --server-protocols "{3}" --server-tls-features "{4}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_9, + https_server_stack, + server_tls_features) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# +# Test 8: Verify correct protcol dumping of client-side TLS and server-side HTTP. +# +tr = Test.AddTestRun("Conduct a client-side TLS connection with an HTTP server-side connection.") +tr.Processes.Default.Command = \ + ('curl --http1.1 -k -H"Host: www.client_only_tls.com" ' + '--resolve "www.client_only_tls.com:{0}:127.0.0.1" ' + '--cert ./signed-foo.pem --key ./signed-foo.key ' + '--verbose https://www.client_only_tls.com:{0}/client_only_tls'.format( + ts.Variables.ssl_port)) + +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the client protocol stack.") +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_10, + https_protocols) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Verify the server protocol stack.") +tr.Setup.CopyAs(verify_replay, Test.RunDirectory) +http_server_stack = "http,tcp,ip" +tr.Processes.Default.Command = 'python3 {0} {1} {2} --server-protocols "{3}"'.format( + verify_replay, + os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'), + replay_file_session_10, + http_server_stack) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py index 4e269f20dd2..c38789e4593 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py +++ b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py @@ -30,7 +30,7 @@ server = Test.MakeOriginServer("server") request_header = {"headers": "GET / HTTP/1.1\r\n" - "Host: www.example.com\r\nContent-Length: 0\r\n\r\n", + "Host: bob\r\nContent-Length: 0\r\n\r\n", "timestamp": "1469733493.993", "body": ""} response_header = {"headers": "HTTP/1.1 200 OK" "\r\nConnection: close\r\nContent-Length: 0" @@ -46,6 +46,9 @@ ts.addSSLfile("ssl/server.key") ts.addSSLfile("ssl/signer.pem") +ts.Setup.Copy("ssl/signed-foo.pem") +ts.Setup.Copy("ssl/signed-foo.key") + ts.Disk.records_config.update({ 'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'traffic_dump', @@ -77,7 +80,7 @@ ' verify_client: STRICT', ]) -# Configure traffic_dump to filter to only dump with connections with SNI bob. +# Configure traffic_dump's SNI filter to only dump connections with SNI bob. sni_filter = "bob" ts.Disk.plugin_config.AddLine( 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 ' @@ -114,8 +117,6 @@ # Execute the first transaction with an SNI of bob. tr = Test.AddTestRun("Verify dumping of a session with the filtered SNI") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) tr.Processes.Default.StartBefore(Test.Processes.ts) tr.Processes.Default.Command = \ @@ -125,8 +126,10 @@ tr.Processes.Default.Streams.stderr = "gold/200_sni_bob.gold" tr.StillRunningAfter = server tr.StillRunningAfter = ts -session_1_protocols = "h2,tls/1.2,tcp,ipv4" -session_1_tls_features = 'sni:bob' +session_1_protocols = "http,tls,tcp,ip" +# Observe that the sni.yaml config dictates STRICT as the verify_client +# attribute. +session_1_tls_features = 'sni:bob,proxy-verify-mode:7,proxy-provided-cert:true' # Execute the second transaction with an SNI of dave. tr = Test.AddTestRun("Verify that a session of a different SNI is not dumped.") diff --git a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py index e48819ce119..cf45910c26c 100644 --- a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py +++ b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py @@ -139,39 +139,115 @@ def verify_sensitive_fields_not_dumped(replay_json, sensitive_fields): return True -def verify_client_protocols(replay_json, expected_protocol_features): - expected_protocols_list = expected_protocol_features.split(',') - expected_protocols_list.sort() +def verify_protocols_helper(replay_json, expected_protocols, is_client): try: - protocol_node = replay_json['sessions'][0]['protocol'] - protocol_list = protocol_node.copy() - protocol_list.sort() - if protocol_list == expected_protocols_list: + if is_client: + protocol_node = replay_json['sessions'][0]['protocol'] + else: + protocol_node = replay_json['sessions'][0]['transactions'][0]['proxy-request']['protocol'] + + dumped_protocols = '' + is_first_protocol = True + for protocol in protocol_node: + if not is_first_protocol: + dumped_protocols += "," + is_first_protocol = False + dumped_protocols += protocol['name'] + + if dumped_protocols == expected_protocols: return True else: - print('Unexpected protocol stack. Expected: "{}", found: "{}".'.format( - ','.join(expected_protocols_list), ','.join(protocol_list))) + host_name = "client" if is_client else "server" + print('Unexpected protocol {} stack. Expected: "{}", found: "{}".'.format( + host_name, expected_protocols, dumped_protocols)) return False except KeyError: - print("Could not find client protocol stack node in the replay file.") + print("Could not find {} protocol stack node in the replay file.".format(host_name)) return False +def verify_client_protocols(replay_json, expected_protocols): + return verify_protocols_helper(replay_json, expected_protocols, is_client=True) + + +def verify_server_protocols(replay_json, expected_protocols): + return verify_protocols_helper(replay_json, expected_protocols, is_client=False) + + +def get_tls_features(replay_json, is_client): + session = replay_json['sessions'][0] + if is_client: + protocol_parent = session['protocol'] + else: + protocol_parent = session['transactions'][0]['proxy-request']['protocol'] + + for protocol in protocol_parent: + if protocol['name'] == "tls": + return protocol + else: + raise KeyError('The "tls" protocol node was not found.') + + +def verify_tls_features(expected_tls_features, found_tls_features): + for expected_tls_feature in expected_tls_features.split(','): + expected_key, expected_value = expected_tls_feature.split(':') + try: + found_value = found_tls_features[expected_key] + except KeyError: + print("Could not find tls feature in the replay file: {}".format(expected_key)) + return False + value_matches = False + if type(found_value) == str: + value_matches = found_value == expected_value + elif type(found_value) == int: + value_matches = found_value == int(expected_value) + elif type(found_value) == bool: + if expected_value.lower() == "false": + expected_value = False + elif expected_value.lower() == "true": + expected_value = True + else: + raise ValueError("Cannot convert expected value to a boolean: {}, found: {}".format( + expected_value, found_value)) + value_matches = found_value == bool(expected_value) + else: + raise ValueError("Cannot determine type of found value: {}".format( + found_value)) + + if not value_matches: + print('Mismatched value for "{}", expected "{}", received "{}"'.format( + expected_key, expected_value, found_value)) + return False + return True + + def verify_client_tls_features(replay_json, expected_tls_features): try: - session = replay_json['sessions'][0] - for expected_tls_feature in expected_tls_features.split(','): - expected_key, expected_value = expected_tls_feature.split(':') - tls_features = session['tls'] - try: - return tls_features[expected_key] == expected_value - except KeyError: - print("Could not find client tls feature in the replay file: {}".format(expected_key)) - return False + tls_features = get_tls_features(replay_json, is_client=True) except KeyError: print("Could not find client tls node in the replay file.") return False + if not verify_tls_features(expected_tls_features, tls_features): + print('Failed to verify client tls features in "{}"'.format( + expected_tls_features)) + return False + return True + + +def verify_server_tls_features(replay_json, expected_tls_features): + try: + tls_features = get_tls_features(replay_json, is_client=False) + except KeyError: + print("Could not find server tls node in the replay file.") + return False + + if not verify_tls_features(expected_tls_features, tls_features): + print('Failed to verify server tls features in "{}"'.format( + expected_tls_features)) + return False + return True + def parse_args(): parser = argparse.ArgumentParser() @@ -190,9 +266,13 @@ def parse_args(): action="append", help="The fields that are considered sensitive and replaced with insensitive values.") parser.add_argument("--client-protocols", - help="The comma-separated protocol features to expect for the client connection.") + help="The comma-separated sequence of protocols to expect for the client connection.") + parser.add_argument("--server-protocols", + help="The comma-separated sequence of protocols to expect for the server connection.") parser.add_argument("--client-tls-features", help="The TLS values to expect for the client connection.") + parser.add_argument("--server-tls-features", + help="The TLS values to expect for the server connection.") return parser.parse_args() @@ -229,9 +309,15 @@ def main(): if args.client_protocols and not verify_client_protocols(replay_json, args.client_protocols): return 1 + if args.server_protocols and not verify_server_protocols(replay_json, args.server_protocols): + return 1 + if args.client_tls_features and not verify_client_tls_features(replay_json, args.client_tls_features): return 1 + if args.server_tls_features and not verify_server_tls_features(replay_json, args.server_tls_features): + return 1 + return 0 diff --git a/tests/tools/lib/replay_schema.json b/tests/tools/lib/replay_schema.json index 35906f4516d..07708bde081 100644 --- a/tests/tools/lib/replay_schema.json +++ b/tests/tools/lib/replay_schema.json @@ -24,12 +24,8 @@ "required": [ "transactions" ], "properties": { "protocol": { - "description": "The network protocol stack of the inbound connection.", - "type": "array", - "items": { - "description": "Protocol tag", - "type": "string" - } + "description": "The network protocol description of the inbound connection.", + "$ref": "#/definitions/protocol" }, "connect-time": { "description": "User Agent connection time.", @@ -120,6 +116,41 @@ } ] }, + "protocol": { + "description": "The characteristics of the lower level protocols.", + "type": "array", + "required": ["name"], + "properties": { + "name": { + "desciption": "The name of the protocol.", + "type": "string" + }, + "version": { + "desciption": "The version of the protocol.", + "type": "string" + }, + "sni": { + "desciption": "The SNI in the client hello.", + "type": "string" + }, + "request-certificate": { + "desciption": "Whether a certificate should be requested from the proxy.", + "type": "boolean" + }, + "verify-mode": { + "desciption": "The server or client's OpenSSL verify mode against the proxy.", + "type": "boolean" + }, + "proxy-verify-mode": { + "desciption": "The proxy's OpenSSL verify mode against the peer.", + "type": "integer" + }, + "proxy-provided-certificate": { + "desciption": "Whether the proxy provided a cert in the handshake.", + "type": "boolean" + } + } + }, "header-fields": { "description": "HTTP header fields.", "type": "object", @@ -178,6 +209,10 @@ "headers": { "description": "HTTP header fields.", "$ref": "#/definitions/header-fields" + }, + "protocol": { + "description": "The network protocol description of the connection.", + "$ref": "#/definitions/protocol" } } },