1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/ReportingHeader.h"
9 #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
11 #include "js/PropertyAndElement.h" // JS_GetElement
12 #include "mozilla/dom/ReportingBinding.h"
13 #include "mozilla/dom/ScriptSettings.h"
14 #include "mozilla/dom/SimpleGlobalObject.h"
15 #include "mozilla/ipc/BackgroundUtils.h"
16 #include "mozilla/OriginAttributes.h"
17 #include "mozilla/Services.h"
18 #include "mozilla/StaticPrefs_dom.h"
19 #include "mozilla/StaticPtr.h"
21 #include "nsContentUtils.h"
22 #include "nsIEffectiveTLDService.h"
23 #include "nsIHttpChannel.h"
24 #include "nsIHttpProtocolHandler.h"
25 #include "nsIObserverService.h"
26 #include "nsIPrincipal.h"
27 #include "nsIRandomGenerator.h"
28 #include "nsIScriptError.h"
29 #include "nsNetUtil.h"
30 #include "nsXULAppAPI.h"
32 #define REPORTING_PURGE_ALL "reporting:purge-all"
33 #define REPORTING_PURGE_HOST "reporting:purge-host"
35 namespace mozilla::dom
{
39 StaticRefPtr
<ReportingHeader
> gReporting
;
44 void ReportingHeader::Initialize() {
45 MOZ_ASSERT(!gReporting
);
46 MOZ_ASSERT(NS_IsMainThread());
48 if (!XRE_IsParentProcess()) {
52 RefPtr
<ReportingHeader
> service
= new ReportingHeader();
54 nsCOMPtr
<nsIObserverService
> obs
= services::GetObserverService();
55 if (NS_WARN_IF(!obs
)) {
59 obs
->AddObserver(service
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
, false);
60 obs
->AddObserver(service
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
, false);
61 obs
->AddObserver(service
, "clear-origin-attributes-data", false);
62 obs
->AddObserver(service
, REPORTING_PURGE_HOST
, false);
63 obs
->AddObserver(service
, REPORTING_PURGE_ALL
, false);
69 void ReportingHeader::Shutdown() {
70 MOZ_ASSERT(NS_IsMainThread());
76 RefPtr
<ReportingHeader
> service
= gReporting
;
79 if (service
->mCleanupTimer
) {
80 service
->mCleanupTimer
->Cancel();
81 service
->mCleanupTimer
= nullptr;
84 nsCOMPtr
<nsIObserverService
> obs
= services::GetObserverService();
85 if (NS_WARN_IF(!obs
)) {
89 obs
->RemoveObserver(service
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
);
90 obs
->RemoveObserver(service
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
);
91 obs
->RemoveObserver(service
, "clear-origin-attributes-data");
92 obs
->RemoveObserver(service
, REPORTING_PURGE_HOST
);
93 obs
->RemoveObserver(service
, REPORTING_PURGE_ALL
);
96 ReportingHeader::ReportingHeader() = default;
97 ReportingHeader::~ReportingHeader() = default;
100 ReportingHeader::Observe(nsISupports
* aSubject
, const char* aTopic
,
101 const char16_t
* aData
) {
102 if (!strcmp(aTopic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
)) {
108 if (!StaticPrefs::dom_reporting_header_enabled()) {
112 if (!strcmp(aTopic
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
)) {
113 nsCOMPtr
<nsIHttpChannel
> channel
= do_QueryInterface(aSubject
);
114 if (NS_WARN_IF(!channel
)) {
118 ReportingFromChannel(channel
);
122 if (!strcmp(aTopic
, REPORTING_PURGE_HOST
)) {
123 RemoveOriginsFromHost(nsDependentString(aData
));
127 if (!strcmp(aTopic
, "clear-origin-attributes-data")) {
128 OriginAttributesPattern pattern
;
129 if (!pattern
.Init(nsDependentString(aData
))) {
130 NS_ERROR("Cannot parse origin attributes pattern");
131 return NS_ERROR_FAILURE
;
134 RemoveOriginsFromOriginAttributesPattern(pattern
);
138 if (!strcmp(aTopic
, REPORTING_PURGE_ALL
)) {
143 return NS_ERROR_FAILURE
;
146 void ReportingHeader::ReportingFromChannel(nsIHttpChannel
* aChannel
) {
147 MOZ_ASSERT(aChannel
);
149 if (!StaticPrefs::dom_reporting_header_enabled()) {
153 // We want to use the final URI to check if Report-To should be allowed or
155 nsCOMPtr
<nsIURI
> uri
;
156 nsresult rv
= aChannel
->GetURI(getter_AddRefs(uri
));
157 if (NS_WARN_IF(NS_FAILED(rv
))) {
161 if (!IsSecureURI(uri
)) {
165 if (NS_UsePrivateBrowsing(aChannel
)) {
169 nsAutoCString headerValue
;
170 rv
= aChannel
->GetResponseHeader("Report-To"_ns
, headerValue
);
175 nsIScriptSecurityManager
* ssm
= nsContentUtils::GetSecurityManager();
176 if (NS_WARN_IF(!ssm
)) {
180 nsCOMPtr
<nsIPrincipal
> principal
;
181 rv
= ssm
->GetChannelURIPrincipal(aChannel
, getter_AddRefs(principal
));
182 if (NS_WARN_IF(NS_FAILED(rv
)) || !principal
) {
186 nsAutoCString origin
;
187 rv
= principal
->GetOrigin(origin
);
188 if (NS_WARN_IF(NS_FAILED(rv
))) {
192 UniquePtr
<Client
> client
= ParseHeader(aChannel
, uri
, headerValue
);
197 // Here we override the previous data.
198 mOrigins
.InsertOrUpdate(origin
, std::move(client
));
200 MaybeCreateCleanupTimer();
203 /* static */ UniquePtr
<ReportingHeader::Client
> ReportingHeader::ParseHeader(
204 nsIHttpChannel
* aChannel
, nsIURI
* aURI
, const nsACString
& aHeaderValue
) {
206 // aChannel can be null in gtest
210 JSObject
* cleanGlobal
=
211 SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail
);
212 if (NS_WARN_IF(!cleanGlobal
)) {
216 if (NS_WARN_IF(!jsapi
.Init(cleanGlobal
))) {
220 // WebIDL dictionary parses single items. Let's create a object to parse the
223 json
.AppendASCII("{ \"items\": [");
224 json
.Append(NS_ConvertUTF8toUTF16(aHeaderValue
));
225 json
.AppendASCII("]}");
227 JSContext
* cx
= jsapi
.cx();
228 JS::Rooted
<JS::Value
> jsonValue(cx
);
229 bool ok
= JS_ParseJSON(cx
, json
.BeginReading(), json
.Length(), &jsonValue
);
231 LogToConsoleInvalidJSON(aChannel
, aURI
);
235 dom::ReportingHeaderValue data
;
236 if (!data
.Init(cx
, jsonValue
)) {
237 LogToConsoleInvalidJSON(aChannel
, aURI
);
241 if (!data
.mItems
.WasPassed() || data
.mItems
.Value().IsEmpty()) {
245 UniquePtr
<Client
> client
= MakeUnique
<Client
>();
247 for (const dom::ReportingItem
& item
: data
.mItems
.Value()) {
248 nsAutoString groupName
;
250 if (item
.mGroup
.isUndefined()) {
251 groupName
.AssignLiteral("default");
252 } else if (!item
.mGroup
.isString()) {
253 LogToConsoleInvalidNameItem(aChannel
, aURI
);
256 JS::Rooted
<JSString
*> groupStr(cx
, item
.mGroup
.toString());
257 MOZ_ASSERT(groupStr
);
259 nsAutoJSString string
;
260 if (NS_WARN_IF(!string
.init(cx
, groupStr
))) {
267 if (!item
.mMax_age
.isNumber() || !item
.mEndpoints
.isObject()) {
268 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
272 JS::Rooted
<JSObject
*> endpoints(cx
, &item
.mEndpoints
.toObject());
273 MOZ_ASSERT(endpoints
);
275 bool isArray
= false;
276 if (!JS::IsArrayObject(cx
, endpoints
, &isArray
) || !isArray
) {
277 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
281 uint32_t endpointsLength
;
282 if (!JS::GetArrayLength(cx
, endpoints
, &endpointsLength
) ||
283 endpointsLength
== 0) {
284 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
288 const auto [begin
, end
] = client
->mGroups
.NonObservingRange();
289 if (std::any_of(begin
, end
, [&groupName
](const Group
& group
) {
290 return group
.mName
== groupName
;
292 LogToConsoleDuplicateGroup(aChannel
, aURI
, groupName
);
296 Group
* group
= client
->mGroups
.AppendElement();
297 group
->mName
= groupName
;
298 group
->mIncludeSubdomains
= item
.mInclude_subdomains
;
299 group
->mTTL
= item
.mMax_age
.toNumber();
300 group
->mCreationTime
= TimeStamp::Now();
302 for (uint32_t i
= 0; i
< endpointsLength
; ++i
) {
303 JS::Rooted
<JS::Value
> element(cx
);
304 if (!JS_GetElement(cx
, endpoints
, i
, &element
)) {
308 RootedDictionary
<ReportingEndpoint
> endpoint(cx
);
309 if (!endpoint
.Init(cx
, element
)) {
310 LogToConsoleIncompleteEndpoint(aChannel
, aURI
, groupName
);
314 if (!endpoint
.mUrl
.isString() ||
315 (!endpoint
.mPriority
.isUndefined() &&
316 (!endpoint
.mPriority
.isNumber() ||
317 endpoint
.mPriority
.toNumber() < 0)) ||
318 (!endpoint
.mWeight
.isUndefined() &&
319 (!endpoint
.mWeight
.isNumber() || endpoint
.mWeight
.toNumber() < 0))) {
320 LogToConsoleIncompleteEndpoint(aChannel
, aURI
, groupName
);
324 JS::Rooted
<JSString
*> endpointUrl(cx
, endpoint
.mUrl
.toString());
325 MOZ_ASSERT(endpointUrl
);
327 nsAutoJSString endpointString
;
328 if (NS_WARN_IF(!endpointString
.init(cx
, endpointUrl
))) {
332 nsCOMPtr
<nsIURI
> uri
;
333 nsresult rv
= NS_NewURI(getter_AddRefs(uri
), endpointString
);
335 LogToConsoleInvalidURLEndpoint(aChannel
, aURI
, groupName
,
340 Endpoint
* ep
= group
->mEndpoints
.AppendElement();
343 endpoint
.mPriority
.isUndefined() ? 1 : endpoint
.mPriority
.toNumber();
345 endpoint
.mWeight
.isUndefined() ? 1 : endpoint
.mWeight
.toNumber();
349 if (client
->mGroups
.IsEmpty()) {
356 bool ReportingHeader::IsSecureURI(nsIURI
* aURI
) const {
359 bool prioriAuthenticated
= false;
360 if (NS_WARN_IF(NS_FAILED(NS_URIChainHasFlags(
361 aURI
, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY
,
362 &prioriAuthenticated
)))) {
366 return prioriAuthenticated
;
370 void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel
* aChannel
,
372 nsTArray
<nsString
> params
;
373 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidJSON", params
);
377 void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel
* aChannel
,
379 const nsAString
& aName
) {
380 nsTArray
<nsString
> params
;
381 params
.AppendElement(aName
);
383 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderDuplicateGroup", params
);
387 void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel
* aChannel
,
389 nsTArray
<nsString
> params
;
390 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidNameItem",
395 void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel
* aChannel
,
397 const nsAString
& aName
) {
398 nsTArray
<nsString
> params
;
399 params
.AppendElement(aName
);
401 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidItem", params
);
405 void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel
* aChannel
,
407 const nsAString
& aName
) {
408 nsTArray
<nsString
> params
;
409 params
.AppendElement(aName
);
411 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidEndpoint",
416 void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel
* aChannel
,
418 const nsAString
& aName
,
419 const nsAString
& aURL
) {
420 nsTArray
<nsString
> params
;
421 params
.AppendElement(aURL
);
422 params
.AppendElement(aName
);
424 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidURLEndpoint",
429 void ReportingHeader::LogToConsoleInternal(nsIHttpChannel
* aChannel
,
430 nsIURI
* aURI
, const char* aMsg
,
431 const nsTArray
<nsString
>& aParams
) {
435 // We are in a gtest.
439 uint64_t windowID
= 0;
441 nsresult rv
= aChannel
->GetTopLevelContentWindowId(&windowID
);
442 if (NS_WARN_IF(NS_FAILED(rv
))) {
447 nsCOMPtr
<nsILoadGroup
> loadGroup
;
448 nsresult rv
= aChannel
->GetLoadGroup(getter_AddRefs(loadGroup
));
449 if (NS_WARN_IF(NS_FAILED(rv
))) {
454 windowID
= nsContentUtils::GetInnerWindowID(loadGroup
);
458 nsAutoString localizedMsg
;
459 rv
= nsContentUtils::FormatLocalizedString(
460 nsContentUtils::eSECURITY_PROPERTIES
, aMsg
, aParams
, localizedMsg
);
461 if (NS_WARN_IF(NS_FAILED(rv
))) {
465 rv
= nsContentUtils::ReportToConsoleByWindowID(
466 localizedMsg
, nsIScriptError::infoFlag
, "Reporting"_ns
, windowID
, aURI
);
467 Unused
<< NS_WARN_IF(NS_FAILED(rv
));
471 void ReportingHeader::GetEndpointForReport(
472 const nsAString
& aGroupName
,
473 const mozilla::ipc::PrincipalInfo
& aPrincipalInfo
,
474 nsACString
& aEndpointURI
) {
475 auto principalOrErr
= PrincipalInfoToPrincipal(aPrincipalInfo
);
476 if (NS_WARN_IF(principalOrErr
.isErr())) {
480 nsCOMPtr
<nsIPrincipal
> principal
= principalOrErr
.unwrap();
481 GetEndpointForReport(aGroupName
, principal
, aEndpointURI
);
485 void ReportingHeader::GetEndpointForReport(const nsAString
& aGroupName
,
486 nsIPrincipal
* aPrincipal
,
487 nsACString
& aEndpointURI
) {
488 MOZ_ASSERT(aEndpointURI
.IsEmpty());
494 nsAutoCString origin
;
495 nsresult rv
= aPrincipal
->GetOrigin(origin
);
496 if (NS_WARN_IF(NS_FAILED(rv
))) {
500 Client
* client
= gReporting
->mOrigins
.Get(origin
);
505 const auto [begin
, end
] = client
->mGroups
.NonObservingRange();
506 const auto foundIt
= std::find_if(
508 [&aGroupName
](const Group
& group
) { return group
.mName
== aGroupName
; });
509 if (foundIt
!= end
) {
510 GetEndpointForReportInternal(*foundIt
, aEndpointURI
);
513 // XXX More explicitly report an error if not found?
517 void ReportingHeader::GetEndpointForReportInternal(
518 const ReportingHeader::Group
& aGroup
, nsACString
& aEndpointURI
) {
519 TimeDuration diff
= TimeStamp::Now() - aGroup
.mCreationTime
;
520 if (diff
.ToSeconds() > aGroup
.mTTL
) {
525 if (aGroup
.mEndpoints
.IsEmpty()) {
529 int64_t minPriority
= -1;
530 uint32_t totalWeight
= 0;
532 for (const Endpoint
& endpoint
: aGroup
.mEndpoints
.NonObservingRange()) {
533 if (minPriority
== -1 || minPriority
> endpoint
.mPriority
) {
534 minPriority
= endpoint
.mPriority
;
535 totalWeight
= endpoint
.mWeight
;
536 } else if (minPriority
== endpoint
.mPriority
) {
537 totalWeight
+= endpoint
.mWeight
;
541 nsCOMPtr
<nsIRandomGenerator
> randomGenerator
=
542 do_GetService("@mozilla.org/security/random-generator;1");
543 if (NS_WARN_IF(!randomGenerator
)) {
547 uint32_t randomNumber
= 0;
549 nsresult rv
= randomGenerator
->GenerateRandomBytesInto(randomNumber
);
550 if (NS_WARN_IF(NS_FAILED(rv
))) {
554 totalWeight
= randomNumber
% totalWeight
;
556 const auto [begin
, end
] = aGroup
.mEndpoints
.NonObservingRange();
557 const auto foundIt
= std::find_if(
558 begin
, end
, [minPriority
, totalWeight
](const Endpoint
& endpoint
) {
559 return minPriority
== endpoint
.mPriority
&&
560 totalWeight
< endpoint
.mWeight
;
562 if (foundIt
!= end
) {
563 Unused
<< NS_WARN_IF(NS_FAILED(foundIt
->mUrl
->GetSpec(aEndpointURI
)));
565 // XXX More explicitly report an error if not found?
569 void ReportingHeader::RemoveEndpoint(
570 const nsAString
& aGroupName
, const nsACString
& aEndpointURL
,
571 const mozilla::ipc::PrincipalInfo
& aPrincipalInfo
) {
576 nsCOMPtr
<nsIURI
> uri
;
577 nsresult rv
= NS_NewURI(getter_AddRefs(uri
), aEndpointURL
);
578 if (NS_WARN_IF(NS_FAILED(rv
))) {
582 auto principalOrErr
= PrincipalInfoToPrincipal(aPrincipalInfo
);
583 if (NS_WARN_IF(principalOrErr
.isErr())) {
587 nsAutoCString origin
;
588 rv
= principalOrErr
.unwrap()->GetOrigin(origin
);
589 if (NS_WARN_IF(NS_FAILED(rv
))) {
593 Client
* client
= gReporting
->mOrigins
.Get(origin
);
598 // Scope for the group iterator.
600 nsTObserverArray
<Group
>::BackwardIterator
iter(client
->mGroups
);
601 while (iter
.HasMore()) {
602 const Group
& group
= iter
.GetNext();
603 if (group
.mName
!= aGroupName
) {
607 // Scope for the endpoint iterator.
609 nsTObserverArray
<Endpoint
>::BackwardIterator
endpointIter(
611 while (endpointIter
.HasMore()) {
612 const Endpoint
& endpoint
= endpointIter
.GetNext();
615 rv
= endpoint
.mUrl
->Equals(uri
, &equal
);
616 if (NS_WARN_IF(NS_FAILED(rv
))) {
621 endpointIter
.Remove();
627 if (group
.mEndpoints
.IsEmpty()) {
635 if (client
->mGroups
.IsEmpty()) {
636 gReporting
->mOrigins
.Remove(origin
);
637 gReporting
->MaybeCancelCleanupTimer();
641 void ReportingHeader::RemoveOriginsFromHost(const nsAString
& aHost
) {
642 nsCOMPtr
<nsIEffectiveTLDService
> tldService
=
643 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID
);
644 if (NS_WARN_IF(!tldService
)) {
648 NS_ConvertUTF16toUTF8
host(aHost
);
650 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
651 bool hasRootDomain
= false;
652 nsresult rv
= tldService
->HasRootDomain(iter
.Key(), host
, &hasRootDomain
);
653 if (NS_WARN_IF(NS_FAILED(rv
)) || !hasRootDomain
) {
660 MaybeCancelCleanupTimer();
663 void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
664 const OriginAttributesPattern
& aPattern
) {
665 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
666 nsAutoCString suffix
;
667 OriginAttributes attr
;
668 if (NS_WARN_IF(!attr
.PopulateFromOrigin(iter
.Key(), suffix
))) {
672 if (aPattern
.Matches(attr
)) {
677 MaybeCancelCleanupTimer();
680 void ReportingHeader::RemoveOrigins() {
682 MaybeCancelCleanupTimer();
685 void ReportingHeader::RemoveOriginsForTTL() {
686 TimeStamp now
= TimeStamp::Now();
688 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
689 Client
* client
= iter
.UserData();
691 // Scope of the iterator.
693 nsTObserverArray
<Group
>::BackwardIterator
groupIter(client
->mGroups
);
694 while (groupIter
.HasMore()) {
695 const Group
& group
= groupIter
.GetNext();
696 TimeDuration diff
= now
- group
.mCreationTime
;
697 if (diff
.ToSeconds() > group
.mTTL
) {
704 if (client
->mGroups
.IsEmpty()) {
711 bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString
& aOrigin
) {
716 return gReporting
->mOrigins
.Contains(aOrigin
);
720 ReportingHeader::Notify(nsITimer
* aTimer
) {
721 mCleanupTimer
= nullptr;
723 RemoveOriginsForTTL();
724 MaybeCreateCleanupTimer();
730 ReportingHeader::GetName(nsACString
& aName
) {
731 aName
.AssignLiteral("ReportingHeader");
735 void ReportingHeader::MaybeCreateCleanupTimer() {
740 if (mOrigins
.Count() == 0) {
744 uint32_t timeout
= StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
746 NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer
), this, timeout
,
747 nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY
);
748 Unused
<< NS_WARN_IF(NS_FAILED(rv
));
751 void ReportingHeader::MaybeCancelCleanupTimer() {
752 if (!mCleanupTimer
) {
756 if (mOrigins
.Count() != 0) {
760 mCleanupTimer
->Cancel();
761 mCleanupTimer
= nullptr;
764 NS_INTERFACE_MAP_BEGIN(ReportingHeader
)
765 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIObserver
)
766 NS_INTERFACE_MAP_ENTRY(nsIObserver
)
767 NS_INTERFACE_MAP_ENTRY(nsITimerCallback
)
768 NS_INTERFACE_MAP_ENTRY(nsINamed
)
771 NS_IMPL_ADDREF(ReportingHeader
)
772 NS_IMPL_RELEASE(ReportingHeader
)
774 } // namespace mozilla::dom