pnacl_component_installer.cc revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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// Check that the component's manifest is for PNaCl, and check the
141// PNaCl manifest indicates this is the correct arch-specific package.
142bool CheckPnaclComponentManifest(const base::DictionaryValue& manifest,
143                                 const base::DictionaryValue& pnacl_manifest,
144                                 Version* version_out) {
145  // Make sure we have the right |manifest| file.
146  std::string name;
147  if (!manifest.GetStringASCII("name", &name)) {
148    LOG(WARNING) << "'name' field is missing from manifest!";
149    return false;
150  }
151  // For the webstore, we've given different names to each of the
152  // architecture specific packages (and test/QA vs not test/QA)
153  // so only part of it is the same.
154  if (name.find(kPnaclManifestName) == std::string::npos) {
155    LOG(WARNING) << "'name' field in manifest is invalid ("
156                 << name << ") -- missing ("
157                 << kPnaclManifestName << ")";
158    return false;
159  }
160
161  std::string proposed_version;
162  if (!manifest.GetStringASCII("version", &proposed_version)) {
163    LOG(WARNING) << "'version' field is missing from manifest!";
164    return false;
165  }
166  Version version(proposed_version.c_str());
167  if (!version.IsValid()) {
168    LOG(WARNING) << "'version' field in manifest is invalid "
169                 << version.GetString();
170    return false;
171  }
172
173  // Now check the |pnacl_manifest|.
174  std::string arch;
175  if (!pnacl_manifest.GetStringASCII("pnacl-arch", &arch)) {
176    LOG(WARNING) << "'pnacl-arch' field is missing from pnacl-manifest!";
177    return false;
178  }
179  if (arch.compare(OmahaQueryParams::getNaclArch()) != 0) {
180    LOG(WARNING) << "'pnacl-arch' field in manifest is invalid ("
181                 << arch << " vs " << OmahaQueryParams::getNaclArch() << ")";
182    return false;
183  }
184
185  *version_out = version;
186  return true;
187}
188
189}  // namespace
190
191PnaclComponentInstaller::PnaclComponentInstaller()
192    : per_user_(false),
193      updates_disabled_(false),
194      cus_(NULL) {
195#if defined(OS_CHROMEOS)
196  per_user_ = true;
197#endif
198  updater_observer_.reset(new PnaclUpdaterObserver(this));
199}
200
201PnaclComponentInstaller::~PnaclComponentInstaller() {
202}
203
204void PnaclComponentInstaller::OnUpdateError(int error) {
205  NOTREACHED() << "Pnacl update error: " << error;
206}
207
208// Pnacl components have the version encoded in the path itself:
209// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\0.1.2.3\.
210// and the base directory will be:
211// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\.
212base::FilePath PnaclComponentInstaller::GetPnaclBaseDirectory() {
213  // For ChromeOS, temporarily make this user-dependent (for integrity) until
214  // we find a better solution.
215  // This is not ideal because of the following:
216  //   (a) We end up with per-user copies instead of a single copy
217  //   (b) The profile can change as users log in to different accounts
218  //   so we need to watch for user-login-events (see pnacl_profile_observer.h).
219  if (per_user_) {
220    DCHECK(!current_profile_path_.empty());
221    base::FilePath path = current_profile_path_.Append(
222        FILE_PATH_LITERAL("pnacl"));
223    return path;
224  } else {
225    base::FilePath result;
226    CHECK(PathService::Get(chrome::DIR_PNACL_BASE, &result));
227    return result;
228  }
229}
230
231void PnaclComponentInstaller::OnProfileChange() {
232  // On chromeos, we want to find the --login-profile=<foo> dir.
233  // Even though the path does vary between users, the content
234  // changes when logging out and logging in.
235  ProfileManager* pm = g_browser_process->profile_manager();
236  current_profile_path_ = pm->user_data_dir().Append(
237      pm->GetInitialProfileDir());
238}
239
240bool PnaclComponentInstaller::Install(const base::DictionaryValue& manifest,
241                                      const base::FilePath& unpack_path) {
242  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
243  scoped_ptr<base::DictionaryValue> pnacl_manifest(
244      ReadPnaclManifest(unpack_path));
245  if (pnacl_manifest == NULL) {
246    LOG(WARNING) << "Failed to read pnacl manifest.";
247    NotifyInstallError();
248    return false;
249  }
250
251  Version version;
252  if (!CheckPnaclComponentManifest(manifest, *pnacl_manifest, &version)) {
253    LOG(WARNING) << "CheckPnaclComponentManifest failed, not installing.";
254    NotifyInstallError();
255    return false;
256  }
257
258  // Don't install if the current version is actually newer.
259  if (current_version().CompareTo(version) > 0) {
260    NotifyInstallError();
261    return false;
262  }
263
264  // Passed the basic tests. Time to install it.
265  base::FilePath path = GetPnaclBaseDirectory().AppendASCII(
266      version.GetString());
267  if (base::PathExists(path)) {
268    LOG(WARNING) << "Target path already exists, not installing.";
269    NotifyInstallError();
270    return false;
271  }
272  if (!base::Move(unpack_path, path)) {
273    LOG(WARNING) << "Move failed, not installing.";
274    NotifyInstallError();
275    return false;
276  }
277
278  // Installation is done. Now tell the rest of chrome.
279  // - The path service.
280  // - Callbacks that requested an update.
281  set_current_version(version);
282  NotifyInstallSuccess();
283  OverrideDirPnaclComponent(path);
284  return true;
285}
286
287// Given |file|, which can be a path like "_platform_specific/arm/pnacl_foo",
288// returns the assumed install path. The path separator in |file| is '/'
289// for all platforms. Caller is responsible for checking that the
290// |installed_file| actually exists.
291bool PnaclComponentInstaller::GetInstalledFile(
292    const std::string& file, base::FilePath* installed_file) {
293  if (current_version().Equals(Version(kNullVersion)))
294    return false;
295
296  *installed_file = GetPnaclBaseDirectory().AppendASCII(
297      current_version().GetString()).AppendASCII(file);
298  return true;
299}
300
301void PnaclComponentInstaller::NotifyInstallError() {
302  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
303    BrowserThread::PostTask(
304        BrowserThread::UI, FROM_HERE,
305        base::Bind(&PnaclComponentInstaller::NotifyInstallError,
306                   // Unretained because installer lives until process shutdown.
307                   base::Unretained(this)));
308    return;
309  }
310  if (!install_callback_.is_null()) {
311    updater_observer_->StopObserving();
312    install_callback_.Run(false);
313    install_callback_.Reset();
314  }
315}
316
317void PnaclComponentInstaller::NotifyInstallSuccess() {
318  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
319    BrowserThread::PostTask(
320        BrowserThread::UI, FROM_HERE,
321        base::Bind(&PnaclComponentInstaller::NotifyInstallSuccess,
322                   // Unretained because installer lives until process shutdown.
323                   base::Unretained(this)));
324    return;
325  }
326  if (!install_callback_.is_null()) {
327    updater_observer_->StopObserving();
328    install_callback_.Run(true);
329    install_callback_.Reset();
330  }
331}
332
333CrxComponent PnaclComponentInstaller::GetCrxComponent() {
334  CrxComponent pnacl_component;
335  pnacl_component.version = current_version();
336  pnacl_component.name = "pnacl";
337  pnacl_component.installer = this;
338  pnacl_component.observer = updater_observer_.get();
339  pnacl_component.fingerprint = current_fingerprint();
340  SetPnaclHash(&pnacl_component);
341
342  return pnacl_component;
343}
344
345namespace {
346
347void FinishPnaclUpdateRegistration(const Version& current_version,
348                                   const std::string& current_fingerprint,
349                                   PnaclComponentInstaller* pci) {
350  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
351  pci->set_current_version(current_version);
352  pci->set_current_fingerprint(current_fingerprint);
353  CrxComponent pnacl_component = pci->GetCrxComponent();
354
355  ComponentUpdateService::Status status =
356      pci->cus()->RegisterComponent(pnacl_component);
357  if (status != ComponentUpdateService::kOk
358      && status != ComponentUpdateService::kReplaced) {
359    NOTREACHED() << "Pnacl component registration failed.";
360  }
361}
362
363// Check if there is an existing version on disk first to know when
364// a hosted version is actually newer.
365void StartPnaclUpdateRegistration(PnaclComponentInstaller* pci) {
366  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
367  base::FilePath path = pci->GetPnaclBaseDirectory();
368  if (!base::PathExists(path)) {
369    if (!file_util::CreateDirectory(path)) {
370      NOTREACHED() << "Could not create base Pnacl directory.";
371      return;
372    }
373  }
374
375  Version current_version(kNullVersion);
376  std::string current_fingerprint;
377  std::vector<base::FilePath> older_dirs;
378  if (GetLatestPnaclDirectory(pci, &path, &current_version, &older_dirs)) {
379    scoped_ptr<base::DictionaryValue> manifest(
380        ReadComponentManifest(path));
381    scoped_ptr<base::DictionaryValue> pnacl_manifest(
382        ReadPnaclManifest(path));
383    Version manifest_version;
384    // Check that the component manifest and PNaCl manifest files
385    // are legit, and that the indicated version matches the one
386    // encoded within the path name.
387    if (manifest == NULL || pnacl_manifest == NULL
388        || !CheckPnaclComponentManifest(*manifest,
389                                        *pnacl_manifest,
390                                        &manifest_version)
391        || !current_version.Equals(manifest_version)) {
392      current_version = Version(kNullVersion);
393    } else {
394      OverrideDirPnaclComponent(path);
395      base::ReadFileToString(path.AppendASCII("manifest.fingerprint"),
396                             &current_fingerprint);
397    }
398  }
399
400  // If updates are disabled, only discover the current version
401  // and OverrideDirPnaclComponent. That way, developers can use
402  // a pinned version. Do not actually finish registration with
403  // the component update service.
404  if (pci->updates_disabled())
405    return;
406
407  BrowserThread::PostTask(
408      BrowserThread::UI, FROM_HERE,
409      base::Bind(&FinishPnaclUpdateRegistration,
410                 current_version,
411                 current_fingerprint,
412                 pci));
413
414  // Remove older versions of PNaCl.
415  for (std::vector<base::FilePath>::iterator iter = older_dirs.begin();
416       iter != older_dirs.end(); ++iter) {
417    base::DeleteFile(*iter, true);
418  }
419}
420
421// Remove old per-profile copies of PNaCl (was for ChromeOS).
422// TODO(jvoung): Delete this code once most ChromeOS users have reaped
423// their old per-profile copies of PNaCl.
424void ReapOldChromeOSPnaclFiles(PnaclComponentInstaller* pci) {
425  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
426  base::FilePath path = pci->GetPnaclBaseDirectory();
427  if (!base::PathExists(path))
428    return;
429
430  // Do a basic sanity check first.
431  if (pci->per_user()
432      && path.BaseName().value().compare(FILE_PATH_LITERAL("pnacl")) == 0)
433    base::DeleteFile(path, true);
434}
435
436
437void GetProfileInformation(PnaclComponentInstaller* pci) {
438  // Bail if not logged in yet.
439  if (!g_browser_process->profile_manager()->IsLoggedIn()) {
440    return;
441  }
442
443  pci->OnProfileChange();
444
445  // Do not actually register PNaCl for component updates, for CHROMEOS.
446  // Just get the profile information and delete the per-profile files
447  // if they exist.
448 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
449                         base::Bind(&ReapOldChromeOSPnaclFiles, pci));
450}
451
452}  // namespace
453
454void PnaclComponentInstaller::RegisterPnaclComponent(
455                            ComponentUpdateService* cus,
456                            const CommandLine& command_line) {
457  // Register PNaCl by default (can be disabled).
458  updates_disabled_ = command_line.HasSwitch(switches::kDisablePnaclInstall);
459  cus_ = cus;
460  // If per_user, create a profile observer to watch for logins.
461  // Only do so after cus_ is set to something non-null.
462  if (per_user_ && !profile_observer_) {
463    profile_observer_.reset(new PnaclProfileObserver(this));
464  }
465  if (per_user_) {
466    // Figure out profile information, before proceeding to look for files.
467    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
468                            base::Bind(&GetProfileInformation, this));
469  } else {
470    BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
471                            base::Bind(&StartPnaclUpdateRegistration, this));
472  }
473}
474
475void PnaclComponentInstaller::ReRegisterPnacl() {
476  DCHECK(per_user_);
477  // Figure out profile information, before proceeding to look for files.
478  BrowserThread::PostTask(
479      BrowserThread::UI, FROM_HERE,
480      base::Bind(&GetProfileInformation, this));
481}
482
483void PnaclComponentInstaller::RequestFirstInstall(const InstallCallback& cb) {
484  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
485  // Only one request can happen at once.
486  if (!install_callback_.is_null()) {
487    cb.Run(false);
488    return;
489  }
490  set_current_version(Version(kNullVersion));
491  CrxComponent pnacl_component = GetCrxComponent();
492  ComponentUpdateService::Status status = cus_->CheckForUpdateSoon(
493      GetCrxComponentID(pnacl_component));
494  if (status != ComponentUpdateService::kOk) {
495    cb.Run(false);
496    return;
497  }
498  install_callback_ = cb;
499  updater_observer_->EnsureObserving();
500}
501