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"
40 StaticRefPtr
<ReportingHeader
> gReporting
;
45 void ReportingHeader::Initialize() {
46 MOZ_ASSERT(!gReporting
);
47 MOZ_ASSERT(NS_IsMainThread());
49 if (!XRE_IsParentProcess()) {
53 RefPtr
<ReportingHeader
> service
= new ReportingHeader();
55 nsCOMPtr
<nsIObserverService
> obs
= services::GetObserverService();
56 if (NS_WARN_IF(!obs
)) {
60 obs
->AddObserver(service
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
, false);
61 obs
->AddObserver(service
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
, false);
62 obs
->AddObserver(service
, "clear-origin-attributes-data", false);
63 obs
->AddObserver(service
, REPORTING_PURGE_HOST
, false);
64 obs
->AddObserver(service
, REPORTING_PURGE_ALL
, false);
70 void ReportingHeader::Shutdown() {
71 MOZ_ASSERT(NS_IsMainThread());
77 RefPtr
<ReportingHeader
> service
= gReporting
;
80 if (service
->mCleanupTimer
) {
81 service
->mCleanupTimer
->Cancel();
82 service
->mCleanupTimer
= nullptr;
85 nsCOMPtr
<nsIObserverService
> obs
= services::GetObserverService();
86 if (NS_WARN_IF(!obs
)) {
90 obs
->RemoveObserver(service
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
);
91 obs
->RemoveObserver(service
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
);
92 obs
->RemoveObserver(service
, "clear-origin-attributes-data");
93 obs
->RemoveObserver(service
, REPORTING_PURGE_HOST
);
94 obs
->RemoveObserver(service
, REPORTING_PURGE_ALL
);
97 ReportingHeader::ReportingHeader() = default;
98 ReportingHeader::~ReportingHeader() = default;
101 ReportingHeader::Observe(nsISupports
* aSubject
, const char* aTopic
,
102 const char16_t
* aData
) {
103 if (!strcmp(aTopic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
)) {
109 if (!StaticPrefs::dom_reporting_header_enabled()) {
113 if (!strcmp(aTopic
, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC
)) {
114 nsCOMPtr
<nsIHttpChannel
> channel
= do_QueryInterface(aSubject
);
115 if (NS_WARN_IF(!channel
)) {
119 ReportingFromChannel(channel
);
123 if (!strcmp(aTopic
, REPORTING_PURGE_HOST
)) {
124 RemoveOriginsFromHost(nsDependentString(aData
));
128 if (!strcmp(aTopic
, "clear-origin-attributes-data")) {
129 OriginAttributesPattern pattern
;
130 if (!pattern
.Init(nsDependentString(aData
))) {
131 NS_ERROR("Cannot parse origin attributes pattern");
132 return NS_ERROR_FAILURE
;
135 RemoveOriginsFromOriginAttributesPattern(pattern
);
139 if (!strcmp(aTopic
, REPORTING_PURGE_ALL
)) {
144 return NS_ERROR_FAILURE
;
147 void ReportingHeader::ReportingFromChannel(nsIHttpChannel
* aChannel
) {
148 MOZ_ASSERT(aChannel
);
150 if (!StaticPrefs::dom_reporting_header_enabled()) {
154 // We want to use the final URI to check if Report-To should be allowed or
156 nsCOMPtr
<nsIURI
> uri
;
157 nsresult rv
= aChannel
->GetURI(getter_AddRefs(uri
));
158 if (NS_WARN_IF(NS_FAILED(rv
))) {
162 if (!IsSecureURI(uri
)) {
166 if (NS_UsePrivateBrowsing(aChannel
)) {
170 nsAutoCString headerValue
;
171 rv
= aChannel
->GetResponseHeader("Report-To"_ns
, headerValue
);
176 nsIScriptSecurityManager
* ssm
= nsContentUtils::GetSecurityManager();
177 if (NS_WARN_IF(!ssm
)) {
181 nsCOMPtr
<nsIPrincipal
> principal
;
182 rv
= ssm
->GetChannelURIPrincipal(aChannel
, getter_AddRefs(principal
));
183 if (NS_WARN_IF(NS_FAILED(rv
)) || !principal
) {
187 nsAutoCString origin
;
188 rv
= principal
->GetOrigin(origin
);
189 if (NS_WARN_IF(NS_FAILED(rv
))) {
193 UniquePtr
<Client
> client
= ParseHeader(aChannel
, uri
, headerValue
);
198 // Here we override the previous data.
199 mOrigins
.InsertOrUpdate(origin
, std::move(client
));
201 MaybeCreateCleanupTimer();
204 /* static */ UniquePtr
<ReportingHeader::Client
> ReportingHeader::ParseHeader(
205 nsIHttpChannel
* aChannel
, nsIURI
* aURI
, const nsACString
& aHeaderValue
) {
207 // aChannel can be null in gtest
211 JSObject
* cleanGlobal
=
212 SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail
);
213 if (NS_WARN_IF(!cleanGlobal
)) {
217 if (NS_WARN_IF(!jsapi
.Init(cleanGlobal
))) {
221 // WebIDL dictionary parses single items. Let's create a object to parse the
224 json
.AppendASCII("{ \"items\": [");
225 json
.Append(NS_ConvertUTF8toUTF16(aHeaderValue
));
226 json
.AppendASCII("]}");
228 JSContext
* cx
= jsapi
.cx();
229 JS::Rooted
<JS::Value
> jsonValue(cx
);
230 bool ok
= JS_ParseJSON(cx
, json
.BeginReading(), json
.Length(), &jsonValue
);
232 LogToConsoleInvalidJSON(aChannel
, aURI
);
236 dom::ReportingHeaderValue data
;
237 if (!data
.Init(cx
, jsonValue
)) {
238 LogToConsoleInvalidJSON(aChannel
, aURI
);
242 if (!data
.mItems
.WasPassed() || data
.mItems
.Value().IsEmpty()) {
246 UniquePtr
<Client
> client
= MakeUnique
<Client
>();
248 for (const dom::ReportingItem
& item
: data
.mItems
.Value()) {
249 nsAutoString groupName
;
251 if (item
.mGroup
.isUndefined()) {
252 groupName
.AssignLiteral("default");
253 } else if (!item
.mGroup
.isString()) {
254 LogToConsoleInvalidNameItem(aChannel
, aURI
);
257 JS::Rooted
<JSString
*> groupStr(cx
, item
.mGroup
.toString());
258 MOZ_ASSERT(groupStr
);
260 nsAutoJSString string
;
261 if (NS_WARN_IF(!string
.init(cx
, groupStr
))) {
268 if (!item
.mMax_age
.isNumber() || !item
.mEndpoints
.isObject()) {
269 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
273 JS::Rooted
<JSObject
*> endpoints(cx
, &item
.mEndpoints
.toObject());
274 MOZ_ASSERT(endpoints
);
276 bool isArray
= false;
277 if (!JS::IsArrayObject(cx
, endpoints
, &isArray
) || !isArray
) {
278 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
282 uint32_t endpointsLength
;
283 if (!JS::GetArrayLength(cx
, endpoints
, &endpointsLength
) ||
284 endpointsLength
== 0) {
285 LogToConsoleIncompleteItem(aChannel
, aURI
, groupName
);
289 const auto [begin
, end
] = client
->mGroups
.NonObservingRange();
290 if (std::any_of(begin
, end
, [&groupName
](const Group
& group
) {
291 return group
.mName
== groupName
;
293 LogToConsoleDuplicateGroup(aChannel
, aURI
, groupName
);
297 Group
* group
= client
->mGroups
.AppendElement();
298 group
->mName
= groupName
;
299 group
->mIncludeSubdomains
= item
.mInclude_subdomains
;
300 group
->mTTL
= item
.mMax_age
.toNumber();
301 group
->mCreationTime
= TimeStamp::Now();
303 for (uint32_t i
= 0; i
< endpointsLength
; ++i
) {
304 JS::Rooted
<JS::Value
> element(cx
);
305 if (!JS_GetElement(cx
, endpoints
, i
, &element
)) {
309 RootedDictionary
<ReportingEndpoint
> endpoint(cx
);
310 if (!endpoint
.Init(cx
, element
)) {
311 LogToConsoleIncompleteEndpoint(aChannel
, aURI
, groupName
);
315 if (!endpoint
.mUrl
.isString() ||
316 (!endpoint
.mPriority
.isUndefined() &&
317 (!endpoint
.mPriority
.isNumber() ||
318 endpoint
.mPriority
.toNumber() < 0)) ||
319 (!endpoint
.mWeight
.isUndefined() &&
320 (!endpoint
.mWeight
.isNumber() || endpoint
.mWeight
.toNumber() < 0))) {
321 LogToConsoleIncompleteEndpoint(aChannel
, aURI
, groupName
);
325 JS::Rooted
<JSString
*> endpointUrl(cx
, endpoint
.mUrl
.toString());
326 MOZ_ASSERT(endpointUrl
);
328 nsAutoJSString endpointString
;
329 if (NS_WARN_IF(!endpointString
.init(cx
, endpointUrl
))) {
333 nsCOMPtr
<nsIURI
> uri
;
334 nsresult rv
= NS_NewURI(getter_AddRefs(uri
), endpointString
);
336 LogToConsoleInvalidURLEndpoint(aChannel
, aURI
, groupName
,
341 Endpoint
* ep
= group
->mEndpoints
.AppendElement();
344 endpoint
.mPriority
.isUndefined() ? 1 : endpoint
.mPriority
.toNumber();
346 endpoint
.mWeight
.isUndefined() ? 1 : endpoint
.mWeight
.toNumber();
350 if (client
->mGroups
.IsEmpty()) {
357 bool ReportingHeader::IsSecureURI(nsIURI
* aURI
) const {
360 bool prioriAuthenticated
= false;
361 if (NS_WARN_IF(NS_FAILED(NS_URIChainHasFlags(
362 aURI
, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY
,
363 &prioriAuthenticated
)))) {
367 return prioriAuthenticated
;
371 void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel
* aChannel
,
373 nsTArray
<nsString
> params
;
374 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidJSON", params
);
378 void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel
* aChannel
,
380 const nsAString
& aName
) {
381 nsTArray
<nsString
> params
;
382 params
.AppendElement(aName
);
384 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderDuplicateGroup", params
);
388 void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel
* aChannel
,
390 nsTArray
<nsString
> params
;
391 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidNameItem",
396 void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel
* aChannel
,
398 const nsAString
& aName
) {
399 nsTArray
<nsString
> params
;
400 params
.AppendElement(aName
);
402 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidItem", params
);
406 void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel
* aChannel
,
408 const nsAString
& aName
) {
409 nsTArray
<nsString
> params
;
410 params
.AppendElement(aName
);
412 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidEndpoint",
417 void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel
* aChannel
,
419 const nsAString
& aName
,
420 const nsAString
& aURL
) {
421 nsTArray
<nsString
> params
;
422 params
.AppendElement(aURL
);
423 params
.AppendElement(aName
);
425 LogToConsoleInternal(aChannel
, aURI
, "ReportingHeaderInvalidURLEndpoint",
430 void ReportingHeader::LogToConsoleInternal(nsIHttpChannel
* aChannel
,
431 nsIURI
* aURI
, const char* aMsg
,
432 const nsTArray
<nsString
>& aParams
) {
436 // We are in a gtest.
440 uint64_t windowID
= 0;
442 nsresult rv
= aChannel
->GetTopLevelContentWindowId(&windowID
);
443 if (NS_WARN_IF(NS_FAILED(rv
))) {
448 nsCOMPtr
<nsILoadGroup
> loadGroup
;
449 nsresult rv
= aChannel
->GetLoadGroup(getter_AddRefs(loadGroup
));
450 if (NS_WARN_IF(NS_FAILED(rv
))) {
455 windowID
= nsContentUtils::GetInnerWindowID(loadGroup
);
459 nsAutoString localizedMsg
;
460 rv
= nsContentUtils::FormatLocalizedString(
461 nsContentUtils::eSECURITY_PROPERTIES
, aMsg
, aParams
, localizedMsg
);
462 if (NS_WARN_IF(NS_FAILED(rv
))) {
466 rv
= nsContentUtils::ReportToConsoleByWindowID(
467 localizedMsg
, nsIScriptError::infoFlag
, "Reporting"_ns
, windowID
, aURI
);
468 Unused
<< NS_WARN_IF(NS_FAILED(rv
));
472 void ReportingHeader::GetEndpointForReport(
473 const nsAString
& aGroupName
,
474 const mozilla::ipc::PrincipalInfo
& aPrincipalInfo
,
475 nsACString
& aEndpointURI
) {
476 auto principalOrErr
= PrincipalInfoToPrincipal(aPrincipalInfo
);
477 if (NS_WARN_IF(principalOrErr
.isErr())) {
481 nsCOMPtr
<nsIPrincipal
> principal
= principalOrErr
.unwrap();
482 GetEndpointForReport(aGroupName
, principal
, aEndpointURI
);
486 void ReportingHeader::GetEndpointForReport(const nsAString
& aGroupName
,
487 nsIPrincipal
* aPrincipal
,
488 nsACString
& aEndpointURI
) {
489 MOZ_ASSERT(aEndpointURI
.IsEmpty());
495 nsAutoCString origin
;
496 nsresult rv
= aPrincipal
->GetOrigin(origin
);
497 if (NS_WARN_IF(NS_FAILED(rv
))) {
501 Client
* client
= gReporting
->mOrigins
.Get(origin
);
506 const auto [begin
, end
] = client
->mGroups
.NonObservingRange();
507 const auto foundIt
= std::find_if(
509 [&aGroupName
](const Group
& group
) { return group
.mName
== aGroupName
; });
510 if (foundIt
!= end
) {
511 GetEndpointForReportInternal(*foundIt
, aEndpointURI
);
514 // XXX More explicitly report an error if not found?
518 void ReportingHeader::GetEndpointForReportInternal(
519 const ReportingHeader::Group
& aGroup
, nsACString
& aEndpointURI
) {
520 TimeDuration diff
= TimeStamp::Now() - aGroup
.mCreationTime
;
521 if (diff
.ToSeconds() > aGroup
.mTTL
) {
526 if (aGroup
.mEndpoints
.IsEmpty()) {
530 int64_t minPriority
= -1;
531 uint32_t totalWeight
= 0;
533 for (const Endpoint
& endpoint
: aGroup
.mEndpoints
.NonObservingRange()) {
534 if (minPriority
== -1 || minPriority
> endpoint
.mPriority
) {
535 minPriority
= endpoint
.mPriority
;
536 totalWeight
= endpoint
.mWeight
;
537 } else if (minPriority
== endpoint
.mPriority
) {
538 totalWeight
+= endpoint
.mWeight
;
542 nsCOMPtr
<nsIRandomGenerator
> randomGenerator
=
543 do_GetService("@mozilla.org/security/random-generator;1");
544 if (NS_WARN_IF(!randomGenerator
)) {
548 uint32_t randomNumber
= 0;
552 randomGenerator
->GenerateRandomBytes(sizeof(randomNumber
), &buffer
);
553 if (NS_WARN_IF(NS_FAILED(rv
))) {
557 memcpy(&randomNumber
, buffer
, sizeof(randomNumber
));
560 totalWeight
= randomNumber
% totalWeight
;
562 const auto [begin
, end
] = aGroup
.mEndpoints
.NonObservingRange();
563 const auto foundIt
= std::find_if(
564 begin
, end
, [minPriority
, totalWeight
](const Endpoint
& endpoint
) {
565 return minPriority
== endpoint
.mPriority
&&
566 totalWeight
< endpoint
.mWeight
;
568 if (foundIt
!= end
) {
569 Unused
<< NS_WARN_IF(NS_FAILED(foundIt
->mUrl
->GetSpec(aEndpointURI
)));
571 // XXX More explicitly report an error if not found?
575 void ReportingHeader::RemoveEndpoint(
576 const nsAString
& aGroupName
, const nsACString
& aEndpointURL
,
577 const mozilla::ipc::PrincipalInfo
& aPrincipalInfo
) {
582 nsCOMPtr
<nsIURI
> uri
;
583 nsresult rv
= NS_NewURI(getter_AddRefs(uri
), aEndpointURL
);
584 if (NS_WARN_IF(NS_FAILED(rv
))) {
588 auto principalOrErr
= PrincipalInfoToPrincipal(aPrincipalInfo
);
589 if (NS_WARN_IF(principalOrErr
.isErr())) {
593 nsAutoCString origin
;
594 rv
= principalOrErr
.unwrap()->GetOrigin(origin
);
595 if (NS_WARN_IF(NS_FAILED(rv
))) {
599 Client
* client
= gReporting
->mOrigins
.Get(origin
);
604 // Scope for the group iterator.
606 nsTObserverArray
<Group
>::BackwardIterator
iter(client
->mGroups
);
607 while (iter
.HasMore()) {
608 const Group
& group
= iter
.GetNext();
609 if (group
.mName
!= aGroupName
) {
613 // Scope for the endpoint iterator.
615 nsTObserverArray
<Endpoint
>::BackwardIterator
endpointIter(
617 while (endpointIter
.HasMore()) {
618 const Endpoint
& endpoint
= endpointIter
.GetNext();
621 rv
= endpoint
.mUrl
->Equals(uri
, &equal
);
622 if (NS_WARN_IF(NS_FAILED(rv
))) {
627 endpointIter
.Remove();
633 if (group
.mEndpoints
.IsEmpty()) {
641 if (client
->mGroups
.IsEmpty()) {
642 gReporting
->mOrigins
.Remove(origin
);
643 gReporting
->MaybeCancelCleanupTimer();
647 void ReportingHeader::RemoveOriginsFromHost(const nsAString
& aHost
) {
648 nsCOMPtr
<nsIEffectiveTLDService
> tldService
=
649 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID
);
650 if (NS_WARN_IF(!tldService
)) {
654 NS_ConvertUTF16toUTF8
host(aHost
);
656 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
657 bool hasRootDomain
= false;
658 nsresult rv
= tldService
->HasRootDomain(iter
.Key(), host
, &hasRootDomain
);
659 if (NS_WARN_IF(NS_FAILED(rv
)) || !hasRootDomain
) {
666 MaybeCancelCleanupTimer();
669 void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
670 const OriginAttributesPattern
& aPattern
) {
671 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
672 nsAutoCString suffix
;
673 OriginAttributes attr
;
674 if (NS_WARN_IF(!attr
.PopulateFromOrigin(iter
.Key(), suffix
))) {
678 if (aPattern
.Matches(attr
)) {
683 MaybeCancelCleanupTimer();
686 void ReportingHeader::RemoveOrigins() {
688 MaybeCancelCleanupTimer();
691 void ReportingHeader::RemoveOriginsForTTL() {
692 TimeStamp now
= TimeStamp::Now();
694 for (auto iter
= mOrigins
.Iter(); !iter
.Done(); iter
.Next()) {
695 Client
* client
= iter
.UserData();
697 // Scope of the iterator.
699 nsTObserverArray
<Group
>::BackwardIterator
groupIter(client
->mGroups
);
700 while (groupIter
.HasMore()) {
701 const Group
& group
= groupIter
.GetNext();
702 TimeDuration diff
= now
- group
.mCreationTime
;
703 if (diff
.ToSeconds() > group
.mTTL
) {
710 if (client
->mGroups
.IsEmpty()) {
717 bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString
& aOrigin
) {
722 return gReporting
->mOrigins
.Contains(aOrigin
);
726 ReportingHeader::Notify(nsITimer
* aTimer
) {
727 mCleanupTimer
= nullptr;
729 RemoveOriginsForTTL();
730 MaybeCreateCleanupTimer();
736 ReportingHeader::GetName(nsACString
& aName
) {
737 aName
.AssignLiteral("ReportingHeader");
741 void ReportingHeader::MaybeCreateCleanupTimer() {
746 if (mOrigins
.Count() == 0) {
750 uint32_t timeout
= StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
752 NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer
), this, timeout
,
753 nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY
);
754 Unused
<< NS_WARN_IF(NS_FAILED(rv
));
757 void ReportingHeader::MaybeCancelCleanupTimer() {
758 if (!mCleanupTimer
) {
762 if (mOrigins
.Count() != 0) {
766 mCleanupTimer
->Cancel();
767 mCleanupTimer
= nullptr;
770 NS_INTERFACE_MAP_BEGIN(ReportingHeader
)
771 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIObserver
)
772 NS_INTERFACE_MAP_ENTRY(nsIObserver
)
773 NS_INTERFACE_MAP_ENTRY(nsITimerCallback
)
774 NS_INTERFACE_MAP_ENTRY(nsINamed
)
777 NS_IMPL_ADDREF(ReportingHeader
)
778 NS_IMPL_RELEASE(ReportingHeader
)
781 } // namespace mozilla