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
26namespace 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.
30class 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.
143class 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
169class 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.
248IN_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.
316IN_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.
358IN_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.
415IN_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