diff options
author | erikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-01-29 23:19:19 +0000 |
---|---|---|
committer | erikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-01-29 23:19:19 +0000 |
commit | cc65591412746641bf656d52d71663726cdd7934 (patch) | |
tree | f31a1514d566e91d00219f38d57e2cbacac988dd | |
parent | 219a862ce1d8b647364c31dfa619c4d956cfae35 (diff) | |
download | chromium_src-cc65591412746641bf656d52d71663726cdd7934.zip chromium_src-cc65591412746641bf656d52d71663726cdd7934.tar.gz chromium_src-cc65591412746641bf656d52d71663726cdd7934.tar.bz2 |
Simple installation of extensions using chrome.exe --install-extensionChanged manifest filename to end in .json.Updated authoring script to include sha256 hash of zip file.
Review URL: http://codereview.chromium.org/18477
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@8926 0039d316-1c4b-4281-b951-d872f2087c98
18 files changed, 683 insertions, 46 deletions
diff --git a/chrome/browser/browser_init.cc b/chrome/browser/browser_init.cc index ce4febe..2e94348 100644 --- a/chrome/browser/browser_init.cc +++ b/chrome/browser/browser_init.cc @@ -222,6 +222,15 @@ bool BrowserInit::LaunchWithProfile::Launch(Profile* profile, } } + // Start up the extensions service + profile->InitExtensions(); + if (parsed_command_line.HasSwitch(switches::kInstallExtension)) { + std::wstring path_string = + parsed_command_line.GetSwitchValue(switches::kInstallExtension); + FilePath path = FilePath::FromWStringHack(path_string); + profile->GetExtensionsService()->InstallExtension(path); + } + return true; } diff --git a/chrome/browser/extensions/extension.cc b/chrome/browser/extensions/extension.cc index b5918d5..f932eff 100644 --- a/chrome/browser/extensions/extension.cc +++ b/chrome/browser/extensions/extension.cc @@ -4,6 +4,7 @@ #include "chrome/browser/extensions/extension.h" +#include "base/file_path.h" #include "base/logging.h" #include "base/string_util.h" #include "net/base/net_util.h" @@ -11,7 +12,7 @@ const char kExtensionURLScheme[] = "chrome-extension"; const char kUserScriptURLScheme[] = "chrome-user-script"; -const char Extension::kManifestFilename[] = "manifest"; +const char Extension::kManifestFilename[] = "manifest.json"; const wchar_t* Extension::kDescriptionKey = L"description"; const wchar_t* Extension::kFilesKey = L"files"; @@ -21,6 +22,7 @@ const wchar_t* Extension::kMatchesKey = L"matches"; const wchar_t* Extension::kNameKey = L"name"; const wchar_t* Extension::kUserScriptsKey = L"user_scripts"; const wchar_t* Extension::kVersionKey = L"version"; +const wchar_t* Extension::kZipHashKey = L"zip_hash"; // Extension-related error messages. Some of these are simple patterns, where a // '*' is replaced at runtime with a specific value. This is used instead of @@ -56,6 +58,12 @@ const char* Extension::kInvalidUserScriptsListError = "Invalid value for 'user_scripts'."; const char* Extension::kInvalidVersionError = "Required value 'version' is missing or invalid."; +const char* Extension::kInvalidZipHashError = + "Required key 'zip_hash' is missing or invalid."; + +const std::string Extension::VersionString() const { + return version_->GetString(); +} // Defined in extension_protocols.h. extern const char kExtensionURLScheme[]; @@ -149,7 +157,7 @@ bool Extension::InitFromValue(const DictionaryValue& source, // Check format version. int format_version = 0; if (!source.GetInteger(kFormatVersionKey, &format_version) || - format_version != kExpectedFormatVersion) { + static_cast<uint32>(format_version) != kExpectedFormatVersion) { *error = kInvalidFormatVersionError; return false; } @@ -159,12 +167,34 @@ bool Extension::InitFromValue(const DictionaryValue& source, *error = kInvalidIdError; return false; } + // Verify that the id is legal. This test is basically verifying that it + // is ASCII and doesn't have any path components in it. + // TODO(erikkay): verify the actual id format - it will be more restrictive + // than this. Perhaps just a hex string? + if (!IsStringASCII(id_)) { + *error = kInvalidIdError; + return false; + } + FilePath id_path; + id_path = id_path.AppendASCII(id_); + if ((id_path.value() == FilePath::kCurrentDirectory) || + (id_path.value() == FilePath::kParentDirectory) || + !(id_path.BaseName() == id_path)) { + *error = kInvalidIdError; + return false; + } // Initialize URL. extension_url_ = GURL(std::string(kExtensionURLScheme) + "://" + id_ + "/"); // Initialize version. - if (!source.GetString(kVersionKey, &version_)) { + std::string version_str; + if (!source.GetString(kVersionKey, &version_str)) { + *error = kInvalidVersionError; + return false; + } + version_.reset(Version::GetVersionFromString(version_str)); + if (!version_.get()) { *error = kInvalidVersionError; return false; } @@ -183,6 +213,16 @@ bool Extension::InitFromValue(const DictionaryValue& source, } } + // Initialize zip hash (only present in zip) + // There's no need to verify it at this point. If it's in a bogus format + // it won't pass the hash verify step. + if (source.HasKey(kZipHashKey)) { + if (!source.GetString(kZipHashKey, &zip_hash_)) { + *error = kInvalidZipHashError; + return false; + } + } + // Initialize user scripts (optional). if (source.HasKey(kUserScriptsKey)) { ListValue* list_value; @@ -250,3 +290,4 @@ bool Extension::InitFromValue(const DictionaryValue& source, return true; } + diff --git a/chrome/browser/extensions/extension.h b/chrome/browser/extensions/extension.h index e6a2ca8..22e8fd6 100644 --- a/chrome/browser/extensions/extension.h +++ b/chrome/browser/extensions/extension.h @@ -9,8 +9,10 @@ #include <vector> #include "base/file_path.h" +#include "base/scoped_ptr.h" #include "base/string16.h" #include "base/values.h" +#include "base/version.h" #include "chrome/browser/extensions/user_script_master.h" #include "googleurl/src/gurl.h" @@ -25,10 +27,11 @@ extern const char kUserScriptURLScheme[]; // Represents a Chromium extension. class Extension { public: - Extension(const FilePath& path); + Extension() {} + explicit Extension(const FilePath& path); // The format for extension manifests that this code understands. - static const int kExpectedFormatVersion = 1; + static const unsigned int kExpectedFormatVersion = 1; // The name of the manifest inside an extension. static const char kManifestFilename[]; @@ -42,6 +45,7 @@ class Extension { static const wchar_t* kNameKey; static const wchar_t* kUserScriptsKey; static const wchar_t* kVersionKey; + static const wchar_t* kZipHashKey; // Error messages returned from InitFromValue(). static const char* kInvalidDescriptionError; @@ -58,6 +62,7 @@ class Extension { static const char* kInvalidUserScriptError; static const char* kInvalidUserScriptsListError; static const char* kInvalidVersionError; + static const char* kInvalidZipHashError; // Creates an absolute url to a resource inside an extension. The // |extension_url| argument should be the url() from an Extension object. The @@ -90,7 +95,10 @@ class Extension { const std::string& id() const { return id_; } // The version number for the extension. - const std::string& version() const { return version_; } + const Version* version() const { return version_.get(); } + + // String representation of the version number. + const std::string VersionString() const; // A human-readable name of the extension. const std::string& name() const { return name_; } @@ -117,7 +125,7 @@ class Extension { std::string id_; // The extension's version. - std::string version_; + scoped_ptr<Version> version_; // The extension's human-readable name. std::string name_; @@ -128,6 +136,11 @@ class Extension { // Paths to the content scripts the extension contains. UserScriptList user_scripts_; + // A SHA1 hash of the contents of the zip file. Note that this key is only + // present in the manifest that's prepended to the zip. The inner manifest + // will not have this key. + std::string zip_hash_; + DISALLOW_COPY_AND_ASSIGN(Extension); }; diff --git a/chrome/browser/extensions/extension_unittest.cc b/chrome/browser/extensions/extension_unittest.cc index 83973dd..125b939 100644 --- a/chrome/browser/extensions/extension_unittest.cc +++ b/chrome/browser/extensions/extension_unittest.cc @@ -25,9 +25,9 @@ TEST(ExtensionTest, InitFromValueInvalid) { std::wstring extensions_dir; ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_dir)); FilePath extensions_path = FilePath::FromWStringHack(extensions_dir) - .Append(FILE_PATH_LITERAL("extensions")) - .Append(FILE_PATH_LITERAL("extension1")) - .Append(FILE_PATH_LITERAL("manifest")); + .AppendASCII("extensions") + .AppendASCII("extension1") + .AppendASCII(Extension::kManifestFilename); JSONFileValueSerializer serializer(extensions_path.ToWStringHack()); scoped_ptr<DictionaryValue> valid_value( @@ -99,6 +99,7 @@ TEST(ExtensionTest, InitFromValueInvalid) { input_value.reset(static_cast<DictionaryValue*>(valid_value->DeepCopy())); ListValue* user_scripts = NULL; input_value->GetList(Extension::kUserScriptsKey, &user_scripts); + ASSERT_FALSE(NULL == user_scripts); user_scripts->Set(0, Value::CreateIntegerValue(42)); EXPECT_FALSE(extension.InitFromValue(*input_value, &error)); EXPECT_TRUE(MatchPattern(error, Extension::kInvalidUserScriptError)); @@ -168,13 +169,13 @@ TEST(ExtensionTest, InitFromValueValid) { // Test minimal extension input_value.SetInteger(Extension::kFormatVersionKey, 1); input_value.SetString(Extension::kIdKey, "com.google.myextension"); - input_value.SetString(Extension::kVersionKey, "1.0"); + input_value.SetString(Extension::kVersionKey, "1.0.0.0"); input_value.SetString(Extension::kNameKey, "my extension"); EXPECT_TRUE(extension.InitFromValue(input_value, &error)); EXPECT_EQ("", error); EXPECT_EQ("com.google.myextension", extension.id()); - EXPECT_EQ("1.0", extension.version()); + EXPECT_EQ("1.0.0.0", extension.VersionString()); EXPECT_EQ("my extension", extension.name()); EXPECT_EQ("chrome-extension://com.google.myextension/", extension.url().spec()); @@ -191,7 +192,7 @@ TEST(ExtensionTest, GetResourceURLAndPath) { DictionaryValue input_value; input_value.SetInteger(Extension::kFormatVersionKey, 1); input_value.SetString(Extension::kIdKey, "com.google.myextension"); - input_value.SetString(Extension::kVersionKey, "1.0"); + input_value.SetString(Extension::kVersionKey, "1.0.0.0"); input_value.SetString(Extension::kNameKey, "my extension"); EXPECT_TRUE(extension.InitFromValue(input_value, NULL)); diff --git a/chrome/browser/extensions/extensions_service.cc b/chrome/browser/extensions/extensions_service.cc index e103821..78758df 100644 --- a/chrome/browser/extensions/extensions_service.cc +++ b/chrome/browser/extensions/extensions_service.cc @@ -5,24 +5,43 @@ #include "chrome/browser/extensions/extensions_service.h" #include "base/file_util.h" -#include "base/values.h" +#include "base/scoped_handle.h" +#include "base/scoped_temp_dir.h" #include "base/string_util.h" +#include "base/third_party/nss/blapi.h" +#include "base/third_party/nss/sha256.h" #include "base/thread.h" +#include "base/values.h" +#include "net/base/file_stream.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/extensions/user_script_master.h" #include "chrome/common/json_value_serializer.h" #include "chrome/common/notification_service.h" +#include "chrome/common/unzip.h" // ExtensionsService -const FilePath::CharType* ExtensionsService::kInstallDirectoryName = - FILE_PATH_LITERAL("Extensions"); +const char* ExtensionsService::kInstallDirectoryName = "Extensions"; +const char* ExtensionsService::kCurrentVersionFileName = "Current Version"; +const char* ExtensionsServiceBackend::kTempExtensionName = "TEMP_INSTALL"; +// Chromium Extension magic number +static const char kExtensionFileMagic[] = "Cr24"; + +struct ExtensionHeader { + char magic[sizeof(kExtensionFileMagic) - 1]; + uint32 version; + size_t header_size; + size_t manifest_size; +}; + +const size_t kZipHashBytes = 32; // SHA-256 +const size_t kZipHashHexBytes = kZipHashBytes * 2; // Hex string is 2x size. ExtensionsService::ExtensionsService(const FilePath& profile_directory, UserScriptMaster* user_script_master) : message_loop_(MessageLoop::current()), backend_(new ExtensionsServiceBackend), - install_directory_(profile_directory.Append(kInstallDirectoryName)), + install_directory_(profile_directory.AppendASCII(kInstallDirectoryName)), user_script_master_(user_script_master) { } @@ -87,6 +106,31 @@ void ExtensionsService::OnExtensionLoadError(const std::string& error) { LOG(WARNING) << error; } +void ExtensionsService::InstallExtension(const FilePath& extension_path) { + // TODO(aa): This message loop should probably come from a backend + // interface, similar to how the message loop for the frontend comes + // from the frontend interface. + g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, + NewRunnableMethod(backend_.get(), + &ExtensionsServiceBackend::InstallExtension, + extension_path, + install_directory_, + scoped_refptr<ExtensionsServiceFrontendInterface>(this))); +} + +void ExtensionsService::OnExtensionInstallError(const std::string& error) { + // TODO(erikkay): Print the error message out somewhere better. + LOG(WARNING) << error; +} + +void ExtensionsService::OnExtensionInstalled(FilePath path) { + NotificationService::current()->Notify(NOTIFY_EXTENSION_INSTALLED, + NotificationService::AllSources(), + Details<FilePath>(&path)); + + // TODO(erikkay): now what? +} + // ExtensionsServicesBackend @@ -101,14 +145,14 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory( // manifests. Post errors and results to the frontend. scoped_ptr<ExtensionList> extensions(new ExtensionList); file_util::FileEnumerator enumerator(path, - false, // not recursive + false, // not recursive file_util::FileEnumerator::DIRECTORIES); for (FilePath child_path = enumerator.Next(); !child_path.value().empty(); child_path = enumerator.Next()) { FilePath manifest_path = child_path.AppendASCII(Extension::kManifestFilename); if (!file_util::PathExists(manifest_path)) { - ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(), + ReportExtensionLoadError(frontend.get(), child_path, Extension::kInvalidManifestError); continue; } @@ -117,13 +161,13 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory( std::string error; scoped_ptr<Value> root(serializer.Deserialize(&error)); if (!root.get()) { - ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(), + ReportExtensionLoadError(frontend.get(), child_path, error); continue; } if (!root->IsType(Value::TYPE_DICTIONARY)) { - ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(), + ReportExtensionLoadError(frontend.get(), child_path, Extension::kInvalidManifestError); continue; } @@ -131,8 +175,7 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory( scoped_ptr<Extension> extension(new Extension(child_path)); if (!extension->InitFromValue(*static_cast<DictionaryValue*>(root.get()), &error)) { - ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(), - error); + ReportExtensionLoadError(frontend.get(), child_path, error); continue; } @@ -144,10 +187,12 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory( } void ExtensionsServiceBackend::ReportExtensionLoadError( - ExtensionsServiceFrontendInterface *frontend, const std::wstring& path, + ExtensionsServiceFrontendInterface *frontend, const FilePath& path, const std::string &error) { + // TODO(erikkay): note that this isn't guaranteed to work properly on Linux. + std::string path_str = WideToASCII(path.ToWStringHack()); std::string message = StringPrintf("Could not load extension from '%s'. %s", - WideToASCII(path).c_str(), error.c_str()); + path_str.c_str(), error.c_str()); frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod( frontend, &ExtensionsServiceFrontendInterface::OnExtensionLoadError, message)); @@ -160,3 +205,329 @@ void ExtensionsServiceBackend::ReportExtensionsLoaded( &ExtensionsServiceFrontendInterface::OnExtensionsLoadedFromDirectory, extensions)); } + +// The extension file format is a header, followed by the manifest, followed +// by the zip file. The header is a magic number, a version, the size of the +// header, and the size of the manifest. These ints are 4 byte little endian. +DictionaryValue* ExtensionsServiceBackend::ReadManifest( + const FilePath& extension_path, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + ScopedStdioHandle file(file_util::OpenFile(extension_path, "rb")); + if (!file.get()) { + ReportExtensionInstallError(frontend, extension_path, + "no such extension file"); + return NULL; + } + + // Read and verify the header. + ExtensionHeader header; + size_t len; + // TODO(erikkay): Yuck. I'm not a big fan of this kind of code, but it + // appears that we don't have any endian/alignment aware serialization + // code in the code base. So for now, this assumes that we're running + // on a little endian machine with 4 byte alignment. + len = fread(&header, 1, sizeof(ExtensionHeader), file.get()); + if (len < sizeof(ExtensionHeader)) { + ReportExtensionInstallError(frontend, extension_path, + "invalid extension header"); + return NULL; + } + if (strncmp(kExtensionFileMagic, header.magic, sizeof(header.magic))) { + ReportExtensionInstallError(frontend, extension_path, + "bad magic number"); + return NULL; + } + if (header.version != Extension::kExpectedFormatVersion) { + ReportExtensionInstallError(frontend, extension_path, + "bad version number"); + return NULL; + } + if (header.header_size > sizeof(ExtensionHeader)) + fseek(file.get(), header.header_size - sizeof(ExtensionHeader), SEEK_CUR); + + char buf[1 << 16]; + std::string manifest_str; + size_t read_size = std::min(sizeof(buf), header.manifest_size); + size_t remainder = header.manifest_size; + while ((len = fread(buf, 1, read_size, file.get())) > 0) { + manifest_str.append(buf, len); + if (len <= remainder) + break; + remainder -= len; + read_size = std::min(sizeof(buf), remainder); + } + + // Verify the JSON + JSONStringValueSerializer json(manifest_str); + std::string error; + Value* val = json.Deserialize(&error); + if (!val) { + ReportExtensionInstallError(frontend, extension_path, error); + return NULL; + } + if (!val->IsType(Value::TYPE_DICTIONARY)) { + ReportExtensionInstallError(frontend, extension_path, + "manifest isn't a JSON dictionary"); + return NULL; + } + DictionaryValue* manifest = static_cast<DictionaryValue*>(val); + std::string zip_hash; + if (!manifest->GetString(Extension::kZipHashKey, &zip_hash)) { + ReportExtensionInstallError(frontend, extension_path, + "missing zip_hash key"); + return NULL; + } + if (zip_hash.size() != kZipHashHexBytes) { + ReportExtensionInstallError(frontend, extension_path, + "invalid zip_hash key"); + return NULL; + } + + // Read the rest of the zip file and compute a hash to compare against + // what the manifest claims. Compute the hash incrementally since the + // zip file could be large. + const unsigned char* ubuf = reinterpret_cast<const unsigned char*>(buf); + SHA256Context ctx; + SHA256_Begin(&ctx); + while ((len = fread(buf, 1, sizeof(buf), file.get())) > 0) + SHA256_Update(&ctx, ubuf, len); + uint8 hash[32]; + SHA256_End(&ctx, hash, NULL, sizeof(hash)); + + std::vector<uint8> zip_hash_bytes; + if (!HexStringToBytes(zip_hash, &zip_hash_bytes)) { + ReportExtensionInstallError(frontend, extension_path, + "invalid zip_hash key"); + return NULL; + } + if (zip_hash_bytes.size() != kZipHashBytes) { + ReportExtensionInstallError(frontend, extension_path, + "invalid zip_hash key"); + return NULL; + } + for (size_t i = 0; i < kZipHashBytes; ++i) { + if (zip_hash_bytes[i] != hash[i]) { + ReportExtensionInstallError(frontend, extension_path, + "zip_hash key didn't match zip hash"); + return NULL; + } + } + + // TODO(erikkay): The manifest will also contain a signature of the hash + // (or perhaps the whole manifest) for authentication purposes. + + return manifest; +} + +bool ExtensionsServiceBackend::CheckCurrentVersion( + const FilePath& extension_path, + const std::string& version, + const FilePath& dest_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + FilePath current_version = + dest_dir.AppendASCII(ExtensionsService::kCurrentVersionFileName); + if (file_util::PathExists(current_version)) { + std::string version_str; + if (file_util::ReadFileToString(current_version, &version_str)) { + if (version_str == version) { + ReportExtensionInstallError(frontend, extension_path, + "Extension version already installed"); + return false; + } else { + scoped_ptr<Version> cur_version( + Version::GetVersionFromString(version_str)); + scoped_ptr<Version> new_version( + Version::GetVersionFromString(version)); + if (cur_version->CompareTo(*new_version) >= 0) { + ReportExtensionInstallError(frontend, extension_path, + "More recent version of extension already installed"); + return false; + } + } + } + } + return true; +} + +bool ExtensionsServiceBackend::UnzipExtension(const FilePath& extension_path, + const FilePath& temp_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + // <profile>/Extensions/INSTALL_TEMP/<version> + if (!file_util::CreateDirectory(temp_dir)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't create version directory."); + return false; + } + if (!Unzip(extension_path, temp_dir, NULL)) { + // Remove what we just installed. + file_util::Delete(temp_dir, true); + ReportExtensionInstallError(frontend, extension_path, + "Couldn't unzip extension."); + return false; + } + return true; +} + +bool ExtensionsServiceBackend::InstallDirSafely( + const FilePath& extension_path, + const FilePath& source_dir, + const FilePath& dest_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + + if (file_util::PathExists(dest_dir)) { + // By the time we get here, it should be safe to assume that this directory + // is not currently in use (it's not the current active version). + if (!file_util::Delete(dest_dir, true)) { + ReportExtensionInstallError(frontend, extension_path, + "Can't delete existing version directory."); + return false; + } + } else { + FilePath parent = dest_dir.DirName(); + if (!file_util::DirectoryExists(parent)) { + if (!file_util::CreateDirectory(parent)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't create extension directory."); + return false; + } + } + } + if (!file_util::Move(source_dir, dest_dir)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't move temporary directory."); + return false; + } + + return true; +} + +bool ExtensionsServiceBackend::SetCurrentVersion( + const FilePath& extension_path, + const FilePath& dest_dir, + std::string version, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + // Write out the new CurrentVersion file. + // <profile>/Extension/<name>/CurrentVersion + FilePath current_version = + dest_dir.AppendASCII(ExtensionsService::kCurrentVersionFileName); + FilePath current_version_old = + current_version.InsertBeforeExtension(FILE_PATH_LITERAL("_old")); + if (file_util::PathExists(current_version_old)) { + if (!file_util::Delete(current_version_old, false)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't remove CurrentVersion_old file."); + return false; + } + } + if (file_util::PathExists(current_version)) { + if (!file_util::Move(current_version, current_version_old)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't move CurrentVersion file."); + return false; + } + } + net::FileStream stream; + int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE; + if (stream.Open(current_version, flags) != 0) + return false; + if (stream.Write(version.c_str(), version.size(), NULL) < 0) { + // Restore the old CurrentVersion. + if (file_util::PathExists(current_version_old)) { + if (!file_util::Move(current_version_old, current_version)) { + LOG(WARNING) << "couldn't restore " << current_version_old.value() << + " to " << current_version.value(); + // TODO(erikkay): This is an ugly state to be in. Try harder? + } + } + ReportExtensionInstallError(frontend, extension_path, + "Couldn't create CurrentVersion file."); + return false; + } + return true; +} + +bool ExtensionsServiceBackend::InstallExtension( + const FilePath& extension_path, + const FilePath& install_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend) { + LOG(INFO) << "Installing extension " << extension_path.value(); + + // <profile>/Extensions/INSTALL_TEMP + FilePath temp_dir = install_dir.AppendASCII(kTempExtensionName); + // Ensure we're starting with a clean slate. + if (file_util::PathExists(temp_dir)) { + if (!file_util::Delete(temp_dir, true)) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't delete existing temporary directory."); + return false; + } + } + ScopedTempDir scoped_temp; + scoped_temp.Set(temp_dir); + if (!scoped_temp.IsValid()) { + ReportExtensionInstallError(frontend, extension_path, + "Couldn't create temporary directory."); + return false; + } + + // Read and verify the extension. + scoped_ptr<DictionaryValue> manifest(ReadManifest(extension_path, frontend)); + if (!manifest.get()) { + // ReadManifest has already reported the extension error. + return false; + } + DictionaryValue* dict = manifest.get(); + Extension extension; + std::string error; + if (!extension.InitFromValue(*dict, &error)) { + ReportExtensionInstallError(frontend, extension_path, + "Invalid extension manifest."); + return false; + } + + // <profile>/Extensions/<id> + FilePath dest_dir = install_dir.AppendASCII(extension.id()); + std::string version = extension.VersionString(); + if (!CheckCurrentVersion(extension_path, version, dest_dir, frontend)) + return false; + + // <profile>/Extensions/INSTALL_TEMP/<version> + FilePath temp_version = temp_dir.AppendASCII(version); + if (!UnzipExtension(extension_path, temp_version, frontend)) + return false; + + // <profile>/Extensions/<dir_name>/<version> + FilePath version_dir = dest_dir.AppendASCII(version); + if (!InstallDirSafely(extension_path, temp_version, version_dir, frontend)) + return false; + + if (!SetCurrentVersion(extension_path, dest_dir, version, frontend)) { + if (!file_util::Delete(version_dir, true)) + LOG(WARNING) << "Can't remove " << dest_dir.value(); + return false; + } + + ReportExtensionInstalled(frontend, dest_dir); + return true; +} + +void ExtensionsServiceBackend::ReportExtensionInstallError( + ExtensionsServiceFrontendInterface *frontend, const FilePath& path, + const std::string &error) { + // TODO(erikkay): note that this isn't guaranteed to work properly on Linux. + std::string path_str = WideToASCII(path.ToWStringHack()); + std::string message = + StringPrintf("Could not install extension from '%s'. %s", + path_str.c_str(), error.c_str()); + frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod( + frontend, &ExtensionsServiceFrontendInterface::OnExtensionInstallError, + message)); +} + +void ExtensionsServiceBackend::ReportExtensionInstalled( + ExtensionsServiceFrontendInterface *frontend, FilePath path) { + frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod( + frontend, + &ExtensionsServiceFrontendInterface::OnExtensionInstalled, + path)); +} diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h index 1a78d0a..b9cf4a6 100644 --- a/chrome/browser/extensions/extensions_service.h +++ b/chrome/browser/extensions/extensions_service.h @@ -33,6 +33,15 @@ class ExtensionsServiceFrontendInterface // Called with results from LoadExtensionsFromDirectory(). The frontend // takes ownership of the list. virtual void OnExtensionsLoadedFromDirectory(ExtensionList* extensions) = 0; + + // Install the extension file at extension_path. + virtual void InstallExtension(const FilePath& extension_path) = 0; + + // Called when installing an extension fails. + virtual void OnExtensionInstallError(const std::string& message) = 0; + + // Called with results from InstallExtension(). + virtual void OnExtensionInstalled(FilePath path) = 0; }; @@ -55,11 +64,17 @@ class ExtensionsService : public ExtensionsServiceFrontendInterface { virtual MessageLoop* GetMessageLoop(); virtual void OnExtensionLoadError(const std::string& message); virtual void OnExtensionsLoadedFromDirectory(ExtensionList* extensions); + virtual void InstallExtension(const FilePath& extension_path); + virtual void OnExtensionInstallError(const std::string& message); + virtual void OnExtensionInstalled(FilePath path); + + // The name of the file that the current active version number is stored in. + static const char* kCurrentVersionFileName; private: // The name of the directory inside the profile where extensions are // installed to. - static const FilePath::CharType* kInstallDirectoryName; + static const char* kInstallDirectoryName; // The message loop for the thread the ExtensionsService is running on. MessageLoop* message_loop_; @@ -96,16 +111,64 @@ class ExtensionsServiceBackend const FilePath &path, scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + // Install the extension file at extension_path to install_dir. + // ReportExtensionInstallError is called on error. + // ReportExtensionInstalled is called on success. + bool InstallExtension( + const FilePath& extension_path, + const FilePath& install_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + private: // Notify a frontend that there was an error loading an extension. void ReportExtensionLoadError(ExtensionsServiceFrontendInterface* frontend, - const std::wstring& path, + const FilePath& path, const std::string& error); // Notify a frontend that extensions were loaded. void ReportExtensionsLoaded(ExtensionsServiceFrontendInterface* frontend, ExtensionList* extensions); + // Notify a frontend that there was an error installing an extension. + void ReportExtensionInstallError(ExtensionsServiceFrontendInterface* frontend, + const FilePath& path, + const std::string& error); + + // Notify a frontend that extensions were installed. + void ReportExtensionInstalled(ExtensionsServiceFrontendInterface* frontend, + FilePath path); + + // Read the manifest from the front of the extension file. + DictionaryValue* ReadManifest(const FilePath& extension_path, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + + // Check that the version to be installed is > the current installed + // extension. + bool CheckCurrentVersion(const FilePath& extension_path, + const std::string& version, + const FilePath& dest_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + + // Unzip the extension into |dest_dir|. + bool UnzipExtension(const FilePath& extension_path, + const FilePath& dest_dir, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + + // Install the extension dir by moving it from |source| to |dest| safely. + bool InstallDirSafely(const FilePath& extension_path, + const FilePath& source, const FilePath& dest, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + + // Update the CurrentVersion file in |dest_dir| to |version|. + bool SetCurrentVersion(const FilePath& extension_path, + const FilePath& dest_dir, + std::string version, + scoped_refptr<ExtensionsServiceFrontendInterface> frontend); + + // The name of a temporary directory to install an extension into for + // validation before finalizing install. + static const char* kTempExtensionName; + DISALLOW_COPY_AND_ASSIGN(ExtensionsServiceBackend); }; diff --git a/chrome/browser/extensions/extensions_service_unittest.cc b/chrome/browser/extensions/extensions_service_unittest.cc index bfc5664..03b0b24 100644 --- a/chrome/browser/extensions/extensions_service_unittest.cc +++ b/chrome/browser/extensions/extensions_service_unittest.cc @@ -32,6 +32,12 @@ struct ExtensionsOrder { class ExtensionsServiceTestFrontend : public ExtensionsServiceFrontendInterface { public: + + ExtensionsServiceTestFrontend() { + file_util::CreateNewTempDirectory(FILE_PATH_LITERAL("ext_test"), + &install_dir_); + } + ~ExtensionsServiceTestFrontend() { for (ExtensionList::iterator iter = extensions_.begin(); iter != extensions_.end(); ++iter) { @@ -47,6 +53,14 @@ class ExtensionsServiceTestFrontend return &extensions_; } + std::vector<FilePath>* installed() { + return &installed_; + } + + FilePath install_dir() { + return install_dir_; + } + // ExtensionsServiceFrontendInterface virtual MessageLoop* GetMessageLoop() { return &message_loop_; @@ -65,10 +79,43 @@ class ExtensionsServiceTestFrontend std::stable_sort(extensions_.begin(), extensions_.end(), ExtensionsOrder()); } + virtual void InstallExtension(const FilePath& extension_path) { + } + + virtual void OnExtensionInstallError(const std::string& message) { + errors_.push_back(message); + } + + virtual void OnExtensionInstalled(FilePath path) { + installed_.push_back(path); + } + + void TestInstallExtension(const FilePath& path, + ExtensionsServiceBackend* backend, + bool should_succeed) { + ASSERT_TRUE(file_util::PathExists(path)); + EXPECT_EQ(should_succeed, + backend->InstallExtension(path, install_dir_, + scoped_refptr<ExtensionsServiceFrontendInterface>(this))); + message_loop_.RunAllPending(); + if (should_succeed) { + EXPECT_EQ(1u, installed_.size()); + EXPECT_EQ(0u, errors_.size()); + } else { + EXPECT_EQ(0u, installed_.size()); + EXPECT_EQ(1u, errors_.size()); + } + installed_.clear(); + errors_.clear(); + } + + private: MessageLoop message_loop_; ExtensionList extensions_; std::vector<std::string> errors_; + std::vector<FilePath> installed_; + FilePath install_dir_; }; // make the test a PlatformTest to setup autorelease pools properly on mac @@ -76,10 +123,9 @@ typedef PlatformTest ExtensionsServiceTest; // Test loading extensions from the profile directory. TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) { - std::wstring extensions_dir; - ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_dir)); - FilePath extensions_path = FilePath::FromWStringHack(extensions_dir).Append( - FILE_PATH_LITERAL("extensions")); + FilePath extensions_path; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path)); + extensions_path = extensions_path.AppendASCII("extensions"); scoped_refptr<ExtensionsServiceBackend> backend(new ExtensionsServiceBackend); scoped_refptr<ExtensionsServiceTestFrontend> frontend( @@ -108,11 +154,11 @@ TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) { EXPECT_EQ(2u, scripts[0].matches.size()); EXPECT_EQ("http://*.google.com/*", scripts[0].matches[0]); EXPECT_EQ("https://*.google.com/*", scripts[0].matches[1]); - EXPECT_EQ(extension->path().Append(FILE_PATH_LITERAL("script1.js")).value(), + EXPECT_EQ(extension->path().AppendASCII("script1.js").value(), scripts[0].path.value()); EXPECT_EQ(1u, scripts[1].matches.size()); EXPECT_EQ("http://*.yahoo.com/*", scripts[1].matches[0]); - EXPECT_EQ(extension->path().Append(FILE_PATH_LITERAL("script2.js")).value(), + EXPECT_EQ(extension->path().AppendASCII("script2.js").value(), scripts[1].path.value()); EXPECT_EQ(std::string("com.google.myextension2"), @@ -123,3 +169,43 @@ TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) { frontend->extensions()->at(1)->description()); ASSERT_EQ(0u, frontend->extensions()->at(1)->user_scripts().size()); }; + +// Test installing extensions. +TEST_F(ExtensionsServiceTest, InstallExtension) { + FilePath extensions_path; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path)); + extensions_path = extensions_path.AppendASCII("extensions"); + + scoped_refptr<ExtensionsServiceBackend> backend(new ExtensionsServiceBackend); + scoped_refptr<ExtensionsServiceTestFrontend> frontend( + new ExtensionsServiceTestFrontend); + + FilePath path = extensions_path.AppendASCII("good.crx"); + + // A simple extension that should install without error. + frontend->TestInstallExtension(path, backend, true); + // TODO(erikkay): verify the contents of the installed extension. + + // Installing the same extension twice should fail. + frontend->TestInstallExtension(path, backend, false); + + // 0-length extension file. + path = extensions_path.AppendASCII("not_an_extension.crx"); + frontend->TestInstallExtension(path, backend, false); + + // Bad magic number. + path = extensions_path.AppendASCII("bad_magic.crx"); + frontend->TestInstallExtension(path, backend, false); + + // Poorly formed JSON. + path = extensions_path.AppendASCII("bad_json.crx"); + frontend->TestInstallExtension(path, backend, false); + + // Incorrect zip hash. + path = extensions_path.AppendASCII("bad_hash.crx"); + frontend->TestInstallExtension(path, backend, false); + + // TODO(erikkay): add more tests for many of the failure cases. + // TODO(erikkay): add tests for upgrade cases. +} + diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc index a156748..f2e0c68 100644 --- a/chrome/common/chrome_switches.cc +++ b/chrome/common/chrome_switches.cc @@ -340,6 +340,10 @@ const wchar_t kEnableUserScripts[] = L"enable-user-scripts"; // Enable extensions. const wchar_t kEnableExtensions[] = L"enable-extensions"; +// Install the extension specified in the argument. This is for MIME type +// handling so that users can double-click on an extension. +const wchar_t kInstallExtension[] = L"install-extension"; + // Causes the browser to launch directly in incognito mode. const wchar_t kIncognito[] = L"incognito"; diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h index 65a7e00..046c690 100644 --- a/chrome/common/chrome_switches.h +++ b/chrome/common/chrome_switches.h @@ -130,6 +130,7 @@ extern const wchar_t kSdchFilter[]; extern const wchar_t kEnableUserScripts[]; extern const wchar_t kEnableExtensions[]; +extern const wchar_t kInstallExtension[]; extern const wchar_t kIncognito[]; extern const wchar_t kUseOldSafeBrowsing[]; diff --git a/chrome/common/notification_types.h b/chrome/common/notification_types.h index e6e2e2e..c07a3aa 100644 --- a/chrome/common/notification_types.h +++ b/chrome/common/notification_types.h @@ -511,6 +511,9 @@ enum NotificationType { // Sent when new extensions are loaded. The details are an ExtensionList*. NOTIFY_EXTENSIONS_LOADED, + // Sent when new extensions are installed. The details are a FilePath. + NOTIFY_EXTENSION_INSTALLED, + // Count (must be last) ------------------------------------------------------ // Used to determine the number of notification types. Not valid as // a type parameter when registering for or posting notifications. diff --git a/chrome/test/data/extensions/bad_hash.crx b/chrome/test/data/extensions/bad_hash.crx Binary files differnew file mode 100644 index 0000000..9959e9c --- /dev/null +++ b/chrome/test/data/extensions/bad_hash.crx diff --git a/chrome/test/data/extensions/bad_json.crx b/chrome/test/data/extensions/bad_json.crx Binary files differnew file mode 100644 index 0000000..cc15c0e --- /dev/null +++ b/chrome/test/data/extensions/bad_json.crx diff --git a/chrome/test/data/extensions/bad_magic.crx b/chrome/test/data/extensions/bad_magic.crx Binary files differnew file mode 100644 index 0000000..4603e00 --- /dev/null +++ b/chrome/test/data/extensions/bad_magic.crx diff --git a/chrome/test/data/extensions/extension1/manifest b/chrome/test/data/extensions/extension1/manifest.json index a7b3d74..1f5e362 100755 --- a/chrome/test/data/extensions/extension1/manifest +++ b/chrome/test/data/extensions/extension1/manifest.json @@ -1,7 +1,7 @@ {
"format_version": 1,
"id": "com.google.myextension1",
- "version": "1.0",
+ "version": "1.0.0.0",
"name": "My extension 1",
"description": "The first extension that I made.",
"user_scripts": [
diff --git a/chrome/test/data/extensions/extension2/manifest b/chrome/test/data/extensions/extension2/manifest.json index 0f58ffd..4e157c4 100755 --- a/chrome/test/data/extensions/extension2/manifest +++ b/chrome/test/data/extensions/extension2/manifest.json @@ -1,6 +1,6 @@ {
"format_version": 1,
"id": "com.google.myextension2",
- "version": "1.0",
+ "version": "1.0.0.0",
"name": "My extension 2"
}
diff --git a/chrome/test/data/extensions/good.crx b/chrome/test/data/extensions/good.crx Binary files differnew file mode 100644 index 0000000..c2108e2 --- /dev/null +++ b/chrome/test/data/extensions/good.crx diff --git a/chrome/test/data/extensions/not_an_extension.crx b/chrome/test/data/extensions/not_an_extension.crx new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/chrome/test/data/extensions/not_an_extension.crx diff --git a/chrome/tools/extensions/chromium_extension.py b/chrome/tools/extensions/chromium_extension.py index 1151652..de5d3bf 100755 --- a/chrome/tools/extensions/chromium_extension.py +++ b/chrome/tools/extensions/chromium_extension.py @@ -5,17 +5,22 @@ # chromium_extension.py +import array +import hashlib import logging import optparse import os import re import shutil +import simplejson as json import sys import zipfile ignore_dirs = [".svn", "CVS"] ignore_files = [re.compile(".*~")] +MANIFEST_FILENAME = "manifest.json" + class ExtensionDir: def __init__(self, path): self._root = os.path.abspath(path) @@ -34,8 +39,8 @@ class ExtensionDir: self._files.append(os.path.join(root, f)) def validate(self): - if os.path.join(self._root, "manifest") not in self._files: - logging.error("package is missing a valid manifest") + if os.path.join(self._root, MANIFEST_FILENAME) not in self._files: + logging.error("package is missing a valid %s file" % MANIFEST_FILENAME) return False return True @@ -43,21 +48,61 @@ class ExtensionDir: if not self.validate(): return False try: - if os.path.exists(path): - os.remove(path) - shutil.copy(os.path.join(self._root, "manifest"), path) - # This is a bit odd - we're actually appending a new zip file to the end - # of the manifest. Believe it or not, this is actually an explicit - # feature of the zipfile package, and most zip utilities (this library - # and three others I tried) can still read the underlying zip file. - zip = zipfile.ZipFile(path, "a") + f = open(os.path.join(self._root, MANIFEST_FILENAME)) + manifest = json.load(f) + f.close() + + zip_path = path + ".zip" + if os.path.exists(zip_path): + os.remove(zip_path) + zip = zipfile.ZipFile(zip_path, "w") (root, dir) = os.path.split(self._root) - root_len = len(root) + root_len = len(self._root) for file in self._files: arcname = file[root_len+1:] logging.debug("%s: %s" % (arcname, file)) zip.write(file, arcname) zip.close() + + zip = open(zip_path, mode="rb") + hash = hashlib.sha256() + while True: + buf = zip.read(32 * 1024) + if not len(buf): + break + hash.update(buf) + zip.close() + + manifest["zip_hash"] = hash.hexdigest() + + # This is a bit odd - we're actually appending a new zip file to the end + # of the manifest. Believe it or not, this is actually an explicit + # feature of the zip format, and many zip utilities (this library + # and three others I tried) can still read the underlying zip file. + if os.path.exists(path): + os.remove(path) + out = open(path, "wb") + out.write("Cr24") # Extension file magic number + # The rest of the header is currently made up of three ints: + # version, header size, manifest size + header = array.array("l") + header.append(1) # version + header.append(16) # header size + manifest_json = json.dumps(manifest); + header.append(len(manifest_json)) # manifest size + header.tofile(out) + out.write(manifest_json); + zip = open(zip_path, "rb") + while True: + buf = zip.read(32 * 1024) + if not len(buf): + break + out.write(buf) + zip.close() + out.close() + + os.remove(zip_path) + logging.info("created extension package %s" % path) except IOError, (errno, strerror): logging.error("error creating extension %s (%d, %s)" % (path, errno, |