// Copyright (c) 2012 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 <memory>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame_messages.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/resource_dispatcher_host_delegate.h"
#include "content/public/browser/resource_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.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/public/test/navigation_handle_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/escape.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_status.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "url/gurl.h"

namespace content {

// WebContentsDelegate that fails to open a URL when there's a request that
// needs to be transferred between renderers.
class NoTransferRequestDelegate : public WebContentsDelegate {
 public:
  NoTransferRequestDelegate() {}

  bool ShouldTransferNavigation(bool is_main_frame_navigation) override {
    // Intentionally cancel the transfer.
    return false;
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(NoTransferRequestDelegate);
};

class CrossSiteTransferTest : public ContentBrowserTest {
 public:
  CrossSiteTransferTest() {}

  // ContentBrowserTest implementation:
  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    content::SetupCrossSiteRedirector(embedded_test_server());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

 protected:
  void NavigateToURLContentInitiated(Shell* window,
                                     const GURL& url,
                                     bool should_replace_current_entry,
                                     bool should_wait_for_navigation) {
    std::unique_ptr<TestNavigationManager> navigation_manager =
        should_wait_for_navigation
            ? std::unique_ptr<TestNavigationManager>(
                  new TestNavigationManager(window->web_contents(), url))
            : nullptr;
    std::string script;
    if (should_replace_current_entry)
      script = base::StringPrintf("location.replace('%s')", url.spec().c_str());
    else
      script = base::StringPrintf("location.href = '%s'", url.spec().c_str());
    bool result = ExecuteScript(window, script);
    EXPECT_TRUE(result);
    if (should_wait_for_navigation) {
      EXPECT_TRUE(navigation_manager->WaitForRequestStart());
      EXPECT_TRUE(navigation_manager->WaitForResponse());
      navigation_manager->WaitForNavigationFinished();
      EXPECT_TRUE(navigation_manager->was_successful());
    }
  }

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

// The following tests crash in the ThreadSanitizer runtime,
// http://crbug.com/356758.
#if defined(THREAD_SANITIZER)
#define MAYBE_ReplaceEntryCrossProcessThenTransfer \
    DISABLED_ReplaceEntryCrossProcessThenTransfer
#define MAYBE_ReplaceEntryCrossProcessTwice \
    DISABLED_ReplaceEntryCrossProcessTwice
#else
#define MAYBE_ReplaceEntryCrossProcessThenTransfer \
    ReplaceEntryCrossProcessThenTransfer
#define MAYBE_ReplaceEntryCrossProcessTwice ReplaceEntryCrossProcessTwice
#endif
// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that began with a cross-process navigation.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
                       MAYBE_ReplaceEntryCrossProcessThenTransfer) {
  const NavigationController& controller =
      shell()->web_contents()->GetController();

  // Navigate to a starting URL, so there is a history entry to replace.
  GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Navigate to a page on A.com with entry replacement. This navigation is
  // cross-site, so the renderer will send it to the browser via OpenURL to give
  // to a new process. It will then be transferred into yet another process due
  // to the call above.
  GURL url2 =
      embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
  NavigateToURLContentInitiated(shell(), url2, true, true);

  // There should be one history entry. url2 should have replaced url1.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());

  // Now navigate as before to a page on B.com, but normally (without
  // replacement). This will still perform a double process-swap as above, via
  // OpenURL and then transfer.
  GURL url3 =
      embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
  NavigateToURLContentInitiated(shell(), url3, false, true);

  // There should be two history entries. url2 should have replaced url1. url2
  // should not have replaced url3.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
}

// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that began with a content-initiated in-process
// navigation. This test is the same as the test above, except transfering from
// in-process.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
                       ReplaceEntryInProcessThenTransfer) {
  const NavigationController& controller =
      shell()->web_contents()->GetController();

  // Navigate to a starting URL, so there is a history entry to replace.
  GURL url = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Navigate in-process with entry replacement. It will then be transferred
  // into a new one due to the call above.
  GURL url2 = embedded_test_server()->GetURL("/site_isolation/blank.html?2");
  NavigateToURLContentInitiated(shell(), url2, true, true);

  // There should be one history entry. url2 should have replaced url1.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());

  // Now navigate as before, but without replacement.
  GURL url3 = embedded_test_server()->GetURL("/site_isolation/blank.html?3");
  NavigateToURLContentInitiated(shell(), url3, false, true);

  // There should be two history entries. url2 should have replaced url1. url2
  // should not have replaced url3.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
}

// Tests that the |should_replace_current_entry| flag persists correctly across
// request transfers that cross processes twice from renderer policy.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
                       MAYBE_ReplaceEntryCrossProcessTwice) {
  const NavigationController& controller =
      shell()->web_contents()->GetController();

  // Navigate to a starting URL, so there is a history entry to replace.
  GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Navigate to a page on A.com which redirects to B.com with entry
  // replacement. This will switch processes via OpenURL twice. First to A.com,
  // and second in response to the server redirect to B.com. The second swap is
  // also renderer-initiated via OpenURL because decidePolicyForNavigation is
  // currently applied on redirects.
  GURL::Replacements replace_host;
  GURL url2b =
      embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?2");
  GURL url2a = embedded_test_server()->GetURL(
      "A.com", "/cross-site/" + url2b.host() + url2b.PathForRequest());
  NavigateToURLContentInitiated(shell(), url2a, true, true);

  // There should be one history entry. url2b should have replaced url1.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());

  // Now repeat without replacement.
  GURL url3b =
      embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
  GURL url3a = embedded_test_server()->GetURL(
      "A.com", "/cross-site/" + url3b.host() + url3b.PathForRequest());
  NavigateToURLContentInitiated(shell(), url3a, false, true);

  // There should be two history entries. url2b should have replaced url1. url3b
  // should not have replaced url2b.
  EXPECT_TRUE(controller.GetPendingEntry() == nullptr);
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url3b, controller.GetEntryAtIndex(1)->GetURL());
}

