Bug 1783019 - Implement cookie banner handling components. r=timhuang,necko-reviewers,valentin

This patch adds the following components:
 - nsICookieBannerService: Main service singleton managing the rules and initiating other components.
   It's exposed via Services.cookieBanners and can be configured via the cookiebanners.* prefs.
   To enable it set "cookiebanners.service.mode" to 1 or 2 and restart the browser.
 - nsCookieInjector: Looks up rules and injects cookies for matching top level loads.
 - nsICookieBannerListService: Imports and updates the cookie banner rules.
 - nsICookieBannerRule: Rules for a given domain.
 - nsICookieRule: Part of nsICookieBannerRule. Holds cookie specific rules.

Depends on D153641

Differential Revision: https://phabricator.services.mozilla.com/D153642
This commit is contained in:
Paul Zuehlcke 2022-08-16 12:07:13 +00:00
parent 4a75c4c2b3
commit 017ccb0be2
20 changed files with 1250 additions and 0 deletions

View File

@ -1849,6 +1849,33 @@
value: false
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "cookiebanners."
#---------------------------------------------------------------------------
# Controls the cookie banner handling mode.
# 0: Disables all cookie banner handling.
# 1: Reject-all if possible, otherwise do nothing.
# 2: Reject-all if possible, otherwise accept-all.
- name: cookiebanners.service.mode
type: uint32_t
value: 0
mirror: always
# Enables the cookie banner cookie injector.
- name: cookiebanners.cookieInjector.enabled
type: bool
value: true
mirror: always
# By default, how many seconds in the future cookies should expire after they
# have been injected. Defaults to 12 months. Individual cookie rules may
# override this.
- name: cookiebanners.cookieInjector.defaultExpiryRelative
type: uint32_t
value: 31536000
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "datareporting."
#---------------------------------------------------------------------------

View File

