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

#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// Returns true if |window.scriptExecuted| is true for the given frame.
bool WasFrameWithScriptLoaded(content::RenderFrameHost* render_frame_host) {
  if (!render_frame_host) {
    return false;
  }
  return content::EvalJs(render_frame_host, "!!window.scriptExecuted")
      .ExtractBool();
}

class ExtensionCSPBypassTest : public ExtensionBrowserTest {
 public:
  ExtensionCSPBypassTest() = default;

  ExtensionCSPBypassTest(const ExtensionCSPBypassTest&) = delete;
  ExtensionCSPBypassTest& operator=(const ExtensionCSPBypassTest&) = delete;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("same-origin.com", "127.0.0.1");
    host_resolver()->AddRule("cross-origin.com", "127.0.0.1");
    ExtensionBrowserTest::SetUpOnMainThread();
    ASSERT_TRUE(embedded_test_server()->Start());
  }

 protected:
  const Extension* AddExtension(bool is_component, bool all_urls_permission) {
    TestExtensionDir dir;

    std::string unique_name = base::StringPrintf(
        "component=%d, all_urls=%d", is_component, all_urls_permission);
    auto manifest =
        base::DictValue()
            .Set("name", unique_name)
            .Set("version", "1")
            .Set("manifest_version", 3)
            .Set("web_accessible_resources",
                 base::ListValue().Append(
                     base::DictValue()
                         .Set("resources", base::ListValue().Append("*"))
                         .Set("matches", base::ListValue().Append("*://*/*"))));

    if (all_urls_permission) {
      manifest.Set("host_permissions", base::ListValue().Append("<all_urls>"));
    }
    if (is_component) {
      // LoadExtensionAsComponent requires the manifest to contain a key.
      std::string key;
      EXPECT_TRUE(Extension::ProducePEM(unique_name, &key));
      manifest.Set("key", key);
    }

    dir.WriteFile(FILE_PATH_LITERAL("script.js"), "");
    dir.WriteManifest(manifest);

    const Extension* extension = nullptr;
    if (is_component) {
      extension = LoadExtensionAsComponent(dir.UnpackedPath());
    } else {
      extension = LoadExtension(dir.UnpackedPath());
    }
    CHECK(extension);
    temp_dirs_.push_back(std::move(dir));
    return extension;
  }

  bool CanLoadScript(const Extension* extension) {
    content::RenderFrameHost* render_frame_host =
        GetActiveWebContents()->GetPrimaryMainFrame();
    std::string code = base::StringPrintf(
        R"(
        function canLoadScript() {
          const s = document.createElement('script');
          try {
            s.src = '%s';
            document.body.appendChild(s);
          } catch(e) {
            // Blocked by TrustedTypes CSP.
            return false;
          }

          // Not blocked by CSP.
          return true;
        }
        canLoadScript();
        )",
        extension->GetResourceURL("script.js").spec().c_str());
    return EvalJs(render_frame_host, code).ExtractBool();
  }

  content::RenderFrameHost* GetFrameByName(const std::string& name) {
    return content::FrameMatchingPredicate(
        GetActiveWebContents()->GetPrimaryPage(),
        base::BindRepeating(&content::FrameMatchesName, name));
  }

 private:
  std::vector<TestExtensionDir> temp_dirs_;
};

}  // namespace

IN_PROC_BROWSER_TEST_F(ExtensionCSPBypassTest, LoadWebAccessibleScript) {
  const Extension* component_ext_with_permission = AddExtension(true, true);
  const Extension* component_ext_without_permission = AddExtension(true, false);
  const Extension* ext_with_permission = AddExtension(false, true);
  const Extension* ext_without_permission = AddExtension(false, false);

  // chrome-extension:-URLs can always bypass CSP in normal pages.
  GURL non_webui_url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), non_webui_url));

  EXPECT_TRUE(CanLoadScript(component_ext_with_permission));
  EXPECT_TRUE(CanLoadScript(component_ext_without_permission));
  EXPECT_TRUE(CanLoadScript(ext_with_permission));
  EXPECT_TRUE(CanLoadScript(ext_without_permission));

  // chrome-extension:-URLs can never bypass CSP in WebUI.
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            GURL(chrome::kChromeUIExtensionsURL)));

  EXPECT_FALSE(CanLoadScript(component_ext_with_permission));
  EXPECT_FALSE(CanLoadScript(component_ext_without_permission));
  EXPECT_FALSE(CanLoadScript(ext_with_permission));
  EXPECT_FALSE(CanLoadScript(ext_without_permission));
}

