// Copyright 2021 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/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/preloading_trigger_type.h"
#include "content/public/browser/prerender_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {

namespace {

static constexpr WebExposedIsolationLevel kNotIsolated =
    WebExposedIsolationLevel::kNotIsolated;
static constexpr WebExposedIsolationLevel kIsolatedApplication =
    WebExposedIsolationLevel::kIsolatedApplication;

const char kAppHost[] = "app.com";
const char kNonAppHost[] = "other.com";

class IsolatedWebAppContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  explicit IsolatedWebAppContentBrowserClient(
      net::EmbeddedTestServer* embedded_https_server) {
    GURL app_url = embedded_https_server->GetURL(kAppHost, "/");
    app_origin_ = url::Origin::Create(app_url);
  }

  bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
                                             const GURL& url) override {
    return url.GetHost() == kAppHost;
  }

  bool AreIsolatedWebAppsEnabled(BrowserContext*) override { return true; }

 private:
  url::Origin app_origin_;
};

}  // namespace

class HttpsBrowserTest : public ContentBrowserTest {
 public:
  HttpsBrowserTest() : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    mock_cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    ContentBrowserTest::SetUpInProcessBrowserTestFixture();
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    ContentBrowserTest::TearDownInProcessBrowserTestFixture();
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    net::test_server::RegisterDefaultHandlers(https_server());
    ASSERT_TRUE(https_server()->Start());
  }

 protected:
  net::EmbeddedTestServer* https_server() { return &https_server_; }

 private:
  net::EmbeddedTestServer https_server_;
  ContentMockCertVerifier mock_cert_verifier_;
};

class IsolatedWebAppThrottleBrowserTest : public HttpsBrowserTest {
 public:
  void SetUpOnMainThread() override {
    HttpsBrowserTest::SetUpOnMainThread();

    test_client_ =
        std::make_unique<IsolatedWebAppContentBrowserClient>(https_server());
  }

  void TearDownOnMainThread() override {
    HttpsBrowserTest::TearDownOnMainThread();
    test_client_.reset();
  }

 protected:
  GURL GetAppURL() {
    return https_server()->GetURL(
        kAppHost,
        base::StrCat({"/set-header?",
                      "Cross-Origin-Opener-Policy: same-origin&"
                      "Cross-Origin-Embedder-Policy: require-corp&"
                      "Cross-Origin-Resource-Policy: same-origin&"
                      "Permissions-Policy: cross-origin-isolated%3D(*)"}));
  }

  GURL GetNonAppURL(bool cross_origin_isolated) {
    if (cross_origin_isolated) {
      return https_server()->GetURL(
          kNonAppHost,
          base::StrCat({"/set-header?",
                        "Cross-Origin-Opener-Policy: same-origin&"
                        "Cross-Origin-Embedder-Policy: require-corp&"
                        "Cross-Origin-Resource-Policy: cross-origin"}));
    } else {
      return https_server()->GetURL(kNonAppHost, "/set-header");
    }
  }

  RenderFrameHost* CreateChildIframe(RenderFrameHost* parent_rfh,
                                     const GURL& iframe_src) {
    // For now assume this is the only child iframe.
    EXPECT_FALSE(ChildFrameAt(parent_rfh, 0));

    TestNavigationObserver navigation_observer(web_contents());
    EXPECT_TRUE(ExecJs(
        parent_rfh, JsReplace("const iframe = document.createElement('iframe');"
                              "iframe.id = 'child_iframe';"
                              "iframe.src = $1;"
                              "document.body.appendChild(iframe);",
                              iframe_src)));
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
    EXPECT_EQ(net::OK, navigation_observer.last_net_error_code());
    EXPECT_EQ(iframe_src, navigation_observer.last_navigation_url());

    RenderFrameHost* iframe = ChildFrameAt(parent_rfh, 0);
    EXPECT_TRUE(iframe);
    return iframe;
  }