@ -36,6 +36,7 @@ pref_groups = [
"clipboard",
"content",
"converter",
"cookiebanners",
"datareporting",
"device",
"devtools",

View File

@ -20,6 +20,7 @@ XPIDL_MODULE = "necko_cookie"
EXPORTS.mozilla.net = [
"Cookie.h",
"CookieJarSettings.h",
"CookieKey.h",
"CookiePersistentStorage.h",

View File

@ -0,0 +1,106 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// Some test rules for cookie injection.
const RULES_TESTING = [
// {
// domain: "example.com",
// cookies: {
// optOut: [
// {
// name: "consentCookieTest",
// value: "rejectAll",
// unsetValue: "UNSET",
// },
// {
// name: "consentCookieTestSecondary",
// value: "true",
// },
// ],
// optIn: [
// {
// name: "consentCookieTest",
// value: "acceptAll",
// expiryRelative: 3600,
// unsetValue: "UNSET",
// },
// ],
// },
// },
// {
// domain: "example.org",
// cookies: {
// optIn: [
// {
// host: "example.org",
// isSecure: false,
// name: "consentCookieTest",
// path: "/foo",
// value: "acceptAll",
// expiryRelative: 3600,
// unsetValue: "UNSET",
// },
// ],
// },
// },
];
/**
* See nsICookieBannerListService
*/
class CookieBannerListService {
classId = Components.ID("{1d8d9470-97d3-4885-a108-44a5c4fb36e2}");
QueryInterface = ChromeUtils.generateQI(["nsICookieBannerListService"]);
/**
* Iterate over RULES_TESTING and insert rules via nsICookieBannerService.
*/
importRules() {
RULES_TESTING.forEach(({ domain, cookies }) => {
let rule = Services.cookieBanners.lookupOrInsertRuleForDomain(domain);
// Clear any previously stored cookie rules.
rule.clearCookies();
// Skip rules that don't have cookies.
if (!cookies) {
return;
}
// Import opt-in and opt-out cookies if defined.
for (let category of ["optOut", "optIn"]) {
if (!cookies[category]) {
continue;
}
let isOptOut = category == "optOut";
for (let c of cookies[category]) {
rule.addCookie(
isOptOut,
c.host,
c.name,
c.value,
// The following fields are optional and may not be defined by the
// rule. They will fall back to defaults.
c.path,
c.expiryRelative,
c.unsetValue,
c.isSecure,
c.isHTTPOnly,
c.isSession,
c.sameSite,
c.schemeMap
);
}
}
});
}
}
var EXPORTED_SYMBOLS = ["CookieBannerListService"];

View File

@ -0,0 +1,28 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Classes = [
{
'name': 'CookieBannerService',
'cid': '{eac9cdc4-ecee-49f2-91da-7627e15c1f3c}',
'interfaces': ['nsICookieBannerService'],
'contract_ids': ['@mozilla.org/cookie-banner-service;1'],
'type': 'mozilla::nsCookieBannerService',
'headers': ['/toolkit/components/cookiebanners/nsCookieBannerService.h'],
'singleton': True,
'constructor': 'mozilla::nsCookieBannerService::GetSingleton',
'js_name': 'cookieBanners',
'categories': {'profile-after-change': 'nsCookieBannerService'},
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
{
'cid': '{1d8d9470-97d3-4885-a108-44a5c4fb36e2}',
'contract_ids': ['@mozilla.org/cookie-banner-list-service;1'],
'jsm': 'resource://gre/modules/CookieBannerListService.jsm',
'constructor': 'CookieBannerListService',
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
]

View File

@ -0,0 +1,47 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files("**"):
BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking")
XPIDL_SOURCES += [
"nsICookieBannerListService.idl",
"nsICookieBannerRule.idl",
"nsICookieBannerService.idl",
"nsICookieRule.idl",
]
XPIDL_MODULE = "toolkit_cookiebanners"
EXTRA_JS_MODULES += [
"CookieBannerListService.jsm",
]
XPCOM_MANIFESTS += [
"components.conf",
]
EXPORTS.mozilla += [
"nsCookieBannerRule.h",
"nsCookieBannerService.h",
"nsCookieInjector.h",
"nsCookieRule.h",
]
UNIFIED_SOURCES += [
"nsCookieBannerRule.cpp",
"nsCookieBannerService.cpp",
"nsCookieInjector.cpp",
"nsCookieRule.cpp",
]
include("/ipc/chromium/chromium-config.mozbuild")
LOCAL_INCLUDES += ["/netwerk/base", "/netwerk/cookie"]
FINAL_LIBRARY = "xul"
REQUIRES_UNIFIED_BUILD = True

View File

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCookieBannerRule.h"
#include "mozilla/Logging.h"
#include "nsString.h"
#include "nsCOMPtr.h"
#include "nsCookieRule.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsCookieBannerRule, nsICookieBannerRule)
LazyLogModule gCookieRuleLog("nsCookieBannerRule");
NS_IMETHODIMP
nsCookieBannerRule::ClearCookies() {
mCookiesOptOut.Clear();
mCookiesOptIn.Clear();
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerRule::AddCookie(bool aIsOptOut, const nsACString& aHost,
const nsACString& aName, const nsACString& aValue,
// Optional
const nsACString& aPath, int64_t aExpiryRelative,
const nsACString& aUnsetValue, bool aIsSecure,
bool aIsHttpOnly, bool aIsSession,
int32_t aSameSite,
nsICookie::schemeType aSchemeMap) {
MOZ_LOG(gCookieRuleLog, LogLevel::Debug,
("%s: mDomain: %s, aIsOptOut: %d, aHost: %s, aName: %s", __FUNCTION__,
mDomain.get(), aIsOptOut, nsPromiseFlatCString(aHost).get(),
nsPromiseFlatCString(aName).get()));
// Default cookie host to .<domain>
nsAutoCString host(aHost);
if (host.IsEmpty()) {
host.AppendLiteral(".");
host.Append(mDomain);
}
// Create and insert cookie rule.
nsCOMPtr<nsICookieRule> cookieRule = new nsCookieRule(
aIsOptOut, host, aName, aValue, aPath, aExpiryRelative, aUnsetValue,
aIsSecure, aIsHttpOnly, aIsSession, aSameSite, aSchemeMap);
Cookies(aIsOptOut).AppendElement(cookieRule);
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerRule::GetDomain(nsACString& aDomain) {
aDomain.Assign(mDomain);
return NS_OK;
}
nsTArray<nsCOMPtr<nsICookieRule>>& nsCookieBannerRule::Cookies(bool isOptOut) {
if (isOptOut) {
return mCookiesOptOut;
}
return mCookiesOptIn;
}
NS_IMETHODIMP
nsCookieBannerRule::GetCookiesOptOut(
nsTArray<RefPtr<nsICookieRule>>& aCookies) {
nsTArray<nsCOMPtr<nsICookieRule>>& cookies = Cookies(true);
for (nsICookieRule* cookie : cookies) {
aCookies.AppendElement(cookie);
}
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerRule::GetCookiesOptIn(nsTArray<RefPtr<nsICookieRule>>& aCookies) {
nsTArray<nsCOMPtr<nsICookieRule>>& cookies = Cookies(false);
for (nsICookieRule* cookie : cookies) {
aCookies.AppendElement(cookie);
}
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_nsCookieBannerRule_h__
#define mozilla_nsCookieBannerRule_h__
#include "nsICookieBannerRule.h"
#include "nsICookieRule.h"
#include "nsString.h"
#include "nsCOMPtr.h"
namespace mozilla {
class nsCookieBannerRule final : public nsICookieBannerRule {
NS_DECL_ISUPPORTS
NS_DECL_NSICOOKIEBANNERRULE
public:
nsCookieBannerRule() = default;
explicit nsCookieBannerRule(const nsACString& aDomain) : mDomain(aDomain) {}
private:
~nsCookieBannerRule() = default;
nsCString mDomain;
nsTArray<nsCOMPtr<nsICookieRule>> mCookiesOptOut;
nsTArray<nsCOMPtr<nsICookieRule>> mCookiesOptIn;
// Internal getter for easy access of cookie rule arrays.
nsTArray<nsCOMPtr<nsICookieRule>>& Cookies(bool isOptOut);
};
} // namespace mozilla
#endif

View File

@ -0,0 +1,249 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCookieBannerService.h"
#include "nsCookieBannerRule.h"
#include "nsCookieInjector.h"
#include "nsICookieBannerListService.h"
#include "nsICookieBannerRule.h"
#include "nsICookie.h"
#include "nsIEffectiveTLDService.h"
#include "mozilla/StaticPrefs_cookiebanners.h"
#include "ErrorList.h"
#include "mozilla/Logging.h"
#include "nsDebug.h"
#include "nsCOMPtr.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#include "nsCRT.h"
#include "mozilla/ClearOnShutdown.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsCookieBannerService, nsICookieBannerService, nsIObserver)
LazyLogModule gCookieBannerLog("nsCookieBannerService");
static const char kCookieBannerServiceModePref[] = "cookiebanners.service.mode";
static StaticRefPtr<nsCookieBannerService> sCookieBannerServiceSingleton;
// static
already_AddRefed<nsCookieBannerService> nsCookieBannerService::GetSingleton() {
if (!sCookieBannerServiceSingleton) {
sCookieBannerServiceSingleton = new nsCookieBannerService();
RunOnShutdown([] {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("RunOnShutdown. Mode: %d",
StaticPrefs::cookiebanners_service_mode()));
// Unregister pref listeners.
DebugOnly<nsresult> rv = Preferences::UnregisterCallback(
&nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Unregistering kCookieBannerServiceModePref callback failed");
rv = sCookieBannerServiceSingleton->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Shutdown failed.");
sCookieBannerServiceSingleton = nullptr;
});
}
return do_AddRef(sCookieBannerServiceSingleton);
}
// static
void nsCookieBannerService::OnPrefChange(const char* aPref, void* aData) {
RefPtr<nsCookieBannerService> service = GetSingleton();
if (StaticPrefs::cookiebanners_service_mode() !=
nsICookieBannerService::MODE_DISABLED) {
MOZ_LOG(
gCookieBannerLog, LogLevel::Info,
("Initializing nsCookieBannerService after pref change. %s", aPref));
DebugOnly<nsresult> rv = service->Init();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Init failed");
return;
}
MOZ_LOG(gCookieBannerLog, LogLevel::Info,
("Disabling nsCookieBannerService after pref change. %s", aPref));
DebugOnly<nsresult> rv = service->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Shutdown failed");
}
// This method initializes the cookie banner service on startup on
// "profile-after-change".
NS_IMETHODIMP
nsCookieBannerService::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (nsCRT::strcmp(aTopic, "profile-after-change") != 0) {
return NS_OK;
}
return Preferences::RegisterCallbackAndCall(
&nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
}
nsresult nsCookieBannerService::Init() {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Mode: %d", __FUNCTION__,
StaticPrefs::cookiebanners_service_mode()));
// Check if already initialized.
if (mIsInitialized) {
return NS_OK;
}
// Initialize the service which fetches cookie banner rules.
mListService = do_GetService(NS_COOKIEBANNERLISTSERVICE_CONTRACTID);
NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
// Setting mIsInitialized before importing rules, because the list service
// needs to call nsCookieBannerService methods that would throw if not marked
// initialized.
mIsInitialized = true;
// Import initial rule-set.
mListService->ImportRules();
// Initialize the cookie injector.
RefPtr<nsCookieInjector> injector = nsCookieInjector::GetSingleton();
return NS_OK;
}
nsresult nsCookieBannerService::Shutdown() {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Mode: %d", __FUNCTION__,
StaticPrefs::cookiebanners_service_mode()));
// Check if already shutdown.
if (!mIsInitialized) {
return NS_OK;
}
mIsInitialized = false;
// Clear all stored cookie banner rules. They will be imported again on Init.
mRules.Clear();
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::GetRules(nsTArray<RefPtr<nsICookieBannerRule>>& aRules) {
aRules.Clear();
// Service is disabled, throw with empty array.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
AppendToArray(aRules, mRules.Values());
return NS_OK;
}
nsresult nsCookieBannerService::GetRuleForDomain(const nsACString& aDomain,
nsICookieBannerRule** aRule) {
NS_ENSURE_ARG_POINTER(aRule);
*aRule = nullptr;
// Service is disabled, throw with null.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsICookieBannerRule> rule = mRules.Get(aDomain);
if (rule) {
rule.forget(aRule);
}
return NS_OK;
}
nsresult nsCookieBannerService::GetRuleForURI(nsIURI* aURI,
nsICookieBannerRule** aRule) {
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(aRule);
*aRule = nullptr;
// Service is disabled, throw with null.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult rv;
nsCOMPtr<nsIEffectiveTLDService> eTLDService(
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
NS_ENSURE_SUCCESS(rv, rv);
nsCString baseDomain;
rv = eTLDService->GetBaseDomain(aURI, 0, baseDomain);
NS_ENSURE_SUCCESS(rv, rv);
return GetRuleForDomain(baseDomain, aRule);
}
NS_IMETHODIMP
nsCookieBannerService::GetCookiesForURI(
nsIURI* aURI, nsTArray<RefPtr<nsICookieRule>>& aCookies) {
aCookies.Clear();
// Service is disabled, throw with empty array.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsICookieBannerRule> rule;
nsresult rv = GetRuleForURI(aURI, getter_AddRefs(rule));
NS_ENSURE_SUCCESS(rv, rv);
if (!rule) {
return NS_OK;
}
// MODE_REJECT: In this mode we only handle the banner if we can reject. We
// don't care about the opt-in cookies.
rv = rule->GetCookiesOptOut(aCookies);
NS_ENSURE_SUCCESS(rv, rv);
// MODE_REJECT_OR_ACCEPT: In this mode we will try to opt-out, but if we don't
// have any opt-out cookies we will fallback to the opt-in cookies.
if (StaticPrefs::cookiebanners_service_mode() ==
nsICookieBannerService::MODE_REJECT_OR_ACCEPT &&
aCookies.IsEmpty()) {
return rule->GetCookiesOptIn(aCookies);
}
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::LookupOrInsertRuleForDomain(
const nsACString& aDomain, nsICookieBannerRule** aRule) {
NS_ENSURE_ARG_POINTER(aRule);
// Service is disabled, throw with null.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsICookieBannerRule> rule =
mRules.LookupOrInsert(aDomain, RefPtr{new nsCookieBannerRule(aDomain)});
NS_ENSURE_TRUE(rule, NS_ERROR_FAILURE);
rule.forget(aRule);
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_nsCookieBannerService_h__
#define mozilla_nsCookieBannerService_h__
#include "nsICookieBannerRule.h"
#include "nsICookieBannerService.h"
#include "nsICookieBannerListService.h"
#include "nsCOMPtr.h"
#include "nsTHashMap.h"
#include "nsIObserver.h"
#include "mozilla/StaticPtr.h"
namespace mozilla {
class nsCookieBannerService final : public nsIObserver,
public nsICookieBannerService {
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
NS_DECL_NSICOOKIEBANNERSERVICE
public:
static already_AddRefed<nsCookieBannerService> GetSingleton();
private:
nsCookieBannerService() = default;
~nsCookieBannerService() = default;
// Whether the service is enabled and ready to accept requests.
bool mIsInitialized = false;
nsCOMPtr<nsICookieBannerListService> mListService;
nsTHashMap<nsCStringHashKey, nsCOMPtr<nsICookieBannerRule>> mRules;
// Pref change callback which initializes and shuts down the service. This is
// also called on startup.
static void OnPrefChange(const char* aPref, void* aData);
/**
* Initializes internal state. Will be called on profile-after-change and on
* pref changes.
*/
[[nodiscard]] nsresult Init();
/**
* Cleanup method to be called on shutdown or pref change.
*/
[[nodiscard]] nsresult Shutdown();
nsresult GetRuleForDomain(const nsACString& aDomain,
nsICookieBannerRule** aRule);
nsresult GetRuleForURI(nsIURI* aURI, nsICookieBannerRule** aRule);
};
} // namespace mozilla
#endif

View File

@ -0,0 +1,275 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCookieInjector.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Logging.h"
#include "nsICookieBannerService.h"
#include "nsICookieManager.h"
#include "nsIObserverService.h"
#include "nsCRT.h"
#include "nsCOMPtr.h"
#include "mozilla/Components.h"
#include "Cookie.h"
#include "nsIHttpProtocolHandler.h"
#include "mozilla/StaticPrefs_cookiebanners.h"
namespace mozilla {
LazyLogModule gCookieInjectorLog("nsCookieInjector");
StaticRefPtr<nsCookieInjector> sCookieInjectorSingleton;
static constexpr auto kHttpObserverMessage =
NS_HTTP_ON_MODIFY_REQUEST_BEFORE_COOKIES_TOPIC;
static const char kCookieInjectorEnabledPref[] =
"cookiebanners.cookieInjector.enabled";
NS_IMPL_ISUPPORTS(nsCookieInjector, nsIObserver);
already_AddRefed<nsCookieInjector> nsCookieInjector::GetSingleton() {
if (!sCookieInjectorSingleton) {
sCookieInjectorSingleton = new nsCookieInjector();
// Register pref listeners.
DebugOnly<nsresult> rv = Preferences::RegisterCallbackAndCall(
&nsCookieInjector::OnPrefChange, kCookieInjectorEnabledPref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to register pref listener for kCookieInjectorEnabledPref.");
rv = Preferences::RegisterCallbackAndCall(&nsCookieInjector::OnPrefChange,
kCookieBannerServiceModePref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to register pref listener for kCookieBannerServiceModePref.");
// Clean up on shutdown.
RunOnShutdown([] {
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug, ("RunOnShutdown"));
// Unregister pref listeners.
DebugOnly<nsresult> rv = Preferences::UnregisterCallback(
&nsCookieInjector::OnPrefChange, kCookieInjectorEnabledPref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to unregister pref listener for kCookieInjectorEnabledPref.");
rv = Preferences::UnregisterCallback(&nsCookieInjector::OnPrefChange,
kCookieBannerServiceModePref);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to unregister pref listener for "
"kCookieBannerServiceModePref.");
rv = sCookieInjectorSingleton->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieInjector::Shutdown failed.");
sCookieInjectorSingleton = nullptr;
});
}
return do_AddRef(sCookieInjectorSingleton);
}
// static
void nsCookieInjector::OnPrefChange(const char* aPref, void* aData) {
RefPtr<nsCookieInjector> injector = nsCookieInjector::GetSingleton();
if (StaticPrefs::cookiebanners_service_mode() !=
nsICookieBannerService::MODE_DISABLED &&
StaticPrefs::cookiebanners_cookieInjector_enabled()) {
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
("Initializing cookie injector after pref change. %s", aPref));
DebugOnly<nsresult> rv = injector->Init();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsCookieInjector::Init failed");
return;
}
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
("Disabling cookie injector after pref change. %s", aPref));
DebugOnly<nsresult> rv = injector->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsCookieInjector::Shutdown failed");
}
nsresult nsCookieInjector::Init() {
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug, ("%s", __FUNCTION__));
// Check if already initialized.
if (mIsInitialized) {
return NS_OK;
}
mIsInitialized = true;
// Add http observer.
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE);
return observerService->AddObserver(this, kHttpObserverMessage, false);
}
nsresult nsCookieInjector::Shutdown() {
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug, ("%s", __FUNCTION__));
// Check if already shutdown.
if (!mIsInitialized) {
return NS_OK;
}
mIsInitialized = false;
// Remove http observer.
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE);
return observerService->RemoveObserver(this, kHttpObserverMessage);
}
// nsIObserver
NS_IMETHODIMP
nsCookieInjector::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
MOZ_LOG(gCookieInjectorLog, LogLevel::Verbose, ("Observe topic %s", aTopic));
if (nsCRT::strcmp(aTopic, kHttpObserverMessage) == 0) {
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aSubject);
NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE);
return MaybeInjectCookies(channel, aTopic);
}
return NS_OK;
}
nsresult nsCookieInjector::MaybeInjectCookies(nsIHttpChannel* aChannel,
const char* aTopic) {
NS_ENSURE_ARG_POINTER(aChannel);
NS_ENSURE_ARG_POINTER(aTopic);
// Skip non-document loads.
if (!aChannel->IsDocument()) {
MOZ_LOG(gCookieInjectorLog, LogLevel::Verbose,
("%s: Skip non-document load.", aTopic));
return NS_OK;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
NS_ENSURE_TRUE(loadInfo, NS_ERROR_FAILURE);
// Skip non-toplevel loads.
if (!loadInfo->GetIsTopLevelLoad()) {
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
("%s: Skip non-top-level load.", aTopic));
return NS_OK;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, rv);
// Get hostPort string, used for logging only.
nsCString hostPort;
rv = uri->GetHostPort(hostPort);
NS_ENSURE_SUCCESS(rv, rv);
// Cookie banner handling rules are fetched from the cookie banner service.
nsCOMPtr<nsICookieBannerService> cookieBannerService =
components::CookieBannerService::Service(&rv);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
("Looking up rules for %s.", hostPort.get()));
nsTArray<RefPtr<nsICookieRule>> rules;
rv = cookieBannerService->GetCookiesForURI(uri, rules);
NS_ENSURE_SUCCESS(rv, rv);
// No cookie rules found.
if (rules.IsEmpty()) {
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
("Abort: No cookie rules for %s.", hostPort.get()));
return NS_OK;
}
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
("Got rules for %s.", hostPort.get()));
// Get the OA from the channel. We may need to set the cookie in a specific
// bucket, for example Private Browsing Mode.
OriginAttributes attr = loadInfo->GetOriginAttributes();
return InjectCookiesFromRules(hostPort, rules, attr);
}
nsresult nsCookieInjector::InjectCookiesFromRules(
const nsCString& aHostPort, const nsTArray<RefPtr<nsICookieRule>>& aRules,
OriginAttributes& aOriginAttributes) {
NS_ENSURE_TRUE(aRules.Length(), NS_ERROR_FAILURE);
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
("Injecting cookies for %s.", aHostPort.get()));
// Write cookies from aRules to storage via the cookie manager.
nsCOMPtr<nsICookieManager> cookieManager =
do_GetService("@mozilla.org/cookiemanager;1");
NS_ENSURE_TRUE(cookieManager, NS_ERROR_FAILURE);
for (nsICookieRule* cookieRule : aRules) {
nsCOMPtr<nsICookie> cookie;
nsresult rv = cookieRule->GetCookie(getter_AddRefs(cookie));
NS_ENSURE_SUCCESS(rv, rv);
if (NS_WARN_IF(!cookie)) {
continue;
}
// Convert to underlying implementer class to get fast non-xpcom property
// access.
net::Cookie* c = static_cast<net::Cookie*>(cookie.get());
// Check if the cookie is already set to avoid overwriting any custom
// settings.
bool exists = false;
rv = cookieManager->CookieExistsNative(c->Host(), c->Path(), c->Name(),
&aOriginAttributes, &exists);
NS_ENSURE_SUCCESS(rv, rv);
// If a cookie with the same name already exists we need to perform further
// checks. We can only overwrite if the rule defines the cookie's value as
// the "unset" state.
if (exists) {
nsCString unsetValue;
rv = cookieRule->GetUnsetValue(unsetValue);
NS_ENSURE_SUCCESS(rv, rv);
// No defined unset value means we shouldn't overwrite the cookie if it is
// set. Skip setting this cookie.
if (unsetValue.IsEmpty()) {
MOZ_LOG(
gCookieInjectorLog, LogLevel::Info,
("Skip setting already existing cookie. Cookie: %s, %s, %s, %s\n",
c->Host().get(), c->Name().get(), c->Path().get(),
c->Value().get()));
continue;
}
// TODO: Bug 1784874
// Check if cookie value == unsetValue. In this case we can
// overwrite the cookie. Otherwise log a message and skip setting this
// cookie. This might require extending CookieExistsNative and
// CookieStorage::FindCookie to take an optional "value" argument or
// passing in a lambda that can inspect cookies found.
}
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
("Setting cookie: %s, %s, %s, %s\n", c->Host().get(),
c->Name().get(), c->Path().get(), c->Value().get()));
rv = cookieManager->AddNative(
c->Host(), c->Path(), c->Name(), c->Value(), c->IsSecure(),
c->IsHttpOnly(), c->IsSession(), c->Expiry(), &aOriginAttributes,
c->SameSite(), static_cast<nsICookie::schemeType>(c->SchemeMap()));
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_nsCookieInjector_h__
#define mozilla_nsCookieInjector_h__
#include "nsCOMPtr.h"
#include "nsICookieBannerRule.h"
#include "nsIHttpChannel.h"
#include "nsIObserver.h"
namespace mozilla {
class nsCookieInjector final : public nsIObserver {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
static already_AddRefed<nsCookieInjector> GetSingleton();
[[nodiscard]] nsresult Init();
[[nodiscard]] nsresult Shutdown();
private:
nsCookieInjector() = default;
~nsCookieInjector() = default;
// Whether the component is enabled and ready to inject cookies.
bool mIsInitialized = false;
// Enables or disables the component when the relevant prefs change.
static void OnPrefChange(const char* aPref, void* aData);
// Called when the http observer topic is dispatched.
nsresult MaybeInjectCookies(nsIHttpChannel* aChannel, const char* aTopic);
// Inserts cookies via the cookie manager given a list of cookie injection
// rules.
nsresult InjectCookiesFromRules(const nsCString& aHostPort,
const nsTArray<RefPtr<nsICookieRule>>& aRules,
OriginAttributes& aOriginAttributes);
};
} // namespace mozilla
#endif

