1 // Copyright 2014 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/chromeos/app_mode/kiosk_external_updater.h"
6 
7 #include "base/bind.h"
8 #include "base/files/file_util.h"
9 #include "base/json/json_file_value_serializer.h"
10 #include "base/location.h"
11 #include "base/logging.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/task_runner_util.h"
14 #include "base/version.h"
15 #include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h"
16 #include "chrome/browser/chromeos/ui/kiosk_external_update_notification.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "components/version_info/version_info.h"
19 #include "content/public/browser/browser_thread.h"
20 #include "extensions/browser/sandboxed_unpacker.h"
21 #include "extensions/common/extension.h"
22 #include "extensions/common/verifier_formats.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/resource/resource_bundle.h"
25 
26 namespace chromeos {
27 
28 namespace {
29 
30 constexpr base::FilePath::CharType kExternalUpdateManifest[] =
31     "external_update.json";
32 constexpr char kExternalCrx[] = "external_crx";
33 constexpr char kExternalVersion[] = "external_version";
34 
35 std::pair<std::unique_ptr<base::DictionaryValue>,
36           KioskExternalUpdater::ExternalUpdateErrorCode>
ParseExternalUpdateManifest(const base::FilePath & external_update_dir)37 ParseExternalUpdateManifest(const base::FilePath& external_update_dir) {
38   base::FilePath manifest = external_update_dir.Append(kExternalUpdateManifest);
39   if (!base::PathExists(manifest)) {
40     return std::make_pair(nullptr, KioskExternalUpdater::ERROR_NO_MANIFEST);
41   }
42 
43   JSONFileValueDeserializer deserializer(manifest);
44   std::unique_ptr<base::DictionaryValue> extensions =
45       base::DictionaryValue::From(deserializer.Deserialize(nullptr, nullptr));
46   if (!extensions) {
47     return std::make_pair(nullptr,
48                           KioskExternalUpdater::ERROR_INVALID_MANIFEST);
49   }
50 
51   return std::make_pair(std::move(extensions),
52                         KioskExternalUpdater::ERROR_NONE);
53 }
54 
55 // Copies |external_crx_file| to |temp_crx_file|, and removes |temp_dir|
56 // created for unpacking |external_crx_file|.
CopyExternalCrxAndDeleteTempDir(const base::FilePath & external_crx_file,const base::FilePath & temp_crx_file,const base::FilePath & temp_dir)57 bool CopyExternalCrxAndDeleteTempDir(const base::FilePath& external_crx_file,
58                                      const base::FilePath& temp_crx_file,
59                                      const base::FilePath& temp_dir) {
60   base::DeletePathRecursively(temp_dir);
61   return base::CopyFile(external_crx_file, temp_crx_file);
62 }
63 
64 // Returns true if |version_1| < |version_2|, and
65 // if |update_for_same_version| is true and |version_1| = |version_2|.
ShouldUpdateForHigherVersion(const std::string & version_1,const std::string & version_2,bool update_for_same_version)66 bool ShouldUpdateForHigherVersion(const std::string& version_1,
67                                   const std::string& version_2,
68                                   bool update_for_same_version) {
69   const base::Version v1(version_1);
70   const base::Version v2(version_2);
71   if (!v1.IsValid() || !v2.IsValid())
72     return false;
73   int compare_result = v1.CompareTo(v2);
74   if (compare_result < 0)
75     return true;
76   return update_for_same_version && compare_result == 0;
77 }
78 
79 }  // namespace
80 
ExternalUpdate()81 KioskExternalUpdater::ExternalUpdate::ExternalUpdate() {
82 }
83 
84 KioskExternalUpdater::ExternalUpdate::ExternalUpdate(
85     const ExternalUpdate& other) = default;
86 
~ExternalUpdate()87 KioskExternalUpdater::ExternalUpdate::~ExternalUpdate() {
88 }
89 
KioskExternalUpdater(const scoped_refptr<base::SequencedTaskRunner> & backend_task_runner,const base::FilePath & crx_cache_dir,const base::FilePath & crx_unpack_dir)90 KioskExternalUpdater::KioskExternalUpdater(
91     const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner,
92     const base::FilePath& crx_cache_dir,
93     const base::FilePath& crx_unpack_dir)
94     : backend_task_runner_(backend_task_runner),
95       crx_cache_dir_(crx_cache_dir),
96       crx_unpack_dir_(crx_unpack_dir) {
97   // Subscribe to DiskMountManager.
98   DCHECK(disks::DiskMountManager::GetInstance());
99   disks::DiskMountManager::GetInstance()->AddObserver(this);
100 }
101 
~KioskExternalUpdater()102 KioskExternalUpdater::~KioskExternalUpdater() {
103   if (disks::DiskMountManager::GetInstance())
104     disks::DiskMountManager::GetInstance()->RemoveObserver(this);
105 }
106 
OnMountEvent(disks::DiskMountManager::MountEvent event,MountError error_code,const disks::DiskMountManager::MountPointInfo & mount_info)107 void KioskExternalUpdater::OnMountEvent(
108     disks::DiskMountManager::MountEvent event,
109     MountError error_code,
110     const disks::DiskMountManager::MountPointInfo& mount_info) {
111   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
112 
113   if (mount_info.mount_type != MOUNT_TYPE_DEVICE ||
114       error_code != MOUNT_ERROR_NONE) {
115     return;
116   }
117 
118   if (event == disks::DiskMountManager::MOUNTING) {
119     // If multiple disks have been mounted, skip the rest of them if kiosk
120     // update has already been found.
121     if (!external_update_path_.empty()) {
122       LOG(WARNING) << "External update path already found, skip "
123                    << mount_info.mount_path;
124       return;
125     }
126 
127     base::PostTaskAndReplyWithResult(
128         backend_task_runner_.get(), FROM_HERE,
129         base::BindOnce(&ParseExternalUpdateManifest,
130                        base::FilePath(mount_info.mount_path)),
131         base::BindOnce(&KioskExternalUpdater::ProcessParsedManifest,
132                        weak_factory_.GetWeakPtr(),
133                        base::FilePath(mount_info.mount_path)));
134     return;
135   }
136 
137   // unmounting a removable device case.
138   if (external_update_path_.value().empty()) {
139     // Clear any previously displayed message.
140     DismissKioskUpdateNotification();
141   } else if (external_update_path_.value() == mount_info.mount_path) {
142     DismissKioskUpdateNotification();
143     if (IsExternalUpdatePending()) {
144       LOG(ERROR) << "External kiosk update is not completed when the usb "
145                  << "stick is unmoutned.";
146     }
147     external_updates_.clear();
148     external_update_path_.clear();
149   }
150 }
151 
OnExternalUpdateUnpackSuccess(const std::string & app_id,const std::string & version,const std::string & min_browser_version,const base::FilePath & temp_dir)152 void KioskExternalUpdater::OnExternalUpdateUnpackSuccess(
153     const std::string& app_id,
154     const std::string& version,
155     const std::string& min_browser_version,
156     const base::FilePath& temp_dir) {
157   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
158 
159   // User might pull out the usb stick before updating is completed.
160   if (CheckExternalUpdateInterrupted())
161     return;
162 
163   if (!ShouldDoExternalUpdate(app_id, version, min_browser_version)) {
164     external_updates_[app_id].update_status = FAILED;
165     MaybeValidateNextExternalUpdate();
166     return;
167   }
168 
169   // User might pull out the usb stick before updating is completed.
170   if (CheckExternalUpdateInterrupted())
171     return;
172 
173   base::FilePath external_crx_path =
174       external_updates_[app_id].external_crx.path;
175   base::FilePath temp_crx_path =
176       crx_unpack_dir_.Append(external_crx_path.BaseName());
177   base::PostTaskAndReplyWithResult(
178       backend_task_runner_.get(), FROM_HERE,
179       base::BindOnce(&CopyExternalCrxAndDeleteTempDir, external_crx_path,
180                      temp_crx_path, temp_dir),
181       base::BindOnce(&KioskExternalUpdater::PutValidatedExtension,
182                      weak_factory_.GetWeakPtr(), app_id, temp_crx_path,
183                      version));
184 }
185 
OnExternalUpdateUnpackFailure(const std::string & app_id)186 void KioskExternalUpdater::OnExternalUpdateUnpackFailure(
187     const std::string& app_id) {
188   // User might pull out the usb stick before updating is completed.
189   if (CheckExternalUpdateInterrupted())
190     return;
191 
192   external_updates_[app_id].update_status = FAILED;
193   external_updates_[app_id].error =
194       ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
195           IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX);
196   MaybeValidateNextExternalUpdate();
197 }
198 
ProcessParsedManifest(const base::FilePath & external_update_dir,const ParseManifestResult & result)199 void KioskExternalUpdater::ProcessParsedManifest(
200     const base::FilePath& external_update_dir,
201     const ParseManifestResult& result) {
202   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
203 
204   const std::unique_ptr<base::DictionaryValue>& parsed_manifest = result.first;
205   ExternalUpdateErrorCode parsing_error = result.second;
206   if (parsing_error == ERROR_NO_MANIFEST) {
207     KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
208     return;
209   }
210   if (parsing_error == ERROR_INVALID_MANIFEST) {
211     NotifyKioskUpdateProgress(
212         ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
213             IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST));
214     KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
215     return;
216   }
217 
218   NotifyKioskUpdateProgress(
219       ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
220           IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS));
221 
222   external_update_path_ = external_update_dir;
223   for (base::DictionaryValue::Iterator it(*parsed_manifest); !it.IsAtEnd();
224        it.Advance()) {
225     std::string app_id = it.key();
226     std::string cached_version_str;
227     base::FilePath cached_crx;
228     if (!KioskAppManager::Get()->GetCachedCrx(
229             app_id, &cached_crx, &cached_version_str)) {
230       LOG(WARNING) << "Can't find app in existing cache " << app_id;
231       continue;
232     }
233 
234     const base::DictionaryValue* extension = nullptr;
235     if (!it.value().GetAsDictionary(&extension)) {
236       LOG(ERROR) << "Found bad entry in manifest type " << it.value().type();
237       continue;
238     }
239 
240     std::string external_crx_str;
241     if (!extension->GetString(kExternalCrx, &external_crx_str)) {
242       LOG(ERROR) << "Can't find external crx in manifest " << app_id;
243       continue;
244     }
245 
246     std::string external_version_str;
247     if (extension->GetString(kExternalVersion, &external_version_str)) {
248       if (!ShouldUpdateForHigherVersion(
249               cached_version_str, external_version_str, false)) {
250         LOG(WARNING) << "External app " << app_id
251                      << " is at the same or lower version comparing to "
252                      << " the existing one.";
253         continue;
254       }
255     }
256 
257     ExternalUpdate update;
258     KioskAppManager::App app;
259     if (KioskAppManager::Get()->GetApp(app_id, &app)) {
260       update.app_name = app.name;
261     } else {
262       NOTREACHED();
263     }
264     update.external_crx = extensions::CRXFileInfo(
265         external_update_path_.AppendASCII(external_crx_str),
266         extensions::GetExternalVerifierFormat());
267     update.external_crx.extension_id = app_id;
268     update.update_status = PENDING;
269     external_updates_[app_id] = update;
270   }
271 
272   if (external_updates_.empty()) {
273     NotifyKioskUpdateProgress(
274         ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
275             IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES));
276     KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
277     return;
278   }
279 
280   ValidateExternalUpdates();
281 }
282 
CheckExternalUpdateInterrupted()283 bool KioskExternalUpdater::CheckExternalUpdateInterrupted() {
284   if (external_updates_.empty()) {
285     // This could happen if user pulls out the usb stick before the updating
286     // operation is completed.
287     LOG(ERROR) << "external_updates_ has been cleared before external "
288                << "updating completes.";
289     return true;
290   }
291 
292   return false;
293 }
294 
ValidateExternalUpdates()295 void KioskExternalUpdater::ValidateExternalUpdates() {
296   for (const auto& it : external_updates_) {
297     const ExternalUpdate& update = it.second;
298     if (update.update_status == PENDING) {
299       auto crx_validator = base::MakeRefCounted<KioskExternalUpdateValidator>(
300           backend_task_runner_, update.external_crx, crx_unpack_dir_,
301           weak_factory_.GetWeakPtr());
302       crx_validator->Start();
303       break;
304     }
305   }
306 }
307 
IsExternalUpdatePending() const308 bool KioskExternalUpdater::IsExternalUpdatePending() const {
309   for (const auto& it : external_updates_) {
310     if (it.second.update_status == PENDING)
311       return true;
312   }
313   return false;
314 }
315 
IsAllExternalUpdatesSucceeded() const316 bool KioskExternalUpdater::IsAllExternalUpdatesSucceeded() const {
317   for (const auto& it : external_updates_) {
318     if (it.second.update_status != SUCCESS)
319       return false;
320   }
321   return true;
322 }
323 
ShouldDoExternalUpdate(const std::string & app_id,const std::string & version,const std::string & min_browser_version)324 bool KioskExternalUpdater::ShouldDoExternalUpdate(
325     const std::string& app_id,
326     const std::string& version,
327     const std::string& min_browser_version) {
328   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
329 
330   std::string existing_version_str;
331   base::FilePath existing_path;
332   bool cached = KioskAppManager::Get()->GetCachedCrx(
333       app_id, &existing_path, &existing_version_str);
334   DCHECK(cached);
335 
336   // Compare app version.
337   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
338   if (!ShouldUpdateForHigherVersion(existing_version_str, version, false)) {
339     external_updates_[app_id].error = rb->GetLocalizedString(
340         IDS_KIOSK_EXTERNAL_UPDATE_SAME_OR_LOWER_APP_VERSION);
341     return false;
342   }
343 
344   // Check minimum browser version.
345   if (!min_browser_version.empty() &&
346       !ShouldUpdateForHigherVersion(min_browser_version,
347                                     version_info::GetVersionNumber(), true)) {
348     external_updates_[app_id].error = l10n_util::GetStringFUTF16(
349         IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION,
350         base::UTF8ToUTF16(min_browser_version));
351     return false;
352   }
353 
354   return true;
355 }
356 
PutValidatedExtension(const std::string & app_id,const base::FilePath & crx_file,const std::string & version,bool crx_copied)357 void KioskExternalUpdater::PutValidatedExtension(const std::string& app_id,
358                                                  const base::FilePath& crx_file,
359                                                  const std::string& version,
360                                                  bool crx_copied) {
361   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
362 
363   if (CheckExternalUpdateInterrupted())
364     return;
365 
366   if (!crx_copied) {
367     LOG(ERROR) << "Cannot copy external crx file to " << crx_file.value();
368     external_updates_[app_id].update_status = FAILED;
369     external_updates_[app_id].error = l10n_util::GetStringFUTF16(
370         IDS_KIOSK_EXTERNAL_UPDATE_FAILED_COPY_CRX_TO_TEMP,
371         base::UTF8ToUTF16(crx_file.value()));
372     MaybeValidateNextExternalUpdate();
373     return;
374   }
375 
376   KioskAppManager::Get()->PutValidatedExternalExtension(
377       app_id, crx_file, version,
378       base::BindOnce(&KioskExternalUpdater::OnPutValidatedExtension,
379                      weak_factory_.GetWeakPtr()));
380 }
381 
OnPutValidatedExtension(const std::string & app_id,bool success)382 void KioskExternalUpdater::OnPutValidatedExtension(const std::string& app_id,
383                                                    bool success) {
384   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
385 
386   if (CheckExternalUpdateInterrupted())
387     return;
388 
389   if (!success) {
390     external_updates_[app_id].update_status = FAILED;
391     external_updates_[app_id].error = l10n_util::GetStringFUTF16(
392         IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_INSTALL_IN_LOCAL_CACHE,
393         base::UTF8ToUTF16(external_updates_[app_id].external_crx.path.value()));
394   } else {
395     external_updates_[app_id].update_status = SUCCESS;
396   }
397 
398   // Validate the next pending external update.
399   MaybeValidateNextExternalUpdate();
400 }
401 
MaybeValidateNextExternalUpdate()402 void KioskExternalUpdater::MaybeValidateNextExternalUpdate() {
403   if (IsExternalUpdatePending())
404     ValidateExternalUpdates();
405   else
406     MayBeNotifyKioskAppUpdate();
407 }
408 
MayBeNotifyKioskAppUpdate()409 void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() {
410   if (IsExternalUpdatePending())
411     return;
412 
413   NotifyKioskUpdateProgress(GetUpdateReportMessage());
414   NotifyKioskAppUpdateAvailable();
415   KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(
416       IsAllExternalUpdatesSucceeded());
417 }
418 
NotifyKioskAppUpdateAvailable()419 void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() {
420   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
421 
422   for (const auto& it : external_updates_) {
423     if (it.second.update_status == SUCCESS) {
424       KioskAppManager::Get()->OnKioskAppCacheUpdated(it.first);
425     }
426   }
427 }
428 
NotifyKioskUpdateProgress(const base::string16 & message)429 void KioskExternalUpdater::NotifyKioskUpdateProgress(
430     const base::string16& message) {
431   if (!notification_)
432     notification_ = std::make_unique<KioskExternalUpdateNotification>(message);
433   else
434     notification_->ShowMessage(message);
435 }
436 
DismissKioskUpdateNotification()437 void KioskExternalUpdater::DismissKioskUpdateNotification() {
438   if (notification_.get()) {
439     notification_.reset();
440   }
441 }
442 
GetUpdateReportMessage() const443 base::string16 KioskExternalUpdater::GetUpdateReportMessage() const {
444   DCHECK(!IsExternalUpdatePending());
445   int updated = 0;
446   int failed = 0;
447   base::string16 updated_apps;
448   base::string16 failed_apps;
449   for (const auto& it : external_updates_) {
450     const ExternalUpdate& update = it.second;
451     base::string16 app_name = base::UTF8ToUTF16(update.app_name);
452     if (update.update_status == SUCCESS) {
453       ++updated;
454       if (updated_apps.empty())
455         updated_apps = app_name;
456       else
457         updated_apps += base::ASCIIToUTF16(", ") + app_name;
458     } else {  // FAILED
459       ++failed;
460       if (failed_apps.empty()) {
461         failed_apps = app_name + base::ASCIIToUTF16(": ") + update.error;
462       } else {
463         failed_apps += base::ASCIIToUTF16("\n") + app_name +
464                        base::ASCIIToUTF16(": ") + update.error;
465       }
466     }
467   }
468 
469   base::string16 message =
470       ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
471           IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE);
472   if (updated) {
473     base::string16 success_app_msg = l10n_util::GetStringFUTF16(
474         IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS, updated_apps);
475     message += base::ASCIIToUTF16("\n") + success_app_msg;
476   }
477 
478   if (failed) {
479     base::string16 failed_app_msg =
480         ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
481             IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS) +
482         base::ASCIIToUTF16("\n") + failed_apps;
483     message += base::ASCIIToUTF16("\n") + failed_app_msg;
484   }
485   return message;
486 }
487 
488 }  // namespace chromeos
489