diff options
author | jamescook@chromium.org <jamescook@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-11 08:29:01 +0000 |
---|---|---|
committer | jamescook@chromium.org <jamescook@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-11 08:29:01 +0000 |
commit | 1791e6c97d8c12bf93c2812f9e4e658dba99a93d (patch) | |
tree | 784fb047aff898d4202f0f6574dba165b910aed1 /extensions | |
parent | 9cebf3e5a7d645ab0e4388d6d24e7c809abf7c2d (diff) | |
download | chromium_src-1791e6c97d8c12bf93c2812f9e4e658dba99a93d.zip chromium_src-1791e6c97d8c12bf93c2812f9e4e658dba99a93d.tar.gz chromium_src-1791e6c97d8c12bf93c2812f9e4e658dba99a93d.tar.bz2 |
Toro: Move ExtensionProtocolHandler to //extensions
This breaks Chrome dependencies in ExtensionProtocolHandler and moves //chrome/browser/extensions/extension_protocol.cc into //extensions/browser so it can be used in app_shell.
* Extract URLRequestResourceBundleJob to Chrome as it is used only to load component extension resources from Chrome's PAK files.
* Replace Profile::ProfileType usage with is_incognito to break the Profile dependency.
* Delegate out the decision to allow cross-renderer extension resource loads because this is primarily used for Chrome-specific extension features
* Eliminate chrome-extension-resource:// handling in app_shell (it allows sharing common extension data in the <chrome-install>/resources/extension/ directory, which app_shell doesn't need).
BUG=361373
TEST=browser_tests *Extension* and PlatformApp*, unit_tests ExtensionProtocol*, app_shell loads calculator
TBR=sky@chromium.org for mechanical header file move/refactor across chrome/browser
Review URL: https://codereview.chromium.org/229733002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@263198 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/browser/extension_protocols.cc | 488 | ||||
-rw-r--r-- | extensions/browser/extension_protocols.h | 40 | ||||
-rw-r--r-- | extensions/browser/extension_protocols_unittest.cc | 340 | ||||
-rw-r--r-- | extensions/browser/extensions_browser_client.h | 28 | ||||
-rw-r--r-- | extensions/browser/test_extensions_browser_client.cc | 18 | ||||
-rw-r--r-- | extensions/browser/test_extensions_browser_client.h | 11 | ||||
-rw-r--r-- | extensions/extensions.gyp | 2 |
7 files changed, 927 insertions, 0 deletions
diff --git a/extensions/browser/extension_protocols.cc b/extensions/browser/extension_protocols.cc new file mode 100644 index 0000000..681a85f --- /dev/null +++ b/extensions/browser/extension_protocols.cc @@ -0,0 +1,488 @@ +// Copyright 2014 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 "extensions/browser/extension_protocols.h" + +#include <algorithm> + +#include "base/base64.h" +#include "base/compiler_specific.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/metrics/histogram.h" +#include "base/path_service.h" +#include "base/sha1.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/sequenced_worker_pool.h" +#include "base/threading/thread_restrictions.h" +#include "base/timer/elapsed_timer.h" +#include "build/build_config.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/resource_request_info.h" +#include "extensions/browser/extensions_browser_client.h" +#include "extensions/browser/info_map.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_resource.h" +#include "extensions/common/file_util.h" +#include "extensions/common/manifest_handlers/background_info.h" +#include "extensions/common/manifest_handlers/csp_info.h" +#include "extensions/common/manifest_handlers/icons_handler.h" +#include "extensions/common/manifest_handlers/incognito_info.h" +#include "extensions/common/manifest_handlers/shared_module_info.h" +#include "extensions/common/manifest_handlers/web_accessible_resources_info.h" +#include "net/base/net_errors.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_response_info.h" +#include "net/url_request/url_request_error_job.h" +#include "net/url_request/url_request_file_job.h" +#include "net/url_request/url_request_simple_job.h" +#include "url/url_util.h" + +using content::BrowserThread; +using content::ResourceRequestInfo; +using extensions::Extension; +using extensions::SharedModuleInfo; + +namespace extensions { +namespace { + +class GeneratedBackgroundPageJob : public net::URLRequestSimpleJob { + public: + GeneratedBackgroundPageJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const scoped_refptr<const Extension> extension, + const std::string& content_security_policy) + : net::URLRequestSimpleJob(request, network_delegate), + extension_(extension) { + const bool send_cors_headers = false; + // Leave cache headers out of generated background page jobs. + response_info_.headers = BuildHttpHeaders(content_security_policy, + send_cors_headers, + base::Time()); + } + + // Overridden from URLRequestSimpleJob: + virtual int GetData(std::string* mime_type, + std::string* charset, + std::string* data, + const net::CompletionCallback& callback) const OVERRIDE { + *mime_type = "text/html"; + *charset = "utf-8"; + + *data = "<!DOCTYPE html>\n<body>\n"; + const std::vector<std::string>& background_scripts = + extensions::BackgroundInfo::GetBackgroundScripts(extension_.get()); + for (size_t i = 0; i < background_scripts.size(); ++i) { + *data += "<script src=\""; + *data += background_scripts[i]; + *data += "\"></script>\n"; + } + + return net::OK; + } + + virtual void GetResponseInfo(net::HttpResponseInfo* info) OVERRIDE { + *info = response_info_; + } + + private: + virtual ~GeneratedBackgroundPageJob() {} + + scoped_refptr<const Extension> extension_; + net::HttpResponseInfo response_info_; +}; + +base::Time GetFileLastModifiedTime(const base::FilePath& filename) { + if (base::PathExists(filename)) { + base::File::Info info; + if (base::GetFileInfo(filename, &info)) + return info.last_modified; + } + return base::Time(); +} + +base::Time GetFileCreationTime(const base::FilePath& filename) { + if (base::PathExists(filename)) { + base::File::Info info; + if (base::GetFileInfo(filename, &info)) + return info.creation_time; + } + return base::Time(); +} + +void ReadResourceFilePathAndLastModifiedTime( + const extensions::ExtensionResource& resource, + const base::FilePath& directory, + base::FilePath* file_path, + base::Time* last_modified_time) { + *file_path = resource.GetFilePath(); + *last_modified_time = GetFileLastModifiedTime(*file_path); + // While we're here, log the delta between extension directory + // creation time and the resource's last modification time. + base::ElapsedTimer query_timer; + base::Time dir_creation_time = GetFileCreationTime(directory); + UMA_HISTOGRAM_TIMES("Extensions.ResourceDirectoryTimestampQueryLatency", + query_timer.Elapsed()); + int64 delta_seconds = (*last_modified_time - dir_creation_time).InSeconds(); + if (delta_seconds >= 0) { + UMA_HISTOGRAM_CUSTOM_COUNTS("Extensions.ResourceLastModifiedDelta", + delta_seconds, + 0, + base::TimeDelta::FromDays(30).InSeconds(), + 50); + } else { + UMA_HISTOGRAM_CUSTOM_COUNTS("Extensions.ResourceLastModifiedNegativeDelta", + -delta_seconds, + 1, + base::TimeDelta::FromDays(30).InSeconds(), + 50); + } +} + +class URLRequestExtensionJob : public net::URLRequestFileJob { + public: + URLRequestExtensionJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const std::string& extension_id, + const base::FilePath& directory_path, + const base::FilePath& relative_path, + const std::string& content_security_policy, + bool send_cors_header, + bool follow_symlinks_anywhere) + : net::URLRequestFileJob( + request, network_delegate, base::FilePath(), + BrowserThread::GetBlockingPool()->GetTaskRunnerWithShutdownBehavior( + base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)), + directory_path_(directory_path), + // TODO(tc): Move all of these files into resources.pak so we don't break + // when updating on Linux. + resource_(extension_id, directory_path, relative_path), + content_security_policy_(content_security_policy), + send_cors_header_(send_cors_header), + weak_factory_(this) { + if (follow_symlinks_anywhere) { + resource_.set_follow_symlinks_anywhere(); + } + } + + virtual void GetResponseInfo(net::HttpResponseInfo* info) OVERRIDE { + *info = response_info_; + } + + virtual void Start() OVERRIDE { + base::FilePath* read_file_path = new base::FilePath; + base::Time* last_modified_time = new base::Time(); + bool posted = BrowserThread::PostBlockingPoolTaskAndReply( + FROM_HERE, + base::Bind(&ReadResourceFilePathAndLastModifiedTime, resource_, + directory_path_, + base::Unretained(read_file_path), + base::Unretained(last_modified_time)), + base::Bind(&URLRequestExtensionJob::OnFilePathAndLastModifiedTimeRead, + weak_factory_.GetWeakPtr(), + base::Owned(read_file_path), + base::Owned(last_modified_time))); + DCHECK(posted); + } + + private: + virtual ~URLRequestExtensionJob() {} + + void OnFilePathAndLastModifiedTimeRead(base::FilePath* read_file_path, + base::Time* last_modified_time) { + file_path_ = *read_file_path; + response_info_.headers = BuildHttpHeaders( + content_security_policy_, + send_cors_header_, + *last_modified_time); + URLRequestFileJob::Start(); + } + + net::HttpResponseInfo response_info_; + base::FilePath directory_path_; + extensions::ExtensionResource resource_; + std::string content_security_policy_; + bool send_cors_header_; + base::WeakPtrFactory<URLRequestExtensionJob> weak_factory_; +}; + +bool ExtensionCanLoadInIncognito(const ResourceRequestInfo* info, + const std::string& extension_id, + extensions::InfoMap* extension_info_map) { + if (!extension_info_map->IsIncognitoEnabled(extension_id)) + return false; + + // Only allow incognito toplevel navigations to extension resources in + // split mode. In spanning mode, the extension must run in a single process, + // and an incognito tab prevents that. + if (info->GetResourceType() == ResourceType::MAIN_FRAME) { + const Extension* extension = + extension_info_map->extensions().GetByID(extension_id); + return extension && extensions::IncognitoInfo::IsSplitMode(extension); + } + + return true; +} + +// Returns true if an chrome-extension:// resource should be allowed to load. +// Pass true for |is_incognito| only for incognito profiles and not Chrome OS +// guest mode profiles. +// TODO(aa): This should be moved into ExtensionResourceRequestPolicy, but we +// first need to find a way to get CanLoadInIncognito state into the renderers. +bool AllowExtensionResourceLoad(net::URLRequest* request, + bool is_incognito, + const Extension* extension, + extensions::InfoMap* extension_info_map) { + const ResourceRequestInfo* info = ResourceRequestInfo::ForRequest(request); + + // We have seen crashes where info is NULL: crbug.com/52374. + if (!info) { + LOG(ERROR) << "Allowing load of " << request->url().spec() + << "from unknown origin. Could not find user data for " + << "request."; + return true; + } + + if (is_incognito && !ExtensionCanLoadInIncognito( + info, request->url().host(), extension_info_map)) { + return false; + } + + // The following checks are meant to replicate similar set of checks in the + // renderer process, performed by ResourceRequestPolicy::CanRequestResource. + // These are not exactly equivalent, because we don't have the same bits of + // information. The two checks need to be kept in sync as much as possible, as + // an exploited renderer can bypass the checks in ResourceRequestPolicy. + + // Check if the extension for which this request is made is indeed loaded in + // the process sending the request. If not, we need to explicitly check if + // the resource is explicitly accessible or fits in a set of exception cases. + // Note: This allows a case where two extensions execute in the same renderer + // process to request each other's resources. We can't do a more precise + // check, since the renderer can lie about which extension has made the + // request. + if (extension_info_map->process_map().Contains( + request->url().host(), info->GetChildID())) { + return true; + } + + // Allow the extension module embedder to grant permission for loads. + if (ExtensionsBrowserClient::Get()->AllowCrossRendererResourceLoad( + request, is_incognito, extension, extension_info_map)) { + return true; + } + + // No special exceptions for cross-process loading. Block the load. + return false; +} + +// Returns true if the given URL references an icon in the given extension. +bool URLIsForExtensionIcon(const GURL& url, const Extension* extension) { + DCHECK(url.SchemeIs(extensions::kExtensionScheme)); + + if (!extension) + return false; + + std::string path = url.path(); + DCHECK_EQ(url.host(), extension->id()); + DCHECK(path.length() > 0 && path[0] == '/'); + path = path.substr(1); + return extensions::IconsInfo::GetIcons(extension).ContainsPath(path); +} + +class ExtensionProtocolHandler + : public net::URLRequestJobFactory::ProtocolHandler { + public: + ExtensionProtocolHandler(bool is_incognito, + extensions::InfoMap* extension_info_map) + : is_incognito_(is_incognito), extension_info_map_(extension_info_map) {} + + virtual ~ExtensionProtocolHandler() {} + + virtual net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const OVERRIDE; + + private: + const bool is_incognito_; + extensions::InfoMap* const extension_info_map_; + DISALLOW_COPY_AND_ASSIGN(ExtensionProtocolHandler); +}; + +// Creates URLRequestJobs for extension:// URLs. +net::URLRequestJob* +ExtensionProtocolHandler::MaybeCreateJob( + net::URLRequest* request, net::NetworkDelegate* network_delegate) const { + // chrome-extension://extension-id/resource/path.js + std::string extension_id = request->url().host(); + const Extension* extension = + extension_info_map_->extensions().GetByID(extension_id); + + // TODO(mpcomplete): better error code. + if (!AllowExtensionResourceLoad( + request, is_incognito_, extension, extension_info_map_)) { + return new net::URLRequestErrorJob( + request, network_delegate, net::ERR_ADDRESS_UNREACHABLE); + } + + // If this is a disabled extension only allow the icon to load. + base::FilePath directory_path; + if (extension) + directory_path = extension->path(); + if (directory_path.value().empty()) { + const Extension* disabled_extension = + extension_info_map_->disabled_extensions().GetByID(extension_id); + if (URLIsForExtensionIcon(request->url(), disabled_extension)) + directory_path = disabled_extension->path(); + if (directory_path.value().empty()) { + LOG(WARNING) << "Failed to GetPathForExtension: " << extension_id; + return NULL; + } + } + + // Set up content security policy. + std::string content_security_policy; + bool send_cors_header = false; + bool follow_symlinks_anywhere = false; + if (extension) { + std::string resource_path = request->url().path(); + content_security_policy = + extensions::CSPInfo::GetResourceContentSecurityPolicy(extension, + resource_path); + if ((extension->manifest_version() >= 2 || + extensions::WebAccessibleResourcesInfo::HasWebAccessibleResources( + extension)) && + extensions::WebAccessibleResourcesInfo::IsResourceWebAccessible( + extension, resource_path)) + send_cors_header = true; + + follow_symlinks_anywhere = + (extension->creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE) + != 0; + } + + // Create a job for a generated background page. + std::string path = request->url().path(); + if (path.size() > 1 && + path.substr(1) == extensions::kGeneratedBackgroundPageFilename) { + return new GeneratedBackgroundPageJob( + request, network_delegate, extension, content_security_policy); + } + + // Component extension resources may be part of the embedder's resource files, + // for example component_extension_resources.pak in Chrome. + net::URLRequestJob* resource_bundle_job = + extensions::ExtensionsBrowserClient::Get() + ->MaybeCreateResourceBundleRequestJob(request, + network_delegate, + directory_path, + content_security_policy, + send_cors_header); + if (resource_bundle_job) + return resource_bundle_job; + + base::FilePath relative_path = + extensions::file_util::ExtensionURLToRelativeFilePath(request->url()); + + // Handle shared resources (extension A loading resources out of extension B). + if (SharedModuleInfo::IsImportedPath(path)) { + std::string new_extension_id; + std::string new_relative_path; + SharedModuleInfo::ParseImportedPath(path, &new_extension_id, + &new_relative_path); + const Extension* new_extension = + extension_info_map_->extensions().GetByID(new_extension_id); + + bool first_party_in_import = false; + // NB: This first_party_for_cookies call is not for security, it is only + // used so an exported extension can limit the visible surface to the + // extension that imports it, more or less constituting its API. + const std::string& first_party_path = + request->first_party_for_cookies().path(); + if (SharedModuleInfo::IsImportedPath(first_party_path)) { + std::string first_party_id; + std::string dummy; + SharedModuleInfo::ParseImportedPath(first_party_path, &first_party_id, + &dummy); + if (first_party_id == new_extension_id) { + first_party_in_import = true; + } + } + + if (SharedModuleInfo::ImportsExtensionById(extension, new_extension_id) && + new_extension && + (first_party_in_import || + SharedModuleInfo::IsExportAllowed(new_extension, new_relative_path))) { + directory_path = new_extension->path(); + extension_id = new_extension_id; + relative_path = base::FilePath::FromUTF8Unsafe(new_relative_path); + } else { + return NULL; + } + } + + return new URLRequestExtensionJob(request, + network_delegate, + extension_id, + directory_path, + relative_path, + content_security_policy, + send_cors_header, + follow_symlinks_anywhere); +} + +} // namespace + +net::HttpResponseHeaders* BuildHttpHeaders( + const std::string& content_security_policy, + bool send_cors_header, + const base::Time& last_modified_time) { + std::string raw_headers; + raw_headers.append("HTTP/1.1 200 OK"); + if (!content_security_policy.empty()) { + raw_headers.append(1, '\0'); + raw_headers.append("Content-Security-Policy: "); + raw_headers.append(content_security_policy); + } + + if (send_cors_header) { + raw_headers.append(1, '\0'); + raw_headers.append("Access-Control-Allow-Origin: *"); + } + + if (!last_modified_time.is_null()) { + // Hash the time and make an etag to avoid exposing the exact + // user installation time of the extension. + std::string hash = + base::StringPrintf("%" PRId64, last_modified_time.ToInternalValue()); + hash = base::SHA1HashString(hash); + std::string etag; + base::Base64Encode(hash, &etag); + raw_headers.append(1, '\0'); + raw_headers.append("ETag: \""); + raw_headers.append(etag); + raw_headers.append("\""); + // Also force revalidation. + raw_headers.append(1, '\0'); + raw_headers.append("cache-control: no-cache"); + } + + raw_headers.append(2, '\0'); + return new net::HttpResponseHeaders(raw_headers); +} + +net::URLRequestJobFactory::ProtocolHandler* CreateExtensionProtocolHandler( + bool is_incognito, + extensions::InfoMap* extension_info_map) { + return new ExtensionProtocolHandler(is_incognito, extension_info_map); +} + +} // namespace extensions diff --git a/extensions/browser/extension_protocols.h b/extensions/browser/extension_protocols.h new file mode 100644 index 0000000..2260374 --- /dev/null +++ b/extensions/browser/extension_protocols.h @@ -0,0 +1,40 @@ +// Copyright 2014 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. + +#ifndef EXTENSIONS_BROWSER_EXTENSION_PROTOCOLS_H_ +#define EXTENSIONS_BROWSER_EXTENSION_PROTOCOLS_H_ + +#include <string> + +#include "net/url_request/url_request_job_factory.h" + +namespace base { +class Time; +} + +namespace net { +class HttpResponseHeaders; +} + +namespace extensions { + +class InfoMap; + +// Builds HTTP headers for an extension request. Hashes the time to avoid +// exposing the exact user installation time of the extension. +net::HttpResponseHeaders* BuildHttpHeaders( + const std::string& content_security_policy, + bool send_cors_header, + const base::Time& last_modified_time); + +// Creates the handlers for the chrome-extension:// scheme. Pass true for +// |is_incognito| only for incognito profiles and not for Chrome OS guest mode +// profiles. +net::URLRequestJobFactory::ProtocolHandler* CreateExtensionProtocolHandler( + bool is_incognito, + extensions::InfoMap* extension_info_map); + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_EXTENSION_PROTOCOLS_H_ diff --git a/extensions/browser/extension_protocols_unittest.cc b/extensions/browser/extension_protocols_unittest.cc new file mode 100644 index 0000000..0aa097b --- /dev/null +++ b/extensions/browser/extension_protocols_unittest.cc @@ -0,0 +1,340 @@ +// Copyright 2014 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 <string> + +#include "base/file_util.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "chrome/common/chrome_paths.h" +//#include "chrome/common/url_constants.h" +#include "content/public/browser/resource_request_info.h" +#include "content/public/test/mock_resource_context.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "extensions/browser/extension_protocols.h" +#include "extensions/browser/info_map.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "net/base/request_priority.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_job_factory_impl.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace extensions { + +scoped_refptr<Extension> CreateTestExtension(const std::string& name, + bool incognito_split_mode) { + base::DictionaryValue manifest; + manifest.SetString("name", name); + manifest.SetString("version", "1"); + manifest.SetInteger("manifest_version", 2); + manifest.SetString("incognito", incognito_split_mode ? "split" : "spanning"); + + base::FilePath path; + EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &path)); + path = path.AppendASCII("extensions").AppendASCII("response_headers"); + + std::string error; + scoped_refptr<Extension> extension( + Extension::Create(path, Manifest::INTERNAL, manifest, + Extension::NO_FLAGS, &error)); + EXPECT_TRUE(extension.get()) << error; + return extension; +} + +scoped_refptr<Extension> CreateWebStoreExtension() { + base::DictionaryValue manifest; + manifest.SetString("name", "WebStore"); + manifest.SetString("version", "1"); + manifest.SetString("icons.16", "webstore_icon_16.png"); + + base::FilePath path; + EXPECT_TRUE(PathService::Get(chrome::DIR_RESOURCES, &path)); + path = path.AppendASCII("web_store"); + + std::string error; + scoped_refptr<Extension> extension( + Extension::Create(path, Manifest::COMPONENT, manifest, + Extension::NO_FLAGS, &error)); + EXPECT_TRUE(extension.get()) << error; + return extension; +} + +scoped_refptr<Extension> CreateTestResponseHeaderExtension() { + base::DictionaryValue manifest; + manifest.SetString("name", "An extension with web-accessible resources"); + manifest.SetString("version", "2"); + + base::ListValue* web_accessible_list = new base::ListValue(); + web_accessible_list->AppendString("test.dat"); + manifest.Set("web_accessible_resources", web_accessible_list); + + base::FilePath path; + EXPECT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &path)); + path = path.AppendASCII("extensions").AppendASCII("response_headers"); + + std::string error; + scoped_refptr<Extension> extension( + Extension::Create(path, Manifest::UNPACKED, manifest, + Extension::NO_FLAGS, &error)); + EXPECT_TRUE(extension.get()) << error; + return extension; +} + +class ExtensionProtocolTest : public testing::Test { + public: + ExtensionProtocolTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP), + old_factory_(NULL), + resource_context_(&test_url_request_context_) {} + + virtual void SetUp() OVERRIDE { + testing::Test::SetUp(); + extension_info_map_ = new InfoMap(); + net::URLRequestContext* request_context = + resource_context_.GetRequestContext(); + old_factory_ = request_context->job_factory(); + } + + virtual void TearDown() { + net::URLRequestContext* request_context = + resource_context_.GetRequestContext(); + request_context->set_job_factory(old_factory_); + } + + void SetProtocolHandler(bool is_incognito) { + net::URLRequestContext* request_context = + resource_context_.GetRequestContext(); + job_factory_.SetProtocolHandler( + kExtensionScheme, + CreateExtensionProtocolHandler(is_incognito, + extension_info_map_.get())); + request_context->set_job_factory(&job_factory_); + } + + void StartRequest(net::URLRequest* request, + ResourceType::Type resource_type) { + content::ResourceRequestInfo::AllocateForTesting(request, + resource_type, + &resource_context_, + -1, + -1, + -1, + false); + request->Start(); + base::MessageLoop::current()->Run(); + } + + protected: + content::TestBrowserThreadBundle thread_bundle_; + scoped_refptr<InfoMap> extension_info_map_; + net::URLRequestJobFactoryImpl job_factory_; + const net::URLRequestJobFactory* old_factory_; + net::TestDelegate test_delegate_; + net::TestURLRequestContext test_url_request_context_; + content::MockResourceContext resource_context_; +}; + +// Tests that making a chrome-extension request in an incognito context is +// only allowed under the right circumstances (if the extension is allowed +// in incognito, and it's either a non-main-frame request or a split-mode +// extension). +TEST_F(ExtensionProtocolTest, IncognitoRequest) { + // Register an incognito extension protocol handler. + SetProtocolHandler(true); + + struct TestCase { + // Inputs. + std::string name; + bool incognito_split_mode; + bool incognito_enabled; + + // Expected results. + bool should_allow_main_frame_load; + bool should_allow_sub_frame_load; + } cases[] = { + {"spanning disabled", false, false, false, false}, + {"split disabled", true, false, false, false}, + {"spanning enabled", false, true, false, true}, + {"split enabled", true, true, true, true}, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + scoped_refptr<Extension> extension = + CreateTestExtension(cases[i].name, cases[i].incognito_split_mode); + extension_info_map_->AddExtension( + extension.get(), base::Time::Now(), cases[i].incognito_enabled, false); + + // First test a main frame request. + { + // It doesn't matter that the resource doesn't exist. If the resource + // is blocked, we should see ADDRESS_UNREACHABLE. Otherwise, the request + // should just fail because the file doesn't exist. + net::URLRequest request(extension->GetResourceURL("404.html"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MAIN_FRAME); + EXPECT_EQ(net::URLRequestStatus::FAILED, request.status().status()); + + if (cases[i].should_allow_main_frame_load) { + EXPECT_EQ(net::ERR_FILE_NOT_FOUND, request.status().error()) << + cases[i].name; + } else { + EXPECT_EQ(net::ERR_ADDRESS_UNREACHABLE, request.status().error()) << + cases[i].name; + } + } + + // Now do a subframe request. + { + net::URLRequest request(extension->GetResourceURL("404.html"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::SUB_FRAME); + EXPECT_EQ(net::URLRequestStatus::FAILED, request.status().status()); + + if (cases[i].should_allow_sub_frame_load) { + EXPECT_EQ(net::ERR_FILE_NOT_FOUND, request.status().error()) << + cases[i].name; + } else { + EXPECT_EQ(net::ERR_ADDRESS_UNREACHABLE, request.status().error()) << + cases[i].name; + } + } + } +} + +void CheckForContentLengthHeader(net::URLRequest* request) { + std::string content_length; + request->GetResponseHeaderByName(net::HttpRequestHeaders::kContentLength, + &content_length); + EXPECT_FALSE(content_length.empty()); + int length_value = 0; + EXPECT_TRUE(base::StringToInt(content_length, &length_value)); + EXPECT_GT(length_value, 0); +} + +// Tests getting a resource for a component extension works correctly, both when +// the extension is enabled and when it is disabled. +TEST_F(ExtensionProtocolTest, ComponentResourceRequest) { + // Register a non-incognito extension protocol handler. + SetProtocolHandler(false); + + scoped_refptr<Extension> extension = CreateWebStoreExtension(); + extension_info_map_->AddExtension(extension.get(), + base::Time::Now(), + false, + false); + + // First test it with the extension enabled. + { + net::URLRequest request(extension->GetResourceURL("webstore_icon_16.png"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MEDIA); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, request.status().status()); + CheckForContentLengthHeader(&request); + } + + // And then test it with the extension disabled. + extension_info_map_->RemoveExtension(extension->id(), + UnloadedExtensionInfo::REASON_DISABLE); + { + net::URLRequest request(extension->GetResourceURL("webstore_icon_16.png"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MEDIA); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, request.status().status()); + CheckForContentLengthHeader(&request); + } +} + +// Tests that a URL request for resource from an extension returns a few +// expected response headers. +TEST_F(ExtensionProtocolTest, ResourceRequestResponseHeaders) { + // Register a non-incognito extension protocol handler. + SetProtocolHandler(false); + + scoped_refptr<Extension> extension = CreateTestResponseHeaderExtension(); + extension_info_map_->AddExtension(extension.get(), + base::Time::Now(), + false, + false); + + { + net::URLRequest request(extension->GetResourceURL("test.dat"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MEDIA); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, request.status().status()); + + // Check that cache-related headers are set. + std::string etag; + request.GetResponseHeaderByName("ETag", &etag); + EXPECT_TRUE(StartsWithASCII(etag, "\"", false)); + EXPECT_TRUE(EndsWith(etag, "\"", false)); + + std::string revalidation_header; + request.GetResponseHeaderByName("cache-control", &revalidation_header); + EXPECT_EQ("no-cache", revalidation_header); + + // We set test.dat as web-accessible, so it should have a CORS header. + std::string access_control; + request.GetResponseHeaderByName("Access-Control-Allow-Origin", + &access_control); + EXPECT_EQ("*", access_control); + } +} + +// Tests that a URL request for main frame or subframe from an extension +// succeeds, but subresources fail. See http://crbug.com/312269. +TEST_F(ExtensionProtocolTest, AllowFrameRequests) { + // Register a non-incognito extension protocol handler. + SetProtocolHandler(false); + + scoped_refptr<Extension> extension = CreateTestExtension("foo", false); + extension_info_map_->AddExtension(extension.get(), + base::Time::Now(), + false, + false); + + // All MAIN_FRAME and SUB_FRAME requests should succeed. + { + net::URLRequest request(extension->GetResourceURL("test.dat"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MAIN_FRAME); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, request.status().status()); + } + { + net::URLRequest request(extension->GetResourceURL("test.dat"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::SUB_FRAME); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, request.status().status()); + } + + // And subresource types, such as media, should fail. + { + net::URLRequest request(extension->GetResourceURL("test.dat"), + net::DEFAULT_PRIORITY, + &test_delegate_, + resource_context_.GetRequestContext()); + StartRequest(&request, ResourceType::MEDIA); + EXPECT_EQ(net::URLRequestStatus::FAILED, request.status().status()); + } +} + +} // namespace extensions diff --git a/extensions/browser/extensions_browser_client.h b/extensions/browser/extensions_browser_client.h index c7542a7..d7c02d9 100644 --- a/extensions/browser/extensions_browser_client.h +++ b/extensions/browser/extensions_browser_client.h @@ -16,6 +16,7 @@ class PrefService; namespace base { class CommandLine; +class FilePath; } namespace content { @@ -23,6 +24,12 @@ class BrowserContext; class WebContents; } +namespace net { +class NetworkDelegate; +class URLRequest; +class URLRequestJob; +} + namespace extensions { class ApiActivityMonitor; @@ -32,6 +39,7 @@ class ExtensionHostDelegate; class ExtensionPrefsObserver; class ExtensionSystem; class ExtensionSystemProvider; +class InfoMap; // Interface to allow the extensions module to make browser-process-specific // queries of the embedder. Should be Set() once in the browser process. @@ -88,6 +96,26 @@ class ExtensionsBrowserClient { const extensions::Extension* extension, content::BrowserContext* context) const = 0; + // Returns an URLRequestJob to load an extension resource from the embedder's + // resource bundle (.pak) files. Returns NULL if the request is not for a + // resource bundle resource or if the embedder does not support this feature. + // Used for component extensions. Called on the IO thread. + virtual net::URLRequestJob* MaybeCreateResourceBundleRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const base::FilePath& directory_path, + const std::string& content_security_policy, + bool send_cors_header) = 0; + + // Returns true if the embedder wants to allow a chrome-extension:// resource + // request coming from renderer A to access a resource in an extension running + // in renderer B. For example, Chrome overrides this to provide support for + // webview and dev tools. Called on the IO thread. + virtual bool AllowCrossRendererResourceLoad(net::URLRequest* request, + bool is_incognito, + const Extension* extension, + InfoMap* extension_info_map) = 0; + // Returns the PrefService associated with |context|. virtual PrefService* GetPrefServiceForContext( content::BrowserContext* context) = 0; diff --git a/extensions/browser/test_extensions_browser_client.cc b/extensions/browser/test_extensions_browser_client.cc index f1d3cc9..132ac19 100644 --- a/extensions/browser/test_extensions_browser_client.cc +++ b/extensions/browser/test_extensions_browser_client.cc @@ -83,6 +83,24 @@ bool TestExtensionsBrowserClient::CanExtensionCrossIncognito( return false; } +net::URLRequestJob* +TestExtensionsBrowserClient::MaybeCreateResourceBundleRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const base::FilePath& directory_path, + const std::string& content_security_policy, + bool send_cors_header) { + return NULL; +} + +bool TestExtensionsBrowserClient::AllowCrossRendererResourceLoad( + net::URLRequest* request, + bool is_incognito, + const Extension* extension, + InfoMap* extension_info_map) { + return false; +} + PrefService* TestExtensionsBrowserClient::GetPrefServiceForContext( BrowserContext* context) { return NULL; diff --git a/extensions/browser/test_extensions_browser_client.h b/extensions/browser/test_extensions_browser_client.h index 4db3270..ee3a625 100644 --- a/extensions/browser/test_extensions_browser_client.h +++ b/extensions/browser/test_extensions_browser_client.h @@ -42,6 +42,17 @@ class TestExtensionsBrowserClient : public ExtensionsBrowserClient { virtual bool CanExtensionCrossIncognito( const extensions::Extension* extension, content::BrowserContext* context) const OVERRIDE; + virtual net::URLRequestJob* MaybeCreateResourceBundleRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const base::FilePath& directory_path, + const std::string& content_security_policy, + bool send_cors_header) OVERRIDE; + virtual bool AllowCrossRendererResourceLoad(net::URLRequest* request, + bool is_incognito, + const Extension* extension, + InfoMap* extension_info_map) + OVERRIDE; virtual PrefService* GetPrefServiceForContext( content::BrowserContext* context) OVERRIDE; virtual void GetEarlyExtensionPrefsObservers( diff --git a/extensions/extensions.gyp b/extensions/extensions.gyp index 4227cc0..bf4a61d 100644 --- a/extensions/extensions.gyp +++ b/extensions/extensions.gyp @@ -327,6 +327,8 @@ 'browser/extension_prefs_factory.h', 'browser/extension_prefs_observer.h', 'browser/extension_prefs_scope.h', + 'browser/extension_protocols.cc', + 'browser/extension_protocols.h', 'browser/extension_registry.cc', 'browser/extension_registry.h', 'browser/extension_registry_factory.cc', |