View File

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCookieRule.h"
#include "mozilla/OriginAttributes.h"
#include "nsICookie.h"
#include "nsString.h"
#include "nsCOMPtr.h"
#include "Cookie.h"
#include "prtime.h"
#include "mozilla/StaticPrefs_cookiebanners.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsCookieRule, nsICookieRule)
nsCookieRule::nsCookieRule(bool aIsOptOut, const nsACString& aHost,
const nsACString& aName, const nsACString& aValue,
// Optional
const nsACString& aPath, int64_t aExpiryRelative,
const nsACString& aUnsetValue, bool aIsSecure,
bool aIsHttpOnly, bool aIsSession, int32_t aSameSite,
nsICookie::schemeType aSchemeMap) {
// Default expiry time is defined by pref.
if (aExpiryRelative <= 0) {
aExpiryRelative =
StaticPrefs::cookiebanners_cookieInjector_defaultExpiryRelative();
}
mExpiryRelative = aExpiryRelative;
nsCString path(aPath);
if (path.IsEmpty()) {
path.AssignLiteral("/");
}
mUnsetValue = aUnsetValue;
net::CookieStruct cookieData(
nsCString(aName), nsCString(aValue), nsCString(aHost), path, 0, 0, 0,
aIsHttpOnly, aIsSession, aIsSecure, aSameSite, aSameSite, aSchemeMap);
OriginAttributes attrs;
mCookie = net::Cookie::Create(cookieData, attrs);
}
/* readonly attribute int64_t expiryRelative; */
NS_IMETHODIMP nsCookieRule::GetExpiryRelative(int64_t* aExpiryRelative) {
NS_ENSURE_ARG_POINTER(aExpiryRelative);
*aExpiryRelative = mExpiryRelative;
return NS_OK;
}
/* readonly attribute AUTF8String unsetValue; */
NS_IMETHODIMP nsCookieRule::GetUnsetValue(nsACString& aUnsetValue) {
aUnsetValue = mUnsetValue;
return NS_OK;
}
/* readonly attribute nsICookie cookie; */
NS_IMETHODIMP nsCookieRule::GetCookie(nsICookie** aCookie) {
NS_ENSURE_ARG_POINTER(aCookie);
// Copy cookie and update expiry, creation and last accessed time.
nsICookie* cookie = mCookie;
RefPtr<net::Cookie> cookieNative = static_cast<net::Cookie*>(cookie)->Clone();
int64_t currentTimeInUsec = PR_Now();
cookieNative->SetCreationTime(
net::Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
cookieNative->SetLastAccessed(currentTimeInUsec);
cookieNative->SetExpiry((currentTimeInUsec / PR_USEC_PER_SEC) +
mExpiryRelative);
cookieNative.forget(aCookie);
return NS_OK;
}
} // namespace mozilla

