From 5c6aa408ed6e2ed8aeda09a861cd7451a84729fb Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 14 Apr 2021 10:18:13 -0400 Subject: [PATCH] Browser: Implement spec-compliant cookie retrieval https://tools.ietf.org/html/rfc6265#section-5.4 --- Userland/Applications/Browser/CookieJar.cpp | 85 +++++++++++++++++++-- Userland/Applications/Browser/CookieJar.h | 2 + 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/Userland/Applications/Browser/CookieJar.cpp b/Userland/Applications/Browser/CookieJar.cpp index 8da46a07fa2..6b8e07f4884 100644 --- a/Userland/Applications/Browser/CookieJar.cpp +++ b/Userland/Applications/Browser/CookieJar.cpp @@ -33,7 +33,7 @@ namespace Browser { -String CookieJar::get_cookie(const URL& url, Web::Cookie::Source) +String CookieJar::get_cookie(const URL& url, Web::Cookie::Source source) { purge_expired_cookies(); @@ -41,15 +41,16 @@ String CookieJar::get_cookie(const URL& url, Web::Cookie::Source) if (!domain.has_value()) return {}; + Vector cookie_list = get_matching_cookies(url, domain.value(), source); StringBuilder builder; - for (const auto& cookie : m_cookies) { - if (!domain_matches(domain.value(), cookie.value.domain)) - continue; - + for (const auto* 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("; "); - builder.appendff("{}={}", cookie.value.name, cookie.value.value); + + // Output the cookie's name, the %x3D ("=") character, and the cookie's value. + builder.appendff("{}={}", cookie->name, cookie->value); } return builder.build(); @@ -130,6 +131,29 @@ bool CookieJar::domain_matches(const String& string, const String& domain_string return true; } +bool CookieJar::path_matches(const String& request_path, const String& 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. + 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 ("/"). + 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. + if (request_path[cookie_path.length()] == '/') + return true; + } + + return false; +} + String CookieJar::default_path(const URL& url) { // https://tools.ietf.org/html/rfc6265#section-5.1.4 @@ -238,6 +262,55 @@ void CookieJar::store_cookie(Web::Cookie::ParsedCookie& parsed_cookie, const URL m_cookies.set(key, move(cookie)); } +Vector CookieJar::get_matching_cookies(const URL& url, const String& canonicalized_domain, Web::Cookie::Source source) +{ + // https://tools.ietf.org/html/rfc6265#section-5.4 + + auto now = Core::DateTime::now(); + + // 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements: + Vector cookie_list; + + for (auto& cookie : m_cookies) { + // 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. + bool is_host_only_and_has_identical_domain = cookie.value.host_only && (canonicalized_domain == cookie.value.domain); + bool is_not_host_only_and_domain_matches = !cookie.value.host_only && domain_matches(canonicalized_domain, cookie.value.domain); + if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches) + continue; + + // The request-uri's path path-matches the cookie's path. + if (!path_matches(url.path(), cookie.value.path)) + continue; + + // If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol. + if (cookie.value.secure && (url.protocol() != "https")) + continue; + + // 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 (cookie.value.http_only && (source != Web::Cookie::Source::Http)) + continue; + + // 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. + cookie_list.insert_before_matching(&cookie.value, [&cookie](auto* entry) { + if (cookie.value.path.length() > entry->path.length()) { + return true; + } else if (cookie.value.path.length() == entry->path.length()) { + if (cookie.value.creation_time.timestamp() < entry->creation_time.timestamp()) + return true; + } + return false; + }); + + // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time. + cookie.value.last_access_time = now; + } + + return cookie_list; +} + void CookieJar::purge_expired_cookies() { time_t now = Core::DateTime::now().timestamp(); diff --git a/Userland/Applications/Browser/CookieJar.h b/Userland/Applications/Browser/CookieJar.h index 58b8f3f65a1..b3f69da1ca3 100644 --- a/Userland/Applications/Browser/CookieJar.h +++ b/Userland/Applications/Browser/CookieJar.h @@ -53,9 +53,11 @@ public: private: static Optional canonicalize_domain(const URL& url); static bool domain_matches(const String& string, const String& domain_string); + static bool path_matches(const String& request_path, const String& cookie_path); static String default_path(const URL& url); void store_cookie(Web::Cookie::ParsedCookie& parsed_cookie, const URL& url, String canonicalized_domain, Web::Cookie::Source source); + Vector get_matching_cookies(const URL& url, const String& canonicalized_domain, Web::Cookie::Source source); void purge_expired_cookies(); HashMap m_cookies;