// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/loader/cross_site_document_resource_handler.h"

#include <string.h>

#include <algorithm>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/loader/detachable_resource_handler.h"
#include "content/browser/loader/resource_request_info_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/resource_context.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "net/base/io_buffer.h"
#include "net/base/mime_sniffer.h"
#include "net/url_request/url_request.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"

using MimeType = network::CrossOriginReadBlocking::MimeType;

namespace content {

namespace {

// An IOBuffer to enable writing into a existing IOBuffer at a given offset.
class LocalIoBufferWithOffset : public net::WrappedIOBuffer {
 public:
  LocalIoBufferWithOffset(net::IOBuffer* buf, int offset)
      : net::WrappedIOBuffer(buf->data() + offset), buf_(buf) {}

 private:
  ~LocalIoBufferWithOffset() override {}

  scoped_refptr<net::IOBuffer> buf_;
};

}  // namespace

// static
void CrossSiteDocumentResourceHandler::LogBlockedResponseOnUIThread(
    ResourceRequestInfo::WebContentsGetter web_contents_getter,
    bool needed_sniffing,
    MimeType canonical_mime_type,
    ResourceType resource_type,
    int http_response_code,
    int64_t content_length) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  WebContents* web_contents = std::move(web_contents_getter).Run();
  if (!web_contents)
    return;

  ukm::SourceId source_id = static_cast<WebContentsImpl*>(web_contents)
                                ->GetUkmSourceIdForLastCommittedSource();
  ukm::builders::SiteIsolation_XSD_Browser_Blocked(source_id)
      .SetCanonicalMimeType(static_cast<int64_t>(canonical_mime_type))
      .SetContentLengthWasZero(content_length == 0)
      .SetContentResourceType(resource_type)
      .SetHttpResponseCode(http_response_code)
      .SetNeededSniffing(needed_sniffing)
      .Record(ukm::UkmRecorder::Get());
}

void CrossSiteDocumentResourceHandler::LogBlockedResponse(
    ResourceRequestInfoImpl* resource_request_info,
    int http_response_code) {
  DCHECK(resource_request_info);
  DCHECK(analyzer_);
  DCHECK_NE(network::CrossOriginReadBlocking::MimeType::kInvalidMimeType,
            analyzer_->canonical_mime_type());

  analyzer_->LogBlockedResponse();

  ResourceType resource_type = resource_request_info->GetResourceType();
  UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked", resource_type,
                            content::RESOURCE_TYPE_LAST_TYPE);
  switch (analyzer_->canonical_mime_type()) {
    case MimeType::kHtml:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.HTML",
                                resource_type,
                                content::RESOURCE_TYPE_LAST_TYPE);
      break;
    case MimeType::kXml:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.XML",
                                resource_type,
                                content::RESOURCE_TYPE_LAST_TYPE);
      break;
    case MimeType::kJson:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.JSON",
                                resource_type,
                                content::RESOURCE_TYPE_LAST_TYPE);
      break;
    case MimeType::kPlain:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Plain",
                                resource_type,
                                content::RESOURCE_TYPE_LAST_TYPE);
      break;
    case MimeType::kOthers:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolation.XSD.Browser.Blocked.Others",
                                resource_type,
                                content::RESOURCE_TYPE_LAST_TYPE);
      break;
    default:
      NOTREACHED();
  }
  if (analyzer_->found_parser_breaker()) {
    UMA_HISTOGRAM_ENUMERATION(
        "SiteIsolation.XSD.Browser.BlockedForParserBreaker", resource_type,
        content::RESOURCE_TYPE_LAST_TYPE);
  }

  // The last committed URL is only available on the UI thread - we need to hop
  // onto the UI thread to log an UKM event.  Note that this is racey - by the
  // time the posted task runs, the WebContents could have been closed and/or
  // navigated to another URL.  This is understood and acceptable - this should
  // be rare enough to not matter for the collected UKM data.
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(
          &CrossSiteDocumentResourceHandler::LogBlockedResponseOnUIThread,
          resource_request_info->GetWebContentsGetterForRequest(),
          analyzer_->needs_sniffing(), analyzer_->canonical_mime_type(),
          resource_type, http_response_code, analyzer_->content_length()));
}

// ResourceController that runs a closure on Resume(), and forwards failures
// back to CrossSiteDocumentHandler. The closure can optionally be run as
// a PostTask.
class CrossSiteDocumentResourceHandler::Controller : public ResourceController {
 public:
  explicit Controller(CrossSiteDocumentResourceHandler* document_handler,
                      bool post_task,
                      base::OnceClosure resume_callback)
      : document_handler_(document_handler),
        resume_callback_(std::move(resume_callback)),
        post_task_(post_task) {}