View File

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_nsCookieRule_h__
#define mozilla_nsCookieRule_h__
#include "nsICookieRule.h"
#include "nsICookie.h"
#include "nsString.h"
#include "nsCOMPtr.h"
#include "mozilla/StaticPrefs_cookiebanners.h"
namespace mozilla {
class nsCookieRule final : public nsICookieRule {
NS_DECL_ISUPPORTS
NS_DECL_NSICOOKIERULE
public:
nsCookieRule() = default;
explicit nsCookieRule(
bool aIsOptOut, const nsACString& aHost, const nsACString& aName,
const nsACString& aValue, const nsACString& aPath = "/"_ns,
int64_t aExpiryRelative =
StaticPrefs::cookiebanners_cookieInjector_defaultExpiryRelative(),
const nsACString& aUnsetValue = ""_ns, bool aIsSecure = true,
bool aIsHttpOnly = false, bool aIsSession = false,
int32_t aSameSite = nsICookie::SAMESITE_LAX,
nsICookie::schemeType aSchemeMap = nsICookie::SCHEME_HTTPS);
private:
~nsCookieRule() = default;
nsCOMPtr<nsICookie> mCookie;
int64_t mExpiryRelative{};
nsCString mUnsetValue;
};
} // namespace mozilla
#endif

View File

