340 lines
12 KiB
C++
340 lines
12 KiB
C++
/* 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 "mozilla/RefPtr.h"
|
|
#include "nsDebug.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"
|
|
#include "nsNetUtil.h"
|
|
|
|
namespace mozilla {
|
|
|
|
LazyLogModule gCookieInjectorLog("nsCookieInjector");
|
|
|
|
StaticRefPtr<nsCookieInjector> sCookieInjectorSingleton;
|
|
|
|
static constexpr auto kHttpObserverMessage =
|
|
NS_HTTP_ON_MODIFY_REQUEST_BEFORE_COOKIES_TOPIC;
|
|
|
|
// List of prefs the injector needs to observe changes for.
|
|
// They are used to determine whether the component should be enabled.
|
|
static constexpr nsLiteralCString kObservedPrefs[] = {
|
|
"cookiebanners.service.mode"_ns,
|
|
"cookiebanners.service.mode.privateBrowsing"_ns,
|
|
"cookiebanners.service.detectOnly"_ns,
|
|
"cookiebanners.cookieInjector.enabled"_ns,
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(nsCookieInjector, nsIObserver);
|
|
|
|
already_AddRefed<nsCookieInjector> nsCookieInjector::GetSingleton() {
|
|
if (!sCookieInjectorSingleton) {
|
|
sCookieInjectorSingleton = new nsCookieInjector();
|
|
|
|
// Register pref listeners.
|
|
for (const auto& pref : kObservedPrefs) {
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
|
|
("Registering pref observer. %s", pref.get()));
|
|
DebugOnly<nsresult> rv =
|
|
Preferences::RegisterCallback(&nsCookieInjector::OnPrefChange, pref);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"Failed to register pref listener.");
|
|
}
|
|
|
|
// The code above only runs on pref change. Call pref change handler for
|
|
// inspecting the initial pref state.
|
|
nsCookieInjector::OnPrefChange(nullptr, nullptr);
|
|
|
|
// Clean up on shutdown.
|
|
RunOnShutdown([] {
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug, ("RunOnShutdown"));
|
|
|
|
// Unregister pref listeners.
|
|
for (const auto& pref : kObservedPrefs) {
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
|
|
("Unregistering pref observer. %s", pref.get()));
|
|
DebugOnly<nsresult> rv = Preferences::UnregisterCallback(
|
|
&nsCookieInjector::OnPrefChange, pref);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"Failed to unregister pref listener.");
|
|
}
|
|
|
|
DebugOnly<nsresult> rv = sCookieInjectorSingleton->Shutdown();
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"nsCookieInjector::Shutdown failed.");
|
|
sCookieInjectorSingleton = nullptr;
|
|
});
|
|
}
|
|
|
|
return do_AddRef(sCookieInjectorSingleton);
|
|
}
|
|
|
|
// static
|
|
bool nsCookieInjector::IsEnabledForCurrentPrefState() {
|
|
// For detect-only mode the component should be disabled because it does not
|
|
// have banner detection capabilities.
|
|
if (!StaticPrefs::cookiebanners_cookieInjector_enabled() ||
|
|
StaticPrefs::cookiebanners_service_detectOnly()) {
|
|
return false;
|
|
}
|
|
|
|
// The cookie injector is initialized if enabled by pref and the main service
|
|
// is enabled (either in private browsing or normal browsing).
|
|
return StaticPrefs::cookiebanners_service_mode() !=
|
|
nsICookieBannerService::MODE_DISABLED ||
|
|
StaticPrefs::cookiebanners_service_mode_privateBrowsing() !=
|
|
nsICookieBannerService::MODE_DISABLED;
|
|
}
|
|
|
|
// static
|
|
void nsCookieInjector::OnPrefChange(const char* aPref, void* aData) {
|
|
RefPtr<nsCookieInjector> injector = nsCookieInjector::GetSingleton();
|
|
|
|
if (IsEnabledForCurrentPrefState()) {
|
|
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 browser tab loads, e.g. extension panels.
|
|
RefPtr<mozilla::dom::BrowsingContext> browsingContext;
|
|
nsresult rv = loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (!browsingContext ||
|
|
!browsingContext->GetMessageManagerGroup().EqualsLiteral("browsers")) {
|
|
MOZ_LOG(
|
|
gCookieInjectorLog, LogLevel::Verbose,
|
|
("%s: Skip load for BC message manager group != browsers.", aTopic));
|
|
return NS_OK;
|
|
}
|
|
|
|
// 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;
|
|
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, NS_UsePrivateBrowsing(aChannel), 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();
|
|
|
|
bool hasInjectedCookie = false;
|
|
|
|
rv = InjectCookiesFromRules(hostPort, rules, attr, hasInjectedCookie);
|
|
|
|
if (hasInjectedCookie) {
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Debug,
|
|
("Setting HasInjectedCookieForCookieBannerHandling on loadInfo"));
|
|
loadInfo->SetHasInjectedCookieForCookieBannerHandling(true);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
nsresult nsCookieInjector::InjectCookiesFromRules(
|
|
const nsCString& aHostPort, const nsTArray<RefPtr<nsICookieRule>>& aRules,
|
|
OriginAttributes& aOriginAttributes, bool& aHasInjectedCookie) {
|
|
NS_ENSURE_TRUE(aRules.Length(), NS_ERROR_FAILURE);
|
|
aHasInjectedCookie = false;
|
|
|
|
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.
|
|
const net::Cookie& c = cookie->AsCookie();
|
|
|
|
// Check if the cookie is already set to avoid overwriting any custom
|
|
// settings.
|
|
nsCOMPtr<nsICookie> existingCookie;
|
|
rv = cookieManager->GetCookieNative(c.Host(), c.Path(), c.Name(),
|
|
&aOriginAttributes,
|
|
getter_AddRefs(existingCookie));
|
|
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 (existingCookie) {
|
|
nsCString unsetValue;
|
|
rv = cookieRule->GetUnsetValue(unsetValue);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Cookie exists and the rule doesn't specify an unset value, skip.
|
|
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;
|
|
}
|
|
|
|
nsAutoCString existingCookieValue;
|
|
rv = existingCookie->GetValue(existingCookieValue);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// If the unset value specified by the rule does not match the cookie
|
|
// value. We must not overwrite, skip.
|
|
if (!unsetValue.Equals(existingCookieValue)) {
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
|
|
("Skip setting already existing cookie. Cookie: %s, %s, %s, "
|
|
"%s. Rule unset value: %s",
|
|
c.Host().get(), c.Name().get(), c.Path().get(),
|
|
c.Value().get(), unsetValue.get()));
|
|
continue;
|
|
}
|
|
|
|
MOZ_LOG(gCookieInjectorLog, LogLevel::Info,
|
|
("Overwriting cookie because of known unset value state %s.",
|
|
unsetValue.get()));
|
|
}
|
|
|
|
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(
|
|
nullptr, 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()),
|
|
/* is partitioned: */ false, /* is from http: */ true, nullptr,
|
|
[](mozilla::net::CookieStruct&) { return true; });
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
aHasInjectedCookie = true;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace mozilla
|