summaryrefslogtreecommitdiffstats
path: root/dom/reporting/ReportDeliver.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/reporting/ReportDeliver.cpp410
1 files changed, 410 insertions, 0 deletions
diff --git a/dom/reporting/ReportDeliver.cpp b/dom/reporting/ReportDeliver.cpp
new file mode 100644
index 0000000000..08a31e57ee
--- /dev/null
+++ b/dom/reporting/ReportDeliver.cpp
@@ -0,0 +1,410 @@
+/* -*- 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/JSONStringWriteFuncs.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/EndpointForReportChild.h"
+#include "mozilla/dom/Fetch.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ReportBody.h"
+#include "mozilla/dom/ReportDeliver.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;
+
+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) {
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+
+ for (const auto& report : mReports) {
+ mozilla::ipc::PrincipalInfo principalInfo;
+ nsresult rv =
+ PrincipalToPrincipalInfo(report.mPrincipal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ actorChild->SendRemoveEndpoint(report.mGroupName, report.mEndpointURL,
+ principalInfo);
+ }
+ }
+ }
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ if (gReportDeliver) {
+ for (auto& report : mReports) {
+ ++report.mFailures;
+ gReportDeliver->AppendReportData(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);
+
+ w.StartArrayElement();
+ for (const auto& report : aReports) {
+ MOZ_ASSERT(report.mPrincipal == aPrincipal);
+ MOZ_ASSERT(report.mEndpointURL == aEndPointUrl);
+ w.StartObjectElement();
+ w.IntProperty("age",
+ (TimeStamp::Now() - report.mCreationTime).ToMilliseconds());
+ 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 uriSpec;
+ rv = uriClone->GetSpec(uriSpec);
+ 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>(uriSpec, 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::Include);
+
+ RefPtr<Request> request =
+ new Request(globalObject, std::move(internalRequest), nullptr);
+
+ RequestOrUSVString fetchInput;
+ fetchInput.SetAsRequest() = request;
+
+ RootedDictionary<RequestInit> requestInit(RootingCx());
+ RefPtr<Promise> promise = FetchRequest(globalObject, fetchInput, requestInit,
+ CallerType::NonSystem, error);
+ if (error.Failed()) {
+ for (auto& report : aReports) {
+ ++report.mFailures;
+ if (gReportDeliver) {
+ gReportDeliver->AppendReportData(report);
+ }
+ }
+ return;
+ }
+
+ RefPtr<ReportFetchHandler> handler = new ReportFetchHandler(aReports);
+ promise->AppendNativeHandler(handler);
+}
+
+} // namespace
+
+/* static */
+void ReportDeliver::Record(nsPIDOMWindowInner* aWindow, const nsAString& aType,
+ const nsAString& aGroupName, const nsAString& aURL,
+ ReportBody* aBody) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aBody);
+
+ JSONStringWriteFunc<nsAutoCString> reportBodyJSON;
+ ReportJSONWriter w(reportBodyJSON);
+
+ w.Start();
+ aBody->ToJSON(w);
+ w.End();
+
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ mozilla::ipc::PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ mozilla::ipc::PBackgroundChild* actorChild =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+
+ PEndpointForReportChild* actor =
+ actorChild->SendPEndpointForReportConstructor(nsString(aGroupName),
+ principalInfo);
+ if (NS_WARN_IF(!actor)) {
+ return;
+ }
+
+ ReportData data;
+ data.mType = aType;
+ data.mGroupName = aGroupName;
+ data.mURL = aURL;
+ data.mCreationTime = TimeStamp::Now();
+ data.mReportBodyJSON = std::move(reportBodyJSON).StringRRef();
+ data.mPrincipal = principal;
+ data.mFailures = 0;
+
+ Navigator* navigator = aWindow->Navigator();
+ MOZ_ASSERT(navigator);
+
+ IgnoredErrorResult error;
+ navigator->GetUserAgent(data.mUserAgent, CallerType::NonSystem, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return;
+ }
+
+ static_cast<EndpointForReportChild*>(actor)->Initialize(data);
+}
+
+/* static */
+void ReportDeliver::Fetch(const ReportData& aReportData) {
+ 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;
+ }
+
+ gReportDeliver->AppendReportData(aReportData);
+}
+
+void ReportDeliver::AppendReportData(const ReportData& aReportData) {
+ if (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);
+ }
+
+ if (!mTimer) {
+ uint32_t timeout = StaticPrefs::dom_reporting_delivering_timeout() * 1000;
+ nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+}
+
+NS_IMETHODIMP
+ReportDeliver::Notify(nsITimer* aTimer) {
+ mTimer = nullptr;
+
+ 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);
+ }
+
+ return NS_OK;
+}
+
+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);
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ 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(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(ReportDeliver)
+NS_IMPL_RELEASE(ReportDeliver)
+
+} // namespace mozilla::dom