diff options
Diffstat (limited to 'netwerk/protocol/http/InterceptedHttpChannel.cpp')
-rw-r--r-- | netwerk/protocol/http/InterceptedHttpChannel.cpp | 1631 |
1 files changed, 1631 insertions, 0 deletions
diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp new file mode 100644 index 0000000000..c493259905 --- /dev/null +++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp @@ -0,0 +1,1631 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "InterceptedHttpChannel.h" +#include "NetworkMarker.h" +#include "nsContentSecurityManager.h" +#include "nsEscape.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "nsHttpChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIRedirectResultListener.h" +#include "nsStringStream.h" +#include "nsStreamUtils.h" +#include "nsQueryObject.h" +#include "mozilla/Logging.h" + +namespace mozilla::net { + +mozilla::LazyLogModule gInterceptedLog("Intercepted"); + +#define INTERCEPTED_LOG(args) MOZ_LOG(gInterceptedLog, LogLevel::Debug, args) + +NS_IMPL_ISUPPORTS_INHERITED(InterceptedHttpChannel, HttpBaseChannel, + nsIInterceptedChannel, nsICacheInfoChannel, + nsIAsyncVerifyRedirectCallback, nsIRequestObserver, + nsIStreamListener, nsIThreadRetargetableRequest, + nsIThreadRetargetableStreamListener, + nsIClassOfService) + +InterceptedHttpChannel::InterceptedHttpChannel( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp) + : HttpAsyncAborter<InterceptedHttpChannel>(this), + mProgress(0), + mProgressReported(0), + mSynthesizedStreamLength(-1), + mResumeStartPos(0), + mCallingStatusAndProgress(false), + mTimeStamps() { + // Pre-set the creation and AsyncOpen times based on the original channel + // we are intercepting. We don't want our extra internal redirect to mask + // any time spent processing the channel. + INTERCEPTED_LOG(("Creating InterceptedHttpChannel [%p]", this)); + mChannelCreationTime = aCreationTime; + mChannelCreationTimestamp = aCreationTimestamp; + mInterceptedChannelCreationTimestamp = TimeStamp::Now(); + mAsyncOpenTime = aAsyncOpenTimestamp; +} + +void InterceptedHttpChannel::ReleaseListeners() { + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + } + HttpBaseChannel::ReleaseListeners(); + mSynthesizedResponseHead.reset(); + mRedirectChannel = nullptr; + mBodyReader = nullptr; + mReleaseHandle = nullptr; + mProgressSink = nullptr; + mBodyCallback = nullptr; + mPump = nullptr; + + MOZ_DIAGNOSTIC_ASSERT(!LoadIsPending()); +} + +nsresult InterceptedHttpChannel::SetupReplacementChannel( + nsIURI* aURI, nsIChannel* aChannel, bool aPreserveMethod, + uint32_t aRedirectFlags) { + INTERCEPTED_LOG( + ("InterceptedHttpChannel::SetupReplacementChannel [%p] flag: %u", this, + aRedirectFlags)); + nsresult rv = HttpBaseChannel::SetupReplacementChannel( + aURI, aChannel, aPreserveMethod, aRedirectFlags); + if (NS_FAILED(rv)) { + return rv; + } + + rv = CheckRedirectLimit(aRedirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // While we can't resume an synthetic response, we can still propagate + // the resume params across redirects for other channels to handle. + if (mResumeStartPos > 0) { + nsCOMPtr<nsIResumableChannel> resumable = do_QueryInterface(aChannel); + if (!resumable) { + return NS_ERROR_NOT_RESUMABLE; + } + + resumable->ResumeAt(mResumeStartPos, mResumeEntityId); + } + + return NS_OK; +} + +void InterceptedHttpChannel::AsyncOpenInternal() { + // We save this timestamp from outside of the if block in case we enable the + // profiler after AsyncOpen(). + INTERCEPTED_LOG(("InterceptedHttpChannel::AsyncOpenInternal [%p]", this)); + mLastStatusReported = TimeStamp::Now(); + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + } + + // If an error occurs in this file we must ensure mListener callbacks are + // invoked in some way. We either Cancel() or ResetInterception below + // depending on which path we take. + nsresult rv = NS_OK; + + // Start the interception, record the start time. + mTimeStamps.Init(this); + mTimeStamps.RecordTime(); + + // We should have pre-set the AsyncOpen time based on the original channel if + // timings are enabled. + if (LoadTimingEnabled()) { + MOZ_DIAGNOSTIC_ASSERT(!mAsyncOpenTime.IsNull()); + } + + StoreIsPending(true); + StoreResponseCouldBeSynthesized(true); + + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + + // If we already have a synthesized body then we are pre-synthesized. + // This can happen for two reasons: + // 1. We have a pre-synthesized redirect in e10s mode. In this case + // we should follow the redirect. + // 2. We are handling a "fake" redirect for an opaque response. Here + // we should just process the synthetic body. + if (mBodyReader) { + // If we fail in this path, then cancel the channel. We don't want + // to ResetInterception() after a synthetic result has already been + // produced by the ServiceWorker. + auto autoCancel = MakeScopeExit([&] { + if (NS_FAILED(rv)) { + Cancel(rv); + } + }); + + // The fetch event will not be dispatched, record current time for + // FetchHandlerStart and FetchHandlerFinish. + SetFetchHandlerStart(TimeStamp::Now()); + SetFetchHandlerFinish(TimeStamp::Now()); + + if (ShouldRedirect()) { + rv = FollowSyntheticRedirect(); + return; + } + + rv = StartPump(); + return; + } + + // If we fail the initial interception, then attempt to ResetInterception + // to fall back to network. We only cancel if the reset fails. + auto autoReset = MakeScopeExit([&] { + if (NS_FAILED(rv)) { + rv = ResetInterception(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + Cancel(rv); + } + } + }); + + // Otherwise we need to trigger a FetchEvent in a ServiceWorker. + nsCOMPtr<nsINetworkInterceptController> controller; + GetCallback(controller); + + if (NS_WARN_IF(!controller)) { + rv = NS_ERROR_DOM_INVALID_STATE_ERR; + return; + } + + rv = controller->ChannelIntercepted(this); + NS_ENSURE_SUCCESS_VOID(rv); +} + +bool InterceptedHttpChannel::ShouldRedirect() const { + // Determine if the synthetic response requires us to perform a real redirect. + return nsHttpChannel::WillRedirect(*mResponseHead) && + !mLoadInfo->GetDontFollowRedirects(); +} + +nsresult InterceptedHttpChannel::FollowSyntheticRedirect() { + // Perform a real redirect based on the synthetic response. + + nsCOMPtr<nsIIOService> ioService; + nsresult rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString location; + rv = mResponseHead->GetHeader(nsHttp::Location, location); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // make sure non-ASCII characters in the location header are escaped. + nsAutoCString locationBuf; + if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII | esc_Spaces, + locationBuf)) { + location = locationBuf; + } + + nsCOMPtr<nsIURI> redirectURI; + rv = ioService->NewURI(nsDependentCString(location.get()), nullptr, mURI, + getter_AddRefs(redirectURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_CORRUPTED_CONTENT); + + uint32_t redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + if (nsHttp::IsPermanentRedirect(mResponseHead->Status())) { + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + } + + PropagateReferenceIfNeeded(mURI, redirectURI); + + bool rewriteToGET = ShouldRewriteRedirectToGET(mResponseHead->Status(), + mRequestHead.ParsedMethod()); + + nsCOMPtr<nsIChannel> newChannel; + nsCOMPtr<nsILoadInfo> redirectLoadInfo = + CloneLoadInfoForRedirect(redirectURI, redirectFlags); + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), redirectURI, + redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + mLoadFlags, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(redirectURI, newChannel, !rewriteToGET, + redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + mRedirectChannel = std::move(newChannel); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, + redirectFlags); + + if (NS_WARN_IF(NS_FAILED(rv))) { + OnRedirectVerifyCallback(rv); + } else { + // Redirect success, record the finish time and the final status. + mTimeStamps.RecordTime(InterceptionTimeStamps::Redirected); + } + + return rv; +} + +nsresult InterceptedHttpChannel::RedirectForResponseURL( + nsIURI* aResponseURI, bool aResponseRedirected) { + // Perform a service worker redirect to another InterceptedHttpChannel using + // the given response URL. It allows content to see the final URL where + // appropriate and also helps us enforce cross-origin restrictions. The + // resulting channel will then process the synthetic response as normal. This + // extra redirect is performed so that listeners treat the result as unsafe + // cross-origin data. + + nsresult rv = NS_OK; + + // We want to pass ownership of the body callback to the new synthesized + // channel. We need to hold a reference to the callbacks on the stack + // as well, though, so we can call them if a failure occurs. + nsCOMPtr<nsIInterceptedBodyCallback> bodyCallback = std::move(mBodyCallback); + + RefPtr<InterceptedHttpChannel> newChannel = CreateForSynthesis( + mResponseHead.get(), mBodyReader, bodyCallback, mChannelCreationTime, + mChannelCreationTimestamp, mAsyncOpenTime); + + // If the response has been redirected, propagate all the URLs to content. + // Thus, the exact value of the redirect flag does not matter as long as it's + // not REDIRECT_INTERNAL. + uint32_t flags = aResponseRedirected ? nsIChannelEventSink::REDIRECT_TEMPORARY + : nsIChannelEventSink::REDIRECT_INTERNAL; + + nsCOMPtr<nsILoadInfo> redirectLoadInfo = + CloneLoadInfoForRedirect(aResponseURI, flags); + + ExtContentPolicyType contentPolicyType = + redirectLoadInfo->GetExternalContentPolicyType(); + + rv = newChannel->Init(aResponseURI, mCaps, + static_cast<nsProxyInfo*>(mProxyInfo.get()), + mProxyResolveFlags, mProxyURI, mChannelId, + contentPolicyType, redirectLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // Normally we don't propagate the LoadInfo's service worker tainting + // synthesis flag on redirect. A real redirect normally will want to allow + // normal tainting to proceed from its starting taint. For this particular + // redirect, though, we are performing a redirect to communicate the URL of + // the service worker synthetic response itself. This redirect still + // represents the synthetic response, so we must preserve the flag. + if (redirectLoadInfo && mLoadInfo && + mLoadInfo->GetServiceWorkerTaintingSynthesized()) { + redirectLoadInfo->SynthesizeServiceWorkerTainting(mLoadInfo->GetTainting()); + } + + rv = SetupReplacementChannel(aResponseURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + mRedirectChannel = newChannel; + + MOZ_ASSERT(mBodyReader); + MOZ_ASSERT(!LoadApplyConversion()); + newChannel->SetApplyConversion(false); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, flags); + + if (NS_FAILED(rv)) { + // Make sure to call the body callback since we took ownership + // above. Neither the new channel or our standard + // OnRedirectVerifyCallback() code will invoke the callback. Do it here. + bodyCallback->BodyComplete(rv); + + OnRedirectVerifyCallback(rv); + } + + return rv; +} + +nsresult InterceptedHttpChannel::StartPump() { + MOZ_DIAGNOSTIC_ASSERT(!mPump); + MOZ_DIAGNOSTIC_ASSERT(mBodyReader); + + // We don't support resuming an intercepted channel. We can't guarantee the + // ServiceWorker will always return the same data and we can't rely on the + // http cache code to detect changes. For now, just force the channel to + // NS_ERROR_NOT_RESUMABLE which should cause the front-end to recreate the + // channel without calling ResumeAt(). + // + // It would also be possible to convert this information to a range request, + // but its unclear if we should do that for ServiceWorker FetchEvents. See: + // + // https://github.com/w3c/ServiceWorker/issues/1201 + if (mResumeStartPos > 0) { + return NS_ERROR_NOT_RESUMABLE; + } + + // For progress we trust the content-length for the "maximum" size. + // We can't determine the full size from the stream itself since + // we may only receive the data incrementally. We can't trust + // Available() here. + // TODO: We could implement an nsIFixedLengthInputStream interface and + // QI to it here. This would let us determine the total length + // for streams that support it. See bug 1388774. + Unused << GetContentLength(&mSynthesizedStreamLength); + + nsresult rv = + nsInputStreamPump::Create(getter_AddRefs(mPump), mBodyReader, 0, 0, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mPump->AsyncRead(this); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) { + mPump->Suspend(); + } + + MOZ_DIAGNOSTIC_ASSERT(!mCanceled); + + return rv; +} + +nsresult InterceptedHttpChannel::OpenRedirectChannel() { + INTERCEPTED_LOG( + ("InterceptedHttpChannel::OpenRedirectChannel [%p], mRedirectChannel: %p", + this, mRedirectChannel.get())); + nsresult rv = NS_OK; + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + if (!mRedirectChannel) { + return NS_ERROR_DOM_ABORT_ERR; + } + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // open new channel + rv = mRedirectChannel->AsyncOpen(mListener); + NS_ENSURE_SUCCESS(rv, rv); + + mStatus = NS_BINDING_REDIRECTED; + + return rv; +} + +void InterceptedHttpChannel::MaybeCallStatusAndProgress() { + // OnStatus() and OnProgress() must only be called on the main thread. If + // we are on a separate thread, then we maybe need to schedule a runnable + // to call them asynchronousnly. + if (!NS_IsMainThread()) { + // Check to see if we are already trying to call OnStatus/OnProgress + // asynchronously. If we are, then don't queue up another runnable. + // We don't want to flood the main thread. + if (mCallingStatusAndProgress) { + return; + } + mCallingStatusAndProgress = true; + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "InterceptedHttpChannel::MaybeCallStatusAndProgress", this, + &InterceptedHttpChannel::MaybeCallStatusAndProgress); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + + // We are about to capture out progress position. Clear the flag we use + // to de-duplicate progress report runnables. We want any further progress + // updates to trigger another runnable. We do this before capture the + // progress value since we're using atomics and not a mutex lock. + mCallingStatusAndProgress = false; + + // Capture the current status from our atomic count. + int64_t progress = mProgress; + + MOZ_DIAGNOSTIC_ASSERT(progress >= mProgressReported); + + // Do nothing if we've already made the calls for this amount of progress + // or if the channel is not configured for these calls. Note, the check + // for mProgressSink here means we will not fire any spurious late calls + // after ReleaseListeners() is executed. + if (progress <= mProgressReported || mCanceled || !mProgressSink || + (mLoadFlags & HttpBaseChannel::LOAD_BACKGROUND)) { + return; + } + + // Capture the host name on the first set of calls to avoid doing this + // string processing repeatedly. + if (mProgressReported == 0) { + nsAutoCString host; + MOZ_ALWAYS_SUCCEEDS(mURI->GetHost(host)); + CopyUTF8toUTF16(host, mStatusHost); + } + + mProgressSink->OnStatus(this, NS_NET_STATUS_READING, mStatusHost.get()); + + mProgressSink->OnProgress(this, progress, mSynthesizedStreamLength); + + mProgressReported = progress; +} + +void InterceptedHttpChannel::MaybeCallBodyCallback() { + nsCOMPtr<nsIInterceptedBodyCallback> callback = std::move(mBodyCallback); + if (callback) { + callback->BodyComplete(mStatus); + } +} + +// static +already_AddRefed<InterceptedHttpChannel> +InterceptedHttpChannel::CreateForInterception( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp) { + // Create an InterceptedHttpChannel that will trigger a FetchEvent + // in a ServiceWorker when opened. + RefPtr<InterceptedHttpChannel> ref = new InterceptedHttpChannel( + aCreationTime, aCreationTimestamp, aAsyncOpenTimestamp); + + return ref.forget(); +} + +// static +already_AddRefed<InterceptedHttpChannel> +InterceptedHttpChannel::CreateForSynthesis( + const nsHttpResponseHead* aHead, nsIInputStream* aBody, + nsIInterceptedBodyCallback* aBodyCallback, PRTime aCreationTime, + const TimeStamp& aCreationTimestamp, const TimeStamp& aAsyncOpenTimestamp) { + MOZ_DIAGNOSTIC_ASSERT(aHead); + MOZ_DIAGNOSTIC_ASSERT(aBody); + + // Create an InterceptedHttpChannel that already has a synthesized response. + // The synthetic response will be processed when opened. A FetchEvent + // will not be triggered. + RefPtr<InterceptedHttpChannel> ref = new InterceptedHttpChannel( + aCreationTime, aCreationTimestamp, aAsyncOpenTimestamp); + + ref->mResponseHead = MakeUnique<nsHttpResponseHead>(*aHead); + ref->mBodyReader = aBody; + ref->mBodyCallback = aBodyCallback; + + return ref.forget(); +} + +NS_IMETHODIMP InterceptedHttpChannel::SetCanceledReason( + const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP InterceptedHttpChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP +InterceptedHttpChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +InterceptedHttpChannel::Cancel(nsresult aStatus) { + INTERCEPTED_LOG(("InterceptedHttpChannel::Cancel [%p]", this)); + // Note: This class has been designed to send all error results through + // Cancel(). Don't add calls directly to AsyncAbort() or + // DoNotifyListener(). Instead call Cancel(). + + if (mCanceled) { + return NS_OK; + } + + // The interception is canceled, record the finish time stamp and the final + // status + mTimeStamps.RecordTime(InterceptionTimeStamps::Canceled); + + mCanceled = true; + + if (mLastStatusReported && profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + // mLastStatusReported can be null if Cancel is called before we added the + // start marker. + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_CANCEL, + mLastStatusReported, TimeStamp::Now(), size, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource)); + } + + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(aStatus)); + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + if (mPump) { + return mPump->Cancel(mStatus); + } + + return AsyncAbort(mStatus); +} + +NS_IMETHODIMP +InterceptedHttpChannel::Suspend(void) { + ++mSuspendCount; + if (mPump) { + return mPump->Suspend(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::Resume(void) { + --mSuspendCount; + if (mPump) { + return mPump->Resume(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + nsCOMPtr<nsITransportSecurityInfo> ref(mSecurityInfo); + ref.forget(aSecurityInfo); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + INTERCEPTED_LOG(("InterceptedHttpChannel::AsyncOpen [%p], listener: %p", this, + aListener)); + nsCOMPtr<nsIStreamListener> listener(aListener); + + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + Cancel(rv); + return rv; + } + if (mCanceled) { + return mStatus; + } + + // After this point we should try to return NS_OK and notify the listener + // of the result. + mListener = aListener; + + AsyncOpenInternal(); + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory, + bool aIsWarning) { + // Synthetic responses should not trigger CORS blocking. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetPriority(int32_t aPriority) { + mPriority = clamped<int32_t>(aPriority, INT16_MIN, INT16_MAX); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetClassFlags(uint32_t aClassFlags) { + mClassOfService.SetFlags(aClassFlags); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::ClearClassFlags(uint32_t aClassFlags) { + mClassOfService.SetFlags(~aClassFlags & mClassOfService.Flags()); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AddClassFlags(uint32_t aClassFlags) { + mClassOfService.SetFlags(aClassFlags | mClassOfService.Flags()); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetClassOfService(ClassOfService cos) { + mClassOfService = cos; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetIncremental(bool incremental) { + mClassOfService.SetIncremental(incremental); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::ResumeAt(uint64_t aStartPos, + const nsACString& aEntityId) { + // We don't support resuming synthesized responses, but we do track this + // information so it can be passed on to the resulting nsHttpChannel if + // ResetInterception is called. + mResumeStartPos = aStartPos; + mResumeEntityId = aEntityId; + return NS_OK; +} + +void InterceptedHttpChannel::DoNotifyListenerCleanup() { + // Prefer to cleanup in ReleaseListeners() as it seems to be called + // more consistently in necko. +} + +void InterceptedHttpChannel::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); +} + +namespace { + +class ResetInterceptionHeaderVisitor final : public nsIHttpHeaderVisitor { + nsCOMPtr<nsIHttpChannel> mTarget; + + ~ResetInterceptionHeaderVisitor() = default; + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + // We skip Cookie header here, since it will be added during + // nsHttpChannel::AsyncOpen. + if (aHeader.Equals(nsHttp::Cookie)) { + return NS_OK; + } + if (aValue.IsEmpty()) { + return mTarget->SetEmptyRequestHeader(aHeader); + } + return mTarget->SetRequestHeader(aHeader, aValue, false /* merge */); + } + + public: + explicit ResetInterceptionHeaderVisitor(nsIHttpChannel* aTarget) + : mTarget(aTarget) { + MOZ_DIAGNOSTIC_ASSERT(mTarget); + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(ResetInterceptionHeaderVisitor, nsIHttpHeaderVisitor) + +} // anonymous namespace + +NS_IMETHODIMP +InterceptedHttpChannel::ResetInterception(bool aBypass) { + INTERCEPTED_LOG(("InterceptedHttpChannel::ResetInterception [%p] bypass: %s", + this, aBypass ? "true" : "false")); + if (mCanceled) { + return mStatus; + } + + mInterceptionReset = true; + + uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL; + + nsCOMPtr<nsIChannel> newChannel; + nsCOMPtr<nsILoadInfo> redirectLoadInfo = + CloneLoadInfoForRedirect(mURI, flags); + + if (aBypass) { + redirectLoadInfo->ClearController(); + // TODO: Audit whether we should also be calling + // ServiceWorkerManager::StopControllingClient for maximum correctness. + } + + nsresult rv = + NS_NewChannelInternal(getter_AddRefs(newChannel), mURI, redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + mLoadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + + RefPtr<HttpBaseChannel> newBaseChannel = do_QueryObject(newChannel); + MOZ_ASSERT(newBaseChannel, + "The redirect channel should be a base channel."); + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, + NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), + size, kCacheUnknown, mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get())), mURI, flags, + newBaseChannel->ChannelId()); + } + + rv = SetupReplacementChannel(mURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + // Restore the non-default headers for fallback channel. + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(newChannel)); + nsCOMPtr<nsIHttpHeaderVisitor> visitor = + new ResetInterceptionHeaderVisitor(httpChannel); + rv = VisitNonDefaultRequestHeaders(visitor); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsITimedChannel> newTimedChannel = do_QueryInterface(newChannel); + if (newTimedChannel) { + if (!mAsyncOpenTime.IsNull()) { + newTimedChannel->SetAsyncOpen(mAsyncOpenTime); + } + if (!mChannelCreationTimestamp.IsNull()) { + newTimedChannel->SetChannelCreation(mChannelCreationTimestamp); + } + } + + if (mRedirectMode != nsIHttpChannelInternal::REDIRECT_MODE_MANUAL) { + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + rv = newChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + rv = newChannel->SetLoadFlags(loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + + mRedirectChannel = std::move(newChannel); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, flags); + + if (NS_FAILED(rv)) { + OnRedirectVerifyCallback(rv); + } else { + // ResetInterception success, record the finish time stamps and the final + // status. + mTimeStamps.RecordTime(InterceptionTimeStamps::Reset); + } + + return rv; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SynthesizeStatus(uint16_t aStatus, + const nsACString& aReason) { + if (mCanceled) { + return mStatus; + } + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + nsAutoCString statusLine; + statusLine.AppendLiteral("HTTP/1.1 "); + statusLine.AppendInt(aStatus); + statusLine.AppendLiteral(" "); + statusLine.Append(aReason); + + mSynthesizedResponseHead->ParseStatusLine(statusLine); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SynthesizeHeader(const nsACString& aName, + const nsACString& aValue) { + if (mCanceled) { + return mStatus; + } + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + nsAutoCString header = aName + ": "_ns + aValue; + // Overwrite any existing header. + nsresult rv = mSynthesizedResponseHead->ParseHeaderLine(header); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::StartSynthesizedResponse( + nsIInputStream* aBody, nsIInterceptedBodyCallback* aBodyCallback, + nsICacheInfoChannel* aSynthesizedCacheInfo, const nsACString& aFinalURLSpec, + bool aResponseRedirected) { + nsresult rv = NS_OK; + + auto autoCleanup = MakeScopeExit([&] { + // Auto-cancel on failure. Do this first to get mStatus set, if necessary. + if (NS_FAILED(rv)) { + Cancel(rv); + } + + // If we early exit before taking ownership of the body, then automatically + // invoke the callback. This could be due to an error or because we're not + // going to consume it due to a redirect, etc. + if (aBodyCallback) { + aBodyCallback->BodyComplete(mStatus); + } + }); + + if (NS_FAILED(mStatus)) { + // Return NS_OK. The channel should fire callbacks with an error code + // if it was cancelled before this point. + return NS_OK; + } + + // Take ownership of the body callbacks If a failure occurs we will + // automatically Cancel() the channel. This will then invoke OnStopRequest() + // which will invoke the correct callback. In the case of an opaque response + // redirect we pass ownership of the callback to the new channel. + mBodyCallback = aBodyCallback; + aBodyCallback = nullptr; + + mSynthesizedCacheInfo = aSynthesizedCacheInfo; + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + mResponseHead = std::move(mSynthesizedResponseHead); + + if (ShouldRedirect()) { + rv = FollowSyntheticRedirect(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + // Intercepted responses should already be decoded. + SetApplyConversion(false); + + // Errors and redirects may not have a body. Synthesize an empty string + // stream here so later code can be simpler. + mBodyReader = aBody; + if (!mBodyReader) { + rv = NS_NewCStringInputStream(getter_AddRefs(mBodyReader), ""_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIURI> responseURI; + if (!aFinalURLSpec.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec); + NS_ENSURE_SUCCESS(rv, rv); + } else { + responseURI = mURI; + } + + bool equal = false; + Unused << mURI->Equals(responseURI, &equal); + if (!equal) { + rv = RedirectForResponseURL(responseURI, aResponseRedirected); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + rv = StartPump(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::FinishSynthesizedResponse() { + if (mCanceled) { + // Return NS_OK. The channel should fire callbacks with an error code + // if it was cancelled before this point. + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::CancelInterception(nsresult aStatus) { + return Cancel(aStatus); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetChannel(nsIChannel** aChannel) { + nsCOMPtr<nsIChannel> ref(this); + ref.forget(aChannel); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetSecureUpgradedChannelURI( + nsIURI** aSecureUpgradedChannelURI) { + nsCOMPtr<nsIURI> ref(mURI); + ref.forget(aSecureUpgradedChannelURI); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetChannelInfo( + mozilla::dom::ChannelInfo* aChannelInfo) { + return aChannelInfo->ResurrectInfoOnChannel(this); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetInternalContentPolicyType( + nsContentPolicyType* aPolicyType) { + if (mLoadInfo) { + *aPolicyType = mLoadInfo->InternalContentPolicyType(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetConsoleReportCollector( + nsIConsoleReportCollector** aConsoleReportCollector) { + nsCOMPtr<nsIConsoleReportCollector> ref(this); + ref.forget(aConsoleReportCollector); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetFetchHandlerStart(TimeStamp aTimeStamp) { + mTimeStamps.RecordTime(std::move(aTimeStamp)); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetFetchHandlerFinish(TimeStamp aTimeStamp) { + mTimeStamps.RecordTime(std::move(aTimeStamp)); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetIsReset(bool* aResult) { + *aResult = mInterceptionReset; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetReleaseHandle(nsISupports* aHandle) { + mReleaseHandle = aHandle; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnRedirectVerifyCallback(nsresult rv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(rv)) { + rv = OpenRedirectChannel(); + } + + nsCOMPtr<nsIRedirectResultListener> hook; + GetCallback(hook); + if (hook) { + hook->OnRedirectResult(rv); + } + + if (NS_FAILED(rv)) { + Cancel(rv); + } + + MaybeCallBodyCallback(); + + StoreIsPending(false); + // We can only release listeners after the redirected channel really owns + // mListener. Otherwise, the OnStart/OnStopRequest functions of mListener will + // not be called. + if (NS_SUCCEEDED(rv)) { + ReleaseListeners(); + } + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnStartRequest(nsIRequest* aRequest) { + INTERCEPTED_LOG(("InterceptedHttpChannel::OnStartRequest [%p]", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mProgressSink) { + GetCallback(mProgressSink); + } + + MOZ_ASSERT_IF(!mLoadInfo->GetServiceWorkerTaintingSynthesized(), + mLoadInfo->GetLoadingPrincipal()); + // No need to do ORB checks if these conditions hold. + MOZ_DIAGNOSTIC_ASSERT(mLoadInfo->GetServiceWorkerTaintingSynthesized() || + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()); + + if (mPump && mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { + mPump->PeekStream(CallTypeSniffers, static_cast<nsIChannel*>(this)); + } + + nsresult rv = ProcessCrossOriginEmbedderPolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + Cancel(mStatus); + } + + rv = ProcessCrossOriginResourcePolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_DOM_CORP_FAILED; + Cancel(mStatus); + } + + rv = ComputeCrossOriginOpenerPolicyMismatch(); + if (rv == NS_ERROR_BLOCKED_BY_POLICY) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + Cancel(mStatus); + } + + rv = ValidateMIMEType(); + if (NS_FAILED(rv)) { + mStatus = rv; + Cancel(mStatus); + } + + StoreOnStartRequestCalled(true); + if (mListener) { + return mListener->OnStartRequest(this); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + INTERCEPTED_LOG(("InterceptedHttpChannel::OnStopRequest [%p]", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + MaybeCallBodyCallback(); + + mTimeStamps.RecordTime(InterceptionTimeStamps::Synthesized); + + // Its possible that we have any async runnable queued to report some + // progress when OnStopRequest() is triggered. Report any left over + // progress immediately. The extra runnable will then do nothing thanks + // to the ReleaseListeners() call below. + MaybeCallStatusAndProgress(); + + StoreIsPending(false); + + // Register entry to the PerformanceStorage resource timing + MaybeReportTimingData(); + + if (profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, + mLastStatusReported, TimeStamp::Now(), size, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } + + nsresult rv = NS_OK; + if (mListener) { + rv = mListener->OnStopRequest(this, mStatus); + } + + gHttpHandler->OnStopRequest(this); + + ReleaseListeners(); + + return rv; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // Any thread if the channel has been retargeted. + + if (mCanceled || !mListener) { + // If there is no listener, we still need to drain the stream in order + // maintain necko invariants. + uint32_t unused = 0; + aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &unused); + return mStatus; + } + if (mProgressSink) { + if (!(mLoadFlags & HttpBaseChannel::LOAD_BACKGROUND)) { + mProgress = aOffset + aCount; + MaybeCallStatusAndProgress(); + } + } + + return mListener->OnDataAvailable(this, aInputStream, aOffset, aCount); +} + +NS_IMETHODIMP +InterceptedHttpChannel::RetargetDeliveryTo(nsISerialEventTarget* aNewTarget) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aNewTarget); + + // If retargeting to the main thread, do nothing. + if (aNewTarget->IsOnCurrentThread()) { + return NS_OK; + } + + // Retargeting is only valid during OnStartRequest for nsIChannels. So + // we should only be called if we have a pump. + if (!mPump) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mPump->RetargetDeliveryTo(aNewTarget); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetDeliveryTarget(nsISerialEventTarget** aEventTarget) { + if (!mPump) { + return NS_ERROR_NOT_AVAILABLE; + } + return mPump->GetDeliveryTarget(aEventTarget); +} + +NS_IMETHODIMP +InterceptedHttpChannel::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = NS_OK; + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener = + do_QueryInterface(mListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + return rv; +} + +//----------------------------------------------------------------------------- +// InterceptedHttpChannel::nsICacheInfoChannel +//----------------------------------------------------------------------------- +// InterceptedHttpChannel does not really implement the nsICacheInfoChannel +// interface, we tranfers parameters to the saved +// nsICacheInfoChannel(mSynthesizedCacheInfo) from StartSynthesizedResponse. And +// we return false in IsFromCache and NS_ERROR_NOT_AVAILABLE for all other +// methods while the saved mSynthesizedCacheInfo does not exist. +NS_IMETHODIMP +InterceptedHttpChannel::IsFromCache(bool* value) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->IsFromCache(value); + } + *value = false; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::IsRacing(bool* value) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->IsRacing(value); + } + *value = false; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheEntryId(uint64_t* aCacheEntryId) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheEntryId(aCacheEntryId); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheTokenFetchCount(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheTokenFetchCount(_retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheTokenExpirationTime(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheTokenExpirationTime(_retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetAllowStaleCacheContent( + bool aAllowStaleCacheContent) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetAllowStaleCacheContent( + aAllowStaleCacheContent); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAllowStaleCacheContent( + bool* aAllowStaleCacheContent) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAllowStaleCacheContent( + aAllowStaleCacheContent); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetForceValidateCacheContent( + bool aForceValidateCacheContent) { + // We store aForceValidateCacheContent locally because + // mSynthesizedCacheInfo isn't present until a response + // is actually synthesized, which is too late for the value + // to be forwarded during the redirect to the intercepted + // channel. + StoreForceValidateCacheContent(aForceValidateCacheContent); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetForceValidateCacheContent( + aForceValidateCacheContent); + } + return NS_OK; +} +NS_IMETHODIMP +InterceptedHttpChannel::GetForceValidateCacheContent( + bool* aForceValidateCacheContent) { + *aForceValidateCacheContent = LoadForceValidateCacheContent(); +#ifdef DEBUG + if (mSynthesizedCacheInfo) { + bool synthesizedForceValidateCacheContent; + mSynthesizedCacheInfo->GetForceValidateCacheContent( + &synthesizedForceValidateCacheContent); + MOZ_ASSERT(*aForceValidateCacheContent == + synthesizedForceValidateCacheContent); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetPreferCacheLoadOverBypass( + bool* aPreferCacheLoadOverBypass) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetPreferCacheLoadOverBypass( + aPreferCacheLoadOverBypass); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetPreferCacheLoadOverBypass( + bool aPreferCacheLoadOverBypass) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetPreferCacheLoadOverBypass( + aPreferCacheLoadOverBypass); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::PreferAlternativeDataType( + const nsACString& aType, const nsACString& aContentType, + PreferredAlternativeDataDeliveryType aDeliverAltData) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + mPreferredCachedAltDataTypes.AppendElement(PreferredAlternativeDataTypeParams( + nsCString(aType), nsCString(aContentType), aDeliverAltData)); + return NS_OK; +} + +const nsTArray<PreferredAlternativeDataTypeParams>& +InterceptedHttpChannel::PreferredAlternativeDataTypes() { + return mPreferredCachedAltDataTypes; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAlternativeDataType(nsACString& aType) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAlternativeDataType(aType); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->OpenAlternativeOutputStream( + type, predictedSize, _retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetOriginalInputStream( + nsIInputStreamReceiver* aReceiver) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetOriginalInputStream(aReceiver); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAlternativeDataInputStream( + nsIInputStream** aInputStream) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAlternativeDataInputStream(aInputStream); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheKey(uint32_t* key) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheKey(key); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetCacheKey(uint32_t key) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetCacheKey(key); + } + return NS_ERROR_NOT_AVAILABLE; +} + +// InterceptionTimeStamps implementation +InterceptedHttpChannel::InterceptionTimeStamps::InterceptionTimeStamps() + : mStage(InterceptedHttpChannel::InterceptionTimeStamps::InterceptionStart), + mStatus(InterceptedHttpChannel::InterceptionTimeStamps::Created) {} + +void InterceptedHttpChannel::InterceptionTimeStamps::Init( + nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(mStatus == Created); + + mStatus = Initialized; + + mIsNonSubresourceRequest = nsContentUtils::IsNonSubresourceRequest(aChannel); + mKey = mIsNonSubresourceRequest ? "navigation"_ns : "subresource"_ns; + nsCOMPtr<nsIInterceptedChannel> interceptedChannel = + do_QueryInterface(aChannel); + // It must be a InterceptedHttpChannel + MOZ_ASSERT(interceptedChannel); + if (!mIsNonSubresourceRequest) { + interceptedChannel->GetSubresourceTimeStampKey(aChannel, mSubresourceKey); + } +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTime( + InterceptedHttpChannel::InterceptionTimeStamps::Status&& aStatus, + TimeStamp&& aTimeStamp) { + // Only allow passing Synthesized, Reset, Redirected, and Canceled in this + // method. + MOZ_ASSERT(aStatus == Synthesized || aStatus == Reset || + aStatus == Canceled || aStatus == Redirected); + if (mStatus == Canceled) { + return; + } + + // If current status is not Initialized, only Canceled can be recorded. + // That means it is canceled after other operation is done, ex. synthesized. + MOZ_ASSERT(mStatus == Initialized || aStatus == Canceled); + + switch (mStatus) { + case Initialized: + mStatus = aStatus; + break; + case Synthesized: + mStatus = CanceledAfterSynthesized; + break; + case Reset: + mStatus = CanceledAfterReset; + break; + case Redirected: + mStatus = CanceledAfterRedirected; + break; + // Channel is cancelled before calling AsyncOpenInternal(), no need to + // record the cancel time stamp. + case Created: + return; + default: + MOZ_ASSERT(false); + break; + } + + RecordTimeInternal(std::move(aTimeStamp)); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTime( + TimeStamp&& aTimeStamp) { + MOZ_ASSERT(mStatus == Initialized || mStatus == Canceled); + if (mStatus == Canceled) { + return; + } + RecordTimeInternal(std::move(aTimeStamp)); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTimeInternal( + TimeStamp&& aTimeStamp) { + MOZ_ASSERT(mStatus != Created); + + if (mStatus == Canceled && mStage != InterceptionFinish) { + mFetchHandlerStart = aTimeStamp; + mFetchHandlerFinish = aTimeStamp; + mStage = InterceptionFinish; + } + + switch (mStage) { + case InterceptionStart: { + MOZ_ASSERT(mInterceptionStart.IsNull()); + mInterceptionStart = aTimeStamp; + mStage = FetchHandlerStart; + break; + } + case (FetchHandlerStart): { + MOZ_ASSERT(mFetchHandlerStart.IsNull()); + mFetchHandlerStart = aTimeStamp; + mStage = FetchHandlerFinish; + break; + } + case (FetchHandlerFinish): { + MOZ_ASSERT(mFetchHandlerFinish.IsNull()); + mFetchHandlerFinish = aTimeStamp; + mStage = InterceptionFinish; + break; + } + case InterceptionFinish: { + mInterceptionFinish = aTimeStamp; + SaveTimeStamps(); + return; + } + default: { + return; + } + } +} + +void InterceptedHttpChannel::InterceptionTimeStamps::GenKeysWithStatus( + nsCString& aKey, nsCString& aSubresourceKey) { + nsAutoCString statusString; + switch (mStatus) { + case Synthesized: + statusString = "synthesized"_ns; + break; + case Reset: + statusString = "reset"_ns; + break; + case Redirected: + statusString = "redirected"_ns; + break; + case Canceled: + statusString = "canceled"_ns; + break; + case CanceledAfterSynthesized: + statusString = "canceled-after-synthesized"_ns; + break; + case CanceledAfterReset: + statusString = "canceled-after-reset"_ns; + break; + case CanceledAfterRedirected: + statusString = "canceled-after-redirected"_ns; + break; + default: + return; + } + aKey = mKey; + aSubresourceKey = mSubresourceKey; + aKey.AppendLiteral("_"); + aSubresourceKey.AppendLiteral("_"); + aKey.Append(statusString); + aSubresourceKey.Append(statusString); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::SaveTimeStamps() { + MOZ_ASSERT(mStatus != Initialized && mStatus != Created); + + if (mStatus == Synthesized || mStatus == Reset) { + Telemetry::HistogramID id = + Telemetry::SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS_2; + if (mStatus == Reset) { + id = Telemetry::SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS_2; + } + + Telemetry::Accumulate( + id, mKey, + static_cast<uint32_t>( + (mInterceptionFinish - mFetchHandlerFinish).ToMilliseconds())); + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + id, mSubresourceKey, + static_cast<uint32_t>( + (mInterceptionFinish - mFetchHandlerFinish).ToMilliseconds())); + } + } + + if (!mFetchHandlerStart.IsNull()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2, mKey, + static_cast<uint32_t>( + (mFetchHandlerStart - mInterceptionStart).ToMilliseconds())); + + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2, mSubresourceKey, + static_cast<uint32_t>( + (mFetchHandlerStart - mInterceptionStart).ToMilliseconds())); + } + } + + nsAutoCString key, subresourceKey; + GenKeysWithStatus(key, subresourceKey); + + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2, key, + static_cast<uint32_t>( + (mInterceptionFinish - mInterceptionStart).ToMilliseconds())); + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2, + subresourceKey, + static_cast<uint32_t>( + (mInterceptionFinish - mInterceptionStart).ToMilliseconds())); + } +} + +} // namespace mozilla::net |