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(¶meters, 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