| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "extensions/browser/sandboxed_unpacker.h" | 5 #include "extensions/browser/sandboxed_unpacker.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 #include <stdint.h> | 8 #include <stdint.h> |
| 9 | 9 |
| 10 #include <set> | 10 #include <set> |
| 11 #include <tuple> | 11 #include <tuple> |
| 12 | 12 |
| 13 #include "base/bind.h" | 13 #include "base/bind.h" |
| 14 #include "base/command_line.h" | 14 #include "base/command_line.h" |
| 15 #include "base/files/file_util.h" | 15 #include "base/files/file_util.h" |
| 16 #include "base/json/json_string_value_serializer.h" | 16 #include "base/json/json_string_value_serializer.h" |
| 17 #include "base/message_loop/message_loop.h" | |
| 18 #include "base/metrics/histogram_macros.h" | 17 #include "base/metrics/histogram_macros.h" |
| 19 #include "base/numerics/safe_conversions.h" | |
| 20 #include "base/path_service.h" | 18 #include "base/path_service.h" |
| 21 #include "base/sequenced_task_runner.h" | 19 #include "base/sequenced_task_runner.h" |
| 22 #include "base/strings/string_number_conversions.h" | |
| 23 #include "base/strings/utf_string_conversions.h" | 20 #include "base/strings/utf_string_conversions.h" |
| 24 #include "base/threading/sequenced_worker_pool.h" | 21 #include "base/threading/sequenced_worker_pool.h" |
| 25 #include "build/build_config.h" | 22 #include "build/build_config.h" |
| 26 #include "components/crx_file/crx_file.h" | 23 #include "components/crx_file/crx_file.h" |
| 27 #include "content/public/browser/browser_thread.h" | 24 #include "content/public/browser/browser_thread.h" |
| 28 #include "content/public/browser/utility_process_host.h" | |
| 29 #include "content/public/common/common_param_traits.h" | |
| 30 #include "extensions/common/constants.h" | 25 #include "extensions/common/constants.h" |
| 31 #include "extensions/common/extension.h" | 26 #include "extensions/common/extension.h" |
| 32 #include "extensions/common/extension_l10n_util.h" | 27 #include "extensions/common/extension_l10n_util.h" |
| 33 #include "extensions/common/extension_utility_messages.h" | 28 #include "extensions/common/extension_unpacker.mojom.h" |
| 29 #include "extensions/common/extension_utility_types.h" |
| 34 #include "extensions/common/extensions_client.h" | 30 #include "extensions/common/extensions_client.h" |
| 35 #include "extensions/common/file_util.h" | 31 #include "extensions/common/file_util.h" |
| 36 #include "extensions/common/manifest_constants.h" | 32 #include "extensions/common/manifest_constants.h" |
| 37 #include "extensions/common/manifest_handlers/icons_handler.h" | 33 #include "extensions/common/manifest_handlers/icons_handler.h" |
| 38 #include "extensions/common/switches.h" | 34 #include "extensions/common/switches.h" |
| 39 #include "extensions/strings/grit/extensions_strings.h" | 35 #include "extensions/strings/grit/extensions_strings.h" |
| 40 #include "third_party/skia/include/core/SkBitmap.h" | 36 #include "third_party/skia/include/core/SkBitmap.h" |
| 41 #include "ui/base/l10n/l10n_util.h" | 37 #include "ui/base/l10n/l10n_util.h" |
| 42 #include "ui/gfx/codec/png_codec.h" | 38 #include "ui/gfx/codec/png_codec.h" |
| 43 | 39 |
| 44 using base::ASCIIToUTF16; | 40 using base::ASCIIToUTF16; |
| 45 using content::BrowserThread; | 41 using content::BrowserThread; |
| 46 using content::UtilityProcessHost; | |
| 47 using crx_file::CrxFile; | 42 using crx_file::CrxFile; |
| 48 | 43 |
| 49 // The following macro makes histograms that record the length of paths | 44 // The following macro makes histograms that record the length of paths |
| 50 // in this file much easier to read. | 45 // in this file much easier to read. |
| 51 // Windows has a short max path length. If the path length to a | 46 // Windows has a short max path length. If the path length to a |
| 52 // file being unpacked from a CRX exceeds the max length, we might | 47 // file being unpacked from a CRX exceeds the max length, we might |
| 53 // fail to install. To see if this is happening, see how long the | 48 // fail to install. To see if this is happening, see how long the |
| 54 // path to the temp unpack directory is. See crbug.com/69693 . | 49 // path to the temp unpack directory is. See crbug.com/69693 . |
| 55 #define PATH_LENGTH_HISTOGRAM(name, path) \ | 50 #define PATH_LENGTH_HISTOGRAM(name, path) \ |
| 56 UMA_HISTOGRAM_CUSTOM_COUNTS(name, path.value().length(), 1, 500, 100) | 51 UMA_HISTOGRAM_CUSTOM_COUNTS(name, path.value().length(), 1, 500, 100) |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 122 // and uses NormalizeFilePath to check if the path is junction free. | 117 // and uses NormalizeFilePath to check if the path is junction free. |
| 123 bool VerifyJunctionFreeLocation(base::FilePath* temp_dir) { | 118 bool VerifyJunctionFreeLocation(base::FilePath* temp_dir) { |
| 124 if (temp_dir->empty()) | 119 if (temp_dir->empty()) |
| 125 return false; | 120 return false; |
| 126 | 121 |
| 127 base::FilePath temp_file; | 122 base::FilePath temp_file; |
| 128 if (!base::CreateTemporaryFileInDir(*temp_dir, &temp_file)) { | 123 if (!base::CreateTemporaryFileInDir(*temp_dir, &temp_file)) { |
| 129 LOG(ERROR) << temp_dir->value() << " is not writable"; | 124 LOG(ERROR) << temp_dir->value() << " is not writable"; |
| 130 return false; | 125 return false; |
| 131 } | 126 } |
| 127 |
| 132 // NormalizeFilePath requires a non-empty file, so write some data. | 128 // NormalizeFilePath requires a non-empty file, so write some data. |
| 133 // If you change the exit points of this function please make sure all | 129 // If you change the exit points of this function please make sure all |
| 134 // exit points delete this temp file! | 130 // exit points delete this temp file! |
| 135 if (base::WriteFile(temp_file, ".", 1) != 1) | 131 if (base::WriteFile(temp_file, ".", 1) != 1) { |
| 132 base::DeleteFile(temp_file, false); |
| 136 return false; | 133 return false; |
| 134 } |
| 137 | 135 |
| 138 base::FilePath normalized_temp_file; | 136 base::FilePath normalized_temp_file; |
| 139 bool normalized = base::NormalizeFilePath(temp_file, &normalized_temp_file); | 137 bool normalized = base::NormalizeFilePath(temp_file, &normalized_temp_file); |
| 140 if (!normalized) { | 138 if (!normalized) { |
| 141 // If |temp_file| contains a link, the sandbox will block al file system | 139 // If |temp_file| contains a link, the sandbox will block all file |
| 142 // operations, and the install will fail. | 140 // system operations, and the install will fail. |
| 143 LOG(ERROR) << temp_dir->value() << " seem to be on remote drive."; | 141 LOG(ERROR) << temp_dir->value() << " seem to be on remote drive."; |
| 144 } else { | 142 } else { |
| 145 *temp_dir = normalized_temp_file.DirName(); | 143 *temp_dir = normalized_temp_file.DirName(); |
| 146 } | 144 } |
| 145 |
| 147 // Clean up the temp file. | 146 // Clean up the temp file. |
| 148 base::DeleteFile(temp_file, false); | 147 base::DeleteFile(temp_file, false); |
| 149 | 148 |
| 150 return normalized; | 149 return normalized; |
| 151 } | 150 } |
| 152 | 151 |
| 153 // This function tries to find a location for unpacking the extension archive | 152 // This function tries to find a location for unpacking the extension archive |
| 154 // that is writable and does not lie on a shared drive so that the sandboxed | 153 // that is writable and does not lie on a shared drive so that the sandboxed |
| 155 // unpacking process can write there. If no such location exists we can not | 154 // unpacking process can write there. If no such location exists we can not |
| 156 // proceed and should fail. | 155 // proceed and should fail. |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 217 } | 216 } |
| 218 | 217 |
| 219 SandboxedUnpacker::SandboxedUnpacker( | 218 SandboxedUnpacker::SandboxedUnpacker( |
| 220 Manifest::Location location, | 219 Manifest::Location location, |
| 221 int creation_flags, | 220 int creation_flags, |
| 222 const base::FilePath& extensions_dir, | 221 const base::FilePath& extensions_dir, |
| 223 const scoped_refptr<base::SequencedTaskRunner>& unpacker_io_task_runner, | 222 const scoped_refptr<base::SequencedTaskRunner>& unpacker_io_task_runner, |
| 224 SandboxedUnpackerClient* client) | 223 SandboxedUnpackerClient* client) |
| 225 : client_(client), | 224 : client_(client), |
| 226 extensions_dir_(extensions_dir), | 225 extensions_dir_(extensions_dir), |
| 227 got_response_(false), | |
| 228 location_(location), | 226 location_(location), |
| 229 creation_flags_(creation_flags), | 227 creation_flags_(creation_flags), |
| 230 unpacker_io_task_runner_(unpacker_io_task_runner), | 228 unpacker_io_task_runner_(unpacker_io_task_runner) { |
| 231 utility_wrapper_(new UtilityHostWrapper) { | |
| 232 // Tracking for crbug.com/692069. The location must be valid. If it's invalid, | 229 // Tracking for crbug.com/692069. The location must be valid. If it's invalid, |
| 233 // the utility process kills itself for a bad IPC. | 230 // the utility process kills itself for a bad IPC. |
| 234 CHECK_GT(location, Manifest::INVALID_LOCATION); | 231 CHECK_GT(location, Manifest::INVALID_LOCATION); |
| 235 CHECK_LT(location, Manifest::NUM_LOCATIONS); | 232 CHECK_LT(location, Manifest::NUM_LOCATIONS); |
| 236 } | 233 } |
| 237 | 234 |
| 238 bool SandboxedUnpacker::CreateTempDirectory() { | 235 bool SandboxedUnpacker::CreateTempDirectory() { |
| 239 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); | 236 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); |
| 240 | 237 |
| 241 base::FilePath temp_dir; | 238 base::FilePath temp_dir; |
| (...skipping 10 matching lines...) Expand all Loading... |
| 252 l10n_util::GetStringFUTF16( | 249 l10n_util::GetStringFUTF16( |
| 253 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 250 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 254 ASCIIToUTF16("COULD_NOT_CREATE_TEMP_DIRECTORY"))); | 251 ASCIIToUTF16("COULD_NOT_CREATE_TEMP_DIRECTORY"))); |
| 255 return false; | 252 return false; |
| 256 } | 253 } |
| 257 | 254 |
| 258 return true; | 255 return true; |
| 259 } | 256 } |
| 260 | 257 |
| 261 void SandboxedUnpacker::StartWithCrx(const CRXFileInfo& crx_info) { | 258 void SandboxedUnpacker::StartWithCrx(const CRXFileInfo& crx_info) { |
| 262 // We assume that we are started on the thread that the client wants us to do | 259 // We assume that we are started on the thread that the client wants us |
| 263 // file IO on. | 260 // to do file IO on. |
| 264 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); | 261 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); |
| 265 | 262 |
| 266 crx_unpack_start_time_ = base::TimeTicks::Now(); | 263 crx_unpack_start_time_ = base::TimeTicks::Now(); |
| 267 std::string expected_hash; | 264 std::string expected_hash; |
| 268 if (!crx_info.expected_hash.empty() && | 265 if (!crx_info.expected_hash.empty() && |
| 269 base::CommandLine::ForCurrentProcess()->HasSwitch( | 266 base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 270 extensions::switches::kEnableCrxHashCheck)) { | 267 extensions::switches::kEnableCrxHashCheck)) { |
| 271 expected_hash = base::ToLowerASCII(crx_info.expected_hash); | 268 expected_hash = base::ToLowerASCII(crx_info.expected_hash); |
| 272 } | 269 } |
| 273 | 270 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 307 // will cause file system access outside the sandbox path, and the sandbox | 304 // will cause file system access outside the sandbox path, and the sandbox |
| 308 // will deny the operation. | 305 // will deny the operation. |
| 309 base::FilePath link_free_crx_path; | 306 base::FilePath link_free_crx_path; |
| 310 if (!base::NormalizeFilePath(temp_crx_path, &link_free_crx_path)) { | 307 if (!base::NormalizeFilePath(temp_crx_path, &link_free_crx_path)) { |
| 311 LOG(ERROR) << "Could not get the normalized path of " | 308 LOG(ERROR) << "Could not get the normalized path of " |
| 312 << temp_crx_path.value(); | 309 << temp_crx_path.value(); |
| 313 ReportFailure(COULD_NOT_GET_SANDBOX_FRIENDLY_PATH, | 310 ReportFailure(COULD_NOT_GET_SANDBOX_FRIENDLY_PATH, |
| 314 l10n_util::GetStringUTF16(IDS_EXTENSION_UNPACK_FAILED)); | 311 l10n_util::GetStringUTF16(IDS_EXTENSION_UNPACK_FAILED)); |
| 315 return; | 312 return; |
| 316 } | 313 } |
| 314 |
| 317 PATH_LENGTH_HISTOGRAM("Extensions.SandboxUnpackLinkFreeCrxPathLength", | 315 PATH_LENGTH_HISTOGRAM("Extensions.SandboxUnpackLinkFreeCrxPathLength", |
| 318 link_free_crx_path); | 316 link_free_crx_path); |
| 319 | 317 |
| 320 BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, | 318 BrowserThread::PostTask( |
| 321 base::Bind(&SandboxedUnpacker::StartUnzipOnIOThread, | 319 BrowserThread::IO, FROM_HERE, |
| 322 this, link_free_crx_path)); | 320 base::Bind(&SandboxedUnpacker::Unzip, this, link_free_crx_path)); |
| 323 } | 321 } |
| 324 | 322 |
| 325 void SandboxedUnpacker::StartWithDirectory(const std::string& extension_id, | 323 void SandboxedUnpacker::StartWithDirectory(const std::string& extension_id, |
| 326 const std::string& public_key, | 324 const std::string& public_key, |
| 327 const base::FilePath& directory) { | 325 const base::FilePath& directory) { |
| 328 extension_id_ = extension_id; | 326 extension_id_ = extension_id; |
| 329 public_key_ = public_key; | 327 public_key_ = public_key; |
| 330 if (!CreateTempDirectory()) | 328 if (!CreateTempDirectory()) |
| 331 return; // ReportFailure() already called. | 329 return; // ReportFailure() already called. |
| 332 | 330 |
| 333 extension_root_ = temp_dir_.GetPath().AppendASCII(kTempExtensionName); | 331 extension_root_ = temp_dir_.GetPath().AppendASCII(kTempExtensionName); |
| 334 | 332 |
| 335 if (!base::Move(directory, extension_root_)) { | 333 if (!base::Move(directory, extension_root_)) { |
| 336 LOG(ERROR) << "Could not move " << directory.value() << " to " | 334 LOG(ERROR) << "Could not move " << directory.value() << " to " |
| 337 << extension_root_.value(); | 335 << extension_root_.value(); |
| 338 ReportFailure( | 336 ReportFailure( |
| 339 DIRECTORY_MOVE_FAILED, | 337 DIRECTORY_MOVE_FAILED, |
| 340 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 338 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 341 ASCIIToUTF16("DIRECTORY_MOVE_FAILED"))); | 339 ASCIIToUTF16("DIRECTORY_MOVE_FAILED"))); |
| 342 return; | 340 return; |
| 343 } | 341 } |
| 344 | 342 |
| 345 BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, | 343 BrowserThread::PostTask( |
| 346 base::Bind(&SandboxedUnpacker::StartUnpackOnIOThread, | 344 BrowserThread::IO, FROM_HERE, |
| 347 this, extension_root_)); | 345 base::Bind(&SandboxedUnpacker::Unpack, this, extension_root_)); |
| 348 } | 346 } |
| 349 | 347 |
| 350 SandboxedUnpacker::~SandboxedUnpacker() { | 348 SandboxedUnpacker::~SandboxedUnpacker() { |
| 351 // To avoid blocking shutdown, don't delete temporary directory here if it | 349 // To avoid blocking shutdown, don't delete temporary directory here if it |
| 352 // hasn't been cleaned up or passed on to another owner yet. | 350 // hasn't been cleaned up or passed on to another owner yet. |
| 353 temp_dir_.Take(); | 351 temp_dir_.Take(); |
| 354 } | 352 } |
| 355 | 353 |
| 356 bool SandboxedUnpacker::OnMessageReceived(const IPC::Message& message) { | 354 void SandboxedUnpacker::StartUtilityProcessIfNeeded() { |
| 357 bool handled = true; | 355 DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| 358 IPC_BEGIN_MESSAGE_MAP(SandboxedUnpacker, message) | 356 |
| 359 IPC_MESSAGE_HANDLER(ExtensionUtilityHostMsg_UnzipToDir_Succeeded, | 357 if (utility_process_mojo_client_) |
| 360 OnUnzipToDirSucceeded) | 358 return; |
| 361 IPC_MESSAGE_HANDLER(ExtensionUtilityHostMsg_UnzipToDir_Failed, | 359 |
| 362 OnUnzipToDirFailed) | 360 utility_process_mojo_client_ = base::MakeUnique< |
| 363 IPC_MESSAGE_HANDLER(ExtensionUtilityHostMsg_UnpackExtension_Succeeded, | 361 content::UtilityProcessMojoClient<mojom::ExtensionUnpacker>>( |
| 364 OnUnpackExtensionSucceeded) | 362 l10n_util::GetStringUTF16(IDS_UTILITY_PROCESS_EXTENSION_UNPACKER_NAME)); |
| 365 IPC_MESSAGE_HANDLER(ExtensionUtilityHostMsg_UnpackExtension_Failed, | 363 utility_process_mojo_client_->set_error_callback( |
| 366 OnUnpackExtensionFailed) | 364 base::Bind(&SandboxedUnpacker::UtilityProcessCrashed, this)); |
| 367 IPC_MESSAGE_UNHANDLED(handled = false) | 365 |
| 368 IPC_END_MESSAGE_MAP() | 366 utility_process_mojo_client_->set_exposed_directory(temp_dir_.GetPath()); |
| 369 return handled; | 367 |
| 368 utility_process_mojo_client_->Start(); |
| 370 } | 369 } |
| 371 | 370 |
| 372 void SandboxedUnpacker::OnProcessCrashed(int exit_code) { | 371 void SandboxedUnpacker::UtilityProcessCrashed() { |
| 373 // Don't report crashes if they happen after we got a response. | 372 DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| 374 if (got_response_) | |
| 375 return; | |
| 376 | 373 |
| 377 // Utility process crashed while trying to install. | 374 utility_process_mojo_client_.reset(); |
| 375 |
| 378 ReportFailure( | 376 ReportFailure( |
| 379 UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL, | 377 UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL, |
| 380 l10n_util::GetStringFUTF16( | 378 l10n_util::GetStringFUTF16( |
| 381 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 379 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 382 ASCIIToUTF16("UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL")) + | 380 ASCIIToUTF16("UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL")) + |
| 383 ASCIIToUTF16(". ") + | 381 ASCIIToUTF16(". ") + |
| 384 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALL_PROCESS_CRASHED)); | 382 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALL_PROCESS_CRASHED)); |
| 385 } | 383 } |
| 386 | 384 |
| 387 void SandboxedUnpacker::StartUnzipOnIOThread(const base::FilePath& crx_path) { | 385 void SandboxedUnpacker::Unzip(const base::FilePath& crx_path) { |
| 388 if (!utility_wrapper_->StartIfNeeded(temp_dir_.GetPath(), this, | 386 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 389 unpacker_io_task_runner_)) { | 387 |
| 390 ReportFailure( | 388 StartUtilityProcessIfNeeded(); |
| 391 COULD_NOT_START_UTILITY_PROCESS, | 389 |
| 392 l10n_util::GetStringFUTF16( | |
| 393 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | |
| 394 FailureReasonToString16(COULD_NOT_START_UTILITY_PROCESS))); | |
| 395 return; | |
| 396 } | |
| 397 DCHECK(crx_path.DirName() == temp_dir_.GetPath()); | 390 DCHECK(crx_path.DirName() == temp_dir_.GetPath()); |
| 398 base::FilePath unzipped_dir = | 391 base::FilePath unzipped_dir = |
| 399 crx_path.DirName().AppendASCII(kTempExtensionName); | 392 crx_path.DirName().AppendASCII(kTempExtensionName); |
| 400 utility_wrapper_->host()->Send( | 393 |
| 401 new ExtensionUtilityMsg_UnzipToDir(crx_path, unzipped_dir)); | 394 utility_process_mojo_client_->service()->Unzip( |
| 395 crx_path, unzipped_dir, |
| 396 base::Bind(&SandboxedUnpacker::UnzipDone, this, unzipped_dir)); |
| 402 } | 397 } |
| 403 | 398 |
| 404 void SandboxedUnpacker::StartUnpackOnIOThread( | 399 void SandboxedUnpacker::UnzipDone(const base::FilePath& directory, |
| 405 const base::FilePath& directory_path) { | 400 bool success) { |
| 406 if (!utility_wrapper_->StartIfNeeded(temp_dir_.GetPath(), this, | 401 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 407 unpacker_io_task_runner_)) { | 402 |
| 408 ReportFailure( | 403 if (!success) { |
| 409 COULD_NOT_START_UTILITY_PROCESS, | 404 utility_process_mojo_client_.reset(); |
| 410 l10n_util::GetStringFUTF16( | 405 ReportFailure(UNZIP_FAILED, |
| 411 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 406 l10n_util::GetStringUTF16(IDS_EXTENSION_PACKAGE_UNZIP_ERROR)); |
| 412 FailureReasonToString16(COULD_NOT_START_UTILITY_PROCESS))); | |
| 413 return; | 407 return; |
| 414 } | 408 } |
| 415 DCHECK(directory_path.DirName() == temp_dir_.GetPath()); | 409 |
| 416 utility_wrapper_->host()->Send(new ExtensionUtilityMsg_UnpackExtension( | 410 BrowserThread::PostTask( |
| 417 directory_path, extension_id_, location_, creation_flags_)); | 411 BrowserThread::IO, FROM_HERE, |
| 412 base::Bind(&SandboxedUnpacker::Unpack, this, directory)); |
| 418 } | 413 } |
| 419 | 414 |
| 420 void SandboxedUnpacker::OnUnzipToDirSucceeded(const base::FilePath& directory) { | 415 void SandboxedUnpacker::Unpack(const base::FilePath& directory) { |
| 421 BrowserThread::PostTask( | 416 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 422 BrowserThread::IO, FROM_HERE, | 417 |
| 423 base::Bind(&SandboxedUnpacker::StartUnpackOnIOThread, this, directory)); | 418 StartUtilityProcessIfNeeded(); |
| 419 |
| 420 DCHECK(directory.DirName() == temp_dir_.GetPath()); |
| 421 |
| 422 utility_process_mojo_client_->service()->Unpack( |
| 423 directory, extension_id_, location_, creation_flags_, |
| 424 base::Bind(&SandboxedUnpacker::UnpackDone, this)); |
| 424 } | 425 } |
| 425 | 426 |
| 426 void SandboxedUnpacker::OnUnzipToDirFailed(const std::string& error) { | 427 void SandboxedUnpacker::UnpackDone( |
| 427 got_response_ = true; | 428 const base::string16& error, |
| 428 utility_wrapper_ = nullptr; | 429 std::unique_ptr<base::DictionaryValue> manifest) { |
| 429 ReportFailure(UNZIP_FAILED, | 430 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 430 l10n_util::GetStringUTF16(IDS_EXTENSION_PACKAGE_UNZIP_ERROR)); | 431 |
| 432 utility_process_mojo_client_.reset(); |
| 433 |
| 434 if (!error.empty()) { |
| 435 unpacker_io_task_runner_->PostTask( |
| 436 FROM_HERE, |
| 437 base::Bind(&SandboxedUnpacker::UnpackExtensionFailed, this, error)); |
| 438 return; |
| 439 } |
| 440 |
| 441 unpacker_io_task_runner_->PostTask( |
| 442 FROM_HERE, base::Bind(&SandboxedUnpacker::UnpackExtensionSucceeded, this, |
| 443 base::Passed(&manifest))); |
| 431 } | 444 } |
| 432 | 445 |
| 433 void SandboxedUnpacker::OnUnpackExtensionSucceeded( | 446 void SandboxedUnpacker::UnpackExtensionSucceeded( |
| 434 const base::DictionaryValue& manifest) { | 447 std::unique_ptr<base::DictionaryValue> manifest) { |
| 435 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); | 448 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); |
| 436 got_response_ = true; | |
| 437 utility_wrapper_ = nullptr; | |
| 438 | 449 |
| 439 std::unique_ptr<base::DictionaryValue> final_manifest( | 450 std::unique_ptr<base::DictionaryValue> final_manifest( |
| 440 RewriteManifestFile(manifest)); | 451 RewriteManifestFile(*manifest)); |
| 441 if (!final_manifest) | 452 if (!final_manifest) |
| 442 return; | 453 return; |
| 443 | 454 |
| 444 // Create an extension object that refers to the temporary location the | 455 // Create an extension object that refers to the temporary location the |
| 445 // extension was unpacked to. We use this until the extension is finally | 456 // extension was unpacked to. We use this until the extension is finally |
| 446 // installed. For example, the install UI shows images from inside the | 457 // installed. For example, the install UI shows images from inside the |
| 447 // extension. | 458 // extension. |
| 448 | 459 |
| 449 // Localize manifest now, so confirm UI gets correct extension name. | 460 // Localize manifest now, so confirm UI gets correct extension name. |
| 450 | 461 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 470 return; | 481 return; |
| 471 } | 482 } |
| 472 | 483 |
| 473 SkBitmap install_icon; | 484 SkBitmap install_icon; |
| 474 if (!RewriteImageFiles(&install_icon)) | 485 if (!RewriteImageFiles(&install_icon)) |
| 475 return; | 486 return; |
| 476 | 487 |
| 477 if (!RewriteCatalogFiles()) | 488 if (!RewriteCatalogFiles()) |
| 478 return; | 489 return; |
| 479 | 490 |
| 480 ReportSuccess(manifest, install_icon); | 491 ReportSuccess(std::move(manifest), install_icon); |
| 481 } | 492 } |
| 482 | 493 |
| 483 void SandboxedUnpacker::OnUnpackExtensionFailed(const base::string16& error) { | 494 void SandboxedUnpacker::UnpackExtensionFailed(const base::string16& error) { |
| 484 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); | 495 CHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); |
| 485 got_response_ = true; | 496 |
| 486 utility_wrapper_ = nullptr; | |
| 487 ReportFailure( | 497 ReportFailure( |
| 488 UNPACKER_CLIENT_FAILED, | 498 UNPACKER_CLIENT_FAILED, |
| 489 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_MESSAGE, error)); | 499 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_MESSAGE, error)); |
| 490 } | 500 } |
| 491 | 501 |
| 492 base::string16 SandboxedUnpacker::FailureReasonToString16( | 502 base::string16 SandboxedUnpacker::FailureReasonToString16( |
| 493 FailureReason reason) { | 503 FailureReason reason) { |
| 494 switch (reason) { | 504 switch (reason) { |
| 495 case COULD_NOT_GET_TEMP_DIRECTORY: | 505 case COULD_NOT_GET_TEMP_DIRECTORY: |
| 496 return ASCIIToUTF16("COULD_NOT_GET_TEMP_DIRECTORY"); | 506 return ASCIIToUTF16("COULD_NOT_GET_TEMP_DIRECTORY"); |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 565 case ERROR_SAVING_CATALOG: | 575 case ERROR_SAVING_CATALOG: |
| 566 return ASCIIToUTF16("ERROR_SAVING_CATALOG"); | 576 return ASCIIToUTF16("ERROR_SAVING_CATALOG"); |
| 567 | 577 |
| 568 case CRX_HASH_VERIFICATION_FAILED: | 578 case CRX_HASH_VERIFICATION_FAILED: |
| 569 return ASCIIToUTF16("CRX_HASH_VERIFICATION_FAILED"); | 579 return ASCIIToUTF16("CRX_HASH_VERIFICATION_FAILED"); |
| 570 | 580 |
| 571 case UNZIP_FAILED: | 581 case UNZIP_FAILED: |
| 572 return ASCIIToUTF16("UNZIP_FAILED"); | 582 return ASCIIToUTF16("UNZIP_FAILED"); |
| 573 case DIRECTORY_MOVE_FAILED: | 583 case DIRECTORY_MOVE_FAILED: |
| 574 return ASCIIToUTF16("DIRECTORY_MOVE_FAILED"); | 584 return ASCIIToUTF16("DIRECTORY_MOVE_FAILED"); |
| 575 case COULD_NOT_START_UTILITY_PROCESS: | |
| 576 return ASCIIToUTF16("COULD_NOT_START_UTILITY_PROCESS"); | |
| 577 | 585 |
| 578 case NUM_FAILURE_REASONS: | 586 case NUM_FAILURE_REASONS: |
| 579 NOTREACHED(); | 587 NOTREACHED(); |
| 580 return base::string16(); | 588 return base::string16(); |
| 581 } | 589 } |
| 590 |
| 582 NOTREACHED(); | 591 NOTREACHED(); |
| 583 return base::string16(); | 592 return base::string16(); |
| 584 } | 593 } |
| 585 | 594 |
| 586 void SandboxedUnpacker::FailWithPackageError(FailureReason reason) { | 595 void SandboxedUnpacker::FailWithPackageError(FailureReason reason) { |
| 587 ReportFailure(reason, | 596 ReportFailure(reason, |
| 588 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_CODE, | 597 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_CODE, |
| 589 FailureReasonToString16(reason))); | 598 FailureReasonToString16(reason))); |
| 590 } | 599 } |
| 591 | 600 |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 636 FailWithPackageError(CRX_SIGNATURE_VERIFICATION_FAILED); | 645 FailWithPackageError(CRX_SIGNATURE_VERIFICATION_FAILED); |
| 637 break; | 646 break; |
| 638 case CrxFile::ValidateError::CRX_HASH_VERIFICATION_FAILED: | 647 case CrxFile::ValidateError::CRX_HASH_VERIFICATION_FAILED: |
| 639 // We should never get this result unless we had specifically asked for | 648 // We should never get this result unless we had specifically asked for |
| 640 // verification of the crx file's hash. | 649 // verification of the crx file's hash. |
| 641 CHECK(!expected_hash.empty()); | 650 CHECK(!expected_hash.empty()); |
| 642 UMA_HISTOGRAM_BOOLEAN("Extensions.SandboxUnpackHashCheck", false); | 651 UMA_HISTOGRAM_BOOLEAN("Extensions.SandboxUnpackHashCheck", false); |
| 643 FailWithPackageError(CRX_HASH_VERIFICATION_FAILED); | 652 FailWithPackageError(CRX_HASH_VERIFICATION_FAILED); |
| 644 break; | 653 break; |
| 645 } | 654 } |
| 655 |
| 646 return false; | 656 return false; |
| 647 } | 657 } |
| 648 | 658 |
| 649 void SandboxedUnpacker::ReportFailure(FailureReason reason, | 659 void SandboxedUnpacker::ReportFailure(FailureReason reason, |
| 650 const base::string16& error) { | 660 const base::string16& error) { |
| 651 utility_wrapper_ = nullptr; | |
| 652 UMA_HISTOGRAM_ENUMERATION("Extensions.SandboxUnpackFailureReason", reason, | 661 UMA_HISTOGRAM_ENUMERATION("Extensions.SandboxUnpackFailureReason", reason, |
| 653 NUM_FAILURE_REASONS); | 662 NUM_FAILURE_REASONS); |
| 654 if (!crx_unpack_start_time_.is_null()) | 663 if (!crx_unpack_start_time_.is_null()) |
| 655 UMA_HISTOGRAM_TIMES("Extensions.SandboxUnpackFailureTime", | 664 UMA_HISTOGRAM_TIMES("Extensions.SandboxUnpackFailureTime", |
| 656 base::TimeTicks::Now() - crx_unpack_start_time_); | 665 base::TimeTicks::Now() - crx_unpack_start_time_); |
| 657 Cleanup(); | 666 Cleanup(); |
| 658 | 667 |
| 659 CrxInstallError error_info(reason == CRX_HASH_VERIFICATION_FAILED | 668 CrxInstallError error_info(reason == CRX_HASH_VERIFICATION_FAILED |
| 660 ? CrxInstallError::ERROR_HASH_MISMATCH | 669 ? CrxInstallError::ERROR_HASH_MISMATCH |
| 661 : CrxInstallError::ERROR_OTHER, | 670 : CrxInstallError::ERROR_OTHER, |
| 662 error); | 671 error); |
| 663 | 672 |
| 664 client_->OnUnpackFailure(error_info); | 673 client_->OnUnpackFailure(error_info); |
| 665 } | 674 } |
| 666 | 675 |
| 667 void SandboxedUnpacker::ReportSuccess( | 676 void SandboxedUnpacker::ReportSuccess( |
| 668 const base::DictionaryValue& original_manifest, | 677 std::unique_ptr<base::DictionaryValue> original_manifest, |
| 669 const SkBitmap& install_icon) { | 678 const SkBitmap& install_icon) { |
| 670 utility_wrapper_ = nullptr; | |
| 671 UMA_HISTOGRAM_COUNTS("Extensions.SandboxUnpackSuccess", 1); | 679 UMA_HISTOGRAM_COUNTS("Extensions.SandboxUnpackSuccess", 1); |
| 672 | 680 |
| 673 if (!crx_unpack_start_time_.is_null()) | 681 if (!crx_unpack_start_time_.is_null()) |
| 674 RecordSuccessfulUnpackTimeHistograms( | 682 RecordSuccessfulUnpackTimeHistograms( |
| 675 crx_path_for_histograms_, | 683 crx_path_for_histograms_, |
| 676 base::TimeTicks::Now() - crx_unpack_start_time_); | 684 base::TimeTicks::Now() - crx_unpack_start_time_); |
| 677 DCHECK(!temp_dir_.GetPath().empty()); | 685 DCHECK(!temp_dir_.GetPath().empty()); |
| 678 | 686 |
| 679 // Client takes ownership of temporary directory and extension. | 687 // Client takes ownership of temporary directory and extension. |
| 688 // TODO(https://crbug.com/699528): we should consider transferring the |
| 689 // ownership of original_manifest to the client as well. |
| 680 client_->OnUnpackSuccess(temp_dir_.Take(), extension_root_, | 690 client_->OnUnpackSuccess(temp_dir_.Take(), extension_root_, |
| 681 &original_manifest, extension_.get(), install_icon); | 691 original_manifest.get(), extension_.get(), |
| 692 install_icon); |
| 682 extension_ = NULL; | 693 extension_ = NULL; |
| 683 } | 694 } |
| 684 | 695 |
| 685 base::DictionaryValue* SandboxedUnpacker::RewriteManifestFile( | 696 base::DictionaryValue* SandboxedUnpacker::RewriteManifestFile( |
| 686 const base::DictionaryValue& manifest) { | 697 const base::DictionaryValue& manifest) { |
| 687 // Add the public key extracted earlier to the parsed manifest and overwrite | 698 // Add the public key extracted earlier to the parsed manifest and overwrite |
| 688 // the original manifest. We do this to ensure the manifest doesn't contain an | 699 // the original manifest. We do this to ensure the manifest doesn't contain an |
| 689 // exploitable bug that could be used to compromise the browser. | 700 // exploitable bug that could be used to compromise the browser. |
| 690 DCHECK(!public_key_.empty()); | 701 DCHECK(!public_key_.empty()); |
| 691 std::unique_ptr<base::DictionaryValue> final_manifest(manifest.DeepCopy()); | 702 std::unique_ptr<base::DictionaryValue> final_manifest(manifest.DeepCopy()); |
| (...skipping 20 matching lines...) Expand all Loading... |
| 712 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 723 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 713 ASCIIToUTF16("ERROR_SAVING_MANIFEST_JSON"))); | 724 ASCIIToUTF16("ERROR_SAVING_MANIFEST_JSON"))); |
| 714 return NULL; | 725 return NULL; |
| 715 } | 726 } |
| 716 | 727 |
| 717 return final_manifest.release(); | 728 return final_manifest.release(); |
| 718 } | 729 } |
| 719 | 730 |
| 720 bool SandboxedUnpacker::RewriteImageFiles(SkBitmap* install_icon) { | 731 bool SandboxedUnpacker::RewriteImageFiles(SkBitmap* install_icon) { |
| 721 DCHECK(!temp_dir_.GetPath().empty()); | 732 DCHECK(!temp_dir_.GetPath().empty()); |
| 733 |
| 722 DecodedImages images; | 734 DecodedImages images; |
| 723 if (!ReadImagesFromFile(temp_dir_.GetPath(), &images)) { | 735 if (!ReadImagesFromFile(temp_dir_.GetPath(), &images)) { |
| 724 // Couldn't read image data from disk. | 736 // Couldn't read image data from disk. |
| 725 ReportFailure(COULD_NOT_READ_IMAGE_DATA_FROM_DISK, | 737 ReportFailure(COULD_NOT_READ_IMAGE_DATA_FROM_DISK, |
| 726 l10n_util::GetStringFUTF16( | 738 l10n_util::GetStringFUTF16( |
| 727 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 739 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 728 ASCIIToUTF16("COULD_NOT_READ_IMAGE_DATA_FROM_DISK"))); | 740 ASCIIToUTF16("COULD_NOT_READ_IMAGE_DATA_FROM_DISK"))); |
| 729 return false; | 741 return false; |
| 730 } | 742 } |
| 731 | 743 |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 787 *install_icon = image; | 799 *install_icon = image; |
| 788 | 800 |
| 789 if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) { | 801 if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) { |
| 790 // Invalid path for bitmap image. | 802 // Invalid path for bitmap image. |
| 791 ReportFailure(INVALID_PATH_FOR_BITMAP_IMAGE, | 803 ReportFailure(INVALID_PATH_FOR_BITMAP_IMAGE, |
| 792 l10n_util::GetStringFUTF16( | 804 l10n_util::GetStringFUTF16( |
| 793 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 805 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 794 ASCIIToUTF16("INVALID_PATH_FOR_BITMAP_IMAGE"))); | 806 ASCIIToUTF16("INVALID_PATH_FOR_BITMAP_IMAGE"))); |
| 795 return false; | 807 return false; |
| 796 } | 808 } |
| 809 |
| 797 base::FilePath path = extension_root_.Append(path_suffix); | 810 base::FilePath path = extension_root_.Append(path_suffix); |
| 798 | 811 |
| 799 std::vector<unsigned char> image_data; | 812 std::vector<unsigned char> image_data; |
| 800 // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even | 813 // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even |
| 801 // though they may originally be .jpg, etc. Figure something out. | 814 // though they may originally be .jpg, etc. Figure something out. |
| 802 // http://code.google.com/p/chromium/issues/detail?id=12459 | 815 // http://code.google.com/p/chromium/issues/detail?id=12459 |
| 803 if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) { | 816 if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) { |
| 804 // Error re-encoding theme image. | 817 // Error re-encoding theme image. |
| 805 ReportFailure(ERROR_RE_ENCODING_THEME_IMAGE, | 818 ReportFailure(ERROR_RE_ENCODING_THEME_IMAGE, |
| 806 l10n_util::GetStringFUTF16( | 819 l10n_util::GetStringFUTF16( |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 853 base::FilePath relative_path = base::FilePath::FromUTF8Unsafe(it.key()); | 866 base::FilePath relative_path = base::FilePath::FromUTF8Unsafe(it.key()); |
| 854 relative_path = relative_path.Append(kMessagesFilename); | 867 relative_path = relative_path.Append(kMessagesFilename); |
| 855 if (relative_path.IsAbsolute() || relative_path.ReferencesParent()) { | 868 if (relative_path.IsAbsolute() || relative_path.ReferencesParent()) { |
| 856 // Invalid path for catalog. | 869 // Invalid path for catalog. |
| 857 ReportFailure( | 870 ReportFailure( |
| 858 INVALID_PATH_FOR_CATALOG, | 871 INVALID_PATH_FOR_CATALOG, |
| 859 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 872 l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| 860 ASCIIToUTF16("INVALID_PATH_FOR_CATALOG"))); | 873 ASCIIToUTF16("INVALID_PATH_FOR_CATALOG"))); |
| 861 return false; | 874 return false; |
| 862 } | 875 } |
| 876 |
| 863 base::FilePath path = extension_root_.Append(relative_path); | 877 base::FilePath path = extension_root_.Append(relative_path); |
| 864 | 878 |
| 865 std::string catalog_json; | 879 std::string catalog_json; |
| 866 JSONStringValueSerializer serializer(&catalog_json); | 880 JSONStringValueSerializer serializer(&catalog_json); |
| 867 serializer.set_pretty_print(true); | 881 serializer.set_pretty_print(true); |
| 868 if (!serializer.Serialize(*catalog)) { | 882 if (!serializer.Serialize(*catalog)) { |
| 869 // Error serializing catalog. | 883 // Error serializing catalog. |
| 870 ReportFailure(ERROR_SERIALIZING_CATALOG, | 884 ReportFailure(ERROR_SERIALIZING_CATALOG, |
| 871 l10n_util::GetStringFUTF16( | 885 l10n_util::GetStringFUTF16( |
| 872 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, | 886 IDS_EXTENSION_PACKAGE_INSTALL_ERROR, |
| (...skipping 18 matching lines...) Expand all Loading... |
| 891 } | 905 } |
| 892 | 906 |
| 893 void SandboxedUnpacker::Cleanup() { | 907 void SandboxedUnpacker::Cleanup() { |
| 894 DCHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); | 908 DCHECK(unpacker_io_task_runner_->RunsTasksOnCurrentThread()); |
| 895 if (!temp_dir_.Delete()) { | 909 if (!temp_dir_.Delete()) { |
| 896 LOG(WARNING) << "Can not delete temp directory at " | 910 LOG(WARNING) << "Can not delete temp directory at " |
| 897 << temp_dir_.GetPath().value(); | 911 << temp_dir_.GetPath().value(); |
| 898 } | 912 } |
| 899 } | 913 } |
| 900 | 914 |
| 901 SandboxedUnpacker::UtilityHostWrapper::UtilityHostWrapper() {} | |
| 902 | |
| 903 bool SandboxedUnpacker::UtilityHostWrapper::StartIfNeeded( | |
| 904 const base::FilePath& exposed_dir, | |
| 905 const scoped_refptr<UtilityProcessHostClient>& client, | |
| 906 const scoped_refptr<base::SequencedTaskRunner>& client_task_runner) { | |
| 907 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
| 908 if (!utility_host_) { | |
| 909 utility_host_ = | |
| 910 UtilityProcessHost::Create(client, client_task_runner)->AsWeakPtr(); | |
| 911 utility_host_->SetName( | |
| 912 l10n_util::GetStringUTF16(IDS_UTILITY_PROCESS_EXTENSION_UNPACKER_NAME)); | |
| 913 | |
| 914 // Grant the subprocess access to our temp dir so it can write out files. | |
| 915 DCHECK(!exposed_dir.empty()); | |
| 916 utility_host_->SetExposedDir(exposed_dir); | |
| 917 if (!utility_host_->StartBatchMode()) { | |
| 918 utility_host_.reset(); | |
| 919 return false; | |
| 920 } | |
| 921 } | |
| 922 return true; | |
| 923 } | |
| 924 | |
| 925 content::UtilityProcessHost* SandboxedUnpacker::UtilityHostWrapper::host() | |
| 926 const { | |
| 927 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
| 928 return utility_host_.get(); | |
| 929 } | |
| 930 | |
| 931 SandboxedUnpacker::UtilityHostWrapper::~UtilityHostWrapper() { | |
| 932 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
| 933 if (utility_host_) { | |
| 934 utility_host_->EndBatchMode(); | |
| 935 utility_host_.reset(); | |
| 936 } | |
| 937 } | |
| 938 | |
| 939 } // namespace extensions | 915 } // namespace extensions |
| OLD | NEW |