FetchUtil.cpp (27768B)
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 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "FetchUtil.h" 8 9 #include "js/BuildId.h" 10 #include "js/friend/ErrorMessages.h" // JSMSG_* 11 #include "mozilla/ClearOnShutdown.h" 12 #include "mozilla/dom/DOMException.h" 13 #include "mozilla/dom/Document.h" 14 #include "mozilla/dom/InternalRequest.h" 15 #include "mozilla/dom/ReferrerInfo.h" 16 #include "mozilla/dom/Response.h" 17 #include "mozilla/dom/WorkerRef.h" 18 #include "nsCRT.h" 19 #include "nsError.h" 20 #include "nsIAsyncInputStream.h" 21 #include "nsICloneableInputStream.h" 22 #include "nsIHttpChannel.h" 23 #include "nsNetUtil.h" 24 #include "nsStreamUtils.h" 25 #include "nsString.h" 26 #include "zlib.h" 27 28 namespace mozilla::dom { 29 30 // static 31 nsresult FetchUtil::GetValidRequestMethod(const nsACString& aMethod, 32 nsCString& outMethod) { 33 nsAutoCString upperCaseMethod(aMethod); 34 ToUpperCase(upperCaseMethod); 35 if (!NS_IsValidHTTPToken(aMethod)) { 36 outMethod.SetIsVoid(true); 37 return NS_ERROR_DOM_SYNTAX_ERR; 38 } 39 40 if (upperCaseMethod.EqualsLiteral("CONNECT") || 41 upperCaseMethod.EqualsLiteral("TRACE") || 42 upperCaseMethod.EqualsLiteral("TRACK")) { 43 outMethod.SetIsVoid(true); 44 return NS_ERROR_DOM_SECURITY_ERR; 45 } 46 47 if (upperCaseMethod.EqualsLiteral("DELETE") || 48 upperCaseMethod.EqualsLiteral("GET") || 49 upperCaseMethod.EqualsLiteral("HEAD") || 50 upperCaseMethod.EqualsLiteral("OPTIONS") || 51 upperCaseMethod.EqualsLiteral("POST") || 52 upperCaseMethod.EqualsLiteral("PUT")) { 53 outMethod = upperCaseMethod; 54 } else { 55 outMethod = aMethod; // Case unchanged for non-standard methods 56 } 57 return NS_OK; 58 } 59 60 static bool FindCRLF(nsACString::const_iterator& aStart, 61 nsACString::const_iterator& aEnd) { 62 nsACString::const_iterator end(aEnd); 63 return FindInReadable("\r\n"_ns, aStart, end); 64 } 65 66 // Reads over a CRLF and positions start after it. 67 static bool PushOverLine(nsACString::const_iterator& aStart, 68 const nsACString::const_iterator& aEnd) { 69 if (*aStart == nsCRT::CR && (aEnd - aStart > 1) && *(++aStart) == nsCRT::LF) { 70 ++aStart; // advance to after CRLF 71 return true; 72 } 73 74 return false; 75 } 76 77 // static 78 bool FetchUtil::IncrementPendingKeepaliveRequestSize( 79 nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) { 80 uint64_t pendingKeepaliveRequestSize = 0; 81 MOZ_ASSERT(aLoadGroup); 82 83 aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize); 84 pendingKeepaliveRequestSize += aBodyLength; 85 86 if (pendingKeepaliveRequestSize > FETCH_KEEPALIVE_MAX_SIZE) { 87 return false; 88 } 89 90 aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize); 91 return true; 92 } 93 94 // static 95 void FetchUtil::DecrementPendingKeepaliveRequestSize( 96 nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) { 97 MOZ_ASSERT(aLoadGroup); 98 99 uint64_t pendingKeepaliveRequestSize = 0; 100 aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize); 101 MOZ_ASSERT(pendingKeepaliveRequestSize >= aBodyLength); 102 pendingKeepaliveRequestSize -= aBodyLength; 103 aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize); 104 } 105 106 // static 107 nsCOMPtr<nsILoadGroup> FetchUtil::GetLoadGroupFromGlobal( 108 nsIGlobalObject* aGlobalObject) { 109 MOZ_ASSERT(NS_IsMainThread()); 110 nsCOMPtr<nsILoadGroup> loadGroup = nullptr; 111 auto* innerWindow = aGlobalObject->GetAsInnerWindow(); 112 113 if (innerWindow) { 114 Document* doc = innerWindow->GetExtantDoc(); 115 if (doc) { 116 loadGroup = doc->GetDocumentLoadGroup(); 117 } 118 } 119 120 return loadGroup; 121 } 122 123 // static 124 bool FetchUtil::ExtractHeader(nsACString::const_iterator& aStart, 125 nsACString::const_iterator& aEnd, 126 nsCString& aHeaderName, nsCString& aHeaderValue, 127 bool* aWasEmptyHeader) { 128 MOZ_ASSERT(aWasEmptyHeader); 129 // Set it to a valid value here so we don't forget later. 130 *aWasEmptyHeader = false; 131 132 const char* beginning = aStart.get(); 133 nsACString::const_iterator end(aEnd); 134 if (!FindCRLF(aStart, end)) { 135 return false; 136 } 137 138 if (aStart.get() == beginning) { 139 *aWasEmptyHeader = true; 140 return true; 141 } 142 143 nsAutoCString header(beginning, aStart.get() - beginning); 144 145 nsACString::const_iterator headerStart, iter, headerEnd; 146 header.BeginReading(headerStart); 147 header.EndReading(headerEnd); 148 iter = headerStart; 149 if (!FindCharInReadable(':', iter, headerEnd)) { 150 return false; 151 } 152 153 aHeaderName.Assign(StringHead(header, iter - headerStart)); 154 aHeaderName.CompressWhitespace(); 155 if (!NS_IsValidHTTPToken(aHeaderName)) { 156 return false; 157 } 158 159 aHeaderValue.Assign(Substring(++iter, headerEnd)); 160 if (!NS_IsReasonableHTTPHeaderValue(aHeaderValue)) { 161 return false; 162 } 163 aHeaderValue.CompressWhitespace(); 164 165 return PushOverLine(aStart, aEnd); 166 } 167 168 // static 169 nsresult FetchUtil::SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, 170 nsIHttpChannel* aChannel, 171 InternalRequest& aRequest) { 172 MOZ_ASSERT(NS_IsMainThread()); 173 174 nsresult rv = NS_OK; 175 nsAutoCString referrer; 176 aRequest.GetReferrer(referrer); 177 178 ReferrerPolicy policy = aRequest.ReferrerPolicy_(); 179 nsCOMPtr<nsIReferrerInfo> referrerInfo; 180 if (referrer.IsEmpty()) { 181 // This is the case request’s referrer is "no-referrer" 182 referrerInfo = new ReferrerInfo(nullptr, ReferrerPolicy::No_referrer); 183 } else if (referrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { 184 referrerInfo = ReferrerInfo::CreateForFetch(aPrincipal, aDoc); 185 // In the first step, we should use referrer info from requetInit 186 referrerInfo = static_cast<ReferrerInfo*>(referrerInfo.get()) 187 ->CloneWithNewPolicy(policy); 188 } else { 189 // From "Determine request's Referrer" step 3 190 // "If request's referrer is a URL, let referrerSource be request's 191 // referrer." 192 nsCOMPtr<nsIURI> referrerURI; 193 rv = NS_NewURI(getter_AddRefs(referrerURI), referrer); 194 NS_ENSURE_SUCCESS(rv, rv); 195 referrerInfo = new ReferrerInfo(referrerURI, policy); 196 } 197 198 rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); 199 NS_ENSURE_SUCCESS(rv, rv); 200 201 nsAutoCString computedReferrerSpec; 202 referrerInfo = aChannel->GetReferrerInfo(); 203 if (referrerInfo) { 204 (void)referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); 205 } 206 207 // Step 8 https://fetch.spec.whatwg.org/#main-fetch 208 // If request’s referrer is not "no-referrer", set request’s referrer to 209 // the result of invoking determine request’s referrer. 210 aRequest.SetReferrer(computedReferrerSpec); 211 212 return NS_OK; 213 } 214 215 class StoreOptimizedEncodingRunnable final : public Runnable { 216 nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; 217 Vector<uint8_t> mBytes; 218 219 public: 220 StoreOptimizedEncodingRunnable( 221 nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, 222 Vector<uint8_t>&& aBytes) 223 : Runnable("StoreOptimizedEncodingRunnable"), 224 mCache(std::move(aCache)), 225 mBytes(std::move(aBytes)) {} 226 227 NS_IMETHOD Run() override { 228 nsresult rv; 229 230 nsCOMPtr<nsIAsyncOutputStream> stream; 231 rv = mCache->OpenAlternativeOutputStream(FetchUtil::GetWasmAltDataType(), 232 int64_t(mBytes.length()), 233 getter_AddRefs(stream)); 234 if (NS_FAILED(rv)) { 235 return rv; 236 } 237 238 auto closeStream = MakeScopeExit([&]() { stream->CloseWithStatus(rv); }); 239 240 uint32_t written; 241 rv = stream->Write((char*)mBytes.begin(), mBytes.length(), &written); 242 if (NS_FAILED(rv)) { 243 return rv; 244 } 245 246 MOZ_RELEASE_ASSERT(mBytes.length() == written); 247 return NS_OK; 248 }; 249 }; 250 251 class WindowStreamOwner final : public GlobalTeardownObserver { 252 private: 253 // Read from any thread but only set/cleared on the main thread. The lifecycle 254 // of WindowStreamOwner prevents concurrent read/clear. 255 nsCOMPtr<nsIAsyncInputStream> mStream; 256 257 ~WindowStreamOwner() { MOZ_ASSERT(NS_IsMainThread()); } 258 259 public: 260 NS_DECL_ISUPPORTS 261 262 WindowStreamOwner(nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal) 263 : GlobalTeardownObserver(aGlobal), mStream(aStream) { 264 MOZ_DIAGNOSTIC_ASSERT(aGlobal); 265 MOZ_ASSERT(NS_IsMainThread()); 266 } 267 268 // GlobalTeardownObserver: 269 270 void DisconnectFromOwner() override { 271 MOZ_ASSERT(NS_IsMainThread()); 272 273 if (!mStream) { 274 return; 275 } 276 277 // mStream->Close() will call JSStreamConsumer::OnInputStreamReady which may 278 // then destory itself, but GTO should be strongly grabbing us right as it's 279 // calling DisconnectFromOwner. 280 281 mStream->Close(); 282 mStream = nullptr; 283 284 GlobalTeardownObserver::DisconnectFromOwner(); 285 } 286 }; 287 288 NS_IMPL_ISUPPORTS0(WindowStreamOwner) 289 290 inline nsISupports* ToSupports(WindowStreamOwner* aObj) { 291 return static_cast<GlobalTeardownObserver*>(aObj); 292 } 293 294 class WorkerStreamOwner final { 295 public: 296 NS_INLINE_DECL_REFCOUNTING(WorkerStreamOwner) 297 298 explicit WorkerStreamOwner(nsIAsyncInputStream* aStream, 299 nsCOMPtr<nsIEventTarget>&& target) 300 : mStream(aStream), mOwningEventTarget(std::move(target)) {} 301 302 static already_AddRefed<WorkerStreamOwner> Create( 303 nsIAsyncInputStream* aStream, WorkerPrivate* aWorker, 304 nsCOMPtr<nsIEventTarget>&& target) { 305 RefPtr<WorkerStreamOwner> self = 306 new WorkerStreamOwner(aStream, std::move(target)); 307 308 self->mWorkerRef = 309 StrongWorkerRef::Create(aWorker, "JSStreamConsumer", [self]() { 310 if (self->mStream) { 311 // If this Close() calls JSStreamConsumer::OnInputStreamReady and 312 // drops the last reference to the JSStreamConsumer, 'this' will not 313 // be destroyed since ~JSStreamConsumer() only enqueues a release 314 // proxy. 315 self->mStream->Close(); 316 self->mStream = nullptr; 317 } 318 }); 319 320 if (!self->mWorkerRef) { 321 return nullptr; 322 } 323 324 return self.forget(); 325 } 326 327 static void ProxyRelease(already_AddRefed<WorkerStreamOwner> aDoomed) { 328 RefPtr<WorkerStreamOwner> doomed = aDoomed; 329 nsIEventTarget* target = doomed->mOwningEventTarget; 330 NS_ProxyRelease("WorkerStreamOwner", target, doomed.forget(), 331 /* aAlwaysProxy = */ true); 332 } 333 334 private: 335 ~WorkerStreamOwner() = default; 336 337 // Read from any thread but only set/cleared on the worker thread. The 338 // lifecycle of WorkerStreamOwner prevents concurrent read/clear. 339 nsCOMPtr<nsIAsyncInputStream> mStream; 340 RefPtr<StrongWorkerRef> mWorkerRef; 341 nsCOMPtr<nsIEventTarget> mOwningEventTarget; 342 }; 343 344 class JSStreamConsumer final : public nsIInputStreamCallback, 345 public JS::OptimizedEncodingListener { 346 // A LengthPrefixType is stored at the start of the compressed optimized 347 // encoding, allowing the decompressed buffer to be allocated to exactly 348 // the right size. 349 using LengthPrefixType = uint32_t; 350 static const unsigned PrefixBytes = sizeof(LengthPrefixType); 351 352 RefPtr<WindowStreamOwner> mWindowStreamOwner; 353 RefPtr<WorkerStreamOwner> mWorkerStreamOwner; 354 nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; 355 const bool mOptimizedEncoding; 356 z_stream mZStream; 357 bool mZStreamInitialized; 358 Vector<uint8_t> mOptimizedEncodingBytes; 359 JS::StreamConsumer* mConsumer; 360 bool mConsumerAborted; 361 362 JSStreamConsumer(already_AddRefed<WindowStreamOwner> aWindowStreamOwner, 363 nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, 364 nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, 365 bool aOptimizedEncoding) 366 : mWindowStreamOwner(aWindowStreamOwner), 367 mCache(std::move(aCache)), 368 mOptimizedEncoding(aOptimizedEncoding), 369 mZStreamInitialized(false), 370 mConsumer(aConsumer), 371 mConsumerAborted(false) { 372 MOZ_DIAGNOSTIC_ASSERT(mWindowStreamOwner); 373 MOZ_DIAGNOSTIC_ASSERT(mConsumer); 374 } 375 376 JSStreamConsumer(RefPtr<WorkerStreamOwner> aWorkerStreamOwner, 377 nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, 378 nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, 379 bool aOptimizedEncoding) 380 : mWorkerStreamOwner(std::move(aWorkerStreamOwner)), 381 mCache(std::move(aCache)), 382 mOptimizedEncoding(aOptimizedEncoding), 383 mZStreamInitialized(false), 384 mConsumer(aConsumer), 385 mConsumerAborted(false) { 386 MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); 387 MOZ_DIAGNOSTIC_ASSERT(mConsumer); 388 } 389 390 ~JSStreamConsumer() { 391 if (mZStreamInitialized) { 392 inflateEnd(&mZStream); 393 } 394 395 // Both WindowStreamOwner and WorkerStreamOwner need to be destroyed on 396 // their global's event target thread. 397 398 if (mWindowStreamOwner) { 399 MOZ_DIAGNOSTIC_ASSERT(!mWorkerStreamOwner); 400 NS_ReleaseOnMainThread("JSStreamConsumer::mWindowStreamOwner", 401 mWindowStreamOwner.forget(), 402 /* aAlwaysProxy = */ true); 403 } else { 404 MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); 405 WorkerStreamOwner::ProxyRelease(mWorkerStreamOwner.forget()); 406 } 407 408 // Bug 1733674: these annotations currently do nothing, because they are 409 // member variables and the annotation mechanism only applies to locals. But 410 // the analysis could be extended so that these could replace the big-hammer 411 // ~JSStreamConsumer annotation and thus the analysis could check that 412 // nothing is added that might GC for a different reason. 413 JS_HAZ_VALUE_IS_GC_SAFE(mWindowStreamOwner); 414 JS_HAZ_VALUE_IS_GC_SAFE(mWorkerStreamOwner); 415 } 416 417 static nsresult WriteSegment(nsIInputStream* aStream, void* aClosure, 418 const char* aFromSegment, uint32_t aToOffset, 419 uint32_t aCount, uint32_t* aWriteCount) { 420 JSStreamConsumer* self = reinterpret_cast<JSStreamConsumer*>(aClosure); 421 MOZ_DIAGNOSTIC_ASSERT(!self->mConsumerAborted); 422 423 if (self->mOptimizedEncoding) { 424 if (!self->mZStreamInitialized) { 425 // mOptimizedEncodingBytes is used as temporary storage until we have 426 // the full prefix. 427 MOZ_ASSERT(self->mOptimizedEncodingBytes.length() < PrefixBytes); 428 uint32_t remain = PrefixBytes - self->mOptimizedEncodingBytes.length(); 429 uint32_t consume = std::min(remain, aCount); 430 431 if (!self->mOptimizedEncodingBytes.append(aFromSegment, consume)) { 432 return NS_ERROR_UNEXPECTED; 433 } 434 435 if (consume == remain) { 436 // Initialize zlib once all prefix bytes are loaded. 437 LengthPrefixType length; 438 memcpy(&length, self->mOptimizedEncodingBytes.begin(), PrefixBytes); 439 440 if (!self->mOptimizedEncodingBytes.resizeUninitialized(length)) { 441 return NS_ERROR_UNEXPECTED; 442 } 443 444 memset(&self->mZStream, 0, sizeof(self->mZStream)); 445 self->mZStream.avail_out = length; 446 self->mZStream.next_out = self->mOptimizedEncodingBytes.begin(); 447 448 if (inflateInit(&self->mZStream) != Z_OK) { 449 return NS_ERROR_UNEXPECTED; 450 } 451 self->mZStreamInitialized = true; 452 } 453 454 *aWriteCount = consume; 455 return NS_OK; 456 } 457 458 // Zlib is initialized, overwrite the prefix with the inflated data. 459 460 MOZ_DIAGNOSTIC_ASSERT(aCount > 0); 461 self->mZStream.avail_in = aCount; 462 self->mZStream.next_in = (uint8_t*)aFromSegment; 463 464 int ret = inflate(&self->mZStream, Z_NO_FLUSH); 465 466 MOZ_DIAGNOSTIC_ASSERT(ret == Z_OK || ret == Z_STREAM_END, 467 "corrupt optimized wasm cache file: data"); 468 MOZ_DIAGNOSTIC_ASSERT(self->mZStream.avail_in == 0, 469 "corrupt optimized wasm cache file: input"); 470 MOZ_DIAGNOSTIC_ASSERT_IF(ret == Z_STREAM_END, 471 self->mZStream.avail_out == 0); 472 // Gracefully handle corruption in release. 473 bool ok = 474 (ret == Z_OK || ret == Z_STREAM_END) && self->mZStream.avail_in == 0; 475 if (!ok) { 476 return NS_ERROR_UNEXPECTED; 477 } 478 } else { 479 // This callback can be called on any thread which is explicitly allowed 480 // by this particular JS API call. 481 if (!self->mConsumer->consumeChunk((const uint8_t*)aFromSegment, 482 aCount)) { 483 self->mConsumerAborted = true; 484 return NS_ERROR_UNEXPECTED; 485 } 486 } 487 488 *aWriteCount = aCount; 489 return NS_OK; 490 } 491 492 public: 493 NS_DECL_THREADSAFE_ISUPPORTS 494 495 static bool Start(nsCOMPtr<nsIInputStream> aStream, nsIGlobalObject* aGlobal, 496 WorkerPrivate* aMaybeWorker, JS::StreamConsumer* aConsumer, 497 nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, 498 bool aOptimizedEncoding) { 499 nsCOMPtr<nsIAsyncInputStream> asyncStream; 500 nsresult rv = NS_MakeAsyncNonBlockingInputStream( 501 aStream.forget(), getter_AddRefs(asyncStream)); 502 if (NS_WARN_IF(NS_FAILED(rv))) { 503 return false; 504 } 505 506 RefPtr<JSStreamConsumer> consumer; 507 if (aMaybeWorker) { 508 RefPtr<WorkerStreamOwner> owner = WorkerStreamOwner::Create( 509 asyncStream, aMaybeWorker, aGlobal->SerialEventTarget()); 510 if (!owner) { 511 return false; 512 } 513 514 consumer = new JSStreamConsumer(std::move(owner), aGlobal, aConsumer, 515 std::move(aCache), aOptimizedEncoding); 516 } else { 517 RefPtr<WindowStreamOwner> owner = 518 new WindowStreamOwner(asyncStream, aGlobal); 519 if (!owner) { 520 return false; 521 } 522 523 consumer = new JSStreamConsumer(owner.forget(), aGlobal, aConsumer, 524 std::move(aCache), aOptimizedEncoding); 525 } 526 527 // This AsyncWait() creates a ref-cycle between asyncStream and consumer: 528 // 529 // asyncStream -> consumer -> (Window|Worker)StreamOwner -> asyncStream 530 // 531 // The cycle is broken when the stream completes or errors out and 532 // asyncStream drops its reference to consumer. 533 return NS_SUCCEEDED(asyncStream->AsyncWait(consumer, 0, 0, nullptr)); 534 } 535 536 // nsIInputStreamCallback: 537 538 NS_IMETHOD 539 OnInputStreamReady(nsIAsyncInputStream* aStream) override { 540 // Can be called on any stream. The JS API calls made below explicitly 541 // support being called from any thread. 542 MOZ_DIAGNOSTIC_ASSERT(!mConsumerAborted); 543 544 nsresult rv; 545 546 uint64_t available = 0; 547 rv = aStream->Available(&available); 548 if (NS_SUCCEEDED(rv) && available == 0) { 549 rv = NS_BASE_STREAM_CLOSED; 550 } 551 552 if (rv == NS_BASE_STREAM_CLOSED) { 553 if (mOptimizedEncoding) { 554 // Gracefully handle corruption of compressed data stream in release. 555 // From on investigations in bug 1738987, the incomplete data cases 556 // mostly happen during shutdown. Some corruptions in the cache entry 557 // can still happen and will be handled in the WriteSegment above. 558 bool ok = mZStreamInitialized && mZStream.avail_out == 0; 559 if (!ok) { 560 mConsumer->streamError(size_t(NS_ERROR_UNEXPECTED)); 561 return NS_OK; 562 } 563 564 mConsumer->consumeOptimizedEncoding(mOptimizedEncodingBytes.begin(), 565 mOptimizedEncodingBytes.length()); 566 } else { 567 // If there is cache entry associated with this stream, then listen for 568 // an optimized encoding so we can store it in the alt data. By JS API 569 // contract, the compilation process will hold a refcount to 'this' 570 // until it's done, optionally calling storeOptimizedEncoding(). 571 mConsumer->streamEnd(mCache ? this : nullptr); 572 } 573 return NS_OK; 574 } 575 576 if (NS_FAILED(rv)) { 577 mConsumer->streamError(size_t(rv)); 578 return NS_OK; 579 } 580 581 // Check mConsumerAborted before NS_FAILED to avoid calling streamError() 582 // if consumeChunk() returned false per JS API contract. 583 uint32_t written = 0; 584 rv = aStream->ReadSegments(WriteSegment, this, available, &written); 585 if (mConsumerAborted) { 586 return NS_OK; 587 } 588 if (NS_WARN_IF(NS_FAILED(rv))) { 589 mConsumer->streamError(size_t(rv)); 590 return NS_OK; 591 } 592 593 rv = aStream->AsyncWait(this, 0, 0, nullptr); 594 if (NS_WARN_IF(NS_FAILED(rv))) { 595 mConsumer->streamError(size_t(rv)); 596 return NS_OK; 597 } 598 599 return NS_OK; 600 } 601 602 // JS::OptimizedEncodingListener 603 604 void storeOptimizedEncoding(const uint8_t* aSrcBytes, 605 size_t aSrcLength) override { 606 MOZ_ASSERT(mCache, "we only listen if there's a cache entry"); 607 608 z_stream zstream; 609 memset(&zstream, 0, sizeof(zstream)); 610 zstream.avail_in = aSrcLength; 611 zstream.next_in = (uint8_t*)aSrcBytes; 612 613 // The wins from increasing compression levels are tiny, while the time 614 // to compress increases drastically. For example, for a 148mb alt-data 615 // produced by a 40mb .wasm file, the level 2 takes 2.5s to get a 3.7x size 616 // reduction while level 9 takes 22.5s to get a 4x size reduction. Read-time 617 // wins from smaller compressed cache files are not found to be 618 // significant, thus the fastest compression level is used. (On test 619 // workloads, level 2 actually was faster *and* smaller than level 1.) 620 const int COMPRESSION = 2; 621 if (deflateInit(&zstream, COMPRESSION) != Z_OK) { 622 return; 623 } 624 auto autoDestroy = MakeScopeExit([&]() { deflateEnd(&zstream); }); 625 626 Vector<uint8_t> dstBytes; 627 if (!dstBytes.resizeUninitialized(PrefixBytes + 628 deflateBound(&zstream, aSrcLength))) { 629 return; 630 } 631 632 MOZ_RELEASE_ASSERT(LengthPrefixType(aSrcLength) == aSrcLength); 633 LengthPrefixType srcLength = aSrcLength; 634 memcpy(dstBytes.begin(), &srcLength, PrefixBytes); 635 636 uint8_t* compressBegin = dstBytes.begin() + PrefixBytes; 637 zstream.next_out = compressBegin; 638 zstream.avail_out = dstBytes.length() - PrefixBytes; 639 640 int ret = deflate(&zstream, Z_FINISH); 641 if (ret == Z_MEM_ERROR) { 642 return; 643 } 644 MOZ_RELEASE_ASSERT(ret == Z_STREAM_END); 645 646 dstBytes.shrinkTo(zstream.next_out - dstBytes.begin()); 647 648 NS_DispatchToMainThread(new StoreOptimizedEncodingRunnable( 649 std::move(mCache), std::move(dstBytes))); 650 } 651 }; 652 653 NS_IMPL_ISUPPORTS(JSStreamConsumer, nsIInputStreamCallback) 654 655 // static 656 constinit nsCString FetchUtil::WasmAltDataType; 657 658 // static 659 void FetchUtil::InitWasmAltDataType() { 660 MOZ_ASSERT(WasmAltDataType.IsEmpty()); 661 662 RunOnShutdown([]() { 663 // Avoid StringBuffer leak tests failures. 664 WasmAltDataType.Truncate(); 665 }); 666 667 WasmAltDataType.Append(nsLiteralCString("wasm-")); 668 669 JS::BuildIdCharVector buildId; 670 if (!JS::GetOptimizedEncodingBuildId(&buildId)) { 671 MOZ_CRASH("build id oom"); 672 } 673 674 WasmAltDataType.Append(buildId.begin(), buildId.length()); 675 } 676 677 static bool ThrowException(JSContext* aCx, unsigned errorNumber) { 678 JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, errorNumber); 679 return false; 680 } 681 682 // static 683 bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, 684 JS::MimeType aMimeType, 685 JS::StreamConsumer* aConsumer, 686 WorkerPrivate* aMaybeWorker) { 687 MOZ_ASSERT(!WasmAltDataType.IsEmpty()); 688 MOZ_ASSERT(!aMaybeWorker == NS_IsMainThread()); 689 690 RefPtr<Response> response; 691 nsresult rv = UNWRAP_OBJECT(Response, aObj, response); 692 if (NS_FAILED(rv)) { 693 return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_VALUE); 694 } 695 696 const char* requiredMimeType = nullptr; 697 switch (aMimeType) { 698 case JS::MimeType::Wasm: 699 requiredMimeType = WASM_CONTENT_TYPE; 700 break; 701 } 702 703 // For WASM, the Content-Type must be exactly "application/wasm" with no 704 // parameters. Check the raw header value before parsing normalizes it. 705 ErrorResult result; 706 nsAutoCString mimeType; 707 response->GetInternalHeaders()->Get("Content-Type"_ns, mimeType, result); 708 MOZ_ALWAYS_TRUE(!result.Failed()); 709 ToLowerCase(mimeType); 710 711 if (!mimeType.EqualsASCII(requiredMimeType)) { 712 JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, 713 JSMSG_WASM_BAD_RESPONSE_MIME_TYPE, mimeType.get(), 714 requiredMimeType); 715 return false; 716 } 717 718 if (response->Type() != ResponseType::Basic && 719 response->Type() != ResponseType::Cors && 720 response->Type() != ResponseType::Default) { 721 return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_CORS_SAME_ORIGIN); 722 } 723 724 if (!response->Ok()) { 725 return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_STATUS); 726 } 727 728 if (response->BodyUsed()) { 729 return ThrowException(aCx, JSMSG_WASM_RESPONSE_ALREADY_CONSUMED); 730 } 731 732 switch (aMimeType) { 733 case JS::MimeType::Wasm: 734 nsAutoCString url; 735 response->GetUrl(url); 736 737 IgnoredErrorResult result; 738 nsCString sourceMapUrl; 739 response->GetInternalHeaders()->Get("SourceMap"_ns, sourceMapUrl, result); 740 if (NS_WARN_IF(result.Failed())) { 741 return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); 742 } 743 aConsumer->noteResponseURLs( 744 url.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get()); 745 break; 746 } 747 748 SafeRefPtr<InternalResponse> ir = response->GetInternalResponse(); 749 if (NS_WARN_IF(!ir)) { 750 return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); 751 } 752 753 nsCOMPtr<nsIInputStream> stream; 754 755 nsMainThreadPtrHandle<nsICacheInfoChannel> cache; 756 bool optimizedEncoding = false; 757 if (ir->HasCacheInfoChannel()) { 758 cache = ir->TakeCacheInfoChannel(); 759 760 nsAutoCString altDataType; 761 if (NS_SUCCEEDED(cache->GetAlternativeDataType(altDataType)) && 762 WasmAltDataType.Equals(altDataType)) { 763 optimizedEncoding = true; 764 rv = cache->GetAlternativeDataInputStream(getter_AddRefs(stream)); 765 if (NS_WARN_IF(NS_FAILED(rv))) { 766 return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); 767 } 768 if (ir->HasBeenCloned()) { 769 // If `Response` is cloned, clone alternative data stream instance. 770 // The cache entry does not clone automatically, and multiple 771 // JSStreamConsumer instances will collide during read if not cloned. 772 nsCOMPtr<nsICloneableInputStream> original = do_QueryInterface(stream); 773 if (NS_WARN_IF(!original)) { 774 return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); 775 } 776 rv = original->Clone(getter_AddRefs(stream)); 777 if (NS_WARN_IF(NS_FAILED(rv))) { 778 return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); 779 } 780 } 781 } 782 } 783 784 if (!optimizedEncoding) { 785 ir->GetUnfilteredBody(getter_AddRefs(stream)); 786 if (!stream) { 787 aConsumer->streamEnd(); 788 return true; 789 } 790 } 791 792 MOZ_ASSERT(stream); 793 794 IgnoredErrorResult error; 795 response->SetBodyUsed(aCx, error); 796 if (NS_WARN_IF(error.Failed())) { 797 return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); 798 } 799 800 nsIGlobalObject* global = xpc::NativeGlobal(js::UncheckedUnwrap(aObj)); 801 802 if (!JSStreamConsumer::Start(stream, global, aMaybeWorker, aConsumer, 803 std::move(cache), optimizedEncoding)) { 804 return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); 805 } 806 807 return true; 808 } 809 810 // static 811 void FetchUtil::ReportJSStreamError(JSContext* aCx, size_t aErrorCode) { 812 // For now, convert *all* errors into AbortError. 813 814 RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); 815 816 JS::Rooted<JS::Value> value(aCx); 817 if (!GetOrCreateDOMReflector(aCx, e, &value)) { 818 return; 819 } 820 821 JS_SetPendingException(aCx, value); 822 } 823 824 } // namespace mozilla::dom