1// Copyright 2014 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 <string>
6
7#include "base/files/file_util.h"
8#include "base/memory/scoped_ptr.h"
9#include "base/message_loop/message_loop.h"
10#include "base/strings/string_number_conversions.h"
11#include "base/strings/string_util.h"
12#include "base/values.h"
13#include "chrome/common/chrome_paths.h"
14#include "content/public/browser/resource_request_info.h"
15#include "content/public/test/mock_resource_context.h"
16#include "content/public/test/test_browser_thread_bundle.h"
17#include "extensions/browser/extension_protocols.h"
18#include "extensions/browser/info_map.h"
19#include "extensions/common/constants.h"
20#include "extensions/common/extension.h"
21#include "net/base/request_priority.h"
22#include "net/url_request/url_request.h"
23#include "net/url_request/url_request_job_factory_impl.h"
24#include "net/url_request/url_request_status.h"
25#include "net/url_request/url_request_test_util.h"
26#include "testing/gtest/include/gtest/gtest.h"
27
28using content::ResourceType;
29
30namespace extensions {
31namespace {
32
33scoped_refptr<Extension> CreateTestExtension(const std::string& name,
34                                             bool incognito_split_mode) {
35  base::DictionaryValue manifest;
36  manifest.SetString("name", name);
37  manifest.SetString("version", "1");
38  manifest.SetInteger("manifest_version", 2);
39  manifest.SetString("incognito", incognito_split_mode ? "split" : "spanning");
40
41  base::FilePath path;
42  EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &path));
43  path = path.AppendASCII("extensions").AppendASCII("response_headers");
44
45  std::string error;
46  scoped_refptr<Extension> extension(
47      Extension::Create(path, Manifest::INTERNAL, manifest,
48                        Extension::NO_FLAGS, &error));
49  EXPECT_TRUE(extension.get()) << error;
50  return extension;
51}
52
53scoped_refptr<Extension> CreateWebStoreExtension() {
54  base::DictionaryValue manifest;
55  manifest.SetString("name", "WebStore");
56  manifest.SetString("version", "1");
57  manifest.SetString("icons.16", "webstore_icon_16.png");
58
59  base::FilePath path;
60  EXPECT_TRUE(PathService::Get(chrome::DIR_RESOURCES, &path));
61  path = path.AppendASCII("web_store");
62
63  std::string error;
64  scoped_refptr<Extension> extension(
65      Extension::Create(path, Manifest::COMPONENT, manifest,
66                        Extension::NO_FLAGS, &error));
67  EXPECT_TRUE(extension.get()) << error;
68  return extension;
69}
70
71scoped_refptr<Extension> CreateTestResponseHeaderExtension() {
72  base::DictionaryValue manifest;
73  manifest.SetString("name", "An extension with web-accessible resources");
74  manifest.SetString("version", "2");
75
76  base::ListValue* web_accessible_list = new base::ListValue();
77  web_accessible_list->AppendString("test.dat");
78  manifest.Set("web_accessible_resources", web_accessible_list);
79
80  base::FilePath path;
81  EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &path));
82  path = path.AppendASCII("extensions").AppendASCII("response_headers");
83
84  std::string error;
85  scoped_refptr<Extension> extension(
86      Extension::Create(path, Manifest::UNPACKED, manifest,
87                        Extension::NO_FLAGS, &error));
88  EXPECT_TRUE(extension.get()) << error;
89  return extension;
90}
91
92}  // namespace
93
94// This test lives in src/chrome instead of src/extensions because it tests
95// functionality delegated back to Chrome via ChromeExtensionsBrowserClient.
96// See chrome/browser/extensions/chrome_url_request_util.cc.
97class ExtensionProtocolTest : public testing::Test {
98 public:
99  ExtensionProtocolTest()
100      : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
101        old_factory_(NULL),
102        resource_context_(&test_url_request_context_) {}
103
104  virtual void SetUp() OVERRIDE {
105    testing::Test::SetUp();
106    extension_info_map_ = new InfoMap();
107    net::URLRequestContext* request_context =
108        resource_context_.GetRequestContext();
109    old_factory_ = request_context->job_factory();
110  }
111
112  virtual void TearDown() {
113    net::URLRequestContext* request_context =
114        resource_context_.GetRequestContext();
115    request_context->set_job_factory(old_factory_);
116  }
117
118  void SetProtocolHandler(bool is_incognito) {
119    net::URLRequestContext* request_context =
120        resource_context_.GetRequestContext();
121    job_factory_.SetProtocolHandler(
122        kExtensionScheme,
123        CreateExtensionProtocolHandler(is_incognito,
124                                       extension_info_map_.get()));
125    request_context->set_job_factory(&job_factory_);
126  }
127
128  void StartRequest(net::URLRequest* request,
129                    ResourceType resource_type) {
130    content::ResourceRequestInfo::AllocateForTesting(request,
131                                                     resource_type,
132                                                     &resource_context_,
133                                                     -1,
134                                                     -1,
135                                                     -1,
136                                                     false);
137    request->Start();
138    base::MessageLoop::current()->Run();
139  }
140
141 protected:
142  content::TestBrowserThreadBundle thread_bundle_;
143  scoped_refptr<InfoMap> extension_info_map_;
144  net::URLRequestJobFactoryImpl job_factory_;
145  const net::URLRequestJobFactory* old_factory_;
146  net::TestDelegate test_delegate_;
147  net::TestURLRequestContext test_url_request_context_;
148  content::MockResourceContext resource_context_;
149};
150
151// Tests that making a chrome-extension request in an incognito context is
152// only allowed under the right circumstances (if the extension is allowed
153// in incognito, and it's either a non-main-frame request or a split-mode
154// extension).
155TEST_F(ExtensionProtocolTest, IncognitoRequest) {
156  // Register an incognito extension protocol handler.
157  SetProtocolHandler(true);
158
159  struct TestCase {
160    // Inputs.
161    std::string name;
162    bool incognito_split_mode;
163    bool incognito_enabled;
164
165    // Expected results.
166    bool should_allow_main_frame_load;
167    bool should_allow_sub_frame_load;
168  } cases[] = {
169    {"spanning disabled", false, false, false, false},
170    {"split disabled", true, false, false, false},
171    {"spanning enabled", false, true, false, true},
172    {"split enabled", true, true, true, true},
173  };
174
175  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) {
176    scoped_refptr<Extension> extension =
177        CreateTestExtension(cases[i].name, cases[i].incognito_split_mode);
178    extension_info_map_->AddExtension(
179        extension.get(), base::Time::Now(), cases[i].incognito_enabled, false);
180
181    // First test a main frame request.
182    {
183      // It doesn't matter that the resource doesn't exist. If the resource
184      // is blocked, we should see ADDRESS_UNREACHABLE. Otherwise, the request
185      // should just fail because the file doesn't exist.
186      scoped_ptr<net::URLRequest> request(
187          resource_context_.GetRequestContext()->CreateRequest(
188              extension->GetResourceURL("404.html"),
189              net::DEFAULT_PRIORITY,
190              &test_delegate_,
191              NULL));
192      StartRequest(request.get(), content::RESOURCE_TYPE_MAIN_FRAME);
193      EXPECT_EQ(net::URLRequestStatus::FAILED, request->status().status());
194
195      if (cases[i].should_allow_main_frame_load) {
196        EXPECT_EQ(net::ERR_FILE_NOT_FOUND, request->status().error()) <<
197            cases[i].name;
198      } else {
199        EXPECT_EQ(net::ERR_ADDRESS_UNREACHABLE, request->status().error()) <<
200            cases[i].name;
201      }
202    }
203
204    // Now do a subframe request.
205    {
206      scoped_ptr<net::URLRequest> request(
207          resource_context_.GetRequestContext()->CreateRequest(
208              extension->GetResourceURL("404.html"),
209              net::DEFAULT_PRIORITY,
210              &test_delegate_,
211              NULL));
212      StartRequest(request.get(), content::RESOURCE_TYPE_SUB_FRAME);
213      EXPECT_EQ(net::URLRequestStatus::FAILED, request->status().status());
214
215      if (cases[i].should_allow_sub_frame_load) {
216        EXPECT_EQ(net::ERR_FILE_NOT_FOUND, request->status().error()) <<
217            cases[i].name;
218      } else {
219        EXPECT_EQ(net::ERR_ADDRESS_UNREACHABLE, request->status().error()) <<
220            cases[i].name;
221      }
222    }
223  }
224}
225
226void CheckForContentLengthHeader(net::URLRequest* request) {
227  std::string content_length;
228  request->GetResponseHeaderByName(net::HttpRequestHeaders::kContentLength,
229                                  &content_length);
230  EXPECT_FALSE(content_length.empty());
231  int length_value = 0;
232  EXPECT_TRUE(base::StringToInt(content_length, &length_value));
233  EXPECT_GT(length_value, 0);
234}
235
236// Tests getting a resource for a component extension works correctly, both when
237// the extension is enabled and when it is disabled.
238TEST_F(ExtensionProtocolTest, ComponentResourceRequest) {
239  // Register a non-incognito extension protocol handler.
240  SetProtocolHandler(false);
241
242  scoped_refptr<Extension> extension = CreateWebStoreExtension();
243  extension_info_map_->AddExtension(extension.get(),
244                                    base::Time::Now(),
245                                    false,
246                                    false);
247
248  // First test it with the extension enabled.
249  {
250    scoped_ptr<net::URLRequest> request(
251        resource_context_.GetRequestContext()->CreateRequest(
252            extension->GetResourceURL("webstore_icon_16.png"),
253            net::DEFAULT_PRIORITY,
254            &test_delegate_,
255            NULL));
256    StartRequest(request.get(), content::RESOURCE_TYPE_MEDIA);
257    EXPECT_EQ(net::URLRequestStatus::SUCCESS, request->status().status());
258    CheckForContentLengthHeader(request.get());
259  }
260
261  // And then test it with the extension disabled.
262  extension_info_map_->RemoveExtension(extension->id(),
263                                       UnloadedExtensionInfo::REASON_DISABLE);
264  {
265    scoped_ptr<net::URLRequest> request(
266        resource_context_.GetRequestContext()->CreateRequest(
267            extension->GetResourceURL("webstore_icon_16.png"),
268            net::DEFAULT_PRIORITY,
269            &test_delegate_,
270            NULL));
271    StartRequest(request.get(), content::RESOURCE_TYPE_MEDIA);
272    EXPECT_EQ(net::URLRequestStatus::SUCCESS, request->status().status());
273    CheckForContentLengthHeader(request.get());
274  }
275}
276
277// Tests that a URL request for resource from an extension returns a few
278// expected response headers.
279TEST_F(ExtensionProtocolTest, ResourceRequestResponseHeaders) {
280  // Register a non-incognito extension protocol handler.
281  SetProtocolHandler(false);
282
283  scoped_refptr<Extension> extension = CreateTestResponseHeaderExtension();
284  extension_info_map_->AddExtension(extension.get(),
285                                    base::Time::Now(),
286                                    false,
287                                    false);
288
289  {
290    scoped_ptr<net::URLRequest> request(
291        resource_context_.GetRequestContext()->CreateRequest(
292            extension->GetResourceURL("test.dat"),
293            net::DEFAULT_PRIORITY,
294            &test_delegate_,
295            NULL));
296    StartRequest(request.get(), content::RESOURCE_TYPE_MEDIA);
297    EXPECT_EQ(net::URLRequestStatus::SUCCESS, request->status().status());
298
299    // Check that cache-related headers are set.
300    std::string etag;
301    request->GetResponseHeaderByName("ETag", &etag);
302    EXPECT_TRUE(StartsWithASCII(etag, "\"", false));
303    EXPECT_TRUE(EndsWith(etag, "\"", false));
304
305    std::string revalidation_header;
306    request->GetResponseHeaderByName("cache-control", &revalidation_header);
307    EXPECT_EQ("no-cache", revalidation_header);
308
309    // We set test.dat as web-accessible, so it should have a CORS header.
310    std::string access_control;
311    request->GetResponseHeaderByName("Access-Control-Allow-Origin",
312                                    &access_control);
313    EXPECT_EQ("*", access_control);
314  }
315}
316
317// Tests that a URL request for main frame or subframe from an extension
318// succeeds, but subresources fail. See http://crbug.com/312269.
319TEST_F(ExtensionProtocolTest, AllowFrameRequests) {
320  // Register a non-incognito extension protocol handler.
321  SetProtocolHandler(false);
322
323  scoped_refptr<Extension> extension = CreateTestExtension("foo", false);
324  extension_info_map_->AddExtension(extension.get(),
325                                    base::Time::Now(),
326                                    false,
327                                    false);
328
329  // All MAIN_FRAME and SUB_FRAME requests should succeed.
330  {
331    scoped_ptr<net::URLRequest> request(
332        resource_context_.GetRequestContext()->CreateRequest(
333            extension->GetResourceURL("test.dat"),
334            net::DEFAULT_PRIORITY,
335            &test_delegate_,
336            NULL));
337    StartRequest(request.get(), content::RESOURCE_TYPE_MAIN_FRAME);
338    EXPECT_EQ(net::URLRequestStatus::SUCCESS, request->status().status());
339  }
340  {
341    scoped_ptr<net::URLRequest> request(
342        resource_context_.GetRequestContext()->CreateRequest(
343            extension->GetResourceURL("test.dat"),
344            net::DEFAULT_PRIORITY,
345            &test_delegate_,
346            NULL));
347    StartRequest(request.get(), content::RESOURCE_TYPE_SUB_FRAME);
348    EXPECT_EQ(net::URLRequestStatus::SUCCESS, request->status().status());
349  }
350
351  // And subresource types, such as media, should fail.
352  {
353    scoped_ptr<net::URLRequest> request(
354        resource_context_.GetRequestContext()->CreateRequest(
355            extension->GetResourceURL("test.dat"),
356            net::DEFAULT_PRIORITY,
357            &test_delegate_,
358            NULL));
359    StartRequest(request.get(), content::RESOURCE_TYPE_MEDIA);
360    EXPECT_EQ(net::URLRequestStatus::FAILED, request->status().status());
361  }
362}
363
364}  // namespace extensions
365