  ~Controller() override {}

  // ResourceController implementation:
  void Resume() override {
    MarkAsUsed();

    if (post_task_) {
      base::ThreadTaskRunnerHandle::Get()->PostTask(
          FROM_HERE, std::move(resume_callback_));
    } else {
      std::move(resume_callback_).Run();
    }
  }

  void ResumeForRedirect(const base::Optional<net::HttpRequestHeaders>&
                             modified_request_headers) override {
    DCHECK(!modified_request_headers.has_value())
        << "Redirect with modified headers was not supported yet. "
           "crbug.com/845683";
    Resume();
  }

  void Cancel() override {
    MarkAsUsed();
    document_handler_->Cancel();
  }

  void CancelWithError(int error_code) override {
    MarkAsUsed();
    document_handler_->CancelWithError(error_code);
  }

 private:
  void MarkAsUsed() {
#if DCHECK_IS_ON()
    DCHECK(!used_);
    used_ = true;
#endif
  }

#if DCHECK_IS_ON()
  bool used_ = false;
#endif

  CrossSiteDocumentResourceHandler* document_handler_;

  // Runs on Resume().
  base::OnceClosure resume_callback_;
  bool post_task_;

  DISALLOW_COPY_AND_ASSIGN(Controller);
};

CrossSiteDocumentResourceHandler::CrossSiteDocumentResourceHandler(
    std::unique_ptr<ResourceHandler> next_handler,
    net::URLRequest* request,
    bool is_nocors_plugin_request)
    : LayeredResourceHandler(request, std::move(next_handler)),
      weak_next_handler_(next_handler_.get()),
      is_nocors_plugin_request_(is_nocors_plugin_request),
      weak_this_(this) {}

CrossSiteDocumentResourceHandler::~CrossSiteDocumentResourceHandler() {}

void CrossSiteDocumentResourceHandler::OnResponseStarted(
    network::ResourceResponse* response,
    std::unique_ptr<ResourceController> controller) {
  has_response_started_ = true;

  network::CrossOriginReadBlocking::LogAction(
      network::CrossOriginReadBlocking::Action::kResponseStarted);

  should_block_based_on_headers_ = ShouldBlockBasedOnHeaders(*response);

  // If blocking is possible, postpone forwarding |response| to the
  // |next_handler_|, until we have made the allow-vs-block decision
  // (which might need more time depending on the sniffing needs).
  if (should_block_based_on_headers_) {
    pending_response_start_ = response;
    controller->Resume();
  } else {
    next_handler_->OnResponseStarted(response, std::move(controller));
  }
}

void CrossSiteDocumentResourceHandler::OnWillRead(
    scoped_refptr<net::IOBuffer>* buf,
    int* buf_size,
    std::unique_ptr<ResourceController> controller) {
  // For allowed responses, the data is directly streamed to the next handler.
  // Note that OnWillRead may be called before OnResponseStarted (because the
  // MimeSniffingResourceHandler upstream changes the order of the calls) - this
  // means that |has_response_started_| has to be explicitly checked below.
  if (has_response_started_ &&
      (!should_block_based_on_headers_ || allow_based_on_sniffing_)) {
    DCHECK(!local_buffer_);
    next_handler_->OnWillRead(buf, buf_size, std::move(controller));
    return;
  }

  // If |local_buffer_| exists, continue buffering data into the end of it.
  if (local_buffer_) {
    // Check that we still have room for more local bufferring.
    DCHECK_GT(next_handler_buffer_size_, local_buffer_bytes_read_);
    *buf = new LocalIoBufferWithOffset(local_buffer_.get(),
                                       local_buffer_bytes_read_);
    *buf_size = next_handler_buffer_size_ - local_buffer_bytes_read_;
    controller->Resume();
    return;
  }

  // On the next read attempt after the response was blocked, either cancel the
  // rest of the request or allow it to proceed in a detached state.
  if (blocked_read_completed_) {
    DCHECK(should_block_based_on_headers_);
    DCHECK(!allow_based_on_sniffing_);
    const ResourceRequestInfoImpl* info = GetRequestInfo();
    if (info && info->detachable_handler()) {
      // Ensure that prefetch, etc, continue to cache the response, without
      // sending it to the renderer.
      info->detachable_handler()->Detach();
    } else {
      // If it's not detachable, cancel the rest of the request.
      controller->Cancel();
    }
    return;
  }

  // If we haven't yet decided to allow or block the response, we should read
  // the data into a local buffer 1) to temporarily prevent the data from
  // reaching the renderer and 2) to potentially sniff the data to confirm the
  // content type.
  //
  // Since the downstream handler may defer during the OnWillRead call below,
  // the values of |buf| and |buf_size| may not be available right away.
  // Instead, create a Controller that will start the sniffing after the
  // downstream handler has called Resume on it.
  HoldController(std::move(controller));
  controller = std::make_unique<Controller>(
      this, false /* post_task */,
      base::BindOnce(&CrossSiteDocumentResourceHandler::ResumeOnWillRead,
                     weak_this_.GetWeakPtr(), buf, buf_size));
  next_handler_->OnWillRead(buf, buf_size, std::move(controller));
}

