diff options
Diffstat (limited to 'dom/reporting/ReportingHeader.cpp')
-rw-r--r-- | dom/reporting/ReportingHeader.cpp | 779 |
1 files changed, 779 insertions, 0 deletions
diff --git a/dom/reporting/ReportingHeader.cpp b/dom/reporting/ReportingHeader.cpp new file mode 100644 index 0000000000..66a7c93e1e --- /dev/null +++ b/dom/reporting/ReportingHeader.cpp @@ -0,0 +1,779 @@ +/* -*- 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; + + uint8_t* buffer; + nsresult rv = + randomGenerator->GenerateRandomBytes(sizeof(randomNumber), &buffer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + memcpy(&randomNumber, buffer, sizeof(randomNumber)); + free(buffer); + + 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 |