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.
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/test/browser_test_utils.h"
13 #include "content/public/test/content_browser_test.h"
14 #include "content/public/test/content_browser_test_utils.h"
15 #include "content/public/test/test_navigation_observer.h"
16 #include "content/shell/browser/shell.h"
17 #include "content/shell/browser/shell_content_browser_client.h"
18 #include "content/shell/browser/shell_resource_dispatcher_host_delegate.h"
19 #include "net/base/escape.h"
20 #include "net/dns/mock_host_resolver.h"
21 #include "net/test/embedded_test_server/embedded_test_server.h"
22 #include "net/url_request/url_request.h"
23 #include "net/url_request/url_request_status.h"
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
{
33 TrackingResourceDispatcherHostDelegate() : throttle_created_(false) {
36 void RequestBeginning(net::URLRequest
* request
,
37 ResourceContext
* resource_context
,
38 AppCacheService
* appcache_service
,
39 ResourceType resource_type
,
40 ScopedVector
<ResourceThrottle
>* throttles
) override
{
41 CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO
));
42 ShellResourceDispatcherHostDelegate::RequestBeginning(
43 request
, resource_context
, appcache_service
, resource_type
, throttles
);
44 // Expect only a single request for the tracked url.
45 ASSERT_FALSE(throttle_created_
);
46 // If this is a request for the tracked URL, add a throttle to track it.
47 if (request
->url() == tracked_url_
)
48 throttles
->push_back(new TrackingThrottle(request
, this));
51 // Starts tracking a URL. The request for previously tracked URL, if any,
52 // must have been made and deleted before calling this function.
53 void SetTrackedURL(const GURL
& tracked_url
) {
54 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
55 // Should not currently be tracking any URL.
56 ASSERT_FALSE(run_loop_
);
58 // Create a RunLoop that will be stopped once the request for the tracked
59 // URL has been destroyed, to allow tracking the URL while also waiting for
61 run_loop_
.reset(new base::RunLoop());
63 BrowserThread::PostTask(
64 BrowserThread::IO
, FROM_HERE
,
66 &TrackingResourceDispatcherHostDelegate::SetTrackedURLOnIOThread
,
67 base::Unretained(this),
71 // Waits until the tracked URL has been requests, and the request for it has
73 bool WaitForTrackedURLAndGetCompleted() {
74 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
77 return tracked_request_completed_
;
81 // ResourceThrottle attached to request for the tracked URL. On destruction,
82 // passes the final URLRequestStatus back to the delegate.
83 class TrackingThrottle
: public ResourceThrottle
{
85 TrackingThrottle(net::URLRequest
* request
,
86 TrackingResourceDispatcherHostDelegate
* tracker
)
87 : request_(request
), tracker_(tracker
) {
90 ~TrackingThrottle() override
{
91 // If the request is deleted without being cancelled, its status will
92 // indicate it succeeded, so have to check if the request is still pending
94 tracker_
->OnTrackedRequestDestroyed(
95 !request_
->is_pending() && request_
->status().is_success());
98 // ResourceThrottle implementation:
99 const char* GetNameForLogging() const override
{
100 return "TrackingThrottle";
104 net::URLRequest
* request_
;
105 TrackingResourceDispatcherHostDelegate
* tracker_
;
107 DISALLOW_COPY_AND_ASSIGN(TrackingThrottle
);
110 void SetTrackedURLOnIOThread(const GURL
& tracked_url
) {
111 CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO
));
112 throttle_created_
= false;
113 tracked_url_
= tracked_url
;
116 void OnTrackedRequestDestroyed(bool completed
) {
117 CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO
));
118 tracked_request_completed_
= completed
;
119 tracked_url_
= GURL();
121 BrowserThread::PostTask(
122 BrowserThread::UI
, FROM_HERE
, run_loop_
->QuitClosure());
125 // These live on the IO thread.
127 bool throttle_created_
;
129 // This is created and destroyed on the UI thread, but stopped on the IO
131 scoped_ptr
<base::RunLoop
> run_loop_
;
133 // Set on the IO thread while |run_loop_| is non-NULL, read on the UI thread
134 // after deleting run_loop_.
135 bool tracked_request_completed_
;
137 DISALLOW_COPY_AND_ASSIGN(TrackingResourceDispatcherHostDelegate
);
140 // WebContentsDelegate that fails to open a URL when there's a request that
141 // needs to be transferred between renderers.
142 class NoTransferRequestDelegate
: public WebContentsDelegate
{
144 NoTransferRequestDelegate() {}
146 WebContents
* OpenURLFromTab(WebContents
* source
,
147 const OpenURLParams
& params
) override
{
149 (params
.transferred_global_request_id
!= GlobalRequestID());
152 NavigationController::LoadURLParams
load_url_params(params
.url
);
153 load_url_params
.referrer
= params
.referrer
;
154 load_url_params
.frame_tree_node_id
= params
.frame_tree_node_id
;
155 load_url_params
.transition_type
= params
.transition
;
156 load_url_params
.extra_headers
= params
.extra_headers
;
157 load_url_params
.should_replace_current_entry
=
158 params
.should_replace_current_entry
;
159 load_url_params
.is_renderer_initiated
= true;
160 source
->GetController().LoadURLWithParams(load_url_params
);
165 DISALLOW_COPY_AND_ASSIGN(NoTransferRequestDelegate
);
168 class CrossSiteTransferTest
: public ContentBrowserTest
{
170 CrossSiteTransferTest() : old_delegate_(NULL
) {
173 // ContentBrowserTest implementation:
174 void SetUpOnMainThread() override
{
175 BrowserThread::PostTask(
176 BrowserThread::IO
, FROM_HERE
,
178 &CrossSiteTransferTest::InjectResourceDisptcherHostDelegate
,
179 base::Unretained(this)));
180 host_resolver()->AddRule("*", "127.0.0.1");
181 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
182 content::SetupCrossSiteRedirector(embedded_test_server());
185 void TearDownOnMainThread() override
{
186 BrowserThread::PostTask(
187 BrowserThread::IO
, FROM_HERE
,
189 &CrossSiteTransferTest::RestoreResourceDisptcherHostDelegate
,
190 base::Unretained(this)));
194 void NavigateToURLContentInitiated(Shell
* window
,
196 bool should_replace_current_entry
,
197 bool should_wait_for_navigation
) {
199 if (should_replace_current_entry
)
200 script
= base::StringPrintf("location.replace('%s')", url
.spec().c_str());
202 script
= base::StringPrintf("location.href = '%s'", url
.spec().c_str());
203 TestNavigationObserver
load_observer(shell()->web_contents(), 1);
204 bool result
= ExecuteScript(window
->web_contents(), script
);
206 if (should_wait_for_navigation
)
207 load_observer
.Wait();
210 void SetUpCommandLine(base::CommandLine
* command_line
) override
{
211 IsolateAllSitesForTesting(command_line
);
214 void InjectResourceDisptcherHostDelegate() {
215 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
216 old_delegate_
= ResourceDispatcherHostImpl::Get()->delegate();
217 ResourceDispatcherHostImpl::Get()->SetDelegate(&tracking_delegate_
);
220 void RestoreResourceDisptcherHostDelegate() {
221 DCHECK_CURRENTLY_ON(BrowserThread::IO
);
222 ResourceDispatcherHostImpl::Get()->SetDelegate(old_delegate_
);
223 old_delegate_
= NULL
;
226 TrackingResourceDispatcherHostDelegate
& tracking_delegate() {
227 return tracking_delegate_
;
231 TrackingResourceDispatcherHostDelegate tracking_delegate_
;
232 ResourceDispatcherHostDelegate
* old_delegate_
;
235 // The following tests crash in the ThreadSanitizer runtime,
236 // http://crbug.com/356758.
237 #if defined(THREAD_SANITIZER)
238 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
239 DISABLED_ReplaceEntryCrossProcessThenTransfer
240 #define MAYBE_ReplaceEntryCrossProcessTwice \
241 DISABLED_ReplaceEntryCrossProcessTwice
243 #define MAYBE_ReplaceEntryCrossProcessThenTransfer \
244 ReplaceEntryCrossProcessThenTransfer
245 #define MAYBE_ReplaceEntryCrossProcessTwice ReplaceEntryCrossProcessTwice
247 // Tests that the |should_replace_current_entry| flag persists correctly across
248 // request transfers that began with a cross-process navigation.
249 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest
,
250 MAYBE_ReplaceEntryCrossProcessThenTransfer
) {
251 const NavigationController
& controller
=
252 shell()->web_contents()->GetController();
254 // Navigate to a starting URL, so there is a history entry to replace.
255 GURL url1
= embedded_test_server()->GetURL("/site_isolation/blank.html?1");
256 NavigateToURL(shell(), url1
);
258 // Force all future navigations to transfer. Note that this includes same-site
259 // navigiations which may cause double process swaps (via OpenURL and then via
260 // transfer). This test intentionally exercises that case.
261 ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
263 // Navigate to a page on A.com with entry replacement. This navigation is
264 // cross-site, so the renderer will send it to the browser via OpenURL to give
265 // to a new process. It will then be transferred into yet another process due
266 // to the call above.
268 embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
269 // Used to make sure the request for url2 succeeds, and there was only one of
271 tracking_delegate().SetTrackedURL(url2
);
272 NavigateToURLContentInitiated(shell(), url2
, true, true);
274 // There should be one history entry. url2 should have replaced url1.
275 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
276 EXPECT_EQ(1, controller
.GetEntryCount());
277 EXPECT_EQ(0, controller
.GetCurrentEntryIndex());
278 EXPECT_EQ(url2
, controller
.GetEntryAtIndex(0)->GetURL());
279 // Make sure the request succeeded.
280 EXPECT_TRUE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
282 // Now navigate as before to a page on B.com, but normally (without
283 // replacement). This will still perform a double process-swap as above, via
284 // OpenURL and then transfer.
286 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
287 // Used to make sure the request for url3 succeeds, and there was only one of
289 tracking_delegate().SetTrackedURL(url3
);
290 NavigateToURLContentInitiated(shell(), url3
, false, true);
292 // There should be two history entries. url2 should have replaced url1. url2
293 // should not have replaced url3.
294 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
295 EXPECT_EQ(2, controller
.GetEntryCount());
296 EXPECT_EQ(1, controller
.GetCurrentEntryIndex());
297 EXPECT_EQ(url2
, controller
.GetEntryAtIndex(0)->GetURL());
298 EXPECT_EQ(url3
, controller
.GetEntryAtIndex(1)->GetURL());
300 // Make sure the request succeeded.
301 EXPECT_TRUE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
304 // Tests that the |should_replace_current_entry| flag persists correctly across
305 // request transfers that began with a content-initiated in-process
306 // navigation. This test is the same as the test above, except transfering from
308 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest
,
309 ReplaceEntryInProcessThenTranfers
) {
310 const NavigationController
& controller
=
311 shell()->web_contents()->GetController();
313 // Navigate to a starting URL, so there is a history entry to replace.
314 GURL url
= embedded_test_server()->GetURL("/site_isolation/blank.html?1");
315 NavigateToURL(shell(), url
);
317 // Force all future navigations to transfer. Note that this includes same-site
318 // navigiations which may cause double process swaps (via OpenURL and then via
319 // transfer). All navigations in this test are same-site, so it only swaps
320 // processes via request transfer.
321 ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
323 // Navigate in-process with entry replacement. It will then be transferred
324 // into a new one due to the call above.
325 GURL url2
= embedded_test_server()->GetURL("/site_isolation/blank.html?2");
326 NavigateToURLContentInitiated(shell(), url2
, true, true);
328 // There should be one history entry. url2 should have replaced url1.
329 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
330 EXPECT_EQ(1, controller
.GetEntryCount());
331 EXPECT_EQ(0, controller
.GetCurrentEntryIndex());
332 EXPECT_EQ(url2
, controller
.GetEntryAtIndex(0)->GetURL());
334 // Now navigate as before, but without replacement.
335 GURL url3
= embedded_test_server()->GetURL("/site_isolation/blank.html?3");
336 NavigateToURLContentInitiated(shell(), url3
, false, true);
338 // There should be two history entries. url2 should have replaced url1. url2
339 // should not have replaced url3.
340 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
341 EXPECT_EQ(2, controller
.GetEntryCount());
342 EXPECT_EQ(1, controller
.GetCurrentEntryIndex());
343 EXPECT_EQ(url2
, controller
.GetEntryAtIndex(0)->GetURL());
344 EXPECT_EQ(url3
, controller
.GetEntryAtIndex(1)->GetURL());
347 // Tests that the |should_replace_current_entry| flag persists correctly across
348 // request transfers that cross processes twice from renderer policy.
349 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest
,
350 MAYBE_ReplaceEntryCrossProcessTwice
) {
351 const NavigationController
& controller
=
352 shell()->web_contents()->GetController();
354 // Navigate to a starting URL, so there is a history entry to replace.
355 GURL url1
= embedded_test_server()->GetURL("/site_isolation/blank.html?1");
356 NavigateToURL(shell(), url1
);
358 // Navigate to a page on A.com which redirects to B.com with entry
359 // replacement. This will switch processes via OpenURL twice. First to A.com,
360 // and second in response to the server redirect to B.com. The second swap is
361 // also renderer-initiated via OpenURL because decidePolicyForNavigation is
362 // currently applied on redirects.
363 GURL::Replacements replace_host
;
365 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?2");
366 GURL url2a
= embedded_test_server()->GetURL(
367 "A.com", "/cross-site/" + url2b
.host() + url2b
.PathForRequest());
368 NavigateToURLContentInitiated(shell(), url2a
, true, true);
370 // There should be one history entry. url2b should have replaced url1.
371 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
372 EXPECT_EQ(1, controller
.GetEntryCount());
373 EXPECT_EQ(0, controller
.GetCurrentEntryIndex());
374 EXPECT_EQ(url2b
, controller
.GetEntryAtIndex(0)->GetURL());
376 // Now repeat without replacement.
378 embedded_test_server()->GetURL("B.com", "/site_isolation/blank.html?3");
379 GURL url3a
= embedded_test_server()->GetURL(
380 "A.com", "/cross-site/" + url3b
.host() + url3b
.PathForRequest());
381 NavigateToURLContentInitiated(shell(), url3a
, false, true);
383 // There should be two history entries. url2b should have replaced url1. url2b
384 // should not have replaced url3b.
385 EXPECT_TRUE(controller
.GetPendingEntry() == NULL
);
386 EXPECT_EQ(2, controller
.GetEntryCount());
387 EXPECT_EQ(1, controller
.GetCurrentEntryIndex());
388 EXPECT_EQ(url2b
, controller
.GetEntryAtIndex(0)->GetURL());
389 EXPECT_EQ(url3b
, controller
.GetEntryAtIndex(1)->GetURL());
392 // Tests that the request is destroyed when a cross process navigation is
394 IN_PROC_BROWSER_TEST_F(CrossSiteTransferTest
, NoLeakOnCrossSiteCancel
) {
395 const NavigationController
& controller
=
396 shell()->web_contents()->GetController();
398 // Navigate to a starting URL, so there is a history entry to replace.
399 GURL url1
= embedded_test_server()->GetURL("/site_isolation/blank.html?1");
400 NavigateToURL(shell(), url1
);
402 // Force all future navigations to transfer.
403 ShellContentBrowserClient::SetSwapProcessesForRedirect(true);
405 NoTransferRequestDelegate no_transfer_request_delegate
;
406 WebContentsDelegate
* old_delegate
= shell()->web_contents()->GetDelegate();
407 shell()->web_contents()->SetDelegate(&no_transfer_request_delegate
);
409 // Navigate to a page on A.com with entry replacement. This navigation is
410 // cross-site, so the renderer will send it to the browser via OpenURL to give
411 // to a new process. It will then be transferred into yet another process due
412 // to the call above.
414 embedded_test_server()->GetURL("A.com", "/site_isolation/blank.html?2");
415 // Used to make sure the second request is cancelled, and there is only one
417 tracking_delegate().SetTrackedURL(url2
);
419 // Don't wait for the navigation to complete, since that never happens in
421 NavigateToURLContentInitiated(shell(), url2
, false, false);
423 // There should be one history entry, with url1.
424 EXPECT_EQ(1, controller
.GetEntryCount());
425 EXPECT_EQ(0, controller
.GetCurrentEntryIndex());
426 EXPECT_EQ(url1
, controller
.GetEntryAtIndex(0)->GetURL());
428 // Make sure the request for url2 did not complete.
429 EXPECT_FALSE(tracking_delegate().WaitForTrackedURLAndGetCompleted());
431 shell()->web_contents()->SetDelegate(old_delegate
);
434 } // namespace content