void CrossSiteDocumentResourceHandler::ResumeOnWillRead(
    scoped_refptr<net::IOBuffer>* buf,
    int* buf_size) {
  // We should only get here if we haven't yet made a block-vs-allow decision
  // (we get here after downstream handler finishes its work from OnWillRead).
  DCHECK(!allow_based_on_sniffing_);
  DCHECK(!blocked_read_completed_);

  // For most blocked responses, we need to sniff the data to confirm it looks
  // like the claimed MIME type (to avoid blocking mislabeled JavaScript,
  // JSONP, etc).  Read this data into a separate buffer (not shared with the
  // renderer), which we will only copy over if we decide to allow it through.
  // This is only done when we suspect the response should be blocked.
  //
  // Make it as big as the downstream handler's buffer to make it easy to copy
  // over in one operation.
  DCHECK_GE(*buf_size, net::kMaxBytesToSniff * 2);
  local_buffer_ =
      base::MakeRefCounted<net::IOBuffer>(static_cast<size_t>(*buf_size));

  // Store the next handler's buffer but don't read into it while sniffing,
  // since we possibly won't want to send the data to the renderer process.
  next_handler_buffer_ = *buf;
  next_handler_buffer_size_ = *buf_size;
  *buf = local_buffer_;

  Resume();
}

