/core/externals/update-engine/Core/KSDownloadAction.m
http://macfuse.googlecode.com/ · Objective C · 549 lines · 366 code · 79 blank · 104 comment · 61 complexity · eb5ef7708142dc104d456bb50b5e4792 MD5 · raw file
- // Copyright 2008 Google Inc.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #import "KSDownloadAction.h"
- #import "KSActionProcessor.h"
- #import "KSActionPipe.h"
- #import "KSURLData.h"
- #import "KSURLNotification.h"
- #import "GTMLogger.h"
- #import "NSData+Hash.h"
- #import "GTMBase64.h"
- #import "KSUUID.h"
- #import "GTMPath.h"
- #import "GTMNSString+FindFolder.h"
- #import "KSFrameworkStats.h"
- #import <unistd.h>
- #import <sys/stat.h>
- // Overview of secure downloading
- // ------------------------------
- //
- // This action needs to download a URL, save the data to a file, and verify the
- // contents of the file by checking it's SHA-1 hash value. To protect ourselves
- // in the event that root is doing the download, we fork another process
- // ("ksurl") which does the actual network transactions for the download, then
- // when it's done we proceed to verify the download. To ensure we always use the
- // "same" code path, we use the separate ksurl process even if we're not running
- // as root.
- //
- // These security requirements have led us to the following design of this code:
- //
- // 1. Before forking the child process we mark all of our file descriptors as
- // close-on-exec to make sure we don't leak FDs to an unprivileged process
- // 2. We erase the child's environment when NSTask'ing
- // 3. The child (ksurl) itself will change UID to non-root if being run as root.
- // If ksurl was not run as root, then it continues to run as the non-root
- // user who invoked it.
- // 4. Our child ksurl downloads the file to a temporary path at an essentially
- // world-writable location (tempPath_).
- // 5. When the child finishes downloading, this process (possibly running as
- // root) will *copy* the downloaded file from the world-writable location to
- // a secure location that's only writable by this user (perhaps root) to
- // prevent tampering with the file (path_).
- // 6. Once the file is in a secure location, this process (again, possibly root)
- // will verify that file's SHA-1 hash value.
- //
- // Assuming the hash value is OK, then we know we have a valid file and it's
- // stored in a safe location. At this point, it should be OK to report that the
- // download was a success.
- @interface KSDownloadAction (PrivateMethods)
- // Returns YES if the file at |path| has a size of |size_| and a hash value of
- // |hash_|. NO otherwise.
- - (BOOL)isFileAtPathValid:(NSString *)path;
- // Returns a base64 encoded SHA-1 has of the contents of the file at |path|.
- - (NSString *)hashOfFileAtPath:(NSString *)path;
- // Returns the size of the file in bytes (as obtained from NSFileManager)
- - (unsigned long long)sizeOfFileAtPath:(NSString *)path;
- // Returns the full path to the "ksurl" command.
- - (NSString *)ksurlPath;
- // If it cares, tell our delegate about our download progress.
- - (void)markProgress:(float)progress;
- // Called by an NSDistributedNotificationCenter
- - (void)progressNotification:(NSNotification *)notification;
- // Get the permissions for a file at |path|.
- + (mode_t)filePosixPermissionsForPath:(GTMPath *)path;
- // The subfolder in [~]/Library/Caches to use for +writableTempNameForPath:
- + (NSString *)cacheSubfolderName;
- // Returns a path to a file in a "writable" location in the given |domain|.
- // The returned path name is generated by the last path component of
- // |path| (i.e., path's filename) and a UUID to make the returned name
- // unique. This is the path where the 'ksurl' process will download
- // to. The directory in [~]/Library/Caches + +cacheSubfolderName will be
- // created if necessary, and its permissions checked and potentially repaired.
- + (NSString *)writableTempNameForPath:(NSString *)path
- inDomain:(int)domain;
- @end
- // Marks all the file descriptors in this process as "close-on-exec". This
- // ensures that we don't leak FDs to untrusted child processes.
- static void MarkFileDescriptorsCloseOnExec(void) {
- long maxfd = sysconf(_SC_OPEN_MAX);
- if (maxfd < 0) {
- // COV_NF_START
- GTMLoggerError(@"sysconf(_SC_OPEN_MAX) failed: %s", strerror(errno));
- maxfd = 255; // Use a reasonable default
- // COV_NF_END
- }
- for (int i = 0; i < maxfd; i++)
- fcntl(i, FD_CLOEXEC);
- }
- @implementation KSDownloadAction
- // Articulation point where tests can provide their own identifier (used
- // to construct the default download directory path) so that the user's
- // existing default download directory doesn't get stomped.
- + (NSString *)downloadDirectoryIdentifier {
- NSBundle *bundle = [NSBundle bundleForClass:[KSDownloadAction class]];
- NSString *identifier = [bundle bundleIdentifier];
- return identifier;
- }
- + (void)setDirectoryPermissionsForPath:(GTMPath *)path {
- mode_t filePerms = [self filePosixPermissionsForPath:path];
- filePerms &= ~(S_IWGRP | S_IWOTH); // Strip group / other writability.
- filePerms |= S_IRWXU; // Make sure user can rwx
- int result = chmod([[path fullPath] fileSystemRepresentation], filePerms);
- if (result == -1) {
- GTMLoggerError(@"chmod(%@) failed: %s", [path fullPath], strerror(errno));
- }
- }
- + (NSString *)defaultDownloadDirectory {
- short domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
- // nil |identifier| means we're not living in a bundle.
- NSString *identifier = [self downloadDirectoryIdentifier];
- if (identifier == nil) identifier = @"UpdateEngine";
- NSString *name = [NSString stringWithFormat:@"%@.%d", identifier, geteuid()];
- NSString *caches = [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
- inDomain:domain
- doCreate:YES];
- GTMPath *cacheDirectory = [[GTMPath pathWithFullPath:caches]
- createDirectoryName:name mode:0700];
- [self setDirectoryPermissionsForPath:cacheDirectory];
- GTMPath *downloads =
- [cacheDirectory createDirectoryName:@"Downloads" mode:0700];
- [self setDirectoryPermissionsForPath:downloads];
- return [downloads fullPath];
- }
- + (id)actionWithURL:(NSURL *)url
- size:(unsigned long long)size
- hash:(NSString *)hash
- name:(NSString *)name {
- NSString *dir = [self defaultDownloadDirectory];
- NSString *path = [dir stringByAppendingPathComponent:name];
- return [self actionWithURL:url size:size hash:hash path:path];
- }
- + (id)actionWithURL:(NSURL *)url
- size:(unsigned long long)size
- hash:(NSString *)hash
- path:(NSString *)path {
- return [[[self alloc] initWithURL:url
- size:size
- hash:hash
- path:path] autorelease];
- }
- - (id)init {
- return [self initWithURL:nil size:0 hash:nil path:nil];
- }
- - (id)initWithURL:(NSURL *)url
- size:(unsigned long long)size
- hash:(NSString *)hash
- path:(NSString *)path {
- if ((self = [super init])) {
- url_ = [url retain];
- size_ = size;
- hash_ = [hash retain];
- path_ = [path copy];
- int domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
- tempPath_ =
- [[KSDownloadAction writableTempNameForPath:path_ inDomain:domain] copy];
- if (url_ == nil || size_ == 0 || [hash_ length] == 0 ||
- [path_ length] == 0 || [tempPath_ length] == 0) {
- GTMLoggerDebug(@"created with illegal argument: "
- @"url=%@, size=%llu, hash=%@, destinationPath=%@",
- url_, size_, hash_, path_);
- [self release];
- return nil;
- }
- }
- return self;
- }
- - (void)dealloc {
- [url_ release];
- [hash_ release];
- [path_ release];
- [tempPath_ release];
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
- [downloadTask_ terminate];
- [downloadTask_ release];
- [ksurlPath_ release];
- [super dealloc];
- }
- - (NSURL *)url {
- return url_;
- }
- - (unsigned long long)size {
- return size_;
- }
- - (NSString *)hash {
- return hash_;
- }
- - (NSString *)path {
- return path_;
- }
- - (void)performAction {
- // Assert class invariants that we care about here
- _GTMDevAssert(url_ != nil, @"url_ must not be nil");
- _GTMDevAssert(hash_ != nil, @"hash_ must not be nil");
- _GTMDevAssert(size_ != 0, @"size_ must not be 0");
- _GTMDevAssert(path_ != nil, @"destination path must not be nil");
- _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
- _GTMDevAssert(downloadTask_ == nil, @"downloadTask_ must be nil");
- // Announce our progress is just beginning.
- [self markProgress:0.0];
- // If we've already downloaded the file, then we can short circuit the
- // download and just return the one that we already have.
- if ([self isFileAtPathValid:path_]) {
- GTMLoggerInfo(@"Short circuiting download of %@, path=%@, "
- @"size=%llu, hash=%@", url_, path_, size_, hash_);
- [[self outPipe] setContents:path_];
- [self markProgress:1.0];
- [[self processor] finishedProcessing:self successfully:YES];
- [[KSFrameworkStats sharedStats] incrementStat:kStatDownloadCacheHits];
- return; // Short circuit
- }
- NSString *ksurlPath = [self ksurlPath];
- NSArray *args = [NSArray arrayWithObjects:
- @"-url", [url_ description],
- @"-path", tempPath_,
- @"-size", [NSString stringWithFormat:@"%llu", size_],
- nil];
- downloadTask_ = [[NSTask alloc] init];
- [downloadTask_ setLaunchPath:ksurlPath];
- [downloadTask_ setEnvironment:[NSDictionary dictionary]];
- [downloadTask_ setCurrentDirectoryPath:@"/tmp/"];
- [downloadTask_ setArguments:args];
- GTMLoggerInfo(@"Running '%@ %@'", ksurlPath,
- [args componentsJoinedByString:@" "]);
- MarkFileDescriptorsCloseOnExec();
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSNotificationCenter defaultCenter]
- addObserver:self
- selector:@selector(taskExited:)
- name:NSTaskDidTerminateNotification
- object:downloadTask_];
- [[NSDistributedNotificationCenter defaultCenter]
- addObserver:self
- selector:@selector(progressNotification:)
- name:KSURLProgressNotification
- object:tempPath_];
- @try {
- // Is known to throw if launchPath is not executable
- [downloadTask_ launch];
- [[KSFrameworkStats sharedStats] incrementStat:kStatDownloads];
- // COV_NF_START
- }
- @catch (id ex) {
- // It's not really feasible to test the case where this throws because we'd
- // need to delete the installation of ksurl during the unit test, which
- // may break further tests that rely on it. Or we could move it and move
- // it back, but really, ugh.
- GTMLoggerError(@"Failed to launch %@ %@: %@", ksurlPath,
- [args componentsJoinedByString:@" "], ex);
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSDistributedNotificationCenter defaultCenter]
- removeObserver:self];
- [downloadTask_ release];
- downloadTask_ = nil;
- [self markProgress:1.0];
- [[self processor] finishedProcessing:self successfully:NO];
- }
- // COV_NF_END
- }
- - (void)terminateAction {
- if (![self isRunning])
- return;
- GTMLoggerInfo(@"Cancelling download task %@ (%@ %@) at the behest of %@",
- downloadTask_, [downloadTask_ launchPath],
- [[downloadTask_ arguments] componentsJoinedByString:@" "],
- [self processor]);
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
- [downloadTask_ terminate];
- [downloadTask_ waitUntilExit];
- [downloadTask_ release];
- downloadTask_ = nil;
- }
- - (void)taskExited:(NSNotification *)notification {
- _GTMDevAssert(path_ != nil, @"path_ must not be nil");
- _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
- BOOL verified = NO;
- int status = [downloadTask_ terminationStatus];
- if (status == 0) {
- // Move tempPath_ into our safe, non-public location pointed at by path_
- // Why do we use unlink(2) instead of NSFileManager? Because NSFM
- // will remove directories recursively, and if some crazy accident
- // happend where one of these paths pointed to a dir (say, "/"),
- // we'd rather it fail than recursively remove things.
- NSFileManager *fm = [NSFileManager defaultManager];
- unlink([path_ fileSystemRepresentation]); // Remove destination path
- if (![fm copyPath:tempPath_ toPath:path_ handler:nil]) {
- GTMLoggerError(@"Failed to rename %@ -> %@: errno=%d", // COV_NF_LINE
- tempPath_, path_, errno); // COV_NF_LINE
- }
- unlink([tempPath_ fileSystemRepresentation]); // Clean up source path
- verified = [self isFileAtPathValid:path_];
- }
- if (verified)
- [[self outPipe] setContents:path_];
- else
- [[KSFrameworkStats sharedStats] incrementStat:kStatFailedDownloads];
- GTMLoggerInfo(@"Task %d finished status=%d, verified=%d",
- [downloadTask_ processIdentifier], status, verified);
- [self markProgress:1.0];
- [[self processor] finishedProcessing:self successfully:verified];
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
- [downloadTask_ release];
- downloadTask_ = nil;
- }
- - (NSString *)description {
- return [NSString stringWithFormat:@"<%@:%p url=%@ size=%llu hash=%@ ...>",
- [self class], self, url_, size_, hash_];
- }
- @end // KSDownloadAction
- @implementation KSDownloadAction (PrivateMethods)
- // Articulation point for tests.
- + (NSString *)cacheSubfolderName {
- return @"UpdateEngine-Temp";
- }
- + (NSString *)writableTempNameForPath:(NSString *)path
- inDomain:(int)domain {
- if (path == nil) return nil;
- NSString *uniqueName = [NSString stringWithFormat:@"%@-%@",
- [path lastPathComponent], [KSUUID uuidString]];
- NSString *cacheDir =
- [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
- subfolderName:[self cacheSubfolderName]
- inDomain:domain
- doCreate:YES];
- mode_t cachePerms =
- [KSDownloadAction filePosixPermissionsForPath:
- [GTMPath pathWithFullPath:cacheDir]];
- if (domain == kLocalDomain) {
- // The cacheDir for the local domain (/Library/Caches) needs to be
- // world-writable, so that a ksurl running as "nobody" (on behalf of a
- // root-originated update) is able to create a download directory.
- // Make sure this is the case.
- if ((cachePerms & S_IWOTH) == 0) {
- // OR-in 007 permissions
- chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXO);
- }
- } else {
- // The cacheDir for the user domain (~/Library/Caches) need to be
- /// owner-writable for the same reasons as above.
- if ((cachePerms & S_IWUSR) == 0) {
- chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXU);
- }
- }
- return [cacheDir stringByAppendingPathComponent:uniqueName];
- }
- + (mode_t)filePosixPermissionsForPath:(GTMPath *)path {
- mode_t filePerms = [[path attributes] filePosixPermissions];
- return filePerms;
- }
- - (BOOL)isFileAtPathValid:(NSString *)path {
- if (path == nil) return NO;
- unsigned long long size = [self sizeOfFileAtPath:path];
- NSString *hash = [self hashOfFileAtPath:path];
- return (size_ == size) && ([hash_ isEqualToString:hash]);
- }
- // Reads in a whole file and returns the base64 encoded SHA-1 hash of the data.
- // If we start working with HUGE files, then we may need to change this method
- // to stream the data from the file and hash it w/o reading the whole thing in.
- // For now, this should work fine.
- - (NSString *)hashOfFileAtPath:(NSString *)path {
- if (path == nil) return nil;
- NSData *data = [NSData dataWithContentsOfFile:path];
- NSData *hash = [data SHA1Hash];
- return [GTMBase64 stringByEncodingData:hash];
- }
- - (unsigned long long)sizeOfFileAtPath:(NSString *)path {
- if (path == nil) return 0;
- return [[[NSFileManager defaultManager] fileAttributesAtPath:path
- traverseLink:NO] fileSize];
- }
- // Return a directory in which to put ksurl.
- // Performs no error checking or creation.
- - (NSString *)ksurlDirectoryName {
- NSString *directory = [NSString stringWithFormat:@"/tmp/.ksda.%X", geteuid()];
- return directory;
- }
- // Return YES if |directory| is valid for us to place ksurl into, else NO.
- - (BOOL)isValidAndSafeDirectory:(NSString *)directory {
- NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
- NSNumber *properOwner = [NSNumber numberWithUnsignedLong:geteuid()];
- NSDictionary *attributes = nil;
- BOOL isDir = NO;
- if ([[NSFileManager defaultManager]
- fileExistsAtPath:directory
- isDirectory:&isDir] &&
- isDir) {
- attributes = [[NSFileManager defaultManager]
- fileAttributesAtPath:directory
- traverseLink:NO];
- if ([[attributes objectForKey:NSFilePosixPermissions]
- isEqual:properPermission] &&
- [[attributes objectForKey:NSFileOwnerAccountID]
- isEqual:properOwner]) {
- return YES;
- }
- }
- return NO;
- }
- // Return a valid directory for ksurl creation.
- // Create if needed. Delete and recreate if it looks bad.
- - (NSString *)ksurlValidatedDirectory {
- NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
- NSString *directory = [self ksurlDirectoryName];
- if ([self isValidAndSafeDirectory:directory]) {
- return directory;
- }
- // Doesn't exist, or exists with wrong permission/owner. Delete and create.
- [[NSFileManager defaultManager] removeFileAtPath:directory
- handler:nil];
- NSDictionary *attr =
- [NSDictionary dictionaryWithObject:properPermission
- forKey:NSFilePosixPermissions];
- [[NSFileManager defaultManager] createDirectoryAtPath:directory
- attributes:attr];
- // reconfirm in case it failed
- if ([self isValidAndSafeDirectory:directory]) {
- return directory;
- } else {
- return nil;
- }
- }
- - (NSString *)ksurlPath {
- if (ksurlPath_ != nil)
- return ksurlPath_;
- // KSURLData and KSURLData_len come from KSURLData.m, a file
- // generated by xxd'ing the ksurl binary.
- NSData *ksdata = [NSData dataWithBytesNoCopy:KSURLData
- length:KSURLData_len
- freeWhenDone:NO];
- NSString *directory = [self ksurlValidatedDirectory];
- if (directory == nil) {
- GTMLoggerInfo(@"Can't safely create directory for ksurl.");
- return nil;
- }
- NSString *destination = [directory
- stringByAppendingPathComponent:@"ksurl"];
- NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
- NSDictionary *attr =
- [NSDictionary dictionaryWithObject:properPermission
- forKey:NSFilePosixPermissions];
- if ([[NSFileManager defaultManager] createFileAtPath:destination
- contents:ksdata
- attributes:attr] == NO) {
- GTMLoggerInfo(@"Can't create ksurl.");
- return nil;
- }
- ksurlPath_ = [destination retain];
- return ksurlPath_;
- }
- - (void)markProgress:(float)progress {
- [[self processor] runningAction:self progress:progress];
- }
- - (void)progressNotification:(NSNotification *)notification {
- NSNumber *progress = [[notification userInfo] objectForKey:KSURLProgressKey];
- if (progress)
- [self markProgress:[progress floatValue]];
- }
- @end // KSDownloadAction (PrivateMethods)