diff options
Diffstat (limited to 'dom/reporting')
40 files changed, 3860 insertions, 0 deletions
diff --git a/dom/reporting/CrashReport.cpp b/dom/reporting/CrashReport.cpp new file mode 100644 index 0000000000..9cfd5340d5 --- /dev/null +++ b/dom/reporting/CrashReport.cpp @@ -0,0 +1,59 @@ +/* -*- 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/CrashReport.h" + +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/ReportingHeader.h" +#include "mozilla/dom/ReportDeliver.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "nsIPrincipal.h" +#include "nsIURIMutator.h" +#include "nsString.h" + +namespace mozilla::dom { + +/* static */ +bool CrashReport::Deliver(nsIPrincipal* aPrincipal, bool aIsOOM) { + MOZ_ASSERT(aPrincipal); + + nsAutoCString endpoint_url; + ReportingHeader::GetEndpointForReport(u"default"_ns, aPrincipal, + endpoint_url); + if (endpoint_url.IsEmpty()) { + return false; + } + + nsCString safe_origin_spec; + aPrincipal->GetExposableSpec(safe_origin_spec); + + ReportDeliver::ReportData data; + data.mType = u"crash"_ns; + data.mGroupName = u"default"_ns; + CopyUTF8toUTF16(safe_origin_spec, data.mURL); + data.mCreationTime = TimeStamp::Now(); + + Navigator::GetUserAgent(nullptr, nullptr, Nothing(), data.mUserAgent); + data.mPrincipal = aPrincipal; + data.mFailures = 0; + data.mEndpointURL = endpoint_url; + + JSONStringWriteFunc<nsCString> body; + JSONWriter writer{body}; + + writer.Start(); + if (aIsOOM) { + writer.StringProperty("reason", "oom"); + } + writer.End(); + + data.mReportBodyJSON = std::move(body).StringRRef(); + + ReportDeliver::Fetch(data); + return true; +} + +} // namespace mozilla::dom diff --git a/dom/reporting/CrashReport.h b/dom/reporting/CrashReport.h new file mode 100644 index 0000000000..3a2ec64b05 --- /dev/null +++ b/dom/reporting/CrashReport.h @@ -0,0 +1,23 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_CrashReport_h +#define mozilla_dom_CrashReport_h + +#include "nsCOMPtr.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class CrashReport { + public: + static bool Deliver(nsIPrincipal* aPrincipal, bool aIsOOM); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CrashReport_h diff --git a/dom/reporting/DeprecationReportBody.cpp b/dom/reporting/DeprecationReportBody.cpp new file mode 100644 index 0000000000..1154121484 --- /dev/null +++ b/dom/reporting/DeprecationReportBody.cpp @@ -0,0 +1,81 @@ +/* -*- 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/DeprecationReportBody.h" +#include "mozilla/dom/ReportingBinding.h" +#include "mozilla/JSONWriter.h" + +namespace mozilla::dom { + +DeprecationReportBody::DeprecationReportBody( + nsIGlobalObject* aGlobal, const nsAString& aId, + const Nullable<uint64_t>& aDate, const nsAString& aMessage, + const nsAString& aSourceFile, const Nullable<uint32_t>& aLineNumber, + const Nullable<uint32_t>& aColumnNumber) + : ReportBody(aGlobal), + mId(aId), + mDate(aDate), + mMessage(aMessage), + mSourceFile(aSourceFile), + mLineNumber(aLineNumber), + mColumnNumber(aColumnNumber) { + MOZ_ASSERT(aGlobal); +} + +DeprecationReportBody::~DeprecationReportBody() = default; + +JSObject* DeprecationReportBody::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DeprecationReportBody_Binding::Wrap(aCx, this, aGivenProto); +} + +void DeprecationReportBody::GetId(nsAString& aId) const { aId = mId; } + +Nullable<uint64_t> DeprecationReportBody::GetAnticipatedRemoval() const { + return mDate; +} + +void DeprecationReportBody::GetMessage(nsAString& aMessage) const { + aMessage = mMessage; +} + +void DeprecationReportBody::GetSourceFile(nsAString& aSourceFile) const { + aSourceFile = mSourceFile; +} + +Nullable<uint32_t> DeprecationReportBody::GetLineNumber() const { + return mLineNumber; +} + +Nullable<uint32_t> DeprecationReportBody::GetColumnNumber() const { + return mColumnNumber; +} + +void DeprecationReportBody::ToJSON(JSONWriter& aWriter) const { + aWriter.StringProperty("id", NS_ConvertUTF16toUTF8(mId)); + // TODO: anticipatedRemoval? https://github.com/w3c/reporting/issues/132 + aWriter.StringProperty("message", NS_ConvertUTF16toUTF8(mMessage)); + + if (mSourceFile.IsEmpty()) { + aWriter.NullProperty("sourceFile"); + } else { + aWriter.StringProperty("sourceFile", NS_ConvertUTF16toUTF8(mSourceFile)); + } + + if (mLineNumber.IsNull()) { + aWriter.NullProperty("lineNumber"); + } else { + aWriter.IntProperty("lineNumber", mLineNumber.Value()); + } + + if (mColumnNumber.IsNull()) { + aWriter.NullProperty("columnNumber"); + } else { + aWriter.IntProperty("columnNumber", mColumnNumber.Value()); + } +} + +} // namespace mozilla::dom diff --git a/dom/reporting/DeprecationReportBody.h b/dom/reporting/DeprecationReportBody.h new file mode 100644 index 0000000000..9476899b12 --- /dev/null +++ b/dom/reporting/DeprecationReportBody.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_DeprecationReportBody_h +#define mozilla_dom_DeprecationReportBody_h + +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/ReportBody.h" +#include "nsString.h" + +namespace mozilla::dom { + +class DeprecationReportBody final : public ReportBody { + public: + DeprecationReportBody(nsIGlobalObject* aGlobal, const nsAString& aId, + const Nullable<uint64_t>& aDate, + const nsAString& aMessage, const nsAString& aSourceFile, + const Nullable<uint32_t>& aLineNumber, + const Nullable<uint32_t>& aColumnNumber); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetId(nsAString& aId) const; + + Nullable<uint64_t> GetAnticipatedRemoval() const; + + void GetMessage(nsAString& aMessage) const; + + void GetSourceFile(nsAString& aSourceFile) const; + + Nullable<uint32_t> GetLineNumber() const; + + Nullable<uint32_t> GetColumnNumber() const; + + protected: + void ToJSON(JSONWriter& aJSONWriter) const override; + + private: + ~DeprecationReportBody(); + + const nsString mId; + const Nullable<uint64_t> mDate; + const nsString mMessage; + const nsString mSourceFile; + const Nullable<uint32_t> mLineNumber; + const Nullable<uint32_t> mColumnNumber; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_DeprecationReportBody_h diff --git a/dom/reporting/EndpointForReportChild.cpp b/dom/reporting/EndpointForReportChild.cpp new file mode 100644 index 0000000000..ba10e077b1 --- /dev/null +++ b/dom/reporting/EndpointForReportChild.cpp @@ -0,0 +1,30 @@ +/* -*- 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/EndpointForReportChild.h" + +namespace mozilla::dom { + +EndpointForReportChild::EndpointForReportChild() = default; + +EndpointForReportChild::~EndpointForReportChild() = default; + +void EndpointForReportChild::Initialize( + const ReportDeliver::ReportData& aData) { + mReportData = aData; +} + +mozilla::ipc::IPCResult EndpointForReportChild::Recv__delete__( + const nsCString& aEndpointURL) { + if (!aEndpointURL.IsEmpty()) { + mReportData.mEndpointURL = aEndpointURL; + ReportDeliver::Fetch(mReportData); + } + + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/reporting/EndpointForReportChild.h b/dom/reporting/EndpointForReportChild.h new file mode 100644 index 0000000000..bdc2d9c819 --- /dev/null +++ b/dom/reporting/EndpointForReportChild.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_EndpointForReportChild_h +#define mozilla_dom_EndpointForReportChild_h + +#include "mozilla/dom/ReportDeliver.h" +#include "mozilla/dom/PEndpointForReportChild.h" + +namespace mozilla::dom { + +class EndpointForReport; + +class EndpointForReportChild final : public PEndpointForReportChild { + public: + EndpointForReportChild(); + ~EndpointForReportChild(); + + void Initialize(const ReportDeliver::ReportData& aReportData); + + mozilla::ipc::IPCResult Recv__delete__(const nsCString& aEndpointURL); + + private: + ReportDeliver::ReportData mReportData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_EndpointForReportChild_h diff --git a/dom/reporting/EndpointForReportParent.cpp b/dom/reporting/EndpointForReportParent.cpp new file mode 100644 index 0000000000..445cbf1818 --- /dev/null +++ b/dom/reporting/EndpointForReportParent.cpp @@ -0,0 +1,44 @@ +/* -*- 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/EndpointForReportParent.h" +#include "mozilla/dom/ReportingHeader.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/Unused.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +EndpointForReportParent::EndpointForReportParent() + : mPBackgroundThread(NS_GetCurrentThread()), mActive(true) {} + +EndpointForReportParent::~EndpointForReportParent() = default; + +void EndpointForReportParent::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; +} + +void EndpointForReportParent::Run( + const nsAString& aGroupName, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + RefPtr<EndpointForReportParent> self = this; + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "EndpointForReportParent::Run", + [self, aGroupName = nsString(aGroupName), aPrincipalInfo]() { + nsAutoCString uri; + ReportingHeader::GetEndpointForReport(aGroupName, aPrincipalInfo, uri); + self->mPBackgroundThread->Dispatch(NS_NewRunnableFunction( + "EndpointForReportParent::Answer", [self, uri]() { + if (self->mActive) { + Unused << self->Send__delete__(self, uri); + } + })); + })); +} + +} // namespace mozilla::dom diff --git a/dom/reporting/EndpointForReportParent.h b/dom/reporting/EndpointForReportParent.h new file mode 100644 index 0000000000..42d5cce526 --- /dev/null +++ b/dom/reporting/EndpointForReportParent.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_EndpointForReportParent_h +#define mozilla_dom_EndpointForReportParent_h + +#include "mozilla/dom/PEndpointForReportParent.h" + +namespace mozilla { +namespace ipc { +class PrincipalInfo; +} + +namespace dom { + +class EndpointForReport; + +class EndpointForReportParent final : public PEndpointForReportParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(EndpointForReportParent) + + EndpointForReportParent(); + + void Run(const nsAString& aGroupName, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + + private: + ~EndpointForReportParent(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsCOMPtr<nsIThread> mPBackgroundThread; + bool mActive; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_EndpointForReportParent_h diff --git a/dom/reporting/FeaturePolicyViolationReportBody.cpp b/dom/reporting/FeaturePolicyViolationReportBody.cpp new file mode 100644 index 0000000000..a8c46bf6ed --- /dev/null +++ b/dom/reporting/FeaturePolicyViolationReportBody.cpp @@ -0,0 +1,79 @@ +/* -*- 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/FeaturePolicyViolationReportBody.h" + +#include "mozilla/JSONWriter.h" +#include "mozilla/dom/FeaturePolicyBinding.h" + +namespace mozilla::dom { + +FeaturePolicyViolationReportBody::FeaturePolicyViolationReportBody( + nsIGlobalObject* aGlobal, const nsAString& aFeatureId, + const nsAString& aSourceFile, const Nullable<int32_t>& aLineNumber, + const Nullable<int32_t>& aColumnNumber, const nsAString& aDisposition) + : ReportBody(aGlobal), + mFeatureId(aFeatureId), + mSourceFile(aSourceFile), + mLineNumber(aLineNumber), + mColumnNumber(aColumnNumber), + mDisposition(aDisposition) {} + +FeaturePolicyViolationReportBody::~FeaturePolicyViolationReportBody() = default; + +JSObject* FeaturePolicyViolationReportBody::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return FeaturePolicyViolationReportBody_Binding::Wrap(aCx, this, aGivenProto); +} + +void FeaturePolicyViolationReportBody::GetFeatureId( + nsAString& aFeatureId) const { + aFeatureId = mFeatureId; +} + +void FeaturePolicyViolationReportBody::GetSourceFile( + nsAString& aSourceFile) const { + aSourceFile = mSourceFile; +} + +Nullable<int32_t> FeaturePolicyViolationReportBody::GetLineNumber() const { + return mLineNumber; +} + +Nullable<int32_t> FeaturePolicyViolationReportBody::GetColumnNumber() const { + return mColumnNumber; +} + +void FeaturePolicyViolationReportBody::GetDisposition( + nsAString& aDisposition) const { + aDisposition = mDisposition; +} + +void FeaturePolicyViolationReportBody::ToJSON(JSONWriter& aWriter) const { + aWriter.StringProperty("featureId", NS_ConvertUTF16toUTF8(mFeatureId)); + + if (mSourceFile.IsEmpty()) { + aWriter.NullProperty("sourceFile"); + } else { + aWriter.StringProperty("sourceFile", NS_ConvertUTF16toUTF8(mSourceFile)); + } + + if (mLineNumber.IsNull()) { + aWriter.NullProperty("lineNumber"); + } else { + aWriter.IntProperty("lineNumber", mLineNumber.Value()); + } + + if (mColumnNumber.IsNull()) { + aWriter.NullProperty("columnNumber"); + } else { + aWriter.IntProperty("columnNumber", mColumnNumber.Value()); + } + + aWriter.StringProperty("disposition", NS_ConvertUTF16toUTF8(mDisposition)); +} + +} // namespace mozilla::dom diff --git a/dom/reporting/FeaturePolicyViolationReportBody.h b/dom/reporting/FeaturePolicyViolationReportBody.h new file mode 100644 index 0000000000..bf6f749256 --- /dev/null +++ b/dom/reporting/FeaturePolicyViolationReportBody.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_FeaturePolicyViolationReportBody_h +#define mozilla_dom_FeaturePolicyViolationReportBody_h + +#include "mozilla/dom/ReportBody.h" +#include "mozilla/dom/Nullable.h" +#include "nsString.h" + +namespace mozilla::dom { + +class FeaturePolicyViolationReportBody final : public ReportBody { + public: + FeaturePolicyViolationReportBody(nsIGlobalObject* aGlobal, + const nsAString& aFeatureId, + const nsAString& aSourceFile, + const Nullable<int32_t>& aLineNumber, + const Nullable<int32_t>& aColumnNumber, + const nsAString& aDisposition); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetFeatureId(nsAString& aFeatureId) const; + + void GetSourceFile(nsAString& aSourceFile) const; + + Nullable<int32_t> GetLineNumber() const; + + Nullable<int32_t> GetColumnNumber() const; + + void GetDisposition(nsAString& aDisposition) const; + + protected: + void ToJSON(JSONWriter& aJSONWriter) const override; + + private: + ~FeaturePolicyViolationReportBody(); + + const nsString mFeatureId; + const nsString mSourceFile; + const Nullable<int32_t> mLineNumber; + const Nullable<int32_t> mColumnNumber; + const nsString mDisposition; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FeaturePolicyViolationReportBody_h diff --git a/dom/reporting/PEndpointForReport.ipdl b/dom/reporting/PEndpointForReport.ipdl new file mode 100644 index 0000000000..e04897402b --- /dev/null +++ b/dom/reporting/PEndpointForReport.ipdl @@ -0,0 +1,20 @@ +/* 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 protocol PBackground; + +namespace mozilla { +namespace dom { + +[ManualDealloc] +protocol PEndpointForReport +{ + manager PBackground; + +child: + async __delete__(nsCString endpointURL); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/reporting/Report.cpp b/dom/reporting/Report.cpp new file mode 100644 index 0000000000..3fae46709a --- /dev/null +++ b/dom/reporting/Report.cpp @@ -0,0 +1,47 @@ +/* -*- 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/Report.h" +#include "mozilla/dom/ReportBody.h" +#include "mozilla/dom/ReportingBinding.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Report, mGlobal, mBody) +NS_IMPL_CYCLE_COLLECTING_ADDREF(Report) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Report) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Report) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +Report::Report(nsIGlobalObject* aGlobal, const nsAString& aType, + const nsAString& aURL, ReportBody* aBody) + : mGlobal(aGlobal), mType(aType), mURL(aURL), mBody(aBody) { + MOZ_ASSERT(aGlobal); +} + +Report::~Report() = default; + +already_AddRefed<Report> Report::Clone() { + RefPtr<Report> report = new Report(mGlobal, mType, mURL, mBody); + return report.forget(); +} + +JSObject* Report::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Report_Binding::Wrap(aCx, this, aGivenProto); +} + +void Report::GetType(nsAString& aType) const { aType = mType; } + +void Report::GetUrl(nsAString& aURL) const { aURL = mURL; } + +ReportBody* Report::GetBody() const { return mBody; } + +} // namespace mozilla::dom diff --git a/dom/reporting/Report.h b/dom/reporting/Report.h new file mode 100644 index 0000000000..e139e27858 --- /dev/null +++ b/dom/reporting/Report.h @@ -0,0 +1,59 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_Report_h +#define mozilla_dom_Report_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class ReportBody; + +class Report final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Report) + + Report(nsIGlobalObject* aGlobal, const nsAString& aType, + const nsAString& aURL, ReportBody* aBody); + + already_AddRefed<Report> Clone(); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + void GetType(nsAString& aType) const; + + void GetUrl(nsAString& aURL) const; + + ReportBody* GetBody() const; + + private: + ~Report(); + + nsCOMPtr<nsIGlobalObject> mGlobal; + + const nsString mType; + const nsString mURL; + RefPtr<ReportBody> mBody; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_Report_h diff --git a/dom/reporting/ReportBody.cpp b/dom/reporting/ReportBody.cpp new file mode 100644 index 0000000000..ef4330916e --- /dev/null +++ b/dom/reporting/ReportBody.cpp @@ -0,0 +1,27 @@ +/* -*- 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/ReportBody.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ReportBody, mGlobal) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReportBody) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReportBody) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReportBody) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ReportBody::ReportBody(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) { + MOZ_ASSERT(aGlobal); +} + +ReportBody::~ReportBody() = default; + +} // namespace mozilla::dom diff --git a/dom/reporting/ReportBody.h b/dom/reporting/ReportBody.h new file mode 100644 index 0000000000..9043dba16d --- /dev/null +++ b/dom/reporting/ReportBody.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ReportBody_h +#define mozilla_dom_ReportBody_h + +#include "mozilla/Assertions.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla { + +class JSONWriter; + +namespace dom { + +class ReportBody : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ReportBody) + + explicit ReportBody(nsIGlobalObject* aGlobal); + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + virtual void ToJSON(JSONWriter& aJSONWriter) const = 0; + + protected: + virtual ~ReportBody(); + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ReportBody_h 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 diff --git a/dom/reporting/ReportDeliver.h b/dom/reporting/ReportDeliver.h new file mode 100644 index 0000000000..f5d3f0ab6e --- /dev/null +++ b/dom/reporting/ReportDeliver.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ReportDeliver_h +#define mozilla_dom_ReportDeliver_h + +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsTArray.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsIPrincipal.h" + +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla::dom { + +class ReportBody; + +class ReportDeliver final : public nsIObserver, + public nsITimerCallback, + public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + struct ReportData { + nsString mType; + nsString mGroupName; + nsString mURL; + nsCString mEndpointURL; + nsString mUserAgent; + TimeStamp mCreationTime; + nsCString mReportBodyJSON; + nsCOMPtr<nsIPrincipal> mPrincipal; + uint32_t mFailures; + }; + + static void Record(nsPIDOMWindowInner* aWindow, const nsAString& aType, + const nsAString& aGroupName, const nsAString& aURL, + ReportBody* aBody); + + static void Fetch(const ReportData& aReportData); + + void AppendReportData(const ReportData& aReportData); + + private: + ReportDeliver(); + ~ReportDeliver(); + + nsTArray<ReportData> mReportQueue; + + nsCOMPtr<nsITimer> mTimer; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReportDeliver_h diff --git a/dom/reporting/ReportingHeader.cpp b/dom/reporting/ReportingHeader.cpp new file mode 100644 index 0000000000..552036852d --- /dev/null +++ b/dom/reporting/ReportingHeader.cpp @@ -0,0 +1,774 @@ +/* -*- 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 diff --git a/dom/reporting/ReportingHeader.h b/dom/reporting/ReportingHeader.h new file mode 100644 index 0000000000..d0db5f612b --- /dev/null +++ b/dom/reporting/ReportingHeader.h @@ -0,0 +1,142 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ReportingHeader_h +#define mozilla_dom_ReportingHeader_h + +#include "mozilla/TimeStamp.h" +#include "nsClassHashtable.h" +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsTObserverArray.h" + +class nsIHttpChannel; +class nsIPrincipal; +class nsIURI; + +namespace mozilla { + +class OriginAttributesPattern; + +namespace ipc { +class PrincipalInfo; +} + +namespace dom { + +class ReportingHeader final : public nsIObserver, + public nsITimerCallback, + public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + static void Initialize(); + + // Exposed structs for gtests + + struct Endpoint { + nsCOMPtr<nsIURI> mUrl; + uint32_t mPriority; + uint32_t mWeight; + }; + + struct Group { + nsString mName; + bool mIncludeSubdomains; + int32_t mTTL; + TimeStamp mCreationTime; + nsTObserverArray<Endpoint> mEndpoints; + }; + + struct Client { + nsTObserverArray<Group> mGroups; + }; + + static UniquePtr<Client> ParseHeader(nsIHttpChannel* aChannel, nsIURI* aURI, + const nsACString& aHeaderValue); + + static void GetEndpointForReport( + const nsAString& aGroupName, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + nsACString& aEndpointURI); + + static void GetEndpointForReport(const nsAString& aGroupName, + nsIPrincipal* aPrincipal, + nsACString& aEndpointURI); + + static void RemoveEndpoint(const nsAString& aGroupName, + const nsACString& aEndpointURL, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + + // ChromeOnly-WebIDL methods + + static bool HasReportingHeaderForOrigin(const nsACString& aOrigin); + + private: + ReportingHeader(); + ~ReportingHeader(); + + static void Shutdown(); + + // Checks if a channel contains a Report-To header and parses its value. + void ReportingFromChannel(nsIHttpChannel* aChannel); + + // This method checks if the protocol handler of the URI has the + // URI_IS_POTENTIALLY_TRUSTWORTHY flag. + bool IsSecureURI(nsIURI* aURI) const; + + void RemoveOriginsFromHost(const nsAString& aHost); + + void RemoveOriginsFromOriginAttributesPattern( + const OriginAttributesPattern& aPattern); + + void RemoveOrigins(); + + void RemoveOriginsForTTL(); + + void MaybeCreateCleanupTimer(); + + void MaybeCancelCleanupTimer(); + + static void LogToConsoleInvalidJSON(nsIHttpChannel* aChannel, nsIURI* aURI); + + static void LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel, nsIURI* aURI, + const nsAString& aName); + + static void LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel, + nsIURI* aURI); + + static void LogToConsoleIncompleteItem(nsIHttpChannel* aChannel, nsIURI* aURI, + const nsAString& aName); + + static void LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel, + nsIURI* aURI, + const nsAString& aName); + + static void LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel, + nsIURI* aURI, + const nsAString& aName, + const nsAString& aURL); + + static void LogToConsoleInternal(nsIHttpChannel* aChannel, nsIURI* aURI, + const char* aMsg, + const nsTArray<nsString>& aParams); + + static void GetEndpointForReportInternal(const ReportingHeader::Group& aGrup, + nsACString& aEndpointURI); + + nsClassHashtable<nsCStringHashKey, Client> mOrigins; + + nsCOMPtr<nsITimer> mCleanupTimer; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ReportingHeader_h diff --git a/dom/reporting/ReportingObserver.cpp b/dom/reporting/ReportingObserver.cpp new file mode 100644 index 0000000000..808824ce95 --- /dev/null +++ b/dom/reporting/ReportingObserver.cpp @@ -0,0 +1,152 @@ +/* -*- 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/ReportingObserver.h" +#include "mozilla/dom/Report.h" +#include "mozilla/dom/ReportingBinding.h" +#include "nsContentUtils.h" +#include "nsIGlobalObject.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ReportingObserver) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ReportingObserver) + tmp->Disconnect(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReports) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ReportingObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReports) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/* static */ +already_AddRefed<ReportingObserver> ReportingObserver::Constructor( + const GlobalObject& aGlobal, ReportingObserverCallback& aCallback, + const ReportingObserverOptions& aOptions, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + + nsTArray<nsString> types; + if (aOptions.mTypes.WasPassed()) { + types = aOptions.mTypes.Value(); + } + + RefPtr<ReportingObserver> ro = + new ReportingObserver(global, aCallback, types, aOptions.mBuffered); + + return ro.forget(); +} + +ReportingObserver::ReportingObserver(nsIGlobalObject* aGlobal, + ReportingObserverCallback& aCallback, + const nsTArray<nsString>& aTypes, + bool aBuffered) + : mGlobal(aGlobal), + mCallback(&aCallback), + mTypes(aTypes.Clone()), + mBuffered(aBuffered) { + MOZ_ASSERT(aGlobal); +} + +ReportingObserver::~ReportingObserver() { Disconnect(); } + +JSObject* ReportingObserver::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ReportingObserver_Binding::Wrap(aCx, this, aGivenProto); +} + +void ReportingObserver::Observe() { + mGlobal->RegisterReportingObserver(this, mBuffered); +} + +void ReportingObserver::Disconnect() { + if (mGlobal) { + mGlobal->UnregisterReportingObserver(this); + } +} + +void ReportingObserver::TakeRecords(nsTArray<RefPtr<Report>>& aRecords) { + mReports.SwapElements(aRecords); +} + +namespace { + +class ReportRunnable final : public DiscardableRunnable { + public: + explicit ReportRunnable(nsIGlobalObject* aGlobal) + : DiscardableRunnable("ReportRunnable"), mGlobal(aGlobal) {} + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + MOZ_KnownLive(mGlobal)->NotifyReportingObservers(); + return NS_OK; + } + + private: + const nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +} // namespace + +void ReportingObserver::MaybeReport(Report* aReport) { + MOZ_ASSERT(aReport); + + if (!mTypes.IsEmpty()) { + nsAutoString type; + aReport->GetType(type); + + if (!mTypes.Contains(type)) { + return; + } + } + + bool wasEmpty = mReports.IsEmpty(); + + RefPtr<Report> report = aReport->Clone(); + MOZ_ASSERT(report); + + if (NS_WARN_IF(!mReports.AppendElement(report, fallible))) { + return; + } + + if (!wasEmpty) { + return; + } + + RefPtr<ReportRunnable> r = new ReportRunnable(mGlobal); + NS_DispatchToCurrentThread(r); +} + +void ReportingObserver::MaybeNotify() { + if (mReports.IsEmpty()) { + return; + } + + // Let's take the ownership of the reports. + nsTArray<RefPtr<Report>> list = std::move(mReports); + + Sequence<OwningNonNull<Report>> reports; + for (Report* report : list) { + if (NS_WARN_IF(!reports.AppendElement(*report, fallible))) { + return; + } + } + + // We should report if this throws exception. But where? + RefPtr<ReportingObserverCallback> callback(mCallback); + callback->Call(reports, *this); +} + +void ReportingObserver::ForgetReports() { mReports.Clear(); } + +} // namespace mozilla::dom diff --git a/dom/reporting/ReportingObserver.h b/dom/reporting/ReportingObserver.h new file mode 100644 index 0000000000..b89ee33adf --- /dev/null +++ b/dom/reporting/ReportingObserver.h @@ -0,0 +1,77 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ReportingObserver_h +#define mozilla_dom_ReportingObserver_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class GlobalObject; +class Report; +class ReportingObserverCallback; +struct ReportingObserverOptions; + +class ReportingObserver final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ReportingObserver) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(ReportingObserver) + + static already_AddRefed<ReportingObserver> Constructor( + const GlobalObject& aGlobal, ReportingObserverCallback& aCallback, + const ReportingObserverOptions& aOptions, ErrorResult& aRv); + + ReportingObserver(nsIGlobalObject* aGlobal, + ReportingObserverCallback& aCallback, + const nsTArray<nsString>& aTypes, bool aBuffered); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + void Observe(); + + void Disconnect(); + + void TakeRecords(nsTArray<RefPtr<Report>>& aRecords); + + void MaybeReport(Report* aReport); + + MOZ_CAN_RUN_SCRIPT void MaybeNotify(); + + void ForgetReports(); + + private: + ~ReportingObserver(); + + nsTArray<RefPtr<Report>> mReports; + + nsCOMPtr<nsIGlobalObject> mGlobal; + RefPtr<ReportingObserverCallback> mCallback; + nsTArray<nsString> mTypes; + bool mBuffered; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ReportingObserver_h diff --git a/dom/reporting/ReportingUtils.cpp b/dom/reporting/ReportingUtils.cpp new file mode 100644 index 0000000000..ce7966a0a3 --- /dev/null +++ b/dom/reporting/ReportingUtils.cpp @@ -0,0 +1,45 @@ +/* -*- 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/ReportingUtils.h" +#include "mozilla/dom/ReportBody.h" +#include "mozilla/dom/ReportDeliver.h" +#include "mozilla/dom/Report.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsAtom.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +/* static */ +void ReportingUtils::Report(nsIGlobalObject* aGlobal, nsAtom* aType, + const nsAString& aGroupName, const nsAString& aURL, + ReportBody* aBody) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aBody); + + nsDependentAtomString type(aType); + + RefPtr<mozilla::dom::Report> report = + new mozilla::dom::Report(aGlobal, type, aURL, aBody); + aGlobal->BroadcastReport(report); + + if (!NS_IsMainThread()) { + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (!window) { + return; + } + + // Send the report to the server. + ReportDeliver::Record(window, type, aGroupName, aURL, aBody); +} + +} // namespace mozilla::dom diff --git a/dom/reporting/ReportingUtils.h b/dom/reporting/ReportingUtils.h new file mode 100644 index 0000000000..ee9726bed7 --- /dev/null +++ b/dom/reporting/ReportingUtils.h @@ -0,0 +1,28 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ReportingUtils_h +#define mozilla_dom_ReportingUtils_h + +#include "nsString.h" + +class nsAtom; +class nsIGlobalObject; + +namespace mozilla::dom { + +class ReportBody; + +class ReportingUtils final { + public: + static void Report(nsIGlobalObject* aGlobal, nsAtom* aType, + const nsAString& aGroupName, const nsAString& aURL, + ReportBody* aBody); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReportingUtils_h diff --git a/dom/reporting/TestingDeprecatedInterface.cpp b/dom/reporting/TestingDeprecatedInterface.cpp new file mode 100644 index 0000000000..9c03f3125d --- /dev/null +++ b/dom/reporting/TestingDeprecatedInterface.cpp @@ -0,0 +1,48 @@ +/* -*- 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/TestingDeprecatedInterface.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ReportingBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TestingDeprecatedInterface, mGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TestingDeprecatedInterface) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TestingDeprecatedInterface) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TestingDeprecatedInterface) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<TestingDeprecatedInterface> +TestingDeprecatedInterface::Constructor(const GlobalObject& aGlobal) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + + RefPtr<TestingDeprecatedInterface> obj = + new TestingDeprecatedInterface(global); + return obj.forget(); +} + +TestingDeprecatedInterface::TestingDeprecatedInterface(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) {} + +TestingDeprecatedInterface::~TestingDeprecatedInterface() = default; + +JSObject* TestingDeprecatedInterface::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return TestingDeprecatedInterface_Binding::Wrap(aCx, this, aGivenProto); +} + +void TestingDeprecatedInterface::DeprecatedMethod() const {} + +bool TestingDeprecatedInterface::DeprecatedAttribute() const { return true; } + +} // namespace mozilla::dom diff --git a/dom/reporting/TestingDeprecatedInterface.h b/dom/reporting/TestingDeprecatedInterface.h new file mode 100644 index 0000000000..7056a91ed7 --- /dev/null +++ b/dom/reporting/TestingDeprecatedInterface.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_TestingDeprecatedInterface_h +#define mozilla_dom_TestingDeprecatedInterface_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla::dom { +class GlobalObject; + +class TestingDeprecatedInterface final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TestingDeprecatedInterface) + + static already_AddRefed<TestingDeprecatedInterface> Constructor( + const GlobalObject& aGlobal); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + void DeprecatedMethod() const; + + bool DeprecatedAttribute() const; + + private: + explicit TestingDeprecatedInterface(nsIGlobalObject* aGlobal); + ~TestingDeprecatedInterface(); + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TestingDeprecatedInterface_h diff --git a/dom/reporting/moz.build b/dom/reporting/moz.build new file mode 100644 index 0000000000..73a091dc72 --- /dev/null +++ b/dom/reporting/moz.build @@ -0,0 +1,51 @@ +# -*- 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/. + +EXPORTS.mozilla.dom = [ + "CrashReport.h", + "DeprecationReportBody.h", + "EndpointForReportChild.h", + "EndpointForReportParent.h", + "FeaturePolicyViolationReportBody.h", + "Report.h", + "ReportBody.h", + "ReportDeliver.h", + "ReportingHeader.h", + "ReportingObserver.h", + "ReportingUtils.h", + "TestingDeprecatedInterface.h", +] + +UNIFIED_SOURCES += [ + "CrashReport.cpp", + "DeprecationReportBody.cpp", + "EndpointForReportChild.cpp", + "EndpointForReportParent.cpp", + "FeaturePolicyViolationReportBody.cpp", + "Report.cpp", + "ReportBody.cpp", + "ReportDeliver.cpp", + "ReportingHeader.cpp", + "ReportingObserver.cpp", + "ReportingUtils.cpp", + "TestingDeprecatedInterface.cpp", +] + +IPDL_SOURCES += [ + "PEndpointForReport.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Security") + +FINAL_LIBRARY = "xul" + +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"] + +TEST_DIRS += ["tests/gtest"] diff --git a/dom/reporting/tests/browser.toml b/dom/reporting/tests/browser.toml new file mode 100644 index 0000000000..1f6354edd4 --- /dev/null +++ b/dom/reporting/tests/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = [ + "delivering.sjs", + "empty.html", +] + +["browser_cleanup.js"] diff --git a/dom/reporting/tests/browser_cleanup.js b/dom/reporting/tests/browser_cleanup.js new file mode 100644 index 0000000000..e50b8db1da --- /dev/null +++ b/dom/reporting/tests/browser_cleanup.js @@ -0,0 +1,276 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +const TEST_HOST = "example.org"; +const TEST_DOMAIN = "https://" + TEST_HOST; +const TEST_PATH = "/browser/dom/reporting/tests/"; +const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "empty.html"; +const TEST_SJS = TEST_DOMAIN + TEST_PATH + "delivering.sjs"; + +async function storeReportingHeader(browser, extraParams = "") { + await SpecialPowers.spawn( + browser, + [{ url: TEST_SJS, extraParams }], + async obj => { + await content + .fetch( + obj.url + + "?task=header" + + (obj.extraParams.length ? "&" + obj.extraParams : "") + ) + .then(r => r.text()) + .then(text => { + is(text, "OK", "Report-to header sent"); + }); + } + ); +} + +add_task(async function () { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.reporting.enabled", true], + ["dom.reporting.header.enabled", true], + ["dom.reporting.testing.enabled", true], + ["dom.reporting.delivering.timeout", 1], + ["dom.reporting.cleanup.timeout", 1], + ["privacy.userContext.enabled", true], + ], + }); +}); + +add_task(async function () { + info("Testing a total cleanup"); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser); + ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before a full cleanup" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Testing a total QuotaManager cleanup"); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser); + ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_REPORTS, value => + resolve() + ); + }); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before a reports cleanup" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Testing a QuotaManager host cleanup"); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser); + ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); + + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + TEST_HOST, + true, + Ci.nsIClearDataService.CLEAR_REPORTS, + value => resolve() + ); + }); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before a reports cleanup" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Testing a 410 endpoint status"); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser, "410=true"); + ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data"); + + await SpecialPowers.spawn(browser, [], async _ => { + let testingInterface = new content.TestingDeprecatedInterface(); + ok(!!testingInterface, "Created a deprecated interface"); + }); + + await new Promise((resolve, reject) => { + let count = 0; + let id = setInterval(_ => { + ++count; + if (count > 10) { + ok(false, "Something went wrong."); + clearInterval(id); + reject(); + } + + if (!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN)) { + ok(true, "No data after a 410!"); + clearInterval(id); + resolve(); + } + }, 1000); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Creating a new container"); + + let identity = ContextualIdentityService.create( + "Report-To-Test", + "fingerprint", + "orange" + ); + + info("Creating a new container tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE, { + userContextId: identity.userContextId, + }); + is( + tab.getAttribute("usercontextid"), + "" + identity.userContextId, + "New tab has the right UCI" + ); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser); + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "We don't have data for the origin" + ); + ok( + ChromeUtils.hasReportingHeaderForOrigin( + TEST_DOMAIN + "^userContextId=" + identity.userContextId + ), + "We have data for the origin + userContextId" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + ContextualIdentityService.remove(identity.userContextId); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin( + TEST_DOMAIN + "^userContextId=" + identity.userContextId + ), + "No more data after a container removal" + ); +}); + +add_task(async function () { + info("TTL cleanup"); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + ok( + !ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "No data before the test" + ); + + await storeReportingHeader(browser); + ok( + ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), + "We have data for the origin" + ); + + // Let's wait a bit. + await new Promise(resolve => { + setTimeout(resolve, 5000); + }); + + ok(!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "No data anymore"); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/dom/reporting/tests/common_deprecated.js b/dom/reporting/tests/common_deprecated.js new file mode 100644 index 0000000000..8f8415fcf3 --- /dev/null +++ b/dom/reporting/tests/common_deprecated.js @@ -0,0 +1,214 @@ +let testingInterface; + +// eslint-disable-next-line no-unused-vars +function test_deprecatedInterface() { + info("Testing DeprecatedTestingInterface report"); + return new Promise(resolve => { + let obs = new ReportingObserver((reports, o) => { + is(obs, o, "Same observer!"); + ok(reports.length == 1, "We have 1 report"); + + let report = reports[0]; + is(report.type, "deprecation", "Deprecation report received"); + is(report.url, location.href, "URL is location"); + ok(!!report.body, "The report has a body"); + ok( + report.body instanceof DeprecationReportBody, + "Correct type for the body" + ); + is( + report.body.id, + "DeprecatedTestingInterface", + "report.body.id matches DeprecatedTestingMethod" + ); + ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval"); + ok( + report.body.message.includes("TestingDeprecatedInterface"), + "We have a message" + ); + is( + report.body.sourceFile, + location.href + .split("?")[0] + .replace("test_deprecated.html", "common_deprecated.js") + .replace("worker_deprecated.js", "common_deprecated.js"), + "We have a sourceFile" + ); + is(report.body.lineNumber, 48, "We have a lineNumber"); + is(report.body.columnNumber, 24, "We have a columnNumber"); + + obs.disconnect(); + resolve(); + }); + ok(!!obs, "ReportingObserver is a thing"); + + obs.observe(); + ok(true, "ReportingObserver.observe() is callable"); + + testingInterface = new TestingDeprecatedInterface(); + ok(true, "Created a deprecated interface"); + }); +} + +// eslint-disable-next-line no-unused-vars +function test_deprecatedMethod() { + info("Testing DeprecatedTestingMethod report"); + return new Promise(resolve => { + let obs = new ReportingObserver((reports, o) => { + is(obs, o, "Same observer!"); + ok(reports.length == 1, "We have 1 report"); + + let report = reports[0]; + is(report.type, "deprecation", "Deprecation report received"); + is(report.url, location.href, "URL is location"); + ok(!!report.body, "The report has a body"); + ok( + report.body instanceof DeprecationReportBody, + "Correct type for the body" + ); + is( + report.body.id, + "DeprecatedTestingMethod", + "report.body.id matches DeprecatedTestingMethod" + ); + ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval"); + ok( + report.body.message.includes( + "TestingDeprecatedInterface.deprecatedMethod" + ), + "We have a message" + ); + is( + report.body.sourceFile, + location.href + .split("?")[0] + .replace("test_deprecated.html", "common_deprecated.js") + .replace("worker_deprecated.js", "common_deprecated.js"), + "We have a sourceFile" + ); + is(report.body.lineNumber, 100, "We have a lineNumber"); + is(report.body.columnNumber, 22, "We have a columnNumber"); + + obs.disconnect(); + resolve(); + }); + ok(!!obs, "ReportingObserver is a thing"); + + obs.observe(); + ok(true, "ReportingObserver.observe() is callable"); + + testingInterface.deprecatedMethod(); + ok(true, "Run a deprecated method."); + }); +} + +// eslint-disable-next-line no-unused-vars +function test_deprecatedMethodWithDataURI() { + info("Testing deprecatedMethodWithDataURI report"); + + const uri = `data:text/html,<script> + window.onload = () => { + let obs = new ReportingObserver((reports, o) => { + obs.disconnect(); + let report = reports[0]; + const message = (report.url == "data:...") ? "passed" : "failed"; + window.opener.postMessage(message, "http://mochi.test:8888"); + close(); + }); + + obs.observe(); + let testingInterface = new TestingDeprecatedInterface(); + testingInterface.deprecatedMethod(); + }; + </script>`; + + return new Promise((resolve, reject) => { + window.open(uri); + window.addEventListener("message", e => { + is(e.data, "passed", "The data URI is truncated"); + resolve(); + }); + }); +} + +// eslint-disable-next-line no-unused-vars +function test_deprecatedAttribute() { + info("Testing DeprecatedTestingAttribute report"); + return new Promise(resolve => { + let obs = new ReportingObserver((reports, o) => { + is(obs, o, "Same observer!"); + ok(reports.length == 1, "We have 1 report"); + + let report = reports[0]; + is(report.type, "deprecation", "Deprecation report received"); + is(report.url, location.href, "URL is location"); + ok(!!report.body, "The report has a body"); + ok( + report.body instanceof DeprecationReportBody, + "Correct type for the body" + ); + is( + report.body.id, + "DeprecatedTestingAttribute", + "report.body.id matches DeprecatedTestingAttribute" + ); + ok(!report.body.anticipatedRemoval, "We don't have a anticipatedRemoval"); + ok( + report.body.message.includes( + "TestingDeprecatedInterface.deprecatedAttribute" + ), + "We have a message" + ); + is( + report.body.sourceFile, + location.href + .split("?")[0] + .replace("test_deprecated.html", "common_deprecated.js") + .replace("worker_deprecated.js", "common_deprecated.js"), + "We have a sourceFile" + ); + is(report.body.lineNumber, 181, "We have a lineNumber"); + is(report.body.columnNumber, 8, "We have a columnNumber"); + + obs.disconnect(); + resolve(); + }); + ok(!!obs, "ReportingObserver is a thing"); + + obs.observe(); + ok(true, "ReportingObserver.observe() is callable"); + + ok(testingInterface.deprecatedAttribute, "Attributed called"); + }); +} + +// eslint-disable-next-line no-unused-vars +function test_takeRecords() { + info("Testing ReportingObserver.takeRecords()"); + let p = new Promise(resolve => { + let obs = new ReportingObserver((reports, o) => { + is(obs, o, "Same observer!"); + resolve(obs); + }); + ok(!!obs, "ReportingObserver is a thing"); + + obs.observe(); + ok(true, "ReportingObserver.observe() is callable"); + + testingInterface.deprecatedMethod(); + ok(true, "Run a deprecated method."); + }); + + return p.then(obs => { + let reports = obs.takeRecords(); + is(reports.length, 0, "No reports after an callback"); + + testingInterface.deprecatedAttribute + 1; + + reports = obs.takeRecords(); + ok(reports.length >= 1, "We have at least 1 report"); + + reports = obs.takeRecords(); + is(reports.length, 0, "No more reports"); + }); +} diff --git a/dom/reporting/tests/delivering.sjs b/dom/reporting/tests/delivering.sjs new file mode 100644 index 0000000000..5bc3e17f3e --- /dev/null +++ b/dom/reporting/tests/delivering.sjs @@ -0,0 +1,111 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(aRequest, aResponse) { + var params = new URLSearchParams(aRequest.queryString); + + // Report-to setter + if (aRequest.method == "GET" && params.get("task") == "header") { + let extraParams = []; + + if (params.has("410")) { + extraParams.push("410=true"); + } + + if (params.has("worker")) { + extraParams.push("worker=true"); + } + + let body = { + max_age: 1, + endpoints: [ + { + url: + "https://example.org/tests/dom/reporting/tests/delivering.sjs" + + (extraParams.length ? "?" + extraParams.join("&") : ""), + priority: 1, + weight: 1, + }, + ], + }; + + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Report-to", JSON.stringify(body), false); + aResponse.write("OK"); + return; + } + + // Report check + if (aRequest.method == "GET" && params.get("task") == "check") { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + + let reports = getState("report"); + if (!reports) { + aResponse.write(""); + return; + } + + if (params.has("min")) { + let json = JSON.parse(reports); + if (json.length < params.get("min")) { + aResponse.write(""); + return; + } + } + + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.write(getState("report")); + + setState("report", ""); + return; + } + + if (aRequest.method == "POST") { + var body = new BinaryInputStream(aRequest.bodyInputStream); + + var avail; + var bytes = []; + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + let reports = getState("report"); + if (!reports) { + reports = []; + } else { + reports = JSON.parse(reports); + } + + const incoming_reports = JSON.parse(String.fromCharCode.apply(null, bytes)); + for (let report of incoming_reports) { + let data = { + contentType: aRequest.getHeader("content-type"), + origin: aRequest.getHeader("origin"), + body: report, + url: + aRequest.scheme + + "://" + + aRequest.host + + aRequest.path + + (aRequest.queryString ? "&" + aRequest.queryString : ""), + }; + reports.push(data); + } + + setState("report", JSON.stringify(reports)); + + if (params.has("410")) { + aResponse.setStatusLine(aRequest.httpVersion, 410, "Gone"); + } else { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + } + return; + } + + aResponse.setStatusLine(aRequest.httpVersion, 500, "Internal error"); + aResponse.write("Invalid request"); +} diff --git a/dom/reporting/tests/empty.html b/dom/reporting/tests/empty.html new file mode 100644 index 0000000000..cd0875583a --- /dev/null +++ b/dom/reporting/tests/empty.html @@ -0,0 +1 @@ +Hello world! diff --git a/dom/reporting/tests/gtest/TestReportToParser.cpp b/dom/reporting/tests/gtest/TestReportToParser.cpp new file mode 100644 index 0000000000..a3549886a3 --- /dev/null +++ b/dom/reporting/tests/gtest/TestReportToParser.cpp @@ -0,0 +1,418 @@ +/* -*- 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 "gtest/gtest.h" +#include "mozilla/dom/ReportingHeader.h" +#include "nsNetUtil.h" +#include "nsIURI.h" + +using namespace mozilla; +using namespace mozilla::dom; + +TEST(ReportToParser, Basic) +{ + nsCOMPtr<nsIURI> uri; + + nsresult rv = NS_NewURI(getter_AddRefs(uri), "https://example.com"); + ASSERT_EQ(NS_OK, rv); + + bool urlEqual = false; + + // Empty header. + UniquePtr<ReportingHeader::Client> client = + ReportingHeader::ParseHeader(nullptr, uri, ""_ns); + ASSERT_TRUE(!client); + + // Empty header. + client = ReportingHeader::ParseHeader(nullptr, uri, " "_ns); + ASSERT_TRUE(!client); + + // No minimal attributes + client = ReportingHeader::ParseHeader(nullptr, uri, "{}"_ns); + ASSERT_TRUE(!client); + + // Single client + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 42, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default")); + ASSERT_FALSE(client->mGroups.ElementAt(0).mIncludeSubdomains); + ASSERT_EQ(42, client->mGroups.ElementAt(0).mTTL); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 clients, same group name. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 43, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 44, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default")); + ASSERT_EQ(43, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first one with an invalid group name. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 43, \"group\": 123, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 44, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default")); + ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first one with an invalid group name. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 43, \"group\": null, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 44, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default")); + ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first one with an invalid group name. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 43, \"group\": {}, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 44, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("default")); + ASSERT_EQ(44, client->mGroups.ElementAt(0).mTTL); + + // Single client: optional params + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 45, \"group\": \"foobar\", \"include_subdomains\": " + "true, \"endpoints\": [{\"url\": \"https://example.com\", " + "\"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_TRUE(client->mGroups.ElementAt(0).mName.EqualsLiteral("foobar")); + ASSERT_TRUE(client->mGroups.ElementAt(0).mIncludeSubdomains); + ASSERT_EQ(45, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: missing max_age. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"endpoints\": [{\"url\": \"https://example.com\", \"priority\": " + "1, \"weight\": 2}]}," + "{\"max_age\": 46, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid max_age. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": null, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 46, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid max_age. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": \"foobar\", \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 46, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid max_age. + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": {}, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}," + "{\"max_age\": 46, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(46, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: missing endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 47}," + "{\"max_age\": 48, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 47, \"endpoints\": null }," + "{\"max_age\": 48, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 47, \"endpoints\": \"abc\" }," + "{\"max_age\": 48, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 47, \"endpoints\": 42 }," + "{\"max_age\": 48, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: invalid endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 47, \"endpoints\": {} }," + "{\"max_age\": 48, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(48, client->mGroups.ElementAt(0).mTTL); + + // 2 clients, the first incomplete: empty endpoints + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 49, \"endpoints\": []}," + "{\"max_age\": 50, \"endpoints\": [{\"url\": " + "\"https://example.com\", \"priority\": 1, \"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ(50, client->mGroups.ElementAt(0).mTTL); + + // 2 endpoints, the first incomplete: missing url + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 51, \"endpoints\": [" + " {\"priority\": 1, \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 1, " + "\"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid url + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 51, \"endpoints\": [" + " {\"url\": 42, \"priority\": 1, \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 1, " + "\"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid url + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 51, \"endpoints\": [" + " {\"url\": \"something here\", \"priority\": 1, \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 1, \"weight\": " + "2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid url + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 51, \"endpoints\": [" + " {\"url\": {}, \"priority\": 1, \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 1, " + "\"weight\": 2}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: missing priority + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 52, \"endpoints\": [" + " {\"url\": \"https://example.com\", \"weight\": 3}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)3, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid priority + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 52, \"endpoints\": [" + " {\"url\": \"https://example.com\", \"priority\": " + "{}, \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 2, " + "\"weight\": 3}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)3, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid priority + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 52, \"endpoints\": [" + " {\"url\": \"https://example.com\", \"priority\": " + "\"ok\", \"weight\": 2}," + " {\"url\": \"https://example.com\", \"priority\": 2, " + "\"weight\": 3}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)2, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)3, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: missing weight + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString( + "{\"max_age\": 52, \"endpoints\": [" + " {\"url\": \"https://example.com\", \"priority\": 5}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)5, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)1, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); + + // 2 endpoints, the first incomplete: invalid weight + client = ReportingHeader::ParseHeader( + nullptr, uri, + nsLiteralCString("{\"max_age\": 52, \"endpoints\": [" + " {\"url\": \"https://example.com\", \"priority\": 4, " + "\"weight\": []}," + " {\"url\": \"https://example.com\", \"priority\": 5, " + "\"weight\": 6}]}")); + ASSERT_TRUE(!!client); + ASSERT_EQ((uint32_t)1, client->mGroups.Length()); + ASSERT_EQ((uint32_t)1, client->mGroups.ElementAt(0).mEndpoints.Length()); + ASSERT_TRUE( + NS_SUCCEEDED( + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mUrl->Equals( + uri, &urlEqual)) && + urlEqual); + ASSERT_EQ((uint32_t)5, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mPriority); + ASSERT_EQ((uint32_t)6, + client->mGroups.ElementAt(0).mEndpoints.ElementAt(0).mWeight); +} diff --git a/dom/reporting/tests/gtest/moz.build b/dom/reporting/tests/gtest/moz.build new file mode 100644 index 0000000000..860ef48d1e --- /dev/null +++ b/dom/reporting/tests/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +UNIFIED_SOURCES = [ + "TestReportToParser.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/reporting/tests/iframe_delivering.html b/dom/reporting/tests/iframe_delivering.html new file mode 100644 index 0000000000..e8c5e9e3a4 --- /dev/null +++ b/dom/reporting/tests/iframe_delivering.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for delivering reports</title> +</head> +<body> + +<script type="application/javascript"> + +function ok(a, msg) { + parent.postMessage({type: "test", check: !!a, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + parent.postMessage({type: "finish" }, "*"); +} + +function checkReport() { + return new Promise(resolve => { + let id = setInterval(_ => { + fetch("delivering.sjs?task=check&min=3") + .then(r => r.text()) + .then(text => { + if (text) { + resolve(JSON.parse(text)); + clearInterval(id); + } + }); + }, 1000); + }); +} + +function runTests(extraParams = "") { + // Call a deprecating operation. + for (let i = 0; i < 100; ++i) { + new TestingDeprecatedInterface(); + } + ok(true, "Created a deprecated interface"); + + // Check if the report has been received. + return checkReport() + .then(reports => { + is(reports.length, 3, "We have 1 report"); + + let report = reports[0]; + is(report.contentType, "application/reports+json", "Correct mime-type"); + is(report.origin, "https://example.org", "Origin correctly set"); + is(report.url, "https://example.org/tests/dom/reporting/tests/delivering.sjs" + extraParams, "URL is correctly set"); + ok(!!report.body, "We have a report.body"); + ok(report.body.age > 0, "Age is correctly set"); + is(report.body.url, window.location.href, "URL is correctly set"); + is(report.body.user_agent, navigator.userAgent, "User-agent matches"); + ok(report.body.type, "deprecation", "Type is fine."); + ok(!!report.body.body, "We have the real report.body"); + is(report.body.body.id, "DeprecatedTestingInterface", "Correct report.body.id"); + is(report.body.body.message, "TestingDeprecatedInterface is a testing-only interface and this is its testing deprecation message.", "We have a report.body.message"); + is(report.body.body.sourceFile, "https://example.org/tests/dom/reporting/tests/iframe_delivering.html", "report.body.sourceFile"); + is(report.body.body.lineNumber, 40, "report.body.lineNumber"); + is(report.body.body.columnNumber, 5, "report.body.columnNumber"); + }); +} + +// Let's register a group + endpoint +fetch("delivering.sjs?task=header") +.then(r => r.text()) +.then(text => { + is(text, "OK", "Report-to header sent"); +}) +.then(_ => { + return runTests(); +}) + +// Let's register a group + endpoint from a worker +.then(_ => { + return new Promise(resolve => { + let w = new Worker("worker_delivering.js"); + w.onmessage = e => resolve(); + }); +}) +.then(_ => { + return runTests("&worker=true"); +}) + +.then(finish); + +</script> +</body> +</html> diff --git a/dom/reporting/tests/mochitest.toml b/dom/reporting/tests/mochitest.toml new file mode 100644 index 0000000000..4faddd217c --- /dev/null +++ b/dom/reporting/tests/mochitest.toml @@ -0,0 +1,29 @@ +[DEFAULT] +prefs = [ + "dom.reporting.enabled=true", + "dom.reporting.header.enabled=true", + "dom.reporting.testing.enabled=true", +] + +["test_delivering.html"] +support-files = [ + "delivering.sjs", + "iframe_delivering.html", + "worker_delivering.js", +] +skip-if = [ + "http3", + "http2", +] + +["test_deprecated.html"] +support-files = [ + "common_deprecated.js", + "worker_deprecated.js", +] +skip-if = [ + "http3", + "http2", +] + +["test_memoryPressure.html"] diff --git a/dom/reporting/tests/test_delivering.html b/dom/reporting/tests/test_delivering.html new file mode 100644 index 0000000000..2c552273e0 --- /dev/null +++ b/dom/reporting/tests/test_delivering.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for delivering reports</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="application/javascript"> + +// Setting prefs. +SpecialPowers.pushPrefEnv({ set: [ + ["dom_reporting_delivering_timeout", 1], + ["dom_reporting_delivering_maxFailures", 2], + ["dom.reporting.delivering.maxReports", 3], +]}) + +// Tests run in iframes because the origin must be secure for report-to header. +.then(_ => { + window.addEventListener("message", e => { + if (e.data.type == "finish") { + SimpleTest.finish(); + return; + } + + if (e.data.type == "test") { + ok(e.data.check, e.data.msg); + return; + } + + ok(false, "Invalid message"); + }); + + let ifr = document.createElement("iframe"); + ifr.src = "https://example.org/tests/dom/reporting/tests/iframe_delivering.html"; + + document.body.appendChild(ifr); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/reporting/tests/test_deprecated.html b/dom/reporting/tests/test_deprecated.html new file mode 100644 index 0000000000..da55978e9b --- /dev/null +++ b/dom/reporting/tests/test_deprecated.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Deprecated reports</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="common_deprecated.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="application/javascript"> + +test_deprecatedInterface() +.then(() => test_deprecatedMethod()) +.then(() => test_deprecatedMethodWithDataURI()) +.then(() => test_deprecatedAttribute()) +.then(() => test_takeRecords()) +.then(() => { + info("Workers!"); + + return new Promise(resolve => { + const w = new Worker("worker_deprecated.js"); + w.onmessage = e => { + switch (e.data.type) { + case "info": + info(e.data.msg); + break; + + case "check": + ok(e.data.check, e.data.msg); + break; + + case "finish": + resolve(); + break; + + default: + ok(false, "Invalid message"); + break; + } + } + }); +}) + +.then(() => { SimpleTest.finish(); }); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/reporting/tests/test_memoryPressure.html b/dom/reporting/tests/test_memoryPressure.html new file mode 100644 index 0000000000..1bb887b05e --- /dev/null +++ b/dom/reporting/tests/test_memoryPressure.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for ReportingObserver + memory-pressure</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="application/javascript"> + +info("Testing TakeRecords() without memory-pressure"); +let o = new ReportingObserver(() => {}); +o.observe(); + +new TestingDeprecatedInterface(); +let r = o.takeRecords(); +is(r.length, 1, "We have 1 report"); + +r = o.takeRecords(); +is(r.length, 0, "We have 0 reports after a takeRecords()"); + +info("Testing DeprecatedTestingMethod report"); + +new TestingDeprecatedInterface(); +SpecialPowers.Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); + +r = o.takeRecords(); +is(r.length, 0, "We have 0 reports after a memory-pressure"); + +</script> +</body> +</html> diff --git a/dom/reporting/tests/worker_delivering.js b/dom/reporting/tests/worker_delivering.js new file mode 100644 index 0000000000..539bcd231c --- /dev/null +++ b/dom/reporting/tests/worker_delivering.js @@ -0,0 +1,5 @@ +fetch("delivering.sjs?task=header&worker=true") + .then(r => r.text()) + .then(text => { + postMessage("All good!"); + }); diff --git a/dom/reporting/tests/worker_deprecated.js b/dom/reporting/tests/worker_deprecated.js new file mode 100644 index 0000000000..f6b57896f6 --- /dev/null +++ b/dom/reporting/tests/worker_deprecated.js @@ -0,0 +1,28 @@ +/* eslint-disable no-undef */ + +// eslint-disable-next-line no-unused-vars +function ok(a, msg) { + postMessage({ type: "check", check: !!a, msg }); +} + +// eslint-disable-next-line no-unused-vars +function is(a, b, msg) { + ok(a === b, msg); +} + +// eslint-disable-next-line no-unused-vars +function info(msg) { + postMessage({ type: "info", msg }); +} + +function finish() { + postMessage({ type: "finish" }); +} + +importScripts("common_deprecated.js"); + +test_deprecatedInterface() + .then(() => test_deprecatedMethod()) + .then(() => test_deprecatedAttribute()) + .then(() => test_takeRecords()) + .then(() => finish()); |