LibHTTP: Implement a strict method to extract Cache-Control directives
Our previous implementation was a bit too tolerant of bad header values. For example, extracting a "max-age" from a header value of "abmax-agecd" would have incorrectly parsed successfully. We now find exact (case-insensitive) directive matches. We also handle quoted string values, which may contain important delimeters that we would have previously split on.
This commit is contained in:
parent
6840571cb3
commit
40800fd91e
3 changed files with 127 additions and 27 deletions
|
|
@ -4,34 +4,17 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/GenericLexer.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCrypto/Hash/SHA1.h>
|
||||
#include <LibHTTP/Cache/DiskCache.h>
|
||||
#include <LibHTTP/Cache/Utilities.h>
|
||||
#include <LibHTTP/HTTP.h>
|
||||
#include <LibURL/URL.h>
|
||||
|
||||
namespace HTTP {
|
||||
|
||||
static Optional<StringView> extract_cache_control_directive(StringView cache_control, StringView directive)
|
||||
{
|
||||
Optional<StringView> result;
|
||||
|
||||
cache_control.for_each_split_view(","sv, SplitBehavior::Nothing, [&](StringView candidate) {
|
||||
if (!candidate.contains(directive, CaseSensitivity::CaseInsensitive))
|
||||
return IterationDecision::Continue;
|
||||
|
||||
auto index = candidate.find('=');
|
||||
if (!index.has_value())
|
||||
return IterationDecision::Continue;
|
||||
|
||||
result = candidate.substring_view(*index + 1);
|
||||
return IterationDecision::Break;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9110.html#field.date
|
||||
static Optional<UnixDateTime> parse_http_date(Optional<ByteString const&> date)
|
||||
{
|
||||
|
|
@ -184,8 +167,7 @@ bool is_cacheable(u32 status_code, HeaderList const& headers)
|
|||
// FIXME: must-understand is not implemented.
|
||||
|
||||
// * the no-store cache directive is not present in the response (see Section 5.2.2.5);
|
||||
if (cache_control.has_value()
|
||||
&& cache_control->contains("no-store"sv, CaseSensitivity::CaseInsensitive))
|
||||
if (cache_control.has_value() && contains_cache_control_directive(*cache_control, "no-store"sv))
|
||||
return false;
|
||||
|
||||
// * if the cache is shared: the private response directive is either not present or allows a shared cache to store
|
||||
|
|
@ -213,9 +195,9 @@ bool is_cacheable(u32 status_code, HeaderList const& headers)
|
|||
bool has_max_age = false;
|
||||
|
||||
if (cache_control.has_value()) {
|
||||
has_public = cache_control->contains("public"sv, CaseSensitivity::CaseInsensitive);
|
||||
has_private = cache_control->contains("private"sv, CaseSensitivity::CaseInsensitive);
|
||||
has_max_age = cache_control->contains("max-age"sv, CaseSensitivity::CaseInsensitive);
|
||||
has_public = contains_cache_control_directive(*cache_control, "public"sv);
|
||||
has_private = contains_cache_control_directive(*cache_control, "private"sv);
|
||||
has_max_age = contains_cache_control_directive(*cache_control, "max-age"sv);
|
||||
|
||||
// FIXME: cache extensions that explicitly allow caching are not interpreted.
|
||||
}
|
||||
|
|
@ -340,7 +322,7 @@ AK::Duration calculate_freshness_lifetime(u32 status_code, HeaderList const& hea
|
|||
// been marked as explicitly cacheable (e.g., with a public response directive).
|
||||
if (is_heuristically_cacheable_status(status_code)) {
|
||||
heuristics_allowed = true;
|
||||
} else if (cache_control.has_value() && cache_control->contains("public"sv, CaseSensitivity::CaseInsensitive)) {
|
||||
} else if (cache_control.has_value() && contains_cache_control_directive(*cache_control, "public"sv)) {
|
||||
heuristics_allowed = true;
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +397,7 @@ CacheLifetimeStatus cache_lifetime_status(HeaderList const& headers, AK::Duratio
|
|||
// NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response
|
||||
//
|
||||
// FIXME: Handle the qualified form of the no-cache directive, which may allow us to re-use the response.
|
||||
if (cache_control.has_value() && cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive))
|
||||
if (cache_control.has_value() && contains_cache_control_directive(*cache_control, "no-cache"sv))
|
||||
return revalidation_status(CacheLifetimeStatus::MustRevalidate);
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#expiration.model
|
||||
|
|
@ -436,7 +418,7 @@ CacheLifetimeStatus cache_lifetime_status(HeaderList const& headers, AK::Duratio
|
|||
// https://httpwg.org/specs/rfc9111.html#cache-response-directive.must-revalidate
|
||||
// The must-revalidate response directive indicates that once the response has become stale, a cache MUST NOT reuse
|
||||
// that response to satisfy another request until it has been successfully validated by the origin
|
||||
if (cache_control->contains("must-revalidate"sv, CaseSensitivity::CaseInsensitive))
|
||||
if (contains_cache_control_directive(*cache_control, "must-revalidate"sv))
|
||||
return revalidation_status(CacheLifetimeStatus::MustRevalidate);
|
||||
|
||||
return CacheLifetimeStatus::Expired;
|
||||
|
|
@ -495,6 +477,57 @@ void update_header_fields(HeaderList& stored_headers, HeaderList const& updated_
|
|||
}
|
||||
}
|
||||
|
||||
bool contains_cache_control_directive(StringView cache_control, StringView directive)
|
||||
{
|
||||
return extract_cache_control_directive(cache_control, directive).has_value();
|
||||
}
|
||||
|
||||
// This is a modified version of the "get, decode, and split" algorithm. This version stops at the first match found,
|
||||
// does not un-escape quoted strings, and deals only with ASCII encodings. See:
|
||||
// https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
|
||||
Optional<StringView> extract_cache_control_directive(StringView cache_control, StringView directive)
|
||||
{
|
||||
VERIFY(!directive.is_empty());
|
||||
|
||||
GenericLexer lexer { cache_control };
|
||||
size_t directive_start { 0 };
|
||||
|
||||
while (true) {
|
||||
lexer.consume_until(is_any_of("\","sv));
|
||||
|
||||
if (!lexer.is_eof() && lexer.peek() == '"') {
|
||||
auto quoted_string_start = lexer.tell();
|
||||
lexer.consume_quoted_string('\\');
|
||||
|
||||
// FIXME: We currently bail if we come across an unterminated quoted string. Do other engines behave this
|
||||
// way, or do they try to move on by finding the next comma?
|
||||
if (quoted_string_start == lexer.tell())
|
||||
return {};
|
||||
|
||||
if (!lexer.is_eof())
|
||||
continue;
|
||||
}
|
||||
|
||||
auto name = cache_control.substring_view(directive_start, lexer.tell() - directive_start);
|
||||
StringView value;
|
||||
|
||||
if (auto index = name.find_any_of("=\""sv); index.has_value() && name[*index] == '=') {
|
||||
value = name.substring_view(*index + 1);
|
||||
name = name.substring_view(0, *index);
|
||||
}
|
||||
|
||||
if (name.trim(HTTP_WHITESPACE).equals_ignoring_ascii_case(directive))
|
||||
return value.trim(HTTP_WHITESPACE);
|
||||
if (lexer.is_eof())
|
||||
return {};
|
||||
|
||||
VERIFY(lexer.peek() == ',');
|
||||
lexer.ignore(1);
|
||||
|
||||
directive_start = lexer.tell();
|
||||
}
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#caching.negotiated.responses
|
||||
ByteString normalize_request_vary_header_values(StringView header, HeaderList const& request_headers)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ struct RevalidationAttributes {
|
|||
void store_header_and_trailer_fields(HeaderList&, HeaderList const&);
|
||||
void update_header_fields(HeaderList&, HeaderList const&);
|
||||
|
||||
bool contains_cache_control_directive(StringView cache_control, StringView directive);
|
||||
Optional<StringView> extract_cache_control_directive(StringView cache_control, StringView directive);
|
||||
|
||||
ByteString normalize_request_vary_header_values(StringView header, HeaderList const& request_headers);
|
||||
|
||||
AK::Duration compute_current_time_offset_for_testing(Optional<DiskCache&>, HeaderList const& request_headers);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <AK/GenericLexer.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibHTTP/Cache/Utilities.h>
|
||||
#include <LibHTTP/HTTP.h>
|
||||
|
||||
TEST_CASE(collect_an_http_quoted_string)
|
||||
|
|
@ -61,3 +62,66 @@ TEST_CASE(collect_an_http_quoted_string)
|
|||
EXPECT_EQ(result, "\"abc\""_string);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(extract_cache_control_directive)
|
||||
{
|
||||
EXPECT(!HTTP::contains_cache_control_directive({}, "no-cache"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive(","sv, "no-cache"sv));
|
||||
|
||||
EXPECT(!HTTP::contains_cache_control_directive("no-cache"sv, "no"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("no-cache"sv, "cache"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("no-cache"sv, "no cache"sv));
|
||||
|
||||
EXPECT(!HTTP::contains_cache_control_directive("abno-cache"sv, "no-cache"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("no-cachecd"sv, "no-cache"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("abno-cachecd"sv, "no-cache"sv));
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("no-cache"sv, "no-cache"sv), ""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age = 4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age= 4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age =4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age = 4 , no-cache"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("no-cache , max-age = 4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("s-maxage=4, max-age=5"sv, "max-age"sv), "5"sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("Max-Age=4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("MAX-AGE=4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4"sv, "MAX-AGE"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("No-Cache"sv, "no-cache"sv), ""sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4,"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("no-cache,"sv, "no-cache"sv), ""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("no-cache, "sv, "no-cache"sv), ""sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4, max-age=5"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("no-cache, max-age=4, max-age=5"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4, no-cache"sv, "no-cache"sv), ""sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4\""sv, "max-age"sv), "\"4\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"004\""sv, "max-age"sv), "\"004\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4\", no-cache"sv, "max-age"sv), "\"4\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("foo=\"bar\", max-age=\"4\""sv, "max-age"sv), "\"4\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4,5\", no-cache"sv, "max-age"sv), "\"4,5\""sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4\\5\""sv, "max-age"sv), "\"4\\5\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4\\\"5\""sv, "max-age"sv), "\"4\\\"5\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4\\\\5\""sv, "max-age"sv), "\"4\\\\5\""sv);
|
||||
|
||||
EXPECT(!HTTP::contains_cache_control_directive("max-age\"4\""sv, "max-age"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("max-age=\"4"sv, "max-age"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("foo=\"bar, max-age=4"sv, "max-age"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("\"unterminated"sv, "max-age"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("max-age=\"4, no-cache"sv, "max-age"sv));
|
||||
EXPECT(!HTTP::contains_cache_control_directive("max-age=\"4, no-cache"sv, "no-cache"sv));
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4, no-cache\", foo=bar"sv, "max-age"sv), "\"4, no-cache\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=\"4, no-cache\", foo=bar"sv, "foo"sv), "bar"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("foo=\"bar,baz\", max-age=4"sv, "foo"sv), "\"bar,baz\""sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("foo=\"bar,baz\", max-age=4"sv, "max-age"sv), "4"sv);
|
||||
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive(",,max-age=4"sv, "max-age"sv), "4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age==4"sv, "max-age"sv), "=4"sv);
|
||||
EXPECT_EQ(HTTP::extract_cache_control_directive("max-age=4="sv, "max-age"sv), "4="sv);
|
||||
EXPECT(!HTTP::contains_cache_control_directive("=4"sv, "max-age"sv));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue