// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/cocoa/install_from_dmg.h" #include #import #include #include #include #include #include #include #include "base/basictypes.h" #include "base/command_line.h" #include "base/logging.h" #import "base/mac/mac_util.h" #include "base/mac/scoped_nsautorelease_pool.h" #include "chrome/browser/cocoa/authorization_util.h" #include "chrome/browser/cocoa/scoped_authorizationref.h" #import "chrome/browser/cocoa/keystone_glue.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util_mac.h" // When C++ exceptions are disabled, the C++ library defines |try| and // |catch| so as to allow exception-expecting C++ code to build properly when // language support for exceptions is not present. These macros interfere // with the use of |@try| and |@catch| in Objective-C files such as this one. // Undefine these macros here, after everything has been #included, since // there will be no C++ uses and only Objective-C uses from this point on. #undef try #undef catch namespace { // Just like ScopedCFTypeRef but for io_object_t and subclasses. template class scoped_ioobject { public: typedef IOT element_type; explicit scoped_ioobject(IOT object = NULL) : object_(object) { } ~scoped_ioobject() { if (object_) IOObjectRelease(object_); } void reset(IOT object = NULL) { if (object_) IOObjectRelease(object_); object_ = object; } bool operator==(IOT that) const { return object_ == that; } bool operator!=(IOT that) const { return object_ != that; } operator IOT() const { return object_; } IOT get() const { return object_; } void swap(scoped_ioobject& that) { IOT temp = that.object_; that.object_ = object_; object_ = temp; } IOT release() { IOT temp = object_; object_ = NULL; return temp; } private: IOT object_; DISALLOW_COPY_AND_ASSIGN(scoped_ioobject); }; // Returns true if |path| is located on a read-only filesystem of a disk // image. Returns false if not, or in the event of an error. bool IsPathOnReadOnlyDiskImage(const char path[]) { struct statfs statfs_buf; if (statfs(path, &statfs_buf) != 0) { PLOG(ERROR) << "statfs " << path; return false; } if (!(statfs_buf.f_flags & MNT_RDONLY)) { // Not on a read-only filesystem. return false; } const char dev_root[] = "/dev/"; const int dev_root_length = arraysize(dev_root) - 1; if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) { // Not rooted at dev_root, no BSD name to search on. return false; } // BSD names in IOKit don't include dev_root. const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length; const mach_port_t master_port = kIOMasterPortDefault; // IOBSDNameMatching gives ownership of match_dict to the caller, but // IOServiceGetMatchingServices will assume that reference. CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port, 0, bsd_device_name); if (!match_dict) { LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name; return false; } io_iterator_t iterator_ref; kern_return_t kr = IOServiceGetMatchingServices(master_port, match_dict, &iterator_ref); if (kr != KERN_SUCCESS) { LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name << ": kernel error " << kr; return false; } scoped_ioobject iterator(iterator_ref); iterator_ref = NULL; // There needs to be exactly one matching service. scoped_ioobject filesystem_service(IOIteratorNext(iterator)); if (!filesystem_service) { LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service"; return false; } scoped_ioobject unexpected_service(IOIteratorNext(iterator)); if (unexpected_service) { LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services"; return false; } iterator.reset(); const char disk_image_class[] = "IOHDIXController"; // This is highly unlikely. The filesystem service is expected to be of // class IOMedia. Since the filesystem service's entire ancestor chain // will be checked, though, check the filesystem service's class itself. if (IOObjectConformsTo(filesystem_service, disk_image_class)) { return true; } kr = IORegistryEntryCreateIterator(filesystem_service, kIOServicePlane, kIORegistryIterateRecursively | kIORegistryIterateParents, &iterator_ref); if (kr != KERN_SUCCESS) { LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name << ": kernel error " << kr; return false; } iterator.reset(iterator_ref); iterator_ref = NULL; // Look at each of the filesystem service's ancestor services, beginning // with the parent, iterating all the way up to the device tree's root. If // any ancestor service matches the class used for disk images, the // filesystem resides on a disk image. for(scoped_ioobject ancestor_service(IOIteratorNext(iterator)); ancestor_service; ancestor_service.reset(IOIteratorNext(iterator))) { if (IOObjectConformsTo(ancestor_service, disk_image_class)) { return true; } } // The filesystem does not reside on a disk image. return false; } // Returns true if the application is located on a read-only filesystem of a // disk image. Returns false if not, or in the event of an error. bool IsAppRunningFromReadOnlyDiskImage() { return IsPathOnReadOnlyDiskImage( [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]); } // Shows a dialog asking the user whether or not to install from the disk // image. Returns true if the user approves installation. bool ShouldInstallDialog() { NSString* title = l10n_util::GetNSStringFWithFixup( IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); NSString* prompt = l10n_util::GetNSStringFWithFixup( IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES); NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO); NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert setAlertStyle:NSInformationalAlertStyle]; [alert setMessageText:title]; [alert setInformativeText:prompt]; [alert addButtonWithTitle:yes]; NSButton* cancel_button = [alert addButtonWithTitle:no]; [cancel_button setKeyEquivalent:@"\e"]; NSInteger result = [alert runModal]; return result == NSAlertFirstButtonReturn; } // Potentially shows an authorization dialog to request authentication to // copy. If application_directory appears to be unwritable, attempts to // obtain authorization, which may result in the display of the dialog. // Returns NULL if authorization is not performed because it does not appear // to be necessary because the user has permission to write to // application_directory. Returns NULL if authorization fails. AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) { NSFileManager* file_manager = [NSFileManager defaultManager]; if ([file_manager isWritableFileAtPath:application_directory]) { return NULL; } NSString* prompt = l10n_util::GetNSStringFWithFixup( IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); return authorization_util::AuthorizationCreateToRunAsRoot( base::mac::NSToCFCast(prompt)); } // Invokes the installer program at installer_path to copy source_path to // target_path and perform any additional on-disk bookkeeping needed to be // able to launch target_path properly. If authorization_arg is non-NULL, // function will assume ownership of it, will invoke the installer with that // authorization reference, and will attempt Keystone ticket promotion. bool InstallFromDiskImage(AuthorizationRef authorization_arg, NSString* installer_path, NSString* source_path, NSString* target_path) { scoped_AuthorizationRef authorization(authorization_arg); authorization_arg = NULL; int exit_status; if (authorization) { const char* installer_path_c = [installer_path fileSystemRepresentation]; const char* source_path_c = [source_path fileSystemRepresentation]; const char* target_path_c = [target_path fileSystemRepresentation]; const char* arguments[] = {source_path_c, target_path_c, NULL}; OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait( authorization, installer_path_c, kAuthorizationFlagDefaults, arguments, NULL, // pipe &exit_status); if (status != errAuthorizationSuccess) { LOG(ERROR) << "AuthorizationExecuteWithPrivileges install: " << status; return false; } } else { NSArray* arguments = [NSArray arrayWithObjects:source_path, target_path, nil]; NSTask* task; @try { task = [NSTask launchedTaskWithLaunchPath:installer_path arguments:arguments]; } @catch(NSException* exception) { LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: " << [[exception description] UTF8String]; return false; } [task waitUntilExit]; exit_status = [task terminationStatus]; } if (exit_status != 0) { LOG(ERROR) << "install.sh: exit status " << exit_status; return false; } if (authorization) { // As long as an AuthorizationRef is available, promote the Keystone // ticket. Inform KeystoneGlue of the new path to use. KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue]; [keystone_glue setAppPath:target_path]; [keystone_glue promoteTicketWithAuthorization:authorization.release() synchronous:YES]; } return true; } // Launches the application at app_path. The arguments passed to app_path // will be the same as the arguments used to invoke this process, except any // arguments beginning with -psn_ will be stripped. bool LaunchInstalledApp(NSString* app_path) { const UInt8* app_path_c = reinterpret_cast([app_path fileSystemRepresentation]); FSRef app_fsref; OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL); if (err != noErr) { LOG(ERROR) << "FSPathMakeRef: " << err; return false; } const std::vector& argv = CommandLine::ForCurrentProcess()->argv(); NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argv.size() - 1]; // Start at argv[1]. LSOpenApplication adds its own argv[0] as the path of // the launched executable. for (size_t index = 1; index < argv.size(); ++index) { std::string argument = argv[index]; const char psn_flag[] = "-psn_"; const int psn_flag_length = arraysize(psn_flag) - 1; if (argument.compare(0, psn_flag_length, psn_flag) != 0) { // Strip any -psn_ arguments, as they apply to a specific process. [arguments addObject:[NSString stringWithUTF8String:argument.c_str()]]; } } struct LSApplicationParameters parameters = {0}; parameters.flags = kLSLaunchDefaults; parameters.application = &app_fsref; parameters.argv = base::mac::NSToCFCast(arguments); err = LSOpenApplication(¶meters, NULL); if (err != noErr) { LOG(ERROR) << "LSOpenApplication: " << err; return false; } return true; } void ShowErrorDialog() { NSString* title = l10n_util::GetNSStringWithFixup( IDS_INSTALL_FROM_DMG_ERROR_TITLE); NSString* error = l10n_util::GetNSStringFWithFixup( IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK); NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert setAlertStyle:NSWarningAlertStyle]; [alert setMessageText:title]; [alert setInformativeText:error]; [alert addButtonWithTitle:ok]; [alert runModal]; } } // namespace bool MaybeInstallFromDiskImage() { base::mac::ScopedNSAutoreleasePool autorelease_pool; if (!IsAppRunningFromReadOnlyDiskImage()) { return false; } NSArray* application_directories = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES); if ([application_directories count] == 0) { LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: " << "no local application directories"; return false; } NSString* application_directory = [application_directories objectAtIndex:0]; NSFileManager* file_manager = [NSFileManager defaultManager]; BOOL is_directory; if (![file_manager fileExistsAtPath:application_directory isDirectory:&is_directory] || !is_directory) { VLOG(1) << "No application directory at " << [application_directory UTF8String]; return false; } NSString* source_path = [[NSBundle mainBundle] bundlePath]; NSString* application_name = [source_path lastPathComponent]; NSString* target_path = [application_directory stringByAppendingPathComponent:application_name]; if ([file_manager fileExistsAtPath:target_path]) { VLOG(1) << "Something already exists at " << [target_path UTF8String]; return false; } NSString* installer_path = [base::mac::MainAppBundle() pathForResource:@"install" ofType:@"sh"]; if (!installer_path) { VLOG(1) << "Could not locate install.sh"; return false; } if (!ShouldInstallDialog()) { return false; } scoped_AuthorizationRef authorization( MaybeShowAuthorizationDialog(application_directory)); // authorization will be NULL if it's deemed unnecessary or if // authentication fails. In either case, try to install without privilege // escalation. if (!InstallFromDiskImage(authorization.release(), installer_path, source_path, target_path) || !LaunchInstalledApp(target_path)) { ShowErrorDialog(); return false; } return true; }