mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 11:57:35 +00:00
Browser: Add support for CookieJar to run in a transient, in-memory mode
This is to allow running Ladybird without the SQL database for testing. Primarily, this will let us set 'follow-fork-mode' to 'child' within GDB to enter the WebContent process, rather than the SQLServer process. But this is also handy for digging into cookie storage issues.
This commit is contained in:
parent
59a1a3f463
commit
a01ad58e91
3 changed files with 184 additions and 72 deletions
|
@ -59,14 +59,24 @@ ErrorOr<CookieJar> CookieJar::create(Database& database)
|
||||||
statements.select_cookie = TRY(database.prepare_statement("SELECT * FROM Cookies WHERE ((name = ?) AND (domain = ?) AND (path = ?));"sv));
|
statements.select_cookie = TRY(database.prepare_statement("SELECT * FROM Cookies WHERE ((name = ?) AND (domain = ?) AND (path = ?));"sv));
|
||||||
statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv));
|
statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv));
|
||||||
|
|
||||||
return CookieJar { database, move(statements) };
|
return CookieJar { PersistedStorage { database, move(statements) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
CookieJar::CookieJar(Database& database, Statements statements)
|
CookieJar CookieJar::create()
|
||||||
: m_database(database)
|
{
|
||||||
, m_statements(move(statements))
|
return CookieJar { TransientStorage {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieJar::CookieJar(PersistedStorage storage)
|
||||||
|
: m_storage(move(storage))
|
||||||
|
{
|
||||||
|
auto& persisted_storage = m_storage.get<PersistedStorage>();
|
||||||
|
persisted_storage.database.execute_statement(persisted_storage.statements.create_table, {}, {}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieJar::CookieJar(TransientStorage storage)
|
||||||
|
: m_storage(move(storage))
|
||||||
{
|
{
|
||||||
m_database.execute_statement(m_statements.create_table, {}, {}, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DeprecatedString CookieJar::get_cookie(const URL& url, Web::Cookie::Source source)
|
DeprecatedString CookieJar::get_cookie(const URL& url, Web::Cookie::Source source)
|
||||||
|
@ -488,38 +498,52 @@ static ErrorOr<Web::Cookie::Cookie> parse_cookie(ReadonlySpan<SQL::Value> row)
|
||||||
|
|
||||||
void CookieJar::insert_cookie_into_database(Web::Cookie::Cookie const& cookie)
|
void CookieJar::insert_cookie_into_database(Web::Cookie::Cookie const& cookie)
|
||||||
{
|
{
|
||||||
m_database.execute_statement(
|
m_storage.visit(
|
||||||
m_statements.insert_cookie, {}, [this]() { purge_expired_cookies(); }, {},
|
[&](PersistedStorage& storage) {
|
||||||
cookie.name,
|
storage.database.execute_statement(
|
||||||
cookie.value,
|
storage.statements.insert_cookie, {}, [this]() { purge_expired_cookies(); }, {},
|
||||||
to_underlying(cookie.same_site),
|
cookie.name,
|
||||||
cookie.creation_time.to_seconds(),
|
cookie.value,
|
||||||
cookie.last_access_time.to_seconds(),
|
to_underlying(cookie.same_site),
|
||||||
cookie.expiry_time.to_seconds(),
|
cookie.creation_time.to_seconds(),
|
||||||
cookie.domain,
|
cookie.last_access_time.to_seconds(),
|
||||||
cookie.path,
|
cookie.expiry_time.to_seconds(),
|
||||||
cookie.secure,
|
cookie.domain,
|
||||||
cookie.http_only,
|
cookie.path,
|
||||||
cookie.host_only,
|
cookie.secure,
|
||||||
cookie.persistent);
|
cookie.http_only,
|
||||||
|
cookie.host_only,
|
||||||
|
cookie.persistent);
|
||||||
|
},
|
||||||
|
[&](TransientStorage& storage) {
|
||||||
|
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
|
||||||
|
storage.set(key, cookie);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CookieJar::update_cookie_in_database(Web::Cookie::Cookie const& cookie)
|
void CookieJar::update_cookie_in_database(Web::Cookie::Cookie const& cookie)
|
||||||
{
|
{
|
||||||
m_database.execute_statement(
|
m_storage.visit(
|
||||||
m_statements.update_cookie, {}, [this]() { purge_expired_cookies(); }, {},
|
[&](PersistedStorage& storage) {
|
||||||
cookie.value,
|
storage.database.execute_statement(
|
||||||
to_underlying(cookie.same_site),
|
storage.statements.update_cookie, {}, [this]() { purge_expired_cookies(); }, {},
|
||||||
cookie.creation_time.to_seconds(),
|
cookie.value,
|
||||||
cookie.last_access_time.to_seconds(),
|
to_underlying(cookie.same_site),
|
||||||
cookie.expiry_time.to_seconds(),
|
cookie.creation_time.to_seconds(),
|
||||||
cookie.secure,
|
cookie.last_access_time.to_seconds(),
|
||||||
cookie.http_only,
|
cookie.expiry_time.to_seconds(),
|
||||||
cookie.host_only,
|
cookie.secure,
|
||||||
cookie.persistent,
|
cookie.http_only,
|
||||||
cookie.name,
|
cookie.host_only,
|
||||||
cookie.domain,
|
cookie.persistent,
|
||||||
cookie.path);
|
cookie.name,
|
||||||
|
cookie.domain,
|
||||||
|
cookie.path);
|
||||||
|
},
|
||||||
|
[&](TransientStorage& storage) {
|
||||||
|
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
|
||||||
|
storage.set(key, cookie);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WrappedCookie : public RefCounted<WrappedCookie> {
|
struct WrappedCookie : public RefCounted<WrappedCookie> {
|
||||||
|
@ -535,55 +559,88 @@ struct WrappedCookie : public RefCounted<WrappedCookie> {
|
||||||
|
|
||||||
void CookieJar::select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results)
|
void CookieJar::select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results)
|
||||||
{
|
{
|
||||||
auto wrapped_cookie = make_ref_counted<WrappedCookie>(move(cookie));
|
m_storage.visit(
|
||||||
|
[&](PersistedStorage& storage) {
|
||||||
|
auto wrapped_cookie = make_ref_counted<WrappedCookie>(move(cookie));
|
||||||
|
|
||||||
m_database.execute_statement(
|
storage.database.execute_statement(
|
||||||
m_statements.select_cookie,
|
storage.statements.select_cookie,
|
||||||
[on_result = move(on_result), wrapped_cookie = wrapped_cookie](auto row) {
|
[on_result = move(on_result), wrapped_cookie = wrapped_cookie](auto row) {
|
||||||
if (auto selected_cookie = parse_cookie(row); selected_cookie.is_error())
|
if (auto selected_cookie = parse_cookie(row); selected_cookie.is_error())
|
||||||
dbgln("Failed to parse cookie '{}': {}", selected_cookie.error(), row);
|
dbgln("Failed to parse cookie '{}': {}", selected_cookie.error(), row);
|
||||||
|
else
|
||||||
|
on_result(wrapped_cookie->cookie, selected_cookie.release_value());
|
||||||
|
|
||||||
|
wrapped_cookie->had_any_results = true;
|
||||||
|
},
|
||||||
|
[on_complete_without_results = move(on_complete_without_results), wrapped_cookie = wrapped_cookie]() {
|
||||||
|
if (!wrapped_cookie->had_any_results)
|
||||||
|
on_complete_without_results(move(wrapped_cookie->cookie));
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
wrapped_cookie->cookie.name,
|
||||||
|
wrapped_cookie->cookie.domain,
|
||||||
|
wrapped_cookie->cookie.path);
|
||||||
|
},
|
||||||
|
[&](TransientStorage& storage) {
|
||||||
|
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
|
||||||
|
|
||||||
|
if (auto it = storage.find(key); it != storage.end())
|
||||||
|
on_result(cookie, it->value);
|
||||||
else
|
else
|
||||||
on_result(wrapped_cookie->cookie, selected_cookie.release_value());
|
on_complete_without_results(cookie);
|
||||||
|
});
|
||||||
wrapped_cookie->had_any_results = true;
|
|
||||||
},
|
|
||||||
[on_complete_without_results = move(on_complete_without_results), wrapped_cookie = wrapped_cookie]() {
|
|
||||||
if (!wrapped_cookie->had_any_results)
|
|
||||||
on_complete_without_results(move(wrapped_cookie->cookie));
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
wrapped_cookie->cookie.name,
|
|
||||||
wrapped_cookie->cookie.domain,
|
|
||||||
wrapped_cookie->cookie.path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CookieJar::select_all_cookies_from_database(OnSelectAllCookiesResult on_result)
|
void CookieJar::select_all_cookies_from_database(OnSelectAllCookiesResult on_result)
|
||||||
{
|
{
|
||||||
// FIXME: Make surrounding APIs asynchronous.
|
// FIXME: Make surrounding APIs asynchronous.
|
||||||
auto promise = Core::Promise<Empty>::construct();
|
m_storage.visit(
|
||||||
|
[&](PersistedStorage& storage) {
|
||||||
|
auto promise = Core::Promise<Empty>::construct();
|
||||||
|
|
||||||
m_database.execute_statement(
|
storage.database.execute_statement(
|
||||||
m_statements.select_all_cookies,
|
storage.statements.select_all_cookies,
|
||||||
[on_result = move(on_result)](auto row) {
|
[on_result = move(on_result)](auto row) {
|
||||||
if (auto cookie = parse_cookie(row); cookie.is_error())
|
if (auto cookie = parse_cookie(row); cookie.is_error())
|
||||||
dbgln("Failed to parse cookie '{}': {}", cookie.error(), row);
|
dbgln("Failed to parse cookie '{}': {}", cookie.error(), row);
|
||||||
else
|
else
|
||||||
on_result(cookie.release_value());
|
on_result(cookie.release_value());
|
||||||
|
},
|
||||||
|
[&]() {
|
||||||
|
MUST(promise->resolve({}));
|
||||||
|
},
|
||||||
|
[&](auto) {
|
||||||
|
MUST(promise->resolve({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
MUST(promise->await());
|
||||||
},
|
},
|
||||||
[&]() {
|
[&](TransientStorage& storage) {
|
||||||
MUST(promise->resolve({}));
|
for (auto const& cookie : storage)
|
||||||
},
|
on_result(cookie.value);
|
||||||
[&](auto) {
|
|
||||||
MUST(promise->resolve({}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MUST(promise->await());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CookieJar::purge_expired_cookies()
|
void CookieJar::purge_expired_cookies()
|
||||||
{
|
{
|
||||||
auto now = Time::now_realtime().to_seconds();
|
auto now = Time::now_realtime().to_seconds();
|
||||||
m_database.execute_statement(m_statements.expire_cookie, {}, {}, {}, now);
|
|
||||||
|
m_storage.visit(
|
||||||
|
[&](PersistedStorage& storage) {
|
||||||
|
storage.database.execute_statement(storage.statements.expire_cookie, {}, {}, {}, now);
|
||||||
|
},
|
||||||
|
[&](TransientStorage& storage) {
|
||||||
|
Vector<CookieStorageKey> keys_to_evict;
|
||||||
|
|
||||||
|
for (auto const& cookie : storage) {
|
||||||
|
if (cookie.value.expiry_time.to_seconds() < now)
|
||||||
|
keys_to_evict.append(cookie.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto const& key : keys_to_evict)
|
||||||
|
storage.remove(key);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021-2022, Tim Flynn <trflynn89@serenityos.org>
|
* Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "Forward.h"
|
||||||
#include <AK/DeprecatedString.h>
|
#include <AK/DeprecatedString.h>
|
||||||
#include <AK/Function.h>
|
#include <AK/Function.h>
|
||||||
#include <AK/HashMap.h>
|
#include <AK/HashMap.h>
|
||||||
#include <AK/Optional.h>
|
#include <AK/Optional.h>
|
||||||
|
#include <AK/Traits.h>
|
||||||
#include <LibCore/DateTime.h>
|
#include <LibCore/DateTime.h>
|
||||||
#include <LibSQL/Type.h>
|
#include <LibSQL/Type.h>
|
||||||
#include <LibWeb/Cookie/Cookie.h>
|
#include <LibWeb/Cookie/Cookie.h>
|
||||||
|
@ -19,6 +21,14 @@ namespace Browser {
|
||||||
|
|
||||||
class Database;
|
class Database;
|
||||||
|
|
||||||
|
struct CookieStorageKey {
|
||||||
|
bool operator==(CookieStorageKey const&) const = default;
|
||||||
|
|
||||||
|
DeprecatedString name;
|
||||||
|
DeprecatedString domain;
|
||||||
|
DeprecatedString path;
|
||||||
|
};
|
||||||
|
|
||||||
class CookieJar {
|
class CookieJar {
|
||||||
struct Statements {
|
struct Statements {
|
||||||
SQL::StatementID create_table { 0 };
|
SQL::StatementID create_table { 0 };
|
||||||
|
@ -29,8 +39,16 @@ class CookieJar {
|
||||||
SQL::StatementID select_all_cookies { 0 };
|
SQL::StatementID select_all_cookies { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PersistedStorage {
|
||||||
|
Database& database;
|
||||||
|
Statements statements;
|
||||||
|
};
|
||||||
|
|
||||||
|
using TransientStorage = HashMap<CookieStorageKey, Web::Cookie::Cookie>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static ErrorOr<CookieJar> create(Database& database);
|
static ErrorOr<CookieJar> create(Database&);
|
||||||
|
static CookieJar create();
|
||||||
|
|
||||||
DeprecatedString get_cookie(const URL& url, Web::Cookie::Source source);
|
DeprecatedString get_cookie(const URL& url, Web::Cookie::Source source);
|
||||||
void set_cookie(const URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source);
|
void set_cookie(const URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source);
|
||||||
|
@ -41,7 +59,8 @@ public:
|
||||||
Optional<Web::Cookie::Cookie> get_named_cookie(URL const& url, DeprecatedString const& name);
|
Optional<Web::Cookie::Cookie> get_named_cookie(URL const& url, DeprecatedString const& name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CookieJar(Database& database, Statements statements);
|
explicit CookieJar(PersistedStorage);
|
||||||
|
explicit CookieJar(TransientStorage);
|
||||||
|
|
||||||
static Optional<DeprecatedString> canonicalize_domain(const URL& url);
|
static Optional<DeprecatedString> canonicalize_domain(const URL& url);
|
||||||
static bool domain_matches(DeprecatedString const& string, DeprecatedString const& domain_string);
|
static bool domain_matches(DeprecatedString const& string, DeprecatedString const& domain_string);
|
||||||
|
@ -68,8 +87,19 @@ private:
|
||||||
|
|
||||||
void purge_expired_cookies();
|
void purge_expired_cookies();
|
||||||
|
|
||||||
Database& m_database;
|
Variant<PersistedStorage, TransientStorage> m_storage;
|
||||||
Statements m_statements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct AK::Traits<Browser::CookieStorageKey> : public AK::GenericTraits<Browser::CookieStorageKey> {
|
||||||
|
static unsigned hash(Browser::CookieStorageKey const& 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
25
Userland/Applications/Browser/Forward.h
Normal file
25
Userland/Applications/Browser/Forward.h
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Traits.h>
|
||||||
|
|
||||||
|
namespace Browser {
|
||||||
|
|
||||||
|
class CookieJar;
|
||||||
|
class Database;
|
||||||
|
|
||||||
|
struct CookieStorageKey;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AK {
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct Traits<Browser::CookieStorageKey>;
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue