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 +#include #include #include +#include #include namespace Browser { +struct ParsedCookie { + String name; + String value; + Optional expiry_time_from_expires_attribute {}; + Optional expiry_time_from_max_age_attribute {}; + Optional domain {}; + Optional 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()); + builder.appendff("{} cookies stored\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 : 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); - 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("\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 CookieJar::parse_cookie(const String& cookie_string, String default_domain, String default_path) +Optional 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 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 }; + ParsedCookie parsed_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); - - 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 CookieJar::parse_date_time(StringView date_string) @@ -443,4 +441,112 @@ Optional 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)); +} + } diff --git a/Userland/Applications/Browser/CookieJar.h b/Userland/Applications/Browser/CookieJar.h index 6380b6d4f9..2576266b2f 100644 --- a/Userland/Applications/Browser/CookieJar.h +++ b/Userland/Applications/Browser/CookieJar.h @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include namespace Browser { @@ -37,11 +37,25 @@ namespace Browser { struct Cookie { String name; String value; + Core::DateTime creation_time {}; + Core::DateTime last_access_time {}; Core::DateTime expiry_time {}; String domain {}; String path {}; bool secure { false }; bool http_only { false }; + bool host_only { false }; + bool persistent { false }; +}; + +struct ParsedCookie; + +struct CookieStorageKey { + bool operator==(const CookieStorageKey&) const = default; + + String name; + String domain; + String path; }; class CookieJar { @@ -53,18 +67,37 @@ public: private: static Optional canonicalize_domain(const URL& url); static String default_path(const URL& url); - static Optional parse_cookie(const String& cookie_string, String default_domain, String default_path); - static void parse_attributes(Cookie& cookie, StringView unparsed_attributes); - static void process_attribute(Cookie& cookie, StringView attribute_name, StringView attribute_value); - static void on_expires_attribute(Cookie& cookie, StringView attribute_value); - static void on_max_age_attribute(Cookie& cookie, StringView attribute_value); - static void on_domain_attribute(Cookie& cookie, StringView attribute_value); - static void on_path_attribute(Cookie& cookie, StringView attribute_value); - static void on_secure_attribute(Cookie& cookie); - static void on_http_only_attribute(Cookie& cookie); + static Optional parse_cookie(const String& cookie_string); + 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); + static void on_max_age_attribute(ParsedCookie& parsed_cookie, StringView attribute_value); + static void on_domain_attribute(ParsedCookie& parsed_cookie, StringView attribute_value); + static void on_path_attribute(ParsedCookie& parsed_cookie, StringView attribute_value); + static void on_secure_attribute(ParsedCookie& parsed_cookie); + static void on_http_only_attribute(ParsedCookie& parsed_cookie); static Optional parse_date_time(StringView date_string); + static bool domain_matches(const String& string, const String& domain_string); - HashMap> m_cookies; + void store_cookie(ParsedCookie& parsed_cookie, const URL& url, String canonicalized_domain); + + HashMap m_cookies; +}; + +} + +namespace AK { + +template<> +struct Traits : public GenericTraits { + static unsigned hash(const Browser::CookieStorageKey& key) + { + unsigned hash = 0; + hash = pair_int_hash(hash, string_hash(key.name.characters(), key.name.length())); + hash = pair_int_hash(hash, string_hash(key.domain.characters(), key.domain.length())); + hash = pair_int_hash(hash, string_hash(key.path.characters(), key.path.length())); + return hash; + } }; }