// Tests that the request is destroyed when a cross process navigation is
// cancelled.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoLeakOnCrossSiteCancel) {
  const NavigationController& controller =
      shell()->web_contents()->GetController();

  // Navigate to a starting URL, so there is a history entry to replace.
  GURL url1 = embedded_test_server()->GetURL("/site_isolation/blank.html?1");
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NoTransferRequestDelegate no_transfer_request_delegate;
  WebContentsDelegate* old_delegate = shell()->web_contents()->GetDelegate();
  shell()->web_contents()->SetDelegate(&no_transfer_request_delegate);

  // Navigate to a page on A.com with entry replacement. This navigation is
  // cross-site, so the renderer will send it to the browser via OpenURL to give
  // to a new process. It will then be transferred into yet another process due
  // to the call above.
  GURL url2 =
      embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
  TestNavigationManager navigation_manager(shell()->web_contents(), url2);

  NavigationHandleObserver handle_observer(shell()->web_contents(), url2);
  // Don't wait for the navigation to complete, since that never happens in
  // this case.
  NavigateToURLContentInitiated(shell(), url2, false, false);

  // Make sure the request for url2 did not complete.
  EXPECT_FALSE(navigation_manager.WaitForResponse());

  // There should be one history entry, with url1.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());

  EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code());
  shell()->web_contents()->SetDelegate(old_delegate);
}

