pnacl_component_installer.cc revision 2385ea399aae016c0806a4f9ef3c9cfe3d2a39df
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  SetPnaclHash(&pnacl_component);
340
341  return pnacl_component;
342}
343
344namespace {
345
346void FinishPnaclUpdateRegistration(const Version& current_version,
347                                   PnaclComponentInstaller* pci) {
348  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
349  pci->set_current_version(current_version);
350  CrxComponent pnacl_component = pci->GetCrxComponent();
351
352  ComponentUpdateService::Status status =
353      pci->cus()->RegisterComponent(pnacl_component);
354  if (status != ComponentUpdateService::kOk
355      && status != ComponentUpdateService::kReplaced) {
356    NOTREACHED() << "Pnacl component registration failed.";
357  }
358}
359
360// Check if there is an existing version on disk first to know when
361// a hosted version is actually newer.
362void StartPnaclUpdateRegistration(PnaclComponentInstaller* pci) {
363  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
364  base::FilePath path = pci->GetPnaclBaseDirectory();
365  if (!base::PathExists(path)) {
366    if (!file_util::CreateDirectory(path)) {
367      NOTREACHED() << "Could not create base Pnacl directory.";
368      return;
369    }
370  }
371
372  Version version(kNullVersion);
373  std::vector<base::FilePath> older_dirs;
374  if (GetLatestPnaclDirectory(pci, &path, &version, &older_dirs)) {
375    scoped_ptr<base::DictionaryValue> manifest(
376        ReadComponentManifest(path));
377    scoped_ptr<base::DictionaryValue> pnacl_manifest(
378        ReadPnaclManifest(path));
379    Version manifest_version;
380    // Check that the component manifest and PNaCl manifest files
381    // are legit, and that the indicated version matches the one
382    // encoded within the path name.
383    if (manifest == NULL || pnacl_manifest == NULL
384        || !CheckPnaclComponentManifest(*manifest,
385                                        *pnacl_manifest,
386                                        &manifest_version)
387        || !version.Equals(manifest_version)) {
388      version = Version(kNullVersion);
389    } else {
390      OverrideDirPnaclComponent(path);
391    }
392  }
393
394  // If updates are disabled, only discover the current version
395  // and OverrideDirPnaclComponent. That way, developers can use
396  // a pinned version. Do not actually finish registration with
397  // the component update service.
398  if (pci->updates_disabled())
399    return;
400
401  BrowserThread::PostTask(
402      BrowserThread::UI, FROM_HERE,
403      base::Bind(&FinishPnaclUpdateRegistration, version, pci));
404
405  // Remove older versions of PNaCl.
406  for (std::vector<base::FilePath>::iterator iter = older_dirs.begin();
407       iter != older_dirs.end(); ++iter) {
408    base::DeleteFile(*iter, true);
409  }
410}
411
412// Remove old per-profile copies of PNaCl (was for ChromeOS).
413// TODO(jvoung): Delete this code once most ChromeOS users have reaped
414// their old per-profile copies of PNaCl.
415void ReapOldChromeOSPnaclFiles(PnaclComponentInstaller* pci) {
416  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
417  base::FilePath path = pci->GetPnaclBaseDirectory();
418  if (!base::PathExists(path))
419    return;
420
421  // Do a basic sanity check first.
422  if (pci->per_user()
423      && path.BaseName().value().compare(FILE_PATH_LITERAL("pnacl")) == 0)
424    base::DeleteFile(path, true);
425}
426
427
428void GetProfileInformation(PnaclComponentInstaller* pci) {
429  // Bail if not logged in yet.
430  if (!g_browser_process->profile_manager()->IsLoggedIn()) {
431    return;
432  }
433
434  pci->OnProfileChange();
435
436  // Do not actually register PNaCl for component updates, for CHROMEOS.
437  // Just get the profile information and delete the per-profile files
438  // if they exist.
439 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
440                         base::Bind(&ReapOldChromeOSPnaclFiles, pci));
441}
442
443}  // namespace
444
445void PnaclComponentInstaller::RegisterPnaclComponent(
446                            ComponentUpdateService* cus,
447                            const CommandLine& command_line) {
448  // Register PNaCl by default (can be disabled).
449  updates_disabled_ = command_line.HasSwitch(switches::kDisablePnaclInstall);
450  cus_ = cus;
451  // If per_user, create a profile observer to watch for logins.
452  // Only do so after cus_ is set to something non-null.
453  if (per_user_ && !profile_observer_) {
454    profile_observer_.reset(new PnaclProfileObserver(this));
455  }
456  if (per_user_) {
457    // Figure out profile information, before proceeding to look for files.
458    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
459                            base::Bind(&GetProfileInformation, this));
460  } else {
461    BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
462                            base::Bind(&StartPnaclUpdateRegistration, this));
463  }
464}
465
466void PnaclComponentInstaller::ReRegisterPnacl() {
467  DCHECK(per_user_);
468  // Figure out profile information, before proceeding to look for files.
469  BrowserThread::PostTask(
470      BrowserThread::UI, FROM_HERE,
471      base::Bind(&GetProfileInformation, this));
472}
473
474void PnaclComponentInstaller::RequestFirstInstall(const InstallCallback& cb) {
475  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
476  // Only one request can happen at once.
477  if (!install_callback_.is_null()) {
478    cb.Run(false);
479    return;
480  }
481  set_current_version(Version(kNullVersion));
482  CrxComponent pnacl_component = GetCrxComponent();
483  ComponentUpdateService::Status status = cus_->CheckForUpdateSoon(
484      pnacl_component);
485  if (status != ComponentUpdateService::kOk) {
486    cb.Run(false);
487    return;
488  }
489  install_callback_ = cb;
490  updater_observer_->EnsureObserving();
491}
492