diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-04-12 23:16:27 -0400 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-04-13 15:52:50 +0200 |
commit | 86bdfa1edf21293e2c0f3ca8b31808a21882cb99 (patch) | |
tree | ffe8f76844ef1443da3ff8397854e3fa0ce32122 /Userland/Applications/Browser/CookieJar.cpp | |
parent | a0111f6a3e3a410647c70c884ef6d9666454d048 (diff) | |
download | serenity-86bdfa1edf21293e2c0f3ca8b31808a21882cb99.zip |
Browser: Implement spec-compliant cookie storage
https://tools.ietf.org/html/rfc6265#section-5.3
This includes a bit of an update to how cookies are first parsed. The
storage spec requires some extra information from the parsing steps than
just the actual values that were parsed. For example, it needs to know
whether Max-Age or Expires (or both) were specified to give precedence
to Max-Age. To accommodate this, the parser now uses an intermediate
struct for storing this information. The final Cookie struct is not
created until the storage steps.
The storage itself is also updated to be keyed by a combo of the cookie
name, domain, and path.
Retrieving cookies was updated to use the spec's domain-matching
algorithm, but otherwise is not written to the spec yet. This also does
not handle evicting expired cookies yet.
Diffstat (limited to 'Userland/Applications/Browser/CookieJar.cpp')
-rw-r--r-- | Userland/Applications/Browser/CookieJar.cpp | 242 |
1 files changed, 174 insertions, 68 deletions
diff --git a/Userland/Applications/Browser/CookieJar.cpp b/Userland/Applications/Browser/CookieJar.cpp index 9ddbc9fce9..577233aa11 100644 --- a/Userland/Applications/Browser/CookieJar.cpp +++ b/Userland/Applications/Browser/CookieJar.cpp @@ -26,12 +26,25 @@ #include "CookieJar.h" #include <AK/AllOf.h> +#include <AK/IPv4Address.h> #include <AK/StringBuilder.h> #include <AK/URL.h> +#include <AK/Vector.h> #include <ctype.h> namespace Browser { +struct ParsedCookie { + String name; + String value; + Optional<Core::DateTime> expiry_time_from_expires_attribute {}; + Optional<Core::DateTime> expiry_time_from_max_age_attribute {}; + Optional<String> domain {}; + Optional<String> path {}; + bool secure_attribute_present { false }; + bool http_only_attribute_present { false }; +}; + String CookieJar::get_cookie(const URL& url) const { auto domain = canonicalize_domain(url); @@ -40,12 +53,13 @@ String CookieJar::get_cookie(const URL& url) const StringBuilder builder; - if (auto it = m_cookies.find(*domain); it != m_cookies.end()) { - for (const auto& cookie : it->value) { - if (!builder.is_empty()) - builder.append("; "); - builder.appendff("{}={}", cookie.name, cookie.value); - } + for (const auto& cookie : m_cookies) { + if (!domain_matches(domain.value(), cookie.value.domain)) + continue; + + if (!builder.is_empty()) + builder.append("; "); + builder.appendff("{}={}", cookie.value.name, cookie.value.value); } return builder.build(); @@ -57,47 +71,35 @@ void CookieJar::set_cookie(const URL& url, const String& cookie_string) if (!domain.has_value()) return; - auto new_cookie = parse_cookie(cookie_string, *domain, default_path(url)); - if (!new_cookie.has_value()) + auto parsed_cookie = parse_cookie(cookie_string); + if (!parsed_cookie.has_value()) return; - auto it = m_cookies.find(*domain); - if (it == m_cookies.end()) { - m_cookies.set(*domain, { move(*new_cookie) }); - return; - } - - for (auto& cookie : it->value) { - if (cookie.name == new_cookie->name) { - cookie = move(*new_cookie); - return; - } - } - - it->value.append(move(*new_cookie)); + store_cookie(parsed_cookie.value(), url, move(domain.value())); } void CookieJar::dump_cookies() const { - static const char* url_color = "\033[34;1m"; - static const char* cookie_color = "\033[31m"; + static const char* key_color = "\033[34;1m"; static const char* attribute_color = "\033[33m"; static const char* no_color = "\033[0m"; StringBuilder builder; - builder.appendff("{} URLs with cookies\n", m_cookies.size()); - - for (const auto& url_and_cookies : m_cookies) { - builder.appendff("{}Cookies for:{} {}\n", url_color, no_color, url_and_cookies.key.is_empty() ? "file://" : url_and_cookies.key); - - for (const auto& cookie : url_and_cookies.value) { - builder.appendff("\t{}{}{} = {}{}{}\n", cookie_color, cookie.name, no_color, cookie_color, cookie.value, no_color); - builder.appendff("\t\t{}Expiry{} = {}\n", attribute_color, no_color, cookie.expiry_time.to_string()); - builder.appendff("\t\t{}Domain{} = {}\n", attribute_color, no_color, cookie.domain); - builder.appendff("\t\t{}Path{} = {}\n", attribute_color, no_color, cookie.path); - builder.appendff("\t\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.secure); - builder.appendff("\t\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.http_only); - } + builder.appendff("{} cookies stored\n", m_cookies.size()); + + for (const auto& cookie : m_cookies) { + builder.appendff("{}{}{} - ", key_color, cookie.key.name, no_color); + builder.appendff("{}{}{} - ", key_color, cookie.key.domain, no_color); + builder.appendff("{}{}{}\n", key_color, cookie.key.path, no_color); + + builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value.value); + builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.value.creation_time.to_string()); + builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.value.last_access_time.to_string()); + builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.value.expiry_time.to_string()); + builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.value.secure); + builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.value.http_only); + builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.value.host_only); + builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.value.persistent); } dbgln("{}", builder.build()); @@ -135,7 +137,7 @@ String CookieJar::default_path(const URL& url) return uri_path.substring(0, last_separator); } -Optional<Cookie> CookieJar::parse_cookie(const String& cookie_string, String default_domain, String default_path) +Optional<ParsedCookie> CookieJar::parse_cookie(const String& cookie_string) { // https://tools.ietf.org/html/rfc6265#section-5.2 StringView name_value_pair; @@ -177,17 +179,13 @@ Optional<Cookie> CookieJar::parse_cookie(const String& cookie_string, String def return {}; // 6. The cookie-name is the name string, and the cookie-value is the value string. - Cookie cookie { name, value }; - - cookie.expiry_time = Core::DateTime::create(9999, 12, 31, 23, 59, 59); - cookie.domain = move(default_domain); - cookie.path = move(default_path); + ParsedCookie parsed_cookie { name, value }; - parse_attributes(cookie, unparsed_attributes); - return cookie; + parse_attributes(parsed_cookie, unparsed_attributes); + return parsed_cookie; } -void CookieJar::parse_attributes(Cookie& cookie, StringView unparsed_attributes) +void CookieJar::parse_attributes(ParsedCookie& parsed_cookie, StringView unparsed_attributes) { // 1. If the unparsed-attributes string is empty, skip the rest of these steps. if (unparsed_attributes.is_empty()) @@ -231,37 +229,37 @@ void CookieJar::parse_attributes(Cookie& cookie, StringView unparsed_attributes) // 6. 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(cookie, attribute_name, attribute_value); + process_attribute(parsed_cookie, attribute_name, attribute_value); // 7. Return to Step 1 of this algorithm. - parse_attributes(cookie, unparsed_attributes); + parse_attributes(parsed_cookie, unparsed_attributes); } -void CookieJar::process_attribute(Cookie& cookie, StringView attribute_name, StringView attribute_value) +void CookieJar::process_attribute(ParsedCookie& parsed_cookie, StringView attribute_name, StringView attribute_value) { if (attribute_name.equals_ignoring_case("Expires")) { - on_expires_attribute(cookie, attribute_value); + on_expires_attribute(parsed_cookie, attribute_value); } else if (attribute_name.equals_ignoring_case("Max-Age")) { - on_max_age_attribute(cookie, attribute_value); + on_max_age_attribute(parsed_cookie, attribute_value); } else if (attribute_name.equals_ignoring_case("Domain")) { - on_domain_attribute(cookie, attribute_value); + on_domain_attribute(parsed_cookie, attribute_value); } else if (attribute_name.equals_ignoring_case("Path")) { - on_path_attribute(cookie, attribute_value); + on_path_attribute(parsed_cookie, attribute_value); } else if (attribute_name.equals_ignoring_case("Secure")) { - on_secure_attribute(cookie); + on_secure_attribute(parsed_cookie); } else if (attribute_name.equals_ignoring_case("HttpOnly")) { - on_http_only_attribute(cookie); + on_http_only_attribute(parsed_cookie); } } -void CookieJar::on_expires_attribute(Cookie& cookie, StringView attribute_value) +void CookieJar::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()) - cookie.expiry_time = *expiry_time; + parsed_cookie.expiry_time_from_expires_attribute = move(*expiry_time); } -void CookieJar::on_max_age_attribute(Cookie& cookie, StringView attribute_value) +void CookieJar::on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { // https://tools.ietf.org/html/rfc6265#section-5.2.2 @@ -275,16 +273,16 @@ void CookieJar::on_max_age_attribute(Cookie& cookie, StringView attribute_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. - cookie.expiry_time = Core::DateTime::from_timestamp(0); + parsed_cookie.expiry_time_from_max_age_attribute = Core::DateTime::from_timestamp(0); } else { // Otherwise, let the expiry-time be the current date and time plus delta-seconds seconds. time_t now = Core::DateTime::now().timestamp(); - cookie.expiry_time = Core::DateTime::from_timestamp(now + *delta_seconds); + parsed_cookie.expiry_time_from_max_age_attribute = Core::DateTime::from_timestamp(now + *delta_seconds); } } } -void CookieJar::on_domain_attribute(Cookie& cookie, StringView attribute_value) +void CookieJar::on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { // https://tools.ietf.org/html/rfc6265#section-5.2.3 @@ -304,10 +302,10 @@ void CookieJar::on_domain_attribute(Cookie& cookie, StringView attribute_value) } // Convert the cookie-domain to lower case. - cookie.domain = String(cookie_domain).to_lowercase(); + parsed_cookie.domain = String(cookie_domain).to_lowercase(); } -void CookieJar::on_path_attribute(Cookie& cookie, StringView attribute_value) +void CookieJar::on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value) { // https://tools.ietf.org/html/rfc6265#section-5.2.4 @@ -317,19 +315,19 @@ void CookieJar::on_path_attribute(Cookie& cookie, StringView attribute_value) return; // Let cookie-path be the attribute-value - cookie.path = attribute_value; + parsed_cookie.path = attribute_value; } -void CookieJar::on_secure_attribute(Cookie& cookie) +void CookieJar::on_secure_attribute(ParsedCookie& parsed_cookie) { // https://tools.ietf.org/html/rfc6265#section-5.2.5 - cookie.secure = true; + parsed_cookie.secure_attribute_present = true; } -void CookieJar::on_http_only_attribute(Cookie& cookie) +void CookieJar::on_http_only_attribute(ParsedCookie& parsed_cookie) { // https://tools.ietf.org/html/rfc6265#section-5.2.6 - cookie.http_only = true; + parsed_cookie.http_only_attribute_present = true; } Optional<Core::DateTime> CookieJar::parse_date_time(StringView date_string) @@ -443,4 +441,112 @@ Optional<Core::DateTime> CookieJar::parse_date_time(StringView date_string) return Core::DateTime::create(year, month, day_of_month, hour, minute, second); } +bool CookieJar::domain_matches(const String& string, const String& 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. + if (string == domain_string) + return true; + + // 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; + if (string[string.length() - domain_string.length() - 1] != '.') + return false; + if (AK::IPv4Address::from_string(string).has_value()) + return false; + + return true; +} + +void CookieJar::store_cookie(ParsedCookie& parsed_cookie, const URL& url, String canonicalized_domain) +{ + // https://tools.ietf.org/html/rfc6265#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. + Cookie cookie { move(parsed_cookie.name), move(parsed_cookie.value) }; + cookie.creation_time = Core::DateTime::now(); + cookie.last_access_time = cookie.creation_time; + + 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". + cookie.persistent = true; + cookie.expiry_time = move(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". + cookie.persistent = true; + cookie.expiry_time = move(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 gddate. + cookie.persistent = false; + cookie.expiry_time = Core::DateTime::create(9999, 12, 31, 23, 59, 59); + } + + // 4. 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 = move(parsed_cookie.domain.value()); + } + + // 5. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix: + // FIXME: Support rejection of public suffixes. The full list is here: https://publicsuffix.org/list/public_suffix_list.dat + + // 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)) + return; + + // 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. + cookie.host_only = true; + cookie.domain = move(canonicalized_domain); + } + + // 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Path": + 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 = move(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. + 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. + 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. + // FIXME: Update CookieJar to track where the cookie originated (an HTTP request vs document.cookie). + + // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: + CookieStorageKey key { cookie.name, cookie.domain, cookie.path }; + + if (auto old_cookie = m_cookies.find(key); old_cookie != m_cookies.end()) { + // 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. + // FIXME: Similar to step 10, CookieJar needs to track where the cookie originated. + + // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. + cookie.creation_time = old_cookie->value.creation_time; + + // Remove the old-cookie from the cookie store. + m_cookies.remove(old_cookie); + } + + // 12. Insert the newly created cookie into the cookie store. + m_cookies.set(key, move(cookie)); +} + } |