@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
/**
* Service singleton for initializing and updating the list of cookie banner
* handling rules.
*/
[scriptable, uuid(1d8d9470-97d3-4885-a108-44a5c4fb36e2)]
interface nsICookieBannerListService : nsISupports {
// Import the initial rule list.
void importRules();
};
%{C++
#define NS_COOKIEBANNERLISTSERVICE_CONTRACTID "@mozilla.org/cookie-banner-list-service;1"
%}

View File

@ -0,0 +1,53 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
#include "nsICookieRule.idl"
/**
* A rule containing instructions on how to handle a cookie banner for a specific
* domain.
*/
[builtinclass, scriptable, uuid(eb1904db-e0d1-4760-a721-db76b1ca3e94)]
interface nsICookieBannerRule : nsISupports {
// Domain of the site to handle the cookie banner for.
readonly attribute ACString domain;
// Cookies that reflect the opt-out or "reject all" state for the cookie baner.
readonly attribute Array<nsICookieRule> cookiesOptOut;
// Cookies that reflect the opt-in or "accept all" state for the cookie banner.
readonly attribute Array<nsICookieRule> cookiesOptIn;
/**
* Clear both lists of opt-in and opt-out cookies.
*/
void clearCookies();
/**
* Add an opt-in or opt-out cookie to the rule.
* aIsOptOut - Whether this is an opt-out cookie (true) or opt-in cookie (false).
* aExpiryRelative - See nsICookieRule.
* aUnsetValue - See nsICookieRule.
* For a description of the other fields see nsICookieManager#addNative.
*/
void addCookie(in boolean aIsOptOut,
in AUTF8String aHost,
in ACString aName,
in AUTF8String aValue,
[optional] in AUTF8String aPath,
[optional] in int64_t aExpiryRelative,
[optional] in AUTF8String aUnsetValue,
[optional] in boolean aIsSecure,
[optional] in boolean aIsHttpOnly,
[optional] in boolean aIsSession,
[optional] in int32_t aSameSite,
[optional] in nsICookie_schemeType aSchemeMap);
// TODO: additional rule types here e.g. for clicking.
};

