1// Copyright (c) 2010 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/cocoa/install_from_dmg.h"
6
7#include <ApplicationServices/ApplicationServices.h>
8#import <AppKit/AppKit.h>
9#include <CoreFoundation/CoreFoundation.h>
10#include <CoreServices/CoreServices.h>
11#include <IOKit/IOKitLib.h>
12#include <string.h>
13#include <sys/param.h>
14#include <sys/mount.h>
15
16#include "base/basictypes.h"
17#include "base/command_line.h"
18#include "base/logging.h"
19#import "base/mac/mac_util.h"
20#include "base/mac/scoped_nsautorelease_pool.h"
21#include "chrome/browser/cocoa/authorization_util.h"
22#include "chrome/browser/cocoa/scoped_authorizationref.h"
23#import "chrome/browser/cocoa/keystone_glue.h"
24#include "grit/chromium_strings.h"
25#include "grit/generated_resources.h"
26#include "ui/base/l10n/l10n_util.h"
27#include "ui/base/l10n/l10n_util_mac.h"
28
29// When C++ exceptions are disabled, the C++ library defines |try| and
30// |catch| so as to allow exception-expecting C++ code to build properly when
31// language support for exceptions is not present.  These macros interfere
32// with the use of |@try| and |@catch| in Objective-C files such as this one.
33// Undefine these macros here, after everything has been #included, since
34// there will be no C++ uses and only Objective-C uses from this point on.
35#undef try
36#undef catch
37
38namespace {
39
40// Just like ScopedCFTypeRef but for io_object_t and subclasses.
41template<typename IOT>
42class scoped_ioobject {
43 public:
44  typedef IOT element_type;
45
46  explicit scoped_ioobject(IOT object = NULL)
47      : object_(object) {
48  }
49
50  ~scoped_ioobject() {
51    if (object_)
52      IOObjectRelease(object_);
53  }
54
55  void reset(IOT object = NULL) {
56    if (object_)
57      IOObjectRelease(object_);
58    object_ = object;
59  }
60
61  bool operator==(IOT that) const {
62    return object_ == that;
63  }
64
65  bool operator!=(IOT that) const {
66    return object_ != that;
67  }
68
69  operator IOT() const {
70    return object_;
71  }
72
73  IOT get() const {
74    return object_;
75  }
76
77  void swap(scoped_ioobject& that) {
78    IOT temp = that.object_;
79    that.object_ = object_;
80    object_ = temp;
81  }
82
83  IOT release() {
84    IOT temp = object_;
85    object_ = NULL;
86    return temp;
87  }
88
89 private:
90  IOT object_;
91
92  DISALLOW_COPY_AND_ASSIGN(scoped_ioobject);
93};
94
95// Returns true if |path| is located on a read-only filesystem of a disk
96// image.  Returns false if not, or in the event of an error.
97bool IsPathOnReadOnlyDiskImage(const char path[]) {
98  struct statfs statfs_buf;
99  if (statfs(path, &statfs_buf) != 0) {
100    PLOG(ERROR) << "statfs " << path;
101    return false;
102  }
103
104  if (!(statfs_buf.f_flags & MNT_RDONLY)) {
105    // Not on a read-only filesystem.
106    return false;
107  }
108
109  const char dev_root[] = "/dev/";
110  const int dev_root_length = arraysize(dev_root) - 1;
111  if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
112    // Not rooted at dev_root, no BSD name to search on.
113    return false;
114  }
115
116  // BSD names in IOKit don't include dev_root.
117  const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
118
119  const mach_port_t master_port = kIOMasterPortDefault;
120
121  // IOBSDNameMatching gives ownership of match_dict to the caller, but
122  // IOServiceGetMatchingServices will assume that reference.
123  CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
124                                                        0,
125                                                        bsd_device_name);
126  if (!match_dict) {
127    LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name;
128    return false;
129  }
130
131  io_iterator_t iterator_ref;
132  kern_return_t kr = IOServiceGetMatchingServices(master_port,
133                                                  match_dict,
134                                                  &iterator_ref);
135  if (kr != KERN_SUCCESS) {
136    LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name
137               << ": kernel error " << kr;
138    return false;
139  }
140  scoped_ioobject<io_iterator_t> iterator(iterator_ref);
141  iterator_ref = NULL;
142
143  // There needs to be exactly one matching service.
144  scoped_ioobject<io_service_t> filesystem_service(IOIteratorNext(iterator));
145  if (!filesystem_service) {
146    LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service";
147    return false;
148  }
149  scoped_ioobject<io_service_t> unexpected_service(IOIteratorNext(iterator));
150  if (unexpected_service) {
151    LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services";
152    return false;
153  }
154
155  iterator.reset();
156
157  const char disk_image_class[] = "IOHDIXController";
158
159  // This is highly unlikely.  The filesystem service is expected to be of
160  // class IOMedia.  Since the filesystem service's entire ancestor chain
161  // will be checked, though, check the filesystem service's class itself.
162  if (IOObjectConformsTo(filesystem_service, disk_image_class)) {
163    return true;
164  }
165
166  kr = IORegistryEntryCreateIterator(filesystem_service,
167                                     kIOServicePlane,
168                                     kIORegistryIterateRecursively |
169                                         kIORegistryIterateParents,
170                                     &iterator_ref);
171  if (kr != KERN_SUCCESS) {
172    LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name
173               << ": kernel error " << kr;
174    return false;
175  }
176  iterator.reset(iterator_ref);
177  iterator_ref = NULL;
178
179  // Look at each of the filesystem service's ancestor services, beginning
180  // with the parent, iterating all the way up to the device tree's root.  If
181  // any ancestor service matches the class used for disk images, the
182  // filesystem resides on a disk image.
183  for(scoped_ioobject<io_service_t> ancestor_service(IOIteratorNext(iterator));
184      ancestor_service;
185      ancestor_service.reset(IOIteratorNext(iterator))) {
186    if (IOObjectConformsTo(ancestor_service, disk_image_class)) {
187      return true;
188    }
189  }
190
191  // The filesystem does not reside on a disk image.
192  return false;
193}
194
195// Returns true if the application is located on a read-only filesystem of a
196// disk image.  Returns false if not, or in the event of an error.
197bool IsAppRunningFromReadOnlyDiskImage() {
198  return IsPathOnReadOnlyDiskImage(
199      [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]);
200}
201
202// Shows a dialog asking the user whether or not to install from the disk
203// image.  Returns true if the user approves installation.
204bool ShouldInstallDialog() {
205  NSString* title = l10n_util::GetNSStringFWithFixup(
206      IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
207  NSString* prompt = l10n_util::GetNSStringFWithFixup(
208      IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
209  NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
210  NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
211
212  NSAlert* alert = [[[NSAlert alloc] init] autorelease];
213
214  [alert setAlertStyle:NSInformationalAlertStyle];
215  [alert setMessageText:title];
216  [alert setInformativeText:prompt];
217  [alert addButtonWithTitle:yes];
218  NSButton* cancel_button = [alert addButtonWithTitle:no];
219  [cancel_button setKeyEquivalent:@"\e"];
220
221  NSInteger result = [alert runModal];
222
223  return result == NSAlertFirstButtonReturn;
224}
225
226// Potentially shows an authorization dialog to request authentication to
227// copy.  If application_directory appears to be unwritable, attempts to
228// obtain authorization, which may result in the display of the dialog.
229// Returns NULL if authorization is not performed because it does not appear
230// to be necessary because the user has permission to write to
231// application_directory.  Returns NULL if authorization fails.
232AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
233  NSFileManager* file_manager = [NSFileManager defaultManager];
234  if ([file_manager isWritableFileAtPath:application_directory]) {
235    return NULL;
236  }
237
238  NSString* prompt = l10n_util::GetNSStringFWithFixup(
239      IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
240      l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
241  return authorization_util::AuthorizationCreateToRunAsRoot(
242      base::mac::NSToCFCast(prompt));
243}
244
245// Invokes the installer program at installer_path to copy source_path to
246// target_path and perform any additional on-disk bookkeeping needed to be
247// able to launch target_path properly.  If authorization_arg is non-NULL,
248// function will assume ownership of it, will invoke the installer with that
249// authorization reference, and will attempt Keystone ticket promotion.
250bool InstallFromDiskImage(AuthorizationRef authorization_arg,
251                          NSString* installer_path,
252                          NSString* source_path,
253                          NSString* target_path) {
254  scoped_AuthorizationRef authorization(authorization_arg);
255  authorization_arg = NULL;
256  int exit_status;
257  if (authorization) {
258    const char* installer_path_c = [installer_path fileSystemRepresentation];
259    const char* source_path_c = [source_path fileSystemRepresentation];
260    const char* target_path_c = [target_path fileSystemRepresentation];
261    const char* arguments[] = {source_path_c, target_path_c, NULL};
262
263    OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
264        authorization,
265        installer_path_c,
266        kAuthorizationFlagDefaults,
267        arguments,
268        NULL,  // pipe
269        &exit_status);
270    if (status != errAuthorizationSuccess) {
271      LOG(ERROR) << "AuthorizationExecuteWithPrivileges install: " << status;
272      return false;
273    }
274  } else {
275    NSArray* arguments = [NSArray arrayWithObjects:source_path,
276                                                   target_path,
277                                                   nil];
278
279    NSTask* task;
280    @try {
281      task = [NSTask launchedTaskWithLaunchPath:installer_path
282                                      arguments:arguments];
283    } @catch(NSException* exception) {
284      LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
285                 << [[exception description] UTF8String];
286      return false;
287    }
288
289    [task waitUntilExit];
290    exit_status = [task terminationStatus];
291  }
292
293  if (exit_status != 0) {
294    LOG(ERROR) << "install.sh: exit status " << exit_status;
295    return false;
296  }
297
298  if (authorization) {
299    // As long as an AuthorizationRef is available, promote the Keystone
300    // ticket.  Inform KeystoneGlue of the new path to use.
301    KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
302    [keystone_glue setAppPath:target_path];
303    [keystone_glue promoteTicketWithAuthorization:authorization.release()
304                                      synchronous:YES];
305  }
306
307  return true;
308}
309
310// Launches the application at app_path.  The arguments passed to app_path
311// will be the same as the arguments used to invoke this process, except any
312// arguments beginning with -psn_ will be stripped.
313bool LaunchInstalledApp(NSString* app_path) {
314  const UInt8* app_path_c =
315      reinterpret_cast<const UInt8*>([app_path fileSystemRepresentation]);
316  FSRef app_fsref;
317  OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL);
318  if (err != noErr) {
319    LOG(ERROR) << "FSPathMakeRef: " << err;
320    return false;
321  }
322
323  const std::vector<std::string>& argv =
324      CommandLine::ForCurrentProcess()->argv();
325  NSMutableArray* arguments =
326      [NSMutableArray arrayWithCapacity:argv.size() - 1];
327  // Start at argv[1].  LSOpenApplication adds its own argv[0] as the path of
328  // the launched executable.
329  for (size_t index = 1; index < argv.size(); ++index) {
330    std::string argument = argv[index];
331    const char psn_flag[] = "-psn_";
332    const int psn_flag_length = arraysize(psn_flag) - 1;
333    if (argument.compare(0, psn_flag_length, psn_flag) != 0) {
334      // Strip any -psn_ arguments, as they apply to a specific process.
335      [arguments addObject:[NSString stringWithUTF8String:argument.c_str()]];
336    }
337  }
338
339  struct LSApplicationParameters parameters = {0};
340  parameters.flags = kLSLaunchDefaults;
341  parameters.application = &app_fsref;
342  parameters.argv = base::mac::NSToCFCast(arguments);
343
344  err = LSOpenApplication(&parameters, NULL);
345  if (err != noErr) {
346    LOG(ERROR) << "LSOpenApplication: " << err;
347    return false;
348  }
349
350  return true;
351}
352
353void ShowErrorDialog() {
354  NSString* title = l10n_util::GetNSStringWithFixup(
355      IDS_INSTALL_FROM_DMG_ERROR_TITLE);
356  NSString* error = l10n_util::GetNSStringFWithFixup(
357      IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
358  NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
359
360  NSAlert* alert = [[[NSAlert alloc] init] autorelease];
361
362  [alert setAlertStyle:NSWarningAlertStyle];
363  [alert setMessageText:title];
364  [alert setInformativeText:error];
365  [alert addButtonWithTitle:ok];
366
367  [alert runModal];
368}
369
370}  // namespace
371
372bool MaybeInstallFromDiskImage() {
373  base::mac::ScopedNSAutoreleasePool autorelease_pool;
374
375  if (!IsAppRunningFromReadOnlyDiskImage()) {
376    return false;
377  }
378
379  NSArray* application_directories =
380      NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
381                                          NSLocalDomainMask,
382                                          YES);
383  if ([application_directories count] == 0) {
384    LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
385               << "no local application directories";
386    return false;
387  }
388  NSString* application_directory = [application_directories objectAtIndex:0];
389
390  NSFileManager* file_manager = [NSFileManager defaultManager];
391
392  BOOL is_directory;
393  if (![file_manager fileExistsAtPath:application_directory
394                          isDirectory:&is_directory] ||
395      !is_directory) {
396    VLOG(1) << "No application directory at "
397            << [application_directory UTF8String];
398    return false;
399  }
400
401  NSString* source_path = [[NSBundle mainBundle] bundlePath];
402  NSString* application_name = [source_path lastPathComponent];
403  NSString* target_path =
404      [application_directory stringByAppendingPathComponent:application_name];
405
406  if ([file_manager fileExistsAtPath:target_path]) {
407    VLOG(1) << "Something already exists at " << [target_path UTF8String];
408    return false;
409  }
410
411  NSString* installer_path =
412      [base::mac::MainAppBundle() pathForResource:@"install" ofType:@"sh"];
413  if (!installer_path) {
414    VLOG(1) << "Could not locate install.sh";
415    return false;
416  }
417
418  if (!ShouldInstallDialog()) {
419    return false;
420  }
421
422  scoped_AuthorizationRef authorization(
423      MaybeShowAuthorizationDialog(application_directory));
424  // authorization will be NULL if it's deemed unnecessary or if
425  // authentication fails.  In either case, try to install without privilege
426  // escalation.
427
428  if (!InstallFromDiskImage(authorization.release(),
429                            installer_path,
430                            source_path,
431                            target_path) ||
432      !LaunchInstalledApp(target_path)) {
433    ShowErrorDialog();
434    return false;
435  }
436
437  return true;
438}
439