summaryrefslogtreecommitdiffstats
path: root/netwerk/protocol/http/OpaqueResponseUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--netwerk/protocol/http/OpaqueResponseUtils.cpp486
1 files changed, 486 insertions, 0 deletions
diff --git a/netwerk/protocol/http/OpaqueResponseUtils.cpp b/netwerk/protocol/http/OpaqueResponseUtils.cpp
new file mode 100644
index 0000000000..75e0c70d61
--- /dev/null
+++ b/netwerk/protocol/http/OpaqueResponseUtils.cpp
@@ -0,0 +1,486 @@
+/* -*- 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 "ErrorList.h"
+#include "nsContentUtils.h"
+#include "nsHttpResponseHead.h"
+#include "nsISupports.h"
+#include "nsMimeTypes.h"
+#include "nsThreadUtils.h"
+#include "nsStringStream.h"
+#include "HttpBaseChannel.h"
+
+#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);
+}
+
+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);
+}
+
+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;
+ }
+
+ 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(
+ const 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;
+}
+
+void LogORBError(nsILoadInfo* aLoadInfo, nsIURI* aURI) {
+ RefPtr<dom::Document> doc;
+ aLoadInfo->GetLoadingDocument(getter_AddRefs(doc));
+
+ nsAutoCString uri;
+ nsresult rv = nsContentUtils::AnonymizeURI(aURI, uri);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(uri, *params.AppendElement());
+
+ MOZ_LOG(gORBLog, LogLevel::Debug,
+ ("%s: Resource blocked: %s ", __func__, uri.get()));
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "ORB"_ns, doc,
+ nsContentUtils::eNECKO_PROPERTIES,
+ "ResourceBlockedCORS", params);
+}
+
+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) {
+ MOZ_ASSERT(mJSValidator);
+ mPendingOnStopRequestStatus = Some(aStatusCode);
+ mJSValidator->OnStopRequest(aStatusCode);
+ 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::Alllow:
+ AllowResponse();
+ return NS_OK;
+ case OpaqueResponse::Sniff:
+ case OpaqueResponse::SniffCompressed:
+ break;
+ }
+
+ MOZ_ASSERT(mState == State::Sniffing);
+ return ValidateJavaScript(httpBaseChannel, uri, loadInfo);
+}
+
+// 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;
+ }
+
+ 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}](bool aAllowed,
+ Maybe<ipc::Shmem> aSharedData) {
+ MOZ_LOG(gORBLog, LogLevel::Debug,
+ ("JSValidator resolved for %s with %s",
+ uri->GetSpecOrDefault().get(),
+ aSharedData.isSome() ? "true" : "false"));
+ if (aAllowed) {
+ self->AllowResponse();
+ } else {
+ self->BlockResponse(channel, NS_ERROR_FAILURE);
+ LogORBError(loadInfo, uri);
+ }
+ self->ResolveAndProcessData(channel, aAllowed, aSharedData);
+ if (aSharedData.isSome()) {
+ self->mJSValidator->DeallocShmem(aSharedData.ref());
+ }
+
+ 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 aReason) {
+ LOGORB("Sniffer is done, block response, this=%p", this);
+ MOZ_ASSERT(mState == State::Sniffing);
+ mState = State::Blocked;
+ mStatus = aReason;
+ aChannel->SetChannelBlockedByOpaqueResponse();
+ aChannel->CancelWithReason(mStatus,
+ "OpaqueResponseBlocker::BlockResponse"_ns);
+}
+
+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