PageRenderTime 117ms CodeModel.GetById 20ms app.highlight 90ms RepoModel.GetById 2ms app.codeStats 0ms

/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
  1// Copyright 2008 Google Inc.
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     http://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#import "KSDownloadAction.h"
 16#import "KSActionProcessor.h"
 17#import "KSActionPipe.h"
 18#import "KSURLData.h"
 19#import "KSURLNotification.h"
 20#import "GTMLogger.h"
 21#import "NSData+Hash.h"
 22#import "GTMBase64.h"
 23#import "KSUUID.h"
 24#import "GTMPath.h"
 25#import "GTMNSString+FindFolder.h"
 26#import "KSFrameworkStats.h"
 27#import <unistd.h>
 28#import <sys/stat.h>
 29
 30// Overview of secure downloading
 31// ------------------------------
 32//
 33// This action needs to download a URL, save the data to a file, and verify the
 34// contents of the file by checking it's SHA-1 hash value. To protect ourselves
 35// in the event that root is doing the download, we fork another process
 36// ("ksurl") which does the actual network transactions for the download, then
 37// when it's done we proceed to verify the download. To ensure we always use the
 38// "same" code path, we use the separate ksurl process even if we're not running
 39// as root.
 40//
 41// These security requirements have led us to the following design of this code:
 42//
 43// 1. Before forking the child process we mark all of our file descriptors as
 44//    close-on-exec to make sure we don't leak FDs to an unprivileged process
 45// 2. We erase the child's environment when NSTask'ing
 46// 3. The child (ksurl) itself will change UID to non-root if being run as root.
 47//    If ksurl was not run as root, then it continues to run as the non-root
 48//    user who invoked it.
 49// 4. Our child ksurl downloads the file to a temporary path at an essentially
 50//    world-writable location (tempPath_).
 51// 5. When the child finishes downloading, this process (possibly running as
 52//    root) will *copy* the downloaded file from the world-writable location to
 53//    a secure location that's only writable by this user (perhaps root) to
 54//    prevent tampering with the file (path_).
 55// 6. Once the file is in a secure location, this process (again, possibly root)
 56//    will verify that file's SHA-1 hash value.
 57//
 58// Assuming the hash value is OK, then we know we have a valid file and it's
 59// stored in a safe location. At this point, it should be OK to report that the
 60// download was a success.
 61
 62
 63@interface KSDownloadAction (PrivateMethods)
 64
 65// Returns YES if the file at |path| has a size of |size_| and a hash value of
 66// |hash_|. NO otherwise.
 67- (BOOL)isFileAtPathValid:(NSString *)path;
 68
 69// Returns a base64 encoded SHA-1 has of the contents of the file at |path|.
 70- (NSString *)hashOfFileAtPath:(NSString *)path;
 71
 72// Returns the size of the file in bytes (as obtained from NSFileManager)
 73- (unsigned long long)sizeOfFileAtPath:(NSString *)path;
 74
 75// Returns the full path to the "ksurl" command.
 76- (NSString *)ksurlPath;
 77
 78// If it cares, tell our delegate about our download progress.
 79- (void)markProgress:(float)progress;
 80
 81// Called by an NSDistributedNotificationCenter
 82- (void)progressNotification:(NSNotification *)notification;
 83
 84// Get the permissions for a file at |path|.
 85+ (mode_t)filePosixPermissionsForPath:(GTMPath *)path;
 86
 87// The subfolder in [~]/Library/Caches to use for +writableTempNameForPath:
 88+ (NSString *)cacheSubfolderName;
 89
 90// Returns a path to a file in a "writable" location in the given |domain|.
 91// The returned path name is generated by the last path component of
 92// |path| (i.e., path's filename) and a UUID to make the returned name
 93// unique. This is the path where the 'ksurl' process will download
 94// to.  The directory in [~]/Library/Caches + +cacheSubfolderName will be
 95// created if necessary, and its permissions checked and potentially repaired.
 96+ (NSString *)writableTempNameForPath:(NSString *)path
 97                             inDomain:(int)domain;
 98@end
 99
