diff --git a/source/common/common/logger.h b/source/common/common/logger.h index cc4352ea02133..324d08e476a28 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -27,6 +27,7 @@ namespace Logger { FUNCTION(aws) \ FUNCTION(assert) \ FUNCTION(backtrace) \ + FUNCTION(cache_filter) \ FUNCTION(client) \ FUNCTION(config) \ FUNCTION(connection) \ diff --git a/source/common/http/headers.h b/source/common/http/headers.h index 868845ef02485..90d0fa134edb7 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -59,6 +59,7 @@ class HeaderValues { const LowerCaseString AccessControlExposeHeaders{"access-control-expose-headers"}; const LowerCaseString AccessControlMaxAge{"access-control-max-age"}; const LowerCaseString AccessControlAllowCredentials{"access-control-allow-credentials"}; + const LowerCaseString Age{"age"}; const LowerCaseString Authorization{"authorization"}; const LowerCaseString ProxyAuthenticate{"proxy-authenticate"}; const LowerCaseString ProxyAuthorization{"proxy-authorization"}; @@ -172,6 +173,7 @@ class HeaderValues { const std::string NoCache{"no-cache"}; const std::string NoCacheMaxAge0{"no-cache, max-age=0"}; const std::string NoTransform{"no-transform"}; + const std::string Private{"private"}; } CacheControlValues; struct { diff --git a/source/extensions/filters/http/cache/BUILD b/source/extensions/filters/http/cache/BUILD index aa8b976639b66..03c1c4932fefb 100644 --- a/source/extensions/filters/http/cache/BUILD +++ b/source/extensions/filters/http/cache/BUILD @@ -12,6 +12,21 @@ load( envoy_package() +envoy_cc_library( + name = "cache_filter_lib", + srcs = ["cache_filter.cc"], + hdrs = ["cache_filter.h"], + deps = [ + ":http_cache_lib", + "//source/common/common:logger_lib", + "//source/common/common:macros", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//envoy/extensions/filters/http/cache/v3alpha:pkg_cc_proto", + ], +) + envoy_proto_library( name = "key", srcs = ["key.proto"], @@ -48,6 +63,14 @@ envoy_cc_library( envoy_cc_extension( name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], security_posture = "robust_to_untrusted_downstream_and_upstream", status = "wip", + deps = [ + ":cache_filter_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/cache/v3alpha:pkg_cc_proto", + ], ) diff --git a/source/extensions/filters/http/cache/cache_filter.cc b/source/extensions/filters/http/cache/cache_filter.cc new file mode 100644 index 0000000000000..ad43c577034e4 --- /dev/null +++ b/source/extensions/filters/http/cache/cache_filter.cc @@ -0,0 +1,170 @@ +#include "extensions/filters/http/cache/cache_filter.h" + +#include "common/http/headers.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +bool CacheFilter::isCacheableRequest(Http::RequestHeaderMap& headers) { + const Http::HeaderEntry* method = headers.Method(); + const Http::HeaderEntry* forwarded_proto = headers.ForwardedProto(); + const Http::HeaderValues& header_values = Http::Headers::get(); + // TODO(toddmgreer): Also serve HEAD requests from cache. + // TODO(toddmgreer): Check all the other cache-related headers. + return method && forwarded_proto && headers.Path() && headers.Host() && + (method->value() == header_values.MethodValues.Get) && + (forwarded_proto->value() == header_values.SchemeValues.Http || + forwarded_proto->value() == header_values.SchemeValues.Https); +} + +bool CacheFilter::isCacheableResponse(Http::ResponseHeaderMap& headers) { + const Http::HeaderEntry* cache_control = headers.CacheControl(); + // TODO(toddmgreer): fully check for cacheability. See for example + // https://github.com/apache/incubator-pagespeed-mod/blob/master/pagespeed/kernel/http/caching_headers.h. + if (cache_control) { + return !StringUtil::caseFindToken(cache_control->value().getStringView(), ",", + Http::Headers::get().CacheControlValues.Private); + } + return false; +} + +CacheFilter::CacheFilter(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig&, + const std::string&, Stats::Scope&, TimeSource& time_source, + HttpCache& http_cache) + : time_source_(time_source), cache_(http_cache) {} + +void CacheFilter::onDestroy() { + lookup_ = nullptr; + insert_ = nullptr; +} + +Http::FilterHeadersStatus CacheFilter::decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) { + ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders: {}", *decoder_callbacks_, headers); + if (!end_stream) { + ENVOY_STREAM_LOG( + debug, + "CacheFilter::decodeHeaders ignoring request because it has body and/or trailers: {}", + *decoder_callbacks_, headers); + return Http::FilterHeadersStatus::Continue; + } + if (!isCacheableRequest(headers)) { + ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders ignoring uncacheable request: {}", + *decoder_callbacks_, headers); + return Http::FilterHeadersStatus::Continue; + } + ASSERT(decoder_callbacks_); + lookup_ = cache_.makeLookupContext(LookupRequest(headers, time_source_.systemTime())); + ASSERT(lookup_); + + ENVOY_STREAM_LOG(debug, "CacheFilter::decodeHeaders starting lookup", *decoder_callbacks_); + lookup_->getHeaders([this](LookupResult&& result) { onHeaders(std::move(result)); }); + if (state_ == GetHeadersState::GetHeadersResultUnusable) { + // onHeaders has already been called, and no usable cache entry was found--continue iteration. + return Http::FilterHeadersStatus::Continue; + } + // onHeaders hasn't been called yet--stop iteration to wait for it, and tell it that we stopped + // iteration. + state_ = GetHeadersState::FinishedGetHeadersCall; + return Http::FilterHeadersStatus::StopAllIterationAndWatermark; +} + +Http::FilterHeadersStatus CacheFilter::encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) { + if (lookup_ && isCacheableResponse(headers)) { + ENVOY_STREAM_LOG(debug, "CacheFilter::encodeHeaders inserting headers", *encoder_callbacks_); + insert_ = cache_.makeInsertContext(std::move(lookup_)); + insert_->insertHeaders(headers, end_stream); + } + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus CacheFilter::encodeData(Buffer::Instance& data, bool end_stream) { + if (insert_) { + ENVOY_STREAM_LOG(debug, "CacheFilter::encodeHeaders inserting body", *encoder_callbacks_); + // TODO(toddmgreer): Wait for the cache if necessary. + insert_->insertBody( + data, [](bool) {}, end_stream); + } + return Http::FilterDataStatus::Continue; +} + +void CacheFilter::onHeaders(LookupResult&& result) { + switch (result.cache_entry_status_) { + case CacheEntryStatus::RequiresValidation: + case CacheEntryStatus::FoundNotModified: + case CacheEntryStatus::UnsatisfiableRange: + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // We don't yet return or support these codes. + case CacheEntryStatus::Unusable: + if (state_ == GetHeadersState::FinishedGetHeadersCall) { + // decodeHeader returned Http::FilterHeadersStatus::StopAllIterationAndWatermark--restart it + decoder_callbacks_->continueDecoding(); + } else { + // decodeHeader hasn't yet returned--tell it to return Http::FilterHeadersStatus::Continue. + state_ = GetHeadersState::GetHeadersResultUnusable; + } + return; + case CacheEntryStatus::Ok: + response_has_trailers_ = result.has_trailers_; + const bool end_stream = (result.content_length_ == 0 && !response_has_trailers_); + // TODO(toddmgreer): Calculate age per https://httpwg.org/specs/rfc7234.html#age.calculations + result.headers_->addReferenceKey(Http::Headers::get().Age, 0); + decoder_callbacks_->encodeHeaders(std::move(result.headers_), end_stream); + if (end_stream) { + return; + } + if (result.content_length_ > 0) { + remaining_body_.emplace_back(0, result.content_length_); + getBody(); + } else { + lookup_->getTrailers( + [this](Http::ResponseTrailerMapPtr&& trailers) { onTrailers(std::move(trailers)); }); + } + } +} + +void CacheFilter::getBody() { + ASSERT(!remaining_body_.empty(), "No reason to call getBody when there's no body to get."); + lookup_->getBody(remaining_body_[0], + [this](Buffer::InstancePtr&& body) { onBody(std::move(body)); }); +} + +// TODO(toddmgreer): Handle downstream backpressure. +void CacheFilter::onBody(Buffer::InstancePtr&& body) { + ASSERT(!remaining_body_.empty(), + "CacheFilter doesn't call getBody unless there's more body to get, so this is a " + "bogus callback."); + ASSERT(body, "Cache said it had a body, but isn't giving it to us."); + + const uint64_t bytes_from_cache = body->length(); + if (bytes_from_cache < remaining_body_[0].length()) { + remaining_body_[0].trimFront(bytes_from_cache); + } else if (bytes_from_cache == remaining_body_[0].length()) { + remaining_body_.erase(remaining_body_.begin()); + } else { + ASSERT(false, "Received oversized body from cache."); + decoder_callbacks_->resetStream(); + return; + } + + const bool end_stream = remaining_body_.empty() && !response_has_trailers_; + decoder_callbacks_->encodeData(*body, end_stream); + if (!remaining_body_.empty()) { + getBody(); + } else if (response_has_trailers_) { + lookup_->getTrailers( + [this](Http::ResponseTrailerMapPtr&& trailers) { onTrailers(std::move(trailers)); }); + } +} + +void CacheFilter::onTrailers(Http::ResponseTrailerMapPtr&& trailers) { + decoder_callbacks_->encodeTrailers(std::move(trailers)); +} +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/cache_filter.h b/source/extensions/filters/http/cache/cache_filter.h new file mode 100644 index 0000000000000..212ff8728284f --- /dev/null +++ b/source/extensions/filters/http/cache/cache_filter.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/extensions/filters/http/cache/v3alpha/cache.pb.h" + +#include "common/common/logger.h" + +#include "extensions/filters/http/cache/http_cache.h" +#include "extensions/filters/http/common/pass_through_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +/** + * A filter that caches responses and attempts to satisfy requests from cache. + */ +class CacheFilter : public Http::PassThroughFilter, + public Logger::Loggable { +public: + CacheFilter(const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config, + const std::string& stats_prefix, Stats::Scope& scope, TimeSource& time_source, + HttpCache& http_cache); + // Http::StreamFilterBase + void onDestroy() override; + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus encodeData(Buffer::Instance& buffer, bool end_stream) override; + +private: + void getBody(); + void onHeaders(LookupResult&& result); + void onBody(Buffer::InstancePtr&& body); + void onTrailers(Http::ResponseTrailerMapPtr&& trailers); + + // These don't require private access, but are members per envoy convention. + static bool isCacheableRequest(Http::RequestHeaderMap& headers); + static bool isCacheableResponse(Http::ResponseHeaderMap& headers); + + TimeSource& time_source_; + HttpCache& cache_; + LookupContextPtr lookup_; + InsertContextPtr insert_; + + // Tracks what body bytes still need to be read from the cache. This is + // currently only one Range, but will expand when full range support is added. Initialized by + // onOkHeaders. + std::vector remaining_body_; + + // True if the response has trailers. + // TODO(toddmgreer): cache trailers. + bool response_has_trailers_; + + // Used for coordinating between decodeHeaders and onHeaders. + enum class GetHeadersState { Initial, FinishedGetHeadersCall, GetHeadersResultUnusable }; + GetHeadersState state_ = GetHeadersState::Initial; +}; + +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/config.cc b/source/extensions/filters/http/cache/config.cc new file mode 100644 index 0000000000000..86277189a789b --- /dev/null +++ b/source/extensions/filters/http/cache/config.cc @@ -0,0 +1,34 @@ +#include "extensions/filters/http/cache/config.h" + +#include "extensions/filters/http/cache/cache_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +Http::FilterFactoryCb CacheFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + const std::string type{TypeUtil::typeUrlToDescriptorFullName(config.typed_config().type_url())}; + HttpCacheFactory* const http_cache_factory = + Registry::FactoryRegistry::getFactoryByType(type); + if (http_cache_factory == nullptr) { + throw EnvoyException( + fmt::format("Didn't find a registered implementation for type: '{}'", type)); + } + + return [config, stats_prefix, &context, + http_cache_factory](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared(config, stats_prefix, context.scope(), + context.timeSource(), + http_cache_factory->getCache(config))); + }; +} + +REGISTER_FACTORY(CacheFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/config.h b/source/extensions/filters/http/cache/config.h new file mode 100644 index 0000000000000..656a1d68ca959 --- /dev/null +++ b/source/extensions/filters/http/cache/config.h @@ -0,0 +1,28 @@ +#pragma once + +#include "envoy/extensions/filters/http/cache/v3alpha/cache.pb.h" +#include "envoy/extensions/filters/http/cache/v3alpha/cache.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +class CacheFilterFactory + : public Common::FactoryBase { +public: + CacheFilterFactory() : FactoryBase(HttpFilterNames::get().Cache) {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::cache::v3alpha::CacheConfig& config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/http_cache.cc b/source/extensions/filters/http/cache/http_cache.cc index 45a91f56c866e..7ae556d4891b2 100644 --- a/source/extensions/filters/http/cache/http_cache.cc +++ b/source/extensions/filters/http/cache/http_cache.cc @@ -37,18 +37,19 @@ std::ostream& operator<<(std::ostream& os, const AdjustedByteRange& range) { return os << "[" << range.begin() << "," << range.end() << ")"; } -LookupRequest::LookupRequest(const Http::HeaderMap& request_headers, SystemTime timestamp) +LookupRequest::LookupRequest(const Http::RequestHeaderMap& request_headers, SystemTime timestamp) : timestamp_(timestamp), request_cache_control_(request_headers.CacheControl() == nullptr ? "" : request_headers.CacheControl()->value().getStringView()) { // These ASSERTs check prerequisites. A request without these headers can't be looked up in cache; // CacheFilter doesn't create LookupRequests for such requests. - ASSERT(request_headers.Path(), "Can't form cache lookup key for malformed Http::HeaderMap " + ASSERT(request_headers.Path(), "Can't form cache lookup key for malformed Http::RequestHeaderMap " "with null Path."); - ASSERT(request_headers.ForwardedProto(), - "Can't form cache lookup key for malformed Http::HeaderMap with null ForwardedProto."); - ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::HeaderMap " + ASSERT( + request_headers.ForwardedProto(), + "Can't form cache lookup key for malformed Http::RequestHeaderMap with null ForwardedProto."); + ASSERT(request_headers.Host(), "Can't form cache lookup key for malformed Http::RequestHeaderMap " "with null Host."); const Http::HeaderString& forwarded_proto = request_headers.ForwardedProto()->value(); const auto& scheme_values = Http::Headers::get().SchemeValues; @@ -71,7 +72,7 @@ size_t stableHashKey(const Key& key) { return MessageUtil::hash(key); } size_t localHashKey(const Key& key) { return stableHashKey(key); } // Returns true if response_headers is fresh. -bool LookupRequest::isFresh(const Http::HeaderMap& response_headers) const { +bool LookupRequest::isFresh(const Http::ResponseHeaderMap& response_headers) const { if (!response_headers.Date()) { return false; } @@ -86,7 +87,7 @@ bool LookupRequest::isFresh(const Http::HeaderMap& response_headers) const { return timestamp_ <= Utils::httpTime(response_headers.get(Http::Headers::get().Expires)); } -LookupResult LookupRequest::makeLookupResult(Http::HeaderMapPtr&& response_headers, +LookupResult LookupRequest::makeLookupResult(Http::ResponseHeaderMapPtr&& response_headers, uint64_t content_length) const { // TODO(toddmgreer): Implement all HTTP caching semantics. ASSERT(response_headers); diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h index 9bea3a43ea38c..5c82eeecf13ac 100644 --- a/source/extensions/filters/http/cache/http_cache.h +++ b/source/extensions/filters/http/cache/http_cache.h @@ -90,7 +90,7 @@ class AdjustedByteRange { private: uint64_t first_; - const uint64_t last_; + uint64_t last_; }; inline bool operator==(const AdjustedByteRange& lhs, const AdjustedByteRange& rhs) { @@ -115,7 +115,7 @@ struct LookupResult { CacheEntryStatus cache_entry_status_ = CacheEntryStatus::Unusable; // Headers of the cached response. - Http::HeaderMapPtr headers_; + Http::ResponseHeaderMapPtr headers_; // Size of the full response body. Cache filter will generate a content-length // header with this value, replacing any preexisting content-length header. @@ -160,7 +160,7 @@ class LookupRequest { using HeaderVector = std::vector; // Prereq: request_headers's Path(), Scheme(), and Host() are non-null. - LookupRequest(const Http::HeaderMap& request_headers, SystemTime timestamp); + LookupRequest(const Http::RequestHeaderMap& request_headers, SystemTime timestamp); // Caches may modify the key according to local needs, though care must be // taken to ensure that meaningfully distinct responses have distinct keys. @@ -168,7 +168,7 @@ class LookupRequest { Key& key() { return key_; } // Returns the subset of this request's headers that are listed in - // envoy::config::filter::http::cache::v3::CacheConfig::allowed_vary_headers. If a cache + // envoy::extensions::filters::http::cache::v3alpha::CacheConfig::allowed_vary_headers. If a cache // storage implementation forwards lookup requests to a remote cache server that supports *vary* // headers, that server may need to see these headers. For local implementations, it may be // simpler to instead call makeLookupResult with each potential response. @@ -187,11 +187,11 @@ class LookupRequest { // - LookupResult::content_length == content_length. // - LookupResult::response_ranges entries are satisfiable (as documented // there). - LookupResult makeLookupResult(Http::HeaderMapPtr&& response_headers, + LookupResult makeLookupResult(Http::ResponseHeaderMapPtr&& response_headers, uint64_t content_length) const; private: - bool isFresh(const Http::HeaderMap& response_headers) const; + bool isFresh(const Http::ResponseHeaderMap& response_headers) const; Key key_; std::vector request_range_spec_; @@ -208,14 +208,14 @@ struct CacheInfo { using LookupBodyCallback = std::function; using LookupHeadersCallback = std::function; -using LookupTrailersCallback = std::function; +using LookupTrailersCallback = std::function; using InsertCallback = std::function; // Manages the lifetime of an insertion. class InsertContext { public: // Accepts response_headers for caching. Only called once. - virtual void insertHeaders(const Http::HeaderMap& response_headers, bool end_stream) PURE; + virtual void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) PURE; // The insertion is streamed into the cache in chunks whose size is determined // by the client, but with a pace determined by the cache. To avoid streaming @@ -229,7 +229,7 @@ class InsertContext { bool end_stream) PURE; // Inserts trailers into the cache. - virtual void insertTrailers(const Http::HeaderMap& trailers) PURE; + virtual void insertTrailers(const Http::ResponseTrailerMap& trailers) PURE; virtual ~InsertContext() = default; }; @@ -240,13 +240,12 @@ using InsertContextPtr = std::unique_ptr; // an in-progress lookup by simply dropping the LookupContextPtr. class LookupContext { public: - virtual ~LookupContext() = default; - // Get the headers from the cache. It is a programming error to call this // twice. virtual void getHeaders(LookupHeadersCallback&& cb) PURE; // Reads the next chunk from the cache, calling cb when the chunk is ready. + // The Buffer::InstancePtr passed to cb must not be null. // // The cache must call cb with a range of bytes starting at range.start() and // ending at or before range.end(). Caller is responsible for tracking what @@ -264,9 +263,11 @@ class LookupContext { // getBody requests bytes 20-23 .......... callback with bytes 20-23 virtual void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) PURE; - // Get the trailers from the cache. Only called if LookupResult::has_trailers - // == true. + // Get the trailers from the cache. Only called if LookupResult::has_trailers == true. The + // Http::ResponseTrailerMapPtr passed to cb must not be null. virtual void getTrailers(LookupTrailersCallback&& cb) PURE; + + virtual ~LookupContext() = default; }; using LookupContextPtr = std::unique_ptr; @@ -292,7 +293,7 @@ class HttpCache { // This is called when an expired cache entry is successfully validated, to // update the cache entry. virtual void updateHeaders(LookupContextPtr&& lookup_context, - Http::HeaderMapPtr&& response_headers) PURE; + Http::ResponseHeaderMapPtr&& response_headers) PURE; // Returns statically known information about a cache. virtual CacheInfo cacheInfo() const PURE; diff --git a/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.cc b/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.cc index 68457493a79d6..5eadaa6a36922 100644 --- a/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.cc +++ b/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.cc @@ -49,7 +49,7 @@ class SimpleInsertContext : public InsertContext { SimpleInsertContext(LookupContext& lookup_context, SimpleHttpCache& cache) : key_(dynamic_cast(lookup_context).request().key()), cache_(cache) {} - void insertHeaders(const Http::HeaderMap& response_headers, bool end_stream) override { + void insertHeaders(const Http::ResponseHeaderMap& response_headers, bool end_stream) override { ASSERT(!committed_); response_headers_ = Http::createHeaderMap(response_headers); if (end_stream) { @@ -70,7 +70,7 @@ class SimpleInsertContext : public InsertContext { } } - void insertTrailers(const Http::HeaderMap&) override { + void insertTrailers(const Http::ResponseTrailerMap&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; // TODO(toddmgreer): support trailers } @@ -93,7 +93,7 @@ LookupContextPtr SimpleHttpCache::makeLookupContext(LookupRequest&& request) { } void SimpleHttpCache::updateHeaders(LookupContextPtr&& lookup_context, - Http::HeaderMapPtr&& response_headers) { + Http::ResponseHeaderMapPtr&& response_headers) { ASSERT(lookup_context); ASSERT(response_headers); // TODO(toddmgreer): Support updating headers. @@ -112,7 +112,7 @@ SimpleHttpCache::Entry SimpleHttpCache::lookup(const LookupRequest& request) { iter->second.body_}; } -void SimpleHttpCache::insert(const Key& key, Http::HeaderMapPtr&& response_headers, +void SimpleHttpCache::insert(const Key& key, Http::ResponseHeaderMapPtr&& response_headers, std::string&& body) { absl::WriterMutexLock lock(&mutex_); map_[key] = SimpleHttpCache::Entry{std::move(response_headers), std::move(body)}; diff --git a/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.h b/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.h index 4f53c28ec32af..ba3851142874c 100644 --- a/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.h +++ b/source/extensions/filters/http/cache/simple_http_cache/simple_http_cache.h @@ -17,7 +17,7 @@ namespace Cache { class SimpleHttpCache : public HttpCache { private: struct Entry { - Http::HeaderMapPtr response_headers_; + Http::ResponseHeaderMapPtr response_headers_; std::string body_; }; @@ -26,11 +26,11 @@ class SimpleHttpCache : public HttpCache { LookupContextPtr makeLookupContext(LookupRequest&& request) override; InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override; void updateHeaders(LookupContextPtr&& lookup_context, - Http::HeaderMapPtr&& response_headers) override; + Http::ResponseHeaderMapPtr&& response_headers) override; CacheInfo cacheInfo() const override; Entry lookup(const LookupRequest& request); - void insert(const Key& key, Http::HeaderMapPtr&& response_headers, std::string&& body); + void insert(const Key& key, Http::ResponseHeaderMapPtr&& response_headers, std::string&& body); absl::Mutex mutex_; absl::flat_hash_map map_ GUARDED_BY(mutex_); diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index 32297b0a71a40..e7dc609ef3cf2 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -14,6 +14,8 @@ class HttpFilterNameValues { public: // Buffer filter const std::string Buffer = "envoy.buffer"; + // Cache filter + const std::string Cache = "envoy.filters.http.cache"; // CORS filter const std::string Cors = "envoy.cors"; // CSRF filter diff --git a/test/extensions/filters/http/cache/BUILD b/test/extensions/filters/http/cache/BUILD index d2ac0fb3f74a4..faa47807b25f7 100644 --- a/test/extensions/filters/http/cache/BUILD +++ b/test/extensions/filters/http/cache/BUILD @@ -26,8 +26,50 @@ envoy_extension_cc_test( extension_name = "envoy.filters.http.cache", deps = [ "//source/extensions/filters/http/cache:http_cache_lib", + "//source/extensions/filters/http/cache/simple_http_cache:simple_http_cache_lib", "//test/mocks/http:http_mocks", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", ], ) + +envoy_extension_cc_test( + name = "cache_filter_test", + srcs = ["cache_filter_test.cc"], + extension_name = "envoy.filters.http.cache", + deps = [ + "//source/extensions/filters/http/cache:cache_filter_lib", + "//source/extensions/filters/http/cache/simple_http_cache:simple_http_cache_lib", + "//test/mocks/server:server_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.http.cache", + deps = [ + "//source/extensions/filters/http/cache:config", + "//source/extensions/filters/http/cache/simple_http_cache:simple_http_cache_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "cache_filter_integration_test", + srcs = [ + "cache_filter_integration_test.cc", + ], + extension_name = "envoy.filters.http.cache", + deps = [ + "//source/extensions/filters/http/cache:config", + "//source/extensions/filters/http/cache:http_cache_lib", + "//source/extensions/filters/http/cache/simple_http_cache:simple_http_cache_lib", + "//test/integration:http_protocol_integration_lib", + "//test/test_common:simulated_time_system_lib", + ], +) diff --git a/test/extensions/filters/http/cache/cache_filter_integration_test.cc b/test/extensions/filters/http/cache/cache_filter_integration_test.cc new file mode 100644 index 0000000000000..a645734486467 --- /dev/null +++ b/test/extensions/filters/http/cache/cache_filter_integration_test.cc @@ -0,0 +1,123 @@ +#include "test/integration/http_protocol_integration.h" +#include "test/test_common/simulated_time_system.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +// TODO(toddmgreer): Expand integration test to include age header values, +// expiration, range headers, HEAD requests, trailers, config customizations, +// cache-control headers, and conditional header fields, as they are +// implemented. + +class CacheIntegrationTest : public Event::TestUsingSimulatedTime, + public HttpProtocolIntegrationTest { +public: + void TearDown() override { + cleanupUpstreamAndDownstream(); + HttpProtocolIntegrationTest::TearDown(); + } + + void initializeFilter(const std::string& config) { + config_helper_.addFilter(config); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + } + + const std::string default_config{R"EOF( + name: "envoy.filters.http.cache" + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.cache.v3alpha.CacheConfig" + typed_config: + "@type": "type.googleapis.com/envoy.source.extensions.filters.http.cache.SimpleHttpCacheConfig" + )EOF"}; + DateFormatter formatter_{"%a, %d %b %Y %H:%M:%S GMT"}; +}; + +INSTANTIATE_TEST_SUITE_P(Protocols, CacheIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +TEST_P(CacheIntegrationTest, MissInsertHit) { + // Set system time to cause Envoy's cached formatted time to match time on this thread. + simTime().setSystemTime(std::chrono::hours(1)); + initializeFilter(default_config); + + // Include test name and params in URL to make each test's requests unique. + const Http::TestRequestHeaderMapImpl request_headers = { + {":method", "GET"}, + {":path", absl::StrCat("/", protocolTestParamsToString({GetParam(), 0}))}, + {":scheme", "http"}, + {":authority", "MissInsertHit"}}; + Http::TestResponseHeaderMapImpl response_headers = {{":status", "200"}, + {"date", formatter_.now(simTime())}, + {"cache-control", "public,max-age=3600"}, + {"content-length", "42"}}; + + // Send first request, and get response from upstream. + { + IntegrationStreamDecoderPtr request = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(response_headers, /*end_stream=*/false); + // send 42 'a's + upstream_request_->encodeData(42, true); + // Wait for the response to be read by the codec client. + request->waitForEndStream(); + EXPECT_TRUE(request->complete()); + EXPECT_THAT(request->headers(), IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(request->headers().get(Http::Headers::get().Age), nullptr); + EXPECT_EQ(request->body(), std::string(42, 'a')); + } + + // Send second request, and get response from cache. + IntegrationStreamDecoderPtr request = codec_client_->makeHeaderOnlyRequest(request_headers); + request->waitForEndStream(); + EXPECT_TRUE(request->complete()); + EXPECT_THAT(request->headers(), IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(request->body(), std::string(42, 'a')); + EXPECT_NE(request->headers().get(Http::Headers::get().Age), nullptr); +} + +// Send the same GET request twice with body and trailers twice, then check that the response +// doesn't have an age header, to confirm that it wasn't served from cache. +TEST_P(CacheIntegrationTest, GetRequestWithBodyAndTrailers) { + // Set system time to cause Envoy's cached formatted time to match time on this thread. + simTime().setSystemTime(std::chrono::hours(1)); + initializeFilter(default_config); + + // Include test name and params in URL to make each test's requests unique. + const Http::TestRequestHeaderMapImpl request_headers = { + {":method", "GET"}, + {":path", absl::StrCat("/", protocolTestParamsToString({GetParam(), 0}))}, + {":scheme", "http"}, + {":authority", "MissInsertHit"}}; + Http::TestRequestTrailerMapImpl request_trailers{{"request1", "trailer1"}, + {"request2", "trailer2"}}; + Http::TestResponseHeaderMapImpl response_headers = {{":status", "200"}, + {"date", formatter_.now(simTime())}, + {"cache-control", "public,max-age=3600"}, + {"content-length", "42"}}; + + for (int i = 0; i < 2; ++i) { + auto encoder_decoder = codec_client_->startRequest(request_headers); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + codec_client_->sendData(*request_encoder_, 13, false); + codec_client_->sendTrailers(*request_encoder_, request_trailers); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(response_headers, /*end_stream=*/false); + // send 42 'a's + upstream_request_->encodeData(42, true); + // Wait for the response to be read by the codec client. + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_THAT(response->headers(), IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(response->headers().get(Http::Headers::get().Age), nullptr); + EXPECT_EQ(response->body(), std::string(42, 'a')); + } +} +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/cache_filter_test.cc b/test/extensions/filters/http/cache/cache_filter_test.cc new file mode 100644 index 0000000000000..99d79a1e478a0 --- /dev/null +++ b/test/extensions/filters/http/cache/cache_filter_test.cc @@ -0,0 +1,207 @@ +#include "extensions/filters/http/cache/cache_filter.h" +#include "extensions/filters/http/cache/simple_http_cache/simple_http_cache.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace { + +// Wrapper for SimpleHttpCache that delays the onHeaders callback from getHeaders, for verifying +// that CacheFilter works correctly whether the onHeaders call happens immediately, or after +// getHeaders and decodeHeaders return. +class DelayedCache : public SimpleHttpCache { +public: + // HttpCache + LookupContextPtr makeLookupContext(LookupRequest&& request) override { + return std::make_unique( + SimpleHttpCache::makeLookupContext(std::move(request)), delayed_cb_); + } + InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) override { + return SimpleHttpCache::makeInsertContext( + std::move(dynamic_cast(*lookup_context).context_)); + } + + std::function delayed_cb_; + +private: + class DelayedLookupContext : public LookupContext { + public: + DelayedLookupContext(LookupContextPtr&& context, std::function& delayed_cb) + : context_(std::move(context)), delayed_cb_(delayed_cb) {} + void getHeaders(LookupHeadersCallback&& cb) override { + delayed_cb_ = [this, cb]() mutable { context_->getHeaders(std::move(cb)); }; + } + void getBody(const AdjustedByteRange& range, LookupBodyCallback&& cb) override { + context_->getBody(range, std::move(cb)); + } + void getTrailers(LookupTrailersCallback&& cb) override { context_->getTrailers(std::move(cb)); } + + LookupContextPtr context_; + std::function& delayed_cb_; + }; +}; + +class CacheFilterTest : public ::testing::Test { +protected: + CacheFilter makeFilter(HttpCache& cache) { + CacheFilter filter(config_, /*stats_prefix=*/"", context_.scope(), context_.timeSource(), + cache); + filter.setDecoderFilterCallbacks(decoder_callbacks_); + filter.setEncoderFilterCallbacks(encoder_callbacks_); + return filter; + } + + SimpleHttpCache simple_cache_; + DelayedCache delayed_cache_; + envoy::extensions::filters::http::cache::v3alpha::CacheConfig config_; + NiceMock context_; + Event::SimulatedTimeSystem time_source_; + DateFormatter formatter_{"%a, %d %b %Y %H:%M:%S GMT"}; + Http::TestRequestHeaderMapImpl request_headers_{ + {":path", "/"}, {":method", "GET"}, {"x-forwarded-proto", "https"}}; + Http::TestResponseHeaderMapImpl response_headers_{{":status", "200"}, + {"date", formatter_.now(time_source_)}, + {"cache-control", "public,max-age=3600"}}; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; +}; + +TEST_F(CacheFilterTest, ImmediateHitNoBody) { + request_headers_.setHost("ImmediateHitNoBody"); + ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(ReturnRef(context_.dispatcher_)); + ON_CALL(context_.dispatcher_, post(_)).WillByDefault(::testing::InvokeArgument<0>()); + + { + // Create filter for request 1 + CacheFilter filter = makeFilter(simple_cache_); + + // Decode request 1 header + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), Http::FilterHeadersStatus::Continue); + + // Encode response header + EXPECT_EQ(filter.encodeHeaders(response_headers_, true), Http::FilterHeadersStatus::Continue); + filter.onDestroy(); + } + { + // Create filter for request 2 + CacheFilter filter = makeFilter(simple_cache_); + + // Decode request 2 header + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + HeaderHasValueRef("age", "0")), + true)); + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), + Http::FilterHeadersStatus::StopAllIterationAndWatermark); + ::testing::Mock::VerifyAndClearExpectations(&decoder_callbacks_); + filter.onDestroy(); + } +} + +TEST_F(CacheFilterTest, DelayedHitNoBody) { + request_headers_.setHost("ImmediateHitNoBody"); + ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(ReturnRef(context_.dispatcher_)); + ON_CALL(context_.dispatcher_, post(_)).WillByDefault(::testing::InvokeArgument<0>()); + + { + // Create filter for request 1 + CacheFilter filter = makeFilter(delayed_cache_); + + // Decode request 1 header + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), + Http::FilterHeadersStatus::StopAllIterationAndWatermark); + EXPECT_CALL(decoder_callbacks_, continueDecoding); + delayed_cache_.delayed_cb_(); + ::testing::Mock::VerifyAndClearExpectations(&decoder_callbacks_); + + // Encode response header + EXPECT_EQ(filter.encodeHeaders(response_headers_, true), Http::FilterHeadersStatus::Continue); + filter.onDestroy(); + } + { + // Create filter for request 2 + CacheFilter filter = makeFilter(delayed_cache_); + + // Decode request 2 header + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), + Http::FilterHeadersStatus::StopAllIterationAndWatermark); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + HeaderHasValueRef("age", "0")), + true)); + delayed_cache_.delayed_cb_(); + ::testing::Mock::VerifyAndClearExpectations(&decoder_callbacks_); + filter.onDestroy(); + } +} + +TEST_F(CacheFilterTest, ImmediateHitBody) { + request_headers_.setHost("ImmediateHitBody"); + ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(ReturnRef(context_.dispatcher_)); + ON_CALL(context_.dispatcher_, post(_)).WillByDefault(::testing::InvokeArgument<0>()); + const std::string body = "abc"; + + { + // Create filter for request 1 + CacheFilter filter = makeFilter(simple_cache_); + + // Decode request 1 header + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), Http::FilterHeadersStatus::Continue); + + // Encode response header + Buffer::OwnedImpl buffer(body); + response_headers_.setContentLength(body.size()); + EXPECT_EQ(filter.encodeHeaders(response_headers_, false), Http::FilterHeadersStatus::Continue); + EXPECT_EQ(filter.encodeData(buffer, true), Http::FilterDataStatus::Continue); + filter.onDestroy(); + } + { + // Create filter for request 2 + CacheFilter filter = makeFilter(simple_cache_); + + // Decode request 2 header + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + HeaderHasValueRef("age", "0")), + false)); + EXPECT_CALL( + decoder_callbacks_, + encodeData(testing::Property(&Buffer::Instance::toString, testing::Eq(body)), true)); + EXPECT_EQ(filter.decodeHeaders(request_headers_, true), + Http::FilterHeadersStatus::StopAllIterationAndWatermark); + ::testing::Mock::VerifyAndClearExpectations(&decoder_callbacks_); + filter.onDestroy(); + } +} + +// Send two identical GET requests with bodies. The CacheFilter will just pass everything through. +TEST_F(CacheFilterTest, GetRequestWithBodyAndTrailers) { + request_headers_.setHost("GetRequestWithBodyAndTrailers"); + const std::string body = "abc"; + Buffer::OwnedImpl request_buffer(body); + Http::TestRequestTrailerMapImpl request_trailers; + + for (int i = 0; i < 2; ++i) { + CacheFilter filter = makeFilter(simple_cache_); + + EXPECT_EQ(filter.decodeHeaders(request_headers_, false), Http::FilterHeadersStatus::Continue); + EXPECT_EQ(filter.decodeData(request_buffer, false), Http::FilterDataStatus::Continue); + EXPECT_EQ(filter.decodeTrailers(request_trailers), Http::FilterTrailersStatus::Continue); + + EXPECT_EQ(filter.encodeHeaders(response_headers_, true), Http::FilterHeadersStatus::Continue); + filter.onDestroy(); + } +} + +} // namespace +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/config_test.cc b/test/extensions/filters/http/cache/config_test.cc new file mode 100644 index 0000000000000..c314897c33a4e --- /dev/null +++ b/test/extensions/filters/http/cache/config_test.cc @@ -0,0 +1,49 @@ +#include "source/extensions/filters/http/cache/simple_http_cache/config.pb.h" + +#include "extensions/filters/http/cache/cache_filter.h" +#include "extensions/filters/http/cache/config.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +namespace { + +class CacheFilterFactoryTest : public ::testing::Test { +protected: + envoy::extensions::filters::http::cache::v3alpha::CacheConfig config_; + NiceMock context_; + CacheFilterFactory factory_; + Http::MockFilterChainFactoryCallbacks filter_callback_; +}; + +TEST_F(CacheFilterFactoryTest, Basic) { + config_.mutable_typed_config()->PackFrom( + envoy::source::extensions::filters::http::cache::SimpleHttpCacheConfig()); + Http::FilterFactoryCb cb = factory_.createFilterFactoryFromProto(config_, "stats", context_); + Http::StreamFilterSharedPtr filter; + EXPECT_CALL(filter_callback_, addStreamFilter(_)).WillOnce(::testing::SaveArg<0>(&filter)); + cb(filter_callback_); + ASSERT(filter); + ASSERT(dynamic_cast(filter.get())); +} + +TEST_F(CacheFilterFactoryTest, NoTypedConfig) { + EXPECT_THROW(factory_.createFilterFactoryFromProto(config_, "stats", context_), EnvoyException); +} + +TEST_F(CacheFilterFactoryTest, UnregisteredTypedConfig) { + config_.mutable_typed_config()->PackFrom( + envoy::extensions::filters::http::cache::v3alpha::CacheConfig()); + EXPECT_THROW(factory_.createFilterFactoryFromProto(config_, "stats", context_), EnvoyException); +} +} // namespace +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cache/http_cache_test.cc b/test/extensions/filters/http/cache/http_cache_test.cc index a641b659a73d0..3fd9cf6992939 100644 --- a/test/extensions/filters/http/cache/http_cache_test.cc +++ b/test/extensions/filters/http/cache/http_cache_test.cc @@ -71,10 +71,10 @@ class LookupRequestTest : public testing::Test { }; LookupResult makeLookupResult(const LookupRequest& lookup_request, - const Http::TestHeaderMapImpl& response_headers, + const Http::TestResponseHeaderMapImpl& response_headers, uint64_t content_length = 0) { return lookup_request.makeLookupResult( - std::make_unique(response_headers), content_length); + std::make_unique(response_headers), content_length); } TEST_F(LookupRequestTest, MakeLookupResultNoBody) { diff --git a/test/extensions/filters/http/cache/simple_http_cache/simple_http_cache_test.cc b/test/extensions/filters/http/cache/simple_http_cache/simple_http_cache_test.cc index e45d621abac6b..0196b1ffdff97 100644 --- a/test/extensions/filters/http/cache/simple_http_cache/simple_http_cache_test.cc +++ b/test/extensions/filters/http/cache/simple_http_cache/simple_http_cache_test.cc @@ -36,14 +36,15 @@ class SimpleHttpCacheTest : public testing::Test { } // Inserts a value into the cache. - void insert(LookupContextPtr lookup, const Http::TestHeaderMapImpl& response_headers, + void insert(LookupContextPtr lookup, const Http::TestResponseHeaderMapImpl& response_headers, const absl::string_view response_body) { InsertContextPtr inserter = cache_.makeInsertContext(move(lookup)); inserter->insertHeaders(response_headers, false); inserter->insertBody(Buffer::OwnedImpl(response_body), nullptr, true); } - void insert(absl::string_view request_path, const Http::TestHeaderMapImpl& response_headers, + void insert(absl::string_view request_path, + const Http::TestResponseHeaderMapImpl& response_headers, const absl::string_view response_body) { insert(lookup(request_path), response_headers, response_body); } diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index bc5e73f34f73b..9478a70b89e32 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -413,6 +413,7 @@ builtins bulkstrings bursty bytecode +cacheability callee callsite callsites