// Copyright 2013 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/child/site_isolation_policy.h"

#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/resource_response_info.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/http/http_response_headers.h"

using base::StringPiece;

namespace content {

namespace {

// The cross-site document blocking/UMA data collection is deactivated by
// default, and only activated in renderer processes.
static bool g_policy_enabled = false;

// MIME types
const char kTextHtml[] = "text/html";
const char kTextXml[] = "text/xml";
const char xAppRssXml[] = "application/rss+xml";
const char kAppXml[] = "application/xml";
const char kAppJson[] = "application/json";
const char kTextJson[] = "text/json";
const char kTextXjson[] = "text/x-json";
const char kTextPlain[] = "text/plain";

// TODO(dsjang): this is only needed for collecting UMA stat. Will be deleted
// when this class is used for actual blocking.
bool IsRenderableStatusCode(int status_code) {
  // Chrome only uses the content of a response with one of these status codes
  // for CSS/JavaScript. For images, Chrome just ignores status code.
  const int renderable_status_code[] = {200, 201, 202, 203, 206, 300,
                                        301, 302, 303, 305, 306, 307};
  for (size_t i = 0; i < arraysize(renderable_status_code); ++i) {
    if (renderable_status_code[i] == status_code)
      return true;
  }
  return false;
}

bool MatchesSignature(StringPiece data,
                      const StringPiece signatures[],
                      size_t arr_size) {

  size_t offset = data.find_first_not_of(" \t\r\n");
  // There is no not-whitespace character in this document.
  if (offset == base::StringPiece::npos)
    return false;

  data.remove_prefix(offset);
  size_t length = data.length();

  for (size_t sig_index = 0; sig_index < arr_size; ++sig_index) {
    const StringPiece& signature = signatures[sig_index];
    size_t signature_length = signature.length();
    if (length < signature_length)
      continue;

    if (LowerCaseEqualsASCII(
            data.begin(), data.begin() + signature_length, signature.data()))
      return true;
  }
  return false;
}

void IncrementHistogramCount(const std::string& name) {
  // The default value of min, max, bucket_count are copied from histogram.h.
  base::HistogramBase* histogram_pointer = base::Histogram::FactoryGet(
      name, 1, 100000, 50, base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram_pointer->Add(1);
}

void IncrementHistogramEnum(const std::string& name,
                          uint32 sample,
                          uint32 boundary_value) {
  // The default value of min, max, bucket_count are copied from histogram.h.
  base::HistogramBase* histogram_pointer = base::LinearHistogram::FactoryGet(
      name,
      1,
      boundary_value,
      boundary_value + 1,
      base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram_pointer->Add(sample);
}

void HistogramCountBlockedResponse(
    const std::string& bucket_prefix,
    linked_ptr<SiteIsolationResponseMetaData>& resp_data,
    bool nosniff_block) {
  std::string block_label(nosniff_block ? ".NoSniffBlocked" : ".Blocked");
  IncrementHistogramCount(bucket_prefix + block_label);

  // The content is blocked if it is sniffed as HTML/JSON/XML. When
  // the blocked response is with an error status code, it is not
  // disruptive for the following reasons : 1) the blocked content is
  // not a binary object (such as an image) since it is sniffed as
  // text; 2) then, this blocking only breaks the renderer behavior
  // only if it is either JavaScript or CSS. However, the renderer
  // doesn't use the contents of JS/CSS with unaffected status code
  // (e.g, 404). 3) the renderer is expected not to use the cross-site
  // document content for purposes other than JS/CSS (e.g, XHR).
  bool renderable_status_code =
      IsRenderableStatusCode(resp_data->http_status_code);

  if (renderable_status_code) {
    IncrementHistogramEnum(
        bucket_prefix + block_label + ".RenderableStatusCode",
        resp_data->resource_type,
        ResourceType::LAST_TYPE);
  } else {
    IncrementHistogramCount(bucket_prefix + block_label +
                            ".NonRenderableStatusCode");
  }
}

void HistogramCountNotBlockedResponse(const std::string& bucket_prefix,
                                      bool sniffed_as_js) {
  IncrementHistogramCount(bucket_prefix + ".NotBlocked");
  if (sniffed_as_js)
    IncrementHistogramCount(bucket_prefix + ".NotBlocked.MaybeJS");
}

}  // namespace

SiteIsolationResponseMetaData::SiteIsolationResponseMetaData() {}

void SiteIsolationPolicy::SetPolicyEnabled(bool enabled) {
  g_policy_enabled = enabled;
}

linked_ptr<SiteIsolationResponseMetaData>
SiteIsolationPolicy::OnReceivedResponse(const GURL& frame_origin,
                                        const GURL& response_url,
                                        ResourceType::Type resource_type,
                                        int origin_pid,
                                        const ResourceResponseInfo& info) {
  if (!g_policy_enabled)
    return linked_ptr<SiteIsolationResponseMetaData>();

  // if |origin_pid| is non-zero, it means that this response is for a plugin
  // spawned from this renderer process. We exclude responses for plugins for
  // now, but eventually, we're going to make plugin processes directly talk to
  // the browser process so that we don't apply cross-site document blocking to
  // them.
  if (origin_pid)
    return linked_ptr<SiteIsolationResponseMetaData>();

  UMA_HISTOGRAM_COUNTS("SiteIsolation.AllResponses", 1);

  // See if this is for navigation. If it is, don't block it, under the
  // assumption that we will put it in an appropriate process.
  if (ResourceType::IsFrame(resource_type))
    return linked_ptr<SiteIsolationResponseMetaData>();

  if (!IsBlockableScheme(response_url))
    return linked_ptr<SiteIsolationResponseMetaData>();

  if (IsSameSite(frame_origin, response_url))
    return linked_ptr<SiteIsolationResponseMetaData>();

  SiteIsolationResponseMetaData::CanonicalMimeType canonical_mime_type =
      GetCanonicalMimeType(info.mime_type);

  if (canonical_mime_type == SiteIsolationResponseMetaData::Others)
    return linked_ptr<SiteIsolationResponseMetaData>();

  // Every CORS request should have the Access-Control-Allow-Origin header even
  // if it is preceded by a pre-flight request. Therefore, if this is a CORS
  // request, it has this header.  response.httpHeaderField() internally uses
  // case-insensitive matching for the header name.
  std::string access_control_origin;

  // We can use a case-insensitive header name for EnumerateHeader().
  info.headers->EnumerateHeader(
      NULL, "access-control-allow-origin", &access_control_origin);
  if (IsValidCorsHeaderSet(frame_origin, response_url, access_control_origin))
    return linked_ptr<SiteIsolationResponseMetaData>();

  // Real XSD data collection starts from here.
  std::string no_sniff;
  info.headers->EnumerateHeader(NULL, "x-content-type-options", &no_sniff);

  linked_ptr<SiteIsolationResponseMetaData> resp_data(
      new SiteIsolationResponseMetaData);
  resp_data->frame_origin = frame_origin.spec();
  resp_data->response_url = response_url;
  resp_data->resource_type = resource_type;
  resp_data->canonical_mime_type = canonical_mime_type;
  resp_data->http_status_code = info.headers->response_code();
  resp_data->no_sniff = LowerCaseEqualsASCII(no_sniff, "nosniff");

  return resp_data;
}

bool SiteIsolationPolicy::ShouldBlockResponse(
    linked_ptr<SiteIsolationResponseMetaData>& resp_data,
    const char* raw_data,
    int raw_length,
    std::string* alternative_data) {
  if (!g_policy_enabled)
    return false;

  DCHECK(resp_data.get());

  StringPiece data(raw_data, raw_length);

  // Record the length of the first received network packet to see if it's
  // enough for sniffing.
  UMA_HISTOGRAM_COUNTS("SiteIsolation.XSD.DataLength", raw_length);

  // Record the number of cross-site document responses with a specific mime
  // type (text/html, text/xml, etc).
  UMA_HISTOGRAM_ENUMERATION(
      "SiteIsolation.XSD.MimeType",
      resp_data->canonical_mime_type,
      SiteIsolationResponseMetaData::MaxCanonicalMimeType);

  // Store the result of cross-site document blocking analysis.
  bool is_blocked = false;
  bool sniffed_as_js = SniffForJS(data);

  // Record the number of responses whose content is sniffed for what its mime
  // type claims it to be. For example, we apply a HTML sniffer for a document
  // tagged with text/html here. Whenever this check becomes true, we'll block
  // the response.
  if (resp_data->canonical_mime_type !=
          SiteIsolationResponseMetaData::Plain) {
    std::string bucket_prefix;
    bool sniffed_as_target_document = false;
    if (resp_data->canonical_mime_type ==
            SiteIsolationResponseMetaData::HTML) {
      bucket_prefix = "SiteIsolation.XSD.HTML";
      sniffed_as_target_document = SniffForHTML(data);
    } else if (resp_data->canonical_mime_type ==
                   SiteIsolationResponseMetaData::XML) {
      bucket_prefix = "SiteIsolation.XSD.XML";
      sniffed_as_target_document = SniffForXML(data);
    } else if (resp_data->canonical_mime_type ==
                   SiteIsolationResponseMetaData::JSON) {
      bucket_prefix = "SiteIsolation.XSD.JSON";
      sniffed_as_target_document = SniffForJSON(data);
    } else {
      NOTREACHED() << "Not a blockable mime type: "
                   << resp_data->canonical_mime_type;
    }

    if (sniffed_as_target_document) {
      is_blocked = true;
      HistogramCountBlockedResponse(bucket_prefix, resp_data, false);
    } else {
      if (resp_data->no_sniff) {
        is_blocked = true;
        HistogramCountBlockedResponse(bucket_prefix, resp_data, true);
      } else {
        HistogramCountNotBlockedResponse(bucket_prefix, sniffed_as_js);
      }
    }
  } else {
    // This block is for plain text documents. We apply our HTML, XML,
    // and JSON sniffer to a text document in the order, and block it
    // if any of them succeeds in sniffing.
    std::string bucket_prefix;
    if (SniffForHTML(data))
      bucket_prefix = "SiteIsolation.XSD.Plain.HTML";
    else if (SniffForXML(data))
      bucket_prefix = "SiteIsolation.XSD.Plain.XML";
    else if (SniffForJSON(data))
      bucket_prefix = "SiteIsolation.XSD.Plain.JSON";

    if (bucket_prefix.size() > 0) {
      is_blocked = true;
      HistogramCountBlockedResponse(bucket_prefix, resp_data, false);
    } else if (resp_data->no_sniff) {
      is_blocked = true;
      HistogramCountBlockedResponse("SiteIsolation.XSD.Plain", resp_data, true);
    } else {
      HistogramCountNotBlockedResponse("SiteIsolation.XSD.Plain",
                                       sniffed_as_js);
    }
  }

  if (!CommandLine::ForCurrentProcess()->HasSwitch(
           switches::kBlockCrossSiteDocuments))
    is_blocked = false;

  if (is_blocked) {
    alternative_data->erase();
    alternative_data->insert(0, " ");
    LOG(ERROR) << resp_data->response_url
               << " is blocked as an illegal cross-site document from "
               << resp_data->frame_origin;
  }
  return is_blocked;
}

SiteIsolationResponseMetaData::CanonicalMimeType
SiteIsolationPolicy::GetCanonicalMimeType(const std::string& mime_type) {
  if (LowerCaseEqualsASCII(mime_type, kTextHtml)) {
    return SiteIsolationResponseMetaData::HTML;
  }

  if (LowerCaseEqualsASCII(mime_type, kTextPlain)) {
    return SiteIsolationResponseMetaData::Plain;
  }

  if (LowerCaseEqualsASCII(mime_type, kAppJson) ||
      LowerCaseEqualsASCII(mime_type, kTextJson) ||
      LowerCaseEqualsASCII(mime_type, kTextXjson)) {
    return SiteIsolationResponseMetaData::JSON;
  }

  if (LowerCaseEqualsASCII(mime_type, kTextXml) ||
      LowerCaseEqualsASCII(mime_type, xAppRssXml) ||
      LowerCaseEqualsASCII(mime_type, kAppXml)) {
    return SiteIsolationResponseMetaData::XML;
  }

 return SiteIsolationResponseMetaData::Others;
}

bool SiteIsolationPolicy::IsBlockableScheme(const GURL& url) {
  // We exclude ftp:// from here. FTP doesn't provide a Content-Type
  // header which our policy depends on, so we cannot protect any
  // document from FTP servers.
  return url.SchemeIs("http") || url.SchemeIs("https");
}

bool SiteIsolationPolicy::IsSameSite(const GURL& frame_origin,
                                     const GURL& response_url) {

  if (!frame_origin.is_valid() || !response_url.is_valid())
    return false;

  if (frame_origin.scheme() != response_url.scheme())
    return false;

  // SameDomainOrHost() extracts the effective domains (public suffix plus one)
  // from the two URLs and compare them.
  return net::registry_controlled_domains::SameDomainOrHost(
      frame_origin,
      response_url,
      net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
}

// We don't use Webkit's existing CORS policy implementation since
// their policy works in terms of origins, not sites. For example,
// when frame is sub.a.com and it is not allowed to access a document
// with sub1.a.com. But under Site Isolation, it's allowed.
bool SiteIsolationPolicy::IsValidCorsHeaderSet(
    const GURL& frame_origin,
    const GURL& website_origin,
    const std::string& access_control_origin) {
  // Many websites are sending back "\"*\"" instead of "*". This is
  // non-standard practice, and not supported by Chrome. Refer to
  // CrossOriginAccessControl::passesAccessControlCheck().

  // TODO(dsjang): * is not allowed for the response from a request
  // with cookies. This allows for more than what the renderer will
  // eventually be able to receive, so we won't see illegal cross-site
  // documents allowed by this. We have to find a way to see if this
  // response is from a cookie-tagged request or not in the future.
  if (access_control_origin == "*")
    return true;

  // TODO(dsjang): The CORS spec only treats a fully specified URL, except for
  // "*", but many websites are using just a domain for access_control_origin,
  // and this is blocked by Webkit's CORS logic here :
  // CrossOriginAccessControl::passesAccessControlCheck(). GURL is set
  // is_valid() to false when it is created from a URL containing * in the
  // domain part.

  GURL cors_origin(access_control_origin);
  return IsSameSite(frame_origin, cors_origin);
}

// This function is a slight modification of |net::SniffForHTML|.
bool SiteIsolationPolicy::SniffForHTML(StringPiece data) {
  // The content sniffer used by Chrome and Firefox are using "<!--"
  // as one of the HTML signatures, but it also appears in valid
  // JavaScript, considered as well-formed JS by the browser.  Since
  // we do not want to block any JS, we exclude it from our HTML
  // signatures. This can weaken our document block policy, but we can
  // break less websites.
  // TODO(dsjang): parameterize |net::SniffForHTML| with an option
  // that decides whether to include <!-- or not, so that we can
  // remove this function.
  // TODO(dsjang): Once SiteIsolationPolicy is moved into the browser
  // process, we should do single-thread checking here for the static
  // initializer.
  static const StringPiece kHtmlSignatures[] = {
    StringPiece("<!DOCTYPE html"),  // HTML5 spec
    StringPiece("<script"),  // HTML5 spec, Mozilla
    StringPiece("<html"),    // HTML5 spec, Mozilla
    StringPiece("<head"),    // HTML5 spec, Mozilla
    StringPiece("<iframe"),  // Mozilla
    StringPiece("<h1"),      // Mozilla
    StringPiece("<div"),     // Mozilla
    StringPiece("<font"),    // Mozilla
    StringPiece("<table"),   // Mozilla
    StringPiece("<a"),       // Mozilla
    StringPiece("<style"),   // Mozilla
    StringPiece("<title"),   // Mozilla
    StringPiece("<b"),       // Mozilla
    StringPiece("<body"),    // Mozilla
    StringPiece("<br"),      // Mozilla
    StringPiece("<p"),       // Mozilla
    StringPiece("<?xml")     // Mozilla
  };

  while (data.length() > 0) {
    if (MatchesSignature(
          data, kHtmlSignatures, arraysize(kHtmlSignatures)))
      return true;

    // If we cannot find "<!--", we fail sniffing this as HTML.
    static const StringPiece kCommentBegins[] = { StringPiece("<!--") };
    if (!MatchesSignature(data, kCommentBegins, arraysize(kCommentBegins)))
      break;

    // Search for --> and do SniffForHTML after that. If we can find the
    // comment's end, we start HTML sniffing from there again.
    static const char kEndComment[] = "-->";
    size_t offset = data.find(kEndComment);
    if (offset == base::StringPiece::npos)
      break;

    // Proceed to the index next to the ending comment (-->).
    data.remove_prefix(offset + strlen(kEndComment));
  }

  return false;
}

bool SiteIsolationPolicy::SniffForXML(base::StringPiece data) {
  // TODO(dsjang): Chrome's mime_sniffer is using strncasecmp() for
  // this signature. However, XML is case-sensitive. Don't we have to
  // be more lenient only to block documents starting with the exact
  // string <?xml rather than <?XML ?
  // TODO(dsjang): Once SiteIsolationPolicy is moved into the browser
  // process, we should do single-thread checking here for the static
  // initializer.
  static const StringPiece kXmlSignatures[] = { StringPiece("<?xml") };
  return MatchesSignature(data, kXmlSignatures, arraysize(kXmlSignatures));
}

bool SiteIsolationPolicy::SniffForJSON(base::StringPiece data) {
  // TODO(dsjang): We have to come up with a better way to sniff
  // JSON. However, even RE cannot help us that much due to the fact
  // that we don't do full parsing.  This DFA starts with state 0, and
  // finds {, "/' and : in that order. We're avoiding adding a
  // dependency on a regular expression library.
  enum {
    kStartState,
    kLeftBraceState,
    kLeftQuoteState,
    kColonState,
    kTerminalState,
  } state = kStartState;

  size_t length = data.length();
  for (size_t i = 0; i < length && state < kColonState; ++i) {
    const char c = data[i];
    if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
      continue;

    switch (state) {
      case kStartState:
        if (c == '{')
          state = kLeftBraceState;
        else
          state = kTerminalState;
        break;
      case kLeftBraceState:
        if (c == '\"' || c == '\'')
          state = kLeftQuoteState;
        else
          state = kTerminalState;
        break;
      case kLeftQuoteState:
        if (c == ':')
          state = kColonState;
        break;
      case kColonState:
      case kTerminalState:
        NOTREACHED();
        break;
    }
  }
  return state == kColonState;
}

bool SiteIsolationPolicy::SniffForJS(StringPiece data) {
  // TODO(dsjang): This is a real hack. The only purpose of this function is to
  // try to see if there's any possibility that this data can be JavaScript
  // (superset of JS). This function will be removed once UMA stats are
  // gathered.

  // Search for "var " for JS detection.
  return data.find("var ") != base::StringPiece::npos;
}

}  // namespace content