Commit 26a85640 authored by Jay Civelli's avatar Jay Civelli Committed by Commit Bot

Moving the last part of extension unpacking to the browser process.

Moving the manifest parsing and extension validation from the
Unpacker class to SandboxedUnpacker. As a result, all the extension
unpacking is done from the browser process and the Unpacker class is
removed.

The 2 remaining methods in unpacker.cc still used for unzipping the
extension have been moved to utility_handler.cc and the associated
tests renamed from unpacker_unittest.cc to utility_handler_unittest.cc.
The unzipping will also eventually be moved to the browser process at
which point utility_handler files will be removed as well.

Tbr: rockot@chromium.org
Bug: 800540
Change-Id: Ib286fa88edc96e4c186aba6e2ee1d9c2317efe84
Reviewed-on: https://chromium-review.googlesource.com/883967Reviewed-by: default avatarKen Rockot <rockot@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Reviewed-by: default avatarTom Sepez <tsepez@chromium.org>
Commit-Queue: Jay Civelli <jcivelli@chromium.org>
Cr-Commit-Position: refs/heads/master@{#532064}
parent 5fec41e6
......@@ -611,6 +611,7 @@ source_set("unit_tests") {
"//extensions/common",
"//extensions/common/api",
"//extensions/features",
"//extensions/strings",
"//ipc:test_support",
"//net:test_support",
"//services/data_decoder/public/cpp:test_support",
......
......@@ -426,6 +426,8 @@ void SandboxedUnpacker::UnzipDone(const base::FilePath& directory,
bool success) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
utility_process_mojo_client_.reset();
if (!success) {
utility_process_mojo_client_.reset();
unpacker_io_task_runner_->PostTask(
......@@ -444,33 +446,49 @@ void SandboxedUnpacker::UnzipDone(const base::FilePath& directory,
void SandboxedUnpacker::Unpack(const base::FilePath& directory) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
StartUtilityProcessIfNeeded();
DCHECK(directory.DirName() == temp_dir_.GetPath());
utility_process_mojo_client_->service()->Unpack(
GetCurrentChannel(), GetCurrentFeatureSessionType(), directory,
extension_id_, location_, creation_flags_,
base::BindOnce(&SandboxedUnpacker::UnpackDone, this));
base::FilePath manifest_path = extension_root_.Append(kManifestFilename);
unpacker_io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&SandboxedUnpacker::ParseJsonFile, this, manifest_path,
base::BindOnce(&SandboxedUnpacker::ReadManifestDone, this)));
}
void SandboxedUnpacker::UnpackDone(
const base::string16& error,
std::unique_ptr<base::DictionaryValue> manifest) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
void SandboxedUnpacker::ReadManifestDone(
std::unique_ptr<base::Value> manifest,
const base::Optional<std::string>& error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (error) {
ReportUnpackingError(*error);
return;
}
if (!manifest || !manifest->is_dict()) {
ReportUnpackingError(manifest_errors::kInvalidManifest);
return;
}
utility_process_mojo_client_.reset();
std::unique_ptr<base::DictionaryValue> manifest_dict =
base::DictionaryValue::From(std::move(manifest));
if (!error.empty()) {
unpacker_io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SandboxedUnpacker::UnpackExtensionFailed, this, error));
std::string error_msg;
scoped_refptr<Extension> extension(
Extension::Create(extension_root_, location_, *manifest_dict,
creation_flags_, extension_id_, &error_msg));
if (!extension) {
ReportUnpackingError(error_msg);
return;
}
unpacker_io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SandboxedUnpacker::UnpackExtensionSucceeded,
this, std::move(manifest)));
std::vector<InstallWarning> warnings;
if (!file_util::ValidateExtension(extension.get(), &error_msg, &warnings)) {
ReportUnpackingError(error_msg);
return;
}
extension->AddInstallWarnings(warnings);
UnpackExtensionSucceeded(std::move(manifest_dict));
}
void SandboxedUnpacker::UnpackExtensionSucceeded(
......@@ -673,32 +691,18 @@ void SandboxedUnpacker::ReadJSONRulesetIfNeeded(
declarative_net_request::DNRManifestData::GetRulesetResource(
extension_.get());
if (!resource) {
ReadJSONRulesetDone(std::move(manifest), /*json_parser_ptr=*/nullptr,
ReadJSONRulesetDone(std::move(manifest),
/*json_ruleset=*/nullptr, /*error=*/base::nullopt);
return;
}
std::string json_ruleset_data;
if (!base::ReadFileToString(resource->GetFilePath(), &json_ruleset_data)) {
ReportUnpackingError("JSON ruleset file does not exist.");
return;
}
data_decoder::mojom::JsonParserPtr json_parser_ptr;
connector_->BindInterface(data_decoder_identity_, &json_parser_ptr);
json_parser_ptr.set_connection_error_handler(
base::BindOnce(&SandboxedUnpacker::UtilityProcessCrashed, this));
auto* raw_json_parser_ptr = json_parser_ptr.get();
raw_json_parser_ptr->Parse(
json_ruleset_data,
base::BindOnce(&SandboxedUnpacker::ReadJSONRulesetDone, this,
std::move(manifest), std::move(json_parser_ptr)));
ParseJsonFile(resource->GetFilePath(),
base::BindOnce(&SandboxedUnpacker::ReadJSONRulesetDone, this,
std::move(manifest)));
}
void SandboxedUnpacker::ReadJSONRulesetDone(
std::unique_ptr<base::DictionaryValue> manifest,
data_decoder::mojom::JsonParserPtr json_parser_ptr_keep_alive,
std::unique_ptr<base::Value> json_ruleset,
const base::Optional<std::string>& error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
......@@ -756,6 +760,22 @@ bool SandboxedUnpacker::IndexAndPersistRulesIfNeeded(
return true;
}
data_decoder::mojom::JsonParser* SandboxedUnpacker::GetJsonParserPtr() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!json_parser_ptr_) {
connector_->BindInterface(data_decoder_identity_, &json_parser_ptr_);
json_parser_ptr_.set_connection_error_handler(base::BindOnce(
&SandboxedUnpacker::ReportFailure, this,
UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16("UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL")) +
ASCIIToUTF16(". ") +
l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALL_PROCESS_CRASHED)));
}
return json_parser_ptr_.get();
}
void SandboxedUnpacker::ReportUnpackingError(base::StringPiece error) {
unpacker_io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SandboxedUnpacker::UnpackExtensionFailed, this,
......@@ -943,6 +963,8 @@ void SandboxedUnpacker::ReportFailure(FailureReason reason,
void SandboxedUnpacker::ReportSuccess(
std::unique_ptr<base::DictionaryValue> original_manifest,
const base::Optional<int>& dnr_ruleset_checksum) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
UMA_HISTOGRAM_COUNTS("Extensions.SandboxUnpackSuccess", 1);
if (!crx_unpack_start_time_.is_null())
......@@ -956,6 +978,8 @@ void SandboxedUnpacker::ReportSuccess(
std::move(original_manifest), extension_.get(),
install_icon_, dnr_ruleset_checksum);
extension_ = NULL;
Cleanup();
}
base::DictionaryValue* SandboxedUnpacker::RewriteManifestFile(
......@@ -995,13 +1019,29 @@ base::DictionaryValue* SandboxedUnpacker::RewriteManifestFile(
void SandboxedUnpacker::Cleanup() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!temp_dir_.Delete()) {
if (temp_dir_.IsValid() && !temp_dir_.Delete()) {
LOG(WARNING) << "Can not delete temp directory at "
<< temp_dir_.GetPath().value();
}
connector_.reset();
image_sanitizer_.reset();
json_file_sanitizer_.reset();
json_parser_ptr_.reset();
}
void SandboxedUnpacker::ParseJsonFile(
const base::FilePath& path,
data_decoder::mojom::JsonParser::ParseCallback callback) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
std::string contents;
if (!base::ReadFileToString(path, &contents)) {
std::move(callback).Run(
/*value=*/nullptr,
/*error=*/base::Optional<std::string>("File doesn't exist."));
return;
}
GetJsonParserPtr()->Parse(contents, std::move(callback));
}
} // namespace extensions
......@@ -22,6 +22,7 @@
#include "extensions/browser/install/crx_install_error.h"
#include "extensions/browser/json_file_sanitizer.h"
#include "extensions/common/manifest.h"
#include "services/data_decoder/public/interfaces/json_parser.mojom.h"
#include "services/service_manager/public/cpp/identity.h"
class SkBitmap;
......@@ -240,8 +241,8 @@ class SandboxedUnpacker : public base::RefCountedThreadSafe<SandboxedUnpacker> {
// Unpacks the extension in directory and returns the manifest.
void Unpack(const base::FilePath& directory);
void UnpackDone(const base::string16& error,
std::unique_ptr<base::DictionaryValue> manifest);
void ReadManifestDone(std::unique_ptr<base::Value> manifest,
const base::Optional<std::string>& error);
void UnpackExtensionSucceeded(
std::unique_ptr<base::DictionaryValue> manifest);
void UnpackExtensionFailed(const base::string16& error);
......@@ -264,11 +265,9 @@ class SandboxedUnpacker : public base::RefCountedThreadSafe<SandboxedUnpacker> {
const std::string& error_msg);
void ReadJSONRulesetIfNeeded(std::unique_ptr<base::DictionaryValue> manifest);
void ReadJSONRulesetDone(
std::unique_ptr<base::DictionaryValue> manifest,
data_decoder::mojom::JsonParserPtr json_parser_ptr_keep_alive,
std::unique_ptr<base::Value> json_ruleset,
const base::Optional<std::string>& error);
void ReadJSONRulesetDone(std::unique_ptr<base::DictionaryValue> manifest,
std::unique_ptr<base::Value> json_ruleset,
const base::Optional<std::string>& error);
// Reports unpack success or failure, or unzip failure.
void ReportSuccess(std::unique_ptr<base::DictionaryValue> original_manifest,
......@@ -291,6 +290,15 @@ class SandboxedUnpacker : public base::RefCountedThreadSafe<SandboxedUnpacker> {
std::unique_ptr<base::ListValue> json_ruleset,
base::Optional<int>* dnr_ruleset_checksum);
// Returns a JsonParser that can be used on the |unpacker_io_task_runner|.
data_decoder::mojom::JsonParser* GetJsonParserPtr();
// Parses the JSON file at |path| and invokes |callback| when done. |callback|
// is called with a null parameter if parsing failed.
// This must be called from the |unpacker_io_task_runner_|.
void ParseJsonFile(const base::FilePath& path,
data_decoder::mojom::JsonParser::ParseCallback callback);
// Connector to the ServiceManager required by the Unzip API.
std::unique_ptr<service_manager::Connector> connector_;
......@@ -350,6 +358,9 @@ class SandboxedUnpacker : public base::RefCountedThreadSafe<SandboxedUnpacker> {
// data decoder operation use that process.
service_manager::Identity data_decoder_identity_;
// The JSONParser interface pointer from the data decoder service.
data_decoder::mojom::JsonParserPtr json_parser_ptr_;
// The ImageSanitizer used to clean-up images.
std::unique_ptr<ImageSanitizer> image_sanitizer_;
......
......@@ -26,12 +26,14 @@
#include "extensions/common/extension_paths.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/switches.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "extensions/test/test_extensions_client.h"
#include "services/data_decoder/public/cpp/test_data_decoder_service.h"
#include "services/service_manager/public/cpp/connector.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/zlib/google/zip.h"
#include "ui/base/l10n/l10n_util.h"
namespace extensions {
......@@ -77,8 +79,15 @@ class MockSandboxedUnpackerClient : public SandboxedUnpackerClient {
base::FilePath temp_dir() const { return temp_dir_; }
base::string16 unpack_err() const { return error_; }
void set_deleted_tracker(bool* deleted_tracker) {
deleted_tracker_ = deleted_tracker;
}
private:
~MockSandboxedUnpackerClient() override {}
~MockSandboxedUnpackerClient() override {
if (deleted_tracker_)
*deleted_tracker_ = true;
}
void OnUnpackSuccess(
const base::FilePath& temp_dir,
......@@ -99,6 +108,7 @@ class MockSandboxedUnpackerClient : public SandboxedUnpackerClient {
base::string16 error_;
base::Closure quit_closure_;
base::FilePath temp_dir_;
bool* deleted_tracker_ = nullptr;
};
class SandboxedUnpackerTest : public ExtensionsTest {
......@@ -190,6 +200,27 @@ class SandboxedUnpackerTest : public ExtensionsTest {
base::string16 GetInstallError() const { return client_->unpack_err(); }
void ExpectInstallErrorContains(const std::string& error) {
std::string full_error = base::UTF16ToUTF8(client_->unpack_err());
EXPECT_TRUE(full_error.find(error) != std::string::npos)
<< "Error message " << full_error << " does not contain " << error;
}
// Unpacks the package |package_name| and checks that |sandboxed_unpacker_|
// gets deleted.
void TestSandboxedUnpackerDeleted(const std::string& package_name,
bool expect_success) {
bool client_deleted = false;
client_->set_deleted_tracker(&client_deleted);
SetupUnpacker(package_name, "");
EXPECT_EQ(GetInstallError().empty(), expect_success);
// Remove our reference to |sandboxed_unpacker_|, it should get deleted
// since/ it's the last reference.
sandboxed_unpacker_ = nullptr;
// The SandboxedUnpacker should have been deleted and deleted the client.
EXPECT_TRUE(client_deleted);
}
protected:
data_decoder::TestDataDecoderService test_data_decoder_service_;
base::ScopedTempDir extensions_dir_;
......@@ -199,6 +230,47 @@ class SandboxedUnpackerTest : public ExtensionsTest {
in_process_utility_thread_helper_;
};
TEST_F(SandboxedUnpackerTest, EmptyDefaultLocale) {
SetupUnpacker("empty_default_locale.crx", "");
ExpectInstallErrorContains(manifest_errors::kInvalidDefaultLocale);
}
TEST_F(SandboxedUnpackerTest, HasDefaultLocaleMissingLocalesFolder) {
SetupUnpacker("has_default_missing_locales.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesTreeMissing);
}
TEST_F(SandboxedUnpackerTest, InvalidDefaultLocale) {
SetupUnpacker("invalid_default_locale.crx", "");
ExpectInstallErrorContains(manifest_errors::kInvalidDefaultLocale);
}
TEST_F(SandboxedUnpackerTest, MissingDefaultData) {
SetupUnpacker("missing_default_data.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesNoDefaultMessages);
}
TEST_F(SandboxedUnpackerTest, MissingDefaultLocaleHasLocalesFolder) {
SetupUnpacker("missing_default_has_locales.crx", "");
ExpectInstallErrorContains(l10n_util::GetStringUTF8(
IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED));
}
TEST_F(SandboxedUnpackerTest, MissingMessagesFile) {
SetupUnpacker("missing_messages_file.crx", "");
EXPECT_TRUE(base::MatchPattern(
GetInstallError(),
base::ASCIIToUTF16("*") +
base::ASCIIToUTF16(manifest_errors::kLocalesMessagesFileMissing) +
base::ASCIIToUTF16("*_locales?en_US?messages.json'.")))
<< GetInstallError();
}
TEST_F(SandboxedUnpackerTest, NoLocaleData) {
SetupUnpacker("no_locale_data.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesNoDefaultMessages);
}
TEST_F(SandboxedUnpackerTest, ImageDecodingError) {
const char kExpected[] = "Could not decode image: ";
SetupUnpacker("bad_image.crx", "");
......@@ -281,6 +353,17 @@ TEST_F(SandboxedUnpackerTest, SkipHashCheck) {
EXPECT_EQ(base::string16(), GetInstallError());
}
// SandboxedUnpacker is ref counted and is reference by callbacks and
// InterfacePtrs. This tests that it gets deleted as expected (so that no extra
// refs are left).
TEST_F(SandboxedUnpackerTest, DeletedOnSuccess) {
TestSandboxedUnpackerDeleted("good_l10n.crx", /*expect_success=*/true);
}
TEST_F(SandboxedUnpackerTest, DeletedOnFailure) {
TestSandboxedUnpackerDeleted("bad_image.crx", /*expect_success=*/false);
}
class SandboxedUnpackerTestWithRealIOThread : public SandboxedUnpackerTest {
public:
SandboxedUnpackerTestWithRealIOThread()
......
......@@ -8,40 +8,9 @@
module extensions.mojom;
import "mojo/common/file_path.mojom";
import "mojo/common/string16.mojom";
import "mojo/common/values.mojom";
interface ExtensionUnpacker {
// Unzip |file| into the directory |path|.
Unzip(mojo.common.mojom.FilePath file,
mojo.common.mojom.FilePath path) => (bool success);
// Unpack and sanitize the extension in directory |path|, and return its
// parsed manifest.json file in |manifest| and its parsed JSON ruleset for
// the Declarative Net Request API in |json_ruleset|. The supplied |location|,
// and the |creation_flags| defined by Extension::InitFromValueFlags are
// passed into Extension::Create() when unpacking the extension. Decoded
// message catalog data from the extension is written to the
// kDecodedMessageCatalogsFilename file in |path|.
// If Unpack() fails for any reason, |error| contains a user-displayable
// explanation of what went wrong.
// |channel| and |type| are needed to initialize the global state of the
// extension system, which is needed while creating the Extension object.
Unpack(FeatureChannel channel,
FeatureSessionType type,
mojo.common.mojom.FilePath path,
string extension_id,
ManifestLocation location,
int32 creation_flags)
=> (mojo.common.mojom.String16 error,
mojo.common.mojom.DictionaryValue? manifest);
};
[Native]
enum ManifestLocation;
[Native]
enum FeatureChannel;
[Native]
enum FeatureSessionType;
# Copyright 2017 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.
mojom = "//extensions/common/extension_unpacker.mojom"
public_headers = [
"//extensions/common/manifest.h",
"//extensions/common/features/feature_session_type.h",
"//components/version_info/version_info.h",
]
traits_headers = [ "//extensions/common/common_param_traits.h" ]
type_mappings = [
"extensions.mojom.ManifestLocation=extensions::Manifest::Location",
"extensions.mojom.FeatureChannel=version_info::Channel",
"extensions.mojom.FeatureSessionType=extensions::FeatureSessionType",
]
# Copyright 2017 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.
typemaps = [ "//extensions/common/extension_unpacker.typemap" ]
......@@ -9,8 +9,6 @@ assert(enable_extensions)
source_set("utility") {
sources = [
"unpacker.cc",
"unpacker.h",
"utility_handler.cc",
"utility_handler.h",
]
......@@ -29,7 +27,7 @@ source_set("utility") {
source_set("unit_tests") {
testonly = true
sources = [
"unpacker_unittest.cc",
"utility_handler_unittest.cc",
]
deps = [
":utility",
......
// Copyright (c) 2012 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/utility/unpacker.h"
#include <stddef.h>
#include <algorithm>
#include <utility>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/extension_utility_types.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "ui/base/l10n/l10n_util.h"
namespace extensions {
namespace {
namespace errors = manifest_errors;
namespace keys = manifest_keys;
constexpr const base::FilePath::CharType* kAllowedThemeFiletypes[] = {
FILE_PATH_LITERAL(".bmp"), FILE_PATH_LITERAL(".gif"),
FILE_PATH_LITERAL(".jpeg"), FILE_PATH_LITERAL(".jpg"),
FILE_PATH_LITERAL(".json"), FILE_PATH_LITERAL(".png"),
FILE_PATH_LITERAL(".webp")};
} // namespace
Unpacker::Unpacker(const base::FilePath& working_dir,
const base::FilePath& extension_dir,
const std::string& extension_id,
Manifest::Location location,
int creation_flags)
: working_dir_(working_dir),
extension_dir_(extension_dir),
extension_id_(extension_id),
location_(location),
creation_flags_(creation_flags) {}
Unpacker::~Unpacker() {
}
// static
bool Unpacker::ShouldExtractFile(bool is_theme,
const base::FilePath& file_path) {
if (is_theme) {
const base::FilePath::StringType extension =
base::ToLowerASCII(file_path.FinalExtension());
// Allow filenames with no extension.
if (extension.empty())
return true;
return std::find(kAllowedThemeFiletypes,
kAllowedThemeFiletypes + arraysize(kAllowedThemeFiletypes),
extension) !=
(kAllowedThemeFiletypes + arraysize(kAllowedThemeFiletypes));
}
return !base::FilePath::CompareEqualIgnoreCase(file_path.FinalExtension(),
FILE_PATH_LITERAL(".exe"));
}
// static
bool Unpacker::IsManifestFile(const base::FilePath& file_path) {
CHECK(!file_path.IsAbsolute());
return base::FilePath::CompareEqualIgnoreCase(file_path.value(),
kManifestFilename);
}
// static
std::unique_ptr<base::DictionaryValue> Unpacker::ReadManifest(
const base::FilePath& extension_dir,
std::string* error) {
DCHECK(error);
base::FilePath manifest_path = extension_dir.Append(kManifestFilename);
if (!base::PathExists(manifest_path)) {
*error = errors::kInvalidManifest;
return nullptr;
}
JSONFileValueDeserializer deserializer(manifest_path);
std::unique_ptr<base::Value> root = deserializer.Deserialize(NULL, error);
if (!root.get()) {
return nullptr;
}
if (!root->is_dict()) {
*error = errors::kInvalidManifest;
return nullptr;
}
return base::DictionaryValue::From(std::move(root));
}
bool Unpacker::Run() {
// Parse the manifest.
std::string error;
parsed_manifest_ = ReadManifest(extension_dir_, &error);
if (!parsed_manifest_.get()) {
SetError(error);
return false;
}
scoped_refptr<Extension> extension(
Extension::Create(extension_dir_, location_, *parsed_manifest_,
creation_flags_, extension_id_, &error));
if (!extension.get()) {
SetError(error);
return false;
}
std::vector<InstallWarning> warnings;
if (!file_util::ValidateExtension(extension.get(), &error, &warnings)) {
SetError(error);
return false;
}
extension->AddInstallWarnings(warnings);
return true;
}
void Unpacker::SetError(const std::string& error) {
error_message_ = base::UTF8ToUTF16(error);
}
} // namespace extensions
// Copyright (c) 2012 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_UTILITY_UNPACKER_H_
#define EXTENSIONS_UTILITY_UNPACKER_H_
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/macros.h"
#include "extensions/common/manifest.h"
namespace base {
class DictionaryValue;
}
namespace extensions {
// This class unpacks an extension. It is designed to be used in a sandboxed
// child process. We parse various bits of the extension, then report back to
// the browser process, who then transcodes the pre-parsed bits and writes them
// back out to disk for later use.
class Unpacker {
public:
Unpacker(const base::FilePath& working_dir,
const base::FilePath& extension_dir,
const std::string& extension_id,
Manifest::Location location,
int creation_flags);
~Unpacker();
// Returns true if the given base::FilePath should be unzipped.
static bool ShouldExtractFile(bool is_theme, const base::FilePath& file_path);
// Returns true for manifest.json only.
static bool IsManifestFile(const base::FilePath& file_path);
// Parse the manifest.json file inside the extension (not in the header).
static std::unique_ptr<base::DictionaryValue> ReadManifest(
const base::FilePath& extension_dir,
std::string* error);
// Runs the processing steps for the extension. On success, this returns true
// and the decoded messages will be in a
// file at |working_dir|/kDecodedMessageCatalogsFilename.
bool Run();
const base::string16& error_message() { return error_message_; }
base::DictionaryValue* parsed_manifest() { return parsed_manifest_.get(); }
std::unique_ptr<base::DictionaryValue> TakeParsedManifest() {
return std::move(parsed_manifest_);
}
private:
// Write the decoded messages to kDecodedMessageCatalogsFilename. We do this
// instead of sending them over IPC, since they are so large. Returns true on
// success.
bool DumpMessageCatalogsToFile();
// Parse all _locales/*/messages.json files inside the extension.
bool ReadAllMessageCatalogs();
// Decodes the image at the given path and puts it in our list of decoded
// images.
bool AddDecodedImage(const base::FilePath& path);
// Parses the catalog at the given path and puts it in our list of parsed
// catalogs.
bool ReadMessageCatalog(const base::FilePath& message_path);
// Set the error message.
void SetError(const std::string& error);
// The directory to do work in.
base::FilePath working_dir_;
// The directory where the extension source lives.
base::FilePath extension_dir_;
// The extension ID if known.
std::string extension_id_;
// The location to use for the created extension.
Manifest::Location location_;
// The creation flags to use with the created extension.
int creation_flags_;
// The parsed version of the manifest JSON contained in the extension.
std::unique_ptr<base::DictionaryValue> parsed_manifest_;
// The last error message that was set. Empty if there were no errors.
base::string16 error_message_;
DISALLOW_COPY_AND_ASSIGN(Unpacker);
};
} // namespace extensions
#endif // EXTENSIONS_UTILITY_UNPACKER_H_
// Copyright (c) 2012 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 <memory>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "extensions/test/test_extensions_client.h"
#include "extensions/utility/unpacker.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/zlib/google/zip.h"
#include "ui/base/l10n/l10n_util.h"
using base::ASCIIToUTF16;
namespace extensions {
namespace errors = manifest_errors;
namespace keys = manifest_keys;
class UnpackerTest : public testing::Test {
public:
~UnpackerTest() override {
VLOG(1) << "Deleting temp dir: " << temp_dir_.GetPath().LossyDisplayName();
VLOG(1) << temp_dir_.Delete();
}
void SetupUnpacker(const std::string& crx_name) {
base::FilePath crx_path;
ASSERT_TRUE(PathService::Get(DIR_TEST_DATA, &crx_path));
crx_path = crx_path.AppendASCII("unpacker").AppendASCII(crx_name);
ASSERT_TRUE(base::PathExists(crx_path)) << crx_path.value();
// Try bots won't let us write into DIR_TEST_DATA, so we have to create
// a temp folder to play in.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath unzipped_dir = temp_dir_.GetPath().AppendASCII("unzipped");
ASSERT_TRUE(zip::Unzip(crx_path, unzipped_dir))
<< "Failed to unzip " << crx_path.value() << " to "
<< unzipped_dir.value();
unpacker_.reset(new Unpacker(temp_dir_.GetPath(), unzipped_dir,
std::string(), Manifest::INTERNAL,
Extension::NO_FLAGS));
}
protected:
base::ScopedTempDir temp_dir_;
std::unique_ptr<Unpacker> unpacker_;
};
TEST_F(UnpackerTest, EmptyDefaultLocale) {
SetupUnpacker("empty_default_locale.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(ASCIIToUTF16(errors::kInvalidDefaultLocale),
unpacker_->error_message());
}
TEST_F(UnpackerTest, HasDefaultLocaleMissingLocalesFolder) {
SetupUnpacker("has_default_missing_locales.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(ASCIIToUTF16(errors::kLocalesTreeMissing),
unpacker_->error_message());
}
TEST_F(UnpackerTest, InvalidDefaultLocale) {
SetupUnpacker("invalid_default_locale.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(ASCIIToUTF16(errors::kInvalidDefaultLocale),
unpacker_->error_message());
}
TEST_F(UnpackerTest, MissingDefaultData) {
SetupUnpacker("missing_default_data.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(ASCIIToUTF16(errors::kLocalesNoDefaultMessages),
unpacker_->error_message());
}
TEST_F(UnpackerTest, MissingDefaultLocaleHasLocalesFolder) {
SetupUnpacker("missing_default_has_locales.crx");
const base::string16 kExpectedError =
l10n_util::GetStringUTF16(
IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED);
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(kExpectedError, unpacker_->error_message());
}
TEST_F(UnpackerTest, MissingMessagesFile) {
SetupUnpacker("missing_messages_file.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_TRUE(
base::MatchPattern(unpacker_->error_message(),
ASCIIToUTF16(errors::kLocalesMessagesFileMissing) +
ASCIIToUTF16("*_locales?en_US?messages.json")));
}
TEST_F(UnpackerTest, NoLocaleData) {
SetupUnpacker("no_locale_data.crx");
EXPECT_FALSE(unpacker_->Run());
EXPECT_EQ(ASCIIToUTF16(errors::kLocalesNoDefaultMessages),
unpacker_->error_message());
}
struct UnzipFileFilterTestCase {
const base::FilePath::CharType* input;
const bool should_unzip;
};
void RunZipFileFilterTest(const std::vector<UnzipFileFilterTestCase>& cases,
base::Callback<bool(const base::FilePath&)>& filter) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
for (size_t i = 0; i < cases.size(); ++i) {
base::FilePath input(cases[i].input);
bool observed = filter.Run(input);
EXPECT_EQ(cases[i].should_unzip, observed) << "i: " << i
<< ", input: " << input.value();
}
}
TEST_F(UnpackerTest, NonTheme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("foo"), true},
{FILE_PATH_LITERAL("foo.nexe"), true},
{FILE_PATH_LITERAL("foo.dll"), true},
{FILE_PATH_LITERAL("foo.jpg.exe"), false},
{FILE_PATH_LITERAL("foo.exe"), false},
{FILE_PATH_LITERAL("foo.EXE"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::Callback<bool(const base::FilePath&)> filter =
base::Bind(&Unpacker::ShouldExtractFile, false);
RunZipFileFilterTest(cases, filter);
}
TEST_F(UnpackerTest, Theme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("image.jpg"), true},
{FILE_PATH_LITERAL("IMAGE.JPEG"), true},
{FILE_PATH_LITERAL("test/image.bmp"), true},
{FILE_PATH_LITERAL("test/IMAGE.gif"), true},
{FILE_PATH_LITERAL("test/image.WEBP"), true},
{FILE_PATH_LITERAL("test/dir/file.image.png"), true},
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("other.html"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::Callback<bool(const base::FilePath&)> filter =
base::Bind(&Unpacker::ShouldExtractFile, true);
RunZipFileFilterTest(cases, filter);
}
TEST_F(UnpackerTest, ManifestExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("MANIFEST.JSON"), true},
{FILE_PATH_LITERAL("test/manifest.json"), false},
{FILE_PATH_LITERAL("manifest.json/test"), false},
{FILE_PATH_LITERAL("other.file"), false},
};
base::Callback<bool(const base::FilePath&)> filter =
base::Bind(&Unpacker::IsManifestFile);
RunZipFileFilterTest(cases, filter);
}
} // namespace extensions
......@@ -10,6 +10,8 @@
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/location.h"
#include "base/task_scheduler/post_task.h"
#include "content/public/utility/utility_thread.h"
......@@ -19,8 +21,9 @@
#include "extensions/common/extensions_client.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/features/feature_session_type.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "extensions/utility/unpacker.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "third_party/zlib/google/zip.h"
#include "ui/base/l10n/l10n_util.h"
......@@ -30,31 +33,34 @@ namespace extensions {
namespace {
struct UnpackResult {
std::unique_ptr<base::DictionaryValue> parsed_manifest;
base::string16 error;
};
constexpr const base::FilePath::CharType* kAllowedThemeFiletypes[] = {
FILE_PATH_LITERAL(".bmp"), FILE_PATH_LITERAL(".gif"),
FILE_PATH_LITERAL(".jpeg"), FILE_PATH_LITERAL(".jpg"),
FILE_PATH_LITERAL(".json"), FILE_PATH_LITERAL(".png"),
FILE_PATH_LITERAL(".webp")};
std::unique_ptr<base::DictionaryValue> ReadManifest(
const base::FilePath& extension_dir,
std::string* error) {
DCHECK(error);
base::FilePath manifest_path = extension_dir.Append(kManifestFilename);
if (!base::PathExists(manifest_path)) {
*error = manifest_errors::kInvalidManifest;
return nullptr;
}
JSONFileValueDeserializer deserializer(manifest_path);
std::unique_ptr<base::Value> root = deserializer.Deserialize(NULL, error);
if (!root) {
return nullptr;
}
// Unpacks the extension on background task runner.
// On success, returns UnpackResult with |parsed_manifest| set to the parsed
// extension manifest.
// On failure returns UnpackResult with |error| set to the encountered error
// message.
UnpackResult UnpackOnBackgroundTaskRunner(const base::FilePath& path,
const std::string& extension_id,
Manifest::Location location,
int32_t creation_flags) {
Unpacker unpacker(path.DirName(), path, extension_id, location,
creation_flags);
UnpackResult result;
if (unpacker.Run()) {
result.parsed_manifest = unpacker.TakeParsedManifest();
} else {
result.error = unpacker.error_message();
if (!root->is_dict()) {
*error = manifest_errors::kInvalidManifest;
return nullptr;
}
return result;
return base::DictionaryValue::From(std::move(root));
}
class ExtensionUnpackerImpl : public extensions::mojom::ExtensionUnpacker {
......@@ -87,47 +93,15 @@ class ExtensionUnpackerImpl : public extensions::mojom::ExtensionUnpacker {
std::move(callback));
}
void Unpack(version_info::Channel channel,
extensions::FeatureSessionType type,
const base::FilePath& path,
const std::string& extension_id,
Manifest::Location location,
int32_t creation_flags,
UnpackCallback callback) override {
CHECK_GT(location, Manifest::INVALID_LOCATION);
CHECK_LT(location, Manifest::NUM_LOCATIONS);
DCHECK(ExtensionsClient::Get());
content::UtilityThread::Get()->EnsureBlinkInitialized();
// Initialize extension system global state.
SetCurrentChannel(channel);
SetCurrentFeatureSessionType(type);
// Move unpack operation to background thread to prevent it from blocking
// the utility process thread for extended amount of time.
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_BLOCKING, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&UnpackOnBackgroundTaskRunner, path, extension_id,
location, creation_flags),
base::BindOnce(
[](UnpackCallback callback, UnpackResult result) {
std::move(callback).Run(result.error,
std::move(result.parsed_manifest));
},
std::move(callback)));
}
static bool UnzipFileManifestIntoPath(
const base::FilePath& file,
const base::FilePath& path,
std::unique_ptr<base::DictionaryValue>* manifest) {
if (zip::UnzipWithFilterCallback(
file, path, base::Bind(&Unpacker::IsManifestFile), false)) {
file, path, base::BindRepeating(&utility_handler::IsManifestFile),
false)) {
std::string error;
*manifest = Unpacker::ReadManifest(path, &error);
*manifest = ReadManifest(path, &error);
return error.empty() && manifest->get();
}
......@@ -143,7 +117,8 @@ class ExtensionUnpackerImpl : public extensions::mojom::ExtensionUnpacker {
// Add install warnings.
return zip::UnzipWithFilterCallback(
file, path,
base::Bind(&Unpacker::ShouldExtractFile, internal.is_theme()),
base::BindRepeating(&utility_handler::ShouldExtractFile,
internal.is_theme()),
true /* log_skipped_files */);
}
......@@ -183,6 +158,25 @@ void ExposeInterfacesToBrowser(service_manager::BinderRegistry* registry,
base::ThreadTaskRunnerHandle::Get());
}
bool ShouldExtractFile(bool is_theme, const base::FilePath& file_path) {
if (is_theme) {
const base::FilePath::StringType extension =
base::ToLowerASCII(file_path.FinalExtension());
// Allow filenames with no extension.
if (extension.empty())
return true;
return base::ContainsValue(kAllowedThemeFiletypes, extension);
}
return !base::FilePath::CompareEqualIgnoreCase(file_path.FinalExtension(),
FILE_PATH_LITERAL(".exe"));
}
bool IsManifestFile(const base::FilePath& file_path) {
CHECK(!file_path.IsAbsolute());
return base::FilePath::CompareEqualIgnoreCase(file_path.value(),
kManifestFilename);
}
} // namespace utility_handler
} // namespace extensions
......@@ -7,6 +7,10 @@
#include "services/service_manager/public/cpp/binder_registry.h"
namespace base {
class FilePath;
}
namespace extensions {
namespace utility_handler {
......@@ -16,6 +20,10 @@ void UtilityThreadStarted();
void ExposeInterfacesToBrowser(service_manager::BinderRegistry* registry,
bool running_elevated);
bool ShouldExtractFile(bool is_theme, const base::FilePath& file_path);
bool IsManifestFile(const base::FilePath& file_path);
} // namespace utility_handler
} // namespace extensions
......
// Copyright 2018 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 <vector>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "extensions/utility/utility_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace extensions {
using UtilityHandlerTest = testing::Test;
struct UnzipFileFilterTestCase {
const base::FilePath::CharType* input;
const bool should_unzip;
};
void RunZipFileFilterTest(
const std::vector<UnzipFileFilterTestCase>& cases,
base::RepeatingCallback<bool(const base::FilePath&)>& filter) {
for (size_t i = 0; i < cases.size(); ++i) {
base::FilePath input(cases[i].input);
bool observed = filter.Run(input);
EXPECT_EQ(cases[i].should_unzip, observed)
<< "i: " << i << ", input: " << input.value();
}
}
TEST_F(UtilityHandlerTest, NonTheme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("foo"), true},
{FILE_PATH_LITERAL("foo.nexe"), true},
{FILE_PATH_LITERAL("foo.dll"), true},
{FILE_PATH_LITERAL("foo.jpg.exe"), false},
{FILE_PATH_LITERAL("foo.exe"), false},
{FILE_PATH_LITERAL("foo.EXE"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&utility_handler::ShouldExtractFile, false);
RunZipFileFilterTest(cases, filter);
}
TEST_F(UtilityHandlerTest, Theme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("image.jpg"), true},
{FILE_PATH_LITERAL("IMAGE.JPEG"), true},
{FILE_PATH_LITERAL("test/image.bmp"), true},
{FILE_PATH_LITERAL("test/IMAGE.gif"), true},
{FILE_PATH_LITERAL("test/image.WEBP"), true},
{FILE_PATH_LITERAL("test/dir/file.image.png"), true},
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("other.html"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&utility_handler::ShouldExtractFile, true);
RunZipFileFilterTest(cases, filter);
}
TEST_F(UtilityHandlerTest, ManifestExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("MANIFEST.JSON"), true},
{FILE_PATH_LITERAL("test/manifest.json"), false},
{FILE_PATH_LITERAL("manifest.json/test"), false},
{FILE_PATH_LITERAL("other.file"), false},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&utility_handler::IsManifestFile);
RunZipFileFilterTest(cases, filter);
}
} // namespace extensions
......@@ -19,7 +19,6 @@ _typemap_imports = [
"//content/public/common/typemaps.gni",
"//device/bluetooth/public/interfaces/typemaps.gni",
"//device/gamepad/public/interfaces/typemaps.gni",
"//extensions/common/typemaps.gni",
"//gpu/ipc/common/typemaps.gni",
"//media/capture/mojo/typemaps.gni",
"//media/mojo/interfaces/typemaps.gni",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment