1 // Copyright 2015 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/bind.h"
6 #include "base/files/file_path.h"
7 #include "base/strings/stringprintf.h"
8 #include "chrome/browser/extensions/extension_apitest.h"
9 #include "chrome/browser/ui/browser_navigator_params.h"
10 #include "chrome/browser/ui/tabs/tab_strip_model.h"
11 #include "chrome/test/base/ui_test_utils.h"
12 #include "content/public/browser/web_contents.h"
13 #include "content/public/test/browser_test.h"
14 #include "content/public/test/browser_test_utils.h"
15 #include "extensions/common/extension.h"
16 #include "extensions/test/test_extension_dir.h"
17 #include "net/dns/mock_host_resolver.h"
18 #include "net/test/embedded_test_server/embedded_test_server.h"
19 #include "net/test/embedded_test_server/http_request.h"
20 #include "net/test/embedded_test_server/http_response.h"
21 #include "services/network/public/cpp/features.h"
22
23 namespace extensions {
24
25 namespace {
26
27 // Returns a response whose body is request's origin.
HandleEchoOrigin(const net::test_server::HttpRequest & request)28 std::unique_ptr<net::test_server::HttpResponse> HandleEchoOrigin(
29 const net::test_server::HttpRequest& request) {
30 if (request.relative_url != "/echo-origin")
31 return nullptr;
32
33 auto response = std::make_unique<net::test_server::BasicHttpResponse>();
34 response->set_code(net::HTTP_OK);
35 response->set_content_type("text/plain");
36 auto it = request.headers.find("origin");
37 if (it != request.headers.end()) {
38 response->set_content(it->second);
39 } else {
40 response->set_content("<no origin attached>");
41 }
42 response->AddCustomHeader("access-control-allow-origin", "*");
43
44 return response;
45 }
46
47 // JavaScript snippet which performs a fetch given a URL expression to be
48 // substituted as %s, then sends back the fetched content using the
49 // domAutomationController.
50 const char* kFetchScript =
51 "fetch(%s).then(function(result) {\n"
52 " return result.text();\n"
53 "}).then(function(text) {\n"
54 " window.domAutomationController.send(text);\n"
55 "}).catch(function(err) {\n"
56 " window.domAutomationController.send(String(err));\n"
57 "});\n";
58
59 constexpr char kFetchPostScript[] = R"(
60 fetch($1, {method: 'POST'}).then((result) => {
61 return result.text();
62 }).then((text) => {
63 window.domAutomationController.send(text);
64 }).catch((error) => {
65 window.domAutomationController.send(String(err));
66 });
67 )";
68
69 class ExtensionFetchTest : public ExtensionApiTest {
70 protected:
71 // Writes an empty background page and a text file called "text" with content
72 // "text content", then loads and returns the extension. |dir| must already
73 // have a manifest.
WriteFilesAndLoadTestExtension(TestExtensionDir * dir)74 const Extension* WriteFilesAndLoadTestExtension(TestExtensionDir* dir) {
75 dir->WriteFile(FILE_PATH_LITERAL("text"), "text content");
76 dir->WriteFile(FILE_PATH_LITERAL("bg.js"), "");
77 return LoadExtension(dir->UnpackedPath());
78 }
79
80 // Returns |kFetchScript| with |url_expression| substituted as its test URL.
GetFetchScript(const std::string & url_expression)81 std::string GetFetchScript(const std::string& url_expression) {
82 return base::StringPrintf(kFetchScript, url_expression.c_str());
83 }
84
85 // Returns |url| as a string surrounded by single quotes, for passing to
86 // JavaScript as a string literal.
GetQuotedURL(const GURL & url)87 std::string GetQuotedURL(const GURL& url) {
88 return base::StringPrintf("'%s'", url.spec().c_str());
89 }
90
91 // Like GetQuotedURL(), but fetching the URL from the test server's |host|
92 // and |path|.
GetQuotedTestServerURL(const std::string & host,const std::string & path)93 std::string GetQuotedTestServerURL(const std::string& host,
94 const std::string& path) {
95 return GetQuotedURL(embedded_test_server()->GetURL(host, path));
96 }
97
98 // Opens a tab, puts it in the foreground, navigates it to |url| then returns
99 // its WebContents.
CreateAndNavigateTab(const GURL & url)100 content::WebContents* CreateAndNavigateTab(const GURL& url) {
101 NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
102 params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
103 ui_test_utils::NavigateToURL(¶ms);
104 return browser()->tab_strip_model()->GetActiveWebContents();
105 }
106
SetUpOnMainThread()107 void SetUpOnMainThread() override {
108 ExtensionApiTest::SetUpOnMainThread();
109 host_resolver()->AddRule("*", "127.0.0.1");
110
111 embedded_test_server()->RegisterRequestHandler(
112 base::BindRepeating(HandleEchoOrigin));
113 ASSERT_TRUE(StartEmbeddedTestServer());
114 }
115 };
116
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,ExtensionCanFetchExtensionResource)117 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest, ExtensionCanFetchExtensionResource) {
118 TestExtensionDir dir;
119 constexpr char kManifest[] =
120 R"({
121 "background": {"scripts": ["bg.js"]},
122 "manifest_version": 2,
123 "name": "ExtensionCanFetchExtensionResource",
124 "version": "1"
125 })";
126 dir.WriteManifest(kManifest);
127 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
128 ASSERT_TRUE(extension);
129
130 EXPECT_EQ(
131 "text content",
132 ExecuteScriptInBackgroundPage(
133 extension->id(), GetFetchScript("chrome.runtime.getURL('text')")));
134 }
135
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,ExtensionCanFetchHostedResourceWithHostPermissions)136 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,
137 ExtensionCanFetchHostedResourceWithHostPermissions) {
138 TestExtensionDir dir;
139 constexpr char kManifest[] =
140 R"({
141 "background": {"scripts": ["bg.js"]},
142 "manifest_version": 2,
143 "name": "ExtensionCanFetchHostedResourceWithHostPermissions",
144 "permissions": ["http://example.com/*"],
145 "version": "1"
146 })";
147 dir.WriteManifest(kManifest);
148 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
149 ASSERT_TRUE(extension);
150
151 EXPECT_EQ("Hello!", ExecuteScriptInBackgroundPage(
152 extension->id(),
153 GetFetchScript(GetQuotedTestServerURL(
154 "example.com", "/extensions/test_file.txt"))));
155 }
156
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,ExtensionCannotFetchHostedResourceWithoutHostPermissions)157 IN_PROC_BROWSER_TEST_F(
158 ExtensionFetchTest,
159 ExtensionCannotFetchHostedResourceWithoutHostPermissions) {
160 TestExtensionDir dir;
161 constexpr char kManifest[] =
162 R"({
163 "background": {"scripts": ["bg.js"]},
164 "manifest_version": 2,
165 "name": "ExtensionCannotFetchHostedResourceWithoutHostPermissions",
166 "version": "1"
167 })";
168 dir.WriteManifest(kManifest);
169 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
170 ASSERT_TRUE(extension);
171
172 // TODO(kalman): Another test would be to configure the test server to work
173 // with CORS, and test that the fetch succeeds.
174 EXPECT_EQ(
175 "TypeError: Failed to fetch",
176 ExecuteScriptInBackgroundPage(
177 extension->id(), GetFetchScript(GetQuotedTestServerURL(
178 "example.com", "/extensions/test_file.txt"))));
179 }
180
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,HostCanFetchWebAccessibleExtensionResource)181 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,
182 HostCanFetchWebAccessibleExtensionResource) {
183 TestExtensionDir dir;
184 constexpr char kManifest[] =
185 R"({
186 "background": {"scripts": ["bg.js"]},
187 "manifest_version": 2,
188 "name": "HostCanFetchWebAccessibleExtensionResource",
189 "version": "1",
190 "web_accessible_resources": ["text"]
191 })";
192 dir.WriteManifest(kManifest);
193 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
194 ASSERT_TRUE(extension);
195
196 content::WebContents* empty_tab = CreateAndNavigateTab(
197 embedded_test_server()->GetURL("example.com", "/empty.html"));
198
199 // TODO(kalman): Test this from a content script too.
200 std::string fetch_result;
201 ASSERT_TRUE(content::ExecuteScriptAndExtractString(
202 empty_tab,
203 GetFetchScript(GetQuotedURL(extension->GetResourceURL("text"))),
204 &fetch_result));
205 EXPECT_EQ("text content", fetch_result);
206 }
207
208 // Calling fetch() from a http(s) service worker context to a
209 // chrome-extensions:// URL since the loading path in a service worker is
210 // different from pages.
211 // This is a regression test for https://crbug.com/901443.
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,HostCanFetchWebAccessibleExtensionResource_FetchFromServiceWorker)212 IN_PROC_BROWSER_TEST_F(
213 ExtensionFetchTest,
214 HostCanFetchWebAccessibleExtensionResource_FetchFromServiceWorker) {
215 TestExtensionDir dir;
216 constexpr char kManifest[] =
217 R"({
218 "background": {"scripts": ["bg.js"]},
219 "manifest_version": 2,
220 "name": "FetchFromServiceWorker",
221 "version": "1",
222 "web_accessible_resources": ["text"]
223 })";
224 dir.WriteManifest(kManifest);
225 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
226 ASSERT_TRUE(extension);
227
228 content::WebContents* tab =
229 CreateAndNavigateTab(embedded_test_server()->GetURL(
230 "/workers/fetch_from_service_worker.html"));
231 EXPECT_EQ("ready", content::EvalJs(tab, "setup();"));
232 EXPECT_EQ("text content",
233 content::EvalJs(
234 tab, base::StringPrintf(
235 "fetch_from_service_worker('%s');",
236 extension->GetResourceURL("text").spec().c_str())));
237 }
238
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,HostCannotFetchNonWebAccessibleExtensionResource)239 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,
240 HostCannotFetchNonWebAccessibleExtensionResource) {
241 TestExtensionDir dir;
242 constexpr char kManifest[] =
243 R"({
244 "background": {"scripts": ["bg.js"]},
245 "manifest_version": 2,
246 "name": "HostCannotFetchNonWebAccessibleExtensionResource",
247 "version": "1"
248 })";
249 dir.WriteManifest(kManifest);
250 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
251 ASSERT_TRUE(extension);
252
253 content::WebContents* empty_tab = CreateAndNavigateTab(
254 embedded_test_server()->GetURL("example.com", "/empty.html"));
255
256 // TODO(kalman): Test this from a content script too.
257 std::string fetch_result;
258 ASSERT_TRUE(content::ExecuteScriptAndExtractString(
259 empty_tab,
260 GetFetchScript(GetQuotedURL(extension->GetResourceURL("text"))),
261 &fetch_result));
262 EXPECT_EQ("TypeError: Failed to fetch", fetch_result);
263 }
264
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,FetchResponseType)265 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest, FetchResponseType) {
266 const std::string script = base::StringPrintf(
267 "fetch(%s).then(function(response) {\n"
268 " window.domAutomationController.send(response.type);\n"
269 "}).catch(function(err) {\n"
270 " window.domAutomationController.send(String(err));\n"
271 "});\n",
272 GetQuotedTestServerURL("example.com", "/extensions/test_file.txt")
273 .data());
274 TestExtensionDir dir;
275 constexpr char kManifest[] =
276 R"({
277 "background": {"scripts": ["bg.js"]},
278 "manifest_version": 2,
279 "name": "FetchResponseType",
280 "permissions": ["http://example.com/*"],
281 "version": "1"
282 })";
283 dir.WriteManifest(kManifest);
284 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
285 ASSERT_TRUE(extension);
286
287 EXPECT_EQ("basic", ExecuteScriptInBackgroundPage(extension->id(), script));
288 }
289
290 class ExtensionFetchPostOriginTest : public ExtensionFetchTest,
291 public testing::WithParamInterface<bool> {
292 protected:
SetUp()293 void SetUp() override {
294 if (GetParam()) {
295 scoped_feature_list_.InitAndEnableFeature(
296 network::features::
297 kDeriveOriginFromUrlForNeitherGetNorHeadRequestWhenHavingSpecialAccess);
298 } else {
299 scoped_feature_list_.InitAndDisableFeature(
300 network::features::
301 kDeriveOriginFromUrlForNeitherGetNorHeadRequestWhenHavingSpecialAccess);
302 }
303 ExtensionFetchTest::SetUp();
304 }
305
306 private:
307 base::test::ScopedFeatureList scoped_feature_list_;
308 };
309
310 IN_PROC_BROWSER_TEST_P(ExtensionFetchPostOriginTest,
311 OriginOnPostWithPermissions) {
312 TestExtensionDir dir;
313 dir.WriteManifest(R"JSON(
314 {
315 "background": {"scripts": ["bg.js"]},
316 "manifest_version": 2,
317 "name": "FetchResponseType",
318 "permissions": ["http://example.com/*"],
319 "version": "1"
320 })JSON");
321 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
322 ASSERT_TRUE(extension);
323
324 GURL destination_url =
325 embedded_test_server()->GetURL("example.com", "/echo-origin");
326 std::string script = content::JsReplace(kFetchPostScript, destination_url);
327 std::string origin_string =
328 GetParam() ? url::Origin::Create(destination_url).Serialize()
329 : url::Origin::Create(extension->url()).Serialize();
330 EXPECT_EQ(origin_string,
331 ExecuteScriptInBackgroundPage(extension->id(), script));
332 }
333
IN_PROC_BROWSER_TEST_P(ExtensionFetchPostOriginTest,OriginOnPostWithoutPermissions)334 IN_PROC_BROWSER_TEST_P(ExtensionFetchPostOriginTest,
335 OriginOnPostWithoutPermissions) {
336 TestExtensionDir dir;
337 dir.WriteManifest(R"JSON(
338 {
339 "background": {"scripts": ["bg.js"]},
340 "manifest_version": 2,
341 "name": "FetchResponseType",
342 "permissions": [],
343 "version": "1"
344 })JSON");
345 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
346 ASSERT_TRUE(extension);
347
348 const std::string script = content::JsReplace(
349 kFetchPostScript,
350 embedded_test_server()->GetURL("example.com", "/echo-origin"));
351 EXPECT_EQ(url::Origin::Create(extension->url()).Serialize(),
352 ExecuteScriptInBackgroundPage(extension->id(), script));
353 }
354
355 INSTANTIATE_TEST_SUITE_P(UseExtensionOrigin,
356 ExtensionFetchPostOriginTest,
357 testing::Values(false));
358
359 INSTANTIATE_TEST_SUITE_P(UseDestinationUrlOrigin,
360 ExtensionFetchPostOriginTest,
361 testing::Values(true));
362
363 // An extension background script should be able to fetch resources contained in
364 // the extension, and those resources should not be opaque.
IN_PROC_BROWSER_TEST_F(ExtensionFetchTest,ExtensionResourceShouldNotBeOpaque)365 IN_PROC_BROWSER_TEST_F(ExtensionFetchTest, ExtensionResourceShouldNotBeOpaque) {
366 // We use a script to test this feature. Ideally testing with fetch() and
367 // response type is better, but some logic in blink (see the manual
368 // response type handling in blink::FetchManager) would hide potential
369 // breakages, which is why we are using a script.
370 const std::string script = base::StringPrintf(R"(
371 const script = document.createElement('script');
372 window.onerror = (message) => {
373 window.domAutomationController.send('onerror: ' + message);
374 }
375 script.src = 'error.js'
376 document.body.appendChild(script);)");
377 TestExtensionDir dir;
378 dir.WriteManifest(R"JSON(
379 {
380 "background": {"scripts": ["bg.js"]},
381 "manifest_version": 2,
382 "name": "FetchResponseType",
383 "permissions": [],
384 "version": "1"
385 })JSON");
386 dir.WriteFile(FILE_PATH_LITERAL("error.js"), "throw TypeError('hi!')");
387 const Extension* extension = WriteFilesAndLoadTestExtension(&dir);
388 ASSERT_TRUE(extension);
389
390 // We expect that we can read the content of the error here. Otherwise
391 // "onerror: Script error." will be seen.
392 EXPECT_EQ("onerror: Uncaught TypeError: hi!",
393 ExecuteScriptInBackgroundPage(extension->id(), script));
394 }
395
396 } // namespace
397
398 } // namespace extensions
399