// Copyright 2015 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 "base/command_line.h"
#include "base/macros.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/histogram_tester.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/resource_type.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace content {

// These tests simulate exploited renderer processes, which can fetch arbitrary
// resources from other websites, not constrained by the Same Origin Policy.  We
// are trying to verify that the renderer cannot fetch any cross-site document
// responses even when the Same Origin Policy is turned off inside the renderer.
class SiteIsolationStatsGathererBrowserTest
    : public ContentBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  SiteIsolationStatsGathererBrowserTest() {}
  ~SiteIsolationStatsGathererBrowserTest() override {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
    // which is required below. This cannot invoke Start() however as that kicks
    // off the "EmbeddedTestServer IO Thread" which then races with
    // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());

    // Add a host resolver rule to map all outgoing requests to the test server.
    // This allows us to use "real" hostnames in URLs, which we can use to
    // create arbitrary SiteInstances.
    command_line->AppendSwitchASCII(
        switches::kHostResolverRules,
        "MAP * " + embedded_test_server()->host_port_pair().ToString() +
            ",EXCLUDE localhost");

    // Since we assume exploited renderer process, it can bypass the same origin
    // policy at will. Simulate that by passing the disable-web-security flag.
    command_line->AppendSwitch(switches::kDisableWebSecurity);

    if (GetParam()) {
      command_line->AppendSwitchASCII("--enable-blink-features",
                                      "LoadingWithMojo");
    }
  }

  void SetUpOnMainThread() override {
    // Complete the manual Start() after ContentBrowserTest's own
    // initialization, ref. comment on InitializeAndListen() above.
    embedded_test_server()->StartAcceptingConnections();
  }

  void InspectHistograms(const base::HistogramTester& histograms,
                         bool should_be_blocked,
                         const std::string& resource_name) {
    std::string bucket;
    int mime_type = 0;  // Hardcoded because histogram enums mustn't change.
    if (base::MatchPattern(resource_name, "*.html")) {
      bucket = "HTML";
      mime_type = 0;
    } else if (base::MatchPattern(resource_name, "*.xml")) {
      bucket = "XML";
      mime_type = 1;
    } else if (base::MatchPattern(resource_name, "*.json")) {
      bucket = "JSON";
      mime_type = 2;
    } else if (base::MatchPattern(resource_name, "*.txt")) {
      bucket = "Plain";
      mime_type = 3;
      if (base::MatchPattern(resource_name, "json*")) {
        bucket += ".JSON";
      } else if (base::MatchPattern(resource_name, "html*")) {
        bucket += ".HTML";
      } else if (base::MatchPattern(resource_name, "xml*")) {
        bucket += ".XML";
      }
    } else {
      FAIL();
    }
    FetchHistogramsFromChildProcesses();

    // A few histograms are incremented unconditionally.
    histograms.ExpectUniqueSample("SiteIsolation.AllResponses", 1, 1);
    base::HistogramTester::CountsMap expected_metrics;
    expected_metrics["SiteIsolation.XSD.DataLength"] = 1;
    expected_metrics["SiteIsolation.XSD.MimeType"] = 1;

    // Determine the appropriate conditionally-incremented histograms.
    std::string base = "SiteIsolation.XSD." + bucket;
    if (should_be_blocked) {
      expected_metrics[base + ".Blocked"] = 1;
      expected_metrics[base + ".Blocked.RenderableStatusCode2"] = 1;
    } else {
      expected_metrics[base + ".NotBlocked"] = 1;
      if (base::MatchPattern(resource_name, "*js.*")) {
        expected_metrics[base + ".NotBlocked.MaybeJS"] = 1;
      }
    }

    // Make sure that the expected metrics, and only those metrics, were
    // incremented.
    EXPECT_THAT(histograms.GetTotalCountsForPrefix("SiteIsolation.XSD."),
                testing::ContainerEq(expected_metrics))
        << "For resource_name=" << resource_name
        << ", should_be_blocked=" << should_be_blocked;

    EXPECT_THAT(histograms.GetAllSamples("SiteIsolation.XSD.MimeType"),
                testing::ElementsAre(base::Bucket(mime_type, 1)))
        << "The wrong mime type bucket was incremented.";
    if (should_be_blocked) {
      static_assert(13 == RESOURCE_TYPE_XHR, "Histogram enums mustn't change.");
      EXPECT_THAT(
          histograms.GetAllSamples(base + ".Blocked.RenderableStatusCode2"),
          testing::ElementsAre(base::Bucket(RESOURCE_TYPE_XHR, 1)))
          << "The wrong RenderableStatusCode2 bucket was incremented.";
    }
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(SiteIsolationStatsGathererBrowserTest);
};