// Tests that an extension can add a cross-origin iframe to a page
// whose CSP disallows iframes. Regression test for https://crbug.com/41129074.
IN_PROC_BROWSER_TEST_F(ExtensionCSPBypassTest, InjectIframe) {
  // Install an extension that can add a cross-origin iframe to a document.
  const Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("csp/add_iframe_extension"));
  ASSERT_TRUE(extension);

  // Navigate to a page that has CSP with 'frame-src: none' to block any
  // iframes. Use the "same-origin.com" hostname as the test will add iframes to
  // "cross-origin.com" to make clear they are cross-origin.
  GURL test_url = embedded_test_server()->GetURL(
      "same-origin.com", "/extensions/csp/page_with_frame_csp.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), test_url));

  // First, verify that adding an iframe to the page from the main world will
  // fail. Add the frame. Its onload event fires even if it's blocked
  // (see https://crbug.com/40361841), and reports back.
  EXPECT_EQ(true, content::EvalJs(GetActiveWebContents(), "addIframe();"));

  // Use WasFrameWithScriptLoaded() to check whether the target frame really
  // loaded.
  content::RenderFrameHost* frame = GetFrameByName("added-by-page");
  ASSERT_TRUE(frame);
  EXPECT_FALSE(WasFrameWithScriptLoaded(frame));

  // Second, verify that adding an iframe to the page from the extension will
  // succeed. Click a button whose event handler runs in the extension's world
  // which bypasses CSP, and adds the iframe.
  content::DOMMessageQueue message_queue;
  EXPECT_TRUE(
      content::ExecJs(GetActiveWebContents(),
                      "document.querySelector('#addIframeButton').click();"));
  std::string ack;
  EXPECT_TRUE(message_queue.WaitForMessage(&ack));
  EXPECT_EQ("true", ack);
  frame = GetFrameByName("added-by-extension");
  ASSERT_TRUE(frame);
  EXPECT_TRUE(WasFrameWithScriptLoaded(frame));
}

// CSP:frame-ancestor is not bypassed by extensions.
IN_PROC_BROWSER_TEST_F(ExtensionCSPBypassTest, FrameAncestors) {
  std::string manifest = R"(
    {
      "name": "CSP frame-ancestors",
      "manifest_version": 3,
      "version": "0.1",
      "action": {
       "default_popup": "popup.html"
      }
    }
  )";

  std::string popup = R"(
    <!doctype html>
    <html>
      <iframe src = "$1"></iframe>
    </html>
  )";

  GURL iframe_url = embedded_test_server()->GetURL(
      "/extensions/csp/frame-ancestors-none.html");
  popup = base::ReplaceStringPlaceholders(popup, {iframe_url.spec()}, nullptr);

  TestExtensionDir test_dir;
  test_dir.WriteManifest(manifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), popup);

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  content::WebContentsConsoleObserver console_observer(GetActiveWebContents());
  console_observer.SetPattern("Framing '*' violates the following*");

  GURL popup_url = extension->GetResourceURL("popup.html");
  ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(), popup_url));

  // The iframe must be blocked because of CSP.
  ASSERT_TRUE(console_observer.Wait());
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();
  content::RenderFrameHost* child_frame = ChildFrameAt(main_frame, 0);
  EXPECT_EQ(popup_url, main_frame->GetLastCommittedURL());
  EXPECT_EQ(iframe_url, child_frame->GetLastCommittedURL());
  EXPECT_TRUE(child_frame->GetLastCommittedOrigin().opaque());
}

}  // namespace extensions
