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