IN_PROC_BROWSER_TEST_P(SiteIsolationStatsGathererBrowserTest,
                       CrossSiteDocumentBlockingForMimeType) {
  // Load a page that issues illegal cross-site document requests to bar.com.
  // The page uses XHR to request HTML/XML/JSON documents from bar.com, and
  // inspects if any of them were successfully received. Currently, on illegal
  // access, the XHR requests should succeed, but the UMA histograms should
  // record that they would have been blocked. This test is only possible since
  // we run the browser without the same origin policy.
  GURL foo("http://foo.com/cross_site_document_request.html");

  NavigateToURL(shell(), foo);

  // Flush out existing histogram activity.
  FetchHistogramsFromChildProcesses();

  // The following are files under content/test/data/site_isolation. All
  // should be disallowed for cross site XHR under the document blocking policy.
  const char* blocked_resources[] = {
      "comment_valid.html",
      "html.txt",
      "html4_dtd.html",
      "html4_dtd.txt",
      "html5_dtd.html",
      "html5_dtd.txt",
      "json.txt",
      "valid.html",
      "valid.json",
      "valid.xml",
      "xml.txt",
  };

  for (const char* resource : blocked_resources) {
    SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource));
    base::HistogramTester histograms;

    bool was_blocked;
    ASSERT_TRUE(ExecuteScriptAndExtractBool(
        shell(), base::StringPrintf("sendRequest(\"%s\");", resource),
        &was_blocked));
    ASSERT_FALSE(was_blocked);

    InspectHistograms(histograms, true, resource);
  }

  // These files should be allowed for XHR under the document blocking policy.
  const char* allowed_resources[] = {"js.html",
                                     "comment_js.html",
                                     "js.xml",
                                     "js.json",
                                     "js.txt",
                                     "img.html",
                                     "img.xml",
                                     "img.json",
                                     "img.txt",
                                     "comment_js.html"};
  for (const char* resource : allowed_resources) {
    SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource));
    base::HistogramTester histograms;

    bool was_blocked;
    ASSERT_TRUE(ExecuteScriptAndExtractBool(
        shell(), base::StringPrintf("sendRequest(\"%s\");", resource),
        &was_blocked));
    ASSERT_FALSE(was_blocked);

    InspectHistograms(histograms, false, resource);
  }
}

IN_PROC_BROWSER_TEST_P(SiteIsolationStatsGathererBrowserTest,
                       CrossSiteDocumentBlockingForDifferentTargets) {
  // This webpage loads a cross-site HTML page in different targets such as
  // <img>,<link>,<embed>, etc. Since the requested document is blocked, and one
  // character string (' ') is returned instead, this tests that the renderer
  // does not crash even when it receives a response body which is " ", whose
  // length is different from what's described in "content-length" for such
  // different targets.

  // TODO(nick): Split up these cases, and add positive assertions here about
  // what actually happens in these various resource-block cases.
  GURL foo("http://foo.com/cross_site_document_request_target.html");
  NavigateToURL(shell(), foo);
}

INSTANTIATE_TEST_CASE_P(SiteIsolationStatsGathererBrowserTest,
                        SiteIsolationStatsGathererBrowserTest,
                        ::testing::Values(false, true));

}  // namespace content
