pnacl_component_installer.cc revision 7d4cd473f85ac64c3747c96c277f9e506a0d2246
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/command_line.h"
10#include "base/compiler_specific.h"
11#include "base/file_util.h"
12#include "base/files/file_enumerator.h"
13#include "base/files/file_path.h"
14#include "base/json/json_file_value_serializer.h"
15#include "base/logging.h"
16#include "base/path_service.h"
17#include "base/strings/string_util.h"
18#include "base/values.h"
19#include "base/version.h"
20#include "base/win/windows_version.h"
21#include "build/build_config.h"
22#include "chrome/browser/browser_process.h"
23#include "chrome/browser/component_updater/component_updater_service.h"
24#include "chrome/browser/profiles/profile.h"
25#include "chrome/browser/profiles/profile_manager.h"
26#include "chrome/common/chrome_paths.h"
27#include "chrome/common/chrome_switches.h"
28#include "chrome/common/omaha_query_params/omaha_query_params.h"
29#include "content/public/browser/browser_thread.h"
30
31using chrome::OmahaQueryParams;
32using content::BrowserThread;
33
34namespace {
35
36// If PNaCl isn't installed yet, but a user is running chrome with
37// --enable-pnacl, this is the amount of time to wait before starting
38// a background install.
39const int kInitialDelaySeconds = 10;
40
41// Name of the Pnacl component specified in the manifest.
42const char kPnaclManifestName[] = "PNaCl Translator";
43
44// Sanitize characters from Pnacl Arch value so that they can be used
45// in path names.  This should only be characters in the set: [a-z0-9_].
46// Keep in sync with chrome/browser/nacl_host/nacl_file_host.
47std::string SanitizeForPath(const std::string& input) {
48  std::string result;
49  ReplaceChars(input, "-", "_", &result);
50  return result;
51}
52
53// Set the component's hash to the multi-CRX PNaCl package.
54void SetPnaclHash(CrxComponent* component) {
55 static const uint8 sha256_hash[32] =
56     { // This corresponds to AppID: hnimpnehoodheedghdeeijklkeaacbdc
57       0x7d, 0x8c, 0xfd, 0x47, 0xee, 0x37, 0x44, 0x36, 0x73, 0x44,
58       0x89, 0xab, 0xa4, 0x00, 0x21, 0x32, 0x4a, 0x06, 0x06, 0xf1, 0x51,
59       0x3c, 0x51, 0xba, 0x31, 0x2f, 0xbc, 0xb3, 0x99, 0x07, 0xdc, 0x9c};
60
61  component->pk_hash.assign(sha256_hash,
62                            &sha256_hash[arraysize(sha256_hash)]);
63}
64
65// If we don't have Pnacl installed, this is the version we claim.
66const char kNullVersion[] = "0.0.0.0";
67
68// PNaCl is packaged as a multi-CRX.  This returns the platform-specific
69// subdirectory that is part of that multi-CRX.
70base::FilePath GetPlatformDir(const base::FilePath& base_path) {
71  std::string arch = SanitizeForPath(OmahaQueryParams::getNaclArch());
72  return base_path.AppendASCII("_platform_specific").AppendASCII(arch);
73}
74
75// Tell the rest of the world where to find the platform-specific PNaCl files.
76void OverrideDirPnaclComponent(const base::FilePath& base_path) {
77  PathService::Override(chrome::DIR_PNACL_COMPONENT,
78                        GetPlatformDir(base_path));
79}
80
81bool GetLatestPnaclDirectory(PnaclComponentInstaller* pci,
82                             base::FilePath* latest_dir,
83                             Version* latest_version,
84                             std::vector<base::FilePath>* older_dirs) {
85  // Enumerate all versions starting from the base directory.
86  base::FilePath base_dir = pci->GetPnaclBaseDirectory();
87  bool found = false;
88  base::FileEnumerator
89      file_enumerator(base_dir, false, base::FileEnumerator::DIRECTORIES);
90  for (base::FilePath path = file_enumerator.Next(); !path.value().empty();
91       path = file_enumerator.Next()) {
92    Version version(path.BaseName().MaybeAsASCII());
93    if (!version.IsValid())
94      continue;
95    if (found) {
96      if (version.CompareTo(*latest_version) > 0) {
97        older_dirs->push_back(*latest_dir);
98        *latest_dir = path;
99        *latest_version = version;
100      } else {
101        older_dirs->push_back(path);
102      }
103    } else {
104      *latest_version = version;
105      *latest_dir = path;
106      found = true;
107    }
108  }
109  return found;
110}
111
112// Read a manifest file in.
113base::DictionaryValue* ReadJSONManifest(
114    const base::FilePath& manifest_path) {
115  JSONFileValueSerializer serializer(manifest_path);
116  std::string error;
117  scoped_ptr<base::Value> root(serializer.Deserialize(NULL, &error));
118  if (!root.get())
119    return NULL;
120  if (!root->IsType(base::Value::TYPE_DICTIONARY))
121    return NULL;
122  return static_cast<base::DictionaryValue*>(root.release());
123}
124
125// Read the PNaCl specific manifest.
126base::DictionaryValue* ReadPnaclManifest(const base::FilePath& unpack_path) {
127  base::FilePath manifest_path = GetPlatformDir(unpack_path).AppendASCII(
128      "pnacl_public_pnacl_json");
129  if (!file_util::PathExists(manifest_path))
130    return NULL;
131  return ReadJSONManifest(manifest_path);
132}
133
134// Read the component's manifest.json.
135base::DictionaryValue* ReadComponentManifest(
136    const base::FilePath& unpack_path) {
137  base::FilePath manifest_path = unpack_path.Append(
138      FILE_PATH_LITERAL("manifest.json"));
139  if (!file_util::PathExists(manifest_path))
140    return NULL;
141  return ReadJSONManifest(manifest_path);
142}
143
144}  // namespace
145
146// Check that the component's manifest is for PNaCl, and check the
147// PNaCl manifest indicates this is the correct arch-specific package.
148bool CheckPnaclComponentManifest(const base::DictionaryValue& manifest,
149                                 const base::DictionaryValue& pnacl_manifest,
150                                 Version* version_out) {
151  // Make sure we have the right |manifest| file.
152  std::string name;
153  manifest.GetStringASCII("name", &name);
154  // For the webstore, we've given different names to each of the
155  // architecture specific packages (and test/QA vs not test/QA)
156  // so only part of it is the same.
157  if (name.find(kPnaclManifestName) == std::string::npos) {
158    LOG(WARNING) << "'name' field in manifest is invalid ("
159                 << name << ") -- missing ("
160                 << kPnaclManifestName << ")";
161    return false;
162  }
163
164  std::string proposed_version;
165  manifest.GetStringASCII("version", &proposed_version);
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  pnacl_manifest.GetStringASCII("pnacl-arch", &arch);
176  if (arch.compare(OmahaQueryParams::getNaclArch()) != 0) {
177    LOG(WARNING) << "'pnacl-arch' field in manifest is invalid ("
178                 << arch << " vs " << OmahaQueryParams::getNaclArch() << ")";
179    return false;
180  }
181
182  *version_out = version;
183  return true;
184}
185
186PnaclComponentInstaller::PnaclComponentInstaller()
187    : per_user_(false),
188      cus_(NULL) {
189#if defined(OS_CHROMEOS)
190  per_user_ = true;
191#endif
192}
193
194PnaclComponentInstaller::~PnaclComponentInstaller() {
195}
196
197void PnaclComponentInstaller::OnUpdateError(int error) {
198  NOTREACHED() << "Pnacl update error: " << error;
199}
200
201// Pnacl components have the version encoded in the path itself:
202// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\0.1.2.3\.
203// and the base directory will be:
204// <profile>\AppData\Local\Google\Chrome\User Data\pnacl\.
205base::FilePath PnaclComponentInstaller::GetPnaclBaseDirectory() {
206  // For ChromeOS, temporarily make this user-dependent (for integrity) until
207  // we find a better solution.
208  // This is not ideal because of the following:
209  //   (a) We end up with per-user copies instead of a single copy
210  //   (b) The profile can change as users log in to different accounts
211  //   so we need to watch for user-login-events (see pnacl_profile_observer.h).
212  if (per_user_) {
213    DCHECK(!current_profile_path_.empty());
214    base::FilePath path = current_profile_path_.Append(
215        FILE_PATH_LITERAL("pnacl"));
216    return path;
217  } else {
218    base::FilePath result;
219    CHECK(PathService::Get(chrome::DIR_PNACL_BASE, &result));
220    return result;
221  }
222}
223
224void PnaclComponentInstaller::OnProfileChange() {
225  // On chromeos, we want to find the --login-profile=<foo> dir.
226  // Even though the path does vary between users, the content
227  // changes when logging out and logging in.
228  ProfileManager* pm = g_browser_process->profile_manager();
229  current_profile_path_ = pm->user_data_dir().Append(
230      pm->GetInitialProfileDir());
231}
232
233bool PnaclComponentInstaller::Install(const base::DictionaryValue& manifest,
234                                      const base::FilePath& unpack_path) {
235  scoped_ptr<base::DictionaryValue> pnacl_manifest(
236      ReadPnaclManifest(unpack_path));
237  if (pnacl_manifest == NULL) {
238    LOG(WARNING) << "Failed to read pnacl manifest.";
239    return false;
240  }
241
242  Version version;
243  if (!CheckPnaclComponentManifest(manifest, *pnacl_manifest, &version)) {
244    LOG(WARNING) << "CheckPnaclComponentManifest failed, not installing.";
245    return false;
246  }
247
248  // Don't install if the current version is actually newer.
249  if (current_version().CompareTo(version) > 0)
250    return false;
251
252  // Passed the basic tests. Time to install it.
253  base::FilePath path = GetPnaclBaseDirectory().AppendASCII(
254      version.GetString());
255  if (file_util::PathExists(path)) {
256    LOG(WARNING) << "Target path already exists, not installing.";
257    return false;
258  }
259  if (!file_util::Move(unpack_path, path)) {
260    LOG(WARNING) << "Move failed, not installing.";
261    return false;
262  }
263
264  // Installation is done. Now tell the rest of chrome (just the path service
265  // for now). TODO(jvoung): we need notifications if someone surfed to a
266  // Pnacl webpage and Pnacl was just installed at this time. They should
267  // then be able to reload the page and retry (or something).
268  // See: http://code.google.com/p/chromium/issues/detail?id=107438
269  set_current_version(version);
270
271  OverrideDirPnaclComponent(path);
272  return true;
273}
274
275namespace {
276
277void DoCheckForUpdate(ComponentUpdateService* cus,
278                   const CrxComponent& pnacl) {
279  if (cus->CheckForUpdateSoon(pnacl) != ComponentUpdateService::kOk) {
280    LOG(WARNING) << "Pnacl check for update failed.";
281  }
282}
283
284// Finally, do the registration with the right version number.
285void FinishPnaclUpdateRegistration(const Version& current_version,
286                                   PnaclComponentInstaller* pci) {
287  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
288  CrxComponent pnacl_component;
289  pnacl_component.version = current_version;
290  pnacl_component.name = "pnacl";
291  pnacl_component.installer = pci;
292  pci->set_current_version(current_version);
293  SetPnaclHash(&pnacl_component);
294
295  ComponentUpdateService::Status status =
296      pci->cus()->RegisterComponent(pnacl_component);
297  if (status != ComponentUpdateService::kOk
298      && status != ComponentUpdateService::kReplaced) {
299    NOTREACHED() << "Pnacl component registration failed.";
300  }
301
302  // If PNaCl is not yet installed but it is requested by --enable-pnacl,
303  // we want it to be available "soon", so kick off an update check
304  // earlier than usual.
305  Version null_version(kNullVersion);
306  if (pci->current_version().Equals(null_version)) {
307    BrowserThread::PostDelayedTask(
308        BrowserThread::UI, FROM_HERE,
309        base::Bind(DoCheckForUpdate, pci->cus(), pnacl_component),
310        base::TimeDelta::FromSeconds(kInitialDelaySeconds));
311  }
312}
313
314// Check if there is an existing version on disk first to know when
315// a hosted version is actually newer.
316void StartPnaclUpdateRegistration(PnaclComponentInstaller* pci) {
317  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
318  base::FilePath path = pci->GetPnaclBaseDirectory();
319  if (!file_util::PathExists(path)) {
320    if (!file_util::CreateDirectory(path)) {
321      NOTREACHED() << "Could not create base Pnacl directory.";
322      return;
323    }
324  }
325
326  Version version(kNullVersion);
327  std::vector<base::FilePath> older_dirs;
328  if (GetLatestPnaclDirectory(pci, &path, &version, &older_dirs)) {
329    scoped_ptr<base::DictionaryValue> manifest(
330        ReadComponentManifest(path));
331    scoped_ptr<base::DictionaryValue> pnacl_manifest(
332        ReadPnaclManifest(path));
333    Version manifest_version;
334    // Check that the component manifest and PNaCl manifest files
335    // are legit, and that the indicated version matches the one
336    // encoded within the path name.
337    if (!CheckPnaclComponentManifest(*manifest, *pnacl_manifest,
338                                     &manifest_version)
339        || !version.Equals(manifest_version)) {
340      version = Version(kNullVersion);
341    } else {
342      OverrideDirPnaclComponent(path);
343    }
344  }
345
346  BrowserThread::PostTask(
347      BrowserThread::UI, FROM_HERE,
348      base::Bind(&FinishPnaclUpdateRegistration, version, pci));
349
350  // Remove older versions of PNaCl.
351  for (std::vector<base::FilePath>::iterator iter = older_dirs.begin();
352       iter != older_dirs.end(); ++iter) {
353    file_util::Delete(*iter, true);
354  }
355}
356
357void GetProfileInformation(PnaclComponentInstaller* pci) {
358  // Bail if not logged in yet.
359  if (!g_browser_process->profile_manager()->IsLoggedIn()) {
360    return;
361  }
362
363  pci->OnProfileChange();
364
365  BrowserThread::PostTask(
366     BrowserThread::FILE, FROM_HERE,
367     base::Bind(&StartPnaclUpdateRegistration, pci));
368}
369
370
371}  // namespace
372
373void PnaclComponentInstaller::RegisterPnaclComponent(
374                            ComponentUpdateService* cus,
375                            const CommandLine& command_line) {
376  // Only register when given the right flag.  This is important since
377  // we do an early component updater check above (in DoCheckForUpdate).
378  if (command_line.HasSwitch(switches::kEnablePnacl)) {
379    cus_ = cus;
380    // If per_user, create a profile observer to watch for logins.
381    // Only do so after cus_ is set to something non-null.
382    if (per_user_ && !profile_observer_) {
383      profile_observer_.reset(new PnaclProfileObserver(this));
384    }
385    if (per_user_) {
386      // Figure out profile information, before proceeding to look for files.
387      BrowserThread::PostTask(
388           BrowserThread::UI, FROM_HERE,
389           base::Bind(&GetProfileInformation, this));
390    } else {
391      BrowserThread::PostTask(
392           BrowserThread::FILE, FROM_HERE,
393           base::Bind(&StartPnaclUpdateRegistration, this));
394    }
395  }
396}
397
398void PnaclComponentInstaller::ReRegisterPnacl() {
399  // No need to check the commandline flags again here.
400  // We could only have gotten here after RegisterPnaclComponent
401  // found --enable-pnacl, since that is where we create the profile_observer_,
402  // which in turn calls ReRegisterPnacl.
403  DCHECK(per_user_);
404  // Figure out profile information, before proceeding to look for files.
405  BrowserThread::PostTask(
406      BrowserThread::UI, FROM_HERE,
407      base::Bind(&GetProfileInformation, this));
408}
409