View File

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
#include "nsICookieBannerRule.idl"
#include "nsICookieRule.idl"
#include "nsIURI.idl"
/**
* Service singleton which owns the cookie banner feature.
* This service owns the cookie banner handling rules.
* It initializes both the component for importing rules
* (nsICookieBannerListService) and injecting cookies (nsICookieInjector).
*/
[scriptable, uuid(eac9cdc4-ecee-49f2-91da-7627e15c1f3c)]
interface nsICookieBannerService : nsISupports {
/**
* Modes for cookie banner handling
* MODE_DISABLED - No cookie banner handling, service disabled.
* MODE_REJECT - Only handle banners where selecting "reject all" is possible.
* MODE_REJECT_OR_ACCEPT - Prefer selecting "reject all", if not possible
* fall back to "accept all".
*/
cenum Modes : 8 {
MODE_DISABLED,
MODE_REJECT,
MODE_REJECT_OR_ACCEPT,
};
/**
* Getter for a list of all cookie banner rules. This includes both opt-in and opt-out rules.
*/
readonly attribute Array<nsICookieBannerRule> rules;
/**
* Look up all cookie rules for a given URI. Depending on the MODE_ this will
* return none, only reject rules or accept rules if there is no reject rule
* available.
*/
Array<nsICookieRule> getCookiesForURI(in nsIURI aURI);
/**
* Lookup or insert an empty nsICookieBannerRule and return it.
* Used by the CookieBannerListService to create and populate the rules.
*/
nsICookieBannerRule lookupOrInsertRuleForDomain(in ACString aDomain);
};

