tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

OpaqueResponseUtils.cpp (23240B)


      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 "mozilla/net/OpaqueResponseUtils.h"
      8 
      9 #include "mozilla/dom/Document.h"
     10 #include "mozilla/StaticPrefs_browser.h"
     11 #include "mozilla/dom/JSValidatorParent.h"
     12 #include "ErrorList.h"
     13 #include "nsContentUtils.h"
     14 #include "nsHttpResponseHead.h"
     15 #include "nsISupports.h"
     16 #include "nsMimeTypes.h"
     17 #include "nsStreamUtils.h"
     18 #include "nsThreadUtils.h"
     19 #include "nsStringStream.h"
     20 #include "HttpBaseChannel.h"
     21 
     22 static mozilla::LazyLogModule gORBLog("ORB");
     23 
     24 #define LOGORB(msg, ...)            \
     25  MOZ_LOG(gORBLog, LogLevel::Debug, \
     26          ("%s: %p " msg, __func__, this, ##__VA_ARGS__))
     27 
     28 namespace mozilla::net {
     29 
     30 static bool IsOpaqueSafeListedMIMEType(const nsACString& aContentType) {
     31  if (aContentType.EqualsLiteral(TEXT_CSS) ||
     32      aContentType.EqualsLiteral(IMAGE_SVG_XML)) {
     33    return true;
     34  }
     35 
     36  NS_ConvertUTF8toUTF16 typeString(aContentType);
     37  return nsContentUtils::IsJavascriptMIMEType(typeString);
     38 }
     39 
     40 // These need to be kept in sync with
     41 // "browser.opaqueResponseBlocking.mediaExceptionsStrategy"
     42 enum class OpaqueResponseMediaException { NoExceptions, AllowSome, AllowAll };
     43 
     44 static OpaqueResponseMediaException ConfiguredMediaExceptionsStrategy() {
     45  uint32_t pref = StaticPrefs::
     46      browser_opaqueResponseBlocking_mediaExceptionsStrategy_DoNotUseDirectly();
     47  if (NS_WARN_IF(pref > static_cast<uint32_t>(
     48                            OpaqueResponseMediaException::AllowAll))) {
     49    return OpaqueResponseMediaException::AllowAll;
     50  }
     51 
     52  return static_cast<OpaqueResponseMediaException>(pref);
     53 }
     54 
     55 static bool IsOpaqueSafeListedSpecBreakingMIMEType(
     56    const nsACString& aContentType, bool aNoSniff) {
     57  // Avoid trouble with DASH/HLS. See bug 1698040.
     58  if (aContentType.EqualsLiteral(APPLICATION_DASH_XML) ||
     59      aContentType.EqualsLiteral(APPLICATION_MPEGURL) ||
     60      aContentType.EqualsLiteral(AUDIO_MPEG_URL) ||
     61      aContentType.EqualsLiteral(TEXT_VTT)) {
     62    return true;
     63  }
     64 
     65  // Do what Chromium does. This is from bug 1828375, and we should ideally
     66  // revert this.
     67  if (aContentType.EqualsLiteral(TEXT_PLAIN) && aNoSniff) {
     68    return true;
     69  }
     70 
     71  switch (ConfiguredMediaExceptionsStrategy()) {
     72    case OpaqueResponseMediaException::NoExceptions:
     73      break;
     74    case OpaqueResponseMediaException::AllowSome:
     75      if (aContentType.EqualsLiteral(AUDIO_MP3) ||
     76          aContentType.EqualsLiteral(AUDIO_AAC) ||
     77          aContentType.EqualsLiteral(AUDIO_AACP) ||
     78          aContentType.EqualsLiteral(MULTIPART_MIXED_REPLACE)) {
     79        return true;
     80      }
     81      break;
     82    case OpaqueResponseMediaException::AllowAll:
     83      if (StringBeginsWith(aContentType, "audio/"_ns) ||
     84          StringBeginsWith(aContentType, "video/"_ns) ||
     85          aContentType.EqualsLiteral(MULTIPART_MIXED_REPLACE)) {
     86        return true;
     87      }
     88      break;
     89  }
     90 
     91  return false;
     92 }
     93 
     94 static bool IsOpaqueBlockListedMIMEType(const nsACString& aContentType) {
     95  return aContentType.EqualsLiteral(TEXT_HTML) ||
     96         StringEndsWith(aContentType, "+json"_ns) ||
     97         aContentType.EqualsLiteral(APPLICATION_JSON) ||
     98         aContentType.EqualsLiteral(TEXT_JSON) ||
     99         StringEndsWith(aContentType, "+xml"_ns) ||
    100         aContentType.EqualsLiteral(APPLICATION_XML) ||
    101         aContentType.EqualsLiteral(TEXT_XML);
    102 }
    103 
    104 static bool IsOpaqueBlockListedNeverSniffedMIMEType(
    105    const nsACString& aContentType) {
    106  return aContentType.EqualsLiteral(APPLICATION_GZIP2) ||
    107         aContentType.EqualsLiteral(APPLICATION_MSEXCEL) ||
    108         aContentType.EqualsLiteral(APPLICATION_MSPPT) ||
    109         aContentType.EqualsLiteral(APPLICATION_MSWORD) ||
    110         aContentType.EqualsLiteral(APPLICATION_MSWORD_TEMPLATE) ||
    111         aContentType.EqualsLiteral(APPLICATION_PDF) ||
    112         aContentType.EqualsLiteral(APPLICATION_MPEGURL) ||
    113         aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKPOINT) ||
    114         aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKSHEET) ||
    115         aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKWORD) ||
    116         aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL) ||
    117         aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL2) ||
    118         aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT) ||
    119         aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT2) ||
    120         aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD) ||
    121         aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD2) ||
    122         aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD3) ||
    123         aContentType.EqualsLiteral(APPLICATION_VND_MSWORD) ||
    124         aContentType.EqualsLiteral(
    125             APPLICATION_VND_PRESENTATIONML_PRESENTATION) ||
    126         aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATIONML_TEMPLATE) ||
    127         aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_SHEET) ||
    128         aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_TEMPLATE) ||
    129         aContentType.EqualsLiteral(
    130             APPLICATION_VND_WORDPROCESSINGML_DOCUMENT) ||
    131         aContentType.EqualsLiteral(
    132             APPLICATION_VND_WORDPROCESSINGML_TEMPLATE) ||
    133         aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXML) ||
    134         aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXMLM) ||
    135         aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEET_OPENXML) ||
    136         aContentType.EqualsLiteral(APPLICATION_VND_WORDPROSSING_OPENXML) ||
    137         aContentType.EqualsLiteral(APPLICATION_GZIP) ||
    138         aContentType.EqualsLiteral(APPLICATION_XPROTOBUF) ||
    139         aContentType.EqualsLiteral(APPLICATION_XPROTOBUFFER) ||
    140         aContentType.EqualsLiteral(APPLICATION_ZIP) ||
    141         aContentType.EqualsLiteral(AUDIO_MPEG_URL) ||
    142         aContentType.EqualsLiteral(MULTIPART_BYTERANGES) ||
    143         aContentType.EqualsLiteral(MULTIPART_SIGNED) ||
    144         aContentType.EqualsLiteral(TEXT_EVENT_STREAM) ||
    145         aContentType.EqualsLiteral(TEXT_CSV) ||
    146         aContentType.EqualsLiteral(TEXT_VTT) ||
    147         aContentType.EqualsLiteral(APPLICATION_DASH_XML);
    148 }
    149 
    150 OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason(
    151    const nsACString& aContentType, uint16_t aStatus, bool aNoSniff) {
    152  if (aContentType.IsEmpty()) {
    153    return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF;
    154  }
    155 
    156  if (IsOpaqueSafeListedMIMEType(aContentType)) {
    157    return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED;
    158  }
    159 
    160  // For some MIME types we deviate from spec and allow when we ideally
    161  // shouldn't. These are returnened before any blocking takes place.
    162  if (IsOpaqueSafeListedSpecBreakingMIMEType(aContentType, aNoSniff)) {
    163    return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED_SPEC_BREAKING;
    164  }
    165 
    166  if (IsOpaqueBlockListedNeverSniffedMIMEType(aContentType)) {
    167    return OpaqueResponseBlockedReason::BLOCKED_BLOCKLISTED_NEVER_SNIFFED;
    168  }
    169 
    170  if (aStatus == 206 && IsOpaqueBlockListedMIMEType(aContentType)) {
    171    return OpaqueResponseBlockedReason::BLOCKED_206_AND_BLOCKLISTED;
    172  }
    173 
    174  nsAutoCString contentTypeOptionsHeader;
    175  if (aNoSniff && (IsOpaqueBlockListedMIMEType(aContentType) ||
    176                   aContentType.EqualsLiteral(TEXT_PLAIN))) {
    177    return OpaqueResponseBlockedReason::
    178        BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN;
    179  }
    180 
    181  return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF;
    182 }
    183 
    184 OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason(
    185    nsHttpResponseHead& aResponseHead) {
    186  nsAutoCString contentType;
    187  aResponseHead.ContentType(contentType);
    188 
    189  nsAutoCString contentTypeOptionsHeader;
    190  bool nosniff =
    191      aResponseHead.GetContentTypeOptionsHeader(contentTypeOptionsHeader) &&
    192      contentTypeOptionsHeader.EqualsIgnoreCase("nosniff");
    193 
    194  return GetOpaqueResponseBlockedReason(contentType, aResponseHead.Status(),
    195                                        nosniff);
    196 }
    197 
    198 Result<std::tuple<int64_t, int64_t, int64_t>, nsresult>
    199 ParseContentRangeHeaderString(const nsAutoCString& aRangeStr) {
    200  // Parse the range header: e.g. Content-Range: bytes 7000-7999/8000.
    201  const int32_t spacePos = aRangeStr.Find(" "_ns);
    202  const int32_t dashPos = aRangeStr.Find("-"_ns, spacePos);
    203  const int32_t slashPos = aRangeStr.Find("/"_ns, dashPos);
    204 
    205  nsAutoCString rangeStartText;
    206  aRangeStr.Mid(rangeStartText, spacePos + 1, dashPos - (spacePos + 1));
    207 
    208  nsresult rv;
    209  const int64_t rangeStart = rangeStartText.ToInteger64(&rv);
    210  if (NS_FAILED(rv)) {
    211    return Err(rv);
    212  }
    213  if (0 > rangeStart) {
    214    return Err(NS_ERROR_ILLEGAL_VALUE);
    215  }
    216 
    217  nsAutoCString rangeEndText;
    218  aRangeStr.Mid(rangeEndText, dashPos + 1, slashPos - (dashPos + 1));
    219  const int64_t rangeEnd = rangeEndText.ToInteger64(&rv);
    220  if (NS_FAILED(rv)) {
    221    return Err(rv);
    222  }
    223  if (rangeStart > rangeEnd) {
    224    return Err(NS_ERROR_ILLEGAL_VALUE);
    225  }
    226 
    227  nsAutoCString rangeTotalText;
    228  aRangeStr.Right(rangeTotalText, aRangeStr.Length() - (slashPos + 1));
    229  if (rangeTotalText[0] == '*') {
    230    return std::make_tuple(rangeStart, rangeEnd, (int64_t)-1);
    231  }
    232 
    233  const int64_t rangeTotal = rangeTotalText.ToInteger64(&rv);
    234  if (NS_FAILED(rv)) {
    235    return Err(rv);
    236  }
    237  if (rangeEnd >= rangeTotal) {
    238    return Err(NS_ERROR_ILLEGAL_VALUE);
    239  }
    240 
    241  return std::make_tuple(rangeStart, rangeEnd, rangeTotal);
    242 }
    243 
    244 bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead) {
    245  MOZ_ASSERT(aResponseHead.Status() == 206);
    246 
    247  nsAutoCString contentRange;
    248  (void)aResponseHead.GetHeader(nsHttp::Content_Range, contentRange);
    249 
    250  auto rangeOrErr = ParseContentRangeHeaderString(contentRange);
    251  if (rangeOrErr.isErr()) {
    252    return false;
    253  }
    254 
    255  const int64_t responseFirstBytePos = std::get<0>(rangeOrErr.unwrap());
    256  return responseFirstBytePos == 0;
    257 }
    258 
    259 LogModule* GetORBLog() { return gORBLog; }
    260 
    261 OpaqueResponseFilter::OpaqueResponseFilter(nsIStreamListener* aNext)
    262    : mNext(aNext) {
    263  LOGORB();
    264 }
    265 
    266 NS_IMETHODIMP
    267 OpaqueResponseFilter::OnStartRequest(nsIRequest* aRequest) {
    268  LOGORB();
    269  nsCOMPtr<HttpBaseChannel> httpBaseChannel = do_QueryInterface(aRequest);
    270  MOZ_ASSERT(httpBaseChannel);
    271 
    272  nsHttpResponseHead* responseHead = httpBaseChannel->GetResponseHead();
    273 
    274  if (responseHead) {
    275    // Filtered opaque responses doesn't need headers, so we just drop them.
    276    responseHead->ClearHeaders();
    277  }
    278 
    279  mNext->OnStartRequest(aRequest);
    280  return NS_OK;
    281 }
    282 
    283 NS_IMETHODIMP
    284 OpaqueResponseFilter::OnDataAvailable(nsIRequest* aRequest,
    285                                      nsIInputStream* aInputStream,
    286                                      uint64_t aOffset, uint32_t aCount) {
    287  LOGORB();
    288  uint32_t result;
    289  // No data for filtered opaque responses should reach the content process, so
    290  // we just discard them.
    291  return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount,
    292                                    &result);
    293 }
    294 
    295 NS_IMETHODIMP
    296 OpaqueResponseFilter::OnStopRequest(nsIRequest* aRequest,
    297                                    nsresult aStatusCode) {
    298  LOGORB();
    299  mNext->OnStopRequest(aRequest, aStatusCode);
    300  return NS_OK;
    301 }
    302 
    303 NS_IMPL_ISUPPORTS(OpaqueResponseFilter, nsIStreamListener, nsIRequestObserver)
    304 
    305 OpaqueResponseBlocker::OpaqueResponseBlocker(nsIStreamListener* aNext,
    306                                             HttpBaseChannel* aChannel,
    307                                             const nsCString& aContentType,
    308                                             bool aNoSniff)
    309    : mNext(aNext), mContentType(aContentType), mNoSniff(aNoSniff) {
    310  // Storing aChannel as a member is tricky as aChannel owns us and it's
    311  // hard to ensure aChannel is alive when we about to use it without
    312  // creating a cycle. This is all doable but need some extra efforts.
    313  //
    314  // So we are just passing aChannel from the caller when we need to use it.
    315  MOZ_ASSERT(aChannel);
    316 
    317  if (MOZ_UNLIKELY(MOZ_LOG_TEST(gORBLog, LogLevel::Debug))) {
    318    nsCOMPtr<nsIURI> uri;
    319    aChannel->GetURI(getter_AddRefs(uri));
    320    if (uri) {
    321      LOGORB(" channel=%p, uri=%s", aChannel, uri->GetSpecOrDefault().get());
    322    }
    323  }
    324  MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
    325  MOZ_DIAGNOSTIC_ASSERT(aChannel->CachedOpaqueResponseBlockingPref());
    326 }
    327 
    328 NS_IMETHODIMP
    329 OpaqueResponseBlocker::OnStartRequest(nsIRequest* aRequest) {
    330  LOGORB();
    331 
    332  if (mState == State::Sniffing) {
    333    (void)EnsureOpaqueResponseIsAllowedAfterSniff(aRequest);
    334  }
    335 
    336  // mState will remain State::Sniffing if we need to wait
    337  // for JS validator to make a decision.
    338  //
    339  // When the state is Sniffing, we can't call mNext->OnStartRequest
    340  // because fetch requests need the cancellation to be done
    341  // before its FetchDriver::OnStartRequest is called, otherwise it'll
    342  // resolve the promise regardless the decision of JS validator.
    343  if (mState != State::Sniffing) {
    344    nsresult rv = mNext->OnStartRequest(aRequest);
    345    return NS_SUCCEEDED(mStatus) ? rv : mStatus;
    346  }
    347 
    348  return NS_OK;
    349 }
    350 
    351 NS_IMETHODIMP
    352 OpaqueResponseBlocker::OnStopRequest(nsIRequest* aRequest,
    353                                     nsresult aStatusCode) {
    354  LOGORB();
    355 
    356  nsresult statusForStop = aStatusCode;
    357 
    358  if (mState == State::Blocked && NS_FAILED(mStatus)) {
    359    statusForStop = mStatus;
    360  }
    361 
    362  if (mState == State::Sniffing) {
    363    // It is the call to JSValidatorParent::OnStopRequest that will trigger the
    364    // JS parser.
    365    mStartOfJavaScriptValidation = TimeStamp::Now();
    366 
    367    MOZ_ASSERT(mJSValidator);
    368    mPendingOnStopRequestStatus = Some(aStatusCode);
    369    mJSValidator->OnStopRequest(aStatusCode, *aRequest);
    370    return NS_OK;
    371  }
    372 
    373  return mNext->OnStopRequest(aRequest, statusForStop);
    374 }
    375 
    376 NS_IMETHODIMP
    377 OpaqueResponseBlocker::OnDataAvailable(nsIRequest* aRequest,
    378                                       nsIInputStream* aInputStream,
    379                                       uint64_t aOffset, uint32_t aCount) {
    380  LOGORB();
    381 
    382  if (mState == State::Allowed) {
    383    return mNext->OnDataAvailable(aRequest, aInputStream, aOffset, aCount);
    384  }
    385 
    386  if (mState == State::Blocked) {
    387    return NS_BINDING_ABORTED;
    388  }
    389 
    390  MOZ_ASSERT(mState == State::Sniffing);
    391 
    392  nsCString data;
    393  if (!data.SetLength(aCount, fallible)) {
    394    return NS_ERROR_OUT_OF_MEMORY;
    395  }
    396 
    397  uint32_t read;
    398  nsresult rv = aInputStream->Read(data.BeginWriting(), aCount, &read);
    399  if (NS_FAILED(rv)) {
    400    return rv;
    401  }
    402 
    403  MOZ_ASSERT(mJSValidator);
    404 
    405  mJSValidator->OnDataAvailable(data);
    406 
    407  return NS_OK;
    408 }
    409 
    410 nsresult OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterSniff(
    411    nsIRequest* aRequest) {
    412  nsCOMPtr<HttpBaseChannel> httpBaseChannel = do_QueryInterface(aRequest);
    413  MOZ_ASSERT(httpBaseChannel);
    414 
    415  // The `AfterSniff` check shouldn't be run when
    416  // 1. We have made a decision already
    417  // 2. The JS validator is running, so we should wait
    418  // for its result.
    419  if (mState != State::Sniffing || mJSValidator) {
    420    return NS_OK;
    421  }
    422 
    423  nsCOMPtr<nsILoadInfo> loadInfo;
    424 
    425  nsresult rv =
    426      httpBaseChannel->GetLoadInfo(getter_AddRefs<nsILoadInfo>(loadInfo));
    427  if (NS_FAILED(rv)) {
    428    LOGORB("Failed to get LoadInfo");
    429    BlockResponse(httpBaseChannel, rv);
    430    return rv;
    431  }
    432 
    433  nsCOMPtr<nsIURI> uri;
    434  rv = httpBaseChannel->GetURI(getter_AddRefs<nsIURI>(uri));
    435  if (NS_FAILED(rv)) {
    436    LOGORB("Failed to get uri");
    437    BlockResponse(httpBaseChannel, rv);
    438    return rv;
    439  }
    440 
    441  switch (httpBaseChannel->PerformOpaqueResponseSafelistCheckAfterSniff(
    442      mContentType, mNoSniff)) {
    443    case OpaqueResponse::Block:
    444      BlockResponse(httpBaseChannel, NS_BINDING_ABORTED);
    445      return NS_BINDING_ABORTED;
    446    case OpaqueResponse::Allow:
    447      AllowResponse();
    448      return NS_OK;
    449    case OpaqueResponse::Sniff:
    450    case OpaqueResponse::SniffCompressed:
    451      break;
    452  }
    453 
    454  MOZ_ASSERT(mState == State::Sniffing);
    455  return ValidateJavaScript(httpBaseChannel, uri, loadInfo);
    456 }
    457 
    458 OpaqueResponse
    459 OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
    460    HttpBaseChannel* aChannel, bool aAllow) {
    461  if (aAllow) {
    462    return OpaqueResponse::Allow;
    463  }
    464 
    465  return aChannel->BlockOrFilterOpaqueResponse(
    466      this, u"Javascript validation failed"_ns,
    467      OpaqueResponseBlockedTelemetryReason::eJsValidationFailed,
    468      "Javascript validation failed");
    469 }
    470 
    471 static void RecordTelemetry(const TimeStamp& aStartOfValidation,
    472                            const TimeStamp& aStartOfJavaScriptValidation,
    473                            OpaqueResponseBlocker::ValidatorResult aResult) {
    474  using ValidatorResult = OpaqueResponseBlocker::ValidatorResult;
    475  MOZ_DIAGNOSTIC_ASSERT(aStartOfValidation);
    476 
    477  auto key = [aResult]() {
    478    switch (aResult) {
    479      case ValidatorResult::JavaScript:
    480        return "javascript"_ns;
    481      case ValidatorResult::JSON:
    482        return "json"_ns;
    483      case ValidatorResult::Other:
    484        return "other"_ns;
    485      case ValidatorResult::Failure:
    486        return "failure"_ns;
    487    }
    488    MOZ_ASSERT_UNREACHABLE("Switch statement should be saturated");
    489    return "failure"_ns;
    490  }();
    491 
    492  TimeStamp now = TimeStamp::Now();
    493  PROFILER_MARKER_TEXT(
    494      "ORB safelist check", NETWORK,
    495      MarkerTiming::Interval(aStartOfValidation, aStartOfJavaScriptValidation),
    496      nsPrintfCString("Receive data for validation (%s)", key.get()));
    497 
    498  PROFILER_MARKER_TEXT(
    499      "ORB safelist check", NETWORK,
    500      MarkerTiming::Interval(aStartOfJavaScriptValidation, now),
    501      nsPrintfCString("JS Validation (%s)", key.get()));
    502 
    503  glean::orb::receive_data_for_validation.Get(key).AccumulateRawDuration(
    504      aStartOfJavaScriptValidation - aStartOfValidation);
    505 
    506  glean::orb::javascript_validation.Get(key).AccumulateRawDuration(
    507      now - aStartOfJavaScriptValidation);
    508 }
    509 
    510 // The specification for ORB is currently being written:
    511 // https://whatpr.org/fetch/1442.html#orb-algorithm
    512 // The `opaque-response-safelist check` is implemented in:
    513 // * `HttpBaseChannel::OpaqueResponseSafelistCheckBeforeSniff`
    514 // * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck`
    515 // * `HttpBaseChannel::OpaqueResponseSafelistCheckAfterSniff`
    516 // * `OpaqueResponseBlocker::ValidateJavaScript`
    517 nsresult OpaqueResponseBlocker::ValidateJavaScript(HttpBaseChannel* aChannel,
    518                                                   nsIURI* aURI,
    519                                                   nsILoadInfo* aLoadInfo) {
    520  MOZ_DIAGNOSTIC_ASSERT(aChannel);
    521  MOZ_ASSERT(aURI && aLoadInfo);
    522 
    523  if (!StaticPrefs::browser_opaqueResponseBlocking_javascriptValidator()) {
    524    LOGORB("Allowed: JS Validator is disabled");
    525    AllowResponse();
    526    return NS_OK;
    527  }
    528 
    529  int64_t contentLength;
    530  nsresult rv = aChannel->GetContentLength(&contentLength);
    531  if (NS_FAILED(rv)) {
    532    LOGORB("Blocked: No Content Length");
    533    BlockResponse(aChannel, rv);
    534    return rv;
    535  }
    536 
    537  glean::opaque_response_blocking::javascript_validation_count.Add(1);
    538 
    539  LOGORB("Send %s to the validator", aURI->GetSpecOrDefault().get());
    540  // https://whatpr.org/fetch/1442.html#orb-algorithm, step 15
    541  mJSValidator = dom::JSValidatorParent::Create();
    542  mJSValidator->IsOpaqueResponseAllowed(
    543      [self = RefPtr{this}, channel = nsCOMPtr{aChannel}, uri = nsCOMPtr{aURI},
    544       loadInfo = nsCOMPtr{aLoadInfo}, startOfValidation = TimeStamp::Now()](
    545          Maybe<ipc::Shmem> aSharedData, ValidatorResult aResult) {
    546        MOZ_LOG(gORBLog, LogLevel::Debug,
    547                ("JSValidator resolved for %s with %s",
    548                 uri->GetSpecOrDefault().get(),
    549                 aSharedData.isSome() ? "true" : "false"));
    550        bool allowed = aResult == ValidatorResult::JavaScript;
    551        switch (self->EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
    552            channel, allowed)) {
    553          case OpaqueResponse::Allow:
    554            // It's possible that the JS validation failed for this request,
    555            // however we decided that we need to filter the response instead
    556            // of blocking. So we set allowed to true manually when that's the
    557            // case.
    558            allowed = true;
    559            self->AllowResponse();
    560            break;
    561          case OpaqueResponse::Block:
    562            self->BlockResponse(channel, NS_ERROR_FAILURE);
    563            break;
    564          default:
    565            MOZ_ASSERT_UNREACHABLE(
    566                "We should only ever have Allow or Block here.");
    567            allowed = false;
    568            self->BlockResponse(channel, NS_BINDING_ABORTED);
    569            break;
    570        }
    571 
    572        self->ResolveAndProcessData(channel, allowed, aSharedData);
    573        if (aSharedData.isSome()) {
    574          self->mJSValidator->DeallocShmem(aSharedData.ref());
    575        }
    576 
    577        RecordTelemetry(startOfValidation, self->mStartOfJavaScriptValidation,
    578                        aResult);
    579 
    580        (void)dom::PJSValidatorParent::Send__delete__(self->mJSValidator);
    581        self->mJSValidator = nullptr;
    582      });
    583 
    584  return NS_OK;
    585 }
    586 
    587 bool OpaqueResponseBlocker::IsSniffing() const {
    588  return mState == State::Sniffing;
    589 }
    590 
    591 void OpaqueResponseBlocker::AllowResponse() {
    592  LOGORB("Sniffer is done, allow response, this=%p", this);
    593  MOZ_ASSERT(mState == State::Sniffing);
    594  mState = State::Allowed;
    595 }
    596 
    597 void OpaqueResponseBlocker::BlockResponse(HttpBaseChannel* aChannel,
    598                                          nsresult aStatus) {
    599  LOGORB("Sniffer is done, block response, this=%p", this);
    600  MOZ_ASSERT(mState == State::Sniffing);
    601  mState = State::Blocked;
    602  mStatus = aStatus;
    603  aChannel->SetChannelBlockedByOpaqueResponse();
    604  aChannel->CancelWithReason(mStatus,
    605                             "OpaqueResponseBlocker::BlockResponse"_ns);
    606 }
    607 
    608 void OpaqueResponseBlocker::FilterResponse() {
    609  MOZ_ASSERT(mState == State::Sniffing);
    610 
    611  if (mShouldFilter) {
    612    return;
    613  }
    614 
    615  mShouldFilter = true;
    616 
    617  mNext = new OpaqueResponseFilter(mNext);
    618 }
    619 
    620 void OpaqueResponseBlocker::ResolveAndProcessData(
    621    HttpBaseChannel* aChannel, bool aAllowed, Maybe<ipc::Shmem>& aSharedData) {
    622  nsresult rv = OnStartRequest(aChannel);
    623 
    624  if (!aAllowed || NS_FAILED(rv)) {
    625    MOZ_ASSERT_IF(!aAllowed, mState == State::Blocked);
    626    // We decided to block, so nothing more to do.
    627    MaybeRunOnStopRequest(aChannel);
    628    return;
    629  }
    630 
    631  MOZ_ASSERT(mState == State::Allowed);
    632 
    633  if (aSharedData.isNothing()) {
    634    MaybeRunOnStopRequest(aChannel);
    635    return;
    636  }
    637 
    638  const ipc::Shmem& mem = aSharedData.ref();
    639  nsCOMPtr<nsIInputStream> input;
    640  rv = NS_NewByteInputStream(getter_AddRefs(input),
    641                             Span(mem.get<char>(), mem.Size<char>()),
    642                             NS_ASSIGNMENT_DEPEND);
    643 
    644  if (NS_WARN_IF(NS_FAILED(rv))) {
    645    BlockResponse(aChannel, rv);
    646    MaybeRunOnStopRequest(aChannel);
    647    return;
    648  }
    649 
    650  // When this line reaches, the state is either State::Allowed or
    651  // State::Blocked. The OnDataAvailable call will either call
    652  // the next listener or reject the request.
    653  OnDataAvailable(aChannel, input, 0, mem.Size<char>());
    654 
    655  MaybeRunOnStopRequest(aChannel);
    656 }
    657 
    658 void OpaqueResponseBlocker::MaybeRunOnStopRequest(HttpBaseChannel* aChannel) {
    659  MOZ_ASSERT(mState != State::Sniffing);
    660  if (mPendingOnStopRequestStatus.isSome()) {
    661    OnStopRequest(aChannel, mPendingOnStopRequestStatus.value());
    662  }
    663 }
    664 
    665 NS_IMPL_ISUPPORTS(OpaqueResponseBlocker, nsIStreamListener, nsIRequestObserver)
    666 
    667 }  // namespace mozilla::net