void CrossSiteDocumentResourceHandler::OnReadCompleted(
    int bytes_read,
    std::unique_ptr<ResourceController> controller) {
  DCHECK(has_response_started_);
  DCHECK(!blocked_read_completed_);

  if (!should_block_based_on_headers_) {
    // CrossSiteDocumentResourceHandler always intercepts the buffer allocated
    // by the first call to |next_handler_|'s OnWillRead and passes the
    // |local_buffer_| upstream.  If we decide not to block based on headers,
    // then the data needs to be passed into the |next_handler_|.
    if (local_buffer_) {
      DCHECK_EQ(0, local_buffer_bytes_read_);
      local_buffer_bytes_read_ = bytes_read;
      StopLocalBuffering(true /* = copy_data_to_next_handler */);
    }

    next_handler_->OnReadCompleted(bytes_read, std::move(controller));
    return;
  }

  if (allow_based_on_sniffing_) {
    // If CrossSiteDocumentResourceHandler decided to allow the response based
    // on sniffing, then StopLocalBuffering was already called below by the
    // previous execution of CrossSiteDocumentResourceHandler::OnReadCompleted.
    // From there onward, we just need to foward all the calls to the
    // |next_handler_|.
    DCHECK(!local_buffer_);
    next_handler_->OnReadCompleted(bytes_read, std::move(controller));
    return;
  }

  // If we intended to block the response and haven't sniffed yet, try to
  // confirm that we should block it.  If sniffing is needed, look at the local
  // buffer and either report that zero bytes were read (to indicate the
  // response is empty and complete), or copy the sniffed data to the next
  // handler's buffer and resume the response without blocking.
  bool confirmed_blockable = false;
  ResourceRequestInfoImpl* info = GetRequestInfo();
  if (!analyzer_->needs_sniffing()) {
    // If sniffing is impossible (e.g., because this is a range request), or
    // if sniffing is disabled due to a nosniff header AND the server returned
    // a protected mime type, then we have enough information to block
    // immediately.
    confirmed_blockable = true;
  } else {
    // Sniff the data to see if it likely matches the MIME type that caused us
    // to decide to block it.  If it doesn't match, it may be JavaScript,
    // JSONP, or another allowable data type and we should let it through.
    // Record how many bytes were read to see how often it's too small.  (This
    // will typically be under 100,000.)
    const size_t new_data_offset = local_buffer_bytes_read_;
    local_buffer_bytes_read_ += bytes_read;
    DCHECK_LE(local_buffer_bytes_read_, next_handler_buffer_size_);
    const bool more_data_possible =
        bytes_read != 0 && local_buffer_bytes_read_ < net::kMaxBytesToSniff &&
        local_buffer_bytes_read_ < next_handler_buffer_size_;

    // To ensure determinism with respect to network packet ordering and
    // sizing, never examine more than kMaxBytesToSniff bytes, even if more
    // are available.
    size_t bytes_to_sniff =
        std::min(local_buffer_bytes_read_, net::kMaxBytesToSniff);
    base::StringPiece data(local_buffer_->data(), bytes_to_sniff);

    // If we have some new data, ask the |analyzer_| to sniff it.
    analyzer_->SniffResponseBody(data, new_data_offset);

    const bool confirmed_allowed = analyzer_->ShouldAllow();
    confirmed_blockable = analyzer_->ShouldBlock();
    DCHECK(!(confirmed_blockable && confirmed_allowed));

    // If sniffing didn't yield a conclusive response, and we haven't read too
    // many bytes yet or hit the end of the stream, buffer up some more data.
    if (!(confirmed_blockable || confirmed_allowed) && more_data_possible) {
      controller->Resume();
      return;
    }
  }

  // At this point we have already made a block-vs-allow decision and we know
  // that we can wake the |next_handler_| and let it catch-up with our
  // processing of the response.  The first step will always be calling
  // |next_handler_->OnResponseStarted(...)|, but we need to figure out what
  // other steps need to happen, before we can resume the real response
  // upstream.  These steps will be gathered into |controller|.
  // The last step will always be calling
  // CrossSiteDocumentResourceHandler::Resume.
  HoldController(std::move(controller));
  controller = std::make_unique<Controller>(
      this, false /* post_task */,
      base::BindOnce(&CrossSiteDocumentResourceHandler::Resume,
                     weak_this_.GetWeakPtr()));

  if (confirmed_blockable) {
    // Log the blocking event.  Inline the Serialize call to avoid it when
    // tracing is disabled.
    TRACE_EVENT2("navigation",
                 "CrossSiteDocumentResourceHandler::ShouldBlockResponse",
                 "initiator",
                 request()->initiator().has_value()
                     ? request()->initiator().value().Serialize()
                     : "null",
                 "url", request()->url().spec());

    LogBlockedResponse(info, analyzer_->http_response_code());

    // Block the response and throw away the data.  Report zero bytes read.
    blocked_read_completed_ = true;
    info->set_blocked_response_from_reaching_renderer(true);
    if (analyzer_->ShouldReportBlockedResponse())
      info->set_should_report_corb_blocking(true);
    network::CrossOriginReadBlocking::SanitizeBlockedResponse(
        pending_response_start_);

    // Pass an empty/blocked body onto the next handler.  size of the two
    // buffers is the same (see OnWillRead).  After the next statement,
    // |controller| will store a sequence of steps like this:
    //  - next_handler_->OnReadCompleted(bytes_read = 0)
    //  - ... steps from the old |controller| (typically this->Resume()) ...
    controller = std::make_unique<Controller>(
        this, true /* post_task */,
        base::BindOnce(&ResourceHandler::OnReadCompleted,
                       weak_next_handler_.GetWeakPtr(), 0 /* bytes_read */,
                       std::move(controller)));
    StopLocalBuffering(false /* = copy_data_to_next_handler*/);
  } else {
    // Choose not block this response.
    allow_based_on_sniffing_ = true;

    if (bytes_read == 0 && local_buffer_bytes_read_ != 0) {
      // |bytes_read == 0| indicates the end-of-stream. In this case, we need
      // to synthesize an additional OnWillRead() and OnReadCompleted(0) on
      // |next_handler_|, so that |next_handler_| sees both the full response
      // and the end-of-stream marker.  After the next statement, |controller|
      // will store a sequence of steps like this:
      //  - next_handler_->OnWillRead(...)
      //  - next_handler_->OnReadCompleted(bytes_read = 0)
      //  - ... steps from the old |controller| (typically this->Resume()) ...
      //
      // Note that if |weak_next_handler_| is alive, then |this| should also be
      // alive and therefore it is safe to dereference |&next_handler_buffer_|
      // and |&next_handler_buffer_size_|.
      controller = std::make_unique<Controller>(
          this, false /* post_task */,
          base::BindOnce(
              &ResourceHandler::OnWillRead, weak_next_handler_.GetWeakPtr(),
              &next_handler_buffer_, &next_handler_buffer_size_,
              std::make_unique<Controller>(
                  this, true /* post_task */,
                  base::BindOnce(&ResourceHandler::OnReadCompleted,
                                 weak_next_handler_.GetWeakPtr(),
                                 0 /* bytes_read */, std::move(controller)))));
    }

    // Pass the contents of |local_buffer_| onto the next handler.  Afterwards,
    // |controller| will store a sequence of steps like this:
    //  - next_handler_->OnReadCompleted(local_buffer_bytes_read_)
    //  - ... steps from the old |controller| ...
    controller = std::make_unique<Controller>(
        this, true /* post_task */,
        base::BindOnce(&ResourceHandler::OnReadCompleted,
                       weak_next_handler_.GetWeakPtr(),
                       local_buffer_bytes_read_, std::move(controller)));
    StopLocalBuffering(true /* = copy_data_to_next_handler*/);
  }

  // In both the blocked and allowed cases, we need to resume by notifying the
  // downstream handler about the response start.
  DCHECK(pending_response_start_);
  next_handler_->OnResponseStarted(pending_response_start_.get(),
                                   std::move(controller));
  pending_response_start_ = nullptr;
}

