diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 9f7c1ade0bfc..12b72e8aad6d 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -264,6 +264,9 @@ new_features: change: | Added the ability to specify a custom upstream local address selector using :ref:`local_address_selector:`. +- area: http + change: | + added support for Base64 encoding of the raw header values in the custom header operations. deprecated: - area: tracing diff --git a/docs/root/configuration/http/http_conn_man/headers.rst b/docs/root/configuration/http/http_conn_man/headers.rst index a5e95670c187..9f017761b691 100644 --- a/docs/root/configuration/http/http_conn_man/headers.rst +++ b/docs/root/configuration/http/http_conn_man/headers.rst @@ -615,6 +615,9 @@ is empty string. For example, the following configuration uses ``%RESPONSE_CODE% modify request headers using code from the response. The output is an empty string, because request headers are modified before the request is sent upstream and the response is not received yet. +Raw header values accept dynamic values as well and require escaping the +literal percent symbols. Raw header values are encoded as Base64 when appended. + .. literalinclude:: _include/header_formatters.yaml :language: yaml :linenos: diff --git a/source/common/router/BUILD b/source/common/router/BUILD index 1a0d3c11c5c4..1de53e4ac60a 100644 --- a/source/common/router/BUILD +++ b/source/common/router/BUILD @@ -437,6 +437,7 @@ envoy_cc_library( "//envoy/router:string_accessor_interface", "//envoy/stream_info:filter_state_interface", "//envoy/stream_info:stream_info_interface", + "//source/common/common:base64_lib", "//source/common/common:minimal_logger_lib", "//source/common/common:utility_lib", "//source/common/config:metadata_lib", diff --git a/source/common/router/header_formatter.h b/source/common/router/header_formatter.h index 825b70514447..c2f9f2c8f7bb 100644 --- a/source/common/router/header_formatter.h +++ b/source/common/router/header_formatter.h @@ -6,6 +6,7 @@ #include "envoy/formatter/substitution_formatter.h" +#include "source/common/common/base64.h" #include "source/common/http/header_map_impl.h" #include "absl/container/node_hash_map.h" @@ -34,7 +35,8 @@ using HttpHeaderFormatterPtr = std::unique_ptr; */ class HttpHeaderFormatterImpl : public HttpHeaderFormatter { public: - HttpHeaderFormatterImpl(Formatter::FormatterPtr&& formatter) : formatter_(std::move(formatter)) {} + HttpHeaderFormatterImpl(Formatter::FormatterPtr&& formatter, bool raw_value) + : formatter_(std::move(formatter)), raw_value_(raw_value) {} // HttpHeaderFormatter::format // Trailers are not available when HTTP headers are manipulated. @@ -43,11 +45,15 @@ class HttpHeaderFormatterImpl : public HttpHeaderFormatter { const Envoy::StreamInfo::StreamInfo& stream_info) const override { std::string buf; buf = formatter_->formatWithContext({&request_headers, &response_headers}, stream_info); + if (raw_value_) { + return Base64::encode(buf.data(), buf.size()); + } return buf; }; private: const Formatter::FormatterPtr formatter_; + const bool raw_value_; }; } // namespace Router diff --git a/source/common/router/header_parser.cc b/source/common/router/header_parser.cc index bc48fe67f506..9057b52b272f 100644 --- a/source/common/router/header_parser.cc +++ b/source/common/router/header_parser.cc @@ -37,16 +37,21 @@ parseHttpHeaderFormatter(const envoy::config::core::v3::HeaderValue& header_valu if (!Http::HeaderUtility::isModifiableHeader(key)) { throw EnvoyException(":-prefixed or host headers may not be modified"); } + const bool raw_value = !header_value.raw_value().empty(); + if (raw_value && !header_value.value().empty()) { + throw EnvoyException("cannot specify both value and raw_value"); + } // UPSTREAM_METADATA and DYNAMIC_METADATA must be translated from JSON ["a", "b"] format to colon // format (a:b) - std::string final_header_value = HeaderParser::translateMetadataFormat(header_value.value()); + std::string final_header_value = HeaderParser::translateMetadataFormat( + raw_value ? header_value.raw_value() : header_value.value()); // Change PER_REQUEST_STATE to FILTER_STATE. final_header_value = HeaderParser::translatePerRequestState(final_header_value); // Let the substitution formatter parse the final_header_value. return std::make_unique( - std::make_unique(final_header_value, true)); + std::make_unique(final_header_value, true), raw_value); } } // namespace diff --git a/test/common/router/header_formatter_test.cc b/test/common/router/header_formatter_test.cc index 49198acd68d9..c4413ba6955d 100644 --- a/test/common/router/header_formatter_test.cc +++ b/test/common/router/header_formatter_test.cc @@ -655,6 +655,39 @@ match: { prefix: "/new_endpoint" } EXPECT_EQ(1, counts["x-request-start"]); } +TEST(HeaderParserTest, EvaluateRawHeader) { + // The encoded value is: %FILTER_STATE(test_key:PLAIN)% + const std::string yaml = R"EOF( +match: { prefix: "/new_endpoint" } +route: + cluster: www2 +request_headers_to_add: + - header: + key: "x-per-request" + raw_value: JUZJTFRFUl9TVEFURSh0ZXN0X2tleTpQTEFJTiklCg== + )EOF"; + + const auto route = parseRouteFromV3Yaml(yaml); + HeaderParserPtr req_header_parser = + HeaderParser::configure(route.request_headers_to_add(), route.request_headers_to_remove()); + Http::TestRequestHeaderMapImpl header_map{{":method", "POST"}}; + NiceMock stream_info; + + Envoy::StreamInfo::FilterStateSharedPtr filter_state( + std::make_shared( + Envoy::StreamInfo::FilterState::LifeSpan::FilterChain)); + filter_state->setData("test_key", std::make_unique("test_value"), + StreamInfo::FilterState::StateType::ReadOnly, + StreamInfo::FilterState::LifeSpan::FilterChain); + ON_CALL(stream_info, filterState()).WillByDefault(ReturnRef(filter_state)); + ON_CALL(Const(stream_info), filterState()).WillByDefault(ReturnRef(*filter_state)); + + req_header_parser->evaluateHeaders(header_map, stream_info); + + EXPECT_TRUE(header_map.has("x-per-request")); + EXPECT_EQ("dGVzdF92YWx1ZQo=", header_map.get_("x-per-request")); +} + TEST(HeaderParserTest, EvaluateResponseHeaders) { const std::string yaml = R"EOF( match: { prefix: "/new_endpoint" }