pnacl_component_installer.cc revision a36e5920737c6adbddd3e43b760e5de8431db6e0
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
3// found in the LICENSE file.
4
5#include "chrome/browser/component_updater/pnacl/pnacl_component_installer.h"
6
7#include "base/base_paths.h"
8#include "base/bind.h"
9#include "base/callback.h"
10#include "base/command_line.h"
11#include "base/compiler_specific.h"
12#include "base/file_util.h"
13#include "base/files/file_enumerator.h"
14#include "base/files/file_path.h"
15#include "base/json/json_file_value_serializer.h"
16#include "base/logging.h"
17#include "base/path_service.h"
18#include "base/strings/string_util.h"
19#include "base/values.h"
20#include "base/version.h"
21#include "base/win/windows_version.h"
22#include "build/build_config.h"
23#include "chrome/browser/browser_process.h"
24#include "chrome/browser/component_updater/component_updater_service.h"
25#include "chrome/browser/profiles/profile.h"
26#include "chrome/browser/profiles/profile_manager.h"
27#include "chrome/common/chrome_paths.h"
28#include "chrome/common/chrome_switches.h"
29#include "chrome/common/omaha_query_params/omaha_query_params.h"
30#include "content/public/browser/browser_thread.h"
31
32using chrome::OmahaQueryParams;
33using content::BrowserThread;
34
35namespace {
36
37// Name of the Pnacl component specified in the manifest.
38const char kPnaclManifestName[] = "PNaCl Translator";
39
40// Sanitize characters from Pnacl Arch value so that they can be used
41// in path names.  This should only be characters in the set: [a-z0-9_].
42// Keep in sync with chrome/browser/nacl_host/nacl_file_host.
43std::string SanitizeForPath(const std::string& input) {
44  std::string result;
45  ReplaceChars(input, "-", "_", &result);
46  return result;
47}
48
49// Set the component's hash to the multi-CRX PNaCl package.
50void SetPnaclHash(CrxComponent* component) {
51 static const uint8 sha256_hash[32] =
52     { // This corresponds to AppID: hnimpnehoodheedghdeeijklkeaacbdc
53       0x7d, 0x8c, 0xfd, 0x47, 0xee, 0x37, 0x44, 0x36, 0x73, 0x44,
54       0x89, 0xab, 0xa4, 0x00, 0x21, 0x32, 0x4a, 0x06, 0x06, 0xf1, 0x51,
55       0x3c, 0x51, 0xba, 0x31, 0x2f, 0xbc, 0xb3, 0x99, 0x07, 0xdc, 0x9c};
56
57  component->pk_hash.assign(sha256_hash,
58                            &sha256_hash[arraysize(sha256_hash)]);
59}
60
61// If we don't have Pnacl installed, this is the version we claim.
62const char kNullVersion[] = "0.0.0.0";
63
64// PNaCl is packaged as a multi-CRX.  This returns the platform-specific
65// subdirectory that is part of that multi-CRX.
66base::FilePath GetPlatformDir(const base::FilePath& base_path) {
67  std::string arch = SanitizeForPath(OmahaQueryParams::getNaclArch());
68  return base_path.AppendASCII("_platform_specific").AppendASCII(arch);
69}
70
71// Tell the rest of the world where to find the platform-specific PNaCl files.
72void OverrideDirPnaclComponent(const base::FilePath& base_path) {
73  PathService::Override(chrome::DIR_PNACL_COMPONENT,
74                        GetPlatformDir(base_path));
75}
76
77bool GetLatestPnaclDirectory(PnaclComponentInstaller* pci,
78                             base::FilePath* latest_dir,
79                             Version* latest_version,
80                             std::vector<base::FilePath>* older_dirs) {
81  // Enumerate all versions starting from the base directory.
82  base::FilePath base_dir = pci->GetPnaclBaseDirectory();
83  bool found = false;
84  base::FileEnumerator
85      file_enumerator(base_dir, false, base::FileEnumerator::DIRECTORIES);
86  for (base::FilePath path = file_enumerator.Next(); !path.value().empty();
87       path = file_enumerator.Next()) {
88    Version version(path.BaseName().MaybeAsASCII());
89    if (!version.IsValid())
90      continue;
91    if (found) {
92      if (version.CompareTo(*latest_version) > 0) {
93        older_dirs->push_back(*latest_dir);
94        *latest_dir = path;
95        *latest_version = version;
96      } else {
97        older_dirs->push_back(path);
98      }
99    } else {
100      *latest_version = version;
101      *latest_dir = path;
102      found = true;
103    }
104  }
105  return found;
106}
107
108// Read a manifest file in.
109base::DictionaryValue* ReadJSONManifest(
110    const base::FilePath& manifest_path) {
111  JSONFileValueSerializer serializer(manifest_path);
112  std::string error;
113  scoped_ptr<base::Value> root(serializer.Deserialize(NULL, &error));
114  if (!root.get())
115    return NULL;
116  if (!root->IsType(base::Value::TYPE_DICTIONARY))
117    return NULL;
118  return static_cast<base::DictionaryValue*>(root.release());
119}
120
121// Read the PNaCl specific manifest.
122base::DictionaryValue* ReadPnaclManifest(const base::FilePath& unpack_path) {
123  base::FilePath manifest_path = GetPlatformDir(unpack_path).AppendASCII(
124      "pnacl_public_pnacl_json");
125  if (!base::PathExists(manifest_path))
126    return NULL;
127  return ReadJSONManifest(manifest_path);
128}
129
130// Read the component's manifest.json.
131base::DictionaryValue* ReadComponentManifest(
132    const base::FilePath& unpack_path) {
133  base::FilePath manifest_path = unpack_path.Append(
134      FILE_PATH_LITERAL("manifest.json"));
135  if (!base::PathExists(manifest_path))
136    return NULL;
137  return ReadJSONManifest(manifest_path);
138}
139
140}  // namespace
141
142// Check that the component's manifest is for PNaCl, and check the
143// PNaCl manifest indicates this is the correct arch-specific package.
144bool CheckPnaclComponentManifest(const base::DictionaryValue& manifest,
145                                 const base::DictionaryValue& pnacl_manifest,
146                                 Version* version_out) {
147  // Make sure we have the right |manifest| file.
148  std::string name;
149  if (!manifest.GetStringASCII("name", &name)) {
150    LOG(WARNING) << "'name' field is missing from manifest!";
151    return false;
152  }
153  // For the webstore, we've given different names to each of the
154  // architecture specific packages (and test/QA vs not test/QA)
155  // so only part of it is the same.
156  if (name.find(kPnaclManifestName) == std::string::npos) {
157    LOG(WARNING) << "'name' field in manifest is invalid ("
158                 << name << ") -- missing ("
159                 << kPnaclManifestName << ")";
160    return false;
161  }
162
163  std::string proposed_version;
164  if (!manifest.GetStringASCII("version", &proposed_version)) {
165    LOG(WARNING) << "'version' field is missing from manifest!";
166    return false;
167  }
168  Version version(proposed_version.c_str());
169  if (!version.IsValid()) {
170    LOG(WARNING) << "'version' field in manifest is invalid "
171                 << version.GetString();
172    return false;
173  }
174
175  // Now check the |pnacl_manifest|.
176  std::string arch;
177  if (!pnacl_manifest.GetStringASCII("pnacl-arch", &arch)) {
178    LOG(WARNING) << "'pnacl-arch' field is missing from pnacl-manifest!";
179    return false;
180  }
181  if (arch.compare(OmahaQueryParams::getNaclArch()) != 0) {
182    LOG(WARNING) << "'pnacl-arch' field in manifest is invalid ("
183                 << arch << " vs " << OmahaQueryParams::getNaclArch() << ")";
184    return false;
185  }
186
187  *version_out = version;
188  return true;
189}
190
191PnaclComponentInstaller::PnaclComponentInstaller()
192    : per_user_(false),
193      updates_disabled_(false),
194      cus_(NULL),
195      callback_nums_(0) {
196#if defined(OS_CHROMEOS)
197  per_user_ = true;
198#endif
199  updater_observer_.reset(new PnaclUpdaterObserver(this));
200}
201
202PnaclComponentInstaller::~PnaclComponentInstaller() {
203}
204
205void PnaclComponentInstaller::OnUpdateError(int error) {
206  NOTREACHED() << "Pnacl update error: " << error;
207}
208
209// Pnacl components have the version encoded in the path itself:
210// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\0.1.2.3\.
211// and the base directory will be:
212// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\.
213base::FilePath PnaclComponentInstaller::GetPnaclBaseDirectory() {
214  // For ChromeOS, temporarily make this user-dependent (for integrity) until
215  // we find a better solution.
216  // This is not ideal because of the following:
217  //   (a) We end up with per-user copies instead of a single copy
218  //   (b) The profile can change as users log in to different accounts
219  //   so we need to watch for user-login-events (see pnacl_profile_observer.h).
220  if (per_user_) {
221    DCHECK(!current_profile_path_.empty());
222    base::FilePath path = current_profile_path_.Append(
223        FILE_PATH_LITERAL("pnacl"));
224    return path;
225  } else {
226    base::FilePath result;
227    CHECK(PathService::Get(chrome::DIR_PNACL_BASE, &result));
228    return result;
229  }
230}
231
232void PnaclComponentInstaller::OnProfileChange() {
233  // On chromeos, we want to find the --login-profile=<foo> dir.
234  // Even though the path does vary between users, the content
235  // changes when logging out and logging in.
236  ProfileManager* pm = g_browser_process->profile_manager();
237  current_profile_path_ = pm->user_data_dir().Append(
238      pm->GetInitialProfileDir());
239}
240
241bool PnaclComponentInstaller::Install(const base::DictionaryValue& manifest,
242                                      const base::FilePath& unpack_path) {
243  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
244  scoped_ptr<base::DictionaryValue> pnacl_manifest(
245      ReadPnaclManifest(unpack_path));
246  if (pnacl_manifest == NULL) {
247    LOG(WARNING) << "Failed to read pnacl manifest.";
248    NotifyInstallError();
249    return false;
250  }
251
252  Version version;
253  if (!CheckPnaclComponentManifest(manifest, *pnacl_manifest, &version)) {
254    LOG(WARNING) << "CheckPnaclComponentManifest failed, not installing.";
255    NotifyInstallError();
256    return false;
257  }
258
259  // Don't install if the current version is actually newer.
260  if (current_version().CompareTo(version) > 0) {
261    NotifyInstallError();
262    return false;
263  }
264
265  // Passed the basic tests. Time to install it.
266  base::FilePath path = GetPnaclBaseDirectory().AppendASCII(
267      version.GetString());
268  if (base::PathExists(path)) {
269    LOG(WARNING) << "Target path already exists, not installing.";
270    NotifyInstallError();
271    return false;
272  }
273  if (!base::Move(unpack_path, path)) {
274    LOG(WARNING) << "Move failed, not installing.";
275    NotifyInstallError();
276    return false;
277  }
278
279  // Installation is done. Now tell the rest of chrome.
280  // - The path service.
281  // - Callbacks that requested an update.
282  set_current_version(version);
283  NotifyInstallSuccess();
284  OverrideDirPnaclComponent(path);
285  return true;
286}
287
288// Given |file|, which can be a path like "_platform_specific/arm/pnacl_foo",
289// returns the assumed install path. The path separator in |file| is '/'
290// for all platforms. Caller is responsible for checking that the
291// |installed_file| actually exists.
292bool PnaclComponentInstaller::GetInstalledFile(
293    const std::string& file, base::FilePath* installed_file) {
294  if (current_version().Equals(Version(kNullVersion)))
295    return false;
296
297  *installed_file = GetPnaclBaseDirectory().AppendASCII(
298      current_version().GetString()).AppendASCII(file);
299  return true;
300}
301
302void PnaclComponentInstaller::AddInstallCallback(
303    const InstallCallback& cb) {
304  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
305  int num = ++callback_nums_;
306  install_callbacks_.push_back(std::make_pair(cb, num));
307}
308
309void PnaclComponentInstaller::CancelCallback(int num) {
310  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
311  for (std::list<std::pair<InstallCallback, int> >::iterator
312           i = install_callbacks_.begin(),
313           e = install_callbacks_.end(); i != e; ++i) {
314    if (i->second == num) {
315      i->first.Run(false);
316      install_callbacks_.erase(i);
317      return;
318    }
319  }
320}
321
322void PnaclComponentInstaller::NotifyAllWithResult(bool status) {
323  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
324  while (!install_callbacks_.empty()) {
325    install_callbacks_.front().first.Run(status);
326    install_callbacks_.pop_front();
327  }
328}
329
330void PnaclComponentInstaller::NotifyInstallError() {
331  if (!install_callbacks_.empty()) {
332    BrowserThread::PostTask(
333        BrowserThread::UI, FROM_HERE,
334        base::Bind(&PnaclComponentInstaller::NotifyAllWithResult,
335                   // Unretained because installer lives until process shutdown.
336                   base::Unretained(this), false));
337  }
338}
339
340void PnaclComponentInstaller::NotifyInstallSuccess() {
341  if (!install_callbacks_.empty()) {
342    BrowserThread::PostTask(
343        BrowserThread::UI, FROM_HERE,
344        base::Bind(&PnaclComponentInstaller::NotifyAllWithResult,
345                   // Unretained because installer lives until process shutdown.
346                   base::Unretained(this), true));
347  }
348}
349
350namespace {
351
352void FinishPnaclUpdateRegistration(const Version& current_version,
353                                   PnaclComponentInstaller* pci) {
354  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
355  CrxComponent pnacl_component;
356  pnacl_component.version = current_version;
357  pnacl_component.name = "pnacl";
358  pnacl_component.installer = pci;
359  pci->set_current_version(current_version);
360  SetPnaclHash(&pnacl_component);
361
362  ComponentUpdateService::Status status =
363      pci->cus()->RegisterComponent(pnacl_component);
364  if (status != ComponentUpdateService::kOk
365      && status != ComponentUpdateService::kReplaced) {
366    NOTREACHED() << "Pnacl component registration failed.";
367  }
368}
369
370// Check if there is an existing version on disk first to know when
371// a hosted version is actually newer.
372void StartPnaclUpdateRegistration(PnaclComponentInstaller* pci) {
373  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
374  base::FilePath path = pci->GetPnaclBaseDirectory();
375  if (!base::PathExists(path)) {
376    if (!file_util::CreateDirectory(path)) {
377      NOTREACHED() << "Could not create base Pnacl directory.";
378      return;
379    }
380  }
381
382  Version version(kNullVersion);
383  std::vector<base::FilePath> older_dirs;
384  if (GetLatestPnaclDirectory(pci, &path, &version, &older_dirs)) {
385    scoped_ptr<base::DictionaryValue> manifest(
386        ReadComponentManifest(path));
387    scoped_ptr<base::DictionaryValue> pnacl_manifest(
388        ReadPnaclManifest(path));
389    Version manifest_version;
390    // Check that the component manifest and PNaCl manifest files
391    // are legit, and that the indicated version matches the one
392    // encoded within the path name.
393    if (manifest == NULL || pnacl_manifest == NULL
394        || !CheckPnaclComponentManifest(*manifest,
395                                        *pnacl_manifest,
396                                        &manifest_version)
397        || !version.Equals(manifest_version)) {
398      version = Version(kNullVersion);
399    } else {
400      OverrideDirPnaclComponent(path);
401    }
402  }
403
404  // If updates are disabled, only discover the current version
405  // and OverrideDirPnaclComponent. That way, developers can use
406  // a pinned version. Do not actually finish registration with
407  // the component update service.
408  if (pci->updates_disabled())
409    return;
410
411  BrowserThread::PostTask(
412      BrowserThread::UI, FROM_HERE,
413      base::Bind(&FinishPnaclUpdateRegistration, version, pci));
414
415  // Remove older versions of PNaCl.
416  for (std::vector<base::FilePath>::iterator iter = older_dirs.begin();
417       iter != older_dirs.end(); ++iter) {
418    base::DeleteFile(*iter, true);
419  }
420}
421
422// Remove old per-profile copies of PNaCl (was for ChromeOS).
423// TODO(jvoung): Delete this code once most ChromeOS users have reaped
424// their old per-profile copies of PNaCl.
425void ReapOldChromeOSPnaclFiles(PnaclComponentInstaller* pci) {
426  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
427  base::FilePath path = pci->GetPnaclBaseDirectory();
428  if (!base::PathExists(path))
429    return;
430
431  // Do a basic sanity check first.
432  if (pci->per_user()
433      && path.BaseName().value().compare(FILE_PATH_LITERAL("pnacl")) == 0)
434    base::DeleteFile(path, true);
435}
436
437
438void GetProfileInformation(PnaclComponentInstaller* pci) {
439  // Bail if not logged in yet.
440  if (!g_browser_process->profile_manager()->IsLoggedIn()) {
441    return;
442  }
443
444  pci->OnProfileChange();
445
446  // Do not actually register PNaCl for component updates, for CHROMEOS.
447  // Just get the profile information and delete the per-profile files
448  // if they exist.
449 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
450                         base::Bind(&ReapOldChromeOSPnaclFiles, pci));
451}
452
453}  // namespace
454
455void PnaclComponentInstaller::RegisterPnaclComponent(
456                            ComponentUpdateService* cus,
457                            const CommandLine& command_line) {
458  // Register PNaCl by default (can be disabled).
459  updates_disabled_ = command_line.HasSwitch(switches::kDisablePnaclInstall);
460  cus_ = cus;
461  // If per_user, create a profile observer to watch for logins.
462  // Only do so after cus_ is set to something non-null.
463  if (per_user_ && !profile_observer_) {
464    profile_observer_.reset(new PnaclProfileObserver(this));
465  }
466  if (per_user_) {
467    // Figure out profile information, before proceeding to look for files.
468    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
469                            base::Bind(&GetProfileInformation, this));
470  } else {
471    BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
472                            base::Bind(&StartPnaclUpdateRegistration, this));
473  }
474}
475
476void PnaclComponentInstaller::ReRegisterPnacl() {
477  DCHECK(per_user_);
478  // Figure out profile information, before proceeding to look for files.
479  BrowserThread::PostTask(
480      BrowserThread::UI, FROM_HERE,
481      base::Bind(&GetProfileInformation, this));
482}
483
484void RequestFirstInstall(ComponentUpdateService* cus,
485                         PnaclComponentInstaller* pci,
486                          const base::Callback<void(bool)>& installed) {
487  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
488  Version null_version(kNullVersion);
489  CrxComponent pnacl_component;
490  pci->set_current_version(null_version);
491  pnacl_component.version = null_version;
492  pnacl_component.name = "pnacl";
493  pnacl_component.installer = pci;
494  SetPnaclHash(&pnacl_component);
495  ComponentUpdateService::Status status = cus->CheckForUpdateSoon(
496      pnacl_component);
497  if (status != ComponentUpdateService::kOk) {
498    installed.Run(false);
499    return;
500  }
501  pci->AddInstallCallback(installed);
502}
503