Serenity Operating System
1/*
2 * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "CookieJar.h"
10#include "Database.h"
11#include <AK/IPv4Address.h>
12#include <AK/StringBuilder.h>
13#include <AK/StringView.h>
14#include <AK/Time.h>
15#include <AK/URL.h>
16#include <AK/Vector.h>
17#include <LibCore/Promise.h>
18#include <LibSQL/TupleDescriptor.h>
19#include <LibSQL/Value.h>
20#include <LibWeb/Cookie/ParsedCookie.h>
21
22namespace Browser {
23
24ErrorOr<CookieJar> CookieJar::create(Database& database)
25{
26 Statements statements {};
27
28 statements.create_table = TRY(database.prepare_statement(R"#(
29 CREATE TABLE IF NOT EXISTS Cookies (
30 name TEXT,
31 value TEXT,
32 same_site INTEGER,
33 creation_time INTEGER,
34 last_access_time INTEGER,
35 expiry_time INTEGER,
36 domain TEXT,
37 path TEXT,
38 secure BOOLEAN,
39 http_only BOOLEAN,
40 host_only BOOLEAN,
41 persistent BOOLEAN
42 );)#"sv));
43
44 statements.update_cookie = TRY(database.prepare_statement(R"#(
45 UPDATE Cookies SET
46 value=?,
47 same_site=?,
48 creation_time=?,
49 last_access_time=?,
50 expiry_time=?,
51 secure=?,
52 http_only=?,
53 host_only=?,
54 persistent=?
55 WHERE ((name = ?) AND (domain = ?) AND (path = ?));)#"sv));
56
57 statements.insert_cookie = TRY(database.prepare_statement("INSERT INTO Cookies VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"sv));
58 statements.expire_cookie = TRY(database.prepare_statement("DELETE FROM Cookies WHERE (expiry_time < ?);"sv));
59 statements.select_cookie = TRY(database.prepare_statement("SELECT * FROM Cookies WHERE ((name = ?) AND (domain = ?) AND (path = ?));"sv));
60 statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv));
61
62 return CookieJar { database, move(statements) };
63}
64
65CookieJar::CookieJar(Database& database, Statements statements)
66 : m_database(database)
67 , m_statements(move(statements))
68{
69 m_database.execute_statement(m_statements.create_table, {}, {}, {});
70}
71
72DeprecatedString CookieJar::get_cookie(const URL& url, Web::Cookie::Source source)
73{
74 purge_expired_cookies();
75
76 auto domain = canonicalize_domain(url);
77 if (!domain.has_value())
78 return {};
79
80 auto cookie_list = get_matching_cookies(url, domain.value(), source);
81 StringBuilder builder;
82
83 for (auto const& cookie : cookie_list) {
84 // If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ")
85 if (!builder.is_empty())
86 builder.append("; "sv);
87
88 // Output the cookie's name, the %x3D ("=") character, and the cookie's value.
89 builder.appendff("{}={}", cookie.name, cookie.value);
90 }
91
92 return builder.to_deprecated_string();
93}
94
95void CookieJar::set_cookie(const URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source)
96{
97 auto domain = canonicalize_domain(url);
98 if (!domain.has_value())
99 return;
100
101 store_cookie(parsed_cookie, url, move(domain.value()), source);
102}
103
104// This is based on https://www.rfc-editor.org/rfc/rfc6265#section-5.3 as store_cookie() below
105// however the whole ParsedCookie->Cookie conversion is skipped.
106void CookieJar::update_cookie(Web::Cookie::Cookie cookie)
107{
108 select_cookie_from_database(
109 move(cookie),
110
111 // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie:
112 [this](auto& cookie, auto old_cookie) {
113 // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
114 cookie.creation_time = old_cookie.creation_time;
115
116 // Remove the old-cookie from the cookie store.
117 // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
118 update_cookie_in_database(cookie);
119 },
120
121 // 12. Insert the newly created cookie into the cookie store.
122 [this](auto cookie) {
123 insert_cookie_into_database(cookie);
124 });
125}
126
127void CookieJar::dump_cookies()
128{
129 static constexpr auto key_color = "\033[34;1m"sv;
130 static constexpr auto attribute_color = "\033[33m"sv;
131 static constexpr auto no_color = "\033[0m"sv;
132
133 StringBuilder builder;
134 size_t total_cookies { 0 };
135
136 select_all_cookies_from_database([&](auto cookie) {
137 ++total_cookies;
138
139 builder.appendff("{}{}{} - ", key_color, cookie.name, no_color);
140 builder.appendff("{}{}{} - ", key_color, cookie.domain, no_color);
141 builder.appendff("{}{}{}\n", key_color, cookie.path, no_color);
142
143 builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value);
144 builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.creation_time_to_string());
145 builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.last_access_time_to_string());
146 builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.expiry_time_to_string());
147 builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.secure);
148 builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.http_only);
149 builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.host_only);
150 builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.persistent);
151 builder.appendff("\t{}SameSite{} = {:s}\n", attribute_color, no_color, Web::Cookie::same_site_to_string(cookie.same_site));
152 });
153
154 dbgln("{} cookies stored\n{}", total_cookies, builder.to_deprecated_string());
155}
156
157Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies()
158{
159 Vector<Web::Cookie::Cookie> cookies;
160
161 select_all_cookies_from_database([&](auto cookie) {
162 cookies.append(move(cookie));
163 });
164
165 return cookies;
166}
167
168// https://w3c.github.io/webdriver/#dfn-associated-cookies
169Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies(URL const& url)
170{
171 auto domain = canonicalize_domain(url);
172 if (!domain.has_value())
173 return {};
174
175 return get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver);
176}
177
178Optional<Web::Cookie::Cookie> CookieJar::get_named_cookie(URL const& url, DeprecatedString const& name)
179{
180 auto domain = canonicalize_domain(url);
181 if (!domain.has_value())
182 return {};
183
184 auto cookie_list = get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver);
185
186 for (auto const& cookie : cookie_list) {
187 if (cookie.name == name)
188 return cookie;
189 }
190
191 return {};
192}
193
194Optional<DeprecatedString> CookieJar::canonicalize_domain(const URL& url)
195{
196 // https://tools.ietf.org/html/rfc6265#section-5.1.2
197 if (!url.is_valid())
198 return {};
199
200 // FIXME: Implement RFC 5890 to "Convert each label that is not a Non-Reserved LDH (NR-LDH) label to an A-label".
201 return url.host().to_lowercase();
202}
203
204bool CookieJar::domain_matches(DeprecatedString const& string, DeprecatedString const& domain_string)
205{
206 // https://tools.ietf.org/html/rfc6265#section-5.1.3
207
208 // A string domain-matches a given domain string if at least one of the following conditions hold:
209
210 // The domain string and the string are identical.
211 if (string == domain_string)
212 return true;
213
214 // All of the following conditions hold:
215 // - The domain string is a suffix of the string.
216 // - The last character of the string that is not included in the domain string is a %x2E (".") character.
217 // - The string is a host name (i.e., not an IP address).
218 if (!string.ends_with(domain_string))
219 return false;
220 if (string[string.length() - domain_string.length() - 1] != '.')
221 return false;
222 if (AK::IPv4Address::from_string(string).has_value())
223 return false;
224
225 return true;
226}
227
228bool CookieJar::path_matches(DeprecatedString const& request_path, DeprecatedString const& cookie_path)
229{
230 // https://tools.ietf.org/html/rfc6265#section-5.1.4
231
232 // A request-path path-matches a given cookie-path if at least one of the following conditions holds:
233
234 // The cookie-path and the request-path are identical.
235 if (request_path == cookie_path)
236 return true;
237
238 if (request_path.starts_with(cookie_path)) {
239 // The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/").
240 if (cookie_path.ends_with('/'))
241 return true;
242
243 // 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.
244 if (request_path[cookie_path.length()] == '/')
245 return true;
246 }
247
248 return false;
249}
250
251DeprecatedString CookieJar::default_path(const URL& url)
252{
253 // https://tools.ietf.org/html/rfc6265#section-5.1.4
254
255 // 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise).
256 DeprecatedString uri_path = url.path();
257
258 // 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.
259 if (uri_path.is_empty() || (uri_path[0] != '/'))
260 return "/";
261
262 StringView uri_path_view = uri_path;
263 size_t last_separator = uri_path_view.find_last('/').value();
264
265 // 3. If the uri-path contains no more than one %x2F ("/") character, output %x2F ("/") and skip the remaining step.
266 if (last_separator == 0)
267 return "/";
268
269 // 4. Output the characters of the uri-path from the first character up to, but not including, the right-most %x2F ("/").
270 return uri_path.substring(0, last_separator);
271}
272
273void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL& url, DeprecatedString canonicalized_domain, Web::Cookie::Source source)
274{
275 // https://tools.ietf.org/html/rfc6265#section-5.3
276
277 // 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.
278 Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value, parsed_cookie.same_site_attribute };
279 cookie.creation_time = Time::now_realtime();
280 cookie.last_access_time = cookie.creation_time;
281
282 if (parsed_cookie.expiry_time_from_max_age_attribute.has_value()) {
283 // 3. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age": Set the cookie's persistent-flag to true.
284 // 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".
285 cookie.persistent = true;
286 cookie.expiry_time = parsed_cookie.expiry_time_from_max_age_attribute.value();
287 } else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) {
288 // If the cookie-attribute-list contains an attribute with an attribute-name of "Expires": Set the cookie's persistent-flag to true.
289 // Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Expires".
290 cookie.persistent = true;
291 cookie.expiry_time = parsed_cookie.expiry_time_from_expires_attribute.value();
292 } else {
293 // Set the cookie's persistent-flag to false. Set the cookie's expiry-time to the latest representable date.
294 cookie.persistent = false;
295 cookie.expiry_time = Time::max();
296 }
297
298 // 4. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
299 if (parsed_cookie.domain.has_value()) {
300 // Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Domain".
301 cookie.domain = parsed_cookie.domain.value();
302 }
303
304 // 5. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix:
305 // FIXME: Support rejection of public suffixes. The full list is here: https://publicsuffix.org/list/public_suffix_list.dat
306
307 // 6. If the domain-attribute is non-empty:
308 if (!cookie.domain.is_empty()) {
309 // If the canonicalized request-host does not domain-match the domain-attribute: Ignore the cookie entirely and abort these steps.
310 if (!domain_matches(canonicalized_domain, cookie.domain))
311 return;
312
313 // Set the cookie's host-only-flag to false. Set the cookie's domain to the domain-attribute.
314 cookie.host_only = false;
315 } else {
316 // Set the cookie's host-only-flag to true. Set the cookie's domain to the canonicalized request-host.
317 cookie.host_only = true;
318 cookie.domain = move(canonicalized_domain);
319 }
320
321 // 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Path":
322 if (parsed_cookie.path.has_value()) {
323 // Set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path".
324 cookie.path = parsed_cookie.path.value();
325 } else {
326 cookie.path = default_path(url);
327 }
328
329 // 8. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's secure-only-flag to true.
330 cookie.secure = parsed_cookie.secure_attribute_present;
331
332 // 9. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's http-only-flag to false.
333 cookie.http_only = parsed_cookie.http_only_attribute_present;
334
335 // 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.
336 if (source != Web::Cookie::Source::Http && cookie.http_only)
337 return;
338
339 select_cookie_from_database(
340 move(cookie),
341
342 // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie:
343 [this, source](auto& cookie, auto old_cookie) {
344 // If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these
345 // steps and ignore the newly created cookie entirely.
346 if (source != Web::Cookie::Source::Http && old_cookie.http_only)
347 return;
348
349 // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
350 cookie.creation_time = old_cookie.creation_time;
351
352 // Remove the old-cookie from the cookie store.
353 // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
354 update_cookie_in_database(cookie);
355 },
356
357 // 12. Insert the newly created cookie into the cookie store.
358 [this](auto cookie) {
359 insert_cookie_into_database(cookie);
360 });
361}
362
363Vector<Web::Cookie::Cookie> CookieJar::get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode)
364{
365 // https://tools.ietf.org/html/rfc6265#section-5.4
366
367 // 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements:
368 Vector<Web::Cookie::Cookie> cookie_list;
369
370 select_all_cookies_from_database([&](auto cookie) {
371 // Either: The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain.
372 // Or: The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain.
373 bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain);
374 bool is_not_host_only_and_domain_matches = !cookie.host_only && domain_matches(canonicalized_domain, cookie.domain);
375 if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches)
376 return;
377
378 // The request-uri's path path-matches the cookie's path.
379 if (!path_matches(url.path(), cookie.path))
380 return;
381
382 // If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol.
383 if (cookie.secure && (url.scheme() != "https"))
384 return;
385
386 // 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.
387 if (cookie.http_only && (source != Web::Cookie::Source::Http))
388 return;
389
390 // NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies.
391 if (mode == MatchingCookiesSpecMode::WebDriver) {
392 cookie_list.append(move(cookie));
393 return;
394 }
395
396 // 2. The user agent SHOULD sort the cookie-list in the following order:
397 // - Cookies with longer paths are listed before cookies with shorter paths.
398 // - Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.
399 auto cookie_path_length = cookie.path.length();
400 auto cookie_creation_time = cookie.creation_time;
401
402 cookie_list.insert_before_matching(move(cookie), [cookie_path_length, cookie_creation_time](auto const& entry) {
403 if (cookie_path_length > entry.path.length()) {
404 return true;
405 } else if (cookie_path_length == entry.path.length()) {
406 if (cookie_creation_time < entry.creation_time)
407 return true;
408 }
409 return false;
410 });
411 });
412
413 // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time.
414 auto now = Time::now_realtime();
415
416 for (auto& cookie : cookie_list) {
417 cookie.last_access_time = now;
418 update_cookie_in_database(cookie);
419 }
420
421 return cookie_list;
422}
423
424static ErrorOr<Web::Cookie::Cookie> parse_cookie(ReadonlySpan<SQL::Value> row)
425{
426 if (row.size() != 12)
427 return Error::from_string_view("Incorrect number of columns to parse cookie"sv);
428
429 size_t index = 0;
430
431 auto convert_text = [&](auto& field, StringView name) -> ErrorOr<void> {
432 auto const& value = row[index++];
433 if (value.type() != SQL::SQLType::Text)
434 return Error::from_string_view(name);
435
436 field = value.to_deprecated_string();
437 return {};
438 };
439
440 auto convert_bool = [&](auto& field, StringView name) -> ErrorOr<void> {
441 auto const& value = row[index++];
442 if (value.type() != SQL::SQLType::Boolean)
443 return Error::from_string_view(name);
444
445 field = value.to_bool().value();
446 return {};
447 };
448
449 auto convert_time = [&](auto& field, StringView name) -> ErrorOr<void> {
450 auto const& value = row[index++];
451 if (value.type() != SQL::SQLType::Integer)
452 return Error::from_string_view(name);
453
454 auto time = value.to_int<i64>().value();
455 field = Time::from_seconds(time);
456 return {};
457 };
458
459 auto convert_same_site = [&](auto& field, StringView name) -> ErrorOr<void> {
460 auto const& value = row[index++];
461 if (value.type() != SQL::SQLType::Integer)
462 return Error::from_string_view(name);
463
464 auto same_site = value.to_int<UnderlyingType<Web::Cookie::SameSite>>().value();
465 if (same_site > to_underlying(Web::Cookie::SameSite::Lax))
466 return Error::from_string_view(name);
467
468 field = static_cast<Web::Cookie::SameSite>(same_site);
469 return {};
470 };
471
472 Web::Cookie::Cookie cookie;
473 TRY(convert_text(cookie.name, "name"sv));
474 TRY(convert_text(cookie.value, "value"sv));
475 TRY(convert_same_site(cookie.same_site, "same_site"sv));
476 TRY(convert_time(cookie.creation_time, "creation_time"sv));
477 TRY(convert_time(cookie.last_access_time, "last_access_time"sv));
478 TRY(convert_time(cookie.expiry_time, "expiry_time"sv));
479 TRY(convert_text(cookie.domain, "domain"sv));
480 TRY(convert_text(cookie.path, "path"sv));
481 TRY(convert_bool(cookie.secure, "secure"sv));
482 TRY(convert_bool(cookie.http_only, "http_only"sv));
483 TRY(convert_bool(cookie.host_only, "host_only"sv));
484 TRY(convert_bool(cookie.persistent, "persistent"sv));
485
486 return cookie;
487}
488
489void CookieJar::insert_cookie_into_database(Web::Cookie::Cookie const& cookie)
490{
491 m_database.execute_statement(
492 m_statements.insert_cookie, {}, [this]() { purge_expired_cookies(); }, {},
493 cookie.name,
494 cookie.value,
495 to_underlying(cookie.same_site),
496 cookie.creation_time.to_seconds(),
497 cookie.last_access_time.to_seconds(),
498 cookie.expiry_time.to_seconds(),
499 cookie.domain,
500 cookie.path,
501 cookie.secure,
502 cookie.http_only,
503 cookie.host_only,
504 cookie.persistent);
505}
506
507void CookieJar::update_cookie_in_database(Web::Cookie::Cookie const& cookie)
508{
509 m_database.execute_statement(
510 m_statements.update_cookie, {}, [this]() { purge_expired_cookies(); }, {},
511 cookie.value,
512 to_underlying(cookie.same_site),
513 cookie.creation_time.to_seconds(),
514 cookie.last_access_time.to_seconds(),
515 cookie.expiry_time.to_seconds(),
516 cookie.secure,
517 cookie.http_only,
518 cookie.host_only,
519 cookie.persistent,
520 cookie.name,
521 cookie.domain,
522 cookie.path);
523}
524
525struct WrappedCookie : public RefCounted<WrappedCookie> {
526 explicit WrappedCookie(Web::Cookie::Cookie cookie_)
527 : RefCounted()
528 , cookie(move(cookie_))
529 {
530 }
531
532 Web::Cookie::Cookie cookie;
533 bool had_any_results { false };
534};
535
536void CookieJar::select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results)
537{
538 auto wrapped_cookie = make_ref_counted<WrappedCookie>(move(cookie));
539
540 m_database.execute_statement(
541 m_statements.select_cookie,
542 [on_result = move(on_result), wrapped_cookie = wrapped_cookie](auto row) {
543 if (auto selected_cookie = parse_cookie(row); selected_cookie.is_error())
544 dbgln("Failed to parse cookie '{}': {}", selected_cookie.error(), row);
545 else
546 on_result(wrapped_cookie->cookie, selected_cookie.release_value());
547
548 wrapped_cookie->had_any_results = true;
549 },
550 [on_complete_without_results = move(on_complete_without_results), wrapped_cookie = wrapped_cookie]() {
551 if (!wrapped_cookie->had_any_results)
552 on_complete_without_results(move(wrapped_cookie->cookie));
553 },
554 {},
555 wrapped_cookie->cookie.name,
556 wrapped_cookie->cookie.domain,
557 wrapped_cookie->cookie.path);
558}
559
560void CookieJar::select_all_cookies_from_database(OnSelectAllCookiesResult on_result)
561{
562 // FIXME: Make surrounding APIs asynchronous.
563 auto promise = Core::Promise<Empty>::construct();
564
565 m_database.execute_statement(
566 m_statements.select_all_cookies,
567 [on_result = move(on_result)](auto row) {
568 if (auto cookie = parse_cookie(row); cookie.is_error())
569 dbgln("Failed to parse cookie '{}': {}", cookie.error(), row);
570 else
571 on_result(cookie.release_value());
572 },
573 [&]() {
574 MUST(promise->resolve({}));
575 },
576 [&](auto) {
577 MUST(promise->resolve({}));
578 });
579
580 MUST(promise->await());
581}
582
583void CookieJar::purge_expired_cookies()
584{
585 auto now = Time::now_realtime().to_seconds();
586 m_database.execute_statement(m_statements.expire_cookie, {}, {}, {}, now);
587}
588
589}