From 46a403d915db00cc4c12d89a8ed76c8446116422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B5=E9=93=B6?= Date: Sun, 24 Aug 2025 20:07:14 +0800 Subject: [PATCH 1/2] cherry-pick from #39004 https://github.com/envoyproxy/envoy/commit/eb0aa190bdd1a9862653d724eb94086d31f486f9 new feature stateful session envelope, aiming to cover MCP 250326 spec SSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherry-pick from #30573 https://github.com/envoyproxy/envoy/commit/62f4a14e35b4988dc62ebb51a070875fda59e1fe#diff-c904f9b0d49cdd938ffaa952192372529415afa8602d7dc6acb904e61111d8a5 add strict mode to stateful session, return 503 when destination is not available cherry-pick from #32093 https://github.com/envoyproxy/envoy/commit/6354dcffc83153c96b9dbded64eb98a49fd6ad94 support 0 ttl in cookie stateful sessiion to disable cookie expiration Signed-off-by: 爵银 --- CODEOWNERS | 1 + api/BUILD | 1 + .../v3/stateful_session.proto | 4 + .../http/stateful_session/envelope/v3/BUILD | 9 + .../envelope/v3/envelope.proto | 52 + api/envoy/type/http/v3/cookie.proto | 2 +- api/versioning/BUILD | 1 + envoy/http/filter.h | 5 +- envoy/http/stateful_session.h | 4 +- envoy/upstream/load_balancer.h | 2 +- source/common/common/base64.cc | 2 + source/common/common/base64.h | 6 + source/common/http/async_client_impl.h | 7 +- source/common/http/filter_manager.cc | 9 +- source/common/http/filter_manager.h | 6 +- source/common/runtime/runtime_features.cc | 1 - .../common/upstream/cluster_manager_impl.cc | 3 + source/common/upstream/host_utility.cc | 14 +- source/common/upstream/host_utility.h | 1 + source/extensions/common/wasm/context.cc | 2 +- source/extensions/common/wasm/context.h | 6 +- source/extensions/extensions_build_config.bzl | 1 + source/extensions/extensions_metadata.yaml | 7 + .../http/stateful_session/stateful_session.cc | 10 +- .../http/stateful_session/stateful_session.h | 7 +- .../http/stateful_session/cookie/cookie.cc | 25 +- .../http/stateful_session/cookie/cookie.h | 23 +- .../http/stateful_session/envelope/BUILD | 36 + .../http/stateful_session/envelope/config.cc | 20 + .../http/stateful_session/envelope/config.h | 22 + .../stateful_session/envelope/envelope.cc | 57 + .../http/stateful_session/envelope/envelope.h | 42 + .../http/stateful_session/header/header.cc | 3 +- .../http/stateful_session/header/header.h | 5 +- test/common/http/filter_manager_test.cc | 5 +- test/common/router/router_test.cc | 5 +- .../upstream/cluster_manager_impl_test.cc | 45 +- test/common/upstream/host_utility_test.cc | 100 +- .../filters/http/stateful_session/BUILD | 1 + .../stateful_session_integration_test.cc | 986 ++++++++++-------- .../stateful_session/stateful_session_test.cc | 12 +- .../stateful_session/cookie/cookie_test.cc | 123 +-- .../http/stateful_session/envelope/BUILD | 36 + .../stateful_session/envelope/config_test.cc | 29 + .../envelope/envelope_test.cc | 73 ++ .../stateful_session/header/header_test.cc | 25 +- test/mocks/http/mocks.cc | 3 +- test/mocks/http/mocks.h | 11 +- test/mocks/http/stateful_session.h | 6 +- 49 files changed, 1194 insertions(+), 662 deletions(-) create mode 100644 api/envoy/extensions/http/stateful_session/envelope/v3/BUILD create mode 100644 api/envoy/extensions/http/stateful_session/envelope/v3/envelope.proto create mode 100644 source/extensions/http/stateful_session/envelope/BUILD create mode 100644 source/extensions/http/stateful_session/envelope/config.cc create mode 100644 source/extensions/http/stateful_session/envelope/config.h create mode 100644 source/extensions/http/stateful_session/envelope/envelope.cc create mode 100644 source/extensions/http/stateful_session/envelope/envelope.h create mode 100644 test/extensions/http/stateful_session/envelope/BUILD create mode 100644 test/extensions/http/stateful_session/envelope/config_test.cc create mode 100644 test/extensions/http/stateful_session/envelope/envelope_test.cc diff --git a/CODEOWNERS b/CODEOWNERS index d3ac2b86305fa..20c6cd13c8162 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -255,6 +255,7 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123 /*/extensions/access_loggers/file @wbpcode @cpakulski @giantcroc # Stateful session /*/extensions/http/stateful_session/cookie @wbpcode @cpakulski +/*/extensions/http/stateful_session/envelope @wbpcode @adisuissa /*/extensions/http/stateful_session/header @ramaraochavali @wbpcode @cpakulski /*/extensions/filters/http/stateful_session @wbpcode @cpakulski # tracers diff --git a/api/BUILD b/api/BUILD index 16b2487284916..dc08837611313 100644 --- a/api/BUILD +++ b/api/BUILD @@ -252,6 +252,7 @@ proto_library( "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", "//envoy/extensions/http/original_ip_detection/xff/v3:pkg", "//envoy/extensions/http/stateful_session/cookie/v3:pkg", + "//envoy/extensions/http/stateful_session/envelope/v3:pkg", "//envoy/extensions/http/stateful_session/header/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", diff --git a/api/envoy/extensions/filters/http/stateful_session/v3/stateful_session.proto b/api/envoy/extensions/filters/http/stateful_session/v3/stateful_session.proto index e3c612edaacff..42cf37f3cea86 100644 --- a/api/envoy/extensions/filters/http/stateful_session/v3/stateful_session.proto +++ b/api/envoy/extensions/filters/http/stateful_session/v3/stateful_session.proto @@ -23,6 +23,10 @@ message StatefulSession { // // [#extension-category: envoy.http.stateful_session] config.core.v3.TypedExtensionConfig session_state = 1; + + // If set to True, the HTTP request must be routed to the requested destination. + // If the requested destination is not available, Envoy returns 503. Defaults to False. + bool strict = 2; } message StatefulSessionPerRoute { diff --git a/api/envoy/extensions/http/stateful_session/envelope/v3/BUILD b/api/envoy/extensions/http/stateful_session/envelope/v3/BUILD new file mode 100644 index 0000000000000..fa77cab4f140a --- /dev/null +++ b/api/envoy/extensions/http/stateful_session/envelope/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/http/stateful_session/envelope/v3/envelope.proto b/api/envoy/extensions/http/stateful_session/envelope/v3/envelope.proto new file mode 100644 index 0000000000000..cee9e0a12848b --- /dev/null +++ b/api/envoy/extensions/http/stateful_session/envelope/v3/envelope.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package envoy.extensions.http.stateful_session.envelope.v3; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.stateful_session.envelope.v3"; +option java_outer_classname = "EnvelopeProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/http/stateful_session/envelope/v3;envelopev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Envelope stateful session extension] + +// The extension allows the session state is tracked via existing session context that initialized +// by the upstream server. It assumes that the upstream server will generate the session context +// (like session id header or cookie) in the initial response of the session and the client will use +// the same session context in the subsequent requests without any modification. +// +// When processing the response from the upstream, Envoy will check if the response contains the +// session context. If the response contains the session context, no matter if it's a new session +// context or an existing one, Envoy will join it and the upstream host as new session context. +// +// When processing the request from the downstream, Envoy will check if the request contains the +// session context. If the request contains the session context, Envoy will strip the +// upstream host from the session context. +// +// [#extension: envoy.http.stateful_session.envelope] +message EnvelopeSessionState { + message Header { + // Iff the header specified by the ``name`` field is present in the response (assume the ``name`` + // is set to ``session-header`` and original header value is ``xxxxxx``), then the upstream host + // address and value of ``name`` field specified header will be encoded in following format and + // the output will be used to update the ``name`` field specified header in the response: + // + // .. code-block:: none + // + // session-header: "MS4yLjMuNDo4MAo=;UV:eHh4eHh4Cg==" # base64(1.2.3.4:80);UV:base64(xxxxxx) + // + // The ``UV`` (upstream value) part is used to store the original upstream header value of + // ``name`` field specified header. + // + // If this mode is used then Envoy will assume that the header in the request will also be in the + // same format and will contain the ``UV`` part. This extension will parse the upstream host + // address and update the ``name`` field specified header in the request to the ``UV`` part. + string name = 1 [(validate.rules).string = {min_len: 1}]; + } + + // Set the header config to track the session state. + Header header = 1 [(validate.rules).message = {required: true}]; +} diff --git a/api/envoy/type/http/v3/cookie.proto b/api/envoy/type/http/v3/cookie.proto index 4fe0c2f69df60..0ceda999dfd3d 100644 --- a/api/envoy/type/http/v3/cookie.proto +++ b/api/envoy/type/http/v3/cookie.proto @@ -22,7 +22,7 @@ message Cookie { string name = 1 [(validate.rules).string = {min_len: 1}]; // Duration of cookie. This will be used to set the expiry time of a new cookie when it is - // generated. Set this to 0 to use a session cookie. + // generated. Set this to 0s to use a session cookie and disable cookie expiration. google.protobuf.Duration ttl = 2 [(validate.rules).duration = {gte {}}]; // Path of cookie. This will be used to set the path of a new cookie when it is generated. diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 32939e2249848..27612bc1c09c3 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -193,6 +193,7 @@ proto_library( "//envoy/extensions/http/original_ip_detection/xff/v3:pkg", "//envoy/extensions/http/stateful_session/cookie/v3:pkg", "//envoy/extensions/http/stateful_session/header/v3:pkg", + "//envoy/extensions/http/stateful_session/envelope/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/envoy/http/filter.h b/envoy/http/filter.h index ff0c7f4d5ffd8..19a3170cbfbd0 100644 --- a/envoy/http/filter.h +++ b/envoy/http/filter.h @@ -761,13 +761,14 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks, * host list of the routed cluster, the host should be selected first. * @param host The override host address. */ - virtual void setUpstreamOverrideHost(absl::string_view host) PURE; + virtual void setUpstreamOverrideHost(Upstream::LoadBalancerContext::OverrideHost) PURE; /** * @return absl::optional optional override host for the upstream * load balancing. */ - virtual absl::optional upstreamOverrideHost() const PURE; + virtual absl::optional + upstreamOverrideHost() const PURE; #if defined(HIGRESS) virtual bool needBuffering() const { return false; } diff --git a/envoy/http/stateful_session.h b/envoy/http/stateful_session.h index c5da82f3aeeda..8965184cf4f64 100644 --- a/envoy/http/stateful_session.h +++ b/envoy/http/stateful_session.h @@ -34,7 +34,7 @@ class SessionState { * @param host the upstream host that was finally selected. * @param headers the response headers. */ - virtual void onUpdate(const Upstream::HostDescription& host, ResponseHeaderMap& headers) PURE; + virtual void onUpdate(absl::string_view host_address, ResponseHeaderMap& headers) PURE; }; using SessionStatePtr = std::unique_ptr; @@ -51,7 +51,7 @@ class SessionStateFactory { * * @param headers request headers. */ - virtual SessionStatePtr create(const RequestHeaderMap& headers) const PURE; + virtual SessionStatePtr create(RequestHeaderMap& headers) const PURE; }; using SessionStateFactorySharedPtr = std::shared_ptr; diff --git a/envoy/upstream/load_balancer.h b/envoy/upstream/load_balancer.h index 0cf04779bb13c..b5c1194090995 100644 --- a/envoy/upstream/load_balancer.h +++ b/envoy/upstream/load_balancer.h @@ -96,7 +96,7 @@ class LoadBalancerContext { */ virtual Network::TransportSocketOptionsConstSharedPtr upstreamTransportSocketOptions() const PURE; - using OverrideHost = absl::string_view; + using OverrideHost = std::pair; /** * Returns the host the load balancer should select directly. If the expected host exists and * the host can be selected directly, the load balancer can bypass the load balancing algorithm diff --git a/source/common/common/base64.cc b/source/common/common/base64.cc index e719eb9a48630..7ca2109fbb502 100644 --- a/source/common/common/base64.cc +++ b/source/common/common/base64.cc @@ -214,6 +214,8 @@ std::string Base64::encode(const Buffer::Instance& buffer, uint64_t length) { return ret; } +std::string Base64::encode(absl::string_view input) { return encode(input.data(), input.length()); } + std::string Base64::encode(const char* input, uint64_t length) { return encode(input, length, true); } diff --git a/source/common/common/base64.h b/source/common/common/base64.h index ed2181d79b663..a941642cc50c6 100644 --- a/source/common/common/base64.h +++ b/source/common/common/base64.h @@ -29,6 +29,12 @@ class Base64 { */ static std::string encode(const char* input, uint64_t length); + /** + * Base64 encode an input char buffer with a given length. + * @param input string to encode. + */ + static std::string encode(absl::string_view input); + /** * Base64 encode an input char buffer with a given length. * @param input char array to encode. diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index b201dca91cd26..7aa289c1af047 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -494,8 +494,11 @@ class AsyncStreamImpl : public virtual AsyncClient::Stream, OptRef downstreamCallbacks() override { return {}; } OptRef upstreamCallbacks() override { return {}; } void resetIdleTimer() override {} - void setUpstreamOverrideHost(absl::string_view) override {} - absl::optional upstreamOverrideHost() const override { return {}; } + void setUpstreamOverrideHost(Upstream::LoadBalancerContext::OverrideHost) override {} + absl::optional + upstreamOverrideHost() const override { + return absl::nullopt; + } absl::string_view filterConfigName() const override { return ""; } // ScopeTrackedObject diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index 621bf948e2467..2c2c82c5f2083 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -1756,11 +1756,12 @@ Buffer::BufferMemoryAccountSharedPtr ActiveStreamDecoderFilter::account() const return parent_.account(); } -void ActiveStreamDecoderFilter::setUpstreamOverrideHost(absl::string_view host) { - parent_.upstream_override_host_.emplace(std::move(host)); +void ActiveStreamDecoderFilter::setUpstreamOverrideHost( + Upstream::LoadBalancerContext::OverrideHost upstream_override_host) { + parent_.upstream_override_host_ = upstream_override_host; } - -absl::optional ActiveStreamDecoderFilter::upstreamOverrideHost() const { +absl::optional +ActiveStreamDecoderFilter::upstreamOverrideHost() const { return parent_.upstream_override_host_; } diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index 04da25d575466..7e935794ce143 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -246,8 +246,8 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, Network::Socket::OptionsSharedPtr getUpstreamSocketOptions() const override; Buffer::BufferMemoryAccountSharedPtr account() const override; - void setUpstreamOverrideHost(absl::string_view host) override; - absl::optional upstreamOverrideHost() const override; + void setUpstreamOverrideHost(Upstream::LoadBalancerContext::OverrideHost) override; + absl::optional upstreamOverrideHost() const override; #if defined(HIGRESS) bool needBuffering() const override { return need_buffering_; } void setNeedBuffering(bool need) override { need_buffering_ = need; } @@ -1042,7 +1042,7 @@ class FilterManager : public ScopeTrackedObject, std::list watermark_callbacks_; Network::Socket::OptionsSharedPtr upstream_options_ = std::make_shared(); - absl::optional upstream_override_host_; + absl::optional upstream_override_host_; const FilterChainFactory& filter_chain_factory_; // TODO(snowp): Once FM has been moved to its own file we'll make these private classes of FM, diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index dafe75ac29015..c01d02fec5e42 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -76,7 +76,6 @@ RUNTIME_GUARD(envoy_reloadable_features_send_header_raw_value); RUNTIME_GUARD(envoy_reloadable_features_send_local_reply_when_no_buffer_and_upstream_request); RUNTIME_GUARD(envoy_reloadable_features_service_sanitize_non_utf8_strings); RUNTIME_GUARD(envoy_reloadable_features_skip_dns_lookup_for_proxied_requests); -RUNTIME_GUARD(envoy_reloadable_features_stateful_session_encode_ttl_in_cookie); RUNTIME_GUARD(envoy_reloadable_features_successful_active_health_check_uneject_host); RUNTIME_GUARD(envoy_reloadable_features_tcp_pool_idle_timeout); RUNTIME_GUARD(envoy_reloadable_features_test_feature_true); diff --git a/source/common/upstream/cluster_manager_impl.cc b/source/common/upstream/cluster_manager_impl.cc index 6fd11863118e8..14779adc32be2 100644 --- a/source/common/upstream/cluster_manager_impl.cc +++ b/source/common/upstream/cluster_manager_impl.cc @@ -1798,6 +1798,9 @@ HostConstSharedPtr ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEnt if (host != nullptr) { return host; } + if (!HostUtility::allowLBChooseHost(context)) { + return nullptr; + } return lb_->chooseHost(context); } diff --git a/source/common/upstream/host_utility.cc b/source/common/upstream/host_utility.cc index 5efe759dfb36e..7ead5695651cb 100644 --- a/source/common/upstream/host_utility.cc +++ b/source/common/upstream/host_utility.cc @@ -169,7 +169,7 @@ HostConstSharedPtr HostUtility::selectOverrideHost(const HostMap* host_map, Host return nullptr; } - auto host_iter = host_map->find(override_host.value()); + auto host_iter = host_map->find(override_host.value().first); // The override host cannot be found in the host map. if (host_iter == host_map->end()) { @@ -196,5 +196,17 @@ HostConstSharedPtr HostUtility::selectOverrideHost(const HostMap* host_map, Host return nullptr; } +bool HostUtility::allowLBChooseHost(LoadBalancerContext* context) { + if (context == nullptr) { + return true; + } + auto override_host = context->overrideHostToSelect(); + if (!override_host.has_value()) { + return true; + } + // Return opposite value to "strict" setting. + return !override_host.value().second; +} + } // namespace Upstream } // namespace Envoy diff --git a/source/common/upstream/host_utility.h b/source/common/upstream/host_utility.h index 637b24cff2556..9c7507ee10e82 100644 --- a/source/common/upstream/host_utility.h +++ b/source/common/upstream/host_utility.h @@ -28,6 +28,7 @@ class HostUtility { // A utility function to select override host from host map according to load balancer context. static HostConstSharedPtr selectOverrideHost(const HostMap* host_map, HostStatusSet status, LoadBalancerContext* context); + static bool allowLBChooseHost(LoadBalancerContext* context); }; } // namespace Upstream diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc index 592e50d1038d7..756be7b6b5467 100644 --- a/source/extensions/common/wasm/context.cc +++ b/source/extensions/common/wasm/context.cc @@ -1993,7 +1993,7 @@ WasmResult Context::getUpstreamHosts(StringPairs* result) { } WasmResult Context::setUpstreamOverrideHost(std::string_view address) { if (decoder_callbacks_) { - decoder_callbacks_->setUpstreamOverrideHost(address); + decoder_callbacks_->setUpstreamOverrideHost(std::make_pair(address, false)); } return WasmResult::Ok; } diff --git a/source/extensions/common/wasm/context.h b/source/extensions/common/wasm/context.h index 32e6cf30a54a8..403554beed738 100644 --- a/source/extensions/common/wasm/context.h +++ b/source/extensions/common/wasm/context.h @@ -30,11 +30,11 @@ using proxy_wasm::CloseType; using proxy_wasm::ContextBase; using proxy_wasm::Pairs; using proxy_wasm::PairsWithStringValues; -using proxy_wasm::StringPairs; using proxy_wasm::PluginBase; using proxy_wasm::PluginHandleBase; using proxy_wasm::SharedQueueDequeueToken; using proxy_wasm::SharedQueueEnqueueToken; +using proxy_wasm::StringPairs; using proxy_wasm::WasmBase; using proxy_wasm::WasmBufferType; using proxy_wasm::WasmHandleBase; @@ -221,10 +221,10 @@ class Context : public proxy_wasm::ContextBase, #if defined(HIGRESS) WasmResult injectEncodedDataToFilterChain(std::string_view body_text, bool end_stream) override; WasmResult injectEncodedDataToFilterChainOnHeader(std::string_view body_text, bool end_stream); - WasmResult getUpstreamHosts(StringPairs * result) override; + WasmResult getUpstreamHosts(StringPairs* result) override; WasmResult setUpstreamOverrideHost(std::string_view address) override; #endif - + void clearRouteCache() override { #if defined(HIGRESS) if (decoder_callbacks_ && !disable_clear_route_cache_) { diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index cca3b2ed461af..ca33daedfed20 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -357,6 +357,7 @@ EXTENSIONS = { # "envoy.http.stateful_session.cookie": "//source/extensions/http/stateful_session/cookie:config", + "envoy.http.stateful_session.envelope": "//source/extensions/http/stateful_session/envelope:config", "envoy.http.stateful_session.header": "//source/extensions/http/stateful_session/header:config", # diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index a22fdee316f83..90e3099a7c2e2 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1337,6 +1337,13 @@ envoy.http.stateful_session.header: status: alpha type_urls: - envoy.extensions.http.stateful_session.header.v3.HeaderBasedSessionState +envoy.http.stateful_session.envelope: + categories: + - envoy.http.stateful_session + security_posture: unknown + status: alpha + type_urls: + - envoy.extensions.http.stateful_session.envelope.v3.EnvelopeSessionState envoy.matching.inputs.request_headers: categories: - envoy.matching.http.input diff --git a/source/extensions/filters/http/stateful_session/stateful_session.cc b/source/extensions/filters/http/stateful_session/stateful_session.cc index f39921080e715..e41d335c78cd1 100644 --- a/source/extensions/filters/http/stateful_session/stateful_session.cc +++ b/source/extensions/filters/http/stateful_session/stateful_session.cc @@ -16,7 +16,7 @@ namespace { class EmptySessionStateFactory : public Envoy::Http::SessionStateFactory { public: - Envoy::Http::SessionStatePtr create(const Envoy::Http::RequestHeaderMap&) const override { + Envoy::Http::SessionStatePtr create(Envoy::Http::RequestHeaderMap&) const override { return nullptr; } }; @@ -24,7 +24,8 @@ class EmptySessionStateFactory : public Envoy::Http::SessionStateFactory { } // namespace StatefulSessionConfig::StatefulSessionConfig(const ProtoConfig& config, - Server::Configuration::CommonFactoryContext& context) { + Server::Configuration::CommonFactoryContext& context) + : strict_(config.strict()) { if (!config.has_session_state()) { factory_ = std::make_shared(); return; @@ -66,7 +67,8 @@ Http::FilterHeadersStatus StatefulSession::decodeHeaders(Http::RequestHeaderMap& } if (auto upstream_address = session_state_->upstreamAddress(); upstream_address.has_value()) { - decoder_callbacks_->setUpstreamOverrideHost(upstream_address.value()); + decoder_callbacks_->setUpstreamOverrideHost( + std::make_pair(upstream_address.value(), config->isStrict())); } return Http::FilterHeadersStatus::Continue; } @@ -80,7 +82,7 @@ Http::FilterHeadersStatus StatefulSession::encodeHeaders(Http::ResponseHeaderMap upstream_info != nullptr) { auto host = upstream_info->upstreamHost(); if (host != nullptr) { - session_state_->onUpdate(*host, headers); + session_state_->onUpdate(host->address()->asStringView(), headers); } } diff --git a/source/extensions/filters/http/stateful_session/stateful_session.h b/source/extensions/filters/http/stateful_session/stateful_session.h index ede031a2d8623..39cdd3c698c99 100644 --- a/source/extensions/filters/http/stateful_session/stateful_session.h +++ b/source/extensions/filters/http/stateful_session/stateful_session.h @@ -28,13 +28,16 @@ class StatefulSessionConfig { StatefulSessionConfig(const ProtoConfig& config, Server::Configuration::CommonFactoryContext& context); - Http::SessionStatePtr createSessionState(const Http::RequestHeaderMap& headers) const { + Http::SessionStatePtr createSessionState(Http::RequestHeaderMap& headers) const { ASSERT(factory_ != nullptr); return factory_->create(headers); } + bool isStrict() const { return strict_; } + private: Http::SessionStateFactorySharedPtr factory_; + bool strict_{false}; }; using StatefulSessionConfigSharedPtr = std::shared_ptr; @@ -68,7 +71,7 @@ class StatefulSession : public Http::PassThroughFilter, private: Http::SessionStatePtr session_state_; - StatefulSessionConfigSharedPtr config_{}; + StatefulSessionConfigSharedPtr config_; }; } // namespace StatefulSession diff --git a/source/extensions/http/stateful_session/cookie/cookie.cc b/source/extensions/http/stateful_session/cookie/cookie.cc index 631249b763ea9..b339c306fc437 100644 --- a/source/extensions/http/stateful_session/cookie/cookie.cc +++ b/source/extensions/http/stateful_session/cookie/cookie.cc @@ -10,25 +10,20 @@ namespace StatefulSession { namespace Cookie { void CookieBasedSessionStateFactory::SessionStateImpl::onUpdate( - const Upstream::HostDescription& host, Envoy::Http::ResponseHeaderMap& headers) { - absl::string_view host_address = host.address()->asStringView(); - std::string encoded_address; + absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) { if (!upstream_address_.has_value() || host_address != upstream_address_.value()) { - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.stateful_session_encode_ttl_in_cookie")) { - auto expiry_time = std::chrono::duration_cast( + // Build proto message + envoy::Cookie cookie; + cookie.set_address(std::string(host_address)); + if (factory_.ttl_ != std::chrono::seconds::zero()) { + const auto expiry_time = std::chrono::duration_cast( (time_source_.monotonicTime() + std::chrono::seconds(factory_.ttl_)).time_since_epoch()); - // Build proto message - envoy::Cookie cookie; - cookie.set_address(std::string(host_address)); cookie.set_expires(expiry_time.count()); - std::string proto_string; - cookie.SerializeToString(&proto_string); - - encoded_address = Envoy::Base64::encode(proto_string.data(), proto_string.length()); - } else { - encoded_address = Envoy::Base64::encode(host_address.data(), host_address.length()); } + std::string proto_string; + cookie.SerializeToString(&proto_string); + const std::string encoded_address = + Envoy::Base64::encode(proto_string.data(), proto_string.length()); headers.addReferenceKey(Envoy::Http::Headers::get().SetCookie, factory_.makeSetCookie(encoded_address)); } diff --git a/source/extensions/http/stateful_session/cookie/cookie.h b/source/extensions/http/stateful_session/cookie/cookie.h index dc580395ae00f..92e3cdcb7a675 100644 --- a/source/extensions/http/stateful_session/cookie/cookie.h +++ b/source/extensions/http/stateful_session/cookie/cookie.h @@ -30,8 +30,7 @@ class CookieBasedSessionStateFactory : public Envoy::Http::SessionStateFactory { : upstream_address_(std::move(address)), factory_(factory), time_source_(time_source) {} absl::optional upstreamAddress() const override { return upstream_address_; } - void onUpdate(const Upstream::HostDescription& host, - Envoy::Http::ResponseHeaderMap& headers) override; + void onUpdate(absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) override; private: absl::optional upstream_address_; @@ -42,7 +41,7 @@ class CookieBasedSessionStateFactory : public Envoy::Http::SessionStateFactory { CookieBasedSessionStateFactory(const CookieBasedSessionStateProto& config, TimeSource& time_source); - Envoy::Http::SessionStatePtr create(const Envoy::Http::RequestHeaderMap& headers) const override { + Envoy::Http::SessionStatePtr create(Envoy::Http::RequestHeaderMap& headers) const override { if (!requestPathMatch(headers.getPathValue())) { return nullptr; } @@ -69,17 +68,19 @@ class CookieBasedSessionStateFactory : public Envoy::Http::SessionStateFactory { envoy::Cookie cookie; if (cookie.ParseFromString(decoded_value)) { address = cookie.address(); - if (address.empty() || (cookie.expires() == 0)) { + if (address.empty()) { return absl::nullopt; } - std::chrono::seconds expiry_time(cookie.expires()); - auto now = std::chrono::duration_cast( - (time_source_.monotonicTime()).time_since_epoch()); - if (now > expiry_time) { - // Ignore the address extracted from the cookie. This will cause - // upstream cluster to select a new host and new cookie will be generated. - return absl::nullopt; + if (cookie.expires() != 0) { + const std::chrono::seconds expiry_time(cookie.expires()); + const auto now = std::chrono::duration_cast( + (time_source_.monotonicTime()).time_since_epoch()); + if (now > expiry_time) { + // Ignore the address extracted from the cookie. This will cause + // upstream cluster to select a new host and new cookie will be generated. + return absl::nullopt; + } } } else { ENVOY_LOG_ONCE_MISC( diff --git a/source/extensions/http/stateful_session/envelope/BUILD b/source/extensions/http/stateful_session/envelope/BUILD new file mode 100644 index 0000000000000..5fe2d5187b4e8 --- /dev/null +++ b/source/extensions/http/stateful_session/envelope/BUILD @@ -0,0 +1,36 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "envelope_lib", + srcs = ["envelope.cc"], + hdrs = ["envelope.h"], + deps = [ + "//envoy/http:stateful_session_interface", + "//source/common/common:base64_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "@envoy_api//envoy/extensions/http/stateful_session/envelope/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":envelope_lib", + "//envoy/http:stateful_session_interface", + "//envoy/registry", + "//source/common/config:utility_lib", + "@envoy_api//envoy/extensions/http/stateful_session/envelope/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/http/stateful_session/envelope/config.cc b/source/extensions/http/stateful_session/envelope/config.cc new file mode 100644 index 0000000000000..41a663e252d94 --- /dev/null +++ b/source/extensions/http/stateful_session/envelope/config.cc @@ -0,0 +1,20 @@ +#include "source/extensions/http/stateful_session/envelope/config.h" +#include "envoy/extensions/http/stateful_session/envelope/v3/envelope.pb.validate.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +Envoy::Http::SessionStateFactorySharedPtr +EnvelopeSessionStateFactoryConfig::createSessionStateFactory( + const Protobuf::Message& config, Server::Configuration::CommonFactoryContext& context) { + const auto& proto_config = MessageUtil::downcastAndValidate( + config, context.messageValidationVisitor()); + return std::make_shared(proto_config); +} +REGISTER_FACTORY(EnvelopeSessionStateFactoryConfig, Envoy::Http::SessionStateFactoryConfig); +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/stateful_session/envelope/config.h b/source/extensions/http/stateful_session/envelope/config.h new file mode 100644 index 0000000000000..3afaefb54f2f6 --- /dev/null +++ b/source/extensions/http/stateful_session/envelope/config.h @@ -0,0 +1,22 @@ +#pragma once +#include "source/extensions/http/stateful_session/envelope/envelope.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +class EnvelopeSessionStateFactoryConfig : public Envoy::Http::SessionStateFactoryConfig { +public: + Envoy::Http::SessionStateFactorySharedPtr + createSessionStateFactory(const Protobuf::Message& config, + Server::Configuration::CommonFactoryContext& context) override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + std::string name() const override { return "envoy.http.stateful_session.envelope"; } +}; +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/stateful_session/envelope/envelope.cc b/source/extensions/http/stateful_session/envelope/envelope.cc new file mode 100644 index 0000000000000..47ffd2fa035de --- /dev/null +++ b/source/extensions/http/stateful_session/envelope/envelope.cc @@ -0,0 +1,57 @@ +#include "source/extensions/http/stateful_session/envelope/envelope.h" +#include "absl/container/inlined_vector.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +constexpr absl::string_view OriginUpstreamValuePartFlag = "UV:"; +void EnvelopeSessionStateFactory::SessionStateImpl::onUpdate( + absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) { + const auto upstream_value_header = headers.get(factory_.name_); + if (upstream_value_header.size() != 1) { + ENVOY_LOG(trace, "Header {} not exist or occurs multiple times", factory_.name_); + return; + } + const std::string new_header = + absl::StrCat(Envoy::Base64::encode(host_address), ";", OriginUpstreamValuePartFlag, + Envoy::Base64::encode(upstream_value_header[0]->value().getStringView())); + headers.setReferenceKey(factory_.name_, new_header); +} +EnvelopeSessionStateFactory::EnvelopeSessionStateFactory(const EnvelopeSessionStateProto& config) + : name_(config.header().name()) {} +absl::optional +EnvelopeSessionStateFactory::parseAddress(Envoy::Http::RequestHeaderMap& headers) const { + const auto hdr = headers.get(name_); + if (hdr.empty()) { + return absl::nullopt; + } + const absl::InlinedVector parts = + absl::StrSplit(hdr[0]->value().getStringView(), ';', absl::SkipEmpty()); + if (parts.empty()) { + return absl::nullopt; + } + std::string upstream_host = Base64::decode(parts[0]); + absl::string_view upstream_value; + // parts[0] is the base64 encoded upstream address and should be skipped. + const auto other_parts_view = absl::Span(parts).subspan(1); + for (absl::string_view part : other_parts_view) { + if (absl::StartsWith(part, OriginUpstreamValuePartFlag)) { + upstream_value = part.substr(OriginUpstreamValuePartFlag.size()); + break; + } + } + const std::string decoded = Envoy::Base64::decode(upstream_value); + if (decoded.empty()) { + // Do nothing if the 'UV' part is not valid or if there is no UV part. + ENVOY_LOG(info, "Header {} contains invalid 'UV' part or there is no 'UV' part", name_); + return absl::nullopt; + } + headers.setReferenceKey(name_, decoded); + return !upstream_host.empty() ? absl::make_optional(std::move(upstream_host)) : absl::nullopt; +} +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/stateful_session/envelope/envelope.h b/source/extensions/http/stateful_session/envelope/envelope.h new file mode 100644 index 0000000000000..31ffa82b7270d --- /dev/null +++ b/source/extensions/http/stateful_session/envelope/envelope.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include "envoy/extensions/http/stateful_session/envelope/v3/envelope.pb.h" +#include "envoy/http/stateful_session.h" +#include "source/common/common/base64.h" +#include "source/common/http/headers.h" +#include "source/common/http/utility.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +using EnvelopeSessionStateProto = + envoy::extensions::http::stateful_session::envelope::v3::EnvelopeSessionState; +class EnvelopeSessionStateFactory : public Envoy::Http::SessionStateFactory, + public Logger::Loggable { +public: + class SessionStateImpl : public Envoy::Http::SessionState { + public: + SessionStateImpl(absl::optional address, + const EnvelopeSessionStateFactory& factory) + : upstream_address_(std::move(address)), factory_(factory) {} + absl::optional upstreamAddress() const override { return upstream_address_; } + void onUpdate(absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) override; + private: + absl::optional upstream_address_; + const EnvelopeSessionStateFactory& factory_; + }; + EnvelopeSessionStateFactory(const EnvelopeSessionStateProto& config); + Envoy::Http::SessionStatePtr create(Envoy::Http::RequestHeaderMap& headers) const override { + return std::make_unique(parseAddress(headers), *this); + } +private: + absl::optional parseAddress(Envoy::Http::RequestHeaderMap& headers) const; + const Envoy::Http::LowerCaseString name_; +}; +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/stateful_session/header/header.cc b/source/extensions/http/stateful_session/header/header.cc index bb8bebc884c32..e42f8d70c0fd4 100644 --- a/source/extensions/http/stateful_session/header/header.cc +++ b/source/extensions/http/stateful_session/header/header.cc @@ -7,8 +7,7 @@ namespace StatefulSession { namespace Header { void HeaderBasedSessionStateFactory::SessionStateImpl::onUpdate( - const Upstream::HostDescription& host, Envoy::Http::ResponseHeaderMap& headers) { - absl::string_view host_address = host.address()->asStringView(); + absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) { if (!upstream_address_.has_value() || host_address != upstream_address_.value()) { const std::string encoded_address = Envoy::Base64::encode(host_address.data(), host_address.length()); diff --git a/source/extensions/http/stateful_session/header/header.h b/source/extensions/http/stateful_session/header/header.h index 2daac653073d9..d190e37a949a7 100644 --- a/source/extensions/http/stateful_session/header/header.h +++ b/source/extensions/http/stateful_session/header/header.h @@ -28,8 +28,7 @@ class HeaderBasedSessionStateFactory : public Envoy::Http::SessionStateFactory { : upstream_address_(std::move(address)), factory_(factory) {} absl::optional upstreamAddress() const override { return upstream_address_; } - void onUpdate(const Upstream::HostDescription& host, - Envoy::Http::ResponseHeaderMap& headers) override; + void onUpdate(absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) override; private: absl::optional upstream_address_; @@ -38,7 +37,7 @@ class HeaderBasedSessionStateFactory : public Envoy::Http::SessionStateFactory { HeaderBasedSessionStateFactory(const HeaderBasedSessionStateProto& config); - Envoy::Http::SessionStatePtr create(const Envoy::Http::RequestHeaderMap& headers) const override { + Envoy::Http::SessionStatePtr create(Envoy::Http::RequestHeaderMap& headers) const override { return std::make_unique(parseAddress(headers), *this); } diff --git a/test/common/http/filter_manager_test.cc b/test/common/http/filter_manager_test.cc index 42a5602a90063..bb682b472eeb9 100644 --- a/test/common/http/filter_manager_test.cc +++ b/test/common/http/filter_manager_test.cc @@ -335,10 +335,11 @@ TEST_F(FilterManagerTest, SetAndGetUpstreamOverrideHost) { })); filter_manager_->createFilterChain(); - decoder_filter->callbacks_->setUpstreamOverrideHost("1.2.3.4"); + decoder_filter->callbacks_->setUpstreamOverrideHost(std::make_pair("1.2.3.4", true)); auto override_host = decoder_filter->callbacks_->upstreamOverrideHost(); - EXPECT_EQ(override_host.value(), "1.2.3.4"); + EXPECT_EQ(override_host.value().first, "1.2.3.4"); + EXPECT_TRUE(override_host.value().second); filter_manager_->destroyFilters(); }; diff --git a/test/common/router/router_test.cc b/test/common/router/router_test.cc index cd69c7416968f..b164a96dee8a2 100644 --- a/test/common/router/router_test.cc +++ b/test/common/router/router_test.cc @@ -6694,10 +6694,11 @@ TEST_F(RouterTest, RequestWithUpstreamOverrideHost) { // `LoadBalancerContext` is called, `upstreamOverrideHost` of StreamDecoderFilterCallbacks will be // called to get address of upstream host that should be selected first. EXPECT_CALL(callbacks_, upstreamOverrideHost()) - .WillOnce(Return(absl::make_optional("1.2.3.4"))); + .WillOnce(Return(absl::make_optional( + std::make_pair("1.2.3.4", false)))); auto override_host = router_->overrideHostToSelect(); - EXPECT_EQ("1.2.3.4", override_host.value()); + EXPECT_EQ("1.2.3.4", override_host.value().first); Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; HttpTestUtility::addDefaultHeaders(headers); diff --git a/test/common/upstream/cluster_manager_impl_test.cc b/test/common/upstream/cluster_manager_impl_test.cc index 1bf8381178eb5..1ec36fd879220 100644 --- a/test/common/upstream/cluster_manager_impl_test.cc +++ b/test/common/upstream/cluster_manager_impl_test.cc @@ -4189,7 +4189,7 @@ TEST_F(ClusterManagerImplTest, SelectOverrideHostTestNoOverrideHost) { auto to_create = new Tcp::ConnectionPool::MockInstance(); - EXPECT_CALL(context, overrideHostToSelect()).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(context, overrideHostToSelect()).Times(2).WillRepeatedly(Return(absl::nullopt)); EXPECT_CALL(factory_, allocateTcpConnPool_(_)).WillOnce(Return(to_create)); EXPECT_CALL(*to_create, addIdleCallback(_)); @@ -4206,11 +4206,12 @@ TEST_F(ClusterManagerImplTest, SelectOverrideHostTestWithOverrideHost) { auto to_create = new Tcp::ConnectionPool::MockInstance(); EXPECT_CALL(context, overrideHostToSelect()) - .WillRepeatedly(Return(absl::make_optional("127.0.0.1:11002"))); + .WillRepeatedly(Return(absl::make_optional( + std::make_pair("127.0.0.1:11001", false)))); EXPECT_CALL(factory_, allocateTcpConnPool_(_)) .WillOnce(testing::Invoke([&](HostConstSharedPtr host) { - EXPECT_EQ("127.0.0.1:11002", host->address()->asStringView()); + EXPECT_EQ("127.0.0.1:11001", host->address()->asStringView()); return to_create; })); EXPECT_CALL(*to_create, addIdleCallback(_)); @@ -4228,6 +4229,41 @@ TEST_F(ClusterManagerImplTest, SelectOverrideHostTestWithOverrideHost) { EXPECT_TRUE(opt_cp_3.has_value()); } +TEST_F(ClusterManagerImplTest, SelectOverrideHostTestWithNonExistingHost) { + createWithLocalClusterUpdate(); + NiceMock context; + auto to_create = new Tcp::ConnectionPool::MockInstance(); + EXPECT_CALL(context, overrideHostToSelect()) + .WillRepeatedly(Return(absl::make_optional( + // Return non-existing host. Let the LB choose the host. + std::make_pair("127.0.0.2:12345", false)))); + EXPECT_CALL(factory_, allocateTcpConnPool_(_)) + .WillOnce(testing::Invoke([&](HostConstSharedPtr host) { + EXPECT_THAT(host->address()->asStringView(), + testing::AnyOf("127.0.0.1:11001", "127.0.0.1:11002")); + return to_create; + })); + EXPECT_CALL(*to_create, addIdleCallback(_)); + auto opt_cp = cluster_manager_->getThreadLocalCluster("cluster_1") + ->tcpConnPool(ResourcePriority::Default, &context); + EXPECT_TRUE(opt_cp.has_value()); +} +TEST_F(ClusterManagerImplTest, SelectOverrideHostTestWithNonExistingHostStrict) { + createWithLocalClusterUpdate(); + NiceMock context; + EXPECT_CALL(context, overrideHostToSelect()) + .WillRepeatedly(Return(absl::make_optional( + // Return non-existing host and indicate strict mode. + // LB should not be allowed to choose host. + std::make_pair("127.0.0.2:12345", true)))); + // Requested upstream host 127.0.0.2:12345 is not part of the cluster. + // Connection pool should not be created. + EXPECT_CALL(factory_, allocateTcpConnPool_(_)).Times(0); + auto opt_cp = cluster_manager_->getThreadLocalCluster("cluster_1") + ->tcpConnPool(ResourcePriority::Default, &context); + EXPECT_FALSE(opt_cp.has_value()); +} + TEST_F(ClusterManagerImplTest, UpstreamSocketOptionsPassedToConnPool) { createWithLocalClusterUpdate(); NiceMock context; @@ -6116,7 +6152,8 @@ TEST_F(PreconnectTest, PreconnectOnWithOverrideHost) { NiceMock context; EXPECT_CALL(context, overrideHostToSelect()) - .WillRepeatedly(Return(absl::make_optional("127.0.0.1:80"))); + .WillRepeatedly(Return(absl::make_optional( + std::make_pair("127.0.0.1:80", false)))); // Only allocate connection pool once. EXPECT_CALL(factory_, allocateTcpConnPool_) diff --git a/test/common/upstream/host_utility_test.cc b/test/common/upstream/host_utility_test.cc index 4130d55dd8d94..ef89c242ed38c 100644 --- a/test/common/upstream/host_utility_test.cc +++ b/test/common/upstream/host_utility_test.cc @@ -171,57 +171,55 @@ TEST(HostUtilityTest, SelectOverrideHostTest) { EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), all_health_statuses, &context)); } - { - // The host map does not contain the expected host. - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; - EXPECT_CALL(context, overrideHostToSelect()) - .WillOnce(Return(absl::make_optional(override_host))); - auto host_map = std::make_shared(); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); - } - { - auto mock_host = std::make_shared>(); - EXPECT_CALL(*mock_host, healthStatus()) - .WillRepeatedly(Return(envoy::config::core::v3::HealthStatus::UNHEALTHY)); - - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; - EXPECT_CALL(context, overrideHostToSelect()) - .WillRepeatedly(Return(absl::make_optional(override_host))); - - auto host_map = std::make_shared(); - host_map->insert({"1.2.3.4", mock_host}); - - EXPECT_EQ(mock_host, - HostUtility::selectOverrideHost(host_map.get(), UnhealthyStatus, &context)); - EXPECT_EQ(mock_host, - HostUtility::selectOverrideHost(host_map.get(), all_health_statuses, &context)); - - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DegradedStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), TimeoutStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DrainingStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), UnknownStatus, &context)); - } - { - auto mock_host = std::make_shared>(); - EXPECT_CALL(*mock_host, healthStatus()) - .WillRepeatedly(Return(envoy::config::core::v3::HealthStatus::DEGRADED)); - - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; - EXPECT_CALL(context, overrideHostToSelect()) - .WillRepeatedly(Return(absl::make_optional(override_host))); - - auto host_map = std::make_shared(); - host_map->insert({"1.2.3.4", mock_host}); - EXPECT_EQ(mock_host, HostUtility::selectOverrideHost(host_map.get(), DegradedStatus, &context)); - EXPECT_EQ(mock_host, - HostUtility::selectOverrideHost(host_map.get(), all_health_statuses, &context)); - - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), UnhealthyStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), TimeoutStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DrainingStatus, &context)); - EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), UnknownStatus, &context)); + // Test overriding host in strict and non-strict mode. + for (const bool strict_mode : {false, true}) { + { + // The host map does not contain the expected host. + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", strict_mode}; + EXPECT_CALL(context, overrideHostToSelect()) + .WillOnce(Return(absl::make_optional(override_host))); + auto host_map = std::make_shared(); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); + } + { + auto mock_host = std::make_shared>(); + EXPECT_CALL(*mock_host, healthStatus()) + .WillRepeatedly(Return(envoy::config::core::v3::HealthStatus::UNHEALTHY)); + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", strict_mode}; + EXPECT_CALL(context, overrideHostToSelect()) + .WillRepeatedly(Return(absl::make_optional(override_host))); + auto host_map = std::make_shared(); + host_map->insert({"1.2.3.4", mock_host}); + EXPECT_EQ(mock_host, + HostUtility::selectOverrideHost(host_map.get(), UnhealthyStatus, &context)); + EXPECT_EQ(mock_host, + HostUtility::selectOverrideHost(host_map.get(), all_health_statuses, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DegradedStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), TimeoutStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DrainingStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), UnknownStatus, &context)); + } + { + auto mock_host = std::make_shared>(); + EXPECT_CALL(*mock_host, healthStatus()) + .WillRepeatedly(Return(envoy::config::core::v3::HealthStatus::DEGRADED)); + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", strict_mode}; + EXPECT_CALL(context, overrideHostToSelect()) + .WillRepeatedly(Return(absl::make_optional(override_host))); + auto host_map = std::make_shared(); + host_map->insert({"1.2.3.4", mock_host}); + EXPECT_EQ(mock_host, + HostUtility::selectOverrideHost(host_map.get(), DegradedStatus, &context)); + EXPECT_EQ(mock_host, + HostUtility::selectOverrideHost(host_map.get(), all_health_statuses, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), HealthyStatus, &context)); + EXPECT_EQ(nullptr, + HostUtility::selectOverrideHost(host_map.get(), UnhealthyStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), TimeoutStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), DrainingStatus, &context)); + EXPECT_EQ(nullptr, HostUtility::selectOverrideHost(host_map.get(), UnknownStatus, &context)); + } } } diff --git a/test/extensions/filters/http/stateful_session/BUILD b/test/extensions/filters/http/stateful_session/BUILD index fa8f8310f2ff3..5a711e59eff1e 100644 --- a/test/extensions/filters/http/stateful_session/BUILD +++ b/test/extensions/filters/http/stateful_session/BUILD @@ -39,6 +39,7 @@ envoy_extension_cc_test( "//source/common/protobuf", "//source/extensions/filters/http/stateful_session:config", "//source/extensions/http/stateful_session/cookie:config", + "//source/extensions/http/stateful_session/envelope:config", "//source/extensions/http/stateful_session/header:config", "//test/integration:http_integration_lib", "//test/test_common:utility_lib", diff --git a/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc b/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc index a35a9a59d6422..b43c18383272a 100644 --- a/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc +++ b/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc @@ -149,6 +149,24 @@ name: envoy.filters.http.stateful_session name: session-header )EOF"; +static const std::string STATEFUL_SESSION_ENVELOPE_AND_HEADER = + R"EOF( +name: envoy.filters.http.stateful_session +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSession + session_state: + name: envoy.http.stateful_session.envelope + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.stateful_session.envelope.v3.EnvelopeSessionState + header: + name: session-header +)EOF"; + +static const std::string STATEFUL_SESSION_STRICT_MODE = + R"EOF( + strict: true +)EOF"; + static const std::string DISABLE_STATEFUL_SESSION = R"EOF( "@type": type.googleapis.com/envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute @@ -183,28 +201,116 @@ static const std::string OVERRIDE_STATEFUL_SESSION_HEADER = TEST_F(StatefulSessionIntegrationTest, NormalStatefulSession) { initializeFilterAndRoute(STATEFUL_SESSION_FILTER, ""); - // Run the test twice. Once with proto cookie encoding and once with "old", non-proto - // encoding. - for (const bool use_proto : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - use_proto); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(upstream_index.value(), endpoint); + std::string address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // The selected upstream server address would be selected to the response headers. + EXPECT_EQ( + ProtoCookieObject( + response->headers().get(Http::LowerCaseString("set-cookie"))[0]->value().getStringView()), + ProtoCookieObject(address_string, 120, "/test", "HttpOnly")); + cleanupUpstreamAndDownstream(); +} +TEST_F(StatefulSessionIntegrationTest, StatefulSessionHeaderWithEnvelopeAndStrictMode) { + initializeFilterAndRoute(STATEFUL_SESSION_ENVELOPE_AND_HEADER + STATEFUL_SESSION_STRICT_MODE, ""); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(upstream_index.value(), endpoint); + const std::string address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + const std::string encoded_address = fmt::format("{};UV:{}", Envoy::Base64::encode(address_string), + Envoy::Base64::encode("abcdefg")); + default_response_headers_.addCopy(Http::LowerCaseString("session-header"), "abcdefg"); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // The selected upstream server address would be encoded into the response headers. + EXPECT_EQ( + encoded_address, + response->headers().get(Http::LowerCaseString("session-header"))[0]->value().getStringView()); + cleanupUpstreamAndDownstream(); +} +TEST_F(StatefulSessionIntegrationTest, + DownstreamRequestWithStatefulSessionHeaderWithEnvelopeAndStrictMode) { + initializeFilterAndRoute(STATEFUL_SESSION_ENVELOPE_AND_HEADER + STATEFUL_SESSION_STRICT_MODE, ""); + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + const std::string address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + const std::string encoded_address = fmt::format( + "{};UV:{}", Envoy::Base64::encode(address_string), Envoy::Base64::encode("abcdefg")); codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, - {":authority", "stateful.session.com"}}; + {":authority", "stateful.session.com"}, + {"session-header", encoded_address}}; auto response = codec_client_->makeRequestWithBody(request_headers, 0); + // The upstream with index 1 should be selected. auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - + EXPECT_EQ(upstream_index.value(), 1); + EXPECT_EQ(upstream_request_->headers() + .get(Http::LowerCaseString("session-header"))[0] + ->value() + .getStringView(), + "abcdefg"); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added because we will only packages the exist session header + // in the response. If the session header is not exist, we will not add the session header. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); + cleanupUpstreamAndDownstream(); + } + { envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(upstream_index.value(), endpoint); - - std::string address_string = + setUpstreamAddress(2, endpoint); + const std::string address_string = fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + const std::string encoded_address = fmt::format( + "{};UV:{}", Envoy::Base64::encode(address_string), Envoy::Base64::encode("abcdefg")); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"session-header", encoded_address}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + // The upstream with index 2 should be selected. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 2); + EXPECT_EQ(upstream_request_->headers() + .get(Http::LowerCaseString("session-header"))[0] + ->value() + .getStringView(), + "abcdefg"); + upstream_request_->encodeHeaders(default_response_headers_, true); ASSERT_TRUE(response->waitForEndStream()); @@ -212,23 +318,29 @@ TEST_F(StatefulSessionIntegrationTest, NormalStatefulSession) { EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // The selected upstream server address would be selected to the response headers. - if (use_proto) { - EXPECT_EQ(ProtoCookieObject(response->headers() - .get(Http::LowerCaseString("set-cookie"))[0] - ->value() - .getStringView()), - ProtoCookieObject(address_string, 120, "/test", "HttpOnly")); - } else { - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - Http::CookieAttributeRefVector cookie_attributes; - EXPECT_EQ( - Envoy::Http::Utility::makeSetCookieValue("global-session-cookie", encoded_address, - "/test", std::chrono::seconds(120), true, - cookie_attributes), - response->headers().get(Http::LowerCaseString("set-cookie"))[0]->value().getStringView()); - } + // No response header to be added because we will only packages the exist session header + // in the response. If the session header is not exist, we will not add the session header. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); + cleanupUpstreamAndDownstream(); + } + // Upstream endpoint encoded in stateful session header points to unknown server address. + { + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"session-header", fmt::format("{};UV:{}", Envoy::Base64::encode("127.0.0.7:50000"), + Envoy::Base64::encode("abcdefg"))}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ( + "503", + response->headers().get(Http::LowerCaseString(":status"))[0]->value().getStringView()); + cleanupUpstreamAndDownstream(); } } @@ -273,33 +385,162 @@ TEST_F(StatefulSessionIntegrationTest, NormalStatefulSessionHeader) { TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionCookie) { initializeFilterAndRoute(STATEFUL_SESSION_FILTER, ""); - // Run the test twice. Once with proto cookie encoding and once with "old", non-proto - // encoding. - for (const bool use_proto : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - use_proto); - { + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(1, endpoint); + // The upstream with index 1 should be selected. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 1); + + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + + cleanupUpstreamAndDownstream(); + } + + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(2, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + + // The upstream with index 2 should be selected. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 2); + + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + + cleanupUpstreamAndDownstream(); + } - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); + // Test the case that stateful session cookie with unknown server address. + { + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string("127.0.0.1:50000")); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(upstream_index.value(), endpoint); + address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(ProtoCookieObject(response->headers() + .get(Http::LowerCaseString("set-cookie"))[0] + ->value() + .getStringView()), + ProtoCookieObject(address_string, 120, "/test", "HttpOnly")); + cleanupUpstreamAndDownstream(); + } + // Test verifies cookie-based upstream host selection with strict mode. + // When requested upstream host is valid, it should be chosen. + // When requested upstream host is invalid, Envoy should return 503. + TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionCookieStrict) { + initializeFilterAndRoute(STATEFUL_SESSION_FILTER + STATEFUL_SESSION_STRICT_MODE, ""); + // When request does not contain a cookie, cluster should select endpoint using an LB and cookie + // should be returned. In other words, it should work normally even when 'strict' mode is + // enabled. + { + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + waitForNextUpstreamRequest({0, 1, 2, 3}); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // set-cookie header should be added. + EXPECT_FALSE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + cleanupUpstreamAndDownstream(); + } + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + envoy::Cookie cookie; + std::string cookie_string; + cookie.set_address(std::string(fmt::format( + "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&cookie_string); + std::string encoded_address = + Envoy::Base64::encode(cookie_string.data(), cookie_string.size()); codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, @@ -307,46 +548,32 @@ TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionCooki {":scheme", "http"}, {":authority", "stateful.session.com"}, {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - // The upstream with index 1 should be selected. auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); EXPECT_EQ(upstream_index.value(), 1); - upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // No response header to be added. EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - cleanupUpstreamAndDownstream(); } - { envoy::config::endpoint::v3::LbEndpoint endpoint; setUpstreamAddress(2, endpoint); - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - + envoy::Cookie cookie; + std::string cookie_string; + cookie.set_address(std::string(fmt::format( + "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&cookie_string); + std::string encoded_address = + Envoy::Base64::encode(cookie_string.data(), cookie_string.size()); codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, @@ -354,86 +581,41 @@ TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionCooki {":scheme", "http"}, {":authority", "stateful.session.com"}, {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - // The upstream with index 2 should be selected. auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); EXPECT_EQ(upstream_index.value(), 2); - upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); cleanupUpstreamAndDownstream(); } - - // Test the case that stateful session cookie with unknown server address. + // Upstream endpoint encoded in the cookie points to unknown server address. { - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string("127.0.0.1:50000")); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = "127.0.0.1:50000"; - } - std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); codec_client_ = makeHttpConnection(lookupPort("http")); + envoy::Cookie cookie; + std::string cookie_string; + cookie.set_address(std::string("127.0.0.7:50000")); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&cookie_string); + std::string encoded_address = + Envoy::Base64::encode(cookie_string.data(), cookie_string.size()); Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "stateful.session.com"}, {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(upstream_index.value(), endpoint); - address_string = - fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); - - upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - - if (use_proto) { - EXPECT_EQ(ProtoCookieObject(response->headers() - .get(Http::LowerCaseString("set-cookie"))[0] - ->value() - .getStringView()), - ProtoCookieObject(address_string, 120, "/test", "HttpOnly")); - } else { - encoded_address = Envoy::Base64::encode(address_string.data(), address_string.size()); - Http::CookieAttributeRefVector cookie_attributes; - // The selected upstream server address would be selected to the response headers. - EXPECT_EQ(Envoy::Http::Utility::makeSetCookieValue("global-session-cookie", encoded_address, - "/test", std::chrono::seconds(120), true, - cookie_attributes), - response->headers() - .get(Http::LowerCaseString("set-cookie"))[0] - ->value() - .getStringView()); - } - + EXPECT_EQ("503", response->headers().getStatusValue()); cleanupUpstreamAndDownstream(); } } @@ -511,7 +693,7 @@ TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionHeade cleanupUpstreamAndDownstream(); } - // Test the case that stateful session header with unknown server address. + // Upstream endpoint encoded in stateful session header points to unknown server address. { codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl request_headers{ @@ -550,278 +732,253 @@ TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionHeade } } -TEST_F(StatefulSessionIntegrationTest, StatefulSessionDisabledByRoute) { - initializeFilterAndRoute(STATEFUL_SESSION_FILTER, DISABLE_STATEFUL_SESSION); - - // Run the test twice. Once with proto cookie encoding and once with "old", non-proto - // encoding. - for (const bool use_proto : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - use_proto); - { - uint64_t first_index = 0; - uint64_t second_index = 0; - - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(1, endpoint); - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - - Http::TestRequestHeaderMapImpl request_headers{ - {":method", "GET"}, - {":path", "/test"}, - {":scheme", "http"}, - {":authority", "stateful.session.com"}, - {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - - { - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - first_index = upstream_index.value(); - - upstream_request_->encodeHeaders(default_response_headers_, true); - - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - - cleanupUpstreamAndDownstream(); - } - - { - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - second_index = upstream_index.value(); - - upstream_request_->encodeHeaders(default_response_headers_, true); - - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - - cleanupUpstreamAndDownstream(); - } - - // Choose different upstream servers by default. - EXPECT_NE(first_index, second_index); - } - } +// Test verifies header-based upstream host selection with strict mode. +// When requested upstream host is valid, it should be chosen. +// When requested upstream host is invalid, Envoy should return 503. +TEST_F(StatefulSessionIntegrationTest, DownstreamRequestWithStatefulSessionHeaderStrict) { + initializeFilterAndRoute(STATEFUL_SESSION_HEADER_FILTER + STATEFUL_SESSION_STRICT_MODE, ""); { - uint64_t first_index = 0; - uint64_t second_index = 0; - envoy::config::endpoint::v3::LbEndpoint endpoint; setUpstreamAddress(1, endpoint); const std::string address_string = fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); const std::string encoded_address = Envoy::Base64::encode(address_string.data(), address_string.size()); - + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"session-header", encoded_address}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + // The upstream with index 1 should be selected. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 1); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); + cleanupUpstreamAndDownstream(); + } + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(2, endpoint); + const std::string address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "stateful.session.com"}, {"session-header", encoded_address}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + // The upstream with index 2 should be selected. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 2); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); + cleanupUpstreamAndDownstream(); + } + // Upstream endpoint encoded in stateful session header points to unknown server address. + { + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"session-header", Envoy::Base64::encode("127.0.0.7:50000", 15)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("503", response->headers().getStatusValue()); + cleanupUpstreamAndDownstream(); + } +} +TEST_F(StatefulSessionIntegrationTest, StatefulSessionDisabledByRoute) { + initializeFilterAndRoute(STATEFUL_SESSION_FILTER, DISABLE_STATEFUL_SESSION); + { + uint64_t first_index = 0; + uint64_t second_index = 0; + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; { codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); ASSERT(upstream_index.has_value()); first_index = upstream_index.value(); - upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); - + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); cleanupUpstreamAndDownstream(); } - { codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); ASSERT(upstream_index.has_value()); second_index = upstream_index.value(); - upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); - + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); cleanupUpstreamAndDownstream(); } - // Choose different upstream servers by default. EXPECT_NE(first_index, second_index); } -} - -TEST_F(StatefulSessionIntegrationTest, CookieStatefulSessionOverriddenByRoute) { - initializeFilterAndRoute(STATEFUL_SESSION_FILTER, OVERRIDE_STATEFUL_SESSION); - - // Run the test twice. Once with proto cookie encoding and once with "old", non-proto - // encoding. - for (const bool use_proto : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - use_proto); + { + uint64_t first_index = 0; + uint64_t second_index = 0; + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + const std::string address_string = + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"session-header", encoded_address}}; { - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(1, endpoint); - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - codec_client_ = makeHttpConnection(lookupPort("http")); - Http::TestRequestHeaderMapImpl request_headers{ - {":method", "GET"}, - {":path", "/test"}, - {":scheme", "http"}, - {":authority", "stateful.session.com"}, - {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); ASSERT(upstream_index.has_value()); - - envoy::config::endpoint::v3::LbEndpoint route_endpoint; - setUpstreamAddress(upstream_index.value(), route_endpoint); - const std::string route_address_string = fmt::format( - "127.0.0.1:{}", route_endpoint.endpoint().address().socket_address().port_value()); - + first_index = upstream_index.value(); upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - - if (use_proto) { - EXPECT_EQ(ProtoCookieObject(response->headers() - .get(Http::LowerCaseString("set-cookie"))[0] - ->value() - .getStringView()), - ProtoCookieObject(route_address_string, 120, "/test", "HttpOnly")); - } else { - const std::string route_encoded_address = - Envoy::Base64::encode(route_address_string.data(), route_address_string.size()); - Http::CookieAttributeRefVector cookie_attributes; - EXPECT_EQ(Envoy::Http::Utility::makeSetCookieValue( - "route-session-cookie", route_encoded_address, "/test", - std::chrono::seconds(120), true, cookie_attributes), - response->headers() - .get(Http::LowerCaseString("set-cookie"))[0] - ->value() - .getStringView()); - } - + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); cleanupUpstreamAndDownstream(); } { - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(2, endpoint); - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - codec_client_ = makeHttpConnection(lookupPort("http")); - Http::TestRequestHeaderMapImpl request_headers{ - {":method", "GET"}, - {":path", "/test"}, - {":scheme", "http"}, - {":authority", "stateful.session.com"}, - {"cookie", fmt::format("route-session-cookie=\"{}\"", encoded_address)}}; - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - // Stateful session is overridden and the upstream with index 2 should be selected.. auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - EXPECT_EQ(upstream_index.value(), 2); - + ASSERT(upstream_index.has_value()); + second_index = upstream_index.value(); upstream_request_->encodeHeaders(default_response_headers_, true); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("session-header")).empty()); cleanupUpstreamAndDownstream(); } + // Choose different upstream servers by default. + EXPECT_NE(first_index, second_index); + } +} + +TEST_F(StatefulSessionIntegrationTest, CookieStatefulSessionOverriddenByRoute) { + initializeFilterAndRoute(STATEFUL_SESSION_FILTER, OVERRIDE_STATEFUL_SESSION); + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + envoy::config::endpoint::v3::LbEndpoint route_endpoint; + setUpstreamAddress(upstream_index.value(), route_endpoint); + const std::string route_address_string = fmt::format( + "127.0.0.1:{}", route_endpoint.endpoint().address().socket_address().port_value()); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(ProtoCookieObject(response->headers() + .get(Http::LowerCaseString("set-cookie"))[0] + ->value() + .getStringView()), + ProtoCookieObject(route_address_string, 120, "/test", "HttpOnly")); + cleanupUpstreamAndDownstream(); + } + { + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(2, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("route-session-cookie=\"{}\"", encoded_address)}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + // Stateful session is overridden and the upstream with index 2 should be selected.. + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + EXPECT_EQ(upstream_index.value(), 2); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + cleanupUpstreamAndDownstream(); } } @@ -908,92 +1065,61 @@ TEST_F(StatefulSessionIntegrationTest, HeaderStatefulSessionOverriddenByRoute) { TEST_F(StatefulSessionIntegrationTest, CookieBasedStatefulSessionDisabledByRequestPath) { initializeFilterAndRoute(STATEFUL_SESSION_FILTER, ""); - - // Run the test twice. Once with proto cookie encoding and once with "old", non-proto - // encoding. - for (const bool use_proto : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - use_proto); + { + uint64_t first_index = 0; + uint64_t second_index = 0; + envoy::config::endpoint::v3::LbEndpoint endpoint; + setUpstreamAddress(1, endpoint); + std::string address_string; + envoy::Cookie cookie; + cookie.set_address(std::string( + fmt::format("127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); + cookie.set_expires(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() + + 120); + cookie.SerializeToString(&address_string); + const std::string encoded_address = + Envoy::Base64::encode(address_string.data(), address_string.size()); + // Request path is not start with cookie path which means that the stateful session cookie in + // the request my not generated by current filter. The stateful session will skip processing + // this request. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/path_not_match"}, + {":scheme", "http"}, + {":authority", "stateful.session.com"}, + {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; { - uint64_t first_index = 0; - uint64_t second_index = 0; - - envoy::config::endpoint::v3::LbEndpoint endpoint; - setUpstreamAddress(1, endpoint); - std::string address_string; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address(std::string(fmt::format( - "127.0.0.1:{}", endpoint.endpoint().address().socket_address().port_value()))); - cookie.set_expires(std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() + - 120); - cookie.SerializeToString(&address_string); - } else { - address_string = fmt::format("127.0.0.1:{}", - endpoint.endpoint().address().socket_address().port_value()); - } - const std::string encoded_address = - Envoy::Base64::encode(address_string.data(), address_string.size()); - - // Request path is not start with cookie path which means that the stateful session cookie in - // the request my not generated by current filter. The stateful session will skip processing - // this request. - Http::TestRequestHeaderMapImpl request_headers{ - {":method", "GET"}, - {":path", "/path_not_match"}, - {":scheme", "http"}, - {":authority", "stateful.session.com"}, - {"cookie", fmt::format("global-session-cookie=\"{}\"", encoded_address)}}; - - { - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - first_index = upstream_index.value(); - - upstream_request_->encodeHeaders(default_response_headers_, true); - - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - - cleanupUpstreamAndDownstream(); - } - - { - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto response = codec_client_->makeRequestWithBody(request_headers, 0); - - auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); - ASSERT(upstream_index.has_value()); - second_index = upstream_index.value(); - - upstream_request_->encodeHeaders(default_response_headers_, true); - - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - - // No response header to be added. - EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); - - cleanupUpstreamAndDownstream(); - } - - // Choose different upstream servers by default. - EXPECT_NE(first_index, second_index); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + first_index = upstream_index.value(); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + cleanupUpstreamAndDownstream(); } + { + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(request_headers, 0); + auto upstream_index = waitForNextUpstreamRequest({0, 1, 2, 3}); + ASSERT(upstream_index.has_value()); + second_index = upstream_index.value(); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + // No response header to be added. + EXPECT_TRUE(response->headers().get(Http::LowerCaseString("set-cookie")).empty()); + cleanupUpstreamAndDownstream(); + } + // Choose different upstream servers by default. + EXPECT_NE(first_index, second_index); } { uint64_t first_index = 0; @@ -1068,8 +1194,6 @@ TEST_F(StatefulSessionIntegrationTest, CookieBasedStatefulSessionDisabledByReque // reply is in the new proto format. TEST_F(StatefulSessionIntegrationTest, CookieBasedStatefulSessionBackwardCompatibility) { initializeFilterAndRoute(STATEFUL_SESSION_FILTER, ""); - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - true); std::string address_string = "127.0.0.1:50000"; std::string encoded_address = Envoy::Base64::encode(address_string.data(), address_string.size()); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -1108,8 +1232,6 @@ TEST_F(StatefulSessionIntegrationTest, CookieBasedStatefulSessionBackwardCompati // and replying with a new cookie in response headers. TEST_F(StatefulSessionIntegrationTest, CookieBasedStatefulSessionRejectExpiredCookie) { initializeFilterAndRoute(STATEFUL_SESSION_FILTER, ""); - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", - true); envoy::config::endpoint::v3::LbEndpoint endpoint; setUpstreamAddress(1, endpoint); // Create already expired cookie. diff --git a/test/extensions/filters/http/stateful_session/stateful_session_test.cc b/test/extensions/filters/http/stateful_session/stateful_session_test.cc index 82a5fc1452395..0b42e3724e906 100644 --- a/test/extensions/filters/http/stateful_session/stateful_session_test.cc +++ b/test/extensions/filters/http/stateful_session/stateful_session_test.cc @@ -101,7 +101,9 @@ TEST_F(StatefulSessionTest, NormalSessionStateTest) { EXPECT_CALL(*raw_session_state, upstreamAddress()) .WillOnce(Return(absl::make_optional("1.2.3.4"))); EXPECT_CALL(decoder_callbacks_, setUpstreamOverrideHost(_)) - .WillOnce(testing::Invoke([&](absl::string_view host) { EXPECT_EQ("1.2.3.4", host); })); + .WillOnce(testing::Invoke([&](Upstream::LoadBalancerContext::OverrideHost host) { + EXPECT_EQ("1.2.3.4", host.first); + })); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); @@ -139,7 +141,9 @@ TEST_F(StatefulSessionTest, SessionStateOverrideByRoute) { EXPECT_CALL(*raw_session_state, upstreamAddress()) .WillOnce(Return(absl::make_optional("1.2.3.4"))); EXPECT_CALL(decoder_callbacks_, setUpstreamOverrideHost(_)) - .WillOnce(testing::Invoke([&](absl::string_view host) { EXPECT_EQ("1.2.3.4", host); })); + .WillOnce(testing::Invoke([&](Upstream::LoadBalancerContext::OverrideHost host) { + EXPECT_EQ("1.2.3.4", host.first); + })); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); @@ -181,7 +185,9 @@ TEST_F(StatefulSessionTest, NoUpstreamHost) { EXPECT_CALL(*raw_session_state, upstreamAddress()) .WillOnce(Return(absl::make_optional("1.2.3.4"))); EXPECT_CALL(decoder_callbacks_, setUpstreamOverrideHost(_)) - .WillOnce(testing::Invoke([&](absl::string_view host) { EXPECT_EQ("1.2.3.4", host); })); + .WillOnce(testing::Invoke([&](Upstream::LoadBalancerContext::OverrideHost host) { + EXPECT_EQ("1.2.3.4", host.first); + })); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); diff --git a/test/extensions/http/stateful_session/cookie/cookie_test.cc b/test/extensions/http/stateful_session/cookie/cookie_test.cc index 866edd4fd84ae..1b1880780ca37 100644 --- a/test/extensions/http/stateful_session/cookie/cookie_test.cc +++ b/test/extensions/http/stateful_session/cookie/cookie_test.cc @@ -27,7 +27,6 @@ TEST(CookieBasedSessionStateFactoryTest, EmptyCookieName) { } TEST(CookieBasedSessionStateFactoryTest, SessionStateTest) { - testing::NiceMock mock_host; Event::SimulatedTimeSystem time_simulator; time_simulator.setMonotonicTime(std::chrono::seconds(1000)); @@ -41,35 +40,21 @@ TEST(CookieBasedSessionStateFactoryTest, SessionStateTest) { auto session_state = factory.create(request_headers); EXPECT_EQ(absl::nullopt, session_state->upstreamAddress()); - auto upstream_host = std::make_shared("1.2.3.4", 80); - EXPECT_CALL(mock_host, address()).Times(2).WillRepeatedly(testing::Return(upstream_host)); - // No valid address then update it by set-cookie. - // Run the test twice: once with proto cookie format and once - // with "old" style plain address. - for (bool use_proto : std::vector({true, false})) { - std::string cookie_content; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address("1.2.3.4:80"); - cookie.set_expires(1000); - cookie.SerializeToString(&cookie_content); - } else { - cookie_content = "1.2.3.4:80"; - } - Runtime::maybeSetRuntimeGuard( - "envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", use_proto); - - Envoy::Http::TestResponseHeaderMapImpl response_headers; - // Check the format of the cookie sent back to client. - session_state->onUpdate(mock_host, response_headers); - Envoy::Http::CookieAttributeRefVector cookie_attributes; - EXPECT_EQ(response_headers.get_("set-cookie"), - Envoy::Http::Utility::makeSetCookieValue( - "override_host", - Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length()), "", - std::chrono::seconds(0), true, cookie_attributes)); - } + std::string cookie_content; + envoy::Cookie cookie; + cookie.set_address("1.2.3.4:80"); + // The expiration field is not set in the cookie because TTL is 0 in the config. + cookie.SerializeToString(&cookie_content); + Envoy::Http::TestResponseHeaderMapImpl response_headers; + // Check the format of the cookie sent back to client. + session_state->onUpdate("1.2.3.4:80", response_headers); + Envoy::Http::CookieAttributeRefVector cookie_attributes; + EXPECT_EQ(response_headers.get_("set-cookie"), + Envoy::Http::Utility::makeSetCookieValue( + "override_host", + Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length()), "", + std::chrono::seconds(0), true, cookie_attributes)); } { @@ -85,56 +70,32 @@ TEST(CookieBasedSessionStateFactoryTest, SessionStateTest) { // Repeat, but cluster routed to different host 2.3.4.5:80. "set-cookie" should be added to // response headers. - // Run the test twice - once with PROTO style cookie and once with "old" style plain address. - for (bool use_proto : std::vector({true, false})) { - std::string cookie_content; - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address("1.2.3.4:80"); - cookie.set_expires(1005); - cookie.SerializeToString(&cookie_content); - } else { - cookie_content = "1.2.3.4:80"; - } - Runtime::maybeSetRuntimeGuard( - "envoy.reloadable_features.stateful_session_encode_ttl_in_cookie", use_proto); - Envoy::Http::TestRequestHeaderMapImpl request_headers = { - {":path", "/path"}, - {"cookie", "override_host=" + - Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length())}}; - auto session_state = factory.create(request_headers); - EXPECT_EQ("1.2.3.4:80", session_state->upstreamAddress().value()); - - auto upstream_host = std::make_shared("1.2.3.4", 80); - EXPECT_CALL(mock_host, address()).WillOnce(testing::Return(upstream_host)); - - Envoy::Http::TestResponseHeaderMapImpl response_headers; - session_state->onUpdate(mock_host, response_headers); - - // Session state is not updated and then do nothing. - EXPECT_EQ(response_headers.get_("set-cookie"), ""); - - auto upstream_host_2 = std::make_shared("2.3.4.5", 80); - EXPECT_CALL(mock_host, address()).WillOnce(testing::Return(upstream_host_2)); - - session_state->onUpdate(mock_host, response_headers); - - // Update session state because the current request is routed to a new upstream host. - if (use_proto) { - envoy::Cookie cookie; - cookie.set_address("2.3.4.5:80"); - cookie.set_expires(1005); - cookie.SerializeToString(&cookie_content); - } else { - cookie_content = "2.3.4.5:80"; - } - Envoy::Http::CookieAttributeRefVector cookie_attributes; - EXPECT_EQ(response_headers.get_("set-cookie"), - Envoy::Http::Utility::makeSetCookieValue( - "override_host", - Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length()), "/path", - std::chrono::seconds(5), true, cookie_attributes)); - } + std::string cookie_content; + envoy::Cookie cookie; + cookie.set_address("1.2.3.4:80"); + cookie.set_expires(1005); + cookie.SerializeToString(&cookie_content); + Envoy::Http::TestRequestHeaderMapImpl request_headers = { + {":path", "/path"}, + {"cookie", "override_host=" + + Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length())}}; + auto session_state = factory.create(request_headers); + EXPECT_EQ("1.2.3.4:80", session_state->upstreamAddress().value()); + Envoy::Http::TestResponseHeaderMapImpl response_headers; + session_state->onUpdate("1.2.3.4:80", response_headers); + // Session state is not updated and then do nothing. + EXPECT_EQ(response_headers.get_("set-cookie"), ""); + session_state->onUpdate("2.3.4.5:80", response_headers); + // Update session state because the current request is routed to a new upstream host. + cookie.set_address("2.3.4.5:80"); + cookie.set_expires(1005); + cookie.SerializeToString(&cookie_content); + Envoy::Http::CookieAttributeRefVector cookie_attributes; + EXPECT_EQ(response_headers.get_("set-cookie"), + Envoy::Http::Utility::makeSetCookieValue( + "override_host", + Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length()), "/path", + std::chrono::seconds(5), true, cookie_attributes)); } { CookieBasedSessionStateProto config; @@ -176,13 +137,13 @@ TEST(CookieBasedSessionStateFactoryTest, SessionStateProtoCookie) { EXPECT_EQ(absl::nullopt, session_state->upstreamAddress()); // PROTO format - no "expired field" - cookie.set_expires(0); + cookie.set_expires(); cookie.SerializeToString(&cookie_content); request_headers = {{":path", "/path"}, {"cookie", "override_host=" + Envoy::Base64::encode(cookie_content.c_str(), cookie_content.length())}}; session_state = factory.create(request_headers); - EXPECT_EQ(absl::nullopt, session_state->upstreamAddress()); + EXPECT_EQ("2.3.4.5:80", session_state->upstreamAddress().value()); // PROTO format - pass incorrect format. // The content should be treated as "old" style encoding. diff --git a/test/extensions/http/stateful_session/envelope/BUILD b/test/extensions/http/stateful_session/envelope/BUILD new file mode 100644 index 0000000000000..188587f2d855b --- /dev/null +++ b/test/extensions/http/stateful_session/envelope/BUILD @@ -0,0 +1,36 @@ +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 = "envelope_test", + srcs = ["envelope_test.cc"], + extension_names = ["envoy.http.stateful_session.envelope"], + deps = [ + "//source/common/http:utility_lib", + "//source/extensions/http/stateful_session/envelope:envelope_lib", + "//test/mocks/upstream:host_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.http.stateful_session.envelope"], + deps = [ + "//envoy/registry", + "//source/extensions/http/stateful_session/envelope:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/http/stateful_session/envelope/config_test.cc b/test/extensions/http/stateful_session/envelope/config_test.cc new file mode 100644 index 0000000000000..1c55e41d57777 --- /dev/null +++ b/test/extensions/http/stateful_session/envelope/config_test.cc @@ -0,0 +1,29 @@ +#include "source/extensions/http/stateful_session/envelope/config.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" +#include "gtest/gtest.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +namespace { +TEST(EnvelopeSessionStateFactoryConfigTest, Basic) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.http.stateful_session.envelope"); + ASSERT_NE(factory, nullptr); + EnvelopeSessionStateProto proto_config; + const std::string yaml = R"EOF( + header: + name: custom-header + )EOF"; + TestUtility::loadFromYaml(yaml, proto_config); + NiceMock context; + EXPECT_NE(factory->createSessionStateFactory(proto_config, context), nullptr); +} +} // namespace +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/stateful_session/envelope/envelope_test.cc b/test/extensions/http/stateful_session/envelope/envelope_test.cc new file mode 100644 index 0000000000000..c06a103a8bf07 --- /dev/null +++ b/test/extensions/http/stateful_session/envelope/envelope_test.cc @@ -0,0 +1,73 @@ +#include "source/extensions/http/stateful_session/envelope/envelope.h" +#include "test/test_common/utility.h" +#include "gtest/gtest.h" +namespace Envoy { +namespace Extensions { +namespace Http { +namespace StatefulSession { +namespace Envelope { +namespace { +TEST(EnvelopeSessionStateFactoryTest, EnvelopeSessionStateTest) { + { + EnvelopeSessionStateProto config; + config.mutable_header()->set_name("session-header"); + EnvelopeSessionStateFactory factory(config); + // No session header in the request headers. + Envoy::Http::TestRequestHeaderMapImpl request_headers; + auto session_state = factory.create(request_headers); + EXPECT_EQ(absl::nullopt, session_state->upstreamAddress()); + // Empty session header in the request headers. + request_headers.addCopy("session-header", ""); + auto session_state2 = factory.create(request_headers); + EXPECT_EQ(absl::nullopt, session_state2->upstreamAddress()); + Envoy::Http::TestResponseHeaderMapImpl response_headers; + session_state->onUpdate("1.2.3.4:80", response_headers); + // Do nothing because no original session header in the response. + EXPECT_EQ(response_headers.get_("session-header"), ""); + Envoy::Http::TestResponseHeaderMapImpl response_headers2{ + {":status", "200"}, {"session-header", "abcdefg"}, {"session-header", "highklm"}}; + session_state->onUpdate("1.2.3.4:80", response_headers2); + // Do nothing because multiple session headers in the response. + EXPECT_EQ(response_headers2.get(Envoy::Http::LowerCaseString("session-header"))[0]->value(), + "abcdefg"); + EXPECT_EQ(response_headers2.get(Envoy::Http::LowerCaseString("session-header"))[1]->value(), + "highklm"); + Envoy::Http::TestResponseHeaderMapImpl response_headers3{{":status", "200"}, + {"session-header", "abcdefg"}}; + session_state->onUpdate("1.2.3.4:80", response_headers3); + // Update session state because the current request is routed to a new upstream host. + EXPECT_EQ(response_headers3.get_("session-header"), + Envoy::Base64::encode("1.2.3.4:80") + ";UV:" + Envoy::Base64::encode("abcdefg")); + } + { + EnvelopeSessionStateProto config; + config.mutable_header()->set_name("session-header"); + EnvelopeSessionStateFactory factory(config); + // Get upstream address from request headers. + Envoy::Http::TestRequestHeaderMapImpl request_headers = { + {":path", "/path"}, {"session-header", Envoy::Base64::encode("1.2.3.4:80")}}; + // No origin part in the request headers. + auto session_state = factory.create(request_headers); + EXPECT_FALSE(session_state->upstreamAddress().has_value()); + Envoy::Http::TestRequestHeaderMapImpl request_headers2 = { + {":path", "/path"}, {"session-header", Envoy::Base64::encode("1.2.3.4:80") + ";UV:"}}; + // No valid origin part in the request headers. + auto session_state2 = factory.create(request_headers2); + EXPECT_FALSE(session_state2->upstreamAddress().has_value()); + EXPECT_EQ(request_headers2.get_("session-header"), + Envoy::Base64::encode("1.2.3.4:80") + ";UV:"); + Envoy::Http::TestRequestHeaderMapImpl request_headers3 = { + {":path", "/path"}, + {"session-header", + Envoy::Base64::encode("1.2.3.4:80") + ";UV:" + Envoy::Base64::encode("abcdefg")}}; + auto session_state3 = factory.create(request_headers3); + EXPECT_EQ(session_state3->upstreamAddress().value(), "1.2.3.4:80"); + EXPECT_EQ(request_headers3.get_("session-header"), "abcdefg"); + } +} +} // namespace +} // namespace Envelope +} // namespace StatefulSession +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/stateful_session/header/header_test.cc b/test/extensions/http/stateful_session/header/header_test.cc index 7a7ac3ed78b7f..e6909c9bcfb95 100644 --- a/test/extensions/http/stateful_session/header/header_test.cc +++ b/test/extensions/http/stateful_session/header/header_test.cc @@ -1,9 +1,5 @@ -#include - -#include "source/common/network/address_impl.h" #include "source/extensions/http/stateful_session/header/header.h" -#include "test/mocks/upstream/host.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -23,8 +19,6 @@ TEST(HeaderBasedSessionStateFactoryTest, EmptyHeaderName) { } TEST(HeaderBasedSessionStateFactoryTest, SessionStateTest) { - testing::NiceMock mock_host; - { HeaderBasedSessionStateProto config; config.set_name("session-header"); @@ -35,14 +29,11 @@ TEST(HeaderBasedSessionStateFactoryTest, SessionStateTest) { auto session_state = factory.create(request_headers); EXPECT_EQ(absl::nullopt, session_state->upstreamAddress()); - auto upstream_host = std::make_shared("1.2.3.4", 80); - EXPECT_CALL(mock_host, address()).WillOnce(testing::Return(upstream_host)); - Envoy::Http::TestResponseHeaderMapImpl response_headers; - session_state->onUpdate(mock_host, response_headers); + session_state->onUpdate("1.2.3.4:80", response_headers); // No valid address then update it in the headers - EXPECT_EQ(response_headers.get_("session-header"), Envoy::Base64::encode("1.2.3.4:80", 10)); + EXPECT_EQ(response_headers.get_("session-header"), Envoy::Base64::encode("1.2.3.4:80")); } { @@ -57,19 +48,13 @@ TEST(HeaderBasedSessionStateFactoryTest, SessionStateTest) { auto session_state = factory.create(request_headers); EXPECT_EQ("1.2.3.4:80", session_state->upstreamAddress().value()); - auto upstream_host = std::make_shared("1.2.3.4", 80); - EXPECT_CALL(mock_host, address()).WillOnce(testing::Return(upstream_host)); - Envoy::Http::TestResponseHeaderMapImpl response_headers; - session_state->onUpdate(mock_host, response_headers); + session_state->onUpdate("1.2.3.4:80", response_headers); // Session state is not updated so expect no header in response EXPECT_EQ(response_headers.get_("session-header"), ""); - - auto upstream_host_2 = std::make_shared("2.3.4.5", 80); - EXPECT_CALL(mock_host, address()).WillOnce(testing::Return(upstream_host_2)); - - session_state->onUpdate(mock_host, response_headers); + + session_state->onUpdate("2.3.4.5:80", response_headers); // Update session state because the current request is routed to a new upstream host. EXPECT_EQ(response_headers.get_("session-header"), Envoy::Base64::encode("2.3.4.5:80", 10)); diff --git a/test/mocks/http/mocks.cc b/test/mocks/http/mocks.cc index 3c4337d590375..b9a0e5c466338 100644 --- a/test/mocks/http/mocks.cc +++ b/test/mocks/http/mocks.cc @@ -112,7 +112,8 @@ MockStreamDecoderFilterCallbacks::MockStreamDecoderFilterCallbacks() { })); ON_CALL(*this, routeConfig()) .WillByDefault(Return(absl::optional())); - ON_CALL(*this, upstreamOverrideHost()).WillByDefault(Return(absl::optional())); + ON_CALL(*this, upstreamOverrideHost()) + .WillByDefault(Return(absl::optional())); ON_CALL(*this, mostSpecificPerFilterConfig()) .WillByDefault(Invoke([this]() -> const Router::RouteSpecificFilterConfig* { diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index 41743445b1337..ee8831d9e6493 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -60,9 +60,9 @@ class MockFilterManagerCallbacks : public FilterManagerCallbacks { MOCK_METHOD(void, encode1xxHeaders, (ResponseHeaderMap&)); MOCK_METHOD(void, encodeData, (Buffer::Instance&, bool)); MOCK_METHOD(void, encodeTrailers, (ResponseTrailerMap&)); - MOCK_METHOD(void, encodeMetadata, (MetadataMapPtr &&)); + MOCK_METHOD(void, encodeMetadata, (MetadataMapPtr&&)); MOCK_METHOD(void, chargeStats, (const ResponseHeaderMap&)); - MOCK_METHOD(void, setRequestTrailers, (RequestTrailerMapPtr &&)); + MOCK_METHOD(void, setRequestTrailers, (RequestTrailerMapPtr&&)); MOCK_METHOD(void, setInformationalHeaders_, (ResponseHeaderMap&)); void setInformationalHeaders(ResponseHeaderMapPtr&& informational_headers) override { informational_headers_ = std::move(informational_headers); @@ -322,8 +322,9 @@ class MockStreamDecoderFilterCallbacks : public StreamDecoderFilterCallbacks, const absl::optional grpc_status, absl::string_view details)); MOCK_METHOD(Buffer::BufferMemoryAccountSharedPtr, account, (), (const)); - MOCK_METHOD(void, setUpstreamOverrideHost, (absl::string_view host)); - MOCK_METHOD(absl::optional, upstreamOverrideHost, (), (const)); + MOCK_METHOD(void, setUpstreamOverrideHost, (Upstream::LoadBalancerContext::OverrideHost)); + MOCK_METHOD(absl::optional, upstreamOverrideHost, (), + (const)); Buffer::InstancePtr buffer_; std::list callbacks_{}; @@ -373,7 +374,7 @@ class MockStreamEncoderFilterCallbacks : public StreamEncoderFilterCallbacks, MOCK_METHOD(void, addEncodedData, (Buffer::Instance & data, bool streaming)); MOCK_METHOD(void, injectEncodedDataToFilterChain, (Buffer::Instance & data, bool end_stream)); MOCK_METHOD(ResponseTrailerMap&, addEncodedTrailers, ()); - MOCK_METHOD(void, addEncodedMetadata, (Http::MetadataMapPtr &&)); + MOCK_METHOD(void, addEncodedMetadata, (Http::MetadataMapPtr&&)); MOCK_METHOD(void, continueEncoding, ()); MOCK_METHOD(const Buffer::Instance*, encodingBuffer, ()); MOCK_METHOD(void, modifyEncodingBuffer, (std::function)); diff --git a/test/mocks/http/stateful_session.h b/test/mocks/http/stateful_session.h index c89ea4156e85c..b056a2366f4a4 100644 --- a/test/mocks/http/stateful_session.h +++ b/test/mocks/http/stateful_session.h @@ -11,15 +11,15 @@ namespace Http { class MockSessionState : public SessionState { public: MOCK_METHOD(absl::optional, upstreamAddress, (), (const)); - MOCK_METHOD(void, onUpdate, - (const Upstream::HostDescription& host, Http::ResponseHeaderMap& headers)); + MOCK_METHOD(void, onUpdate, (absl::string_view host_address, Http::ResponseHeaderMap& headers)); }; class MockSessionStateFactory : public Http::SessionStateFactory { public: MockSessionStateFactory(); - MOCK_METHOD(Http::SessionStatePtr, create, (const Http::RequestHeaderMap& headers), (const)); + MOCK_METHOD(Http::SessionStatePtr, create, (Http::RequestHeaderMap & headers), (const)); + MOCK_METHOD(bool, isStrict, (), (const)); }; class MockSessionStateFactoryConfig : public Http::SessionStateFactoryConfig { From 0c03177aff537941d667fbd437d4ec0ad11d6824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B5=E9=93=B6?= Date: Sun, 24 Aug 2025 20:20:07 +0800 Subject: [PATCH 2/2] format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 爵银 --- source/common/common/base64.h | 2 +- source/extensions/http/stateful_session/envelope/BUILD | 8 ++++---- .../extensions/http/stateful_session/envelope/envelope.h | 2 ++ source/extensions/http/stateful_session/header/header.cc | 2 +- test/common/upstream/host_utility_test.cc | 6 +++--- test/extensions/http/stateful_session/envelope/BUILD | 8 ++++---- .../http/stateful_session/header/header_test.cc | 2 +- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/source/common/common/base64.h b/source/common/common/base64.h index a941642cc50c6..a0294abc479bd 100644 --- a/source/common/common/base64.h +++ b/source/common/common/base64.h @@ -33,7 +33,7 @@ class Base64 { * Base64 encode an input char buffer with a given length. * @param input string to encode. */ - static std::string encode(absl::string_view input); + static std::string encode(absl::string_view input); /** * Base64 encode an input char buffer with a given length. diff --git a/source/extensions/http/stateful_session/envelope/BUILD b/source/extensions/http/stateful_session/envelope/BUILD index 5fe2d5187b4e8..674785748491c 100644 --- a/source/extensions/http/stateful_session/envelope/BUILD +++ b/source/extensions/http/stateful_session/envelope/BUILD @@ -4,11 +4,11 @@ load( "envoy_cc_library", "envoy_extension_package", ) - + licenses(["notice"]) # Apache 2 - + envoy_extension_package() - + envoy_cc_library( name = "envelope_lib", srcs = ["envelope.cc"], @@ -21,7 +21,7 @@ envoy_cc_library( "@envoy_api//envoy/extensions/http/stateful_session/envelope/v3:pkg_cc_proto", ], ) - + envoy_cc_extension( name = "config", srcs = ["config.cc"], diff --git a/source/extensions/http/stateful_session/envelope/envelope.h b/source/extensions/http/stateful_session/envelope/envelope.h index 31ffa82b7270d..62d1cf9cadbbd 100644 --- a/source/extensions/http/stateful_session/envelope/envelope.h +++ b/source/extensions/http/stateful_session/envelope/envelope.h @@ -23,6 +23,7 @@ class EnvelopeSessionStateFactory : public Envoy::Http::SessionStateFactory, : upstream_address_(std::move(address)), factory_(factory) {} absl::optional upstreamAddress() const override { return upstream_address_; } void onUpdate(absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) override; + private: absl::optional upstream_address_; const EnvelopeSessionStateFactory& factory_; @@ -31,6 +32,7 @@ class EnvelopeSessionStateFactory : public Envoy::Http::SessionStateFactory, Envoy::Http::SessionStatePtr create(Envoy::Http::RequestHeaderMap& headers) const override { return std::make_unique(parseAddress(headers), *this); } + private: absl::optional parseAddress(Envoy::Http::RequestHeaderMap& headers) const; const Envoy::Http::LowerCaseString name_; diff --git a/source/extensions/http/stateful_session/header/header.cc b/source/extensions/http/stateful_session/header/header.cc index e42f8d70c0fd4..ee681638c9894 100644 --- a/source/extensions/http/stateful_session/header/header.cc +++ b/source/extensions/http/stateful_session/header/header.cc @@ -7,7 +7,7 @@ namespace StatefulSession { namespace Header { void HeaderBasedSessionStateFactory::SessionStateImpl::onUpdate( - absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) { + absl::string_view host_address, Envoy::Http::ResponseHeaderMap& headers) { if (!upstream_address_.has_value() || host_address != upstream_address_.value()) { const std::string encoded_address = Envoy::Base64::encode(host_address.data(), host_address.length()); diff --git a/test/common/upstream/host_utility_test.cc b/test/common/upstream/host_utility_test.cc index ef89c242ed38c..50533e752da08 100644 --- a/test/common/upstream/host_utility_test.cc +++ b/test/common/upstream/host_utility_test.cc @@ -336,7 +336,7 @@ TEST(HostUtilityTest, SelectOverrideHostTestRuntimeFlagFlase) { } { // The host map does not contain the expected host. - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", false}; EXPECT_CALL(context, overrideHostToSelect()) .WillOnce(Return(absl::make_optional(override_host))); auto host_map = std::make_shared(); @@ -347,7 +347,7 @@ TEST(HostUtilityTest, SelectOverrideHostTestRuntimeFlagFlase) { auto mock_host = std::make_shared>(); EXPECT_CALL(*mock_host, coarseHealth()).WillOnce(Return(Host::Health::Unhealthy)); - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", false}; EXPECT_CALL(context, overrideHostToSelect()) .WillOnce(Return(absl::make_optional(override_host))); @@ -360,7 +360,7 @@ TEST(HostUtilityTest, SelectOverrideHostTestRuntimeFlagFlase) { auto mock_host = std::make_shared>(); EXPECT_CALL(*mock_host, coarseHealth()).WillOnce(Return(Host::Health::Degraded)); - LoadBalancerContext::OverrideHost override_host{"1.2.3.4"}; + LoadBalancerContext::OverrideHost override_host{"1.2.3.4", false}; EXPECT_CALL(context, overrideHostToSelect()) .WillOnce(Return(absl::make_optional(override_host))); diff --git a/test/extensions/http/stateful_session/envelope/BUILD b/test/extensions/http/stateful_session/envelope/BUILD index 188587f2d855b..825bcfdf21cef 100644 --- a/test/extensions/http/stateful_session/envelope/BUILD +++ b/test/extensions/http/stateful_session/envelope/BUILD @@ -6,11 +6,11 @@ load( "//test/extensions:extensions_build_system.bzl", "envoy_extension_cc_test", ) - + licenses(["notice"]) # Apache 2 - + envoy_package() - + envoy_extension_cc_test( name = "envelope_test", srcs = ["envelope_test.cc"], @@ -22,7 +22,7 @@ envoy_extension_cc_test( "//test/test_common:utility_lib", ], ) - + envoy_extension_cc_test( name = "config_test", srcs = ["config_test.cc"], diff --git a/test/extensions/http/stateful_session/header/header_test.cc b/test/extensions/http/stateful_session/header/header_test.cc index e6909c9bcfb95..db4cdbab88201 100644 --- a/test/extensions/http/stateful_session/header/header_test.cc +++ b/test/extensions/http/stateful_session/header/header_test.cc @@ -53,7 +53,7 @@ TEST(HeaderBasedSessionStateFactoryTest, SessionStateTest) { // Session state is not updated so expect no header in response EXPECT_EQ(response_headers.get_("session-header"), ""); - + session_state->onUpdate("2.3.4.5:80", response_headers); // Update session state because the current request is routed to a new upstream host.