ReportDeliver.cpp (12003B)
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/. */ 6 7 #include "mozilla/dom/ReportDeliver.h" 8 9 #include <algorithm> 10 11 #include "mozilla/JSONStringWriteFuncs.h" 12 #include "mozilla/StaticPrefs_dom.h" 13 #include "mozilla/dom/EndpointForReportChild.h" 14 #include "mozilla/dom/Fetch.h" 15 #include "mozilla/dom/Navigator.h" 16 #include "mozilla/dom/Promise.h" 17 #include "mozilla/dom/ReportBody.h" 18 #include "mozilla/dom/Request.h" 19 #include "mozilla/dom/RequestBinding.h" 20 #include "mozilla/dom/Response.h" 21 #include "mozilla/dom/RootedDictionary.h" 22 #include "mozilla/ipc/BackgroundChild.h" 23 #include "mozilla/ipc/PBackgroundChild.h" 24 #include "mozilla/ipc/PBackgroundSharedTypes.h" 25 #include "nsGlobalWindowInner.h" 26 #include "nsIGlobalObject.h" 27 #include "nsIXPConnect.h" 28 #include "nsNetUtil.h" 29 #include "nsStringStream.h" 30 31 namespace mozilla::dom { 32 33 namespace { 34 35 StaticRefPtr<ReportDeliver> gReportDeliver; 36 37 // This is the same value as the default value of 38 // dom.min_timeout_value, so it's not that random. 39 constexpr double gMinReportAgeInMs = 4.0; 40 41 class ReportFetchHandler final : public PromiseNativeHandler { 42 public: 43 NS_DECL_ISUPPORTS 44 45 explicit ReportFetchHandler( 46 const nsTArray<ReportDeliver::ReportData>& aReportData) 47 : mReports(aReportData.Clone()) {} 48 49 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 50 ErrorResult& aRv) override { 51 if (!gReportDeliver) { 52 return; 53 } 54 55 if (NS_WARN_IF(!aValue.isObject())) { 56 return; 57 } 58 59 JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); 60 MOZ_ASSERT(obj); 61 62 { 63 Response* response = nullptr; 64 if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Response, &obj, response)))) { 65 return; 66 } 67 68 if (response->Status() == 410) { 69 mozilla::ipc::PBackgroundChild* actorChild = 70 mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); 71 72 for (const auto& report : mReports) { 73 mozilla::ipc::PrincipalInfo principalInfo; 74 nsresult rv = 75 PrincipalToPrincipalInfo(report.mPrincipal, &principalInfo); 76 if (NS_WARN_IF(NS_FAILED(rv))) { 77 continue; 78 } 79 80 actorChild->SendRemoveEndpoint(report.mGroupName, report.mEndpointURL, 81 principalInfo); 82 } 83 } 84 } 85 } 86 87 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 88 ErrorResult& aRv) override { 89 if (gReportDeliver) { 90 for (auto& report : mReports) { 91 ++report.mFailures; 92 gReportDeliver->AppendReportData(report); 93 } 94 } 95 } 96 97 private: 98 ~ReportFetchHandler() = default; 99 100 nsTArray<ReportDeliver::ReportData> mReports; 101 }; 102 103 NS_IMPL_ISUPPORTS0(ReportFetchHandler) 104 105 class ReportJSONWriter final : public JSONWriter { 106 public: 107 explicit ReportJSONWriter(JSONStringWriteFunc<nsAutoCString>& aOutput) 108 : JSONWriter(aOutput) {} 109 110 void JSONProperty(const Span<const char>& aProperty, 111 const Span<const char>& aJSON) { 112 Separator(); 113 PropertyNameAndColon(aProperty); 114 mWriter.Write(aJSON); 115 } 116 }; 117 118 void SendReports(nsTArray<ReportDeliver::ReportData>& aReports, 119 const nsCString& aEndPointUrl, nsIPrincipal* aPrincipal) { 120 if (NS_WARN_IF(aReports.IsEmpty())) { 121 return; 122 } 123 124 nsIXPConnect* xpc = nsContentUtils::XPConnect(); 125 MOZ_ASSERT(xpc, "This should never be null!"); 126 127 nsCOMPtr<nsIGlobalObject> globalObject; 128 { 129 AutoJSAPI jsapi; 130 jsapi.Init(); 131 132 JSContext* cx = jsapi.cx(); 133 JS::Rooted<JSObject*> sandbox(cx); 134 nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); 135 if (NS_WARN_IF(NS_FAILED(rv))) { 136 return; 137 } 138 139 // The JSContext is not in a realm, so CreateSandbox returned an unwrapped 140 // global. 141 MOZ_ASSERT(JS_IsGlobalObject(sandbox)); 142 143 globalObject = xpc::NativeGlobal(sandbox); 144 } 145 146 if (NS_WARN_IF(!globalObject)) { 147 return; 148 } 149 150 // The body 151 JSONStringWriteFunc<nsAutoCString> body; 152 ReportJSONWriter w(body); 153 154 w.StartArrayElement(); 155 for (const auto& report : aReports) { 156 MOZ_ASSERT(report.mPrincipal == aPrincipal); 157 MOZ_ASSERT(report.mEndpointURL == aEndPointUrl); 158 w.StartObjectElement(); 159 // It looks like in rare cases, TimeStamp::Now() may be the same 160 // as report.mCreationTime, so we introduce a constant number to 161 // make sure "age" is always not 0. 162 w.IntProperty( 163 "age", 164 std::max((TimeStamp::Now() - report.mCreationTime).ToMilliseconds(), 165 gMinReportAgeInMs)); 166 w.StringProperty("type", NS_ConvertUTF16toUTF8(report.mType)); 167 w.StringProperty("url", NS_ConvertUTF16toUTF8(report.mURL)); 168 w.StringProperty("user_agent", NS_ConvertUTF16toUTF8(report.mUserAgent)); 169 w.JSONProperty(MakeStringSpan("body"), 170 Span<const char>(report.mReportBodyJSON.Data(), 171 report.mReportBodyJSON.Length())); 172 w.EndObject(); 173 } 174 w.EndArray(); 175 176 // The body as stream 177 nsCOMPtr<nsIInputStream> streamBody; 178 nsresult rv = 179 NS_NewCStringInputStream(getter_AddRefs(streamBody), body.StringCRef()); 180 181 // Headers 182 IgnoredErrorResult error; 183 RefPtr<InternalHeaders> internalHeaders = 184 new InternalHeaders(HeadersGuardEnum::Request); 185 internalHeaders->Set("Content-Type"_ns, "application/reports+json"_ns, error); 186 if (NS_WARN_IF(error.Failed())) { 187 return; 188 } 189 190 // URL and fragments 191 nsCOMPtr<nsIURI> uri; 192 rv = NS_NewURI(getter_AddRefs(uri), aEndPointUrl); 193 if (NS_WARN_IF(NS_FAILED(rv))) { 194 return; 195 } 196 197 nsCOMPtr<nsIURI> uriClone; 198 rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone)); 199 if (NS_WARN_IF(NS_FAILED(rv))) { 200 return; 201 } 202 203 nsAutoCString uriSpec; 204 rv = uriClone->GetSpec(uriSpec); 205 if (NS_WARN_IF(NS_FAILED(rv))) { 206 return; 207 } 208 209 nsAutoCString uriFragment; 210 rv = uri->GetRef(uriFragment); 211 if (NS_WARN_IF(NS_FAILED(rv))) { 212 return; 213 } 214 215 auto internalRequest = MakeSafeRefPtr<InternalRequest>(uriSpec, uriFragment); 216 217 internalRequest->SetMethod("POST"_ns); 218 internalRequest->SetBody(streamBody, body.StringCRef().Length()); 219 internalRequest->SetHeaders(internalHeaders); 220 internalRequest->SetSkipServiceWorker(); 221 // TODO: internalRequest->SetContentPolicyType(TYPE_REPORT); 222 internalRequest->SetMode(RequestMode::Cors); 223 internalRequest->SetCredentialsMode(RequestCredentials::Same_origin); 224 225 RefPtr<Request> request = 226 new Request(globalObject, std::move(internalRequest), nullptr); 227 228 RequestOrUTF8String fetchInput; 229 fetchInput.SetAsRequest() = request; 230 231 RootedDictionary<RequestInit> requestInit(RootingCx()); 232 RefPtr<Promise> promise = FetchRequest(globalObject, fetchInput, requestInit, 233 CallerType::NonSystem, error); 234 if (error.Failed()) { 235 for (auto& report : aReports) { 236 ++report.mFailures; 237 if (gReportDeliver) { 238 gReportDeliver->AppendReportData(report); 239 } 240 } 241 return; 242 } 243 244 RefPtr<ReportFetchHandler> handler = new ReportFetchHandler(aReports); 245 promise->AppendNativeHandler(handler); 246 } 247 248 } // namespace 249 250 /* static */ 251 void ReportDeliver::Record(nsPIDOMWindowInner* aWindow, const nsAString& aType, 252 const nsAString& aGroupName, const nsAString& aURL, 253 ReportBody* aBody) { 254 MOZ_ASSERT(NS_IsMainThread()); 255 MOZ_ASSERT(aWindow); 256 MOZ_ASSERT(aBody); 257 258 JSONStringWriteFunc<nsAutoCString> reportBodyJSON; 259 ReportJSONWriter w(reportBodyJSON); 260 261 w.Start(); 262 aBody->ToJSON(w); 263 w.End(); 264 265 nsCOMPtr<nsIPrincipal> principal = 266 nsGlobalWindowInner::Cast(aWindow)->GetPrincipal(); 267 if (NS_WARN_IF(!principal)) { 268 return; 269 } 270 271 mozilla::ipc::PrincipalInfo principalInfo; 272 nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo); 273 if (NS_WARN_IF(NS_FAILED(rv))) { 274 return; 275 } 276 277 mozilla::ipc::PBackgroundChild* actorChild = 278 mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); 279 280 PEndpointForReportChild* actor = 281 actorChild->SendPEndpointForReportConstructor(nsString(aGroupName), 282 principalInfo); 283 if (NS_WARN_IF(!actor)) { 284 return; 285 } 286 287 ReportData data; 288 data.mType = aType; 289 data.mGroupName = aGroupName; 290 data.mURL = aURL; 291 data.mCreationTime = TimeStamp::Now(); 292 data.mReportBodyJSON = std::move(reportBodyJSON).StringRRef(); 293 data.mPrincipal = principal; 294 data.mFailures = 0; 295 296 Navigator* navigator = aWindow->Navigator(); 297 MOZ_ASSERT(navigator); 298 299 IgnoredErrorResult error; 300 navigator->GetUserAgent(data.mUserAgent, CallerType::NonSystem, error); 301 if (NS_WARN_IF(error.Failed())) { 302 return; 303 } 304 305 static_cast<EndpointForReportChild*>(actor)->Initialize(data); 306 } 307 308 /* static */ 309 void ReportDeliver::Fetch(const ReportData& aReportData) { 310 if (!gReportDeliver) { 311 RefPtr<ReportDeliver> rd = new ReportDeliver(); 312 313 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 314 if (NS_WARN_IF(!obs)) { 315 return; 316 } 317 318 obs->AddObserver(rd, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); 319 gReportDeliver = rd; 320 } 321 322 gReportDeliver->AppendReportData(aReportData); 323 } 324 325 void ReportDeliver::AppendReportData(const ReportData& aReportData) { 326 if (aReportData.mFailures > 327 StaticPrefs::dom_reporting_delivering_maxFailures()) { 328 return; 329 } 330 331 if (NS_WARN_IF(!mReportQueue.AppendElement(aReportData, fallible))) { 332 return; 333 } 334 335 while (mReportQueue.Length() > 336 StaticPrefs::dom_reporting_delivering_maxReports()) { 337 mReportQueue.RemoveElementAt(0); 338 } 339 340 RefPtr<ReportDeliver> self{this}; 341 nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( 342 "ReportDeliver::CallNotify", [self]() { self->Notify(); }); 343 344 NS_DispatchToCurrentThreadQueue( 345 runnable.forget(), StaticPrefs::dom_reporting_delivering_timeout() * 1000, 346 EventQueuePriority::Idle); 347 } 348 349 void ReportDeliver::Notify() { 350 nsTArray<ReportData> reports = std::move(mReportQueue); 351 352 // group reports by endpoint and nsIPrincipal 353 std::map<std::pair<nsCString, nsCOMPtr<nsIPrincipal>>, nsTArray<ReportData>> 354 reportsByPrincipal; 355 for (ReportData& report : reports) { 356 auto already_seen = 357 reportsByPrincipal.find({report.mEndpointURL, report.mPrincipal}); 358 if (already_seen == reportsByPrincipal.end()) { 359 reportsByPrincipal.emplace( 360 std::make_pair(report.mEndpointURL, report.mPrincipal), 361 nsTArray<ReportData>({report})); 362 } else { 363 already_seen->second.AppendElement(report); 364 } 365 } 366 367 for (auto& iter : reportsByPrincipal) { 368 std::pair<nsCString, nsCOMPtr<nsIPrincipal>> key = iter.first; 369 nsTArray<ReportData>& value = iter.second; 370 nsCString url = key.first; 371 nsCOMPtr<nsIPrincipal> principal = key.second; 372 nsAutoCString u(url); 373 SendReports(value, url, principal); 374 } 375 } 376 377 NS_IMETHODIMP 378 ReportDeliver::GetName(nsACString& aName) { 379 aName.AssignLiteral("ReportDeliver"); 380 return NS_OK; 381 } 382 383 NS_IMETHODIMP 384 ReportDeliver::Observe(nsISupports* aSubject, const char* aTopic, 385 const char16_t* aData) { 386 MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); 387 388 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 389 if (NS_WARN_IF(!obs)) { 390 return NS_OK; 391 } 392 393 obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); 394 395 gReportDeliver = nullptr; 396 return NS_OK; 397 } 398 399 ReportDeliver::ReportDeliver() = default; 400 401 ReportDeliver::~ReportDeliver() = default; 402 403 NS_INTERFACE_MAP_BEGIN(ReportDeliver) 404 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) 405 NS_INTERFACE_MAP_ENTRY(nsIObserver) 406 NS_INTERFACE_MAP_ENTRY(nsINamed) 407 NS_INTERFACE_MAP_END 408 409 NS_IMPL_ADDREF(ReportDeliver) 410 NS_IMPL_RELEASE(ReportDeliver) 411 412 } // namespace mozilla::dom