void CrossSiteDocumentResourceHandler::StopLocalBuffering(
    bool copy_data_to_next_handler) {
  DCHECK(has_response_started_);
  DCHECK(!should_block_based_on_headers_ || allow_based_on_sniffing_ ||
         blocked_read_completed_);
  DCHECK(local_buffer_);
  DCHECK(next_handler_buffer_);

  if (copy_data_to_next_handler) {
    // Pass the contents of |local_buffer_| onto the next handler. Note that the
    // size of the two buffers is the same (see OnWillRead).
    DCHECK_LE(local_buffer_bytes_read_, next_handler_buffer_size_);
    memcpy(next_handler_buffer_->data(), local_buffer_->data(),
           local_buffer_bytes_read_);
  }

  local_buffer_ = nullptr;
  local_buffer_bytes_read_ = 0;
  next_handler_buffer_ = nullptr;
  next_handler_buffer_size_ = 0;
}

void CrossSiteDocumentResourceHandler::OnResponseCompleted(
    const net::URLRequestStatus& status,
    std::unique_ptr<ResourceController> controller) {
  if (blocked_read_completed_) {
    // Report blocked responses as successful, rather than the cancellation
    // from OnWillRead.
    next_handler_->OnResponseCompleted(net::URLRequestStatus(),
                                       std::move(controller));
  } else {
    // Only report XSDB status for successful (i.e. non-aborted,
    // non-errored-out) requests.
    if (status.is_success())
      analyzer_->LogAllowedResponse();

    next_handler_->OnResponseCompleted(status, std::move(controller));
  }
}

bool CrossSiteDocumentResourceHandler::ShouldBlockBasedOnHeaders(
    const network::ResourceResponse& response) {
  // Give embedder a chance to skip document blocking for this response.
  const char* initiator_scheme_exception =
      GetContentClient()
          ->browser()
          ->GetInitatorSchemeBypassingDocumentBlocking();

  // Delegate most decisions to CrossOriginReadBlocking::ResponseAnalyzer.
  analyzer_ =
      std::make_unique<network::CrossOriginReadBlocking::ResponseAnalyzer>(
          *request(), response, initiator_scheme_exception);
  if (analyzer_->ShouldAllow())
    return false;

  // Check if the response's site needs to have its documents protected.  By
  // default, this will usually return false.
  // TODO(creis): This check can go away once the logic here is made fully
  // backward compatible and we can enforce it always, regardless of Site
  // Isolation policy.
  switch (SiteIsolationPolicy::IsCrossSiteDocumentBlockingEnabled()) {
    case SiteIsolationPolicy::XSDB_ENABLED_UNCONDITIONALLY:
      break;
    case SiteIsolationPolicy::XSDB_ENABLED_IF_ISOLATED: {
      url::Origin target_origin = url::Origin::Create(request()->url());
      if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites() &&
          !ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedOrigin(
              target_origin)) {
        return false;
      }
      break;
    }
    case SiteIsolationPolicy::XSDB_DISABLED:
      return false;
  }

  // Only block if this is a request made from a renderer process.
  const ResourceRequestInfoImpl* info = GetRequestInfo();
  if (!info || info->GetChildID() == -1)
    return false;

  // Don't block plugin requests.
  // TODO(lukasza): Only disable CORB for plugins with universal access (see
  // PepperURLLoaderHost::has_universal_access_), because only such plugins may
  // have their own CORS-like mechanisms - e.g. crossdomain.xml in Flash).  We
  // should still enforce CORB for other kinds of plugins (i.e. ones without
  // universal access).
  if (info->GetResourceType() == RESOURCE_TYPE_PLUGIN_RESOURCE &&
      is_nocors_plugin_request_) {
    return false;
  }

  return true;
}

}  // namespace content
