/* * Copyright (c) 2026, Ladybird contributors * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include namespace DevTools { static constexpr auto cookie_unique_key_separator = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"sv; static constexpr auto max_store_object_count = 50uz; static constexpr auto session_cookie_label = "Session"sv; static constexpr auto default_cookie_value = "value"sv; static bool cookie_matches_storage_host(HTTP::Cookie::Cookie const& cookie, String const& storage_host) { auto host_name = storage_host_name(storage_host); if (!host_name.has_value()) return false; if (cookie.host_only) return cookie.domain == *host_name; return HTTP::Cookie::domain_matches(*host_name, cookie.domain); } static String cookie_unique_key(HTTP::Cookie::Cookie const& cookie) { return MUST(String::formatted("{}{}{}{}{}{}", cookie.name, cookie_unique_key_separator, cookie.domain, cookie_unique_key_separator, cookie.path, cookie_unique_key_separator)); } struct ParsedCookieUniqueKey { String name; String domain; String path; }; static Optional parse_cookie_unique_key(StringView unique_key) { auto parts = unique_key.split_view(cookie_unique_key_separator, SplitBehavior::KeepEmpty); if (parts.size() < 3) return {}; return ParsedCookieUniqueKey { MUST(String::from_utf8(parts[0])), MUST(String::from_utf8(parts[1])), MUST(String::from_utf8(parts[2])), }; } static bool cookie_matches_unique_key(HTTP::Cookie::Cookie const& cookie, ParsedCookieUniqueKey const& unique_key) { return cookie.name == unique_key.name && cookie.domain == unique_key.domain && cookie.path == unique_key.path; } static bool cookie_matches_requested_domain(HTTP::Cookie::Cookie const& cookie, Optional const& domain) { if (!domain.has_value()) return true; return cookie.domain == *domain; } static String same_site_for_devtools(HTTP::Cookie::SameSite same_site) { if (same_site == HTTP::Cookie::SameSite::Default) return {}; return MUST(String::from_utf8(HTTP::Cookie::same_site_to_string(same_site))); } static JsonObject serialize_cookie(HTTP::Cookie::Cookie const& cookie) { JsonObject object; object.set("uniqueKey"sv, cookie_unique_key(cookie)); object.set("name"sv, cookie.name); object.set("value"sv, cookie.value); object.set("host"sv, cookie.domain); object.set("path"sv, cookie.path); object.set("expires"sv, cookie.persistent ? cookie.expiry_time.milliseconds_since_epoch() : 0); object.set("size"sv, cookie.name.bytes().size() + cookie.value.bytes().size()); object.set("isHttpOnly"sv, cookie.http_only); object.set("isSecure"sv, cookie.secure); object.set("sameSite"sv, same_site_for_devtools(cookie.same_site)); object.set("lastAccessed"sv, cookie.last_access_time.milliseconds_since_epoch()); object.set("creationTime"sv, cookie.creation_time.milliseconds_since_epoch()); object.set("updateTime"sv, cookie.creation_time.milliseconds_since_epoch()); object.set("hostOnly"sv, cookie.host_only); object.set("partitionKey"sv, ""sv); return object; } static Optional string_from_devtools_value(JsonValue const& value) { if (value.is_string()) return value.as_string(); if (value.is_bool()) return value.as_bool() ? "true"_string : "false"_string; if (value.is_number()) return MUST(String::formatted("{}", value.get_number_with_precision_loss().release_value())); if (value.is_null()) return {}; return value.serialized(); } static Optional bool_from_devtools_value(JsonValue const& value) { if (value.is_bool()) return value.as_bool(); if (!value.is_string()) return {}; auto string = value.as_string().bytes_as_string_view(); if (string.equals_ignoring_ascii_case("true"sv)) return true; if (string.equals_ignoring_ascii_case("false"sv)) return false; return {}; } static void set_session_cookie_expiry(HTTP::Cookie::Cookie& cookie) { cookie.persistent = false; cookie.expiry_time = UnixDateTime::from_unix_time_parts(3000, 1, 1, 0, 0, 0, 0); } static Optional set_cookie_expiry_from_devtools(HTTP::Cookie::Cookie& cookie, JsonValue const& json_value, StringView value) { if (value.is_empty() || value == session_cookie_label) { set_session_cookie_expiry(cookie); return {}; } if (json_value.is_number()) { cookie.persistent = true; cookie.expiry_time = UnixDateTime::from_milliseconds_since_epoch(json_value.get_number_with_precision_loss().release_value()); return {}; } if (auto milliseconds_since_epoch = value.to_number(); milliseconds_since_epoch.has_value()) { cookie.persistent = true; cookie.expiry_time = UnixDateTime::from_milliseconds_since_epoch(*milliseconds_since_epoch); return {}; } auto expiry_time = HTTP::Cookie::parse_cookie_date(value); if (!expiry_time.has_value()) return "Cookie expiry date is invalid"_string; cookie.persistent = true; cookie.expiry_time = *expiry_time; return {}; } NonnullRefPtr CookiesActor::create(DevToolsServer& devtools, String name, WeakPtr tab) { return adopt_ref(*new CookiesActor(devtools, move(name), move(tab))); } CookiesActor::CookiesActor(DevToolsServer& devtools, String name, WeakPtr tab) : Actor(devtools, move(name)) , m_tab(move(tab)) { if (auto tab = m_tab.strong_ref()) { devtools.delegate().listen_for_host_cookie_changes( tab->description(), weak_callback(*this, [](auto& self, Vector cookies) { self.on_cookies_changed(move(cookies)); })); } } CookiesActor::~CookiesActor() { if (auto tab = m_tab.strong_ref()) devtools().delegate().stop_listening_for_host_cookie_changes(tab->description()); } Optional CookiesActor::host() const { auto tab = m_tab.strong_ref(); if (!tab) return {}; return storage_host_for_url(tab->description().url); } JsonObject CookiesActor::serialize_storage() const { JsonObject hosts; if (auto storage_host = host(); storage_host.has_value()) hosts.set(*storage_host, JsonArray {}); JsonObject traits; traits.set("supportsAddItem"sv, true); traits.set("supportsRemoveAll"sv, true); traits.set("supportsRemoveAllSessionCookies"sv, true); traits.set("supportsRemoveItem"sv, true); JsonObject storage; storage.set("actor"sv, name()); if (auto tab = m_tab.strong_ref()) { storage.set("browsingContextID"sv, tab->description().id); storage.set("innerWindowId"sv, tab->inner_window_id()); storage.set("resourceId"sv, MUST(String::formatted("cookies-{}", tab->inner_window_id()))); } storage.set("hosts"sv, move(hosts)); storage.set("resourceKey"sv, "cookies"sv); storage.set("traits"sv, move(traits)); return storage; } void CookiesActor::handle_message(Message const& message) { if (message.type == "getFields"sv) { get_fields(message); return; } if (message.type == "getStoreObjects"sv) { get_store_objects(message); return; } if (message.type == "editItem"sv) { edit_item(message); return; } if (message.type == "addItem"sv) { add_item(message); return; } if (message.type == "removeItem"sv) { remove_item(message); return; } if (message.type == "removeAll"sv) { remove_all(message); return; } if (message.type == "removeAllSessionCookies"sv) { remove_all_session_cookies(message); return; } send_unrecognized_packet_type_error(message); } void CookiesActor::get_fields(Message const& message) { JsonArray fields; fields.must_append(define_storage_field("uniqueKey"sv, StorageFieldType::Private)); fields.must_append(define_storage_field("name"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("value"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("host"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("path"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("expires"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("size"sv, StorageFieldType::Immutable)); fields.must_append(define_storage_field("isHttpOnly"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("isSecure"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("sameSite"sv, StorageFieldType::Immutable)); fields.must_append(define_storage_field("lastAccessed"sv, StorageFieldType::Immutable)); fields.must_append(define_storage_field("creationTime"sv, StorageFieldType::Hidden)); fields.must_append(define_storage_field("updateTime"sv, StorageFieldType::Hidden)); fields.must_append(define_storage_field("hostOnly"sv, StorageFieldType::Hidden)); fields.must_append(define_storage_field("partitionKey"sv, StorageFieldType::Immutable)); JsonObject response; response.set("value"sv, move(fields)); send_response(message, move(response)); } void CookiesActor::get_store_objects(Message const& message) { auto host = get_required_parameter(message, "host"sv); if (!host.has_value()) return; auto tab = m_tab.strong_ref(); if (!tab) return; auto requested_names = message.data.get_array("names"sv); Vector cookies; for (auto& cookie : devtools().delegate().cookies(tab->description())) { if (!cookie_matches_storage_host(cookie, *host)) continue; if (requested_names.has_value()) { auto unique_key = cookie_unique_key(cookie); auto was_requested = any_of(requested_names->values(), [&](auto const& name) { return name.is_string() && name.as_string() == unique_key; }); if (!was_requested) continue; } cookies.append(move(cookie)); } if (!requested_names.has_value()) { HashTable visible_keys; for (auto const& cookie : cookies) visible_keys.set(cookie_unique_key(cookie)); m_visible_cookie_unique_keys.set(*host, move(visible_keys)); } quick_sort(cookies, [](auto const& left, auto const& right) { if (left.name != right.name) return left.name < right.name; if (left.domain != right.domain) return left.domain < right.domain; if (left.path.bytes().size() != right.path.bytes().size()) return left.path.bytes().size() < right.path.bytes().size(); return cookie_unique_key(left) < cookie_unique_key(right); }); size_t offset = 0; size_t size = max_store_object_count; if (auto options = message.data.get_object("options"sv); options.has_value()) { offset = options->get_integer("offset"sv).value_or(0); size = min(options->get_integer("size"sv).value_or(max_store_object_count), max_store_object_count); } JsonArray data; if (offset < cookies.size()) { auto end = min(cookies.size(), offset + size); for (size_t i = offset; i < end; ++i) data.must_append(serialize_cookie(cookies[i])); } else { offset = cookies.size(); } JsonObject response; response.set("offset"sv, offset); response.set("total"sv, cookies.size()); response.set("data"sv, move(data)); send_response(message, move(response)); } void CookiesActor::edit_item(Message const& message) { auto data = get_required_parameter(message, "data"sv); if (!data.has_value()) return; auto tab = m_tab.strong_ref(); if (!tab) return; auto field = data->get_string("field"sv); if (!field.has_value()) { send_missing_parameter_error(message, "field"sv); return; } auto new_value_json = data->get("newValue"sv); if (!new_value_json.has_value()) { send_missing_parameter_error(message, "newValue"sv); return; } auto new_value = string_from_devtools_value(*new_value_json); if (!new_value.has_value()) { send_response(message, to_storage_operation_result("Missing cookie value"_string)); return; } auto cookies = devtools().delegate().cookies(tab->description()); auto find_cookie = [&](auto const& predicate) -> Optional { auto cookie = cookies.first_matching(predicate); if (!cookie.has_value()) return {}; return *cookie; }; Optional old_cookie; auto items = data->get_object("items"sv); if (items.has_value()) { if (auto unique_key_string = items->get_string("uniqueKey"sv); unique_key_string.has_value()) { if (auto unique_key = parse_cookie_unique_key(unique_key_string->bytes_as_string_view()); unique_key.has_value()) old_cookie = find_cookie([&](auto const& cookie) { return cookie_matches_unique_key(cookie, *unique_key); }); } if (!old_cookie.has_value()) { Optional old_value; if (auto old_value_json = data->get("oldValue"sv); old_value_json.has_value()) old_value = string_from_devtools_value(*old_value_json); Optional name; if (*field == "name"sv) { name = old_value; } else if (auto item_name = items->get_string("name"sv); item_name.has_value()) { name = *item_name; } Optional domain; if (*field == "host"sv) { domain = old_value; } else if (auto item_host = items->get_string("host"sv); item_host.has_value()) { domain = *item_host; } Optional path; if (*field == "path"sv) { path = old_value; } else if (auto item_path = items->get_string("path"sv); item_path.has_value()) { path = *item_path; } if (name.has_value() && domain.has_value() && path.has_value()) { old_cookie = find_cookie([&](auto const& cookie) { return cookie.name == *name && cookie.domain == *domain && cookie.path == *path; }); } } } if (!old_cookie.has_value()) { send_response(message, to_storage_operation_result(Optional {})); return; } auto edited_cookie = *old_cookie; if (*field == "name"sv) { edited_cookie.name = new_value.release_value(); } else if (*field == "value"sv) { edited_cookie.value = new_value.release_value(); } else if (*field == "host"sv) { edited_cookie.domain = new_value.release_value(); } else if (*field == "path"sv) { edited_cookie.path = new_value.release_value(); } else if (*field == "expires"sv) { if (auto error_string = set_cookie_expiry_from_devtools(edited_cookie, *new_value_json, new_value->bytes_as_string_view()); error_string.has_value()) { send_response(message, to_storage_operation_result(move(error_string))); return; } } else if (*field == "isSecure"sv) { auto value = bool_from_devtools_value(*new_value_json); if (!value.has_value()) { send_response(message, to_storage_operation_result("Cookie secure flag must be true or false"_string)); return; } edited_cookie.secure = *value; } else if (*field == "isHttpOnly"sv) { auto value = bool_from_devtools_value(*new_value_json); if (!value.has_value()) { send_response(message, to_storage_operation_result("Cookie HTTP-only flag must be true or false"_string)); return; } edited_cookie.http_only = *value; } else if (*field == "sameSite"sv) { edited_cookie.same_site = HTTP::Cookie::same_site_from_string(new_value->bytes_as_string_view()); } else { send_response(message, to_storage_operation_result(Optional {})); return; } auto result = devtools().delegate().set_cookie(tab->description(), old_cookie, move(edited_cookie)); send_response(message, to_storage_operation_result(move(result))); } void CookiesActor::add_item(Message const& message) { auto guid = get_required_parameter(message, "guid"sv); if (!guid.has_value()) return; auto storage_host = message.data.get_string("host"sv) .value_or_lazy_evaluated_optional([this] { return host(); }); if (!storage_host.has_value()) { send_response(message, to_storage_operation_result("No storage host is available for this page"_string)); return; } auto storage_url = URL::Parser::basic_parse(*storage_host); auto host_name = storage_host_name(*storage_host); if (!storage_url.has_value() || !host_name.has_value()) { send_response(message, to_storage_operation_result("Cannot add a cookie for this storage host"_string)); return; } auto tab = m_tab.strong_ref(); if (!tab) return; auto now = UnixDateTime::now(); HTTP::Cookie::Cookie cookie; cookie.name = move(*guid); cookie.value = MUST(String::from_utf8(default_cookie_value)); cookie.same_site = HTTP::Cookie::SameSite::Lax; cookie.creation_time = now; cookie.last_access_time = now; set_session_cookie_expiry(cookie); cookie.domain = move(*host_name); cookie.path = "/"_string; cookie.secure = storage_url->scheme() == "https"sv; cookie.host_only = true; auto result = devtools().delegate().set_cookie(tab->description(), {}, move(cookie)); send_response(message, to_storage_operation_result(result)); } void CookiesActor::remove_item(Message const& message) { auto host = get_required_parameter(message, "host"sv); if (!host.has_value()) return; auto name = get_required_parameter(message, "name"sv); if (!name.has_value()) return; auto unique_key = parse_cookie_unique_key(name->bytes_as_string_view()); if (!unique_key.has_value()) { send_response(message, {}); return; } auto tab = m_tab.strong_ref(); if (!tab) return; Vector cookies_to_delete; for (auto& cookie : devtools().delegate().cookies(tab->description())) { if (!cookie_matches_storage_host(cookie, *host)) continue; if (!cookie_matches_unique_key(cookie, *unique_key)) continue; cookies_to_delete.append(move(cookie)); } devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete)); send_response(message, {}); } void CookiesActor::remove_all(Message const& message) { auto host = get_required_parameter(message, "host"sv); if (!host.has_value()) return; Optional domain; if (auto requested_domain = message.data.get_string("domain"sv); requested_domain.has_value()) domain = *requested_domain; auto tab = m_tab.strong_ref(); if (!tab) return; Vector cookies_to_delete; for (auto& cookie : devtools().delegate().cookies(tab->description())) { if (!cookie_matches_storage_host(cookie, *host)) continue; if (!cookie_matches_requested_domain(cookie, domain)) continue; cookies_to_delete.append(move(cookie)); } devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete)); send_response(message, {}); } void CookiesActor::remove_all_session_cookies(Message const& message) { auto host = get_required_parameter(message, "host"sv); if (!host.has_value()) return; Optional domain; if (auto requested_domain = message.data.get_string("domain"sv); requested_domain.has_value()) domain = *requested_domain; auto tab = m_tab.strong_ref(); if (!tab) return; Vector cookies_to_delete; for (auto& cookie : devtools().delegate().cookies(tab->description())) { if (!cookie_matches_storage_host(cookie, *host)) continue; if (!cookie_matches_requested_domain(cookie, domain)) continue; if (cookie.persistent) continue; cookies_to_delete.append(move(cookie)); } devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete)); send_response(message, {}); } HashTable CookiesActor::visible_cookie_unique_keys(String const& host) const { HashTable visible_keys; auto tab = m_tab.strong_ref(); if (!tab) return visible_keys; for (auto& cookie : devtools().delegate().cookies(tab->description())) { if (cookie_matches_storage_host(cookie, host)) visible_keys.set(cookie_unique_key(cookie)); } return visible_keys; } void CookiesActor::send_cookie_store_update(String const& host, JsonArray added, JsonArray changed, JsonArray deleted) { JsonObject update; auto append_update = [&](StringView update_type, JsonArray keys) { if (keys.is_empty()) return; JsonObject hosts; hosts.set(host, move(keys)); JsonObject cookies; cookies.set("cookies"sv, move(hosts)); update.set(update_type, move(cookies)); }; append_update("added"sv, move(added)); append_update("changed"sv, move(changed)); append_update("deleted"sv, move(deleted)); JsonObject message; message.set("type"sv, "storesUpdate"sv); message.set("data"sv, move(update)); send_message(move(message)); } void CookiesActor::on_cookies_changed(Vector changed_cookies) { auto storage_host = host(); if (!storage_host.has_value()) return; HashTable old_visible_keys; if (auto it = m_visible_cookie_unique_keys.find(*storage_host); it != m_visible_cookie_unique_keys.end()) old_visible_keys = it->value; auto current_visible_keys = visible_cookie_unique_keys(*storage_host); JsonArray added; JsonArray changed; JsonArray deleted; for (auto const& cookie : changed_cookies) { if (!cookie_matches_storage_host(cookie, *storage_host)) continue; auto unique_key = cookie_unique_key(cookie); auto was_visible = old_visible_keys.contains(unique_key); auto is_visible = current_visible_keys.contains(unique_key); if (is_visible && was_visible) changed.must_append(unique_key); else if (is_visible) added.must_append(unique_key); else if (was_visible) deleted.must_append(unique_key); } m_visible_cookie_unique_keys.set(*storage_host, move(current_visible_keys)); if (added.is_empty() && changed.is_empty() && deleted.is_empty()) return; send_cookie_store_update(*storage_host, move(added), move(changed), move(deleted)); } }