Files
sousa-gecko/dom/reporting/ReportDeliver.cpp
T
Nika Layzell e7bf4a196f Bug 2005006 - Store parsed URIs in InternalRequest/InternalResponse, r=dom-core,dom-worker-reviewers,necko-reviewers,smaug,kershaw,edenchuang
This patch updates InternalRequest and InternalResponse such that it parses the
URL passed to it at creation time, rather than when the channel is created.
This should improve the behaviour of these types around Blob URLs, so that
extra state, such as the "revoked" flag, is calculated at the correct time
as-per spec.

While making this change, I noticed that a previous Cache DB migration could
insert an "" URL in the DB when migrating from a single response URL to a URL
list, rather than storing an empty URL list. The choice was made to do
filtering when reading, rather than performing a migration, to avoid migration
first-time start-up overhead.

Differential Revision: https://phabricator.services.mozilla.com/D276047
2026-04-10 19:23:59 +00:00

550 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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/ReportDeliver.h"
#include <algorithm>
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Fetch.h"
#include "mozilla/dom/Navigator.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ReportBody.h"
#include "mozilla/dom/Request.h"
#include "mozilla/dom/RequestBinding.h"
#include "mozilla/dom/Response.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsGlobalWindowInner.h"
#include "nsIGlobalObject.h"
#include "nsIXPConnect.h"
#include "nsNetUtil.h"
#include "nsStringStream.h"
namespace mozilla::dom {
namespace {
StaticRefPtr<ReportDeliver> gReportDeliver;
// This is the same value as the default value of
// dom.min_timeout_value, so it's not that random.
constexpr double gMinReportAgeInMs = 4.0;
class ReportFetchHandler final : public PromiseNativeHandler {
public:
NS_DECL_ISUPPORTS
explicit ReportFetchHandler(
const nsTArray<ReportDeliver::ReportData>& aReportData)
: mReports(aReportData.Clone()) {}
void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
ErrorResult& aRv) override {
if (!gReportDeliver) {
return;
}
if (NS_WARN_IF(!aValue.isObject())) {
return;
}
JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
MOZ_ASSERT(obj);
{
Response* response = nullptr;
if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Response, &obj, response)))) {
return;
}
if (response->Status() == 410) {
if (XRE_IsContentProcess()) {
for (const auto& report : mReports) {
gReportDeliver->EndpointRespondedWithRemove(report.mGlobalKey,
report.mGroupName);
}
} else {
// Crash Reports will end up here, because they're not sent from with
// a content process. The endpoints used for crash reporting and NEL
// are parsed using the ReportingHeader::ReportingFromChannel, since
// these two variants of Reporting API strictly should run in the
// parent process
for (const auto& report : mReports) {
ReportingHeader::RemoveEndpoint(
report.mGroupName, report.mEndpointURL, report.mPrincipal);
}
}
}
}
}
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
ErrorResult& aRv) override {
if (gReportDeliver) {
for (auto& report : mReports) {
++report.mFailures;
gReportDeliver->EnqueueReport(report);
}
}
}
private:
~ReportFetchHandler() = default;
nsTArray<ReportDeliver::ReportData> mReports;
};
NS_IMPL_ISUPPORTS0(ReportFetchHandler)
class ReportJSONWriter final : public JSONWriter {
public:
explicit ReportJSONWriter(JSONStringWriteFunc<nsAutoCString>& aOutput)
: JSONWriter(aOutput) {}
void JSONProperty(const Span<const char>& aProperty,
const Span<const char>& aJSON) {
Separator();
PropertyNameAndColon(aProperty);
mWriter.Write(aJSON);
}
};
void SendReports(nsTArray<ReportDeliver::ReportData>& aReports,
const nsCString& aEndPointUrl, nsIPrincipal* aPrincipal) {
if (NS_WARN_IF(aReports.IsEmpty())) {
return;
}
nsIXPConnect* xpc = nsContentUtils::XPConnect();
MOZ_ASSERT(xpc, "This should never be null!");
nsCOMPtr<nsIGlobalObject> globalObject;
{
AutoJSAPI jsapi;
jsapi.Init();
JSContext* cx = jsapi.cx();
JS::Rooted<JSObject*> sandbox(cx);
nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address());
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
// The JSContext is not in a realm, so CreateSandbox returned an unwrapped
// global.
MOZ_ASSERT(JS_IsGlobalObject(sandbox));
globalObject = xpc::NativeGlobal(sandbox);
}
if (NS_WARN_IF(!globalObject)) {
return;
}
// The body
JSONStringWriteFunc<nsAutoCString> body;
ReportJSONWriter w(body);
uint64_t associatedBrowsingContextId = aReports[0].mAssociatedBrowsingContext;
w.StartArrayElement();
for (const auto& report : aReports) {
MOZ_ASSERT(report.mPrincipal == aPrincipal);
MOZ_ASSERT(report.mEndpointURL == aEndPointUrl);
w.StartObjectElement();
// It looks like in rare cases, TimeStamp::Now() may be the same
// as report.mCreationTime, so we introduce a constant number to
// make sure "age" is always not 0.
w.IntProperty(
"age",
std::max((TimeStamp::Now() - report.mCreationTime).ToMilliseconds(),
gMinReportAgeInMs));
w.StringProperty("type", NS_ConvertUTF16toUTF8(report.mType));
w.StringProperty("url", NS_ConvertUTF16toUTF8(report.mURL));
w.StringProperty("user_agent", NS_ConvertUTF16toUTF8(report.mUserAgent));
w.JSONProperty(MakeStringSpan("body"),
Span<const char>(report.mReportBodyJSON.Data(),
report.mReportBodyJSON.Length()));
w.EndObject();
}
w.EndArray();
// The body as stream
nsCOMPtr<nsIInputStream> streamBody;
nsresult rv =
NS_NewCStringInputStream(getter_AddRefs(streamBody), body.StringCRef());
// Headers
IgnoredErrorResult error;
RefPtr<InternalHeaders> internalHeaders =
new InternalHeaders(HeadersGuardEnum::Request);
internalHeaders->Set("Content-Type"_ns, "application/reports+json"_ns, error);
if (NS_WARN_IF(error.Failed())) {
return;
}
// URL and fragments
nsCOMPtr<nsIURI> uri;
rv = NS_NewURI(getter_AddRefs(uri), aEndPointUrl);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
nsCOMPtr<nsIURI> uriClone;
rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
nsAutoCString uriFragment;
rv = uri->GetRef(uriFragment);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
auto internalRequest =
MakeSafeRefPtr<InternalRequest>(WrapNotNull(uriClone.get()), uriFragment);
internalRequest->SetMethod("POST"_ns);
internalRequest->SetBody(streamBody, body.StringCRef().Length());
internalRequest->SetHeaders(internalHeaders);
internalRequest->SetSkipServiceWorker();
// TODO: internalRequest->SetContentPolicyType(TYPE_REPORT);
internalRequest->SetMode(RequestMode::Cors);
internalRequest->SetCredentialsMode(RequestCredentials::Same_origin);
internalRequest->SetUnsafeRequest();
if (aReports[0].mCookieJarSettings) {
internalRequest->SetCookieJarSettings(aReports[0].mCookieJarSettings);
}
internalRequest->SetAssociatedBrowsingContextID(associatedBrowsingContextId);
RefPtr<Request> request =
new Request(globalObject, std::move(internalRequest), nullptr);
RequestOrUTF8String fetchInput;
fetchInput.SetAsRequest() = request;
RootedDictionary<RequestInit> requestInit(RootingCx());
RefPtr<Promise> promise = FetchRequest(globalObject, fetchInput, requestInit,
CallerType::NonSystem, error);
if (error.Failed()) {
if (gReportDeliver) {
for (auto& report : aReports) {
++report.mFailures;
gReportDeliver->EnqueueReport(report);
}
}
return;
}
RefPtr<ReportFetchHandler> handler = new ReportFetchHandler(aReports);
promise->AppendNativeHandler(handler);
}
} // namespace
/* static */
void ReportDeliver::AttemptDelivery(nsIGlobalObject* aGlobal,
const nsAString& aType,
const nsAString& aGroupName,
const nsAString& aURL, ReportBody* aBody,
uint64_t aAssociatedBrowsingContextId) {
MOZ_ASSERT(aGlobal && aBody);
if (NS_WARN_IF(!gReportDeliver)) {
return;
}
nsCOMPtr<nsIPrincipal> principal = aGlobal->PrincipalOrNull();
if (NS_WARN_IF(!principal)) {
return;
}
// We have to serialize aBody here because the thread we're sending
// this to, sometimes isn't not the owner, which doesn't work for RefPtr.
JSONStringWriteFunc<nsAutoCString> reportBodyJSON;
ReportJSONWriter w(reportBodyJSON);
w.Start();
aBody->ToJSON(w);
w.End();
RefPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"ReportDeliver::AttemptDelivery",
[aGlobalKey = reinterpret_cast<uintptr_t>(aGlobal),
type = nsString{aType}, group = nsString{aGroupName},
reportUrl = nsString{aURL},
reportBody = std::move(reportBodyJSON).StringRRef(), principal,
browsingContextId = aAssociatedBrowsingContextId]() mutable {
ReportData data;
// https://w3c.github.io/reporting/#report-delivery
// 2.1 If there exists an endpoint (endpoint) in context’s endpoints
// list whose name is report’s destination:
// 2.1.1 Append report to
// endpoint map’s list of reports for endpoint.
nsIURI* endpointURI =
gReportDeliver->GetEndpointURLFor(aGlobalKey, group);
if (!endpointURI) {
return;
}
endpointURI->GetSpec(data.mEndpointURL);
data.mType = std::move(type);
data.mGroupName = std::move(group);
data.mURL = std::move(reportUrl);
data.mCreationTime = TimeStamp::Now();
data.mReportBodyJSON = std::move(reportBody);
data.mPrincipal = std::move(principal);
data.mFailures = 0;
data.mAssociatedBrowsingContext = browsingContextId;
gReportDeliver->SetGlobalAndUserAgentData(data, aGlobalKey);
ReportDeliver::Fetch(data);
});
if (!NS_IsMainThread()) {
NS_DispatchToMainThread(runnable.forget());
} else {
runnable->Run();
}
}
void ReportDeliver::SetGlobalAndUserAgentData(
ReportDeliver::ReportData& aReportData, uintptr_t aGlobalKey) {
// Will be null for workers
aReportData.mGlobalKey = aGlobalKey;
if (auto reportingGlobal = mGlobalsEndpointLists.Lookup(aGlobalKey)) {
aReportData.mUserAgent = reportingGlobal->mUserAgentData;
aReportData.mCookieJarSettings = reportingGlobal->mCookieJarSettings;
}
}
void ReportDeliver::ScheduleFetch() {
MOZ_ASSERT(NS_IsMainThread());
if (mPendingDelivery) {
return;
}
mPendingDelivery = true;
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"ReportDeliver::CallNotify",
[self = RefPtr<ReportDeliver>{gReportDeliver}]() { self->Notify(); });
NS_DispatchToCurrentThreadQueue(
runnable.forget(), StaticPrefs::dom_reporting_delivering_timeout() * 1000,
EventQueuePriority::Idle);
}
void ReportDeliver::EnqueueReport(const ReportData& aReportData) {
MOZ_ASSERT(NS_IsMainThread());
// If this is failed report, and queue is full, don't remove potentially
// non-tried reports, instead discard this one.
if ((aReportData.mFailures > 0 &&
mReportQueue.Length() >
StaticPrefs::dom_reporting_delivering_maxReports()) ||
aReportData.mFailures >=
StaticPrefs::dom_reporting_delivering_maxFailures()) {
return;
}
if (NS_WARN_IF(!mReportQueue.AppendElement(aReportData, fallible))) {
return;
}
while (mReportQueue.Length() >
StaticPrefs::dom_reporting_delivering_maxReports()) {
mReportQueue.RemoveElementAt(0);
}
ScheduleFetch();
}
void ReportDeliver::Initialize() {
if (!gReportDeliver) {
RefPtr<ReportDeliver> rd = new ReportDeliver();
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return;
}
obs->AddObserver(rd, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
gReportDeliver = rd;
}
}
/* static */
void ReportDeliver::WorkerInitializeReportingEndpoints(
uintptr_t aGlobalKey, nsIURI* aResourceURI, nsCString aHeaderContents,
bool aShouldResistFingerprinting,
nsICookieJarSettings* aCookieJarSettings) {
MOZ_ASSERT(!NS_IsMainThread());
if (NS_WARN_IF(!aResourceURI) || aHeaderContents.IsEmpty() ||
aHeaderContents.IsVoid()) {
return;
}
NS_DispatchToMainThread(NS_NewRunnableFunction(
"ReportDeliver::DispatchInitializeReportingEndpoints",
[aGlobalKey, uri = RefPtr{aResourceURI},
header = std::move(aHeaderContents), aShouldResistFingerprinting,
cookieJarSettings = nsCOMPtr{aCookieJarSettings}]() mutable {
EndpointsList list;
ReportingHeader::ParseReportingEndpointsHeader(
header, uri,
[&list](const nsAString& aEndpointName,
nsCOMPtr<nsIURI> aEndpointURL) {
list.mData.EmplaceBack(ReportingHeader::Endpoint::Create(
aEndpointURL.forget(), aEndpointName));
});
nsString userAgent;
mozilla::dom::Navigator::GetUserAgent(
nullptr, nullptr, Some(aShouldResistFingerprinting), userAgent);
gReportDeliver->mGlobalsEndpointLists.InsertOrUpdate(
aGlobalKey,
GlobalReportingData{std::move(userAgent), std::move(list),
cookieJarSettings});
}));
}
/** static */
void ReportDeliver::WindowInitializeReportingEndpoints(
nsIGlobalObject* aGlobal, mozilla::dom::EndpointsList aEndpointList) {
MOZ_ASSERT(NS_IsMainThread());
nsString userAgentData;
if (aEndpointList.mData.IsEmpty()) {
return;
}
nsPIDOMWindowInner* win = aGlobal->GetAsInnerWindow();
RefPtr<Document> doc;
if (win) {
doc = win->GetExtantDoc();
}
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
if (doc) {
cookieJarSettings = doc->CookieJarSettings();
}
(void)mozilla::dom::Navigator::GetUserAgent(
win, doc,
mozilla::Some(
aGlobal->ShouldResistFingerprinting(RFPTarget::NavigatorUserAgent)),
userAgentData);
gReportDeliver->mGlobalsEndpointLists.InsertOrUpdate(
reinterpret_cast<uintptr_t>(aGlobal),
GlobalReportingData{std::move(userAgentData), std::move(aEndpointList),
std::move(cookieJarSettings)});
}
nsIURI* ReportDeliver::GetEndpointURLFor(uintptr_t aGlobalKey,
const nsAString& aGroupName) {
MOZ_ASSERT(NS_IsMainThread());
auto reportingGlobal = mGlobalsEndpointLists.Lookup(aGlobalKey);
if (!reportingGlobal) {
return nullptr;
}
if (ReportingHeader::Endpoint* endpoint =
reportingGlobal->mEndpoints.GetEndpointWithName(aGroupName)) {
return endpoint->mUrl;
}
return nullptr;
}
void ReportDeliver::EndpointRespondedWithRemove(
uint64_t aGlobalKey, const nsAString& aEndpointName) {
auto reportingGlobal = mGlobalsEndpointLists.Lookup(aGlobalKey);
if (!reportingGlobal) {
return;
}
reportingGlobal->mEndpoints.RemoveEndpoint(aEndpointName);
}
/* static */
void ReportDeliver::Fetch(const ReportData& aReportData) {
if (aReportData.mFailures >
StaticPrefs::dom_reporting_delivering_maxFailures()) {
return;
}
gReportDeliver->EnqueueReport(aReportData);
}
void ReportDeliver::Notify() {
MOZ_ASSERT(NS_IsMainThread());
mPendingDelivery = false;
nsTArray<ReportData> reports = std::move(mReportQueue);
// group reports by endpoint and nsIPrincipal
std::map<std::pair<nsCString, nsCOMPtr<nsIPrincipal>>, nsTArray<ReportData>>
reportsByPrincipal;
for (ReportData& report : reports) {
auto already_seen =
reportsByPrincipal.find({report.mEndpointURL, report.mPrincipal});
if (already_seen == reportsByPrincipal.end()) {
reportsByPrincipal.emplace(
std::make_pair(report.mEndpointURL, report.mPrincipal),
nsTArray<ReportData>({report}));
} else {
already_seen->second.AppendElement(report);
}
}
for (auto& iter : reportsByPrincipal) {
std::pair<nsCString, nsCOMPtr<nsIPrincipal>> key = iter.first;
nsTArray<ReportData>& value = iter.second;
nsCString url = key.first;
nsCOMPtr<nsIPrincipal> principal = key.second;
nsAutoCString u(url);
SendReports(value, url, principal);
}
}
NS_IMETHODIMP
ReportDeliver::GetName(nsACString& aName) {
aName.AssignLiteral("ReportDeliver");
return NS_OK;
}
NS_IMETHODIMP
ReportDeliver::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return NS_OK;
}
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
gReportDeliver = nullptr;
return NS_OK;
}
ReportDeliver::ReportDeliver() = default;
ReportDeliver::~ReportDeliver() = default;
NS_INTERFACE_MAP_BEGIN(ReportDeliver)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_END
NS_IMPL_ADDREF(ReportDeliver)
NS_IMPL_RELEASE(ReportDeliver)
} // namespace mozilla::dom