// Test that verifies that a cross-process transfer retains ability to read
// files encapsulated by HTTP POST body that is forwarded to the new renderer.
// Invalid handling of this scenario has been suspected as the cause of at least
// some of the renderer kills tracked in https://crbug.com/613260.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, PostWithFileData) {
  // Navigate to the page with form that posts via 307 redirection to
  // |redirect_target_url| (cross-site from |form_url|).  Using 307 (rather than
  // 302) redirection is important to preserve the HTTP method and POST body.
  GURL form_url(embedded_test_server()->GetURL(
      "a.com", "/form_that_posts_cross_site.html"));
  GURL redirect_target_url(embedded_test_server()->GetURL("x.com", "/echoall"));
  EXPECT_TRUE(NavigateToURL(shell(), form_url));

  // Prepare a file to upload.
  base::ThreadRestrictions::ScopedAllowIO allow_io_for_temp_dir;
  base::ScopedTempDir temp_dir;
  base::FilePath file_path;
  std::string file_content("test-file-content");
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
  ASSERT_LT(
      0, base::WriteFile(file_path, file_content.data(), file_content.size()));

  // Fill out the form to refer to the test file.
  std::unique_ptr<FileChooserDelegate> delegate(
      new FileChooserDelegate(file_path));
  shell()->web_contents()->SetDelegate(delegate.get());
  EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
                            "document.getElementById('file').click();"));
  EXPECT_TRUE(delegate->file_chosen());

  // Remember the old process id for a sanity check below.
  int old_process_id =
      shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();

  // Submit the form.
  TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(
      ExecuteScript(shell(), "document.getElementById('file-form').submit();"));
  form_post_observer.Wait();

  // Verify that we arrived at the expected, redirected location.
  EXPECT_EQ(redirect_target_url,
            shell()->web_contents()->GetLastCommittedURL());

  // Verify that the test really verifies access of a *new* renderer process.
  int new_process_id =
      shell()->web_contents()->GetMainFrame()->GetProcess()->GetID();
  ASSERT_NE(new_process_id, old_process_id);

  // MAIN VERIFICATION: Check if the new renderer process is able to read the
  // file.
  EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
      new_process_id, file_path));

  // Verify that POST body got preserved by 307 redirect.  This expectation
  // comes from: https://tools.ietf.org/html/rfc7231#section-6.4.7
  std::string actual_page_body;
  EXPECT_TRUE(ExecuteScriptAndExtractString(
      shell()->web_contents(),
      "window.domAutomationController.send("
      "document.getElementsByTagName('pre')[0].innerText);",
      &actual_page_body));
  EXPECT_THAT(actual_page_body, ::testing::HasSubstr(file_content));
  EXPECT_THAT(actual_page_body,
              ::testing::HasSubstr(file_path.BaseName().AsUTF8Unsafe()));
  EXPECT_THAT(actual_page_body,
              ::testing::HasSubstr("form-data; name=\"file\""));
}

