LibWeb+LibWebView: Implement the latest cookie draft RFC
Some checks are pending
CI / Lagom (false, NO_FUZZ, ubuntu-22.04, Linux, GNU) (push) Waiting to run
CI / Lagom (false, FUZZ, ubuntu-22.04, Linux, Clang) (push) Waiting to run
CI / Lagom (false, NO_FUZZ, macos-14, macOS, Clang) (push) Waiting to run
CI / Lagom (true, NO_FUZZ, ubuntu-22.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (macos-14, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (ubuntu-22.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Push notes / build (push) Waiting to run

We currently implement the official cookie RFC, which was last updated
in 2011. Unfortunately, web reality conflicts with the RFC. For example,
all of the major browsers allow nameless cookies, which the RFC forbids.

There has since been draft versions of the RFC published to address such
issues. This patch implements the latest draft.

Major differences include:
* Allowing nameless or valueless (but not both) cookies
* Formal cookie length limits
* Formal same-site rules (not fully implemented here)
* More rules around cookie domains
This commit is contained in:
Timothy Flynn 2024-09-16 15:20:01 -04:00 committed by Tim Ledbetter
parent c7db1204ca
commit fce003a8f5
Notes: github-actions[bot] 2024-09-16 23:05:30 +00:00
7 changed files with 548 additions and 176 deletions

View file

@ -1,12 +1,19 @@
Basic test: "cookie=value"
Multiple cookies: "cookie1=value1; cookie2=value2; cookie3=value3"
Nameless cookie: "value"
Valueless cookie: "cookie="
Nameless and valueless cookie: ""
Invalid control character: ""
Non-ASCII domain: ""
Secure cookie prefix: ""
Host cookie prefix: ""
Large value: "cookie=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Overly large value: ""
HTTP only: ""
Public suffix: ""
SameSite=Lax: "cookie=value"
SameSite=Strict: "cookie=value"
SameSite=None: "cookie=value"
SameSite=None: ""
Max-Age (before expiration): "cookie-max-age=value"
Expires (before expiration): "cookie-expires=value; cookie-max-age=value"
Max-Age (after expiration): ""

View file

@ -31,6 +31,47 @@
deleteCookie("cookie3");
};
const namelessCookieTest = () => {
document.cookie = "=value";
printCookies("Nameless cookie");
deleteCookie("");
};
const valuelessCookieTest = () => {
document.cookie = "cookie=";
printCookies("Valueless cookie");
deleteCookie("cookie");
};
const namelessAndValuelessCookieTest = () => {
document.cookie = "=";
printCookies("Nameless and valueless cookie");
};
const invalidControlCharacterTest = () => {
document.cookie = "cookie=ab\ncd";
printCookies("Invalid control character");
};
const nonASCIIDomainTest = () => {
document.cookie = "cookie=value; domain=🤓";
printCookies("Non-ASCII domain");
};
const secureCookiePrefixTest = () => {
document.cookie = "__Secure-cookie=value";
printCookies("Secure cookie prefix");
};
const hostCookiePrefixTest = () => {
document.cookie = "__Host-cookie1=value";
document.cookie = "__Host-cookie2=value; secure";
document.cookie = "__Host-cookie3=value; secure; path=/foo";
printCookies("Host cookie prefix");
};
const largeValueTest = () => {
const value = "x".repeat(256);
@ -41,7 +82,7 @@
};
const overlyLargeValueTest = () => {
const value = "x".repeat(4096 - "cookie=".length + 1);
const value = "x".repeat(4096 - "cookie".length + 1);
document.cookie = `cookie=${value}`;
printCookies("Overly large value");
@ -129,6 +170,15 @@
basicTest();
multipleCookiesTest();
namelessCookieTest();
valuelessCookieTest();
namelessAndValuelessCookieTest();
invalidControlCharacterTest();
nonASCIIDomainTest();
secureCookiePrefixTest();
hostCookiePrefixTest();
largeValueTest();
overlyLargeValueTest();

View file

@ -31,7 +31,7 @@ struct Cookie {
String name;
String value;
SameSite same_site;
SameSite same_site { SameSite::Default };
UnixDateTime creation_time {};
UnixDateTime last_access_time {};
UnixDateTime expiry_time {};

View file

@ -17,8 +17,6 @@
namespace Web::Cookie {
static constexpr size_t s_max_cookie_size = 4096;
static void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attributes);
static void process_attribute(ParsedCookie& parsed_cookie, StringView attribute_name, StringView attribute_value);
static void on_expires_attribute(ParsedCookie& parsed_cookie, StringView attribute_value);
@ -30,49 +28,69 @@ static void on_http_only_attribute(ParsedCookie& parsed_cookie);
static void on_same_site_attribute(ParsedCookie& parsed_cookie, StringView attribute_value);
static Optional<UnixDateTime> parse_date_time(StringView date_string);
bool cookie_contains_invalid_control_character(StringView cookie_string)
{
for (auto code_point : Utf8View { cookie_string }) {
if (code_point <= 0x08)
return true;
if (code_point >= 0x0a && code_point <= 0x1f)
return true;
if (code_point == 0x7f)
return true;
}
return false;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-6
Optional<ParsedCookie> parse_cookie(StringView cookie_string)
{
// https://tools.ietf.org/html/rfc6265#section-5.2
if (cookie_string.length() > s_max_cookie_size)
// 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F character (CTL characters excluding HTAB):
// Abort these steps and ignore the set-cookie-string entirely.
if (cookie_contains_invalid_control_character(cookie_string))
return {};
StringView name_value_pair;
StringView unparsed_attributes;
// 1. If the set-cookie-string contains a %x3B (";") character:
// 2. If the set-cookie-string contains a %x3B (";") character:
if (auto position = cookie_string.find(';'); position.has_value()) {
// The name-value-pair string consists of the characters up to, but not including, the first %x3B (";"), and the unparsed-
// attributes consist of the remainder of the set-cookie-string (including the %x3B (";") in question).
// 1. The name-value-pair string consists of the characters up to, but not including, the first %x3B (";"), and
// the unparsed-attributes consist of the remainder of the set-cookie-string (including the %x3B (";") in
// question).
name_value_pair = cookie_string.substring_view(0, position.value());
unparsed_attributes = cookie_string.substring_view(position.value());
} else {
// The name-value-pair string consists of all the characters contained in the set-cookie-string, and the unparsed-
// attributes is the empty string.
}
// Otherwise:
else {
// 1. The name-value-pair string consists of all the characters contained in the set-cookie-string, and the
// unparsed-attributes is the empty string.
name_value_pair = cookie_string;
}
StringView name;
StringView value;
if (auto position = name_value_pair.find('='); position.has_value()) {
// 3. The (possibly empty) name string consists of the characters up to, but not including, the first %x3D ("=") character, and the
// (possibly empty) value string consists of the characters after the first %x3D ("=") character.
// 3. If the name-value-pair string lacks a %x3D ("=") character, then the name string is empty, and the value
// string is the value of name-value-pair.
if (auto position = name_value_pair.find('='); !position.has_value()) {
value = name_value_pair;
} else {
// Otherwise, the name string consists of the characters up to, but not including, the first %x3D ("=") character
// and the (possibly empty) value string consists of the characters after the first %x3D ("=") character.
name = name_value_pair.substring_view(0, position.value());
if (position.value() < name_value_pair.length() - 1)
value = name_value_pair.substring_view(position.value() + 1);
} else {
// 2. If the name-value-pair string lacks a %x3D ("=") character, ignore the set-cookie-string entirely.
return {};
}
// 4. Remove any leading or trailing WSP characters from the name string and the value string.
name = name.trim_whitespace();
value = value.trim_whitespace();
// 5. If the name string is empty, ignore the set-cookie-string entirely.
if (name.is_empty())
// 5. If the sum of the lengths of the name string and the value string is more than 4096 octets, abort these steps
// and ignore the set-cookie-string entirely.
if (name.length() + value.length() > 4096)
return {};
// 6. The cookie-name is the name string, and the cookie-value is the value string.
@ -82,6 +100,7 @@ Optional<ParsedCookie> parse_cookie(StringView cookie_string)
return parsed_cookie;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6-8
void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attributes)
{
// 1. If the unparsed-attributes string is empty, skip the rest of these steps.
@ -95,28 +114,34 @@ void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attribute
// 3. If the remaining unparsed-attributes contains a %x3B (";") character:
if (auto position = unparsed_attributes.find(';'); position.has_value()) {
// Consume the characters of the unparsed-attributes up to, but not including, the first %x3B (";") character.
// 1. Consume the characters of the unparsed-attributes up to, but not including, the first %x3B (";") character.
cookie_av = unparsed_attributes.substring_view(0, position.value());
unparsed_attributes = unparsed_attributes.substring_view(position.value());
} else {
// Consume the remainder of the unparsed-attributes.
}
// Otherwise:
else {
// 1. Consume the remainder of the unparsed-attributes.
cookie_av = unparsed_attributes;
unparsed_attributes = {};
}
// Let the cookie-av string be the characters consumed in this step.
StringView attribute_name;
StringView attribute_value;
// 4. If the cookie-av string contains a %x3D ("=") character:
if (auto position = cookie_av.find('='); position.has_value()) {
// The (possibly empty) attribute-name string consists of the characters up to, but not including, the first %x3D ("=")
// character, and the (possibly empty) attribute-value string consists of the characters after the first %x3D ("=") character.
// 1. The (possibly empty) attribute-name string consists of the characters up to, but not including, the first
// %x3D ("=") character, and the (possibly empty) attribute-value string consists of the characters after the
// first %x3D ("=") character.
attribute_name = cookie_av.substring_view(0, position.value());
if (position.value() < cookie_av.length() - 1)
attribute_value = cookie_av.substring_view(position.value() + 1);
} else {
// The attribute-name string consists of the entire cookie-av string, and the attribute-value string is empty.
}
// Otherwise:
else {
// 1. The attribute-name string consists of the entire cookie-av string, and the attribute-value string is empty.
attribute_name = cookie_av;
}
@ -124,11 +149,18 @@ void parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attribute
attribute_name = attribute_name.trim_whitespace();
attribute_value = attribute_value.trim_whitespace();
// 6. Process the attribute-name and attribute-value according to the requirements in the following subsections.
// 6. If the attribute-value is longer than 1024 octets, ignore the cookie-av string and return to Step 1 of this
// algorithm.
if (attribute_value.length() > 1024) {
parse_attributes(parsed_cookie, unparsed_attributes);
return;
}
// 7. Process the attribute-name and attribute-value according to the requirements in the following subsections.
// (Notice that attributes with unrecognized attribute-names are ignored.)
process_attribute(parsed_cookie, attribute_name, attribute_value);
// 7. Return to Step 1 of this algorithm.
// 8. Return to Step 1 of this algorithm.
parse_attributes(parsed_cookie, unparsed_attributes);
}
@ -151,92 +183,139 @@ void process_attribute(ParsedCookie& parsed_cookie, StringView attribute_name, S
}
}
static constexpr AK::Duration maximum_cookie_age()
{
return AK::Duration::from_seconds(400LL * 24 * 60 * 60);
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.1
void on_expires_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.1
if (auto expiry_time = parse_date_time(attribute_value); expiry_time.has_value())
parsed_cookie.expiry_time_from_expires_attribute = expiry_time.release_value();
}
// 1. Let the expiry-time be the result of parsing the attribute-value as cookie-date (see Section 5.1.1).
auto expiry_time = parse_date_time(attribute_value);
void on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.2
// If the first character of the attribute-value is not a DIGIT or a "-" character, ignore the cookie-av.
if (attribute_value.is_empty() || (!isdigit(attribute_value[0]) && (attribute_value[0] != '-')))
// 2. If the attribute-value failed to parse as a cookie date, ignore the cookie-av.
if (!expiry_time.has_value())
return;
// Let delta-seconds be the attribute-value converted to an integer.
if (auto delta_seconds = attribute_value.to_number<int>(); delta_seconds.has_value()) {
if (*delta_seconds <= 0) {
// If delta-seconds is less than or equal to zero (0), let expiry-time be the earliest representable date and time.
parsed_cookie.expiry_time_from_max_age_attribute = UnixDateTime::earliest();
} else {
// Otherwise, let the expiry-time be the current date and time plus delta-seconds seconds.
parsed_cookie.expiry_time_from_max_age_attribute = UnixDateTime::now() + AK::Duration::from_seconds(*delta_seconds);
}
}
// 3. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days in the future or sooner, see
// Section 5.5).
auto cookie_age_limit = UnixDateTime::now() + maximum_cookie_age();
// 4. If the expiry-time is more than cookie-age-limit, the user agent MUST set the expiry time to cookie-age-limit
// in seconds.
if (expiry_time->seconds_since_epoch() > cookie_age_limit.seconds_since_epoch())
expiry_time = cookie_age_limit;
// 5. If the expiry-time is earlier than the earliest date the user agent can represent, the user agent MAY replace
// the expiry-time with the earliest representable date.
if (auto earliest = UnixDateTime::earliest(); *expiry_time < earliest)
expiry_time = earliest;
// 6. Append an attribute to the cookie-attribute-list with an attribute-name of Expires and an attribute-value of
// expiry-time.
parsed_cookie.expiry_time_from_expires_attribute = expiry_time.release_value();
}
void on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.2
void on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.3
// If the attribute-value is empty, the behavior is undefined. However, the user agent SHOULD ignore the cookie-av entirely.
// 1. If the attribute-value is empty, ignore the cookie-av.
if (attribute_value.is_empty())
return;
StringView cookie_domain;
// If the first character of the attribute-value string is %x2E ("."):
if (attribute_value[0] == '.') {
// Let cookie-domain be the attribute-value without the leading %x2E (".") character.
cookie_domain = attribute_value.substring_view(1);
} else {
// Let cookie-domain be the entire attribute-value.
cookie_domain = attribute_value;
}
// Convert the cookie-domain to lower case.
parsed_cookie.domain = MUST(Infra::to_ascii_lowercase(cookie_domain));
}
void on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.4
// If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"):
if (attribute_value.is_empty() || attribute_value[0] != '/')
// Let cookie-path be the default-path.
// 2. If the first character of the attribute-value is neither a DIGIT, nor a "-" character followed by a DIGIT,
// ignore the cookie-av.
if (!is_ascii_digit(attribute_value[0]) && attribute_value[0] != '-')
return;
// Let cookie-path be the attribute-value
parsed_cookie.path = MUST(String::from_utf8(attribute_value));
// 3. If the remainder of attribute-value contains a non-DIGIT character, ignore the cookie-av.
// 4. Let delta-seconds be the attribute-value converted to a base 10 integer.
auto delta_seconds = attribute_value.to_number<i64>();
if (!delta_seconds.has_value())
return;
// 5. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days or less, see Section 5.5).
auto cookie_age_limit = maximum_cookie_age();
// 6. Set delta-seconds to the smaller of its present value and cookie-age-limit.
if (*delta_seconds > cookie_age_limit.to_seconds())
delta_seconds = cookie_age_limit.to_seconds();
// 7. If delta-seconds is less than or equal to zero (0), let expiry-time be the earliest representable date and
// time. Otherwise, let the expiry-time be the current date and time plus delta-seconds seconds.
auto expiry_time = *delta_seconds <= 0
? UnixDateTime::earliest()
: UnixDateTime::now() + AK::Duration::from_seconds(*delta_seconds);
// 8. Append an attribute to the cookie-attribute-list with an attribute-name of Max-Age and an attribute-value of
// expiry-time.
parsed_cookie.expiry_time_from_max_age_attribute = expiry_time;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.3
void on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// 1. Let cookie-domain be the attribute-value.
auto cookie_domain = attribute_value;
// 2. If cookie-domain starts with %x2E ("."), let cookie-domain be cookie-domain without its leading %x2E (".").
if (cookie_domain.starts_with('.'))
cookie_domain = cookie_domain.substring_view(1);
// 3. Convert the cookie-domain to lower case.
auto lowercase_cookie_domain = MUST(Infra::to_ascii_lowercase(cookie_domain));
// 4. Append an attribute to the cookie-attribute-list with an attribute-name of Domain and an attribute-value of
// cookie-domain.
parsed_cookie.domain = move(lowercase_cookie_domain);
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.4
void on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// 1. If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"):
if (attribute_value.is_empty() || attribute_value[0] != '/') {
// Let cookie-path be the default-path.
return;
}
// Otherwise:
// 1. Let cookie-path be the attribute-value.
auto cookie_path = attribute_value;
// 2. Append an attribute to the cookie-attribute-list with an attribute-name of Path and an attribute-value of
// cookie-path.
parsed_cookie.path = MUST(String::from_utf8(cookie_path));
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.5
void on_secure_attribute(ParsedCookie& parsed_cookie)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.5
parsed_cookie.secure_attribute_present = true;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.6
void on_http_only_attribute(ParsedCookie& parsed_cookie)
{
// https://tools.ietf.org/html/rfc6265#section-5.2.6
parsed_cookie.http_only_attribute_present = true;
}
// https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-samesite-attribute-2
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.7
void on_same_site_attribute(ParsedCookie& parsed_cookie, StringView attribute_value)
{
// 1. Let enforcement be "Default"
// 1. Let enforcement be "Default".
// 2. If cookie-av's attribute-value is a case-insensitive match for "None", set enforcement to "None".
// 3. If cookie-av's attribute-value is a case-insensitive match for "Strict", set enforcement to "Strict".
// 4. If cookie-av's attribute-value is a case-insensitive match for "Lax", set enforcement to "Lax".
parsed_cookie.same_site_attribute = same_site_from_string(attribute_value);
auto enforcement = same_site_from_string(attribute_value);
// 5. Append an attribute to the cookie-attribute-list with an attribute-name of "SameSite" and an attribute-value
// of enforcement.
parsed_cookie.same_site_attribute = enforcement;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.1
Optional<UnixDateTime> parse_date_time(StringView date_string)
{
// https://tools.ietf.org/html/rfc6265#section-5.1.1
@ -302,20 +381,38 @@ Optional<UnixDateTime> parse_date_time(StringView date_string)
// 1. Using the grammar below, divide the cookie-date into date-tokens.
Vector<StringView> date_tokens = date_string.split_view_if(is_delimiter);
// 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date.
// 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date:
bool found_time = false;
bool found_day_of_month = false;
bool found_month = false;
bool found_year = false;
for (auto const& date_token : date_tokens) {
// 1. If the found-time flag is not set and the token matches the time production, set the found-time flag and
// set the hour-value, minute-value, and second-value to the numbers denoted by the digits in the date-token,
// respectively. Skip the remaining sub-steps and continue to the next date-token.
if (!found_time && parse_time(date_token)) {
found_time = true;
} else if (!found_day_of_month && parse_day_of_month(date_token)) {
}
// 2. If the found-day-of-month flag is not set and the date-token matches the day-of-month production, set the
// found-day-of-month flag and set the day-of-month-value to the number denoted by the date-token. Skip the
// remaining sub-steps and continue to the next date-token.
else if (!found_day_of_month && parse_day_of_month(date_token)) {
found_day_of_month = true;
} else if (!found_month && parse_month(date_token)) {
}
// 3. If the found-month flag is not set and the date-token matches the month production, set the found-month
// flag and set the month-value to the month denoted by the date-token. Skip the remaining sub-steps and
// continue to the next date-token.
else if (!found_month && parse_month(date_token)) {
found_month = true;
} else if (!found_year && parse_year(date_token)) {
}
// 4. If the found-year flag is not set and the date-token matches the year production, set the found-year flag
// and set the year-value to the number denoted by the date-token. Skip the remaining sub-steps and continue
// to the next date-token.
else if (!found_year && parse_year(date_token)) {
found_year = true;
}
}
@ -329,16 +426,22 @@ Optional<UnixDateTime> parse_date_time(StringView date_string)
year += 2000;
// 5. Abort these steps and fail to parse the cookie-date if:
if (!found_time || !found_day_of_month || !found_month || !found_year)
// * at least one of the found-day-of-month, found-month, found-year, or found-time flags is not set,
if (!found_day_of_month || !found_month || !found_year || !found_time)
return {};
// * the day-of-month-value is less than 1 or greater than 31,
if (day_of_month < 1 || day_of_month > 31)
return {};
// * the year-value is less than 1601,
if (year < 1601)
return {};
// * the hour-value is greater than 23,
if (hour > 23)
return {};
// * the minute-value is greater than 59, or
if (minute > 59)
return {};
// * the second-value is greater than 59.
if (second > 59)
return {};

View file

@ -27,6 +27,7 @@ struct ParsedCookie {
};
Optional<ParsedCookie> parse_cookie(StringView cookie_string);
bool cookie_contains_invalid_control_character(StringView);
}

View file

@ -86,6 +86,7 @@ CookieJar::~CookieJar()
m_persisted_storage->synchronization_timer->on_timeout();
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3
String CookieJar::get_cookie(const URL::URL& url, Web::Cookie::Source source)
{
m_transient_storage.purge_expired_cookies();
@ -95,15 +96,23 @@ String CookieJar::get_cookie(const URL::URL& url, Web::Cookie::Source source)
return {};
auto cookie_list = get_matching_cookies(url, domain.value(), source);
// 4. Serialize the cookie-list into a cookie-string by processing each cookie in the cookie-list in order:
StringBuilder builder;
for (auto const& cookie : cookie_list) {
// If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ")
if (!builder.is_empty())
builder.append("; "sv);
// Output the cookie's name, the %x3D ("=") character, and the cookie's value.
builder.appendff("{}={}", cookie.name, cookie.value);
// 1. If the cookies' name is not empty, output the cookie's name followed by the %x3D ("=") character.
if (!cookie.name.is_empty())
builder.appendff("{}=", cookie.name);
// 2. If the cookies' value is not empty, output the cookie's value.
if (!cookie.value.is_empty())
builder.append(cookie.value);
// 3. If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ").
}
return MUST(builder.to_string());
@ -118,22 +127,22 @@ void CookieJar::set_cookie(const URL::URL& url, Web::Cookie::ParsedCookie const&
store_cookie(parsed_cookie, url, domain.release_value(), source);
}
// This is based on https://www.rfc-editor.org/rfc/rfc6265#section-5.3 as store_cookie() below
// however the whole ParsedCookie->Cookie conversion is skipped.
// This is based on store_cookie() below, however the whole ParsedCookie->Cookie conversion is skipped.
void CookieJar::update_cookie(Web::Cookie::Cookie cookie)
{
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
// 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie:
if (auto old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value()) {
// Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
// 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie:
if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) {
// 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie.
cookie.creation_time = old_cookie->creation_time;
// Remove the old-cookie from the cookie store.
// 4. Remove the old-cookie from the cookie store.
// NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
}
// 12. Insert the newly created cookie into the cookie store.
// 24. Insert the newly-created cookie into the cookie store.
m_transient_storage.set_cookie(move(key), move(cookie));
m_transient_storage.purge_expired_cookies();
@ -204,59 +213,62 @@ Optional<Web::Cookie::Cookie> CookieJar::get_named_cookie(URL::URL const& url, S
return {};
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.2
Optional<String> CookieJar::canonicalize_domain(const URL::URL& url)
{
// https://tools.ietf.org/html/rfc6265#section-5.1.2
if (!url.is_valid())
if (!url.is_valid() || url.host().has<Empty>())
return {};
// FIXME: Implement RFC 5890 to "Convert each label that is not a Non-Reserved LDH (NR-LDH) label to an A-label".
if (url.host().has<Empty>())
return {};
// 1. Convert the host name to a sequence of individual domain name labels.
// 2. Convert each label that is not a Non-Reserved LDH (NR-LDH) label, to an A-label (see Section 2.3.2.1 of
// [RFC5890] for the former and latter), or to a "punycode label" (a label resulting from the "ToASCII" conversion
// in Section 4 of [RFC3490]), as appropriate (see Section 6.3 of this specification).
// 3. Concatenate the resulting labels, separated by a %x2E (".") character.
// FIXME: Implement the above conversions.
return MUST(MUST(url.serialized_host()).to_lowercase());
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.3
bool CookieJar::domain_matches(StringView string, StringView domain_string)
{
// https://tools.ietf.org/html/rfc6265#section-5.1.3
// A string domain-matches a given domain string if at least one of the following conditions hold:
// The domain string and the string are identical.
// * The domain string and the string are identical. (Note that both the domain string and the string will have been
// canonicalized to lower case at this point.)
if (string == domain_string)
return true;
// All of the following conditions hold:
// * All of the following conditions hold:
// - The domain string is a suffix of the string.
// - The last character of the string that is not included in the domain string is a %x2E (".") character.
// - The string is a host name (i.e., not an IP address).
if (!string.ends_with(domain_string))
return false;
// - The last character of the string that is not included in the domain string is a %x2E (".") character.
if (string[string.length() - domain_string.length() - 1] != '.')
return false;
// - The string is a host name (i.e., not an IP address).
if (AK::IPv4Address::from_string(string).has_value())
return false;
return true;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.4
bool CookieJar::path_matches(StringView request_path, StringView cookie_path)
{
// https://tools.ietf.org/html/rfc6265#section-5.1.4
// A request-path path-matches a given cookie-path if at least one of the following conditions holds:
// The cookie-path and the request-path are identical.
// * The cookie-path and the request-path are identical.
if (request_path == cookie_path)
return true;
if (request_path.starts_with(cookie_path)) {
// The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/").
// * The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/").
if (cookie_path.ends_with('/'))
return true;
// The cookie-path is a prefix of the request-path, and the first character of the request-path that is not included in the cookie-path is a %x2F ("/") character.
// * The cookie-path is a prefix of the request-path, and the first character of the request-path that is not
// included in the cookie-path is a %x2F ("/") character.
if (request_path[cookie_path.length()] == '/')
return true;
}
@ -264,14 +276,14 @@ bool CookieJar::path_matches(StringView request_path, StringView cookie_path)
return false;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.4
String CookieJar::default_path(const URL::URL& url)
{
// https://tools.ietf.org/html/rfc6265#section-5.1.4
// 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise).
auto uri_path = URL::percent_decode(url.serialize_path());
// 2. If the uri-path is empty or if the first character of the uri-path is not a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
// 2. If the uri-path is empty or if the first character of the uri-path is not a %x2F ("/") character, output
// %x2F ("/") and skip the remaining steps.
if (uri_path.is_empty() || (uri_path[0] != '/'))
return "/"_string;
@ -282,138 +294,325 @@ String CookieJar::default_path(const URL::URL& url)
if (last_separator == 0)
return "/"_string;
// 4. Output the characters of the uri-path from the first character up to, but not including, the right-most %x2F ("/").
// 4. Output the characters of the uri-path from the first character up to, but not including, the right-most
// %x2F ("/").
// FIXME: The path might not be valid UTF-8.
return MUST(String::from_utf8(uri_path.substring_view(0, last_separator)));
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#name-storage-model
void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL::URL& url, String canonicalized_domain, Web::Cookie::Source source)
{
// https://tools.ietf.org/html/rfc6265#section-5.3
// 1. A user agent MAY ignore a received cookie in its entirety. See Section 5.3.
// 2. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time to the current date and time.
Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value, parsed_cookie.same_site_attribute };
// 2. If cookie-name is empty and cookie-value is empty, abort these steps and ignore the cookie entirely.
if (parsed_cookie.name.is_empty() && parsed_cookie.value.is_empty())
return;
// 3. If the cookie-name or the cookie-value contains a %x00-08 / %x0A-1F / %x7F character (CTL characters
// excluding HTAB), abort these steps and ignore the cookie entirely.
if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.name))
return;
if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.value))
return;
// 4. If the sum of the lengths of cookie-name and cookie-value is more than 4096 octets, abort these steps and
// ignore the cookie entirely.
if (parsed_cookie.name.byte_count() + parsed_cookie.value.byte_count() > 4096)
return;
// 5. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time
// to the current date and time.
Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value };
cookie.creation_time = UnixDateTime::now();
cookie.last_access_time = cookie.creation_time;
// 6. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age":
if (parsed_cookie.expiry_time_from_max_age_attribute.has_value()) {
// 3. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age": Set the cookie's persistent-flag to true.
// Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Max-Age".
// 1. Set the cookie's persistent-flag to true.
cookie.persistent = true;
// 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with
// an attribute-name of "Max-Age".
cookie.expiry_time = parsed_cookie.expiry_time_from_max_age_attribute.value();
} else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) {
// If the cookie-attribute-list contains an attribute with an attribute-name of "Expires": Set the cookie's persistent-flag to true.
// Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Expires".
}
// Otherwise, if the cookie-attribute-list contains an attribute with an attribute-name of "Expires" (and does not
// contain an attribute with an attribute-name of "Max-Age"):
else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) {
// 1. Set the cookie's persistent-flag to true.
cookie.persistent = true;
// 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with
// an attribute-name of "Expires".
cookie.expiry_time = parsed_cookie.expiry_time_from_expires_attribute.value();
} else {
// Set the cookie's persistent-flag to false. Set the cookie's expiry-time to the latest representable date.
}
// Otherwise:
else {
// 1. Set the cookie's persistent-flag to false.
cookie.persistent = false;
// 2. Set the cookie's expiry-time to the latest representable date.
cookie.expiry_time = UnixDateTime::from_unix_time_parts(3000, 1, 1, 0, 0, 0, 0);
}
// 4. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
String domain_attribute;
// 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
if (parsed_cookie.domain.has_value()) {
// Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Domain".
cookie.domain = parsed_cookie.domain.value();
// 1. Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with
// both an attribute-name of "Domain" and an attribute-value whose length is no more than 1024 octets. (Note
// that a leading %x2E ("."), if present, is ignored even though that character is not permitted.)
if (parsed_cookie.domain->byte_count() <= 1024)
domain_attribute = parsed_cookie.domain.value();
}
// Otherwise:
else {
// 1. Let the domain-attribute be the empty string.
}
// 5. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix:
if (is_public_suffix(cookie.domain)) {
// If the domain-attribute is identical to the canonicalized request-host:
if (cookie.domain == canonicalized_domain) {
// Let the domain-attribute be the empty string.
cookie.domain = String {};
// 8. If the domain-attribute contains a character that is not in the range of [USASCII] characters, abort these
// steps and ignore the cookie entirely.
for (auto code_point : domain_attribute.code_points()) {
if (!is_ascii(code_point))
return;
}
// 9. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix:
if (is_public_suffix(domain_attribute)) {
// 1. If the domain-attribute is identical to the canonicalized request-host:
if (domain_attribute == canonicalized_domain) {
// 1. Let the domain-attribute be the empty string.
domain_attribute = String {};
}
// Otherwise:
else {
// Ignore the cookie entirely and abort these steps.
// 1. Abort these steps and ignore the cookie entirely.
return;
}
}
// 6. If the domain-attribute is non-empty:
if (!cookie.domain.is_empty()) {
// If the canonicalized request-host does not domain-match the domain-attribute: Ignore the cookie entirely and abort these steps.
if (!domain_matches(canonicalized_domain, cookie.domain))
// 10. If the domain-attribute is non-empty:
if (!domain_attribute.is_empty()) {
// 1. If the canonicalized request-host does not domain-match the domain-attribute:
if (!domain_matches(canonicalized_domain, domain_attribute)) {
// 1. Abort these steps and ignore the cookie entirely.
return;
}
// Otherwise:
else {
// 1. Set the cookie's host-only-flag to false.
cookie.host_only = false;
// Set the cookie's host-only-flag to false. Set the cookie's domain to the domain-attribute.
cookie.host_only = false;
} else {
// Set the cookie's host-only-flag to true. Set the cookie's domain to the canonicalized request-host.
// 2. Set the cookie's domain to the domain-attribute.
cookie.domain = move(domain_attribute);
}
}
// Otherwise:
else {
// 1. Set the cookie's host-only-flag to true.
cookie.host_only = true;
// 2. Set the cookie's domain to the canonicalized request-host.
cookie.domain = move(canonicalized_domain);
}
// 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Path":
// 11. If the cookie-attribute-list contains an attribute with an attribute-name of "Path", set the cookie's path to
// attribute-value of the last attribute in the cookie-attribute-list with both an attribute-name of "Path" and
// an attribute-value whose length is no more than 1024 octets. Otherwise, set the cookie's path to the
// default-path of the request-uri.
if (parsed_cookie.path.has_value()) {
// Set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path".
cookie.path = parsed_cookie.path.value();
if (parsed_cookie.path->byte_count() <= 1024)
cookie.path = parsed_cookie.path.value();
} else {
cookie.path = default_path(url);
}
// 8. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's secure-only-flag to true.
// 12. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's
// secure-only-flag to true. Otherwise, set the cookie's secure-only-flag to false.
cookie.secure = parsed_cookie.secure_attribute_present;
// 9. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's http-only-flag to false.
// 13. If the request-uri does not denote a "secure" connection (as defined by the user agent), and the cookie's
// secure-only-flag is true, then abort these steps and ignore the cookie entirely.
if (cookie.secure && url.scheme() != "https"sv)
return;
// 14. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's
// http-only-flag to true. Otherwise, set the cookie's http-only-flag to false.
cookie.http_only = parsed_cookie.http_only_attribute_present;
// 10. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is set, abort these steps and ignore the cookie entirely.
if (source != Web::Cookie::Source::Http && cookie.http_only)
// 15. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is true, abort these steps
// and ignore the cookie entirely.
if (source == Web::Cookie::Source::NonHttp && cookie.http_only)
return;
// 16. If the cookie's secure-only-flag is false, and the request-uri does not denote a "secure" connection, then
// abort these steps and ignore the cookie entirely if the cookie store contains one or more cookies that meet
// all of the following criteria:
if (!cookie.secure && url.scheme() != "https"sv) {
auto ignore_cookie = false;
m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie const& old_cookie) {
// 1. Their name matches the name of the newly-created cookie.
if (old_cookie.name != cookie.name)
return IterationDecision::Continue;
// 2. Their secure-only-flag is true.
if (!old_cookie.secure)
return IterationDecision::Continue;
// 3. Their domain domain-matches the domain of the newly-created cookie, or vice-versa.
if (!domain_matches(old_cookie.domain, cookie.domain) && !domain_matches(cookie.domain, old_cookie.domain))
return IterationDecision::Continue;
// 4. The path of the newly-created cookie path-matches the path of the existing cookie.
if (!path_matches(cookie.path, old_cookie.path))
return IterationDecision::Continue;
ignore_cookie = true;
return IterationDecision::Break;
});
if (ignore_cookie)
return;
}
// 17. If the cookie-attribute-list contains an attribute with an attribute-name of "SameSite", and an
// attribute-value of "Strict", "Lax", or "None", set the cookie's same-site-flag to the attribute-value of the
// last attribute in the cookie-attribute-list with an attribute-name of "SameSite". Otherwise, set the cookie's
// same-site-flag to "Default".
cookie.same_site = parsed_cookie.same_site_attribute;
// 18. If the cookie's same-site-flag is not "None":
if (cookie.same_site != Web::Cookie::SameSite::None) {
// FIXME: 1. If the cookie was received from a "non-HTTP" API, and the API was called from a navigable's active document
// whose "site for cookies" is not same-site with the top-level origin, then abort these steps and ignore the
// newly created cookie entirely.
// FIXME: 2. If the cookie was received from a "same-site" request (as defined in Section 5.2), skip the remaining
// substeps and continue processing the cookie.
// FIXME: 3. If the cookie was received from a request which is navigating a top-level traversable [HTML] (e.g. if the
// request's "reserved client" is either null or an environment whose "target browsing context"'s navigable
// is a top-level traversable), skip the remaining substeps and continue processing the cookie.
// FIXME: 4. Abort these steps and ignore the newly created cookie entirely.
}
// 19. If the cookie's "same-site-flag" is "None", abort these steps and ignore the cookie entirely unless the
// cookie's secure-only-flag is true.
if (cookie.same_site == Web::Cookie::SameSite::None && !cookie.secure)
return;
auto has_case_insensitive_prefix = [&](StringView value, StringView prefix) {
if (value.length() < prefix.length())
return false;
value = value.substring_view(0, prefix.length());
return value.equals_ignoring_ascii_case(prefix);
};
// 20. If the cookie-name begins with a case-insensitive match for the string "__Secure-", abort these steps and
// ignore the cookie entirely unless the cookie's secure-only-flag is true.
if (has_case_insensitive_prefix(cookie.name, "__Secure-"sv) && !cookie.secure)
return;
// 21. If the cookie-name begins with a case-insensitive match for the string "__Host-", abort these steps and
// ignore the cookie entirely unless the cookie meets all the following criteria:
if (has_case_insensitive_prefix(cookie.name, "__Host-"sv)) {
// 1. The cookie's secure-only-flag is true.
if (!cookie.secure)
return;
// 2. The cookie's host-only-flag is true.
if (!cookie.host_only)
return;
// 3. The cookie-attribute-list contains an attribute with an attribute-name of "Path", and the cookie's path is /.
if (parsed_cookie.path.has_value() && parsed_cookie.path != "/"sv)
return;
}
// 22. If the cookie-name is empty and either of the following conditions are true, abort these steps and ignore
// the cookie entirely:
if (cookie.name.is_empty()) {
// * the cookie-value begins with a case-insensitive match for the string "__Secure-"
if (has_case_insensitive_prefix(cookie.value, "__Secure-"sv))
return;
// * the cookie-value begins with a case-insensitive match for the string "__Host-"
if (has_case_insensitive_prefix(cookie.value, "__Host-"sv))
return;
}
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
// 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie:
if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value()) {
// If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these
// steps and ignore the newly created cookie entirely.
if (source != Web::Cookie::Source::Http && old_cookie->http_only)
// 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie:
if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) {
// 1. Let old-cookie be the existing cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie. (Notice that this algorithm maintains the invariant that there is at most one such
// cookie.)
// 2. If the newly-created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is true,
// abort these steps and ignore the newly created cookie entirely.
if (source == Web::Cookie::Source::NonHttp && old_cookie->http_only)
return;
// Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
// 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie.
cookie.creation_time = old_cookie->creation_time;
// Remove the old-cookie from the cookie store.
// 4. Remove the old-cookie from the cookie store.
// NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
}
// 12. Insert the newly created cookie into the cookie store.
// 24. Insert the newly-created cookie into the cookie store.
m_transient_storage.set_cookie(move(key), move(cookie));
m_transient_storage.purge_expired_cookies();
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3
Vector<Web::Cookie::Cookie> CookieJar::get_matching_cookies(const URL::URL& url, StringView canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode)
{
// https://tools.ietf.org/html/rfc6265#section-5.4
auto now = UnixDateTime::now();
// 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements:
Vector<Web::Cookie::Cookie> cookie_list;
m_transient_storage.for_each_cookie([&](auto& cookie) {
// Either: The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain.
// Or: The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain.
m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie& cookie) {
// * Either:
// The cookie's host-only-flag is true and the canonicalized host of the retrieval's URI is identical to
// the cookie's domain.
bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain);
// Or:
// The cookie's host-only-flag is false and the canonicalized host of the retrieval's URI domain-matches
// the cookie's domain.
bool is_not_host_only_and_domain_matches = !cookie.host_only && domain_matches(canonicalized_domain, cookie.domain);
if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches)
return;
// The request-uri's path path-matches the cookie's path.
// * The retrieval's URI's path path-matches the cookie's path.
if (!path_matches(url.serialize_path(), cookie.path))
return;
// If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol.
if (cookie.secure && (url.scheme() != "https"))
// * If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection (as
// defined by the user agent).
if (cookie.secure && url.scheme() != "https"sv)
return;
// If the cookie's http-only-flag is true, then exclude the cookie if the cookie-string is being generated for a "non-HTTP" API.
// * If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP".
if (cookie.http_only && (source != Web::Cookie::Source::Http))
return;
// FIXME: * If the cookie's same-site-flag is not "None" and the retrieval's same-site status is "cross-site", then
// exclude the cookie unless all of the following conditions are met:
// * The retrieval's type is "HTTP".
// * The same-site-flag is "Lax" or "Default".
// * The HTTP request associated with the retrieval uses a "safe" method.
// * The target browsing context of the HTTP request associated with the retrieval is the active browsing context
// or a top-level traversable.
// NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies.
if (mode == MatchingCookiesSpecMode::WebDriver) {
cookie_list.append(cookie);
@ -424,20 +623,23 @@ Vector<Web::Cookie::Cookie> CookieJar::get_matching_cookies(const URL::URL& url,
// NOTE: We do this first so that both our internal storage and cookie-list are updated.
cookie.last_access_time = now;
// 2. The user agent SHOULD sort the cookie-list in the following order:
// - Cookies with longer paths are listed before cookies with shorter paths.
// - Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.
// 2. The user agent SHOULD sort the cookie-list in the following order:
auto cookie_path_length = cookie.path.bytes().size();
auto cookie_creation_time = cookie.creation_time;
cookie_list.insert_before_matching(cookie, [cookie_path_length, cookie_creation_time](auto const& entry) {
// * Cookies with longer paths are listed before cookies with shorter paths.
if (cookie_path_length > entry.path.bytes().size()) {
return true;
}
// * Among cookies that have equal-length path fields, cookies with earlier creation-times are listed
// before cookies with later creation-times.
if (cookie_path_length == entry.path.bytes().size()) {
if (cookie_creation_time < entry.creation_time)
return true;
}
return false;
});
});

View file

@ -54,8 +54,17 @@ class CookieJar {
template<typename Callback>
void for_each_cookie(Callback callback)
{
for (auto& it : m_cookies)
callback(it.value);
using ReturnType = InvokeResult<Callback, Web::Cookie::Cookie&>;
for (auto& it : m_cookies) {
if constexpr (IsSame<ReturnType, IterationDecision>) {
if (callback(it.value) == IterationDecision::Break)
return;
} else {
static_assert(IsSame<ReturnType, void>);
callback(it.value);
}
}
}
private: