diff --git a/CODEOWNERS b/CODEOWNERS index 971e9976a9e9..92f937184f73 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -192,3 +192,5 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp /*/extensions/http/original_ip_detection/xff @rgs1 @alyssawilk @antoniovicente # set_metadata extension /*/extensions/filters/http/set_metadata @aguinet @snowp +# Formatters +/*/extensions/formatter/req_without_query @dio @tsaarni diff --git a/api/BUILD b/api/BUILD index 179af01ca9ef..245695ad8707 100644 --- a/api/BUILD +++ b/api/BUILD @@ -245,6 +245,7 @@ proto_library( "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", + "//envoy/extensions/formatter/req_without_query/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", diff --git a/api/envoy/config/core/v3/substitution_format_string.proto b/api/envoy/config/core/v3/substitution_format_string.proto index 85eeabe66219..b2a1c5e13ee4 100644 --- a/api/envoy/config/core/v3/substitution_format_string.proto +++ b/api/envoy/config/core/v3/substitution_format_string.proto @@ -109,5 +109,6 @@ message SubstitutionFormatString { // Specifies a collection of Formatter plugins that can be called from the access log configuration. // See the formatters extensions documentation for details. + // [#extension-category: envoy.formatter] repeated TypedExtensionConfig formatters = 6; } diff --git a/api/envoy/config/core/v4alpha/substitution_format_string.proto b/api/envoy/config/core/v4alpha/substitution_format_string.proto index 36b25da75b8e..6f5037f5f177 100644 --- a/api/envoy/config/core/v4alpha/substitution_format_string.proto +++ b/api/envoy/config/core/v4alpha/substitution_format_string.proto @@ -96,5 +96,6 @@ message SubstitutionFormatString { // Specifies a collection of Formatter plugins that can be called from the access log configuration. // See the formatters extensions documentation for details. + // [#extension-category: envoy.formatter] repeated TypedExtensionConfig formatters = 6; } diff --git a/api/envoy/extensions/formatter/req_without_query/v3/BUILD b/api/envoy/extensions/formatter/req_without_query/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/extensions/formatter/req_without_query/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto b/api/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto new file mode 100644 index 000000000000..e1b6c32a97e6 --- /dev/null +++ b/api/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package envoy.extensions.formatter.req_without_query.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.formatter.req_without_query.v3"; +option java_outer_classname = "ReqWithoutQueryProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Formatter extension for printing request without query string] +// [#extension: envoy.formatter.req_without_query] + +// ReqWithoutQuery formatter extension implements REQ_WITHOUT_QUERY command operator that +// works the same way as :ref:`REQ ` except that it will +// remove the query string. It is used to avoid logging any sensitive information into +// the access log. +// See :ref:`here ` for more information on access log configuration. + +// %REQ_WITHOUT_QUERY(X?Y):Z% +// An HTTP request header where X is the main HTTP header, Y is the alternative one, and Z is an +// optional parameter denoting string truncation up to Z characters long. The value is taken from +// the HTTP request header named X first and if it's not set, then request header Y is used. If +// none of the headers are present '-' symbol will be in the log. + +// Configuration for the request without query formatter. +message ReqWithoutQuery { +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 867fe05efa53..8558d13ede40 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -129,6 +129,7 @@ proto_library( "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", + "//envoy/extensions/formatter/req_without_query/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", diff --git a/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index 16a562963760..ef3393e4816d 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -29,3 +29,4 @@ Extensions http/header_formatters http/original_ip_detection quic/quic_extensions + formatter/formatter diff --git a/docs/root/api-v3/config/formatter/formatter.rst b/docs/root/api-v3/config/formatter/formatter.rst new file mode 100644 index 000000000000..b240b2c89f10 --- /dev/null +++ b/docs/root/api-v3/config/formatter/formatter.rst @@ -0,0 +1,10 @@ +.. _api-v3_config_accesslog_formatters: + +Access log formatters +===================== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../extensions/formatter/*/v3/* diff --git a/docs/root/configuration/observability/access_log/usage.rst b/docs/root/configuration/observability/access_log/usage.rst index 6e56189b45e1..ef4e0ec9b010 100644 --- a/docs/root/configuration/observability/access_log/usage.rst +++ b/docs/root/configuration/observability/access_log/usage.rst @@ -417,6 +417,8 @@ The following command operators are supported: %DOWNSTREAM_LOCAL_PORT% Similar to **%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT%**, but only extracts the port portion of the **%DOWNSTREAM_LOCAL_ADDRESS%** +.. _config_access_log_format_req: + %REQ(X?Y):Z% HTTP An HTTP request header where X is the main HTTP header, Y is the alternative one, and Z is an diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 6d4be0e0f1d5..9b7666f1b23b 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -100,6 +100,7 @@ New Features * metric service: added support for sending metric tags as labels. This can be enabled by setting the :ref:`emit_tags_as_labels ` field to true. * proxy protocol: added support for generating the header while using the :ref:`HTTP connection manager `. This is done using the using the :ref:`Proxy Protocol Transport Socket ` on upstream clusters. This feature is currently affected by a memory leak `issue `_. +* req_without_query: added access log formatter extension implementing command operator :ref:`REQ_WITHOUT_QUERY ` to log the request path, while excluding the query string. * router: added flag ``suppress_grpc_request_failure_code_stats`` to :ref:`key ` to allow users to exclude incrementing HTTP status code stats on gRPC requests. * tcp: added support for :ref:`preconnecting `. Preconnecting is off by default, but recommended for clusters serving latency-sensitive traffic. * thrift_proxy: added per upstream metrics within the :ref:`thrift router ` for request and response size histograms. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 179af01ca9ef..245695ad8707 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -245,6 +245,7 @@ proto_library( "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", + "//envoy/extensions/formatter/req_without_query/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", diff --git a/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto b/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto index 85eeabe66219..b2a1c5e13ee4 100644 --- a/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto +++ b/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto @@ -109,5 +109,6 @@ message SubstitutionFormatString { // Specifies a collection of Formatter plugins that can be called from the access log configuration. // See the formatters extensions documentation for details. + // [#extension-category: envoy.formatter] repeated TypedExtensionConfig formatters = 6; } diff --git a/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto b/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto index c58e7dabf6b7..8bb1a9e53e56 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto @@ -113,5 +113,6 @@ message SubstitutionFormatString { // Specifies a collection of Formatter plugins that can be called from the access log configuration. // See the formatters extensions documentation for details. + // [#extension-category: envoy.formatter] repeated TypedExtensionConfig formatters = 6; } diff --git a/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/BUILD b/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto b/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto new file mode 100644 index 000000000000..e1b6c32a97e6 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/formatter/req_without_query/v3/req_without_query.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package envoy.extensions.formatter.req_without_query.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.formatter.req_without_query.v3"; +option java_outer_classname = "ReqWithoutQueryProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Formatter extension for printing request without query string] +// [#extension: envoy.formatter.req_without_query] + +// ReqWithoutQuery formatter extension implements REQ_WITHOUT_QUERY command operator that +// works the same way as :ref:`REQ ` except that it will +// remove the query string. It is used to avoid logging any sensitive information into +// the access log. +// See :ref:`here ` for more information on access log configuration. + +// %REQ_WITHOUT_QUERY(X?Y):Z% +// An HTTP request header where X is the main HTTP header, Y is the alternative one, and Z is an +// optional parameter denoting string truncation up to Z characters long. The value is taken from +// the HTTP request header named X first and if it's not set, then request header Y is used. If +// none of the headers are present '-' symbol will be in the log. + +// Configuration for the request without query formatter. +message ReqWithoutQuery { +} diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 58ac1b6015f5..b8b3d9f0e1b1 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -421,6 +421,13 @@ absl::string_view Utility::findQueryStringStart(const HeaderString& path) { return path_str; } +std::string Utility::stripQueryString(const HeaderString& path) { + absl::string_view path_str = path.getStringView(); + size_t query_offset = path_str.find('?'); + return std::string(path_str.data(), + query_offset != path_str.npos ? query_offset : path_str.size()); +} + std::string Utility::parseCookieValue(const HeaderMap& headers, const std::string& key) { return parseCookie(headers, key, Http::Headers::get().Cookie.get()); } diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 0cf5f612eb36..a035fcafafd9 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -240,6 +240,13 @@ QueryParams parseParameters(absl::string_view data, size_t start, bool decode_pa */ absl::string_view findQueryStringStart(const HeaderString& path); +/** + * Returns the path without the query string. + * @param path supplies a HeaderString& possibly containing a query string. + * @return std::string the path without query string. + */ +std::string stripQueryString(const HeaderString& path); + /** * Parse a particular value out of a cookie * @param headers supplies the headers to get the cookie from. diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 67c9d263f00c..791a441012ab 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -287,6 +287,11 @@ EXTENSIONS = { "envoy.quic.crypto_stream.server.quiche": "//source/extensions/quic/crypto_stream:envoy_quic_default_crypto_server_stream", "envoy.quic.proof_source.filter_chain": "//source/extensions/quic/proof_source:envoy_quic_default_proof_source", + # + # Formatter + # + + "envoy.formatter.req_without_query": "//source/extensions/formatter/req_without_query:config", } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 2500e09a957d..04ddcf535545 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -428,6 +428,11 @@ envoy.filters.udp_listener.udp_proxy: - envoy.filters.udp_listener security_posture: robust_to_untrusted_downstream status: stable +envoy.formatter.req_without_query: + categories: + - envoy.formatter + security_posture: robust_to_untrusted_downstream_and_upstream + status: alpha envoy.grpc_credentials.aws_iam: categories: - envoy.grpc_credentials diff --git a/source/extensions/formatter/req_without_query/BUILD b/source/extensions/formatter/req_without_query/BUILD new file mode 100644 index 000000000000..45c8ee111559 --- /dev/null +++ b/source/extensions/formatter/req_without_query/BUILD @@ -0,0 +1,34 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +# Access log formatter that strips query string from request path +# Public docs: docs/root/TODO(tsaarni) + +envoy_cc_library( + name = "req_without_query_lib", + srcs = ["req_without_query.cc"], + hdrs = ["req_without_query.h"], + deps = [ + "//source/common/formatter:substitution_formatter_lib", + "//source/common/protobuf:utility_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + "//envoy/registry", + "//source/extensions/formatter/req_without_query:req_without_query_lib", + "@envoy_api//envoy/extensions/formatter/req_without_query/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/formatter/req_without_query/config.cc b/source/extensions/formatter/req_without_query/config.cc new file mode 100644 index 000000000000..76035de46951 --- /dev/null +++ b/source/extensions/formatter/req_without_query/config.cc @@ -0,0 +1,26 @@ +#include "source/extensions/formatter/req_without_query/config.h" + +#include "envoy/extensions/formatter/req_without_query/v3/req_without_query.pb.h" + +#include "source/extensions/formatter/req_without_query/req_without_query.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +::Envoy::Formatter::CommandParserPtr +ReqWithoutQueryFactory::createCommandParserFromProto(const Protobuf::Message&) { + return std::make_unique(); +} + +ProtobufTypes::MessagePtr ReqWithoutQueryFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +std::string ReqWithoutQueryFactory::name() const { return "envoy.formatter.req_without_query"; } + +REGISTER_FACTORY(ReqWithoutQueryFactory, ReqWithoutQueryFactory::CommandParserFactory); + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/formatter/req_without_query/config.h b/source/extensions/formatter/req_without_query/config.h new file mode 100644 index 000000000000..71b33f3904b0 --- /dev/null +++ b/source/extensions/formatter/req_without_query/config.h @@ -0,0 +1,19 @@ +#pragma once + +#include "source/common/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +class ReqWithoutQueryFactory : public ::Envoy::Formatter::CommandParserFactory { +public: + ::Envoy::Formatter::CommandParserPtr + createCommandParserFromProto(const Protobuf::Message&) override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + std::string name() const override; +}; + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/formatter/req_without_query/req_without_query.cc b/source/extensions/formatter/req_without_query/req_without_query.cc new file mode 100644 index 000000000000..d0fb6580b442 --- /dev/null +++ b/source/extensions/formatter/req_without_query/req_without_query.cc @@ -0,0 +1,88 @@ +#include "source/extensions/formatter/req_without_query/req_without_query.h" + +#include + +#include "source/common/http/utility.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +namespace { + +void truncate(std::string& str, absl::optional max_length) { + if (!max_length) { + return; + } + + str = str.substr(0, max_length.value()); +} + +} // namespace + +ReqWithoutQuery::ReqWithoutQuery(const std::string& main_header, + const std::string& alternative_header, + absl::optional max_length) + : main_header_(main_header), alternative_header_(alternative_header), max_length_(max_length) {} + +absl::optional ReqWithoutQuery::format(const Http::RequestHeaderMap& request, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + const Http::HeaderEntry* header = findHeader(request); + if (!header) { + return absl::nullopt; + } + + std::string val = Http::Utility::stripQueryString(header->value()); + truncate(val, max_length_); + + return val; +} + +ProtobufWkt::Value ReqWithoutQuery::formatValue(const Http::RequestHeaderMap& request, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + const Http::HeaderEntry* header = findHeader(request); + if (!header) { + return ValueUtil::nullValue(); + } + + std::string val = Http::Utility::stripQueryString(header->value()); + truncate(val, max_length_); + return ValueUtil::stringValue(val); +} + +const Http::HeaderEntry* ReqWithoutQuery::findHeader(const Http::HeaderMap& headers) const { + const auto header = headers.get(main_header_); + + if (header.empty() && !alternative_header_.get().empty()) { + const auto alternate_header = headers.get(alternative_header_); + // TODO(https://github.com/envoyproxy/envoy/issues/13454): Potentially log all header values. + return alternate_header.empty() ? nullptr : alternate_header[0]; + } + + return header.empty() ? nullptr : header[0]; +} + +::Envoy::Formatter::FormatterProviderPtr +ReqWithoutQueryCommandParser::parse(const std::string& token, size_t, size_t) const { + if (absl::StartsWith(token, "REQ_WITHOUT_QUERY(")) { + std::string main_header, alternative_header; + absl::optional max_length; + + Envoy::Formatter::SubstitutionFormatParser::parseCommandHeader( + token, ReqWithoutQueryParamStart, main_header, alternative_header, max_length); + return std::make_unique(main_header, alternative_header, max_length); + } + + return nullptr; +} + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/formatter/req_without_query/req_without_query.h b/source/extensions/formatter/req_without_query/req_without_query.h new file mode 100644 index 000000000000..4fd000351a64 --- /dev/null +++ b/source/extensions/formatter/req_without_query/req_without_query.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "envoy/config/typed_config.h" +#include "envoy/registry/registry.h" + +#include "source/common/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +class ReqWithoutQuery : public ::Envoy::Formatter::FormatterProvider { +public: + ReqWithoutQuery(const std::string& main_header, const std::string& alternative_header, + absl::optional max_length); + + absl::optional format(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; + ProtobufWkt::Value formatValue(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; + +private: + const Http::HeaderEntry* findHeader(const Http::HeaderMap& headers) const; + + Http::LowerCaseString main_header_; + Http::LowerCaseString alternative_header_; + absl::optional max_length_; +}; + +class ReqWithoutQueryCommandParser : public ::Envoy::Formatter::CommandParser { +public: + ReqWithoutQueryCommandParser() = default; + ::Envoy::Formatter::FormatterProviderPtr parse(const std::string& token, size_t, + size_t) const override; + +private: + static const size_t ReqWithoutQueryParamStart{sizeof("REQ_WITHOUT_QUERY(") - 1}; +}; + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/formatter/req_without_query/BUILD b/test/extensions/formatter/req_without_query/BUILD new file mode 100644 index 000000000000..cc480a370334 --- /dev/null +++ b/test/extensions/formatter/req_without_query/BUILD @@ -0,0 +1,28 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "req_without_query_test", + srcs = ["req_without_query_test.cc"], + extension_name = "envoy.formatter.req_without_query", + deps = [ + "//source/common/formatter:substitution_formatter_lib", + "//source/common/json:json_loader_lib", + "//source/extensions/formatter/req_without_query:config", + "//source/extensions/formatter/req_without_query:req_without_query_lib", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/formatter/req_without_query/req_without_query_test.cc b/test/extensions/formatter/req_without_query/req_without_query_test.cc new file mode 100644 index 000000000000..e4a1695625be --- /dev/null +++ b/test/extensions/formatter/req_without_query/req_without_query_test.cc @@ -0,0 +1,168 @@ +#include "envoy/config/core/v3/substitution_format_string.pb.validate.h" + +#include "source/common/formatter/substitution_format_string.h" +#include "source/common/formatter/substitution_formatter.h" + +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +class ReqWithoutQueryTest : public ::testing::Test { +public: + Http::TestRequestHeaderMapImpl request_headers_{ + {":method", "GET"}, + {":path", "/request/path?secret=parameter"}, + {"x-envoy-original-path", "/original/path?secret=parameter"}}; + Http::TestResponseHeaderMapImpl response_headers_; + Http::TestResponseTrailerMapImpl response_trailers_; + StreamInfo::MockStreamInfo stream_info_; + std::string body_; + + envoy::config::core::v3::SubstitutionFormatString config_; + NiceMock context_; +}; + +TEST_F(ReqWithoutQueryTest, TestStripQueryString) { + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%REQ_WITHOUT_QUERY(:PATH)%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("/request/path", formatter->format(request_headers_, response_headers_, + response_trailers_, stream_info_, body_)); +} + +TEST_F(ReqWithoutQueryTest, TestSelectMainHeader) { + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%REQ_WITHOUT_QUERY(X-ENVOY-ORIGINAL-PATH?:PATH)%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("/original/path", formatter->format(request_headers_, response_headers_, + response_trailers_, stream_info_, body_)); +} + +TEST_F(ReqWithoutQueryTest, TestSelectAlternativeHeader) { + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%REQ_WITHOUT_QUERY(X-NON-EXISTING-HEADER?:PATH)%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("/request/path", formatter->format(request_headers_, response_headers_, + response_trailers_, stream_info_, body_)); +} + +TEST_F(ReqWithoutQueryTest, TestTruncateHeader) { + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%REQ_WITHOUT_QUERY(:PATH):5%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("/requ", formatter->format(request_headers_, response_headers_, response_trailers_, + stream_info_, body_)); +} + +TEST_F(ReqWithoutQueryTest, TestNonExistingHeader) { + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%REQ_WITHOUT_QUERY(does-not-exist)%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("-", formatter->format(request_headers_, response_headers_, response_trailers_, + stream_info_, body_)); +} + +TEST_F(ReqWithoutQueryTest, TestFormatJson) { + const std::string yaml = R"EOF( + json_format: + no_query: "%REQ_WITHOUT_QUERY(:PATH)%" + select_main_header: "%REQ_WITHOUT_QUERY(X-ENVOY-ORIGINAL-PATH?:PATH)%" + select_alt_header: "%REQ_WITHOUT_QUERY(X-NON-EXISTING-HEADER?:PATH)%" + truncate: "%REQ_WITHOUT_QUERY(:PATH):5%" + does_not_exist: "%REQ_WITHOUT_QUERY(does-not-exist)%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + const std::string expected = R"EOF({ + "no_query": "/request/path", + "select_main_header": "/original/path", + "select_alt_header": "/request/path", + "truncate": "/requ", + "does_not_exist": null +})EOF"; + + TestUtility::loadFromYaml(yaml, config_); + auto formatter = + Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + const std::string actual = formatter->format(request_headers_, response_headers_, + response_trailers_, stream_info_, body_); + EXPECT_TRUE(TestUtility::jsonStringEqual(actual, expected)); +} + +TEST_F(ReqWithoutQueryTest, TestParserNotRecognizingCommand) { + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%COMMAND_THAT_DOES_NOT_EXIST()%" + formatters: + - name: envoy.formatter.req_without_query + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + EXPECT_THROW(Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_), + EnvoyException); +} + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/tools/extensions/extensions_check.py b/tools/extensions/extensions_check.py index fde5752799b2..b3e0b6865c60 100644 --- a/tools/extensions/extensions_check.py +++ b/tools/extensions/extensions_check.py @@ -46,7 +46,7 @@ "envoy.access_loggers", "envoy.bootstrap", "envoy.clusters", "envoy.compression.compressor", "envoy.compression.decompressor", "envoy.filters.http", "envoy.filters.http.cache", "envoy.filters.listener", "envoy.filters.network", "envoy.filters.udp_listener", - "envoy.grpc_credentials", "envoy.guarddog_actions", "envoy.health_checkers", + "envoy.formatter", "envoy.grpc_credentials", "envoy.guarddog_actions", "envoy.health_checkers", "envoy.http.stateful_header_formatters", "envoy.internal_redirect_predicates", "envoy.io_socket", "envoy.http.original_ip_detection", "envoy.matching.common_inputs", "envoy.matching.input_matchers", "envoy.quic.proof_source", "envoy.quic.server.crypto_stream",