  // Perform a renderer-initiated navigation in |iframe| to |url| whose
  // initiator is the iframe itself.
  std::unique_ptr<TestNavigationObserver> SelfNavigateIframeToURL(
      RenderFrameHost* iframe,
      const GURL& url) {
    auto navigation_observer =
        std::make_unique<TestNavigationObserver>(web_contents());
    EXPECT_TRUE(ExecJs(iframe, JsReplace("location.href = $1", url)));
    navigation_observer->Wait();
    return navigation_observer;
  }

  // Perform a renderer-initiated navigation of |iframe| to |url|, whose
  // initiator is |iframe|'s parent frame.
  std::unique_ptr<TestNavigationObserver> NavigateIframeToUrlFromParent(
      RenderFrameHost* iframe,
      const GURL& url) {
    auto navigation_observer =
        std::make_unique<TestNavigationObserver>(web_contents());
    EXPECT_TRUE(
        ExecJs(iframe->GetParent(), JsReplace("child_iframe.src = $1", url)));
    navigation_observer->Wait();
    return navigation_observer;
  }

  RenderFrameHost* main_rfh() { return web_contents()->GetPrimaryMainFrame(); }

  WebContents* web_contents() { return shell()->web_contents(); }

 private:
  std::unique_ptr<IsolatedWebAppContentBrowserClient> test_client_;
};

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       BlockMainFrameNavigationIntoApp) {
  EXPECT_TRUE(NavigateToURL(web_contents(),
                            GetNonAppURL(/*cross_origin_isolated=*/false)));
  EXPECT_EQ(kNotIsolated, main_rfh()->GetWebExposedIsolationLevel());

  TestNavigationObserver navigation_observer(web_contents());
  shell()->LoadURL(GetAppURL());
  navigation_observer.Wait();
  EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT,
            navigation_observer.last_net_error_code());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       CancelCrossOriginNavigationInApp) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  EXPECT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  TestNavigationObserver navigation_observer(web_contents());
  shell()->LoadURL(GetNonAppURL(/*cross_origin_isolated=*/false));
  navigation_observer.Wait();
  EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
  EXPECT_EQ(app_url, main_rfh()->GetLastCommittedURL());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       IframeInitiatedIframeNavigationIntoAppBlocked) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  EXPECT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  RenderFrameHost* iframe = CreateChildIframe(
      main_rfh(), GetNonAppURL(/*cross_origin_isolated=*/true));
  const blink::LocalFrameToken iframe_token = iframe->GetFrameToken();

  std::unique_ptr<TestNavigationObserver> navigation_observer =
      SelfNavigateIframeToURL(iframe, app_url);
  EXPECT_EQ(iframe_token,
            navigation_observer->last_initiator_frame_token().value());
  EXPECT_FALSE(navigation_observer->last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT,
            navigation_observer->last_net_error_code());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       DataIframeInitiatedNavigationIntoAppBlocked) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  EXPECT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  // A data: URL iframe created by the app commits with an opaque origin whose
  // precursor is the app origin.
  RenderFrameHost* iframe =
      CreateChildIframe(main_rfh(), GURL("data:text/html,body"));
  EXPECT_TRUE(iframe->GetLastCommittedOrigin().opaque());

  // The data: iframe must not be allowed to navigate itself back into the app.
  std::unique_ptr<TestNavigationObserver> navigation_observer =
      SelfNavigateIframeToURL(iframe, app_url);
  EXPECT_FALSE(navigation_observer->last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT,
            navigation_observer->last_net_error_code());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       AppInitiatedDataIframeNavigationIntoAppAllowed) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  EXPECT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  RenderFrameHost* iframe =
      CreateChildIframe(main_rfh(), GURL("data:text/html,body"));
  EXPECT_TRUE(iframe->GetLastCommittedOrigin().opaque());

  // The app's main frame is allowed to navigate the data: iframe into the app.
  std::unique_ptr<TestNavigationObserver> navigation_observer =
      NavigateIframeToUrlFromParent(iframe, app_url);
  EXPECT_EQ(main_rfh()->GetFrameToken(),
            navigation_observer->last_initiator_frame_token().value());
  EXPECT_TRUE(navigation_observer->last_navigation_succeeded());
  EXPECT_EQ(net::OK, navigation_observer->last_net_error_code());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       AppInitiatedIframeNavigationIntoAppAllowed) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  EXPECT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  RenderFrameHost* iframe = CreateChildIframe(
      main_rfh(), GetNonAppURL(/*cross_origin_isolated=*/true));

  std::unique_ptr<TestNavigationObserver> navigation_observer =
      NavigateIframeToUrlFromParent(iframe, app_url);
  EXPECT_EQ(main_rfh()->GetFrameToken(),
            navigation_observer->last_initiator_frame_token().value());
  EXPECT_TRUE(navigation_observer->last_navigation_succeeded());
  EXPECT_EQ(net::OK, navigation_observer->last_net_error_code());
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleBrowserTest,
                       ExternalLinkClickOpensInNewTab) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  ASSERT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  GURL external_url("https://www.example.com/");
  EXPECT_TRUE(ExecJs(main_rfh(), JsReplace(R"(
        const link = document.createElement('a');
        link.href = $1;
        link.id = 'external_link';
        link.target = '_self';
        link.rel = 'noopener';
        link.textContent = 'External Link';
        document.body.appendChild(link);
      )",
                                           external_url)));

  content::WebContentsAddedObserver web_contents_added_observer;

  // Simulate clicking the link, which should create a new tab.
  EXPECT_TRUE(
      ExecJs(main_rfh(), "document.getElementById('external_link').click();"));

  // Despite target="_self", navigations from an IWA to a different origin
  // should always open in a new tab in the main browser.
  content::WebContents* new_web_contents =
      web_contents_added_observer.GetWebContents();
  ASSERT_NE(nullptr, new_web_contents)
      << "New tab was not opened for external link.";

  content::TestNavigationObserver nav_observer(new_web_contents);
  nav_observer.Wait();

  EXPECT_EQ(external_url, nav_observer.last_navigation_url());
  EXPECT_EQ(app_url, web_contents()->GetLastCommittedURL());
}