View File

@ -0,0 +1,38 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
#include "nsICookie.idl"
/**
* Rule which specifies a cookie to be set in order to handle a cookie banner.
*/
[builtinclass, scriptable, uuid(bf049b1e-8a05-481f-a120-332ea1bd65ef)]
interface nsICookieRule : nsISupports {
/**
* The cookie to set.
* When calling this getter creation, expiry and last accessed time are
* computed.
*/
readonly attribute nsICookie cookie;
/**
* Expiry time of the cookie in seconds relative to the injection time.
* If you want a cookie to expire in 1 month after it has been set, set this
* to 2592000.
* Defaults to 'cookiebanners.cookieInjector.defaultExpiryRelative'.
*/
readonly attribute int64_t expiryRelative;
/**
* If an existing cookie sets this value it may be overwritten.
* This is used for sites which set an explicit cookie state, even if a
* cookie banner is still pending.
*/
readonly attribute AUTF8String unsetValue;
};

View File

@ -27,6 +27,7 @@ DIRS += [
"certviewer",
"cleardata",
"clearsitedata",
"cookiebanners",
"commandlines",
"contentprefs",
"contextualidentity",

View File

@ -11,6 +11,7 @@
"nsIClearDataService": "clearData",
"nsIClipboard": "clipboard",
"nsIConsoleService": "console",
"nsICookieBannerService": "cookieBanners",
"nsICookieManager": "cookies",
"nsICookieService": "cookies",
"nsIDOMRequestService": "DOMRequest",