Home | History | Annotate | Download | only in browser
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "base/command_line.h"
      6 #include "base/strings/stringprintf.h"
      7 #include "content/browser/loader/resource_dispatcher_host_impl.h"
      8 #include "content/public/browser/navigation_entry.h"
      9 #include "content/public/browser/resource_dispatcher_host_delegate.h"
     10 #include "content/public/browser/resource_throttle.h"
     11 #include "content/public/browser/web_contents.h"
     12 #include "content/public/common/content_switches.h"
     13 #include "content/public/test/browser_test_utils.h"
     14 #include "content/public/test/content_browser_test.h"
     15 #include "content/public/test/content_browser_test_utils.h"
     16 #include "content/public/test/test_navigation_observer.h"
     17 #include "content/shell/browser/shell.h"
     18 #include "content/shell/browser/shell_content_browser_client.h"
     19 #include "content/shell/browser/shell_resource_dispatcher_host_delegate.h"
     20 #include "net/base/escape.h"
     21 #include "net/dns/mock_host_resolver.h"
     22 #include "net/url_request/url_request.h"
     23 #include "net/url_request/url_request_status.h"
     24 #include "url/gurl.h"
     25 
     26 namespace content {
     27 
     28 // Tracks a single request for a specified URL, and allows waiting until the
     29 // request is destroyed, and then inspecting whether it completed successfully.
     30 class TrackingResourceDispatcherHostDelegate
     31     : public ShellResourceDispatcherHostDelegate {
     32  public:
     33   TrackingResourceDispatcherHostDelegate() : throttle_created_(false) {
     34   }
     35 
     36   virtual void RequestBeginning(
     37       net::URLRequest* request,
     38       ResourceContext* resource_context,
     39       AppCacheService* appcache_service,
     40       ResourceType resource_type,
     41       ScopedVector<ResourceThrottle>* throttles) OVERRIDE {
     42     CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
     43     ShellResourceDispatcherHostDelegate::RequestBeginning(
     44         request, resource_context, appcache_service, resource_type, throttles);
     45     // Expect only a single request for the tracked url.
     46     ASSERT_FALSE(throttle_created_);
     47     // If this is a request for the tracked URL, add a throttle to track it.
     48     if (request->url() == tracked_url_)
     49       throttles->push_back(new TrackingThrottle(request, this));
     50   }
     51 
     52   // Starts tracking a URL.  The request for previously tracked URL, if any,
     53   // must have been made and deleted before calling this function.
     54   void SetTrackedURL(const GURL& tracked_url) {
     55     CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     56     // Should not currently be tracking any URL.
     57     ASSERT_FALSE(run_loop_);
     58 
     59     // Create a RunLoop that will be stopped once the request for the tracked
     60     // URL has been destroyed, to allow tracking the URL while also waiting for
     61     // other events.
     62     run_loop_.reset(new base::RunLoop());
     63 
     64     BrowserThread::PostTask(
     65         BrowserThread::IO, FROM_HERE,
     66         base::Bind(
     67             &TrackingResourceDispatcherHostDelegate::SetTrackedURLOnIOThread,
     68             base::Unretained(this),
     69             tracked_url));
     70   }
     71 
     72   // Waits until the tracked URL has been requests, and the request for it has
     73   // been destroyed.
     74   bool WaitForTrackedURLAndGetCompleted() {
     75     CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     76     run_loop_->Run();
     77     run_loop_.reset();
     78     return tracked_request_completed_;
     79   }
     80 
     81  private:
     82   // ResourceThrottle attached to request for the tracked URL.  On destruction,
     83   // passes the final URLRequestStatus back to the delegate.
     84   class TrackingThrottle : public ResourceThrottle {
     85    public:
     86     TrackingThrottle(net::URLRequest* request,
     87                      TrackingResourceDispatcherHostDelegate* tracker)
     88         : request_(request), tracker_(tracker) {
     89     }
     90 
     91     virtual ~TrackingThrottle() {
     92       // If the request is deleted without being cancelled, its status will
     93       // indicate it succeeded, so have to check if the request is still pending
     94       // as well.
     95       tracker_->OnTrackedRequestDestroyed(
     96           !request_->is_pending() && request_->status().is_success());
     97     }
     98 
     99     // ResourceThrottle implementation:
    100     virtual const char* GetNameForLogging() const OVERRIDE {
    101       return "TrackingThrottle";
    102     }
    103 
    104    private:
    105     net::URLRequest* request_;
    106     TrackingResourceDispatcherHostDelegate* tracker_;
    107 
    108     DISALLOW_COPY_AND_ASSIGN(TrackingThrottle);
    109   };
    110 
    111   void SetTrackedURLOnIOThread(const GURL& tracked_url) {
    112     CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    113     throttle_created_ = false;
    114     tracked_url_ = tracked_url;
    115   }
    116 
    117   void OnTrackedRequestDestroyed(bool completed) {
    118     CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    119     tracked_request_completed_ = completed;
    120     tracked_url_ = GURL();
    121 
    122     BrowserThread::PostTask(
    123         BrowserThread::UI, FROM_HERE, run_loop_->QuitClosure());
    124   }
    125 
    126   // These live on the IO thread.
    127   GURL tracked_url_;
    128   bool throttle_created_;
    129 
    130   // This is created and destroyed on the UI thread, but stopped on the IO
    131   // thread.
    132   scoped_ptr<base::RunLoop> run_loop_;
    133 
    134   // Set on the IO thread while |run_loop_| is non-NULL, read on the UI thread
    135   // after deleting run_loop_.
    136   bool tracked_request_completed_;
    137 
    138   DISALLOW_COPY_AND_ASSIGN(TrackingResourceDispatcherHostDelegate);
    139 };
    140 
    141 // WebContentsDelegate that fails to open a URL when there's a request that
    142 // needs to be transferred between renderers.
    143 class NoTransferRequestDelegate : public WebContentsDelegate {
    144  public:
    145   NoTransferRequestDelegate() {}
    146 
    147   virtual WebContents* OpenURLFromTab(WebContents* source,
    148                                       const OpenURLParams& params) OVERRIDE {
    149     bool is_transfer =
    150         (params.transferred_global_request_id != GlobalRequestID());
    151     if (is_transfer)
    152       return NULL;
    153     NavigationController::LoadURLParams load_url_params(params.url);
    154     load_url_params.referrer = params.referrer;
    155     load_url_params.frame_tree_node_id = params.frame_tree_node_id;
    156     load_url_params.transition_type = params.transition;
    157     load_url_params.extra_headers = params.extra_headers;
    158     load_url_params.should_replace_current_entry =
    159         params.should_replace_current_entry;
    160     load_url_params.is_renderer_initiated = true;
    161     source->GetController().LoadURLWithParams(load_url_params);
    162     return source;
    163   }
    164 
    165  private:
    166   DISALLOW_COPY_AND_ASSIGN(NoTransferRequestDelegate);
    167 };
    168 
    169 class CrossSiteTransferTest : public ContentBrowserTest {
    170  public:
    171   CrossSiteTransferTest() : old_delegate_(NULL) {
    172   }
    173 
    174   // ContentBrowserTest implementation:
    175   virtual void SetUpOnMainThread() OVERRIDE {
    176     BrowserThread::PostTask(
    177         BrowserThread::IO, FROM_HERE,
    178         base::Bind(
    179             &CrossSiteTransferTest::InjectResourceDisptcherHostDelegate,
    180             base::Unretained(this)));
    181   }
    182 
    183   virtual void TearDownOnMainThread() OVERRIDE {
    184     BrowserThread::PostTask(
    185         BrowserThread::IO, FROM_HERE,
    186         base::Bind(
    187             &CrossSiteTransferTest::RestoreResourceDisptcherHostDelegate,
    188             base::Unretained(this)));
    189   }
    190 
    191  protected:
    192   void NavigateToURLContentInitiated(Shell* window,
    193                                      const GURL& url,
    194                                      bool should_replace_current_entry,
    195                                      bool should_wait_for_navigation) {
    196     std::string script;
    197     if (should_replace_current_entry)
    198       script = base::StringPrintf("location.replace('%s')", url.spec().c_str());
    199     else
    200       script = base::StringPrintf("location.href = '%s'", url.spec().c_str());
    201     TestNavigationObserver load_observer(shell()->web_contents(), 1);
    202     bool result = ExecuteScript(window->web_contents(), script);
    203     EXPECT_TRUE(result);
    204     if (should_wait_for_navigation)
    205       load_observer.Wait();
    206   }
    207 
    208   virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
    209     // Use --site-per-process to force process swaps for cross-site transfers.
    210     command_line->AppendSwitch(switches::kSitePerProcess);
    211   }
    212 
    213   void InjectResourceDisptcherHostDelegate() {
    214     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    215     old_delegate_ = ResourceDispatcherHostImpl::Get()->delegate();
    216     ResourceDispatcherHostImpl::Get()->SetDelegate(&tracking_delegate_);
    217   }
    218 
    219   void RestoreResourceDisptcherHostDelegate() {
    220     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    221     ResourceDispatcherHostImpl::Get()->SetDelegate(old_delegate_);
    222     old_delegate_ = NULL;
    223   }
    224 
    225   TrackingResourceDispatcherHostDelegate& tracking_delegate() {
    226     return tracking_delegate_;
    227   }
    228 
    229  private:
    230   TrackingResourceDispatcherHostDelegate tracking_delegate_;
    231   ResourceDispatcherHostDelegate* old_delegate_;
    232 };
    233 
    234 // The following tests crash in the ThreadSanitizer runtime,
    235 // http://crbug.com/356758.
    236 #if defined(THREAD_SANITIZER)
    237 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
    238     DISABLED_ReplaceEntryCrossProcessThenTransfer
    239 #define MAYBE_ReplaceEntryCrossProcessTwice \
    240     DISABLED_ReplaceEntryCrossProcessTwice
    241 #else
    242 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
    243     ReplaceEntryCrossProcessThenTransfer
    244 #define MAYBE_ReplaceEntryCrossProcessTwice ReplaceEntryCrossProcessTwice
    245 #endif
    246 // Tests that the |should_replace_current_entry| flag persists correctly across
    247 // request transfers that began with a cross-process navigation.
    248 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
    249                        MAYBE_ReplaceEntryCrossProcessThenTransfer) {
    250   const NavigationController& controller =
    251       shell()->web_contents()->GetController();
    252   host_resolver()->AddRule("*", "127.0.0.1");
    253   ASSERT_TRUE(test_server()->Start());
    254 
    255   // These must all stay in scope with replace_host.
    256   GURL::Replacements replace_host;
    257   std::string a_com("A.com");
    258   std::string b_com("B.com");
    259 
    260   // Navigate to a starting URL, so there is a history entry to replace.
    261   GURL url1 = test_server()->GetURL("files/site_isolation/blank.html?1");
    262   NavigateToURL(shell(), url1);
    263 
    264   // Force all future navigations to transfer. Note that this includes same-site
    265   // navigiations which may cause double process swaps (via OpenURL and then via
    266   // transfer). This test intentionally exercises that case.
    267   ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
    268 
    269   // Navigate to a page on A.com with entry replacement. This navigation is
    270   // cross-site, so the renderer will send it to the browser via OpenURL to give
    271   // to a new process. It will then be transferred into yet another process due
    272   // to the call above.
    273   GURL url2 = test_server()->GetURL("files/site_isolation/blank.html?2");
    274   replace_host.SetHostStr(a_com);
    275   url2 = url2.ReplaceComponents(replace_host);
    276   // Used to make sure the request for url2 succeeds, and there was only one of
    277   // them.
    278   tracking_delegate().SetTrackedURL(url2);
    279   NavigateToURLContentInitiated(shell(), url2, true, true);
    280 
    281   // There should be one history entry. url2 should have replaced url1.
    282   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    283   EXPECT_EQ(1, controller.GetEntryCount());
    284   EXPECT_EQ(0, controller.GetCurrentEntryIndex());
    285   EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
    286   // Make sure the request succeeded.
    287   EXPECT_TRUE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
    288 
    289   // Now navigate as before to a page on B.com, but normally (without
    290   // replacement). This will still perform a double process-swap as above, via
    291   // OpenURL and then transfer.
    292   GURL url3 = test_server()->GetURL("files/site_isolation/blank.html?3");
    293   replace_host.SetHostStr(b_com);
    294   url3 = url3.ReplaceComponents(replace_host);
    295   // Used to make sure the request for url3 succeeds, and there was only one of
    296   // them.
    297   tracking_delegate().SetTrackedURL(url3);
    298   NavigateToURLContentInitiated(shell(), url3, false, true);
    299 
    300   // There should be two history entries. url2 should have replaced url1. url2
    301   // should not have replaced url3.
    302   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    303   EXPECT_EQ(2, controller.GetEntryCount());
    304   EXPECT_EQ(1, controller.GetCurrentEntryIndex());
    305   EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
    306   EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
    307 
    308   // Make sure the request succeeded.
    309   EXPECT_TRUE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
    310 }
    311 
    312 // Tests that the |should_replace_current_entry| flag persists correctly across
    313 // request transfers that began with a content-initiated in-process
    314 // navigation. This test is the same as the test above, except transfering from
    315 // in-process.
    316 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
    317                        ReplaceEntryInProcessThenTranfers) {
    318   const NavigationController& controller =
    319       shell()->web_contents()->GetController();
    320   ASSERT_TRUE(test_server()->Start());
    321 
    322   // Navigate to a starting URL, so there is a history entry to replace.
    323   GURL url = test_server()->GetURL("files/site_isolation/blank.html?1");
    324   NavigateToURL(shell(), url);
    325 
    326   // Force all future navigations to transfer. Note that this includes same-site
    327   // navigiations which may cause double process swaps (via OpenURL and then via
    328   // transfer). All navigations in this test are same-site, so it only swaps
    329   // processes via request transfer.
    330   ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
    331 
    332   // Navigate in-process with entry replacement. It will then be transferred
    333   // into a new one due to the call above.
    334   GURL url2 = test_server()->GetURL("files/site_isolation/blank.html?2");
    335   NavigateToURLContentInitiated(shell(), url2, true, true);
    336 
    337   // There should be one history entry. url2 should have replaced url1.
    338   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    339   EXPECT_EQ(1, controller.GetEntryCount());
    340   EXPECT_EQ(0, controller.GetCurrentEntryIndex());
    341   EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
    342 
    343   // Now navigate as before, but without replacement.
    344   GURL url3 = test_server()->GetURL("files/site_isolation/blank.html?3");
    345   NavigateToURLContentInitiated(shell(), url3, false, true);
    346 
    347   // There should be two history entries. url2 should have replaced url1. url2
    348   // should not have replaced url3.
    349   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    350   EXPECT_EQ(2, controller.GetEntryCount());
    351   EXPECT_EQ(1, controller.GetCurrentEntryIndex());
    352   EXPECT_EQ(url2, controller.GetEntryAtIndex(0)->GetURL());
    353   EXPECT_EQ(url3, controller.GetEntryAtIndex(1)->GetURL());
    354 }
    355 
    356 // Tests that the |should_replace_current_entry| flag persists correctly across
    357 // request transfers that cross processes twice from renderer policy.
    358 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest,
    359                        MAYBE_ReplaceEntryCrossProcessTwice) {
    360   const NavigationController& controller =
    361       shell()->web_contents()->GetController();
    362   host_resolver()->AddRule("*", "127.0.0.1");
    363   ASSERT_TRUE(test_server()->Start());
    364 
    365   // These must all stay in scope with replace_host.
    366   GURL::Replacements replace_host;
    367   std::string a_com("A.com");
    368   std::string b_com("B.com");
    369 
    370   // Navigate to a starting URL, so there is a history entry to replace.
    371   GURL url1 = test_server()->GetURL("files/site_isolation/blank.html?1");
    372   NavigateToURL(shell(), url1);
    373 
    374   // Navigate to a page on A.com which redirects to B.com with entry
    375   // replacement. This will switch processes via OpenURL twice. First to A.com,
    376   // and second in response to the server redirect to B.com. The second swap is
    377   // also renderer-initiated via OpenURL because decidePolicyForNavigation is
    378   // currently applied on redirects.
    379   GURL url2b = test_server()->GetURL("files/site_isolation/blank.html?2");
    380   replace_host.SetHostStr(b_com);
    381   url2b = url2b.ReplaceComponents(replace_host);
    382   GURL url2a = test_server()->GetURL(
    383       "server-redirect?" + net::EscapeQueryParamValue(url2b.spec(), false));
    384   replace_host.SetHostStr(a_com);
    385   url2a = url2a.ReplaceComponents(replace_host);
    386   NavigateToURLContentInitiated(shell(), url2a, true, true);
    387 
    388   // There should be one history entry. url2b should have replaced url1.
    389   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    390   EXPECT_EQ(1, controller.GetEntryCount());
    391   EXPECT_EQ(0, controller.GetCurrentEntryIndex());
    392   EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
    393 
    394   // Now repeat without replacement.
    395   GURL url3b = test_server()->GetURL("files/site_isolation/blank.html?3");
    396   replace_host.SetHostStr(b_com);
    397   url3b = url3b.ReplaceComponents(replace_host);
    398   GURL url3a = test_server()->GetURL(
    399       "server-redirect?" + net::EscapeQueryParamValue(url3b.spec(), false));
    400   replace_host.SetHostStr(a_com);
    401   url3a = url3a.ReplaceComponents(replace_host);
    402   NavigateToURLContentInitiated(shell(), url3a, false, true);
    403 
    404   // There should be two history entries. url2b should have replaced url1. url2b
    405   // should not have replaced url3b.
    406   EXPECT_TRUE(controller.GetPendingEntry() == NULL);
    407   EXPECT_EQ(2, controller.GetEntryCount());
    408   EXPECT_EQ(1, controller.GetCurrentEntryIndex());
    409   EXPECT_EQ(url2b, controller.GetEntryAtIndex(0)->GetURL());
    410   EXPECT_EQ(url3b, controller.GetEntryAtIndex(1)->GetURL());
    411 }
    412 
    413 // Tests that the request is destroyed when a cross process navigation is
    414 // cancelled.
    415 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest, NoLeakOnCrossSiteCancel) {
    416   const NavigationController& controller =
    417       shell()->web_contents()->GetController();
    418   host_resolver()->AddRule("*", "127.0.0.1");
    419   ASSERT_TRUE(test_server()->Start());
    420 
    421   // These must all stay in scope with replace_host.
    422   GURL::Replacements replace_host;
    423   std::string a_com("A.com");
    424   std::string b_com("B.com");
    425 
    426   // Navigate to a starting URL, so there is a history entry to replace.
    427   GURL url1 = test_server()->GetURL("files/site_isolation/blank.html?1");
    428   NavigateToURL(shell(), url1);
    429 
    430   // Force all future navigations to transfer.
    431   ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
    432 
    433   NoTransferRequestDelegate no_transfer_request_delegate;
    434   WebContentsDelegate* old_delegate = shell()->web_contents()->GetDelegate();
    435   shell()->web_contents()->SetDelegate(&no_transfer_request_delegate);
    436 
    437   // Navigate to a page on A.com with entry replacement. This navigation is
    438   // cross-site, so the renderer will send it to the browser via OpenURL to give
    439   // to a new process. It will then be transferred into yet another process due
    440   // to the call above.
    441   GURL url2 = test_server()->GetURL("files/site_isolation/blank.html?2");
    442   replace_host.SetHostStr(a_com);
    443   url2 = url2.ReplaceComponents(replace_host);
    444   // Used to make sure the second request is cancelled, and there is only one
    445   // request for url2.
    446   tracking_delegate().SetTrackedURL(url2);
    447 
    448   // Don't wait for the navigation to complete, since that never happens in
    449   // this case.
    450   NavigateToURLContentInitiated(shell(), url2, false, false);
    451 
    452   // There should be one history entry, with url1.
    453   EXPECT_EQ(1, controller.GetEntryCount());
    454   EXPECT_EQ(0, controller.GetCurrentEntryIndex());
    455   EXPECT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());
    456 
    457   // Make sure the request for url2 did not complete.
    458   EXPECT_FALSE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
    459 
    460   shell()->web_contents()->SetDelegate(old_delegate);
    461 }
    462 
    463 }  // namespace content
    464