// Test that verifies that if navigation originator doesn't have access to a
// file, then no access is granted after a cross-process transfer of POST data.
// This is a regression test for https://crbug.com/726067.
//
// This test is somewhat similar to
// http/tests/navigation/form-targets-cross-site-frame-post.html layout test
// except that it 1) tests with files, 2) simulates a malicious scenario and 3)
// verifies file access (all of these 3 things are not possible with layout
// tests).
//
// This test is very similar to CrossSiteTransferTest.PostWithFileData above,
// except that it simulates a malicious form / POST originator.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, MaliciousPostWithFileData) {
  // The initial test window is a named form target.
  GURL initial_target_url(
      embedded_test_server()->GetURL("initial-target.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), initial_target_url));
  WebContents* target_contents = shell()->web_contents();
  EXPECT_TRUE(ExecuteScript(target_contents, "window.name = 'form-target';"));

  // Create a new window containing a form targeting |target_contents|.
  GURL form_url(embedded_test_server()->GetURL(
      "main.com", "/form_that_posts_cross_site.html"));
  Shell* other_window = OpenPopup(target_contents, form_url, "form-window");
  WebContents* form_contents = other_window->web_contents();
  EXPECT_TRUE(ExecuteScript(
      form_contents,
      "document.getElementById('file-form').target = 'form-target';"));

  // Verify the current locations and process placement of |target_contents|
  // and |form_contents|.
  EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());
  EXPECT_EQ(form_url, form_contents->GetLastCommittedURL());
  EXPECT_NE(target_contents->GetMainFrame()->GetProcess()->GetID(),
            form_contents->GetMainFrame()->GetProcess()->GetID());

  // Prepare a file to upload.
  base::ThreadRestrictions::ScopedAllowIO allow_io_for_temp_dir;
  base::ScopedTempDir temp_dir;
  base::FilePath file_path;
  std::string file_content("test-file-content");
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
  ASSERT_LT(
      0, base::WriteFile(file_path, file_content.data(), file_content.size()));

  // Fill out the form to refer to the test file.
  std::unique_ptr<FileChooserDelegate> delegate(
      new FileChooserDelegate(file_path));
  form_contents->Focus();
  form_contents->SetDelegate(delegate.get());
  EXPECT_TRUE(
      ExecuteScript(form_contents, "document.getElementById('file').click();"));
  EXPECT_TRUE(delegate->file_chosen());
  ChildProcessSecurityPolicyImpl* security_policy =
      ChildProcessSecurityPolicyImpl::GetInstance();
  EXPECT_TRUE(security_policy->CanReadFile(
      form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));

  // Simulate a malicious situation, where the renderer doesn't really have
  // access to the file.
  security_policy->RevokeAllPermissionsForFile(
      form_contents->GetMainFrame()->GetProcess()->GetID(), file_path);
  EXPECT_FALSE(security_policy->CanReadFile(
      form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
  EXPECT_FALSE(security_policy->CanReadFile(
      target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));

  // Submit the form and wait until the malicious renderer gets killed.
  RenderProcessHostKillWaiter kill_waiter(
      form_contents->GetMainFrame()->GetProcess());
  EXPECT_TRUE(ExecuteScript(
      form_contents,
      "setTimeout(\n"
      "  function() { document.getElementById('file-form').submit(); },\n"
      "  0);"));
  EXPECT_EQ(bad_message::RFPH_ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());

  // The target frame should still be at the original location - the malicious
  // navigation should have been stopped.
  EXPECT_EQ(initial_target_url, target_contents->GetLastCommittedURL());

  // Both processes still shouldn't have access.
  EXPECT_FALSE(security_policy->CanReadFile(
      form_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
  EXPECT_FALSE(security_policy->CanReadFile(
      target_contents->GetMainFrame()->GetProcess()->GetID(), file_path));
}

// Regression test for https://crbug.com/538784 -- ensures that one can't
// sidestep cross-process navigation by detaching a frame mid-request.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoDeliveryToDetachedFrame) {
  GURL attacker_page = embedded_test_server()->GetURL(
      "evil.com", "/cross_site_iframe_factory.html?evil(evil)");
  EXPECT_TRUE(NavigateToURL(shell(), attacker_page));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetFrameTree()
                            ->root();

  RenderFrameHost* child_frame = root->child_at(0)->current_frame_host();

  // Attacker initiates a navigation to a cross-site document. Under --site-per-
  // process, these bytes must not be sent to the attacker process.
  GURL target_resource =
      embedded_test_server()->GetURL("a.com", "/title1.html");
  TestNavigationManager target_navigation(shell()->web_contents(),
                                          target_resource);
  EXPECT_TRUE(ExecuteScript(
      shell()->web_contents()->GetMainFrame(),
      base::StringPrintf("document.getElementById('child-0').src='%s'",
                         target_resource.spec().c_str())));

  // Wait for the navigation to start.
  EXPECT_TRUE(target_navigation.WaitForRequestStart());
  target_navigation.ResumeNavigation();

  // Inject a frame detach message. An attacker-controlled renderer could do
  // this without also cancelling the pending navigation (as blink would, if you
  // removed the iframe from the document via js).
  child_frame->OnMessageReceived(
      FrameHostMsg_Detach(child_frame->GetRoutingID()));

  // This should cancel the navigation.
  EXPECT_FALSE(target_navigation.WaitForResponse())
      << "Request should have been cancelled before reaching the renderer.";
}

// Ensure that we don't send a referrer if a site tries to trigger the forking
// heuristic, even if we would have forked anyways.
IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoReferrerOnFork) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      switches::kContentShellAlwaysFork);

  GURL start_url(embedded_test_server()->GetURL("a.com", "/fork-popup.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(2u, shell()->windows().size());
  Shell* popup = shell()->windows().back();
  EXPECT_NE(popup, shell());

  base::string16 expected_title = base::ASCIIToUTF16("Referrer = ''");
  base::string16 failed_title = base::ASCIIToUTF16(
      base::StringPrintf("Referrer = '%s'", start_url.spec().c_str()));
  TitleWatcher watcher(popup->web_contents(), expected_title);
  watcher.AlsoWaitForTitle(failed_title);
  EXPECT_EQ(expected_title, watcher.WaitAndGetTitle());
}

}  // namespace content
