summaryrefslogtreecommitdiffstats
path: root/netwerk/protocol/http/OpaqueResponseUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--netwerk/protocol/http/OpaqueResponseUtils.cpp667
1 files changed, 667 insertions, 0 deletions
diff --git a/netwerk/protocol/http/OpaqueResponseUtils.cpp b/netwerk/protocol/http/OpaqueResponseUtils.cpp
new file mode 100644
index 0000000000..0a9fb50ef8
--- /dev/null
+++ b/netwerk/protocol/http/OpaqueResponseUtils.cpp
@@ -0,0 +1,667 @@
+/* -*- 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/net/OpaqueResponseUtils.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/dom/JSValidatorParent.h"
+#include "ErrorList.h"
+#include "nsContentUtils.h"
+#include "nsHttpResponseHead.h"
+#include "nsISupports.h"
+#include "nsMimeTypes.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "nsStringStream.h"
+#include "HttpBaseChannel.h"
+
+static mozilla::LazyLogModule gORBLog("ORB");
+
+#define LOGORB(msg, ...) \
+ MOZ_LOG(gORBLog, LogLevel::Debug, \
+ ("%s: %p " msg, __func__, this, ##__VA_ARGS__))
+
+namespace mozilla::net {
+
+static bool IsOpaqueSafeListedMIMEType(const nsACString& aContentType) {
+ if (aContentType.EqualsLiteral(TEXT_CSS) ||
+ aContentType.EqualsLiteral(IMAGE_SVG_XML)) {
+ return true;
+ }
+
+ NS_ConvertUTF8toUTF16 typeString(aContentType);
+ return nsContentUtils::IsJavascriptMIMEType(typeString);
+}
+
+// These need to be kept in sync with
+// "browser.opaqueResponseBlocking.mediaExceptionsStrategy"
+enum class OpaqueResponseMediaException { NoExceptions, AllowSome, AllowAll };
+
+static OpaqueResponseMediaException ConfiguredMediaExceptionsStrategy() {
+ uint32_t pref = StaticPrefs::
+ browser_opaqueResponseBlocking_mediaExceptionsStrategy_DoNotUseDirectly();
+ if (NS_WARN_IF(pref > static_cast<uint32_t>(
+ OpaqueResponseMediaException::AllowAll))) {
+ return OpaqueResponseMediaException::AllowAll;
+ }
+
+ return static_cast<OpaqueResponseMediaException>(pref);
+}
+
+static bool IsOpaqueSafeListedSpecBreakingMIMEType(
+ const nsACString& aContentType, bool aNoSniff) {
+ // Avoid trouble with DASH/HLS. See bug 1698040.
+ if (aContentType.EqualsLiteral(APPLICATION_DASH_XML) ||
+ aContentType.EqualsLiteral(APPLICATION_MPEGURL) ||
+ aContentType.EqualsLiteral(AUDIO_MPEG_URL) ||
+ aContentType.EqualsLiteral(TEXT_VTT)) {
+ return true;
+ }
+
+ // Do what Chromium does. This is from bug 1828375, and we should ideally
+ // revert this.
+ if (aContentType.EqualsLiteral(TEXT_PLAIN) && aNoSniff) {
+ return true;
+ }
+
+ switch (ConfiguredMediaExceptionsStrategy()) {
+ case OpaqueResponseMediaException::NoExceptions:
+ break;
+ case OpaqueResponseMediaException::AllowSome:
+ if (aContentType.EqualsLiteral(AUDIO_MP3) ||
+ aContentType.EqualsLiteral(AUDIO_AAC) ||
+ aContentType.EqualsLiteral(AUDIO_AACP)) {
+ return true;
+ }
+ break;
+ case OpaqueResponseMediaException::AllowAll:
+ if (StringBeginsWith(aContentType, "audio/"_ns) ||
+ StringBeginsWith(aContentType, "video/"_ns)) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
+
+static bool IsOpaqueBlockListedMIMEType(const nsACString& aContentType) {
+ return aContentType.EqualsLiteral(TEXT_HTML) ||
+ StringEndsWith(aContentType, "+json"_ns) ||
+ aContentType.EqualsLiteral(APPLICATION_JSON) ||
+ aContentType.EqualsLiteral(TEXT_JSON) ||
+ StringEndsWith(aContentType, "+xml"_ns) ||
+ aContentType.EqualsLiteral(APPLICATION_XML) ||
+ aContentType.EqualsLiteral(TEXT_XML);
+}
+
+static bool IsOpaqueBlockListedNeverSniffedMIMEType(
+ const nsACString& aContentType) {
+ return aContentType.EqualsLiteral(APPLICATION_GZIP2) ||
+ aContentType.EqualsLiteral(APPLICATION_MSEXCEL) ||
+ aContentType.EqualsLiteral(APPLICATION_MSPPT) ||
+ aContentType.EqualsLiteral(APPLICATION_MSWORD) ||
+ aContentType.EqualsLiteral(APPLICATION_MSWORD_TEMPLATE) ||
+ aContentType.EqualsLiteral(APPLICATION_PDF) ||
+ aContentType.EqualsLiteral(APPLICATION_MPEGURL) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKPOINT) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKSHEET) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKWORD) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL2) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT2) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD2) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD3) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_MSWORD) ||
+ aContentType.EqualsLiteral(
+ APPLICATION_VND_PRESENTATIONML_PRESENTATION) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATIONML_TEMPLATE) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_SHEET) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_TEMPLATE) ||
+ aContentType.EqualsLiteral(
+ APPLICATION_VND_WORDPROCESSINGML_DOCUMENT) ||
+ aContentType.EqualsLiteral(
+ APPLICATION_VND_WORDPROCESSINGML_TEMPLATE) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXML) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXMLM) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEET_OPENXML) ||
+ aContentType.EqualsLiteral(APPLICATION_VND_WORDPROSSING_OPENXML) ||
+ aContentType.EqualsLiteral(APPLICATION_GZIP) ||
+ aContentType.EqualsLiteral(APPLICATION_XPROTOBUF) ||
+ aContentType.EqualsLiteral(APPLICATION_XPROTOBUFFER) ||
+ aContentType.EqualsLiteral(APPLICATION_ZIP) ||
+ aContentType.EqualsLiteral(AUDIO_MPEG_URL) ||
+ aContentType.EqualsLiteral(MULTIPART_BYTERANGES) ||
+ aContentType.EqualsLiteral(MULTIPART_SIGNED) ||
+ aContentType.EqualsLiteral(TEXT_EVENT_STREAM) ||
+ aContentType.EqualsLiteral(TEXT_CSV) ||
+ aContentType.EqualsLiteral(TEXT_VTT) ||
+ aContentType.EqualsLiteral(APPLICATION_DASH_XML);
+}
+
+OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason(
+ const nsACString& aContentType, uint16_t aStatus, bool aNoSniff) {
+ if (aContentType.IsEmpty()) {
+ return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF;
+ }
+
+ if (IsOpaqueSafeListedMIMEType(aContentType)) {
+ return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED;
+ }
+
+ // For some MIME types we deviate from spec and allow when we ideally
+ // shouldn't. These are returnened before any blocking takes place.
+ if (IsOpaqueSafeListedSpecBreakingMIMEType(aContentType, aNoSniff)) {
+ return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED_SPEC_BREAKING;
+ }
+
+ if (IsOpaqueBlockListedNeverSniffedMIMEType(aContentType)) {
+ return OpaqueResponseBlockedReason::BLOCKED_BLOCKLISTED_NEVER_SNIFFED;
+ }
+
+ if (aStatus == 206 && IsOpaqueBlockListedMIMEType(aContentType)) {
+ return OpaqueResponseBlockedReason::BLOCKED_206_AND_BLOCKLISTED;
+ }
+
+ nsAutoCString contentTypeOptionsHeader;
+ if (aNoSniff && (IsOpaqueBlockListedMIMEType(aContentType) ||
+ aContentType.EqualsLiteral(TEXT_PLAIN))) {
+ return OpaqueResponseBlockedReason::
+ BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN;
+ }
+
+ return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF;
+}
+
+OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason(
+ nsHttpResponseHead& aResponseHead) {
+ nsAutoCString contentType;
+ aResponseHead.ContentType(contentType);
+
+ nsAutoCString contentTypeOptionsHeader;
+ bool nosniff =
+ aResponseHead.GetContentTypeOptionsHeader(contentTypeOptionsHeader) &&
+ contentTypeOptionsHeader.EqualsIgnoreCase("nosniff");
+
+ return GetOpaqueResponseBlockedReason(contentType, aResponseHead.Status(),
+ nosniff);
+}
+
+Result<std::tuple<int64_t, int64_t, int64_t>, nsresult>
+ParseContentRangeHeaderString(const nsAutoCString& aRangeStr) {
+ // Parse the range header: e.g. Content-Range: bytes 7000-7999/8000.
+ const int32_t spacePos = aRangeStr.Find(" "_ns);
+ const int32_t dashPos = aRangeStr.Find("-"_ns, spacePos);
+ const int32_t slashPos = aRangeStr.Find("/"_ns, dashPos);
+
+ nsAutoCString rangeStartText;
+ aRangeStr.Mid(rangeStartText, spacePos + 1, dashPos - (spacePos + 1));
+
+ nsresult rv;
+ const int64_t rangeStart = rangeStartText.ToInteger64(&rv);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ if (0 > rangeStart) {
+ return Err(NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ nsAutoCString rangeEndText;
+ aRangeStr.Mid(rangeEndText, dashPos + 1, slashPos - (dashPos + 1));
+ const int64_t rangeEnd = rangeEndText.ToInteger64(&rv);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ if (rangeStart > rangeEnd) {
+ return Err(NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ nsAutoCString rangeTotalText;
+ aRangeStr.Right(rangeTotalText, aRangeStr.Length() - (slashPos + 1));
+ if (rangeTotalText[0] == '*') {
+ return std::make_tuple(rangeStart, rangeEnd, (int64_t)-1);
+ }
+
+ const int64_t rangeTotal = rangeTotalText.ToInteger64(&rv);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ if (rangeEnd >= rangeTotal) {
+ return Err(NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ return std::make_tuple(rangeStart, rangeEnd, rangeTotal);
+}
+
+bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead) {
+ MOZ_ASSERT(aResponseHead.Status() == 206);
+
+ nsAutoCString contentRange;
+ Unused << aResponseHead.GetHeader(nsHttp::Content_Range, contentRange);
+
+ auto rangeOrErr = ParseContentRangeHeaderString(contentRange);
+ if (rangeOrErr.isErr()) {
+ return false;
+ }
+
+ const int64_t responseFirstBytePos = std::get<0>(rangeOrErr.unwrap());
+ return responseFirstBytePos == 0;
+}
+
+LogModule* GetORBLog() { return gORBLog; }
+
+OpaqueResponseFilter::OpaqueResponseFilter(nsIStreamListener* aNext)
+ : mNext(aNext) {
+ LOGORB();
+}
+
+NS_IMETHODIMP
+OpaqueResponseFilter::OnStartRequest(nsIRequest* aRequest) {
+ LOGORB();
+ nsCOMPtr<HttpBaseChannel> httpBaseChannel = do_QueryInterface(aRequest);
+ MOZ_ASSERT(httpBaseChannel);
+
+ nsHttpResponseHead* responseHead = httpBaseChannel->GetResponseHead();
+
+ if (responseHead) {
+ // Filtered opaque responses doesn't need headers, so we just drop them.
+ responseHead->ClearHeaders();
+ }
+
+ mNext->OnStartRequest(aRequest);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OpaqueResponseFilter::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) {
+ LOGORB();
+ uint32_t result;
+ // No data for filtered opaque responses should reach the content process, so
+ // we just discard them.
+ return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount,
+ &result);
+}
+
+NS_IMETHODIMP
+OpaqueResponseFilter::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ LOGORB();
+ mNext->OnStopRequest(aRequest, aStatusCode);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(OpaqueResponseFilter, nsIStreamListener, nsIRequestObserver)
+
+OpaqueResponseBlocker::OpaqueResponseBlocker(nsIStreamListener* aNext,
+ HttpBaseChannel* aChannel,
+ const nsCString& aContentType,
+ bool aNoSniff)
+ : mNext(aNext), mContentType(aContentType), mNoSniff(aNoSniff) {
+ // Storing aChannel as a member is tricky as aChannel owns us and it's
+ // hard to ensure aChannel is alive when we about to use it without
+ // creating a cycle. This is all doable but need some extra efforts.
+ //
+ // So we are just passing aChannel from the caller when we need to use it.
+ MOZ_ASSERT(aChannel);
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gORBLog, LogLevel::Debug))) {
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ LOGORB(" channel=%p, uri=%s", aChannel, uri->GetSpecOrDefault().get());
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ MOZ_DIAGNOSTIC_ASSERT(aChannel->CachedOpaqueResponseBlockingPref());
+}
+
+NS_IMETHODIMP
+OpaqueResponseBlocker::OnStartRequest(nsIRequest* aRequest) {
+ LOGORB();
+
+ if (mState == State::Sniffing) {
+ Unused << EnsureOpaqueResponseIsAllowedAfterSniff(aRequest);
+ }
+
+ // mState will remain State::Sniffing if we need to wait
+ // for JS validator to make a decision.
+ //
+ // When the state is Sniffing, we can't call mNext->OnStartRequest
+ // because fetch requests need the cancellation to be done
+ // before its FetchDriver::OnStartRequest is called, otherwise it'll
+ // resolve the promise regardless the decision of JS validator.
+ if (mState != State::Sniffing) {
+ nsresult rv = mNext->OnStartRequest(aRequest);
+ return NS_SUCCEEDED(mStatus) ? rv : mStatus;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OpaqueResponseBlocker::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ LOGORB();
+
+ nsresult statusForStop = aStatusCode;
+
+ if (mState == State::Blocked && NS_FAILED(mStatus)) {
+ statusForStop = mStatus;
+ }
+
+ if (mState == State::Sniffing) {
+ // It is the call to JSValidatorParent::OnStopRequest that will trigger the
+ // JS parser.
+ mStartOfJavaScriptValidation = TimeStamp::Now();
+
+ MOZ_ASSERT(mJSValidator);
+ mPendingOnStopRequestStatus = Some(aStatusCode);
+ mJSValidator->OnStopRequest(aStatusCode, *aRequest);
+ return NS_OK;
+ }
+
+ return mNext->OnStopRequest(aRequest, statusForStop);
+}
+
+NS_IMETHODIMP
+OpaqueResponseBlocker::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset, uint32_t aCount) {
+ LOGORB();
+
+ if (mState == State::Allowed) {
+ return mNext->OnDataAvailable(aRequest, aInputStream, aOffset, aCount);
+ }
+
+ if (mState == State::Blocked) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mState == State::Sniffing);
+
+ nsCString data;
+ if (!data.SetLength(aCount, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t read;
+ nsresult rv = aInputStream->Read(data.BeginWriting(), aCount, &read);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ MOZ_ASSERT(mJSValidator);
+
+ mJSValidator->OnDataAvailable(data);
+
+ return NS_OK;
+}
+
+nsresult OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterSniff(
+ nsIRequest* aRequest) {
+ nsCOMPtr<HttpBaseChannel> httpBaseChannel = do_QueryInterface(aRequest);
+ MOZ_ASSERT(httpBaseChannel);
+
+ // The `AfterSniff` check shouldn't be run when
+ // 1. We have made a decision already
+ // 2. The JS validator is running, so we should wait
+ // for its result.
+ if (mState != State::Sniffing || mJSValidator) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo;
+
+ nsresult rv =
+ httpBaseChannel->GetLoadInfo(getter_AddRefs<nsILoadInfo>(loadInfo));
+ if (NS_FAILED(rv)) {
+ LOGORB("Failed to get LoadInfo");
+ BlockResponse(httpBaseChannel, rv);
+ return rv;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = httpBaseChannel->GetURI(getter_AddRefs<nsIURI>(uri));
+ if (NS_FAILED(rv)) {
+ LOGORB("Failed to get uri");
+ BlockResponse(httpBaseChannel, rv);
+ return rv;
+ }
+
+ switch (httpBaseChannel->PerformOpaqueResponseSafelistCheckAfterSniff(
+ mContentType, mNoSniff)) {
+ case OpaqueResponse::Block:
+ BlockResponse(httpBaseChannel, NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ case OpaqueResponse::Allow:
+ AllowResponse();
+ return NS_OK;
+ case OpaqueResponse::Sniff:
+ case OpaqueResponse::SniffCompressed:
+ break;
+ }
+
+ MOZ_ASSERT(mState == State::Sniffing);
+ return ValidateJavaScript(httpBaseChannel, uri, loadInfo);
+}
+
+OpaqueResponse
+OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
+ HttpBaseChannel* aChannel, bool aAllow) {
+ if (aAllow) {
+ return OpaqueResponse::Allow;
+ }
+
+ return aChannel->BlockOrFilterOpaqueResponse(
+ this, u"Javascript validation failed"_ns,
+ OpaqueResponseBlockedTelemetryReason::JS_VALIDATION_FAILED,
+ "Javascript validation failed");
+}
+
+static void RecordTelemetry(const TimeStamp& aStartOfValidation,
+ const TimeStamp& aStartOfJavaScriptValidation,
+ OpaqueResponseBlocker::ValidatorResult aResult) {
+ using ValidatorResult = OpaqueResponseBlocker::ValidatorResult;
+ MOZ_DIAGNOSTIC_ASSERT(aStartOfValidation);
+
+ auto key = [aResult]() {
+ switch (aResult) {
+ case ValidatorResult::JavaScript:
+ return "javascript"_ns;
+ case ValidatorResult::JSON:
+ return "json"_ns;
+ case ValidatorResult::Other:
+ return "other"_ns;
+ case ValidatorResult::Failure:
+ return "failure"_ns;
+ }
+ MOZ_ASSERT_UNREACHABLE("Switch statement should be saturated");
+ return "failure"_ns;
+ }();
+
+ TimeStamp now = TimeStamp::Now();
+ PROFILER_MARKER_TEXT(
+ "ORB safelist check", NETWORK,
+ MarkerTiming::Interval(aStartOfValidation, aStartOfJavaScriptValidation),
+ nsPrintfCString("Receive data for validation (%s)", key.get()));
+
+ PROFILER_MARKER_TEXT(
+ "ORB safelist check", NETWORK,
+ MarkerTiming::Interval(aStartOfJavaScriptValidation, now),
+ nsPrintfCString("JS Validation (%s)", key.get()));
+
+ Telemetry::AccumulateTimeDelta(Telemetry::ORB_RECEIVE_DATA_FOR_VALIDATION_MS,
+ key, aStartOfValidation,
+ aStartOfJavaScriptValidation);
+
+ Telemetry::AccumulateTimeDelta(Telemetry::ORB_JAVASCRIPT_VALIDATION_MS, key,
+ aStartOfJavaScriptValidation, now);
+}
+
+// The specification for ORB is currently being written:
+// https://whatpr.org/fetch/1442.html#orb-algorithm
+// The `opaque-response-safelist check` is implemented in:
+// * `HttpBaseChannel::OpaqueResponseSafelistCheckBeforeSniff`
+// * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck`
+// * `HttpBaseChannel::OpaqueResponseSafelistCheckAfterSniff`
+// * `OpaqueResponseBlocker::ValidateJavaScript`
+nsresult OpaqueResponseBlocker::ValidateJavaScript(HttpBaseChannel* aChannel,
+ nsIURI* aURI,
+ nsILoadInfo* aLoadInfo) {
+ MOZ_DIAGNOSTIC_ASSERT(aChannel);
+ MOZ_ASSERT(aURI && aLoadInfo);
+
+ if (!StaticPrefs::browser_opaqueResponseBlocking_javascriptValidator()) {
+ LOGORB("Allowed: JS Validator is disabled");
+ AllowResponse();
+ return NS_OK;
+ }
+
+ int64_t contentLength;
+ nsresult rv = aChannel->GetContentLength(&contentLength);
+ if (NS_FAILED(rv)) {
+ LOGORB("Blocked: No Content Length");
+ BlockResponse(aChannel, rv);
+ return rv;
+ }
+
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::OPAQUE_RESPONSE_BLOCKING_JAVASCRIPT_VALIDATION_COUNT,
+ 1);
+
+ LOGORB("Send %s to the validator", aURI->GetSpecOrDefault().get());
+ // https://whatpr.org/fetch/1442.html#orb-algorithm, step 15
+ mJSValidator = dom::JSValidatorParent::Create();
+ mJSValidator->IsOpaqueResponseAllowed(
+ [self = RefPtr{this}, channel = nsCOMPtr{aChannel}, uri = nsCOMPtr{aURI},
+ loadInfo = nsCOMPtr{aLoadInfo}, startOfValidation = TimeStamp::Now()](
+ Maybe<ipc::Shmem> aSharedData, ValidatorResult aResult) {
+ MOZ_LOG(gORBLog, LogLevel::Debug,
+ ("JSValidator resolved for %s with %s",
+ uri->GetSpecOrDefault().get(),
+ aSharedData.isSome() ? "true" : "false"));
+ bool allowed = aResult == ValidatorResult::JavaScript;
+ switch (self->EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
+ channel, allowed)) {
+ case OpaqueResponse::Allow:
+ // It's possible that the JS validation failed for this request,
+ // however we decided that we need to filter the response instead
+ // of blocking. So we set allowed to true manually when that's the
+ // case.
+ allowed = true;
+ self->AllowResponse();
+ break;
+ case OpaqueResponse::Block:
+ self->BlockResponse(channel, NS_ERROR_FAILURE);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "We should only ever have Allow or Block here.");
+ allowed = false;
+ self->BlockResponse(channel, NS_ERROR_FAILURE);
+ break;
+ }
+
+ self->ResolveAndProcessData(channel, allowed, aSharedData);
+ if (aSharedData.isSome()) {
+ self->mJSValidator->DeallocShmem(aSharedData.ref());
+ }
+
+ RecordTelemetry(startOfValidation, self->mStartOfJavaScriptValidation,
+ aResult);
+
+ Unused << dom::PJSValidatorParent::Send__delete__(self->mJSValidator);
+ self->mJSValidator = nullptr;
+ });
+
+ return NS_OK;
+}
+
+bool OpaqueResponseBlocker::IsSniffing() const {
+ return mState == State::Sniffing;
+}
+
+void OpaqueResponseBlocker::AllowResponse() {
+ LOGORB("Sniffer is done, allow response, this=%p", this);
+ MOZ_ASSERT(mState == State::Sniffing);
+ mState = State::Allowed;
+}
+
+void OpaqueResponseBlocker::BlockResponse(HttpBaseChannel* aChannel,
+ nsresult aStatus) {
+ LOGORB("Sniffer is done, block response, this=%p", this);
+ MOZ_ASSERT(mState == State::Sniffing);
+ mState = State::Blocked;
+ mStatus = aStatus;
+ aChannel->SetChannelBlockedByOpaqueResponse();
+ aChannel->CancelWithReason(mStatus,
+ "OpaqueResponseBlocker::BlockResponse"_ns);
+}
+
+void OpaqueResponseBlocker::FilterResponse() {
+ MOZ_ASSERT(mState == State::Sniffing);
+
+ if (mShouldFilter) {
+ return;
+ }
+
+ mShouldFilter = true;
+
+ mNext = new OpaqueResponseFilter(mNext);
+}
+
+void OpaqueResponseBlocker::ResolveAndProcessData(
+ HttpBaseChannel* aChannel, bool aAllowed, Maybe<ipc::Shmem>& aSharedData) {
+ nsresult rv = OnStartRequest(aChannel);
+
+ if (!aAllowed || NS_FAILED(rv)) {
+ MOZ_ASSERT_IF(!aAllowed, mState == State::Blocked);
+ MaybeRunOnStopRequest(aChannel);
+ return;
+ }
+
+ MOZ_ASSERT(mState == State::Allowed);
+
+ if (aSharedData.isNothing()) {
+ MaybeRunOnStopRequest(aChannel);
+ return;
+ }
+
+ const ipc::Shmem& mem = aSharedData.ref();
+ nsCOMPtr<nsIInputStream> input;
+ rv = NS_NewByteInputStream(getter_AddRefs(input),
+ Span(mem.get<char>(), mem.Size<char>()),
+ NS_ASSIGNMENT_DEPEND);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ BlockResponse(aChannel, rv);
+ MaybeRunOnStopRequest(aChannel);
+ return;
+ }
+
+ // When this line reaches, the state is either State::Allowed or
+ // State::Blocked. The OnDataAvailable call will either call
+ // the next listener or reject the request.
+ OnDataAvailable(aChannel, input, 0, mem.Size<char>());
+
+ MaybeRunOnStopRequest(aChannel);
+}
+
+void OpaqueResponseBlocker::MaybeRunOnStopRequest(HttpBaseChannel* aChannel) {
+ MOZ_ASSERT(mState != State::Sniffing);
+ if (mPendingOnStopRequestStatus.isSome()) {
+ OnStopRequest(aChannel, mPendingOnStopRequestStatus.value());
+ }
+}
+
+NS_IMPL_ISUPPORTS(OpaqueResponseBlocker, nsIStreamListener, nsIRequestObserver)
+
+} // namespace mozilla::net