electron_api_cookies.cc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright (c) 2015 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/api/electron_api_cookies.h"
  5. #include <sstream>
  6. #include <string>
  7. #include <string_view>
  8. #include <utility>
  9. #include "base/containers/fixed_flat_map.h"
  10. #include "base/time/time.h"
  11. #include "base/values.h"
  12. #include "content/public/browser/browser_context.h"
  13. #include "content/public/browser/browser_task_traits.h"
  14. #include "content/public/browser/browser_thread.h"
  15. #include "content/public/browser/storage_partition.h"
  16. #include "gin/dictionary.h"
  17. #include "gin/handle.h"
  18. #include "gin/object_template_builder.h"
  19. #include "net/cookies/canonical_cookie.h"
  20. #include "net/cookies/cookie_change_dispatcher.h"
  21. #include "net/cookies/cookie_inclusion_status.h"
  22. #include "net/cookies/cookie_store.h"
  23. #include "net/cookies/cookie_util.h"
  24. #include "shell/browser/cookie_change_notifier.h"
  25. #include "shell/browser/electron_browser_context.h"
  26. #include "shell/browser/javascript_environment.h"
  27. #include "shell/common/gin_converters/gurl_converter.h"
  28. #include "shell/common/gin_converters/value_converter.h"
  29. #include "shell/common/gin_helper/dictionary.h"
  30. #include "shell/common/gin_helper/object_template_builder.h"
  31. #include "shell/common/gin_helper/promise.h"
  32. namespace gin {
  33. template <>
  34. struct Converter<net::CookieSameSite> {
  35. static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
  36. const net::CookieSameSite& val) {
  37. switch (val) {
  38. case net::CookieSameSite::UNSPECIFIED:
  39. return ConvertToV8(isolate, "unspecified");
  40. case net::CookieSameSite::NO_RESTRICTION:
  41. return ConvertToV8(isolate, "no_restriction");
  42. case net::CookieSameSite::LAX_MODE:
  43. return ConvertToV8(isolate, "lax");
  44. case net::CookieSameSite::STRICT_MODE:
  45. return ConvertToV8(isolate, "strict");
  46. }
  47. DCHECK(false);
  48. return ConvertToV8(isolate, "unknown");
  49. }
  50. };
  51. template <>
  52. struct Converter<net::CanonicalCookie> {
  53. static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
  54. const net::CanonicalCookie& val) {
  55. gin::Dictionary dict(isolate, v8::Object::New(isolate));
  56. dict.Set("name", val.Name());
  57. dict.Set("value", val.Value());
  58. dict.Set("domain", val.Domain());
  59. dict.Set("hostOnly", net::cookie_util::DomainIsHostOnly(val.Domain()));
  60. dict.Set("path", val.Path());
  61. dict.Set("secure", val.SecureAttribute());
  62. dict.Set("httpOnly", val.IsHttpOnly());
  63. dict.Set("session", !val.IsPersistent());
  64. if (val.IsPersistent())
  65. dict.Set("expirationDate", val.ExpiryDate().InSecondsFSinceUnixEpoch());
  66. dict.Set("sameSite", val.SameSite());
  67. return ConvertToV8(isolate, dict).As<v8::Object>();
  68. }
  69. };
  70. template <>
  71. struct Converter<net::CookieChangeCause> {
  72. static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
  73. const net::CookieChangeCause& val) {
  74. switch (val) {
  75. case net::CookieChangeCause::INSERTED:
  76. case net::CookieChangeCause::EXPLICIT:
  77. return gin::StringToV8(isolate, "explicit");
  78. case net::CookieChangeCause::OVERWRITE:
  79. return gin::StringToV8(isolate, "overwrite");
  80. case net::CookieChangeCause::EXPIRED:
  81. return gin::StringToV8(isolate, "expired");
  82. case net::CookieChangeCause::EVICTED:
  83. return gin::StringToV8(isolate, "evicted");
  84. case net::CookieChangeCause::EXPIRED_OVERWRITE:
  85. return gin::StringToV8(isolate, "expired-overwrite");
  86. default:
  87. return gin::StringToV8(isolate, "unknown");
  88. }
  89. }
  90. };
  91. } // namespace gin
  92. namespace electron::api {
  93. namespace {
  94. // Returns whether |domain| matches |filter|.
  95. bool MatchesDomain(std::string filter, const std::string& domain) {
  96. // Add a leading '.' character to the filter domain if it doesn't exist.
  97. if (net::cookie_util::DomainIsHostOnly(filter))
  98. filter.insert(0, ".");
  99. std::string sub_domain(domain);
  100. // Strip any leading '.' character from the input cookie domain.
  101. if (!net::cookie_util::DomainIsHostOnly(sub_domain))
  102. sub_domain = sub_domain.substr(1);
  103. // Now check whether the domain argument is a subdomain of the filter domain.
  104. for (sub_domain.insert(0, "."); sub_domain.length() >= filter.length();) {
  105. if (sub_domain == filter)
  106. return true;
  107. const size_t next_dot = sub_domain.find('.', 1); // Skip over leading dot.
  108. sub_domain.erase(0, next_dot);
  109. }
  110. return false;
  111. }
  112. // Returns whether |cookie| matches |filter|.
  113. bool MatchesCookie(const base::Value::Dict& filter,
  114. const net::CanonicalCookie& cookie) {
  115. const std::string* str;
  116. if ((str = filter.FindString("name")) && *str != cookie.Name())
  117. return false;
  118. if ((str = filter.FindString("path")) && *str != cookie.Path())
  119. return false;
  120. if ((str = filter.FindString("domain")) &&
  121. !MatchesDomain(*str, cookie.Domain()))
  122. return false;
  123. std::optional<bool> secure_filter = filter.FindBool("secure");
  124. if (secure_filter && *secure_filter != cookie.SecureAttribute())
  125. return false;
  126. std::optional<bool> session_filter = filter.FindBool("session");
  127. if (session_filter && *session_filter == cookie.IsPersistent())
  128. return false;
  129. std::optional<bool> httpOnly_filter = filter.FindBool("httpOnly");
  130. if (httpOnly_filter && *httpOnly_filter != cookie.IsHttpOnly())
  131. return false;
  132. return true;
  133. }
  134. // Remove cookies from |list| not matching |filter|, and pass it to |callback|.
  135. void FilterCookies(base::Value::Dict filter,
  136. gin_helper::Promise<net::CookieList> promise,
  137. const net::CookieList& cookies) {
  138. net::CookieList result;
  139. for (const auto& cookie : cookies) {
  140. if (MatchesCookie(filter, cookie))
  141. result.push_back(cookie);
  142. }
  143. promise.Resolve(result);
  144. }
  145. void FilterCookieWithStatuses(
  146. base::Value::Dict filter,
  147. gin_helper::Promise<net::CookieList> promise,
  148. const net::CookieAccessResultList& list,
  149. const net::CookieAccessResultList& excluded_list) {
  150. FilterCookies(std::move(filter), std::move(promise),
  151. net::cookie_util::StripAccessResults(list));
  152. }
  153. // Parse dictionary property to CanonicalCookie time correctly.
  154. base::Time ParseTimeProperty(const std::optional<double>& value) {
  155. if (!value) // empty time means ignoring the parameter
  156. return {};
  157. if (*value == 0) // FromSecondsSinceUnixEpoch would convert 0 to empty Time
  158. return base::Time::UnixEpoch();
  159. return base::Time::FromSecondsSinceUnixEpoch(*value);
  160. }
  161. const std::string InclusionStatusToString(net::CookieInclusionStatus status) {
  162. // See net/cookies/cookie_inclusion_status.h for cookie error descriptions.
  163. using Reason = net::CookieInclusionStatus::ExclusionReason;
  164. static constexpr auto Reasons =
  165. base::MakeFixedFlatMap<Reason, std::string_view>(
  166. {{Reason::EXCLUDE_UNKNOWN_ERROR, "Unknown error"},
  167. {Reason::EXCLUDE_HTTP_ONLY,
  168. "The cookie was HttpOnly, but the attempted access was through a "
  169. "non-HTTP API."},
  170. {Reason::EXCLUDE_SECURE_ONLY,
  171. "The cookie was Secure, but the URL was not allowed to access "
  172. "Secure cookies."},
  173. {Reason::EXCLUDE_DOMAIN_MISMATCH,
  174. "The cookie's domain attribute did not match the domain of the URL "
  175. "attempting access."},
  176. {Reason::EXCLUDE_NOT_ON_PATH,
  177. "The cookie's path attribute did not match the path of the URL "
  178. "attempting access."},
  179. {Reason::EXCLUDE_SAMESITE_STRICT,
  180. "The cookie had SameSite=Strict, and the attempted access did not "
  181. "have an appropriate SameSiteCookieContext"},
  182. {Reason::EXCLUDE_SAMESITE_LAX,
  183. "The cookie had SameSite=Lax, and the attempted access did not "
  184. "have "
  185. "an appropriate SameSiteCookieContext"},
  186. {Reason::EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
  187. "The cookie did not specify a SameSite attribute, and therefore "
  188. "was treated as if it were SameSite=Lax, and the attempted "
  189. "access did not have an appropriate SameSiteCookieContext."},
  190. {Reason::EXCLUDE_SAMESITE_NONE_INSECURE,
  191. "The cookie specified SameSite=None, but it was not Secure."},
  192. {Reason::EXCLUDE_USER_PREFERENCES,
  193. "Caller did not allow access to the cookie."},
  194. {Reason::EXCLUDE_FAILURE_TO_STORE,
  195. "The cookie was malformed and could not be stored"},
  196. {Reason::EXCLUDE_NONCOOKIEABLE_SCHEME,
  197. "Attempted to set a cookie from a scheme that does not support "
  198. "cookies."},
  199. {Reason::EXCLUDE_OVERWRITE_SECURE,
  200. "The cookie would have overwritten a Secure cookie, and was not "
  201. "allowed to do so."},
  202. {Reason::EXCLUDE_OVERWRITE_HTTP_ONLY,
  203. "The cookie would have overwritten an HttpOnly cookie, and was not "
  204. "allowed to do so."},
  205. {Reason::EXCLUDE_INVALID_DOMAIN,
  206. "The cookie was set with an invalid Domain attribute."},
  207. {Reason::EXCLUDE_INVALID_PREFIX,
  208. "The cookie was set with an invalid __Host- or __Secure- prefix."},
  209. {Reason::EXCLUDE_INVALID_PARTITIONED,
  210. "Cookie was set with an invalid Partitioned attribute, which is "
  211. "only valid if the cookie has a __Host- prefix."},
  212. {Reason::EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE,
  213. "The cookie exceeded the name/value pair size limit."},
  214. {Reason::EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,
  215. "Cookie exceeded the attribute size limit."},
  216. {Reason::EXCLUDE_DOMAIN_NON_ASCII,
  217. "The cookie was set with a Domain attribute containing non ASCII "
  218. "characters."},
  219. {Reason::EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET,
  220. "The cookie is blocked by third-party cookie blocking but the two "
  221. "sites are in the same First-Party Set"},
  222. {Reason::EXCLUDE_PORT_MISMATCH,
  223. "The cookie's source_port did not match the port of the request."},
  224. {Reason::EXCLUDE_SCHEME_MISMATCH,
  225. "The cookie's source_scheme did not match the scheme of the "
  226. "request."},
  227. {Reason::EXCLUDE_SHADOWING_DOMAIN,
  228. "The cookie is a domain cookie and has the same name as an origin "
  229. "cookie on this origin."},
  230. {Reason::EXCLUDE_DISALLOWED_CHARACTER,
  231. "The cookie contains ASCII control characters"},
  232. {Reason::EXCLUDE_THIRD_PARTY_PHASEOUT,
  233. "The cookie is blocked for third-party cookie phaseout."},
  234. {Reason::EXCLUDE_NO_COOKIE_CONTENT,
  235. "The cookie contains no content or only whitespace."},
  236. {Reason::EXCLUDE_ALIASING,
  237. "Cookie aliases that of another with a different source_port or "
  238. "source scheme. I.e.: Two or more cookies share the same name "
  239. "but have different ports/schemes."}});
  240. static_assert(
  241. Reasons.size() ==
  242. net::CookieInclusionStatus::ExclusionReasonBitset::kValueCount,
  243. "Please ensure all ExclusionReason variants are enumerated in "
  244. "GetDebugString");
  245. std::ostringstream reason;
  246. reason << "Failed to set cookie - ";
  247. for (const auto& [val, str] : Reasons) {
  248. if (status.HasExclusionReason(val)) {
  249. reason << str;
  250. }
  251. }
  252. reason << status.GetDebugString();
  253. return reason.str();
  254. }
  255. std::string StringToCookieSameSite(const std::string* str_ptr,
  256. net::CookieSameSite* same_site) {
  257. if (!str_ptr) {
  258. *same_site = net::CookieSameSite::LAX_MODE;
  259. return "";
  260. }
  261. const std::string& str = *str_ptr;
  262. if (str == "unspecified") {
  263. *same_site = net::CookieSameSite::UNSPECIFIED;
  264. } else if (str == "no_restriction") {
  265. *same_site = net::CookieSameSite::NO_RESTRICTION;
  266. } else if (str == "lax") {
  267. *same_site = net::CookieSameSite::LAX_MODE;
  268. } else if (str == "strict") {
  269. *same_site = net::CookieSameSite::STRICT_MODE;
  270. } else {
  271. return "Failed to convert '" + str +
  272. "' to an appropriate cookie same site value";
  273. }
  274. return "";
  275. }
  276. } // namespace
  277. gin::WrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
  278. Cookies::Cookies(ElectronBrowserContext* browser_context)
  279. : browser_context_{browser_context} {
  280. cookie_change_subscription_ =
  281. browser_context_->cookie_change_notifier()->RegisterCookieChangeCallback(
  282. base::BindRepeating(&Cookies::OnCookieChanged,
  283. base::Unretained(this)));
  284. }
  285. Cookies::~Cookies() = default;
  286. v8::Local<v8::Promise> Cookies::Get(v8::Isolate* isolate,
  287. const gin_helper::Dictionary& filter) {
  288. gin_helper::Promise<net::CookieList> promise(isolate);
  289. v8::Local<v8::Promise> handle = promise.GetHandle();
  290. auto* storage_partition = browser_context_->GetDefaultStoragePartition();
  291. auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
  292. base::Value::Dict dict;
  293. gin::ConvertFromV8(isolate, filter.GetHandle(), &dict);
  294. std::string url;
  295. filter.Get("url", &url);
  296. if (url.empty()) {
  297. manager->GetAllCookies(
  298. base::BindOnce(&FilterCookies, std::move(dict), std::move(promise)));
  299. } else {
  300. net::CookieOptions options;
  301. options.set_include_httponly();
  302. options.set_same_site_cookie_context(
  303. net::CookieOptions::SameSiteCookieContext::MakeInclusive());
  304. options.set_do_not_update_access_time();
  305. manager->GetCookieList(GURL(url), options,
  306. net::CookiePartitionKeyCollection::Todo(),
  307. base::BindOnce(&FilterCookieWithStatuses,
  308. std::move(dict), std::move(promise)));
  309. }
  310. return handle;
  311. }
  312. v8::Local<v8::Promise> Cookies::Remove(v8::Isolate* isolate,
  313. const GURL& url,
  314. const std::string& name) {
  315. gin_helper::Promise<void> promise(isolate);
  316. v8::Local<v8::Promise> handle = promise.GetHandle();
  317. auto cookie_deletion_filter = network::mojom::CookieDeletionFilter::New();
  318. cookie_deletion_filter->url = url;
  319. cookie_deletion_filter->cookie_name = name;
  320. auto* storage_partition = browser_context_->GetDefaultStoragePartition();
  321. auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
  322. manager->DeleteCookies(
  323. std::move(cookie_deletion_filter),
  324. base::BindOnce(
  325. [](gin_helper::Promise<void> promise, uint32_t num_deleted) {
  326. gin_helper::Promise<void>::ResolvePromise(std::move(promise));
  327. },
  328. std::move(promise)));
  329. return handle;
  330. }
  331. v8::Local<v8::Promise> Cookies::Set(v8::Isolate* isolate,
  332. base::Value::Dict details) {
  333. gin_helper::Promise<void> promise(isolate);
  334. v8::Local<v8::Promise> handle = promise.GetHandle();
  335. const std::string* url_string = details.FindString("url");
  336. if (!url_string) {
  337. promise.RejectWithErrorMessage("Missing required option 'url'");
  338. return handle;
  339. }
  340. const std::string* name = details.FindString("name");
  341. const std::string* value = details.FindString("value");
  342. const std::string* domain = details.FindString("domain");
  343. const std::string* path = details.FindString("path");
  344. bool http_only = details.FindBool("httpOnly").value_or(false);
  345. const std::string* same_site_string = details.FindString("sameSite");
  346. net::CookieSameSite same_site;
  347. std::string error = StringToCookieSameSite(same_site_string, &same_site);
  348. if (!error.empty()) {
  349. promise.RejectWithErrorMessage(error);
  350. return handle;
  351. }
  352. bool secure = details.FindBool("secure").value_or(
  353. same_site == net::CookieSameSite::NO_RESTRICTION);
  354. GURL url(url_string ? *url_string : "");
  355. if (!url.is_valid()) {
  356. net::CookieInclusionStatus cookie_inclusion_status;
  357. cookie_inclusion_status.AddExclusionReason(
  358. net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
  359. promise.RejectWithErrorMessage(
  360. InclusionStatusToString(cookie_inclusion_status));
  361. return handle;
  362. }
  363. net::CookieInclusionStatus status;
  364. auto canonical_cookie = net::CanonicalCookie::CreateSanitizedCookie(
  365. url, name ? *name : "", value ? *value : "", domain ? *domain : "",
  366. path ? *path : "", ParseTimeProperty(details.FindDouble("creationDate")),
  367. ParseTimeProperty(details.FindDouble("expirationDate")),
  368. ParseTimeProperty(details.FindDouble("lastAccessDate")), secure,
  369. http_only, same_site, net::COOKIE_PRIORITY_DEFAULT, std::nullopt,
  370. &status);
  371. if (!canonical_cookie || !canonical_cookie->IsCanonical()) {
  372. net::CookieInclusionStatus cookie_inclusion_status;
  373. cookie_inclusion_status.AddExclusionReason(
  374. net::CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
  375. promise.RejectWithErrorMessage(InclusionStatusToString(
  376. !status.IsInclude() ? status : cookie_inclusion_status));
  377. return handle;
  378. }
  379. net::CookieOptions options;
  380. if (http_only) {
  381. options.set_include_httponly();
  382. }
  383. options.set_same_site_cookie_context(
  384. net::CookieOptions::SameSiteCookieContext::MakeInclusive());
  385. auto* storage_partition = browser_context_->GetDefaultStoragePartition();
  386. auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
  387. manager->SetCanonicalCookie(
  388. *canonical_cookie, url, options,
  389. base::BindOnce(
  390. [](gin_helper::Promise<void> promise, net::CookieAccessResult r) {
  391. if (r.status.IsInclude()) {
  392. promise.Resolve();
  393. } else {
  394. promise.RejectWithErrorMessage(InclusionStatusToString(r.status));
  395. }
  396. },
  397. std::move(promise)));
  398. return handle;
  399. }
  400. v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
  401. gin_helper::Promise<void> promise(isolate);
  402. v8::Local<v8::Promise> handle = promise.GetHandle();
  403. auto* storage_partition = browser_context_->GetDefaultStoragePartition();
  404. auto* manager = storage_partition->GetCookieManagerForBrowserProcess();
  405. manager->FlushCookieStore(base::BindOnce(
  406. gin_helper::Promise<void>::ResolvePromise, std::move(promise)));
  407. return handle;
  408. }
  409. void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
  410. v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  411. v8::HandleScope scope(isolate);
  412. Emit("changed", gin::ConvertToV8(isolate, change.cookie),
  413. gin::ConvertToV8(isolate, change.cause),
  414. gin::ConvertToV8(isolate,
  415. change.cause != net::CookieChangeCause::INSERTED));
  416. }
  417. // static
  418. gin::Handle<Cookies> Cookies::Create(v8::Isolate* isolate,
  419. ElectronBrowserContext* browser_context) {
  420. return gin::CreateHandle(isolate, new Cookies{browser_context});
  421. }
  422. gin::ObjectTemplateBuilder Cookies::GetObjectTemplateBuilder(
  423. v8::Isolate* isolate) {
  424. return gin_helper::EventEmitterMixin<Cookies>::GetObjectTemplateBuilder(
  425. isolate)
  426. .SetMethod("get", &Cookies::Get)
  427. .SetMethod("remove", &Cookies::Remove)
  428. .SetMethod("set", &Cookies::Set)
  429. .SetMethod("flushStore", &Cookies::FlushStore);
  430. }
  431. const char* Cookies::GetTypeName() {
  432. return "Cookies";
  433. }
  434. } // namespace electron::api