100
101// Marks all the file descriptors in this process as "close-on-exec". This
102// ensures that we don't leak FDs to untrusted child processes.
103static void MarkFileDescriptorsCloseOnExec(void) {
104  long maxfd = sysconf(_SC_OPEN_MAX);
105  if (maxfd < 0) {
106    // COV_NF_START
107    GTMLoggerError(@"sysconf(_SC_OPEN_MAX) failed: %s", strerror(errno));
108    maxfd = 255;  // Use a reasonable default
109    // COV_NF_END
110  }
111  for (int i = 0; i < maxfd; i++)
112    fcntl(i, FD_CLOEXEC);
113}
114
115
116@implementation KSDownloadAction
117
118// Articulation point where tests can provide their own identifier (used
119// to construct the default download directory path) so that the user's
120// existing default download directory doesn't get stomped.
121+ (NSString *)downloadDirectoryIdentifier {
122  NSBundle *bundle = [NSBundle bundleForClass:[KSDownloadAction class]];
123  NSString *identifier = [bundle bundleIdentifier];
124  return identifier;
125}
126
127+ (void)setDirectoryPermissionsForPath:(GTMPath *)path {
128  mode_t filePerms = [self filePosixPermissionsForPath:path];
129  filePerms &= ~(S_IWGRP | S_IWOTH);  // Strip group / other writability.
130  filePerms |= S_IRWXU;  // Make sure user can rwx
131  int result = chmod([[path fullPath] fileSystemRepresentation], filePerms);
132
133  if (result == -1) {
134    GTMLoggerError(@"chmod(%@) failed: %s", [path fullPath], strerror(errno));
135  }
136}
137
138+ (NSString *)defaultDownloadDirectory {
139  short domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
140  // nil |identifier| means we're not living in a bundle.
141  NSString *identifier = [self downloadDirectoryIdentifier];
142  if (identifier == nil) identifier = @"UpdateEngine";
143  NSString *name = [NSString stringWithFormat:@"%@.%d", identifier, geteuid()];
144  NSString *caches = [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
145                                                  inDomain:domain
146                                                  doCreate:YES];
147
148  GTMPath *cacheDirectory = [[GTMPath pathWithFullPath:caches]
149                              createDirectoryName:name mode:0700];
150  [self setDirectoryPermissionsForPath:cacheDirectory];
151  GTMPath *downloads =
152    [cacheDirectory createDirectoryName:@"Downloads" mode:0700];
153  [self setDirectoryPermissionsForPath:downloads];
154
155  return [downloads fullPath];
156}
157
158+ (id)actionWithURL:(NSURL *)url
159               size:(unsigned long long)size
160               hash:(NSString *)hash
161               name:(NSString *)name {
162  NSString *dir = [self defaultDownloadDirectory];
163  NSString *path = [dir stringByAppendingPathComponent:name];
164  return [self actionWithURL:url size:size hash:hash path:path];
165}
166
167+ (id)actionWithURL:(NSURL *)url
168               size:(unsigned long long)size
169               hash:(NSString *)hash
170               path:(NSString *)path {
171  return [[[self alloc] initWithURL:url
172                               size:size
173                               hash:hash
174                               path:path] autorelease];
175}
176
177- (id)init {
178  return [self initWithURL:nil size:0 hash:nil path:nil];
179}
180
181- (id)initWithURL:(NSURL *)url
182             size:(unsigned long long)size
183             hash:(NSString *)hash
184             path:(NSString *)path {
185  if ((self = [super init])) {
186    url_ = [url retain];
187    size_ = size;
188    hash_ = [hash retain];
189    path_ = [path copy];
190    int domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
191    tempPath_ =
192      [[KSDownloadAction writableTempNameForPath:path_ inDomain:domain] copy];
193
194    if (url_ == nil || size_ == 0 || [hash_ length] == 0 ||
195        [path_ length] == 0 || [tempPath_ length] == 0) {
196      GTMLoggerDebug(@"created with illegal argument: "
197                     @"url=%@, size=%llu, hash=%@, destinationPath=%@",
198                     url_, size_, hash_, path_);
199      [self release];
200      return nil;
201    }
202  }
203  return self;
204}
205
206- (void)dealloc {
207  [url_ release];
208  [hash_ release];
209  [path_ release];
210  [tempPath_ release];
211  [[NSNotificationCenter defaultCenter] removeObserver:self];
212  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
213  [downloadTask_ terminate];
214  [downloadTask_ release];
215  [ksurlPath_ release];
216  [super dealloc];
217}
218
219- (NSURL *)url {
220  return url_;
221}
222
223- (unsigned long long)size {
224  return size_;
225}
226
227- (NSString *)hash {
228  return hash_;
229}
230
231- (NSString *)path {
232  return path_;
233}
234
235- (void)performAction {
236  // Assert class invariants that we care about here
237  _GTMDevAssert(url_ != nil, @"url_ must not be nil");
238  _GTMDevAssert(hash_ != nil, @"hash_ must not be nil");
239  _GTMDevAssert(size_ != 0, @"size_ must not be 0");
240  _GTMDevAssert(path_ != nil, @"destination path must not be nil");
241  _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
242  _GTMDevAssert(downloadTask_ == nil, @"downloadTask_ must be nil");
243
244  // Announce our progress is just beginning.
245  [self markProgress:0.0];
246
247  // If we've already downloaded the file, then we can short circuit the
248  // download and just return the one that we already have.
249  if ([self isFileAtPathValid:path_]) {
250    GTMLoggerInfo(@"Short circuiting download of %@, path=%@, "
251                  @"size=%llu, hash=%@", url_, path_, size_, hash_);
252    [[self outPipe] setContents:path_];
253    [self markProgress:1.0];
254    [[self processor] finishedProcessing:self successfully:YES];
255    [[KSFrameworkStats sharedStats] incrementStat:kStatDownloadCacheHits];
256    return;  // Short circuit
257  }
258
259  NSString *ksurlPath = [self ksurlPath];
260  NSArray *args = [NSArray arrayWithObjects:
261                           @"-url", [url_ description],
262                           @"-path", tempPath_,
263                           @"-size", [NSString stringWithFormat:@"%llu", size_],
264                           nil];
265
266  downloadTask_ = [[NSTask alloc] init];
267  [downloadTask_ setLaunchPath:ksurlPath];
268  [downloadTask_ setEnvironment:[NSDictionary dictionary]];
269  [downloadTask_ setCurrentDirectoryPath:@"/tmp/"];
270  [downloadTask_ setArguments:args];
271
272  GTMLoggerInfo(@"Running '%@ %@'", ksurlPath,
273                [args componentsJoinedByString:@" "]);
274
275  MarkFileDescriptorsCloseOnExec();
276
277  [[NSNotificationCenter defaultCenter] removeObserver:self];
278  [[NSNotificationCenter defaultCenter]
279    addObserver:self
280       selector:@selector(taskExited:)
281           name:NSTaskDidTerminateNotification
282         object:downloadTask_];
283  [[NSDistributedNotificationCenter defaultCenter]
284    addObserver:self
285       selector:@selector(progressNotification:)
286           name:KSURLProgressNotification
287         object:tempPath_];
288
289  @try {
290    // Is known to throw if launchPath is not executable
291    [downloadTask_ launch];
292    [[KSFrameworkStats sharedStats] incrementStat:kStatDownloads];
293  // COV_NF_START
294  }
295  @catch (id ex) {
296    // It's not really feasible to test the case where this throws because we'd
297    // need to delete the installation of ksurl during the unit test, which
298    // may break further tests that rely on it. Or we could move it and move
299    // it back, but really, ugh.
300    GTMLoggerError(@"Failed to launch %@ %@: %@", ksurlPath,
301                   [args componentsJoinedByString:@" "], ex);
302    [[NSNotificationCenter defaultCenter] removeObserver:self];
303    [[NSDistributedNotificationCenter defaultCenter]
304      removeObserver:self];
305    [downloadTask_ release];
306    downloadTask_ = nil;
307    [self markProgress:1.0];
308    [[self processor] finishedProcessing:self successfully:NO];
309  }
310  // COV_NF_END
311}
312
313- (void)terminateAction {
314  if (![self isRunning])
315    return;
316
317  GTMLoggerInfo(@"Cancelling download task %@ (%@ %@) at the behest of %@",
318                downloadTask_, [downloadTask_ launchPath],
319                [[downloadTask_ arguments] componentsJoinedByString:@" "],
320                [self processor]);
321
322  [[NSNotificationCenter defaultCenter] removeObserver:self];
323  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
324  [downloadTask_ terminate];
325  [downloadTask_ waitUntilExit];
326  [downloadTask_ release];
327  downloadTask_ = nil;
328}
329
330- (void)taskExited:(NSNotification *)notification {
331  _GTMDevAssert(path_ != nil, @"path_ must not be nil");
332  _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
333
334  BOOL verified = NO;
335  int status = [downloadTask_ terminationStatus];
336
337  if (status == 0) {
338    // Move tempPath_ into our safe, non-public location pointed at by path_
339    // Why do we use unlink(2) instead of NSFileManager? Because NSFM
340    // will remove directories recursively, and if some crazy accident
341    // happend where one of these paths pointed to a dir (say, "/"),
342    // we'd rather it fail than recursively remove things.
343    NSFileManager *fm = [NSFileManager defaultManager];
344    unlink([path_ fileSystemRepresentation]);  // Remove destination path
345    if (![fm copyPath:tempPath_ toPath:path_ handler:nil]) {
346      GTMLoggerError(@"Failed to rename %@ -> %@: errno=%d",  // COV_NF_LINE
347                     tempPath_, path_, errno);                // COV_NF_LINE
348    }
349    unlink([tempPath_ fileSystemRepresentation]);  // Clean up source path
350
351    verified = [self isFileAtPathValid:path_];
352  }
353
354  if (verified)
355    [[self outPipe] setContents:path_];
356  else
357    [[KSFrameworkStats sharedStats] incrementStat:kStatFailedDownloads];
358
359  GTMLoggerInfo(@"Task %d finished status=%d, verified=%d",
360                [downloadTask_ processIdentifier], status, verified);
361
362  [self markProgress:1.0];
363  [[self processor] finishedProcessing:self successfully:verified];
364
365  [[NSNotificationCenter defaultCenter] removeObserver:self];
366  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
367  [downloadTask_ release];
368  downloadTask_ = nil;
369}
370
371- (NSString *)description {
372  return [NSString stringWithFormat:@"<%@:%p url=%@ size=%llu hash=%@ ...>",
373                   [self class], self, url_, size_, hash_];
374}
375
376@end  // KSDownloadAction
377
378
379@implementation KSDownloadAction (PrivateMethods)
380
381// Articulation point for tests.
382+ (NSString *)cacheSubfolderName {
383  return @"UpdateEngine-Temp";
384}
385
386+ (NSString *)writableTempNameForPath:(NSString *)path
387                             inDomain:(int)domain {
388  if (path == nil) return nil;
389  NSString *uniqueName = [NSString stringWithFormat:@"%@-%@",
390                          [path lastPathComponent], [KSUUID uuidString]];
391  NSString *cacheDir =
392    [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
393                            subfolderName:[self cacheSubfolderName]
394                                 inDomain:domain
395                                 doCreate:YES];
396  mode_t cachePerms =
397    [KSDownloadAction filePosixPermissionsForPath:
398                        [GTMPath pathWithFullPath:cacheDir]];
399  if (domain == kLocalDomain) {
400    // The cacheDir for the local domain (/Library/Caches) needs to be
401    // world-writable, so that a ksurl running as "nobody" (on behalf of a
402    // root-originated update) is able to create a download directory.
403    // Make sure this is the case.
404    if ((cachePerms & S_IWOTH) == 0) {
405      // OR-in 007 permissions
406      chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXO);
407    }
408  } else {
409    // The cacheDir for the user domain (~/Library/Caches) need to be
410    /// owner-writable for the same reasons as above.
411    if ((cachePerms & S_IWUSR) == 0) {
412      chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXU);
413    }
414  }
415  return [cacheDir stringByAppendingPathComponent:uniqueName];
416}
417
418+ (mode_t)filePosixPermissionsForPath:(GTMPath *)path {
419  mode_t filePerms = [[path attributes] filePosixPermissions];
420  return filePerms;
421}
422
423- (BOOL)isFileAtPathValid:(NSString *)path {
424  if (path == nil) return NO;
425  unsigned long long size = [self sizeOfFileAtPath:path];
426  NSString *hash = [self hashOfFileAtPath:path];
427  return (size_ == size) && ([hash_ isEqualToString:hash]);
428}
429
430// Reads in a whole file and returns the base64 encoded SHA-1 hash of the data.
431// If we start working with HUGE files, then we may need to change this method
432// to stream the data from the file and hash it w/o reading the whole thing in.
433// For now, this should work fine.
434- (NSString *)hashOfFileAtPath:(NSString *)path {
435  if (path == nil) return nil;
436
437  NSData *data = [NSData dataWithContentsOfFile:path];
438  NSData *hash = [data SHA1Hash];
439
440  return [GTMBase64 stringByEncodingData:hash];
441}
442
443- (unsigned long long)sizeOfFileAtPath:(NSString *)path {
444  if (path == nil) return 0;
445  return [[[NSFileManager defaultManager] fileAttributesAtPath:path
446                                                  traverseLink:NO] fileSize];
447}
448
449// Return a directory in which to put ksurl.
450// Performs no error checking or creation.
451- (NSString *)ksurlDirectoryName {
452  NSString *directory = [NSString stringWithFormat:@"/tmp/.ksda.%X", geteuid()];
453  return directory;
454}
455
456// Return YES if |directory| is valid for us to place ksurl into, else NO.
457- (BOOL)isValidAndSafeDirectory:(NSString *)directory {
458  NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
459  NSNumber *properOwner = [NSNumber numberWithUnsignedLong:geteuid()];
460  NSDictionary *attributes = nil;
461
462  BOOL isDir = NO;
463  if ([[NSFileManager defaultManager]
464        fileExistsAtPath:directory
465             isDirectory:&isDir] &&
466      isDir) {
467    attributes = [[NSFileManager defaultManager]
468                                 fileAttributesAtPath:directory
469                                         traverseLink:NO];
470    if ([[attributes objectForKey:NSFilePosixPermissions]
471          isEqual:properPermission] &&
472        [[attributes objectForKey:NSFileOwnerAccountID]
473          isEqual:properOwner]) {
474      return YES;
475    }
476  }
477
478  return NO;
479}
480
481// Return a valid directory for ksurl creation.
482// Create if needed.  Delete and recreate if it looks bad.
483- (NSString *)ksurlValidatedDirectory {
484  NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
485  NSString *directory = [self ksurlDirectoryName];
486  if ([self isValidAndSafeDirectory:directory]) {
487    return directory;
488  }
489
490  // Doesn't exist, or exists with wrong permission/owner.  Delete and create.
491  [[NSFileManager defaultManager] removeFileAtPath:directory
492                                           handler:nil];
493  NSDictionary *attr =
494    [NSDictionary dictionaryWithObject:properPermission
495                                forKey:NSFilePosixPermissions];
496  [[NSFileManager defaultManager] createDirectoryAtPath:directory
497                                             attributes:attr];
498
499  // reconfirm in case it failed
500  if ([self isValidAndSafeDirectory:directory]) {
501    return directory;
502  } else {
503    return nil;
504  }
505}
506
507- (NSString *)ksurlPath {
508  if (ksurlPath_ != nil)
509    return ksurlPath_;
510
511  // KSURLData and KSURLData_len come from KSURLData.m, a file
512  // generated by xxd'ing the ksurl binary.
513  NSData *ksdata = [NSData dataWithBytesNoCopy:KSURLData
514                                        length:KSURLData_len
515                                  freeWhenDone:NO];
516
517  NSString *directory = [self ksurlValidatedDirectory];
518  if (directory == nil) {
519    GTMLoggerInfo(@"Can't safely create directory for ksurl.");
520    return nil;
521  }
522
523  NSString *destination = [directory
524                              stringByAppendingPathComponent:@"ksurl"];
525  NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
526  NSDictionary *attr =
527    [NSDictionary dictionaryWithObject:properPermission
528                                forKey:NSFilePosixPermissions];
529  if ([[NSFileManager defaultManager] createFileAtPath:destination
530                                              contents:ksdata
531                                            attributes:attr] == NO) {
532    GTMLoggerInfo(@"Can't create ksurl.");
533    return nil;
534  }
535  ksurlPath_ = [destination retain];
536  return ksurlPath_;
537}
538
539- (void)markProgress:(float)progress {
540  [[self processor] runningAction:self progress:progress];
541}
542
543- (void)progressNotification:(NSNotification *)notification {
544  NSNumber *progress = [[notification userInfo] objectForKey:KSURLProgressKey];
545  if (progress)
546    [self markProgress:[progress floatValue]];
547}
548
549@end  // KSDownloadAction (PrivateMethods)