gecko-dev/dom/reporting/ReportingHeader.cpp

775 строки
20 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "mozilla/dom/ReportingHeader.h"
#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
#include "js/JSON.h"
#include "js/PropertyAndElement.h" // JS_GetElement
#include "mozilla/dom/ReportingBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/SimpleGlobalObject.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsIEffectiveTLDService.h"
#include "nsIHttpChannel.h"
#include "nsIHttpProtocolHandler.h"
#include "nsIObserverService.h"
#include "nsIPrincipal.h"
#include "nsIRandomGenerator.h"
#include "nsIScriptError.h"
#include "nsNetUtil.h"
#include "nsXULAppAPI.h"
#define REPORTING_PURGE_ALL "reporting:purge-all"
#define REPORTING_PURGE_HOST "reporting:purge-host"
namespace mozilla::dom {
namespace {
StaticRefPtr<ReportingHeader> gReporting;
} // namespace
/* static */
void ReportingHeader::Initialize() {
MOZ_ASSERT(!gReporting);
MOZ_ASSERT(NS_IsMainThread());
if (!XRE_IsParentProcess()) {
return;
}
RefPtr<ReportingHeader> service = new ReportingHeader();
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return;
}
obs->AddObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC, false);
obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
obs->AddObserver(service, "clear-origin-attributes-data", false);
obs->AddObserver(service, REPORTING_PURGE_HOST, false);
obs->AddObserver(service, REPORTING_PURGE_ALL, false);
gReporting = service;
}
/* static */
void ReportingHeader::Shutdown() {
MOZ_ASSERT(NS_IsMainThread());
if (!gReporting) {
return;
}
RefPtr<ReportingHeader> service = gReporting;
gReporting = nullptr;
if (service->mCleanupTimer) {
service->mCleanupTimer->Cancel();
service->mCleanupTimer = nullptr;
}
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return;
}
obs->RemoveObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC);
obs->RemoveObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
obs->RemoveObserver(service, "clear-origin-attributes-data");
obs->RemoveObserver(service, REPORTING_PURGE_HOST);
obs->RemoveObserver(service, REPORTING_PURGE_ALL);
}
ReportingHeader::ReportingHeader() = default;
ReportingHeader::~ReportingHeader() = default;
NS_IMETHODIMP
ReportingHeader::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
Shutdown();
return NS_OK;
}
// Pref disabled.
if (!StaticPrefs::dom_reporting_header_enabled()) {
return NS_OK;
}
if (!strcmp(aTopic, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC)) {
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aSubject);
if (NS_WARN_IF(!channel)) {
return NS_OK;
}
ReportingFromChannel(channel);
return NS_OK;
}
if (!strcmp(aTopic, REPORTING_PURGE_HOST)) {
RemoveOriginsFromHost(nsDependentString(aData));
return NS_OK;
}
if (!strcmp(aTopic, "clear-origin-attributes-data")) {
OriginAttributesPattern pattern;
if (!pattern.Init(nsDependentString(aData))) {
NS_ERROR("Cannot parse origin attributes pattern");
return NS_ERROR_FAILURE;
}
RemoveOriginsFromOriginAttributesPattern(pattern);
return NS_OK;
}
if (!strcmp(aTopic, REPORTING_PURGE_ALL)) {
RemoveOrigins();
return NS_OK;
}
return NS_ERROR_FAILURE;
}
void ReportingHeader::ReportingFromChannel(nsIHttpChannel* aChannel) {
MOZ_ASSERT(aChannel);
if (!StaticPrefs::dom_reporting_header_enabled()) {
return;
}
// We want to use the final URI to check if Report-To should be allowed or
// not.
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (!IsSecureURI(uri)) {
return;
}
if (NS_UsePrivateBrowsing(aChannel)) {
return;
}
nsAutoCString headerValue;
rv = aChannel->GetResponseHeader("Report-To"_ns, headerValue);
if (NS_FAILED(rv)) {
return;
}
nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
if (NS_WARN_IF(!ssm)) {
return;
}
nsCOMPtr<nsIPrincipal> principal;
rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(principal));
if (NS_WARN_IF(NS_FAILED(rv)) || !principal) {
return;
}
nsAutoCString origin;
rv = principal->GetOrigin(origin);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UniquePtr<Client> client = ParseHeader(aChannel, uri, headerValue);
if (!client) {
return;
}
// Here we override the previous data.
mOrigins.InsertOrUpdate(origin, std::move(client));
MaybeCreateCleanupTimer();
}
/* static */ UniquePtr<ReportingHeader::Client> ReportingHeader::ParseHeader(
nsIHttpChannel* aChannel, nsIURI* aURI, const nsACString& aHeaderValue) {
MOZ_ASSERT(aURI);
// aChannel can be null in gtest
AutoJSAPI jsapi;
JSObject* cleanGlobal =
SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail);
if (NS_WARN_IF(!cleanGlobal)) {
return nullptr;
}
if (NS_WARN_IF(!jsapi.Init(cleanGlobal))) {
return nullptr;
}
// WebIDL dictionary parses single items. Let's create a object to parse the
// header.
nsAutoString json;
json.AppendASCII("{ \"items\": [");
json.Append(NS_ConvertUTF8toUTF16(aHeaderValue));
json.AppendASCII("]}");
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> jsonValue(cx);
bool ok = JS_ParseJSON(cx, json.BeginReading(), json.Length(), &jsonValue);
if (!ok) {
LogToConsoleInvalidJSON(aChannel, aURI);
return nullptr;
}
dom::ReportingHeaderValue data;
if (!data.Init(cx, jsonValue)) {
LogToConsoleInvalidJSON(aChannel, aURI);
return nullptr;
}
if (!data.mItems.WasPassed() || data.mItems.Value().IsEmpty()) {
return nullptr;
}
UniquePtr<Client> client = MakeUnique<Client>();
for (const dom::ReportingItem& item : data.mItems.Value()) {
nsAutoString groupName;
if (item.mGroup.isUndefined()) {
groupName.AssignLiteral("default");
} else if (!item.mGroup.isString()) {
LogToConsoleInvalidNameItem(aChannel, aURI);
continue;
} else {
JS::Rooted<JSString*> groupStr(cx, item.mGroup.toString());
MOZ_ASSERT(groupStr);
nsAutoJSString string;
if (NS_WARN_IF(!string.init(cx, groupStr))) {
continue;
}
groupName = string;
}
if (!item.mMax_age.isNumber() || !item.mEndpoints.isObject()) {
LogToConsoleIncompleteItem(aChannel, aURI, groupName);
continue;
}
JS::Rooted<JSObject*> endpoints(cx, &item.mEndpoints.toObject());
MOZ_ASSERT(endpoints);
bool isArray = false;
if (!JS::IsArrayObject(cx, endpoints, &isArray) || !isArray) {
LogToConsoleIncompleteItem(aChannel, aURI, groupName);
continue;
}
uint32_t endpointsLength;
if (!JS::GetArrayLength(cx, endpoints, &endpointsLength) ||
endpointsLength == 0) {
LogToConsoleIncompleteItem(aChannel, aURI, groupName);
continue;
}
const auto [begin, end] = client->mGroups.NonObservingRange();
if (std::any_of(begin, end, [&groupName](const Group& group) {
return group.mName == groupName;
})) {
LogToConsoleDuplicateGroup(aChannel, aURI, groupName);
continue;
}
Group* group = client->mGroups.AppendElement();
group->mName = groupName;
group->mIncludeSubdomains = item.mInclude_subdomains;
group->mTTL = item.mMax_age.toNumber();
group->mCreationTime = TimeStamp::Now();
for (uint32_t i = 0; i < endpointsLength; ++i) {
JS::Rooted<JS::Value> element(cx);
if (!JS_GetElement(cx, endpoints, i, &element)) {
return nullptr;
}
RootedDictionary<ReportingEndpoint> endpoint(cx);
if (!endpoint.Init(cx, element)) {
LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
continue;
}
if (!endpoint.mUrl.isString() ||
(!endpoint.mPriority.isUndefined() &&
(!endpoint.mPriority.isNumber() ||
endpoint.mPriority.toNumber() < 0)) ||
(!endpoint.mWeight.isUndefined() &&
(!endpoint.mWeight.isNumber() || endpoint.mWeight.toNumber() < 0))) {
LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
continue;
}
JS::Rooted<JSString*> endpointUrl(cx, endpoint.mUrl.toString());
MOZ_ASSERT(endpointUrl);
nsAutoJSString endpointString;
if (NS_WARN_IF(!endpointString.init(cx, endpointUrl))) {
continue;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), endpointString);
if (NS_FAILED(rv)) {
LogToConsoleInvalidURLEndpoint(aChannel, aURI, groupName,
endpointString);
continue;
}
Endpoint* ep = group->mEndpoints.AppendElement();
ep->mUrl = uri;
ep->mPriority =
endpoint.mPriority.isUndefined() ? 1 : endpoint.mPriority.toNumber();
ep->mWeight =
endpoint.mWeight.isUndefined() ? 1 : endpoint.mWeight.toNumber();
}
}
if (client->mGroups.IsEmpty()) {
return nullptr;
}
return client;
}
bool ReportingHeader::IsSecureURI(nsIURI* aURI) const {
MOZ_ASSERT(aURI);
bool prioriAuthenticated = false;
if (NS_WARN_IF(NS_FAILED(NS_URIChainHasFlags(
aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
&prioriAuthenticated)))) {
return false;
}
return prioriAuthenticated;
}
/* static */
void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel* aChannel,
nsIURI* aURI) {
nsTArray<nsString> params;
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidJSON", params);
}
/* static */
void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel,
nsIURI* aURI,
const nsAString& aName) {
nsTArray<nsString> params;
params.AppendElement(aName);
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderDuplicateGroup", params);
}
/* static */
void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel,
nsIURI* aURI) {
nsTArray<nsString> params;
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidNameItem",
params);
}
/* static */
void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel* aChannel,
nsIURI* aURI,
const nsAString& aName) {
nsTArray<nsString> params;
params.AppendElement(aName);
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidItem", params);
}
/* static */
void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel,
nsIURI* aURI,
const nsAString& aName) {
nsTArray<nsString> params;
params.AppendElement(aName);
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidEndpoint",
params);
}
/* static */
void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel,
nsIURI* aURI,
const nsAString& aName,
const nsAString& aURL) {
nsTArray<nsString> params;
params.AppendElement(aURL);
params.AppendElement(aName);
LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidURLEndpoint",
params);
}
/* static */
void ReportingHeader::LogToConsoleInternal(nsIHttpChannel* aChannel,
nsIURI* aURI, const char* aMsg,
const nsTArray<nsString>& aParams) {
MOZ_ASSERT(aURI);
if (!aChannel) {
// We are in a gtest.
return;
}
uint64_t windowID = 0;
nsresult rv = aChannel->GetTopLevelContentWindowId(&windowID);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (!windowID) {
nsCOMPtr<nsILoadGroup> loadGroup;
nsresult rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (loadGroup) {
windowID = nsContentUtils::GetInnerWindowID(loadGroup);
}
}
nsAutoString localizedMsg;
rv = nsContentUtils::FormatLocalizedString(
nsContentUtils::eSECURITY_PROPERTIES, aMsg, aParams, localizedMsg);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
rv = nsContentUtils::ReportToConsoleByWindowID(
localizedMsg, nsIScriptError::infoFlag, "Reporting"_ns, windowID, aURI);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
/* static */
void ReportingHeader::GetEndpointForReport(
const nsAString& aGroupName,
const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
nsACString& aEndpointURI) {
auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo);
if (NS_WARN_IF(principalOrErr.isErr())) {
return;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
GetEndpointForReport(aGroupName, principal, aEndpointURI);
}
/* static */
void ReportingHeader::GetEndpointForReport(const nsAString& aGroupName,
nsIPrincipal* aPrincipal,
nsACString& aEndpointURI) {
MOZ_ASSERT(aEndpointURI.IsEmpty());
if (!gReporting) {
return;
}
nsAutoCString origin;
nsresult rv = aPrincipal->GetOrigin(origin);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
Client* client = gReporting->mOrigins.Get(origin);
if (!client) {
return;
}
const auto [begin, end] = client->mGroups.NonObservingRange();
const auto foundIt = std::find_if(
begin, end,
[&aGroupName](const Group& group) { return group.mName == aGroupName; });
if (foundIt != end) {
GetEndpointForReportInternal(*foundIt, aEndpointURI);
}
// XXX More explicitly report an error if not found?
}
/* static */
void ReportingHeader::GetEndpointForReportInternal(
const ReportingHeader::Group& aGroup, nsACString& aEndpointURI) {
TimeDuration diff = TimeStamp::Now() - aGroup.mCreationTime;
if (diff.ToSeconds() > aGroup.mTTL) {
// Expired.
return;
}
if (aGroup.mEndpoints.IsEmpty()) {
return;
}
int64_t minPriority = -1;
uint32_t totalWeight = 0;
for (const Endpoint& endpoint : aGroup.mEndpoints.NonObservingRange()) {
if (minPriority == -1 || minPriority > endpoint.mPriority) {
minPriority = endpoint.mPriority;
totalWeight = endpoint.mWeight;
} else if (minPriority == endpoint.mPriority) {
totalWeight += endpoint.mWeight;
}
}
nsCOMPtr<nsIRandomGenerator> randomGenerator =
do_GetService("@mozilla.org/security/random-generator;1");
if (NS_WARN_IF(!randomGenerator)) {
return;
}
uint32_t randomNumber = 0;
nsresult rv = randomGenerator->GenerateRandomBytesInto(randomNumber);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
totalWeight = randomNumber % totalWeight;
const auto [begin, end] = aGroup.mEndpoints.NonObservingRange();
const auto foundIt = std::find_if(
begin, end, [minPriority, totalWeight](const Endpoint& endpoint) {
return minPriority == endpoint.mPriority &&
totalWeight < endpoint.mWeight;
});
if (foundIt != end) {
Unused << NS_WARN_IF(NS_FAILED(foundIt->mUrl->GetSpec(aEndpointURI)));
}
// XXX More explicitly report an error if not found?
}
/* static */
void ReportingHeader::RemoveEndpoint(
const nsAString& aGroupName, const nsACString& aEndpointURL,
const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
if (!gReporting) {
return;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aEndpointURL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo);
if (NS_WARN_IF(principalOrErr.isErr())) {
return;
}
nsAutoCString origin;
rv = principalOrErr.unwrap()->GetOrigin(origin);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
Client* client = gReporting->mOrigins.Get(origin);
if (!client) {
return;
}
// Scope for the group iterator.
{
nsTObserverArray<Group>::BackwardIterator iter(client->mGroups);
while (iter.HasMore()) {
const Group& group = iter.GetNext();
if (group.mName != aGroupName) {
continue;
}
// Scope for the endpoint iterator.
{
nsTObserverArray<Endpoint>::BackwardIterator endpointIter(
group.mEndpoints);
while (endpointIter.HasMore()) {
const Endpoint& endpoint = endpointIter.GetNext();
bool equal = false;
rv = endpoint.mUrl->Equals(uri, &equal);
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
if (equal) {
endpointIter.Remove();
break;
}
}
}
if (group.mEndpoints.IsEmpty()) {
iter.Remove();
}
break;
}
}
if (client->mGroups.IsEmpty()) {
gReporting->mOrigins.Remove(origin);
gReporting->MaybeCancelCleanupTimer();
}
}
void ReportingHeader::RemoveOriginsFromHost(const nsAString& aHost) {
nsCOMPtr<nsIEffectiveTLDService> tldService =
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
if (NS_WARN_IF(!tldService)) {
return;
}
NS_ConvertUTF16toUTF8 host(aHost);
for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
bool hasRootDomain = false;
nsresult rv = tldService->HasRootDomain(iter.Key(), host, &hasRootDomain);
if (NS_WARN_IF(NS_FAILED(rv)) || !hasRootDomain) {
continue;
}
iter.Remove();
}
MaybeCancelCleanupTimer();
}
void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
const OriginAttributesPattern& aPattern) {
for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
nsAutoCString suffix;
OriginAttributes attr;
if (NS_WARN_IF(!attr.PopulateFromOrigin(iter.Key(), suffix))) {
continue;
}
if (aPattern.Matches(attr)) {
iter.Remove();
}
}
MaybeCancelCleanupTimer();
}
void ReportingHeader::RemoveOrigins() {
mOrigins.Clear();
MaybeCancelCleanupTimer();
}
void ReportingHeader::RemoveOriginsForTTL() {
TimeStamp now = TimeStamp::Now();
for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
Client* client = iter.UserData();
// Scope of the iterator.
{
nsTObserverArray<Group>::BackwardIterator groupIter(client->mGroups);
while (groupIter.HasMore()) {
const Group& group = groupIter.GetNext();
TimeDuration diff = now - group.mCreationTime;
if (diff.ToSeconds() > group.mTTL) {
groupIter.Remove();
return;
}
}
}
if (client->mGroups.IsEmpty()) {
iter.Remove();
}
}
}
/* static */
bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString& aOrigin) {
if (!gReporting) {
return false;
}
return gReporting->mOrigins.Contains(aOrigin);
}
NS_IMETHODIMP
ReportingHeader::Notify(nsITimer* aTimer) {
mCleanupTimer = nullptr;
RemoveOriginsForTTL();
MaybeCreateCleanupTimer();
return NS_OK;
}
NS_IMETHODIMP
ReportingHeader::GetName(nsACString& aName) {
aName.AssignLiteral("ReportingHeader");
return NS_OK;
}
void ReportingHeader::MaybeCreateCleanupTimer() {
if (mCleanupTimer) {
return;
}
if (mOrigins.Count() == 0) {
return;
}
uint32_t timeout = StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
nsresult rv =
NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer), this, timeout,
nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
void ReportingHeader::MaybeCancelCleanupTimer() {
if (!mCleanupTimer) {
return;
}
if (mOrigins.Count() != 0) {
return;
}
mCleanupTimer->Cancel();
mCleanupTimer = nullptr;
}
NS_INTERFACE_MAP_BEGIN(ReportingHeader)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_END
NS_IMPL_ADDREF(ReportingHeader)
NS_IMPL_RELEASE(ReportingHeader)
} // namespace mozilla::dom