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(&params);
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