pnacl_component_installer.cc revision 0f1bc08d4cfcc34181b0b5cbf065c40f687bf740
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}
199
200PnaclComponentInstaller::~PnaclComponentInstaller() {
201}
202
203void PnaclComponentInstaller::OnUpdateError(int error) {
204  NOTREACHED() << "Pnacl update error: " << error;
205}
206
207// Pnacl components have the version encoded in the path itself:
208// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\0.1.2.3\.
209// and the base directory will be:
210// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\.
211base::FilePath PnaclComponentInstaller::GetPnaclBaseDirectory() {
212  // For ChromeOS, temporarily make this user-dependent (for integrity) until
213  // we find a better solution.
214  // This is not ideal because of the following:
215  //   (a) We end up with per-user copies instead of a single copy
216  //   (b) The profile can change as users log in to different accounts
217  //   so we need to watch for user-login-events (see pnacl_profile_observer.h).
218  if (per_user_) {
219    DCHECK(!current_profile_path_.empty());
220    base::FilePath path = current_profile_path_.Append(
221        FILE_PATH_LITERAL("pnacl"));
222    return path;
223  } else {
224    base::FilePath result;
225    CHECK(PathService::Get(chrome::DIR_PNACL_BASE, &result));
226    return result;
227  }
228}
229
230void PnaclComponentInstaller::OnProfileChange() {
231  // On chromeos, we want to find the --login-profile=<foo> dir.
232  // Even though the path does vary between users, the content
233  // changes when logging out and logging in.
234  ProfileManager* pm = g_browser_process->profile_manager();
235  current_profile_path_ = pm->user_data_dir().Append(
236      pm->GetInitialProfileDir());
237}
238
239bool PnaclComponentInstaller::Install(const base::DictionaryValue& manifest,
240                                      const base::FilePath& unpack_path) {
241  scoped_ptr<base::DictionaryValue> pnacl_manifest(
242      ReadPnaclManifest(unpack_path));
243  if (pnacl_manifest == NULL) {
244    LOG(WARNING) << "Failed to read pnacl manifest.";
245    return false;
246  }
247
248  Version version;
249  if (!CheckPnaclComponentManifest(manifest, *pnacl_manifest, &version)) {
250    LOG(WARNING) << "CheckPnaclComponentManifest failed, not installing.";
251    return false;
252  }
253
254  // Don't install if the current version is actually newer.
255  if (current_version().CompareTo(version) > 0) {
256    return false;
257  }
258
259  // Passed the basic tests. Time to install it.
260  base::FilePath path = GetPnaclBaseDirectory().AppendASCII(
261      version.GetString());
262  if (base::PathExists(path)) {
263    LOG(WARNING) << "Target path already exists, not installing.";
264    return false;
265  }
266  if (!base::Move(unpack_path, path)) {
267    LOG(WARNING) << "Move failed, not installing.";
268    return false;
269  }
270
271  // Installation is done. Now tell the rest of chrome.
272  // - The path service.
273  // - Callbacks that requested an update.
274  set_current_version(version);
275  OverrideDirPnaclComponent(path);
276  return true;
277}
278
279// Given |file|, which can be a path like "_platform_specific/arm/pnacl_foo",
280// returns the assumed install path. The path separator in |file| is '/'
281// for all platforms. Caller is responsible for checking that the
282// |installed_file| actually exists.
283bool PnaclComponentInstaller::GetInstalledFile(
284    const std::string& file, base::FilePath* installed_file) {
285  if (current_version().Equals(Version(kNullVersion)))
286    return false;
287
288  *installed_file = GetPnaclBaseDirectory().AppendASCII(
289      current_version().GetString()).AppendASCII(file);
290  return true;
291}
292
293CrxComponent PnaclComponentInstaller::GetCrxComponent() {
294  CrxComponent pnacl_component;
295  pnacl_component.version = current_version();
296  pnacl_component.name = "pnacl";
297  pnacl_component.installer = this;
298  pnacl_component.fingerprint = current_fingerprint();
299  SetPnaclHash(&pnacl_component);
300
301  return pnacl_component;
302}
303
304namespace {
305
306void FinishPnaclUpdateRegistration(const Version& current_version,
307                                   const std::string& current_fingerprint,
308                                   PnaclComponentInstaller* pci) {
309  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
310  pci->set_current_version(current_version);
311  pci->set_current_fingerprint(current_fingerprint);
312  CrxComponent pnacl_component = pci->GetCrxComponent();
313
314  ComponentUpdateService::Status status =
315      pci->cus()->RegisterComponent(pnacl_component);
316  if (status != ComponentUpdateService::kOk
317      && status != ComponentUpdateService::kReplaced) {
318    NOTREACHED() << "Pnacl component registration failed.";
319  }
320}
321
322// Check if there is an existing version on disk first to know when
323// a hosted version is actually newer.
324void StartPnaclUpdateRegistration(PnaclComponentInstaller* pci) {
325  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
326  base::FilePath path = pci->GetPnaclBaseDirectory();
327  if (!base::PathExists(path)) {
328    if (!file_util::CreateDirectory(path)) {
329      NOTREACHED() << "Could not create base Pnacl directory.";
330      return;
331    }
332  }
333
334  Version current_version(kNullVersion);
335  std::string current_fingerprint;
336  std::vector<base::FilePath> older_dirs;
337  if (GetLatestPnaclDirectory(pci, &path, &current_version, &older_dirs)) {
338    scoped_ptr<base::DictionaryValue> manifest(
339        ReadComponentManifest(path));
340    scoped_ptr<base::DictionaryValue> pnacl_manifest(
341        ReadPnaclManifest(path));
342    Version manifest_version;
343    // Check that the component manifest and PNaCl manifest files
344    // are legit, and that the indicated version matches the one
345    // encoded within the path name.
346    if (manifest == NULL || pnacl_manifest == NULL
347        || !CheckPnaclComponentManifest(*manifest,
348                                        *pnacl_manifest,
349                                        &manifest_version)
350        || !current_version.Equals(manifest_version)) {
351      current_version = Version(kNullVersion);
352    } else {
353      OverrideDirPnaclComponent(path);
354      base::ReadFileToString(path.AppendASCII("manifest.fingerprint"),
355                             &current_fingerprint);
356    }
357  }
358
359  // If updates are disabled, only discover the current version
360  // and OverrideDirPnaclComponent. That way, developers can use
361  // a pinned version. Do not actually finish registration with
362  // the component update service.
363  if (pci->updates_disabled())
364    return;
365
366  BrowserThread::PostTask(
367      BrowserThread::UI, FROM_HERE,
368      base::Bind(&FinishPnaclUpdateRegistration,
369                 current_version,
370                 current_fingerprint,
371                 pci));
372
373  // Remove older versions of PNaCl.
374  for (std::vector<base::FilePath>::iterator iter = older_dirs.begin();
375       iter != older_dirs.end(); ++iter) {
376    base::DeleteFile(*iter, true);
377  }
378}
379
380// Remove old per-profile copies of PNaCl (was for ChromeOS).
381// TODO(jvoung): Delete this code once most ChromeOS users have reaped
382// their old per-profile copies of PNaCl.
383void ReapOldChromeOSPnaclFiles(PnaclComponentInstaller* pci) {
384  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
385  base::FilePath path = pci->GetPnaclBaseDirectory();
386  if (!base::PathExists(path))
387    return;
388
389  // Do a basic sanity check first.
390  if (pci->per_user()
391      && path.BaseName().value().compare(FILE_PATH_LITERAL("pnacl")) == 0)
392    base::DeleteFile(path, true);
393}
394
395
396void GetProfileInformation(PnaclComponentInstaller* pci) {
397  // Bail if not logged in yet.
398  if (!g_browser_process->profile_manager()->IsLoggedIn()) {
399    return;
400  }
401
402  pci->OnProfileChange();
403
404  // Do not actually register PNaCl for component updates, for CHROMEOS.
405  // Just get the profile information and delete the per-profile files
406  // if they exist.
407 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
408                         base::Bind(&ReapOldChromeOSPnaclFiles, pci));
409}
410
411}  // namespace
412
413void PnaclComponentInstaller::RegisterPnaclComponent(
414                            ComponentUpdateService* cus,
415                            const CommandLine& command_line) {
416  // Register PNaCl by default (can be disabled).
417  updates_disabled_ = command_line.HasSwitch(switches::kDisablePnaclInstall);
418  cus_ = cus;
419  // If per_user, create a profile observer to watch for logins.
420  // Only do so after cus_ is set to something non-null.
421  if (per_user_ && !profile_observer_) {
422    profile_observer_.reset(new PnaclProfileObserver(this));
423  }
424  if (per_user_) {
425    // Figure out profile information, before proceeding to look for files.
426    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
427                            base::Bind(&GetProfileInformation, this));
428  } else {
429    BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
430                            base::Bind(&StartPnaclUpdateRegistration, this));
431  }
432}
433
434void PnaclComponentInstaller::ReRegisterPnacl() {
435  DCHECK(per_user_);
436  // Figure out profile information, before proceeding to look for files.
437  BrowserThread::PostTask(
438      BrowserThread::UI, FROM_HERE,
439      base::Bind(&GetProfileInformation, this));
440}
441