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