| 1 | // |
|---|
| 2 | // SUPlainInstallerInternals.m |
|---|
| 3 | // Sparkle |
|---|
| 4 | // |
|---|
| 5 | // Created by Andy Matuschak on 3/9/06. |
|---|
| 6 | // Copyright 2006 Andy Matuschak. All rights reserved. |
|---|
| 7 | // |
|---|
| 8 | |
|---|
| 9 | #import "Sparkle.h" |
|---|
| 10 | #import "SUPlainInstallerInternals.h" |
|---|
| 11 | |
|---|
| 12 | #import <Security/Security.h> |
|---|
| 13 | #import <sys/stat.h> |
|---|
| 14 | #import <sys/wait.h> |
|---|
| 15 | #import <dirent.h> |
|---|
| 16 | #import <unistd.h> |
|---|
| 17 | |
|---|
| 18 | @interface SUPlainInstaller (MMExtendedAttributes) |
|---|
| 19 | // Removes the directory tree rooted at |root| from the file quarantine. |
|---|
| 20 | // The quarantine was introduced on Mac OS X 10.5 and is described at: |
|---|
| 21 | // |
|---|
| 22 | // http://developer.apple.com/releasenotes/Carbon/RN-LaunchServices/index.html |
|---|
| 23 | //#apple_ref/doc/uid/TP40001369-DontLinkElementID_2 |
|---|
| 24 | // |
|---|
| 25 | // If |root| is not a directory, then it alone is removed from the quarantine. |
|---|
| 26 | // Symbolic links, including |root| if it is a symbolic link, will not be |
|---|
| 27 | // traversed. |
|---|
| 28 | // |
|---|
| 29 | // Ordinarily, the quarantine is managed by calling LSSetItemAttribute |
|---|
| 30 | // to set the kLSItemQuarantineProperties attribute to a dictionary specifying |
|---|
| 31 | // the quarantine properties to be applied. However, it does not appear to be |
|---|
| 32 | // possible to remove an item from the quarantine directly through any public |
|---|
| 33 | // Launch Services calls. Instead, this method takes advantage of the fact |
|---|
| 34 | // that the quarantine is implemented in part by setting an extended attribute, |
|---|
| 35 | // "com.apple.quarantine", on affected files. Removing this attribute is |
|---|
| 36 | // sufficient to remove files from the quarantine. |
|---|
| 37 | + (void)releaseFromQuarantine:(NSString*)root; |
|---|
| 38 | @end |
|---|
| 39 | |
|---|
| 40 | // Authorization code based on generous contribution from Allan Odgaard. Thanks, Allan! |
|---|
| 41 | |
|---|
| 42 | static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char* executablePath, AuthorizationFlags options, const char* const* arguments) |
|---|
| 43 | { |
|---|
| 44 | sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); |
|---|
| 45 | BOOL returnValue = YES; |
|---|
| 46 | |
|---|
| 47 | if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, (char* const*)arguments, NULL) == errAuthorizationSuccess) |
|---|
| 48 | { |
|---|
| 49 | int status; |
|---|
| 50 | pid_t pid = wait(&status); |
|---|
| 51 | if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) |
|---|
| 52 | returnValue = NO; |
|---|
| 53 | } |
|---|
| 54 | else |
|---|
| 55 | returnValue = NO; |
|---|
| 56 | |
|---|
| 57 | signal(SIGCHLD, oldSigChildHandler); |
|---|
| 58 | return returnValue; |
|---|
| 59 | } |
|---|
| 60 | |
|---|
| 61 | @implementation SUPlainInstaller (Internals) |
|---|
| 62 | |
|---|
| 63 | + (BOOL)currentUserOwnsPath:(NSString *)oPath |
|---|
| 64 | { |
|---|
| 65 | const char *path = [oPath fileSystemRepresentation]; |
|---|
| 66 | uid_t uid = getuid(); |
|---|
| 67 | bool res = false; |
|---|
| 68 | struct stat sb; |
|---|
| 69 | if(stat(path, &sb) == 0) |
|---|
| 70 | { |
|---|
| 71 | if(sb.st_uid == uid) |
|---|
| 72 | { |
|---|
| 73 | res = true; |
|---|
| 74 | if(sb.st_mode & S_IFDIR) |
|---|
| 75 | { |
|---|
| 76 | DIR* dir = opendir(path); |
|---|
| 77 | struct dirent* entry = NULL; |
|---|
| 78 | while(res && (entry = readdir(dir))) |
|---|
| 79 | { |
|---|
| 80 | if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) |
|---|
| 81 | continue; |
|---|
| 82 | |
|---|
| 83 | size_t len = strlen(path) + 1 + entry->d_namlen + 1; |
|---|
| 84 | char descend[len]; |
|---|
| 85 | strlcpy(descend, path, len); |
|---|
| 86 | strlcat(descend, "/", len); |
|---|
| 87 | strlcat(descend, entry->d_name, len); |
|---|
| 88 | NSString* newPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:descend length:strlen(descend)]; |
|---|
| 89 | res = [self currentUserOwnsPath:newPath]; |
|---|
| 90 | } |
|---|
| 91 | closedir(dir); |
|---|
| 92 | } |
|---|
| 93 | } |
|---|
| 94 | } |
|---|
| 95 | return res; |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | + (NSString *)_temporaryCopyNameForPath:(NSString *)path |
|---|
| 99 | { |
|---|
| 100 | // Let's try to read the version number so the filename will be more meaningful. |
|---|
| 101 | NSString *postFix; |
|---|
| 102 | NSBundle *bundle; |
|---|
| 103 | if ((bundle = [NSBundle bundleWithPath:path])) |
|---|
| 104 | { |
|---|
| 105 | // We'll clean it up a little for safety. |
|---|
| 106 | // The cast is necessary because of a bug in the headers in pre-10.5 SDKs |
|---|
| 107 | NSMutableCharacterSet *validCharacters = (id)[NSMutableCharacterSet alphanumericCharacterSet]; |
|---|
| 108 | [validCharacters formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@".-()"]]; |
|---|
| 109 | postFix = [[bundle objectForInfoDictionaryKey:@"CFBundleVersion"] stringByTrimmingCharactersInSet:[validCharacters invertedSet]]; |
|---|
| 110 | } |
|---|
| 111 | else |
|---|
| 112 | postFix = @"old"; |
|---|
| 113 | NSString *prefix = [[path stringByDeletingPathExtension] stringByAppendingFormat:@" (%@)", postFix]; |
|---|
| 114 | NSString *tempDir = [prefix stringByAppendingPathExtension:[path pathExtension]]; |
|---|
| 115 | // Now let's make sure we get a unique path. |
|---|
| 116 | int cnt=2; |
|---|
| 117 | while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999) |
|---|
| 118 | tempDir = [NSString stringWithFormat:@"%@ %d.%@", prefix, cnt++, [path pathExtension]]; |
|---|
| 119 | return tempDir; |
|---|
| 120 | } |
|---|
| 121 | |
|---|
| 122 | + (BOOL)_copyPathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst error:(NSError **)error |
|---|
| 123 | { |
|---|
| 124 | NSString *tmp = [self _temporaryCopyNameForPath:dst]; |
|---|
| 125 | const char* srcPath = [src fileSystemRepresentation]; |
|---|
| 126 | const char* tmpPath = [tmp fileSystemRepresentation]; |
|---|
| 127 | const char* dstPath = [dst fileSystemRepresentation]; |
|---|
| 128 | |
|---|
| 129 | struct stat dstSB; |
|---|
| 130 | stat(dstPath, &dstSB); |
|---|
| 131 | |
|---|
| 132 | AuthorizationRef auth = NULL; |
|---|
| 133 | OSStatus authStat = errAuthorizationDenied; |
|---|
| 134 | while (authStat == errAuthorizationDenied) { |
|---|
| 135 | authStat = AuthorizationCreate(NULL, |
|---|
| 136 | kAuthorizationEmptyEnvironment, |
|---|
| 137 | kAuthorizationFlagDefaults, |
|---|
| 138 | &auth); |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | BOOL res = NO; |
|---|
| 142 | if (authStat == errAuthorizationSuccess) { |
|---|
| 143 | res = YES; |
|---|
| 144 | |
|---|
| 145 | char uidgid[42]; |
|---|
| 146 | snprintf(uidgid, sizeof(uidgid), "%d:%d", |
|---|
| 147 | dstSB.st_uid, dstSB.st_gid); |
|---|
| 148 | |
|---|
| 149 | const char* executables[] = { |
|---|
| 150 | "/bin/rm", |
|---|
| 151 | "/bin/mv", |
|---|
| 152 | "/bin/mv", |
|---|
| 153 | "/bin/rm", |
|---|
| 154 | NULL, // pause here and do some housekeeping before |
|---|
| 155 | // continuing |
|---|
| 156 | "/usr/sbin/chown", |
|---|
| 157 | NULL // stop here for real |
|---|
| 158 | }; |
|---|
| 159 | |
|---|
| 160 | // 4 is the maximum number of arguments to any command, |
|---|
| 161 | // including the NULL that signals the end of an argument |
|---|
| 162 | // list. |
|---|
| 163 | const char* const argumentLists[][4] = { |
|---|
| 164 | { "-rf", tmpPath, NULL }, // make room for the temporary file... this is kinda unsafe; should probably do something better. |
|---|
| 165 | { "-f", dstPath, tmpPath, NULL }, // mv |
|---|
| 166 | { "-f", srcPath, dstPath, NULL }, // mv |
|---|
| 167 | { "-rf", tmpPath, NULL }, // rm |
|---|
| 168 | { NULL }, // pause |
|---|
| 169 | { "-R", uidgid, dstPath, NULL }, // chown |
|---|
| 170 | { NULL } // stop |
|---|
| 171 | }; |
|---|
| 172 | |
|---|
| 173 | // Process the commands up until the first NULL |
|---|
| 174 | int commandIndex = 0; |
|---|
| 175 | for (; executables[commandIndex] != NULL; ++commandIndex) { |
|---|
| 176 | if (res) |
|---|
| 177 | res = AuthorizationExecuteWithPrivilegesAndWait(auth, executables[commandIndex], kAuthorizationFlagDefaults, argumentLists[commandIndex]); |
|---|
| 178 | } |
|---|
| 179 | |
|---|
| 180 | // If the currently-running application is trusted, the new |
|---|
| 181 | // version should be trusted as well. Remove it from the |
|---|
| 182 | // quarantine to avoid a delay at launch, and to avoid |
|---|
| 183 | // presenting the user with a confusing trust dialog. |
|---|
| 184 | // |
|---|
| 185 | // This needs to be done after the application is moved to its |
|---|
| 186 | // new home with "mv" in case it's moved across filesystems: if |
|---|
| 187 | // that happens, "mv" actually performs a copy and may result |
|---|
| 188 | // in the application being quarantined. It also needs to be |
|---|
| 189 | // done before "chown" changes ownership, because the ownership |
|---|
| 190 | // change will almost certainly make it impossible to change |
|---|
| 191 | // attributes to release the files from the quarantine. |
|---|
| 192 | if (res) { |
|---|
| 193 | [self releaseFromQuarantine:dst]; |
|---|
| 194 | } |
|---|
| 195 | |
|---|
| 196 | // Now move past the NULL we found and continue executing |
|---|
| 197 | // commands from the list. |
|---|
| 198 | ++commandIndex; |
|---|
| 199 | |
|---|
| 200 | for (; executables[commandIndex] != NULL; ++commandIndex) { |
|---|
| 201 | if (res) |
|---|
| 202 | res = AuthorizationExecuteWithPrivilegesAndWait(auth, executables[commandIndex], kAuthorizationFlagDefaults, argumentLists[commandIndex]); |
|---|
| 203 | } |
|---|
| 204 | |
|---|
| 205 | AuthorizationFree(auth, 0); |
|---|
| 206 | |
|---|
| 207 | if (!res) |
|---|
| 208 | { |
|---|
| 209 | // Something went wrong somewhere along the way, but we're not sure exactly where. |
|---|
| 210 | NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file copy from %@ to %@ failed.", src, dst]; |
|---|
| 211 | if (error != NULL) |
|---|
| 212 | *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:[NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]]; |
|---|
| 213 | } |
|---|
| 214 | } |
|---|
| 215 | else |
|---|
| 216 | { |
|---|
| 217 | if (error != NULL) |
|---|
| 218 | *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:[NSDictionary dictionaryWithObject:@"Couldn't get permission to authenticate." forKey:NSLocalizedDescriptionKey]]; |
|---|
| 219 | } |
|---|
| 220 | return res; |
|---|
| 221 | } |
|---|
| 222 | |
|---|
| 223 | + (BOOL)copyPathWithAuthentication:(NSString *)src overPath:(NSString *)dst error:(NSError **)error |
|---|
| 224 | { |
|---|
| 225 | if (![[NSFileManager defaultManager] fileExistsAtPath:dst]) |
|---|
| 226 | { |
|---|
| 227 | NSString *errorMessage = [NSString stringWithFormat:@"Couldn't copy %@ over %@ because there is no file at %@.", src, dst, dst]; |
|---|
| 228 | if (error != NULL) |
|---|
| 229 | *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]]; |
|---|
| 230 | return NO; |
|---|
| 231 | } |
|---|
| 232 | |
|---|
| 233 | if (![[NSFileManager defaultManager] isWritableFileAtPath:dst] || ![[NSFileManager defaultManager] isWritableFileAtPath:[dst stringByDeletingLastPathComponent]]) |
|---|
| 234 | return [self _copyPathWithForcedAuthentication:src toPath:dst error:error]; |
|---|
| 235 | |
|---|
| 236 | NSString *tmpPath = [self _temporaryCopyNameForPath:dst]; |
|---|
| 237 | |
|---|
| 238 | if (![[NSFileManager defaultManager] movePath:dst toPath:tmpPath handler:self]) |
|---|
| 239 | { |
|---|
| 240 | if (error != NULL) |
|---|
| 241 | *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] forKey:NSLocalizedDescriptionKey]]; |
|---|
| 242 | return NO; |
|---|
| 243 | } |
|---|
| 244 | if (![[NSFileManager defaultManager] copyPath:src toPath:dst handler:self]) |
|---|
| 245 | { |
|---|
| 246 | if (error != NULL) |
|---|
| 247 | *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't copy %@ to %@.", src, dst] forKey:NSLocalizedDescriptionKey]]; |
|---|
| 248 | return NO; |
|---|
| 249 | } |
|---|
| 250 | |
|---|
| 251 | // Trash the old copy of the app. |
|---|
| 252 | NSInteger tag = 0; |
|---|
| 253 | if (![[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[tmpPath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[tmpPath lastPathComponent]] tag:&tag]) |
|---|
| 254 | NSLog(@"Sparkle error: couldn't move %@ to the trash. This is often a sign of a permissions error.", tmpPath); |
|---|
| 255 | |
|---|
| 256 | // If the currently-running application is trusted, the new |
|---|
| 257 | // version should be trusted as well. Remove it from the |
|---|
| 258 | // quarantine to avoid a delay at launch, and to avoid |
|---|
| 259 | // presenting the user with a confusing trust dialog. |
|---|
| 260 | // |
|---|
| 261 | // This needs to be done after the application is moved to its |
|---|
| 262 | // new home in case it's moved across filesystems: if that |
|---|
| 263 | // happens, the move is actually a copy, and it may result |
|---|
| 264 | // in the application being quarantined. |
|---|
| 265 | [self releaseFromQuarantine:dst]; |
|---|
| 266 | |
|---|
| 267 | return YES; |
|---|
| 268 | } |
|---|
| 269 | |
|---|
| 270 | @end |
|---|
| 271 | |
|---|
| 272 | #include <dlfcn.h> |
|---|
| 273 | #include <errno.h> |
|---|
| 274 | #include <sys/xattr.h> |
|---|
| 275 | |
|---|
| 276 | @implementation SUPlainInstaller (MMExtendedAttributes) |
|---|
| 277 | |
|---|
| 278 | + (int)removeXAttr:(const char*)name |
|---|
| 279 | fromFile:(NSString*)file |
|---|
| 280 | options:(int)options |
|---|
| 281 | { |
|---|
| 282 | typedef int (*removexattr_type)(const char*, const char*, int); |
|---|
| 283 | // Reference removexattr directly, it's in the SDK. |
|---|
| 284 | static removexattr_type removexattr_func = removexattr; |
|---|
| 285 | |
|---|
| 286 | // Make sure that the symbol is present. This checks the deployment |
|---|
| 287 | // target instead of the SDK so that it's able to catch dlsym failures |
|---|
| 288 | // as well as the null symbol that would result from building with the |
|---|
| 289 | // 10.4 SDK and a lower deployment target, and running on 10.3. |
|---|
| 290 | if (!removexattr_func) { |
|---|
| 291 | errno = ENOSYS; |
|---|
| 292 | return -1; |
|---|
| 293 | } |
|---|
| 294 | |
|---|
| 295 | const char* path = NULL; |
|---|
| 296 | @try { |
|---|
| 297 | path = [file fileSystemRepresentation]; |
|---|
| 298 | } |
|---|
| 299 | @catch (id exception) { |
|---|
| 300 | // -[NSString fileSystemRepresentation] throws an exception if it's |
|---|
| 301 | // unable to convert the string to something suitable. Map that to |
|---|
| 302 | // EDOM, "argument out of domain", which sort of conveys that there |
|---|
| 303 | // was a conversion failure. |
|---|
| 304 | errno = EDOM; |
|---|
| 305 | return -1; |
|---|
| 306 | } |
|---|
| 307 | |
|---|
| 308 | return removexattr_func(path, name, options); |
|---|
| 309 | } |
|---|
| 310 | |
|---|
| 311 | + (void)releaseFromQuarantine:(NSString*)root |
|---|
| 312 | { |
|---|
| 313 | const char* quarantineAttribute = "com.apple.quarantine"; |
|---|
| 314 | const int removeXAttrOptions = XATTR_NOFOLLOW; |
|---|
| 315 | |
|---|
| 316 | [self removeXAttr:quarantineAttribute |
|---|
| 317 | fromFile:root |
|---|
| 318 | options:removeXAttrOptions]; |
|---|
| 319 | |
|---|
| 320 | // Only recurse if it's actually a directory. Don't recurse into a |
|---|
| 321 | // root-level symbolic link. |
|---|
| 322 | NSDictionary* rootAttributes = |
|---|
| 323 | [[NSFileManager defaultManager] fileAttributesAtPath:root traverseLink:NO]; |
|---|
| 324 | NSString* rootType = [rootAttributes objectForKey:NSFileType]; |
|---|
| 325 | |
|---|
| 326 | if (rootType == NSFileTypeDirectory) { |
|---|
| 327 | // The NSDirectoryEnumerator will avoid recursing into any contained |
|---|
| 328 | // symbolic links, so no further type checks are needed. |
|---|
| 329 | NSDirectoryEnumerator* directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:root]; |
|---|
| 330 | NSString* file = nil; |
|---|
| 331 | while ((file = [directoryEnumerator nextObject])) { |
|---|
| 332 | [self removeXAttr:quarantineAttribute |
|---|
| 333 | fromFile:[root stringByAppendingPathComponent:file] |
|---|
| 334 | options:removeXAttrOptions]; |
|---|
| 335 | } |
|---|
| 336 | } |
|---|
| 337 | } |
|---|
| 338 | |
|---|
| 339 | @end |
|---|