class IsolatedWebAppThrottleWithPrerenderBrowserTest
    : public IsolatedWebAppThrottleBrowserTest {
 public:
  IsolatedWebAppThrottleWithPrerenderBrowserTest() {
    prerender_helper_ = std::make_unique<test::PrerenderTestHelper>(
        base::BindRepeating(
            &IsolatedWebAppThrottleWithPrerenderBrowserTest::web_contents,
            base::Unretained(this)));
  }

 protected:
  test::PrerenderTestHelper& prerender_helper() { return *prerender_helper_; }

  int GetPageCount() {
    int count = 0;
    web_contents()->ForEachRenderFrameHost([&](RenderFrameHost* rfh) {
      if (!rfh->GetParent()) {
        count++;
      }
    });
    return count;
  }

 private:
  std::unique_ptr<test::PrerenderTestHelper> prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(IsolatedWebAppThrottleWithPrerenderBrowserTest,
                       CrossOriginPrerenderInAppIsCancelled) {
  GURL app_url = GetAppURL();
  EXPECT_TRUE(NavigateToURL(web_contents(), app_url));
  ASSERT_EQ(kIsolatedApplication, main_rfh()->GetWebExposedIsolationLevel());

  EXPECT_EQ(1, GetPageCount());

  // A cross-origin prerender trigger via AddEmbedderTriggeredPrerenderAsync
  // will be succeeded, but its navigation will be blocked. So, the page count
  // should still be 1.
  GURL prerender_url = GetNonAppURL(/*cross_origin_isolated=*/false);
  std::unique_ptr<content::PrerenderHandle> handle =
      prerender_helper().AddEmbedderTriggeredPrerenderAsync(
          prerender_url, content::PreloadingTriggerType::kEmbedder,
          "PrewarmDefaultSearchEngine",
          ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                    ui::PAGE_TRANSITION_FROM_ADDRESS_BAR));
  prerender_helper().WaitForPrerenderLoadCompletion(prerender_url);

  EXPECT_EQ(1, GetPageCount());
}

}  // namespace content
