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