diff --git a/source/common/http/headers.h b/source/common/http/headers.h index 1c0ffc6ed2816..868845ef02485 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -117,6 +117,7 @@ class HeaderValues { const LowerCaseString EnvoyDecoratorOperation{absl::StrCat(prefix(), "-decorator-operation")}; const LowerCaseString Etag{"etag"}; const LowerCaseString Expect{"expect"}; + const LowerCaseString Expires{"expires"}; const LowerCaseString ForwardedClientCert{"x-forwarded-client-cert"}; const LowerCaseString ForwardedFor{"x-forwarded-for"}; const LowerCaseString ForwardedHost{"x-forwarded-host"}; diff --git a/source/extensions/filters/http/cache/BUILD b/source/extensions/filters/http/cache/BUILD index 327fc73eb02fa..b44f80833e9ad 100644 --- a/source/extensions/filters/http/cache/BUILD +++ b/source/extensions/filters/http/cache/BUILD @@ -6,10 +6,34 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", "envoy_package", + "envoy_proto_library", ) envoy_package() +envoy_proto_library( + name = "key", + srcs = ["key.proto"], +) + +envoy_cc_library( + name = "http_cache_lib", + srcs = ["http_cache.cc"], + hdrs = ["http_cache.h"], + deps = [ + ":http_cache_utils_lib", + ":key_cc_proto", + "//include/envoy/buffer:buffer_interface", + "//include/envoy/common:time_interface", + "//include/envoy/config:typed_config_interface", + "//include/envoy/http:codes_interface", + "//include/envoy/http:header_map_interface", + "//source/common/common:assert_lib", + "//source/common/http:headers_lib", + "//source/common/protobuf:utility_lib", + ], +) + envoy_cc_library( name = "http_cache_utils_lib", srcs = ["http_cache_utils.cc"], diff --git a/source/extensions/filters/http/cache/http_cache.cc b/source/extensions/filters/http/cache/http_cache.cc new file mode 100644 index 0000000000000..45a91f56c866e --- /dev/null +++ b/source/extensions/filters/http/cache/http_cache.cc @@ -0,0 +1,160 @@ +#include "extensions/filters/http/cache/http_cache.h" + +#include +#include + +#include "envoy/http/codes.h" + +#include "common/http/headers.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/http/cache/http_cache_utils.h" + +#include "absl/time/time.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { + +std::ostream& operator<<(std::ostream& os, CacheEntryStatus status) { + switch (status) { + case CacheEntryStatus::Ok: + return os << "Ok"; + case CacheEntryStatus::Unusable: + return os << "Unusable"; + case CacheEntryStatus::RequiresValidation: + return os << "RequiresValidation"; + case CacheEntryStatus::FoundNotModified: + return os << "FoundNotModified"; + case CacheEntryStatus::UnsatisfiableRange: + return os << "UnsatisfiableRange"; + } + NOT_REACHED_GCOVR_EXCL_LINE; +} + +std::ostream& operator<<(std::ostream& os, const AdjustedByteRange& range) { + return os << "[" << range.begin() << "," << range.end() << ")"; +} + +LookupRequest::LookupRequest(const Http::HeaderMap& 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 " + "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 " + "with null Host."); + const Http::HeaderString& forwarded_proto = request_headers.ForwardedProto()->value(); + const auto& scheme_values = Http::Headers::get().SchemeValues; + ASSERT(forwarded_proto == scheme_values.Http || forwarded_proto == scheme_values.Https); + // TODO(toddmgreer): Let config determine whether to include forwarded_proto, host, and + // query params. + // TODO(toddmgreer): get cluster name. + // TODO(toddmgreer): Parse Range header into request_range_spec_, and handle the resultant + // vector in CacheFilter::onOkHeaders. + key_.set_cluster_name("cluster_name_goes_here"); + key_.set_host(std::string(request_headers.Host()->value().getStringView())); + key_.set_path(std::string(request_headers.Path()->value().getStringView())); + key_.set_clear_http(forwarded_proto == scheme_values.Http); +} + +// Unless this API is still alpha, calls to stableHashKey() must always return +// the same result, or a way must be provided to deal with a complete cache +// flush. localHashKey however, can be changed at will. +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 { + if (!response_headers.Date()) { + return false; + } + const Http::HeaderEntry* cache_control_header = response_headers.CacheControl(); + if (cache_control_header) { + const SystemTime::duration effective_max_age = + Utils::effectiveMaxAge(cache_control_header->value().getStringView()); + return timestamp_ - Utils::httpTime(response_headers.Date()) < effective_max_age; + } + // We didn't find a cache-control header with enough info to determine + // freshness, so fall back to the expires header. + return timestamp_ <= Utils::httpTime(response_headers.get(Http::Headers::get().Expires)); +} + +LookupResult LookupRequest::makeLookupResult(Http::HeaderMapPtr&& response_headers, + uint64_t content_length) const { + // TODO(toddmgreer): Implement all HTTP caching semantics. + ASSERT(response_headers); + LookupResult result; + result.cache_entry_status_ = + isFresh(*response_headers) ? CacheEntryStatus::Ok : CacheEntryStatus::RequiresValidation; + result.headers_ = std::move(response_headers); + result.content_length_ = content_length; + if (!adjustByteRangeSet(result.response_ranges_, request_range_spec_, content_length)) { + result.headers_->setStatus(static_cast(Http::Code::RangeNotSatisfiable)); + } + result.has_trailers_ = false; + return result; +} + +bool adjustByteRangeSet(std::vector& response_ranges, + const std::vector& request_range_spec, + uint64_t content_length) { + if (request_range_spec.empty()) { + // No range header, so the request can proceed. + return true; + } + + if (content_length == 0) { + // There is a range header, but it's unsatisfiable. + return false; + } + + for (const RawByteRange& spec : request_range_spec) { + if (spec.isSuffix()) { + // spec is a suffix-byte-range-spec + if (spec.suffixLength() == 0) { + // This range is unsatisfiable, so skip it. + continue; + } + if (spec.suffixLength() >= content_length) { + // All bytes are being requested, so we may as well send a '200 + // OK' response. + response_ranges.clear(); + return true; + } + response_ranges.emplace_back(content_length - spec.suffixLength(), content_length); + } else { + // spec is a byte-range-spec + if (spec.firstBytePos() >= content_length) { + // This range is unsatisfiable, so skip it. + continue; + } + if (spec.lastBytePos() >= content_length - 1) { + if (spec.firstBytePos() == 0) { + // All bytes are being requested, so we may as well send a '200 + // OK' response. + response_ranges.clear(); + return true; + } + response_ranges.emplace_back(spec.firstBytePos(), content_length); + } else { + response_ranges.emplace_back(spec.firstBytePos(), spec.lastBytePos() + 1); + } + } + } + if (response_ranges.empty()) { + // All ranges were unsatisfiable. + return false; + } + return true; +} +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/http_cache.h b/source/extensions/filters/http/cache/http_cache.h new file mode 100644 index 0000000000000..46a02884eb8cb --- /dev/null +++ b/source/extensions/filters/http/cache/http_cache.h @@ -0,0 +1,322 @@ +#pragma once + +#include +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/common/time.h" +#include "envoy/config/typed_config.h" +#include "envoy/http/header_map.h" + +#include "common/common/assert.h" + +#include "source/extensions/filters/http/cache/key.pb.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +// Whether a given cache entry is good for the current request. +enum class CacheEntryStatus { + // This entry is fresh, and an appropriate response to the request. + Ok, + // No usable entry was found. If this was generated for a cache entry, the + // cache should delete that entry. + Unusable, + // This entry is stale, but appropriate for validating + RequiresValidation, + // This entry is fresh, and an appropriate basis for a 304 Not Modified + // response. + FoundNotModified, + // This entry is fresh, but can't satisfy the requested range(s). + UnsatisfiableRange, +}; + +std::ostream& operator<<(std::ostream& os, CacheEntryStatus status); + +// Byte range from an HTTP request. +class RawByteRange { +public: + // - If first==UINT64_MAX, construct a RawByteRange requesting the final last body bytes. + // - Otherwise, construct a RawByteRange requesting the [first,last] body bytes. + // Prereq: first == UINT64_MAX || first <= last + // Invariant: isSuffix() || firstBytePos() <= lastBytePos + // Examples: RawByteRange(0,4) requests the first 5 bytes. + // RawByteRange(UINT64_MAX,4) requests the last 4 bytes. + RawByteRange(uint64_t first, uint64_t last) : first_byte_pos_(first), last_byte_pos_(last) { + ASSERT(isSuffix() || first <= last, "Illegal byte range."); + } + bool isSuffix() const { return first_byte_pos_ == UINT64_MAX; } + uint64_t firstBytePos() const { + ASSERT(!isSuffix()); + return first_byte_pos_; + } + uint64_t lastBytePos() const { + ASSERT(!isSuffix()); + return last_byte_pos_; + } + uint64_t suffixLength() const { + ASSERT(isSuffix()); + return last_byte_pos_; + } + +private: + const uint64_t first_byte_pos_; + const uint64_t last_byte_pos_; +}; + +// Byte range from an HTTP request, adjusted for a known response body size, and converted from an +// HTTP-style closed interval to a C++ style half-open interval. +class AdjustedByteRange { +public: + // Construct an AdjustedByteRange representing the [first,last) bytes in the + // response body. Prereq: first <= last Invariant: begin() <= end() + // Example: AdjustedByteRange(0,4) represents the first 4 bytes. + AdjustedByteRange(uint64_t first, uint64_t last) : first_(first), last_(last) { + ASSERT(first < last, "Illegal byte range."); + } + uint64_t begin() const { return first_; } + // Unlike RawByteRange, end() is one past the index of the last offset. + uint64_t end() const { return last_; } + uint64_t length() const { return last_ - first_; } + void trimFront(uint64_t n) { + ASSERT(n <= length(), "Attempt to trim too much from range."); + first_ += n; + } + +private: + uint64_t first_; + const uint64_t last_; +}; + +inline bool operator==(const AdjustedByteRange& lhs, const AdjustedByteRange& rhs) { + return lhs.begin() == rhs.begin() && lhs.end() == rhs.end(); +} + +std::ostream& operator<<(std::ostream& os, const AdjustedByteRange& range); + +// Adjusts request_range_spec to fit a cached response of size content_length, putting the results +// in response_ranges. Returns true if response_ranges is satisfiable (empty is considered +// satisfiable, as it denotes the entire body). +// TODO(toddmgreer): Merge/reorder ranges where appropriate. +bool adjustByteRangeSet(std::vector& response_ranges, + const std::vector& request_range_spec, + uint64_t content_length); + +// Result of a lookup operation, including cached headers and information needed +// to serve a response based on it, or to attempt to validate. +struct LookupResult { + // If cache_entry_status_ == Unusable, none of the other members are + // meaningful. + CacheEntryStatus cache_entry_status_ = CacheEntryStatus::Unusable; + + // Headers of the cached response. + Http::HeaderMapPtr headers_; + + // Size of the full response body. Cache filter will generate a content-length + // header with this value, replacing any preexisting content-length header. + // (This lets us dechunk responses as we insert them, then later serve them + // with a content-length header.) + uint64_t content_length_; + + // Represents the subset of the cached response body that should be served to + // the client. If response_ranges.empty(), the entire body should be served. + // Otherwise, each Range in response_ranges specifies an exact set of bytes to + // serve from the cached response's body. All byte positions in + // response_ranges must be in the range [0,content_length). Caches should + // ensure that they can efficiently serve these ranges, and may merge and/or + // reorder ranges as appropriate, or may clear() response_ranges entirely. + std::vector response_ranges_; + + // TODO(toddmgreer): Implement trailer support. + // True if the cached response has trailers. + bool has_trailers_ = false; +}; + +// Produces a hash of key that is consistent across restarts, architectures, +// builds, and configurations. Caches that store persistent entries based on a +// 64-bit hash should (but are not required to) use stableHashKey. Once this API +// leaves alpha, any improvements to stableHashKey that would change its output +// for existing callers is a breaking change. +// +// For non-persistent storage, use MessageUtil, which has no long-term stability +// guarantees. +// +// When providing a cached response, Caches must ensure that the keys (and not +// just their hashes) match. +// +// TODO(toddmgreer): Ensure that stability guarantees above are accurate. +size_t stableHashKey(const Key& key); + +// LookupRequest holds everything about a request that's needed to look for a +// response in a cache, to evaluate whether an entry from a cache is usable, and +// to determine what ranges are needed. +class LookupRequest { +public: + using HeaderVector = std::vector; + + // Prereq: request_headers's Path(), Scheme(), and Host() are non-null. + LookupRequest(const Http::HeaderMap& 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. + const Key& key() const { return key_; } + 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 + // 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. + HeaderVector& vary_headers() { return vary_headers_; } + const HeaderVector& vary_headers() const { return vary_headers_; } + + // Time when this LookupRequest was created (in response to an HTTP request). + SystemTime timestamp() const { return timestamp_; } + + // WARNING: Incomplete--do not use in production (yet). + // Returns a LookupResult suitable for sending to the cache filter's + // LookupHeadersCallback. Specifically, + // - LookupResult::cache_entry_status_ is set according to HTTP cache + // validation logic. + // - LookupResult::headers takes ownership of response_headers. + // - LookupResult::content_length == content_length. + // - LookupResult::response_ranges entries are satisfiable (as documented + // there). + LookupResult makeLookupResult(Http::HeaderMapPtr&& response_headers, + uint64_t content_length) const; + +private: + bool isFresh(const Http::HeaderMap& response_headers) const; + + Key key_; + std::vector request_range_spec_; + SystemTime timestamp_; + HeaderVector vary_headers_; + const std::string request_cache_control_; +}; + +// Statically known information about a cache. +struct CacheInfo { + absl::string_view name_; + bool supports_range_requests_ = false; +}; + +using LookupBodyCallback = std::function; +using LookupHeadersCallback = 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; + + // 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 + // data into cache too fast for the cache to handle, clients should wait for + // the cache to call ready_for_next_chunk() before streaming the next chunk. + // + // The client can abort the streaming insertion by dropping the + // InsertContextPtr. A cache can abort the insertion by passing 'false' into + // ready_for_next_chunk. + virtual void insertBody(const Buffer::Instance& chunk, InsertCallback ready_for_next_chunk, + bool end_stream) PURE; + + // Inserts trailers into the cache. + virtual void insertTrailers(const Http::HeaderMap& trailers) PURE; + + virtual ~InsertContext() = default; +}; +using InsertContextPtr = std::unique_ptr; + +// Lookup context manages the lifetime of a lookup, helping clients to pull data +// from the cache at a pace that works for them. At any time a client can abort +// 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 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 + // ranges have been received, what to request next, and when to stop. A cache + // can report an error, and cause the response to be aborted, by calling cb + // with nullptr. + // + // If a cache happens to load data in chunks of a set size, it may be + // efficient to respond with fewer than the requested number of bytes. For + // example, assuming a 23 byte full-bodied response from a cache that reads in + // absurdly small 10 byte chunks: + // + // getBody requests bytes 0-23 .......... callback with bytes 0-9 + // getBody requests bytes 10-23 .......... callback with bytes 10-19 + // 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. + virtual void getTrailers(LookupTrailersCallback&& cb) PURE; +}; +using LookupContextPtr = std::unique_ptr; + +// Implement this interface to provide a cache implementation for use by +// CacheFilter. +class HttpCache { +public: + // Returns a LookupContextPtr to manage the state of a cache lookup. On a cache + // miss, the returned LookupContext will be given to the insert call (if any). + virtual LookupContextPtr makeLookupContext(LookupRequest&& request) PURE; + + // Returns an InsertContextPtr to manage the state of a cache insertion. + // Responses with a chunked transfer-encoding must be dechunked before + // insertion. + virtual InsertContextPtr makeInsertContext(LookupContextPtr&& lookup_context) PURE; + + // Precondition: lookup_context represents a prior cache lookup that required + // validation. + // + // Update the headers of that cache entry to match response_headers. The cache + // entry's body and trailers (if any) will not be modified. + // + // 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; + + // Returns statically known information about a cache. + virtual CacheInfo cacheInfo() const PURE; + + virtual ~HttpCache() = default; +}; + +// Factory interface for cache implementations to implement and register. +class HttpCacheFactory : public Config::TypedFactory { +public: + // name should be in reverse DNS format, though this is not enforced. + explicit HttpCacheFactory(std::string name) : name_(std::move(name)) {} + std::string name() const override { return name_; } + std::string category() const override { return "http_cache_factory"; } + + // Returns an HttpCache that will remain valid indefinitely (at least as long + // as the calling CacheFilter). + virtual HttpCache& getCache() PURE; + virtual ~HttpCacheFactory() = default; + +private: + const std::string name_; +}; +using HttpCacheFactoryPtr = std::unique_ptr; +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cache/key.proto b/source/extensions/filters/http/cache/key.proto new file mode 100644 index 0000000000000..e81a1b71cd752 --- /dev/null +++ b/source/extensions/filters/http/cache/key.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package Envoy.Extensions.HttpFilters.Cache; + +// Cache key for lookups and inserts. +message Key { + string cluster_name = 1; + string host = 2; + string path = 3; + string query = 4; + // True for http://, false for https://. + bool clear_http = 5; + // Cache implementations can store arbitrary content in these fields; never set by cache filter. + repeated bytes custom_fields = 6; + repeated int64 custom_ints = 7; +}; diff --git a/test/extensions/filters/http/cache/BUILD b/test/extensions/filters/http/cache/BUILD index 449518f7ba59e..cc18f260aafea 100644 --- a/test/extensions/filters/http/cache/BUILD +++ b/test/extensions/filters/http/cache/BUILD @@ -15,3 +15,14 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "http_cache_test", + srcs = ["http_cache_test.cc"], + deps = [ + "//source/extensions/filters/http/cache:http_cache_lib", + "//test/mocks/http:http_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/filters/http/cache/http_cache_test.cc b/test/extensions/filters/http/cache/http_cache_test.cc new file mode 100644 index 0000000000000..5eee698cf0941 --- /dev/null +++ b/test/extensions/filters/http/cache/http_cache_test.cc @@ -0,0 +1,256 @@ +#include "extensions/filters/http/cache/http_cache.h" + +#include "test/mocks/http/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::ContainerEq; +using testing::TestWithParam; +using testing::ValuesIn; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Cache { +TEST(RawByteRangeTest, IsSuffix) { + auto r = RawByteRange(UINT64_MAX, 4); + ASSERT_TRUE(r.isSuffix()); +} + +TEST(RawByteRangeTest, IsNotSuffix) { + auto r = RawByteRange(3, 4); + ASSERT_FALSE(r.isSuffix()); +} + +TEST(RawByteRangeTest, FirstBytePos) { + auto r = RawByteRange(3, 4); + ASSERT_EQ(3, r.firstBytePos()); +} + +TEST(RawByteRangeTest, LastBytePos) { + auto r = RawByteRange(3, 4); + ASSERT_EQ(4, r.lastBytePos()); +} + +TEST(RawByteRangeTest, SuffixLength) { + auto r = RawByteRange(UINT64_MAX, 4); + ASSERT_EQ(4, r.suffixLength()); +} + +TEST(AdjustedByteRangeTest, Length) { + auto a = AdjustedByteRange(3, 6); + ASSERT_EQ(3, a.length()); +} + +TEST(AdjustedByteRangeTest, TrimFront) { + auto a = AdjustedByteRange(3, 6); + a.trimFront(2); + ASSERT_EQ(5, a.begin()); +} + +TEST(AdjustedByteRangeTest, MaxLength) { + auto a = AdjustedByteRange(0, UINT64_MAX); + ASSERT_EQ(UINT64_MAX, a.length()); +} + +TEST(AdjustedByteRangeTest, MaxTrim) { + auto a = AdjustedByteRange(0, UINT64_MAX); + a.trimFront(UINT64_MAX); + ASSERT_EQ(0, a.length()); +} + +class LookupRequestTest : public testing::Test { +protected: + Event::SimulatedTimeSystem time_source_; + SystemTime current_time_ = time_source_.systemTime(); + DateFormatter formatter_{"%a, %d %b %Y %H:%M:%S GMT"}; + Http::TestHeaderMapImpl request_headers_{ + {":path", "/"}, {"x-forwarded-proto", "https"}, {":authority", "example.com"}}; +}; + +LookupResult makeLookupResult(const LookupRequest& lookup_request, + const Http::TestHeaderMapImpl& response_headers, + uint64_t content_length = 0) { + return lookup_request.makeLookupResult( + std::make_unique(response_headers), content_length); +} + +TEST_F(LookupRequestTest, MakeLookupResultNoBody) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers( + {{"date", formatter_.fromTime(current_time_)}, {"cache-control", "public, max-age=3600"}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + ASSERT_EQ(CacheEntryStatus::Ok, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, 0); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +TEST_F(LookupRequestTest, MakeLookupResultBody) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers( + {{"date", formatter_.fromTime(current_time_)}, {"cache-control", "public, max-age=3600"}}); + const uint64_t content_length = 5; + const LookupResult lookup_response = + makeLookupResult(lookup_request, response_headers, content_length); + ASSERT_EQ(CacheEntryStatus::Ok, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, content_length); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +TEST_F(LookupRequestTest, MakeLookupResultNoDate) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers({{"cache-control", "public, max-age=3600"}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + EXPECT_EQ(CacheEntryStatus::RequiresValidation, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, 0); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +TEST_F(LookupRequestTest, PrivateResponse) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers({{"age", "2"}, + {"cache-control", "private, max-age=3600"}, + {"date", formatter_.fromTime(current_time_)}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + + // We must make sure at cache insertion time, private responses must not be + // inserted. However, if the insertion did happen, it would be served at the + // time of lookup. (Nothing should rely on this.) + ASSERT_EQ(CacheEntryStatus::Ok, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, 0); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +TEST_F(LookupRequestTest, Expired) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers( + {{"cache-control", "public, max-age=3600"}, {"date", "Thu, 01 Jan 2019 00:00:00 GMT"}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + + EXPECT_EQ(CacheEntryStatus::RequiresValidation, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, 0); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +TEST_F(LookupRequestTest, ExpiredViaFallbackheader) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers( + {{"expires", formatter_.fromTime(current_time_ - std::chrono::seconds(5))}, + {"date", formatter_.fromTime(current_time_)}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + + EXPECT_EQ(CacheEntryStatus::RequiresValidation, lookup_response.cache_entry_status_); +} + +TEST_F(LookupRequestTest, NotExpiredViaFallbackheader) { + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers( + {{"expires", formatter_.fromTime(current_time_ + std::chrono::seconds(5))}, + {"date", formatter_.fromTime(current_time_)}}); + const LookupResult lookup_response = makeLookupResult(lookup_request, response_headers); + EXPECT_EQ(CacheEntryStatus::Ok, lookup_response.cache_entry_status_); +} + +TEST_F(LookupRequestTest, FullRange) { + request_headers_.addCopy("Range", "0-99"); + const LookupRequest lookup_request(request_headers_, current_time_); + const Http::TestHeaderMapImpl response_headers({{"date", formatter_.fromTime(current_time_)}, + {"cache-control", "public, max-age=3600"}, + {"content-length", "4"}}); + const uint64_t content_length = 4; + const LookupResult lookup_response = + makeLookupResult(lookup_request, response_headers, content_length); + ASSERT_EQ(CacheEntryStatus::Ok, lookup_response.cache_entry_status_); + ASSERT_TRUE(lookup_response.headers_); + EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); + EXPECT_EQ(lookup_response.content_length_, 4); + EXPECT_TRUE(lookup_response.response_ranges_.empty()); + EXPECT_FALSE(lookup_response.has_trailers_); +} + +struct AdjustByteRangeParams { + std::vector request; + std::vector result; + uint64_t content_length; +}; + +AdjustByteRangeParams satisfiable_ranges[] = + // request, result, content_length + { + // Various ways to request the full body. Full responses are signaled by empty result + // vectors. + {{{0, 3}}, {}, 4}, // byte-range-spec, exact + {{{UINT64_MAX, 4}}, {}, 4}, // suffix-byte-range-spec, exact + {{{0, 99}}, {}, 4}, // byte-range-spec, overlong + {{{0, UINT64_MAX}}, {}, 4}, // byte-range-spec, overlong + {{{UINT64_MAX, 5}}, {}, 4}, // suffix-byte-range-spec, overlong + {{{UINT64_MAX, UINT64_MAX - 1}}, {}, 4}, // suffix-byte-range-spec, overlong + {{{UINT64_MAX, UINT64_MAX}}, {}, 4}, // suffix-byte-range-spec, overlong + + // Single bytes + {{{0, 0}}, {{0, 1}}, 4}, + {{{1, 1}}, {{1, 2}}, 4}, + {{{3, 3}}, {{3, 4}}, 4}, + {{{UINT64_MAX, 1}}, {{3, 4}}, 4}, + + // Multiple bytes, starting in the middle + {{{1, 2}}, {{1, 3}}, 4}, // fully in the middle + {{{1, 3}}, {{1, 4}}, 4}, // to the end + {{{2, 21}}, {{2, 4}}, 4}, // overlong + {{{1, UINT64_MAX}}, {{1, 4}}, 4}}; // overlong +// TODO(toddmgreer): Before enabling support for multi-range requests, test it. + +class AdjustByteRangeTest : public TestWithParam {}; + +TEST_P(AdjustByteRangeTest, All) { + std::vector result; + ASSERT_TRUE(adjustByteRangeSet(result, GetParam().request, GetParam().content_length)); + EXPECT_THAT(result, ContainerEq(GetParam().result)); +} + +INSTANTIATE_TEST_SUITE_P(AdjustByteRangeTest, AdjustByteRangeTest, ValuesIn(satisfiable_ranges)); + +class AdjustByteRangeUnsatisfiableTest : public TestWithParam> {}; + +std::vector unsatisfiable_ranges[] = { + {{4, 5}}, + {{4, 9}}, + {{7, UINT64_MAX}}, + {{UINT64_MAX, 0}}, +}; + +TEST_P(AdjustByteRangeUnsatisfiableTest, All) { + std::vector result; + ASSERT_FALSE(adjustByteRangeSet(result, GetParam(), 3)); +} + +INSTANTIATE_TEST_SUITE_P(AdjustByteRangeUnsatisfiableTest, AdjustByteRangeUnsatisfiableTest, + ValuesIn(unsatisfiable_ranges)); + +TEST(AdjustByteRange, NoRangeRequest) { + std::vector result; + ASSERT_TRUE(adjustByteRangeSet(result, {}, 8)); + EXPECT_THAT(result, ContainerEq(std::vector{})); +} + +} // namespace Cache +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index aafa29d7f61f3..986c8072f3a59 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -215,6 +215,7 @@ PostCBs PREBIND PRNG PROT +Prereq QUIC QoS RAII @@ -478,6 +479,8 @@ deallocated deallocating deallocation dec +dechunk +dechunked dechunking decl decls @@ -936,6 +939,7 @@ rver sandboxed sanitization sanitizer +satisfiable scalability sched schemas @@ -1115,6 +1119,7 @@ unreferenced unregister unregisters unresolvable +unsatisfiable unserializable unsetenv unsubscription