blob: 987b8bccd2ac24633a8ef51346abf0574eb5fb7e [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/service_worker/embedded_worker_instance.h"
#include "content/browser/service_worker/embedded_worker_status.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/allow_service_worker_result.h"
#include "content/public/browser/focused_node_details.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/test_content_browser_client.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/switches.h"
using testing::_;
using testing::NotNull;
namespace content {
class WebContentsObserverBrowserTest : public ContentBrowserTest {
public:
WebContentsObserverBrowserTest() = default;
protected:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
// Some platforms are flaky due to relatively slow loading interacting
// with deferred commits.
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
RenderFrameHostImpl* top_frame_host() {
return static_cast<RenderFrameHostImpl*>(web_contents()->GetMainFrame());
}
base::test::ScopedFeatureList feature_list_;
};
namespace {
class ServiceWorkerAccessObserver : public WebContentsObserver {
public:
ServiceWorkerAccessObserver(WebContentsImpl* web_contents)
: WebContentsObserver(web_contents) {}
MOCK_METHOD3(OnServiceWorkerAccessed,
void(NavigationHandle*, const GURL&, AllowServiceWorkerResult));
MOCK_METHOD3(OnServiceWorkerAccessed,
void(RenderFrameHost*, const GURL&, AllowServiceWorkerResult));
};
} // namespace
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
OnServiceWorkerAccessed) {
GURL service_worker_scope =
embedded_test_server()->GetURL("/service_worker/");
{
// 1) Navigate to a page and register a ServiceWorker. Expect a notification
// to be called when the service worker is accessed from a frame.
ServiceWorkerAccessObserver observer(web_contents());
base::RunLoop run_loop;
EXPECT_CALL(
observer,
OnServiceWorkerAccessed(
testing::Matcher<RenderFrameHost*>(NotNull()), service_worker_scope,
AllowServiceWorkerResult::FromPolicy(false, false)))
.WillOnce([&]() { run_loop.Quit(); });
EXPECT_TRUE(NavigateToURL(
web_contents(), embedded_test_server()->GetURL(
"/service_worker/create_service_worker.html")));
EXPECT_EQ("DONE",
EvalJs(top_frame_host(),
"register('fetch_event.js', '/service_worker/');"));
run_loop.Run();
}
{
// 2) Navigate to a page in scope of the previously registered ServiceWorker
// and expect to get a notification about ServiceWorker being accessed for
// a navigation.
ServiceWorkerAccessObserver observer(web_contents());
base::RunLoop run_loop;
EXPECT_CALL(observer,
OnServiceWorkerAccessed(
testing::Matcher<NavigationHandle*>(NotNull()),
service_worker_scope,
AllowServiceWorkerResult::FromPolicy(false, false)))
.WillOnce([&]() { run_loop.Quit(); });
EXPECT_TRUE(NavigateToURL(
web_contents(),
embedded_test_server()->GetURL("/service_worker/empty.html")));
run_loop.Run();
}
}
namespace {
class ServiceWorkerAccessContentBrowserClient
: public TestContentBrowserClient {
public:
ServiceWorkerAccessContentBrowserClient() = default;
void SetJavascriptAllowed(bool allowed) { javascript_allowed_ = allowed; }
void SetCookiesAllowed(bool allowed) { cookies_allowed_ = allowed; }
AllowServiceWorkerResult AllowServiceWorker(
const GURL& scope,
const GURL& site_for_cookies,
const base::Optional<url::Origin>& top_frame_origin,
const GURL& script_url,
BrowserContext* context) override {
return AllowServiceWorkerResult::FromPolicy(!javascript_allowed_,
!cookies_allowed_);
}
private:
bool cookies_allowed_ = true;
bool javascript_allowed_ = true;
};
} // namespace
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
OnServiceWorkerAccessed_ContentClientBlocked) {
GURL service_worker_scope =
embedded_test_server()->GetURL("/service_worker/");
{
// 1) Navigate to a page and register a ServiceWorker. Expect a notification
// to be called when the service worker is accessed from a frame.
ServiceWorkerAccessObserver observer(web_contents());
base::RunLoop run_loop;
EXPECT_CALL(
observer,
OnServiceWorkerAccessed(
testing::Matcher<RenderFrameHost*>(NotNull()), service_worker_scope,
AllowServiceWorkerResult::FromPolicy(false, false)))
.WillOnce([&]() { run_loop.Quit(); });
EXPECT_TRUE(NavigateToURL(
web_contents(), embedded_test_server()->GetURL(
"/service_worker/create_service_worker.html")));
EXPECT_EQ("DONE",
EvalJs(top_frame_host(),
"register('fetch_event.js', '/service_worker/');"));
run_loop.Run();
}
// 2) Set content client and disallow javascript.
ServiceWorkerAccessContentBrowserClient content_browser_client;
ContentBrowserClient* old_client =
SetBrowserClientForTesting(&content_browser_client);
content_browser_client.SetJavascriptAllowed(false);
{
// 2) Navigate to a page in scope of the previously registered ServiceWorker
// and expect to get a notification about ServiceWorker being accessed for
// a navigation. Javascript should be blocked according to the policy.
ServiceWorkerAccessObserver observer(web_contents());
base::RunLoop run_loop;
EXPECT_CALL(observer, OnServiceWorkerAccessed(
testing::Matcher<NavigationHandle*>(NotNull()),
service_worker_scope,
AllowServiceWorkerResult::FromPolicy(
/* javascript_blocked=*/true,
/* cookies_blocked=*/false)))
.WillOnce([&]() { run_loop.Quit(); });
EXPECT_TRUE(NavigateToURL(
web_contents(),
embedded_test_server()->GetURL("/service_worker/empty.html")));
run_loop.Run();
}
content_browser_client.SetJavascriptAllowed(true);
content_browser_client.SetCookiesAllowed(false);
{
// 3) Navigate to a page in scope of the previously registered ServiceWorker
// and expect to get a notification about ServiceWorker being accessed for
// a navigation. Cookies should be blocked according to the policy.
ServiceWorkerAccessObserver observer(web_contents());
base::RunLoop run_loop;
EXPECT_CALL(observer, OnServiceWorkerAccessed(
testing::Matcher<NavigationHandle*>(NotNull()),
service_worker_scope,
AllowServiceWorkerResult::FromPolicy(
/* javascript_blocked=*/false,
/* cookies_blocked=*/true)))
.WillOnce([&]() { run_loop.Quit(); });
EXPECT_TRUE(NavigateToURL(
web_contents(),
embedded_test_server()->GetURL("/service_worker/empty.html")));
run_loop.Run();
}
SetBrowserClientForTesting(old_client);
}
namespace {
enum class ContextType {
kNavigation,
kFrame,
};
class CookieTracker : public WebContentsObserver {
public:
explicit CookieTracker(WebContentsImpl* web_contents)
: WebContentsObserver(web_contents) {}
struct CookieAccessDescription {
CookieAccessDetails::Type type;
ContextType context_type;
GlobalRoutingID frame_id;
int navigation_id = -1;
GURL url;
GURL first_party_url;
std::string cookie_name;
std::string cookie_value;
friend std::ostream& operator<<(std::ostream& o,
const CookieAccessDescription& d) {
o << (d.type == CookieAccessDetails::Type::kRead ? "read" : "change");
o << " url=" << d.url;
o << " first_party_url=" << d.first_party_url;
o << " name=" << d.cookie_name;
o << " value=" << d.cookie_value;
switch (d.context_type) {
case ContextType::kNavigation:
o << " context=navigation(";
o << "id=" << d.navigation_id;
o << ")";
break;
case ContextType::kFrame:
o << " context=frame(";
o << "process_id=" << d.frame_id.child_id;
o << " frame_id=" << d.frame_id.route_id;
o << ")";
break;
}
return o;
}
private:
auto comparison_key() const {
return std::tie(type, url, first_party_url, cookie_name, cookie_value,
frame_id, navigation_id);
}
public:
bool operator==(const CookieAccessDescription& other) const {
return comparison_key() == other.comparison_key();
}
};
void OnCookiesAccessed(NavigationHandle* navigation,
const CookieAccessDetails& details) override {
for (const auto& cookie : details.cookie_list) {
cookie_accesses_.push_back({details.type,
ContextType::kNavigation,
{},
navigation->GetNavigationId(),
details.url,
details.first_party_url,
cookie.Name(),
cookie.Value()});
}
QuitIfReady();
}
void OnCookiesAccessed(RenderFrameHost* rfh,
const CookieAccessDetails& details) override {
for (const auto& cookie : details.cookie_list) {
cookie_accesses_.push_back(
{details.type,
ContextType::kFrame,
{rfh->GetProcess()->GetID(), rfh->GetRoutingID()},
-1,
details.url,
details.first_party_url,
cookie.Name(),
cookie.Value()});
}
QuitIfReady();
}
void WaitForCookies(size_t count) {
waiting_for_cookies_count_ = count;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
QuitIfReady();
run_loop.Run();
}
std::vector<CookieAccessDescription>& cookie_accesses() {
return cookie_accesses_;
}
GlobalRoutingID frame_id(size_t index) {
if (index < frame_ids_.size())
return frame_ids_[index];
// Return bogus values which will never be returned by the code we are
// testing. This ensures that if we return this value, the subsequent
// comparison will fail.
return {-42, -42};
}
int64_t navigation_id(size_t index) {
if (index < navigation_ids_.size())
return navigation_ids_[index];
// Return bogus values which will never be returned by the code we are
// testing. This ensures that if we return this value, the subsequent
// comparison will fail.
return -42;
}
void DidFinishNavigation(NavigationHandle* navigation) override {
navigation_ids_.push_back(navigation->GetNavigationId());
}
void RenderFrameCreated(RenderFrameHost* rfh) override {
frame_ids_.emplace_back(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
}
private:
void QuitIfReady() {
if (quit_closure_.is_null())
return;
if (cookie_accesses_.size() < waiting_for_cookies_count_)
return;
std::move(quit_closure_).Run();
}
std::vector<CookieAccessDescription> cookie_accesses_;
// List of observed navigation and frame ids to be used in testing.
std::vector<GlobalRoutingID> frame_ids_;
std::vector<int64_t> navigation_ids_;
size_t waiting_for_cookies_count_ = 0;
base::OnceClosure quit_closure_;
};
using CookieAccess = CookieTracker::CookieAccessDescription;
} // namespace
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
CookieCallbacks_MainFrame) {
CookieTracker cookie_tracker(web_contents());
GURL first_party_url("http://a.com/");
GURL url1(
embedded_test_server()->GetURL("a.com", "/cookies/set_cookie.html"));
GURL url2(embedded_test_server()->GetURL("a.com", "/title1.html"));
// 1) Navigate to |url1|. This navigation should set a cookie, which we should
// be notified about.
EXPECT_TRUE(NavigateToURL(web_contents(), url1));
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(CookieAccess{CookieAccessDetails::Type::kChange,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(0),
url1,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
// 2) Navigate to |url2| on the same site. Given that we have set a cookie
// before, this should sent a previously set cookie with the request and we
// should be notified about this.
EXPECT_TRUE(NavigateToURL(web_contents(), url2));
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(1),
url2,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
}
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
CookieCallbacks_MainFrameRedirect) {
CookieTracker cookie_tracker(web_contents());
GURL first_party_url("http://a.com/");
GURL url1(embedded_test_server()->GetURL(
"a.com", "/cookies/redirect_and_set_cookie.html"));
GURL url1_after_redirect(
embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
// 1) Navigate to |url1|. The initial URL redirects and sets a cookie (we
// should be notified about this) and as the redirect points to the same site,
// cookie should be sent for the second request as well (we should be notified
// about this as well).
EXPECT_TRUE(NavigateToURL(web_contents(), url1, url1_after_redirect));
cookie_tracker.WaitForCookies(2);
EXPECT_THAT(cookie_tracker.cookie_accesses(),
testing::UnorderedElementsAre(
CookieAccess{CookieAccessDetails::Type::kChange,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(0),
url1,
first_party_url,
"foo",
"bar"},
CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(0),
url1_after_redirect,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
// 2) Navigate to another url on the same site and expect a notification about
// a read cookie.
EXPECT_TRUE(NavigateToURL(web_contents(), url2));
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(1),
url2,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
}
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
CookieCallbacks_Subframe) {
CookieTracker cookie_tracker(web_contents());
GURL first_party_url("http://a.com/");
GURL url1(embedded_test_server()->GetURL(
"a.com", "/cookies/set_cookie_from_subframe.html"));
GURL url1_subframe(
embedded_test_server()->GetURL("a.com", "/cookies/set_cookie.html"));
GURL url2(embedded_test_server()->GetURL("a.com",
"/cookies/page_with_subframe.html"));
GURL url2_subframe(embedded_test_server()->GetURL("a.com", "/title1.html"));
// 1) Load a page with a subframe. The main resource of the the subframe
// triggers setting a cookie. We should get a cookie change for the
// subresource and no cookie read for the main resource.
EXPECT_TRUE(NavigateToURL(web_contents(), url1));
cookie_tracker.WaitForCookies(1);
// Navigations are: main frame (0), subframe (1).
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(CookieAccess{CookieAccessDetails::Type::kChange,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(1),
url1_subframe,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
EXPECT_TRUE(NavigateToURL(web_contents(), url2));
// 2) Load a page with a subframe. Both main frame and subframe should get a
// cookie read.
cookie_tracker.WaitForCookies(2);
// Navigations are: main frame (2), subframe (3).
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(2),
url2,
first_party_url,
"foo",
"bar"},
CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(3),
url2_subframe,
first_party_url,
"foo",
"bar"}));
cookie_tracker.cookie_accesses().clear();
}
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
CookieCallbacks_Subresource) {
CookieTracker cookie_tracker(web_contents());
GURL first_party_url("http://a.com/");
GURL url1(embedded_test_server()->GetURL(
"a.com", "/cookies/set_cookie_from_subresource.html"));
GURL url1_image(embedded_test_server()->GetURL(
"a.com", "/cookies/image_with_set_cookie.jpg"));
GURL url2(embedded_test_server()->GetURL(
"a.com", "/cookies/page_with_subresource.html"));
GURL url2_image(embedded_test_server()->GetURL(
"a.com", "/cookies/image_without_set_cookie.jpg"));
EXPECT_TRUE(NavigateToURL(web_contents(), url1));
// 1) Load a page with a subresource (image), which sets a cookie when
// fetched.
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(cookie_tracker.cookie_accesses(),
testing::ElementsAre(
CookieAccess{CookieAccessDetails::Type::kChange,
ContextType::kFrame, cookie_tracker.frame_id(0),
-1, url1_image, first_party_url, "foo", "bar"}));
cookie_tracker.cookie_accesses().clear();
// 2) Load a page with subresource. Both the page and the resource should get
// a cookie.
EXPECT_TRUE(NavigateToURL(web_contents(), url2));
// If the RFH changes after navigation, the cookie will be attributed to a
// different frame.
int frame_id_index =
CanSameSiteMainFrameNavigationsChangeRenderFrameHosts() ? 1 : 0;
cookie_tracker.WaitForCookies(2);
EXPECT_THAT(
cookie_tracker.cookie_accesses(),
testing::ElementsAre(
CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kNavigation,
{},
cookie_tracker.navigation_id(1),
url2,
first_party_url,
"foo",
"bar"},
CookieAccess{CookieAccessDetails::Type::kRead, ContextType::kFrame,
cookie_tracker.frame_id(frame_id_index), -1, url2_image,
first_party_url, "foo", "bar"}));
cookie_tracker.cookie_accesses().clear();
}
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
CookieCallbacks_DocumentCookie) {
CookieTracker cookie_tracker(web_contents());
GURL first_party_url("http://a.com/");
GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(web_contents(), url1));
EXPECT_TRUE(ExecJs(web_contents(), "document.cookie='foo=bar'"));
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(cookie_tracker.cookie_accesses(),
testing::ElementsAre(
CookieAccess{CookieAccessDetails::Type::kChange,
ContextType::kFrame, cookie_tracker.frame_id(0),
-1, url1, first_party_url, "foo", "bar"}));
cookie_tracker.cookie_accesses().clear();
EXPECT_EQ("foo=bar", EvalJs(web_contents(), "document.cookie"));
cookie_tracker.WaitForCookies(1);
EXPECT_THAT(cookie_tracker.cookie_accesses(),
testing::ElementsAre(
CookieAccess{CookieAccessDetails::Type::kRead,
ContextType::kFrame, cookie_tracker.frame_id(0),
-1, url1, first_party_url, "foo", "bar"}));
cookie_tracker.cookie_accesses().clear();
}
namespace {
class FocusedNodeObserver : public WebContentsObserver {
public:
explicit FocusedNodeObserver(WebContentsImpl* web_contents)
: WebContentsObserver(web_contents) {}
blink::mojom::FocusType last_focus_type() const { return last_focus_type_; }
void WaitForFocusChangedInPage() { run_loop_.Run(); }
// WebContentsObserver:
void OnFocusChangedInPage(FocusedNodeDetails* details) override {
last_focus_type_ = details->focus_type;
run_loop_.Quit();
}
private:
base::RunLoop run_loop_;
blink::mojom::FocusType last_focus_type_;
};
// Tests that the focus type is reported correctly in FocusedNodeDetails when
// WebContentsObserver::OnFocusChangedInPage() is called.
IN_PROC_BROWSER_TEST_F(WebContentsObserverBrowserTest,
OnFocusChangedInPageFocusType) {
FocusedNodeObserver observer(web_contents());
GURL url(embedded_test_server()->GetURL("/form_that_posts_cross_site.html"));
EXPECT_TRUE(NavigateToURL(web_contents(), url));
SimulateMouseClickOrTapElementWithId(web_contents(), "text");
observer.WaitForFocusChangedInPage();
EXPECT_EQ(blink::mojom::FocusType::kMouse, observer.last_focus_type());
}
} // namespace
} // namespace content