Home | History | Annotate | Download | only in cocoa
      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 
     38 namespace {
     39 
     40 // Just like ScopedCFTypeRef but for io_object_t and subclasses.
     41 template<typename IOT>
     42 class 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.
     97 bool 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.
    197 bool 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.
    204 bool 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.
    232 AuthorizationRef 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.
    250 bool 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.
    313 bool 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 
    353 void 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 
    372 bool 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