/.attic/sshfs-gui/DiskImageUtilities.m
Objective C | 341 lines | 218 code | 60 blank | 63 comment | 53 complexity | eff1ef530efaa965704536db72bc0962 MD5 | raw file
1// sshfs.app 2// Copyright 2007, Google Inc. 3// 4// Redistribution and use in source and binary forms, with or without 5// modification, are permitted provided that the following conditions are met: 6// 7// 1. Redistributions of source code must retain the above copyright notice, 8// this list of conditions and the following disclaimer. 9// 2. Redistributions in binary form must reproduce the above copyright notice, 10// this list of conditions and the following disclaimer in the documentation 11// and/or other materials provided with the distribution. 12// 3. The name of the author may not be used to endorse or promote products 13// derived from this software without specific prior written permission. 14// 15// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 16// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 17// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 18// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 23// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 26#include <unistd.h> 27#include <Security/Security.h> 28#include <SecurityFoundation/SFAuthorization.h> 29 30#import "DiskImageUtilities.h" 31 32static NSString* const kHDIUtilPath = @"/usr/bin/hdiutil"; 33 34@interface DiskImageUtilities (PrivateMethods_handleApplicationLaunchFromReadOnlyDiskImage) 35+ (BOOL)canWriteToPath:(NSString *)path; 36+ (void)copyAndLaunchAppAtPath:(NSString *)oldPath 37 toDirectoryPath:(NSString *)newDirectory; 38+ (void)killAppAtPath:(NSString *)appPath; 39+ (BOOL)doAuthorizedCopyFromPath:(NSString *)src toPath:(NSString *)dest; 40@end 41 42@implementation DiskImageUtilities 43 44// get the mounted disk image info as a dictionary 45+ (NSDictionary *)diskImageInfo { 46 47 NSDictionary *resultDict = nil; 48 49 NSArray* args = [NSArray arrayWithObjects:@"info", @"-plist", nil]; 50 // -plist means the results will be written as a property list to stdout 51 52 NSPipe* outputPipe = [NSPipe pipe]; 53 NSTask* theTask = [[[NSTask alloc] init] autorelease]; 54 [theTask setLaunchPath:kHDIUtilPath]; 55 [theTask setArguments:args]; 56 [theTask setStandardOutput:outputPipe]; 57 58 [theTask launch]; 59 60 NSFileHandle *outputFile = [outputPipe fileHandleForReading]; 61 NSData *plistData = nil; 62 @try { 63 plistData = [outputFile readDataToEndOfFile]; // blocks until EOF delivered 64 } 65 @catch(id obj) { 66 // running in gdb we get exception: Interrupted system call 67 NSLog(@"DiskImageUtilities diskImageInfo: gdb issue -- " 68 "getting file data causes exception: %@", obj); 69 } 70 [theTask waitUntilExit]; 71 int status = [theTask terminationStatus]; 72 73 if (status != 0 || [plistData length] == 0) { 74 75 NSLog(@"DiskImageUtilities diskImageInfo: hdiutil failed, result %d", status); 76 77 } else { 78 NSString *plist = [[[NSString alloc] initWithData:plistData 79 encoding:NSUTF8StringEncoding] autorelease]; 80 resultDict = [plist propertyList]; 81 } 82 return resultDict; 83} 84 85+ (NSArray *)readOnlyDiskImagePaths { 86 87 NSMutableArray *paths = [NSMutableArray array]; 88 NSDictionary *dict = [self diskImageInfo]; 89 if (dict) { 90 91 NSArray *imagesArray = [dict objectForKey:@"images"]; 92 93 // we have an array of dicts for the known images 94 // 95 // we want to find non-writable images, and get the mount 96 // points from their system entities 97 98 if (imagesArray) { 99 int idx; 100 unsigned int numberOfImages = [imagesArray count]; 101 for (idx = 0; idx < numberOfImages; idx++) { 102 103 NSDictionary *imageDict = [imagesArray objectAtIndex:idx]; 104 NSNumber *isWriteable = [imageDict objectForKey:@"writeable"]; 105 if (isWriteable && ![isWriteable boolValue]) { 106 107 NSArray *systemEntitiesArray = [imageDict objectForKey:@"system-entities"]; 108 if (systemEntitiesArray) { 109 int idx; 110 unsigned int numberOfSystemEntities = [systemEntitiesArray count]; 111 for (idx = 0; idx < numberOfSystemEntities; idx++) { 112 113 NSDictionary *entityDict = [systemEntitiesArray objectAtIndex:idx]; 114 NSString *mountPoint = [entityDict objectForKey:@"mount-point"]; 115 if ([mountPoint length] > 0) { 116 117 // found a read-only image mount point; add it to our list 118 // and move to the next image 119 [paths addObject:mountPoint]; 120 break; 121 } 122 } 123 } 124 } 125 } 126 } 127 } 128 return paths; 129} 130 131// checks if the current app is running from a disk image, 132// displays a dialog offering to copy to /Applications, and 133// does the copying 134 135+ (void)handleApplicationLaunchFromReadOnlyDiskImage { 136 137 NSString * const kLastLaunchedPathKey = @"LastLaunchedPath"; 138 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 139 140 NSString *lastLaunchedPath = [defaults objectForKey:kLastLaunchedPathKey]; 141 NSString *mainBundlePath = [[NSBundle mainBundle] bundlePath]; 142 143 BOOL isRunningFromDiskImage = NO; 144 145 if (lastLaunchedPath == nil 146 || ![lastLaunchedPath isEqualToString:mainBundlePath]) { 147 148 // we haven't tested this launch path; check it now 149 NSArray *imagePaths = [self readOnlyDiskImagePaths]; 150 151 int idx; 152 for (idx = 0; idx < [imagePaths count]; idx++) { 153 154 NSString *imagePath = [imagePaths objectAtIndex:idx]; 155 if (![imagePath hasSuffix:@"/"]) 156 imagePath = [NSString stringWithFormat:@"%@/", imagePath]; 157 if ([mainBundlePath hasPrefix:imagePath]) { 158 159 isRunningFromDiskImage = YES; 160 break; 161 162 } 163 } 164 165 // ? should we ask every time the user runs from a read-only disk image 166 if (!isRunningFromDiskImage) { 167 168 // we don't need to check this bundle path again 169 [defaults setObject:mainBundlePath forKey:kLastLaunchedPathKey]; 170 171 } else { 172 // we're running from a disk image 173 174 [NSApp activateIgnoringOtherApps:YES]; 175 176 NSString *displayName = [[NSFileManager defaultManager] displayNameAtPath:mainBundlePath]; 177 NSString *msg1template = NSLocalizedString(@"Would you like to copy %@ to your " 178 @"computer's Applications folder and " 179 @"run it from there?", 180 @"Copy app from disk image: title"); 181 NSString *msg1 = [NSString stringWithFormat:msg1template, displayName]; 182 NSString *msg2 = NSLocalizedString(@"%@ is currently running from the Disk Image, " 183 @"and must be copied for full functionality. " 184 @"Copying may replace an older version in the " 185 @"Applications directory.", 186 @"Copy app from disk image: message"); 187 NSString *btnOK = NSLocalizedString(@"Copy", 188 @"Copy app from disk image: ok button"); 189 NSString *btnCancel = NSLocalizedString(@"Don't Copy", 190 @"Copy app from disk image: cancel button"); 191 192 int result = NSRunAlertPanel(msg1, msg2, btnOK, btnCancel, NULL, displayName); 193 if (result == NSAlertDefaultReturn) { 194 // copy to /Applications and launch from there 195 196 NSArray *appsPaths = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, 197 NSLocalDomainMask, 198 YES); 199 if ([appsPaths count] > 0) { 200 201 [self copyAndLaunchAppAtPath:mainBundlePath 202 toDirectoryPath:[appsPaths objectAtIndex:0]]; 203 // calls exit(0) on successful copy/launch 204 } else { 205 NSLog(@"DiskImageUtilities: Cannot make applications folder path"); 206 } 207 } 208 } 209 } 210} 211 212// copies an application from the given path to a new directory, if necessary 213// authenticating as admin or killing a running process at that location 214+ (void)copyAndLaunchAppAtPath:(NSString *)oldPath 215 toDirectoryPath:(NSString *)newDirectory { 216 217 NSFileManager *fileManager = [NSFileManager defaultManager]; 218 NSString *pathInApps = [newDirectory stringByAppendingPathComponent:[oldPath lastPathComponent]]; 219 BOOL isDir; 220 BOOL dirPathExists = [fileManager fileExistsAtPath:pathInApps isDirectory:&isDir] && isDir; 221 222 // We must authenticate as admin if we don't have write permission 223 // in the /Apps directory, or if there's already an app there 224 // with the same name and we don't have permission to write to it 225 BOOL mustAuth = (![self canWriteToPath:newDirectory] 226 || (dirPathExists && ![self canWriteToPath:pathInApps])); 227 228 [self killAppAtPath:pathInApps]; 229 230 BOOL didCopy; 231 if (mustAuth) { 232 didCopy = [self doAuthorizedCopyFromPath:oldPath toPath:pathInApps]; 233 } else { 234 didCopy = [fileManager copyPath:oldPath toPath:pathInApps handler:nil]; 235 } 236 237 if (didCopy) { 238 // launch the new copy and bail 239 LSLaunchURLSpec spec; 240 spec.appURL = (CFURLRef) [NSURL fileURLWithPath:pathInApps]; 241 spec.launchFlags = kLSLaunchNewInstance; 242 spec.itemURLs = NULL; 243 spec.passThruParams = NULL; 244 spec.asyncRefCon = NULL; 245 246 OSStatus err = LSOpenFromURLSpec(&spec, NULL); // NULL -> don't care about the launched URL 247 if (err == noErr) { 248 exit(0); 249 } else { 250 NSLog(@"DiskImageUtilities: Error %d launching \"%@\"", err, pathInApps); 251 } 252 } else { 253 // copying to /Applications failed 254 NSLog(@"DiskImageUtilities: Error copying to \"%@\"", pathInApps); 255 } 256} 257 258// looks for an app running from the specified path, and calls KillProcess on it 259+ (void)killAppAtPath:(NSString *)appPath { 260 261 // get the FSRef for bundle of the the target app to kill 262 FSRef targetFSRef; 263 OSStatus err = FSPathMakeRef((const UInt8 *)[appPath fileSystemRepresentation], 264 &targetFSRef, nil); 265 if (err == noErr) { 266 267 // search for a PSN of a process with the bundle at that location, if any 268 ProcessSerialNumber psn = { 0, kNoProcess }; 269 while (GetNextProcess(&psn) == noErr) { 270 271 FSRef compareFSRef; 272 if (GetProcessBundleLocation(&psn, &compareFSRef) == noErr 273 && FSCompareFSRefs(&compareFSRef, &targetFSRef) == noErr) { 274 275 // we found an app running from that path; kill it 276 err = KillProcess(&psn); 277 if (err != noErr) { 278 NSLog(@"DiskImageUtilities: Could not kill process at %@, error %d", 279 appPath, err); 280 } 281 } 282 } 283 } 284} 285 286// canWriteToPath checks for permissions to write into the directory |path| 287+ (BOOL)canWriteToPath:(NSString *)path { 288 int stat = access([path fileSystemRepresentation], (W_OK | R_OK)); 289 return (stat == 0); 290} 291 292// doAuthorizedCopyFromPath does an authorized copy, getting admin rights 293// 294// NOTE: when running the task with admin privileges, this waits on any child 295// process, since AEWP doesn't tell us the child's pid. This could be fooled 296// by any other child process that quits in the window between launch and 297// completion of our actual tool. 298+ (BOOL)doAuthorizedCopyFromPath:(NSString *)src toPath:(NSString *)dest { 299 // authorize 300 AuthorizationFlags authFlags = kAuthorizationFlagPreAuthorize 301 | kAuthorizationFlagExtendRights 302 | kAuthorizationFlagInteractionAllowed; 303 AuthorizationItem authItem = {kAuthorizationRightExecute, 0, nil, 0}; 304 AuthorizationRights authRights = {1, &authItem}; 305 SFAuthorization *authorization = [SFAuthorization authorizationWithFlags:authFlags 306 rights:&authRights 307 environment:kAuthorizationEmptyEnvironment]; 308 309 // execute the copy 310 const char taskPath[] = "/usr/bin/ditto"; 311 const char* arguments[] = { 312 "-rsrcFork", // 0: copy resource forks; --rsrc requires 10.3 313 NULL, // 1: src path 314 NULL, // 2: dest path 315 NULL 316 }; 317 arguments[1] = [src fileSystemRepresentation]; 318 arguments[2] = [dest fileSystemRepresentation]; 319 320 FILE **kNoPipe = nil; 321 OSStatus status = AuthorizationExecuteWithPrivileges([authorization authorizationRef], 322 taskPath, 323 kAuthorizationFlagDefaults, 324 (char *const *)arguments, 325 kNoPipe); 326 if (status == errAuthorizationSuccess) { 327 int wait_status; 328 int pid = wait(&wait_status); 329 if (pid == -1 || !WIFEXITED(wait_status)) { 330 status = -1; 331 } else { 332 status = WEXITSTATUS(wait_status); 333 } 334 } 335 336 // deauthorize 337 [authorization invalidateCredentials]; 338 339 return (status == 0); 340} 341@end