electron/shell/browser/api/electron_api_cookies.cc
electron-roller[bot] 2dd4b77ae0
chore: bump chromium to 136.0.7095.0 (36-x-y) (#46184)
* chore: bump chromium in DEPS to 136.0.7081.1

* chore: bump chromium in DEPS to 136.0.7083.1

* chore: bump chromium in DEPS to 136.0.7085.1

* chore: bump chromium in DEPS to 136.0.7087.1

* chore: bump chromium in DEPS to 136.0.7089.0

* chore: bump chromium in DEPS to 136.0.7091.0

* chore: bump chromium in DEPS to 136.0.7092.0

* chore: bump chromium in DEPS to 136.0.7093.1

* chore: bump chromium in DEPS to 136.0.7095.1

* chore: bump chromium in DEPS to 136.0.7097.1

* chore: bump chromium in DEPS to 136.0.7099.1

* chore: bump chromium in DEPS to 136.0.7101.0

* chore: bump chromium in DEPS to 136.0.7103.0

* chore: bump chromium in DEPS to 136.0.7103.15

* chore: bump chromium in DEPS to 136.0.7103.17

* chore: bump chromium to 136.0.7095.0 (main) (#46118)

* chore: bump chromium in DEPS to 136.0.7076.0

* chore: bump chromium in DEPS to 136.0.7077.0

* 6368856: Migrate absl variant.h and utility.h in content (part 2/2) | 6368856

* 6356528: Clean up LegacyRenderWidgetHostHWND code | 6356528

* chore: export patches

* 6339113: [Viewport Segments] Add CDP commands to override Viewport Segments without overriding other device properties. | 6339113

* 6352169: [DevTools][MultiInstance] Support new tab in another window on Android | 6352169

* 6368856: Migrate absl variant.h and utility.h in content (part 2/2) | 6368856

* 6360858:Clickiness: Wire response from URLLoader to DB, add e2e tests| 6360858

* chore: bump chromium in DEPS to 136.0.7079.0

* chore: export patches

* chore: bump chromium in DEPS to 136.0.7081.0

* chore: export patches

* chore: bump chromium in DEPS to 136.0.7083.0

* 6361987: Remove double-declaration with gfx::NativeView and gfx::NativeWindow | 6361987

* chore: export patches

* chore: bump chromium in DEPS to 136.0.7087.0

* chore: export patches

* fix: include node patch for missing AtomicsWaitEvent
6385540

* build: add depot_tools python to path

* fix: cppgc init and unregistering v8 isolate

6333562

CppGc is now initialized earlier so Node can skip reinitializing it.

Additionally, gin::IsolateHandle was attempting to destruct an already destructed
v8::Isolate upon electron::JavaScriptEnvironment destruction. By removing the call
to NodePlatform::UnregisterIsolate, this fixes the crash on app shutdown.

* fix: unregister isolate after destruction

See code comment.

* chore: bump chromium in DEPS to 136.0.7095.0

* chore: sync patches

* fix: add script_parsing::ContentScriptType parameter
6298395

* fix: migrate content::BrowserAccessibilityState methods
6401437
6383275

* feat: enableHappyEyeballs option for host resolver
6332599

* fix: add new cookie exclusion reason
6343479

* fix: add new url loader method
6337340

* fix: add new cppgc header file for electron_node headers
6348644

* fix: disable CREL on Linux ARM64
https://chromium-review.googlesource.com/q/I3a62f02f564f07be63173b0773b4ecaffbe939b9

* fixup! fix: add new cppgc header file for electron_node headers 6348644

* chore: update corner smoothing patch

* fixup! chore: update corner smoothing patch

* chore: disable NAN weak tests

These two tests are incompatible with a V8 change that disallows running JS code from a weak finalizer callback.

Ref: 4733273

* test: fix task starvation in node test

A V8 change makes these contexts get collected in a task that is posted
and run asynchronously. The tests were synchronously GC'ing in an
infinite loop, preventing the task loop from running the task that would
GC these contexts.

This change should be upstreamed in some way.

Ref: 4733273

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: alice <alice@makenotion.com>
Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
Co-authored-by: clavin <clavin@electronjs.org>
(cherry picked from commit 9c019b6147)

* Remove file-wide unsafe buffer suppression from content/ [3 of N]

6341711

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
2025-04-07 13:09:35 -05:00

486 lines
19 KiB
C++

// Copyright (c) 2015 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_cookies.h"
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include "base/containers/fixed_flat_map.h"
#include "base/time/time.h"
#include "base/values.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "gin/dictionary.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_change_dispatcher.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_util.h"
#include "shell/browser/cookie_change_notifier.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
namespace gin {
template <>
struct Converter<net::CookieSameSite> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CookieSameSite& val) {
switch (val) {
case net::CookieSameSite::UNSPECIFIED:
return ConvertToV8(isolate, "unspecified");
case net::CookieSameSite::NO_RESTRICTION:
return ConvertToV8(isolate, "no_restriction");
case net::CookieSameSite::LAX_MODE:
return ConvertToV8(isolate, "lax");
case net::CookieSameSite::STRICT_MODE:
return ConvertToV8(isolate, "strict");
}
DCHECK(false);
return ConvertToV8(isolate, "unknown");
}
};
template <>
struct Converter<net::CanonicalCookie> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CanonicalCookie& val) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("name", val.Name());
dict.Set("value", val.Value());
dict.Set("domain", val.Domain());
dict.Set("hostOnly", net::cookie_util::DomainIsHostOnly(val.Domain()));
dict.Set("path", val.Path());
dict.Set("secure", val.SecureAttribute());
dict.Set("httpOnly", val.IsHttpOnly());
dict.Set("session", !val.IsPersistent());
if (val.IsPersistent())
dict.Set("expirationDate", val.ExpiryDate().InSecondsFSinceUnixEpoch());
dict.Set("sameSite", val.SameSite());
return ConvertToV8(isolate, dict).As<v8::Object>();
}
};
template <>
struct Converter<net::CookieChangeCause> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::CookieChangeCause& val) {
switch (val) {
case net::CookieChangeCause::INSERTED:
case net::CookieChangeCause::EXPLICIT:
return gin::StringToV8(isolate, "explicit");
case net::CookieChangeCause::OVERWRITE:
return gin::StringToV8(isolate, "overwrite");
case net::CookieChangeCause::EXPIRED:
return gin::StringToV8(isolate, "expired");
case net::CookieChangeCause::EVICTED:
return gin::StringToV8(isolate, "evicted");
case net::CookieChangeCause::EXPIRED_OVERWRITE:
return gin::StringToV8(isolate, "expired-overwrite");
default:
return gin::StringToV8(isolate, "unknown");
}
}
};
} // namespace gin
namespace electron::api {
namespace {
// Returns whether |domain| matches |filter|.
bool MatchesDomain(std::string filter, const std::string& domain) {
// Add a leading '.' character to the filter domain if it doesn't exist.
if (net::cookie_util::DomainIsHostOnly(filter))
filter.insert(0, ".");
std::string sub_domain(domain);
// Strip any leading '.' character from the input cookie domain.
if (!net::cookie_util::DomainIsHostOnly(sub_domain))
sub_domain = sub_domain.substr(1);
// Now check whether the domain argument is a subdomain of the filter domain.
for (sub_domain.insert(0, "."); sub_domain.length() >= filter.length();) {
if (sub_domain == filter)
return true;
const size_t next_dot = sub_domain.find('.', 1); // Skip over leading dot.
sub_domain.erase(0, next_dot);
}
return false;
}
// Returns whether |cookie| matches |filter|.
bool MatchesCookie(const base::Value::Dict& filter,
const net::CanonicalCookie& cookie) {
const std::string* str;
if ((str = filter.FindString("name")) && *str != cookie.Name())
return false;
if ((str = filter.FindString("path")) && *str != cookie.Path())
return false;
if ((str = filter.FindString("domain")) &&
!MatchesDomain(*str, cookie.Domain()))
return false;
std::optional<bool> secure_filter = filter.FindBool("secure");
if (secure_filter && *secure_filter != cookie.SecureAttribute())
return false;
std::optional<bool> session_filter = filter.FindBool("session");
if (session_filter && *session_filter == cookie.IsPersistent())
return false;
std::optional<bool> httpOnly_filter = filter.FindBool("httpOnly");
if (httpOnly_filter && *httpOnly_filter != cookie.IsHttpOnly())
return false;
return true;
}
// Remove cookies from |list| not matching |filter|, and pass it to |callback|.
void FilterCookies(base::Value::Dict filter,
gin_helper::Promise<net::CookieList> promise,
const net::CookieList& cookies) {
net::CookieList result;
for (const auto& cookie : cookies) {
if (MatchesCookie(filter, cookie))
result.push_back(cookie);
}
promise.Resolve(result);
}
void FilterCookieWithStatuses(
base::Value::Dict filter,
gin_helper::Promise<net::CookieList> promise,
const net::CookieAccessResultList& list,
const net::CookieAccessResultList& excluded_list) {
FilterCookies(std::move(filter), std::move(promise),
net::cookie_util::StripAccessResults(list));
}
// Parse dictionary property to CanonicalCookie time correctly.
base::Time ParseTimeProperty(const std::optional<double>& value) {
if (!value) // empty time means ignoring the parameter
return {};
if (*value == 0) // FromSecondsSinceUnixEpoch would convert 0 to empty Time
return base::Time::UnixEpoch();
return base::Time::FromSecondsSinceUnixEpoch(*value);
}
const std::string InclusionStatusToString(net::CookieInclusionStatus status) {
// See net/cookies/cookie_inclusion_status.h for cookie error descriptions.
using Reason = net::CookieInclusionStatus::ExclusionReason;
static constexpr auto Reasons =
base::MakeFixedFlatMap<Reason, std::string_view>(
{{Reason::EXCLUDE_UNKNOWN_ERROR, "Unknown error"},
{Reason::EXCLUDE_HTTP_ONLY,
"The cookie was HttpOnly, but the attempted access was through a "
"non-HTTP API."},
{Reason::EXCLUDE_SECURE_ONLY,
"The cookie was Secure, but the URL was not allowed to access "
"Secure cookies."},
{Reason::EXCLUDE_DOMAIN_MISMATCH,
"The cookie's domain attribute did not match the domain of the URL "
"attempting access."},
{Reason::EXCLUDE_NOT_ON_PATH,
"The cookie's path attribute did not match the path of the URL "
"attempting access."},
{Reason::EXCLUDE_SAMESITE_STRICT,
"The cookie had SameSite=Strict, and the attempted access did not "
"have an appropriate SameSiteCookieContext"},
{Reason::EXCLUDE_SAMESITE_LAX,
"The cookie had SameSite=Lax, and the attempted access did not "
"have "
"an appropriate SameSiteCookieContext"},
{Reason::EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
"The cookie did not specify a SameSite attribute, and therefore "
"was treated as if it were SameSite=Lax, and the attempted "
"access did not have an appropriate SameSiteCookieContext."},
{Reason::EXCLUDE_SAMESITE_NONE_INSECURE,
"The cookie specified SameSite=None, but it was not Secure."},
{Reason::EXCLUDE_USER_PREFERENCES,
"Caller did not allow access to the cookie."},
{Reason::EXCLUDE_FAILURE_TO_STORE,
"The cookie was malformed and could not be stored"},
{Reason::EXCLUDE_NONCOOKIEABLE_SCHEME,
"Attempted to set a cookie from a scheme that does not support "
"cookies."},
{Reason::EXCLUDE_OVERWRITE_SECURE,
"The cookie would have overwritten a Secure cookie, and was not "
"allowed to do so."},
{Reason::EXCLUDE_OVERWRITE_HTTP_ONLY,
"The cookie would have overwritten an HttpOnly cookie, and was not "
"allowed to do so."},
{Reason::EXCLUDE_INVALID_DOMAIN,
"The cookie was set with an invalid Domain attribute."},
{Reason::EXCLUDE_INVALID_PREFIX,
"The cookie was set with an invalid __Host- or __Secure- prefix."},
{Reason::EXCLUDE_INVALID_PARTITIONED,
"Cookie was set with an invalid Partitioned attribute, which is "
"only valid if the cookie has a __Host- prefix."},
{Reason::EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE,
"The cookie exceeded the name/value pair size limit."},
{Reason::EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,
"Cookie exceeded the attribute size limit."},
{Reason::EXCLUDE_DOMAIN_NON_ASCII,
"The cookie was set with a Domain attribute containing non ASCII "
"characters."},
{Reason::EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET,
"The cookie is blocked by third-party cookie blocking but the two "
"sites are in the same First-Party Set"},
{Reason::EXCLUDE_PORT_MISMATCH,
"The cookie's source_port did not match the port of the request."},
{Reason::EXCLUDE_SCHEME_MISMATCH,
"The cookie's source_scheme did not match the scheme of the "
"request."},
{Reason::EXCLUDE_SHADOWING_DOMAIN,
"The cookie is a domain cookie and has the same name as an origin "
"cookie on this origin."},
{Reason::EXCLUDE_DISALLOWED_CHARACTER,
"The cookie contains ASCII control characters"},
{Reason::EXCLUDE_THIRD_PARTY_PHASEOUT,
"The cookie is blocked for third-party cookie phaseout."},
{Reason::EXCLUDE_NO_COOKIE_CONTENT,
"The cookie contains no content or only whitespace."},
{Reason::EXCLUDE_ANONYMOUS_CONTEXT,
"The cookie is unpartitioned and being accessed from an anonymous "
"context."}});
static_assert(
Reasons.size() ==
net::CookieInclusionStatus::ExclusionReasonBitset::kValueCount,
"Please ensure all ExclusionReason variants are enumerated in "
"GetDebugString");
std::ostringstream reason;
reason << "Failed to set cookie - ";
for (const auto& [val, str] : Reasons) {
if (status.HasExclusionReason(val)) {
reason << str;
}
}
reason << status.GetDebugString();
return reason.str();
}
std::string StringToCookieSameSite(const std::string* str_ptr,
net::CookieSameSite* same_site) {
if (!str_ptr) {
*same_site = net::CookieSameSite::LAX_MODE;
return "";
}
const std::string& str = *str_ptr;
if (str == "unspecified") {
*same_site = net::CookieSameSite::UNSPECIFIED;
} else if (str == "no_restriction") {
*same_site = net::CookieSameSite::NO_RESTRICTION;
} else if (str == "lax") {
*same_site = net::CookieSameSite::LAX_MODE;
} else if (str == "strict") {
*same_site = net::CookieSameSite::STRICT_MODE;
} else {
return "Failed to convert '" + str +
"' to an appropriate cookie same site value";
}
return "";
}
} // namespace
gin::WrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
Cookies::Cookies(ElectronBrowserContext* browser_context)
: browser_context_{browser_context} {
cookie_change_subscription_ =
browser_context_->cookie_change_notifier()->RegisterCookieChangeCallback(
base::BindRepeating(&Cookies::OnCookieChanged,
base::Unretained(this)));
}
Cookies::~Cookies() = default;
v8::Local<v8::Promise> Cookies::Get(v8::Isolate* isolate,
const gin_helper::Dictionary& filter) {
gin_helper::Promise<net::CookieList> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
base::Value::Dict dict;
gin::ConvertFromV8(isolate, filter.GetHandle(), &dict);
std::string url;
filter.Get("url", &url);
if (url.empty()) {
manager->GetAllCookies(
base::BindOnce(&FilterCookies, std::move(dict), std::move(promise)));
} else {
net::CookieOptions options;
options.set_include_httponly();
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
options.set_do_not_update_access_time();
manager->GetCookieList(GURL(url), options,
net::CookiePartitionKeyCollection::Todo(),
base::BindOnce(&FilterCookieWithStatuses,
std::move(dict), std::move(promise)));
}
return handle;
}
v8::Local<v8::Promise> Cookies::Remove(v8::Isolate* isolate,
const GURL& url,
const std::string& name) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto cookie_deletion_filter = network::mojom::CookieDeletionFilter::New();
cookie_deletion_filter->url = url;
cookie_deletion_filter->cookie_name = name;
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->DeleteCookies(
std::move(cookie_deletion_filter),
base::BindOnce(
[](gin_helper::Promise<void> promise, uint32_t num_deleted) {
gin_helper::Promise<void>::ResolvePromise(std::move(promise));
},
std::move(promise)));
return handle;
}
v8::Local<v8::Promise> Cookies::Set(v8::Isolate* isolate,
base::Value::Dict details) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
const std::string* url_string = details.FindString("url");
if (!url_string) {
promise.RejectWithErrorMessage("Missing required option 'url'");
return handle;
}
const std::string* name = details.FindString("name");
const std::string* value = details.FindString("value");
const std::string* domain = details.FindString("domain");
const std::string* path = details.FindString("path");
bool http_only = details.FindBool("httpOnly").value_or(false);
const std::string* same_site_string = details.FindString("sameSite");
net::CookieSameSite same_site;
std::string error = StringToCookieSameSite(same_site_string, &same_site);
if (!error.empty()) {
promise.RejectWithErrorMessage(error);
return handle;
}
bool secure = details.FindBool("secure").value_or(
same_site == net::CookieSameSite::NO_RESTRICTION);
GURL url(url_string ? *url_string : "");
if (!url.is_valid()) {
net::CookieInclusionStatus cookie_inclusion_status;
cookie_inclusion_status.AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
promise.RejectWithErrorMessage(
InclusionStatusToString(cookie_inclusion_status));
return handle;
}
net::CookieInclusionStatus status;
auto canonical_cookie = net::CanonicalCookie::CreateSanitizedCookie(
url, name ? *name : "", value ? *value : "", domain ? *domain : "",
path ? *path : "", ParseTimeProperty(details.FindDouble("creationDate")),
ParseTimeProperty(details.FindDouble("expirationDate")),
ParseTimeProperty(details.FindDouble("lastAccessDate")), secure,
http_only, same_site, net::COOKIE_PRIORITY_DEFAULT, std::nullopt,
&status);
if (!canonical_cookie || !canonical_cookie->IsCanonical()) {
net::CookieInclusionStatus cookie_inclusion_status;
cookie_inclusion_status.AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
promise.RejectWithErrorMessage(InclusionStatusToString(
!status.IsInclude() ? status : cookie_inclusion_status));
return handle;
}
net::CookieOptions options;
if (http_only) {
options.set_include_httponly();
}
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->SetCanonicalCookie(
*canonical_cookie, url, options,
base::BindOnce(
[](gin_helper::Promise<void> promise, net::CookieAccessResult r) {
if (r.status.IsInclude()) {
promise.Resolve();
} else {
promise.RejectWithErrorMessage(InclusionStatusToString(r.status));
}
},
std::move(promise)));
return handle;
}
v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* storage_partition = browser_context_->GetDefaultStoragePartition();
auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
manager->FlushCookieStore(base::BindOnce(
gin_helper::Promise<void>::ResolvePromise, std::move(promise)));
return handle;
}
void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
Emit("changed", gin::ConvertToV8(isolate, change.cookie),
gin::ConvertToV8(isolate, change.cause),
gin::ConvertToV8(isolate,
change.cause != net::CookieChangeCause::INSERTED));
}
// static
gin::Handle<Cookies> Cookies::Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
return gin::CreateHandle(isolate, new Cookies{browser_context});
}
gin::ObjectTemplateBuilder Cookies::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin_helper::EventEmitterMixin<Cookies>::GetObjectTemplateBuilder(
isolate)
.SetMethod("get", &Cookies::Get)
.SetMethod("remove", &Cookies::Remove)
.SetMethod("set", &Cookies::Set)
.SetMethod("flushStore", &Cookies::FlushStore);
}
const char* Cookies::GetTypeName() {
return "Cookies";
}
} // namespace electron::api