Home | History | Annotate | Download | only in gcapi_mac
      1 // Copyright (c) 2012 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/installer/gcapi_mac/gcapi.h"
      6 
      7 #import <Cocoa/Cocoa.h>
      8 #include <grp.h>
      9 #include <pwd.h>
     10 #include <sys/stat.h>
     11 #include <sys/types.h>
     12 #include <sys/utsname.h>
     13 
     14 namespace {
     15 
     16 // The "~~" prefixes are replaced with the home directory of the
     17 // console owner (i.e. not the home directory of the euid).
     18 NSString* const kChromeInstallPath = @"/Applications/Google Chrome.app";
     19 
     20 NSString* const kBrandKey = @"KSBrandID";
     21 NSString* const kUserBrandPath = @"~~/Library/Google/Google Chrome Brand.plist";
     22 
     23 NSString* const kSystemKsadminPath =
     24     @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
     25      "Contents/MacOS/ksadmin";
     26 
     27 NSString* const kUserKsadminPath =
     28     @"~~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
     29      "Contents/MacOS/ksadmin";
     30 
     31 NSString* const kSystemMasterPrefsPath =
     32     @"/Library/Google/Google Chrome Master Preferences";
     33 NSString* const kUserMasterPrefsPath =
     34     @"~~/Library/Application Support/Google/Chrome/"
     35      "Google Chrome Master Preferences";
     36 
     37 // Condensed from chromium's base/mac/mac_util.mm.
     38 bool IsOSXVersionSupported() {
     39   // On 10.6, Gestalt() was observed to be able to spawn threads (see
     40   // http://crbug.com/53200). Don't call Gestalt().
     41   struct utsname uname_info;
     42   if (uname(&uname_info) != 0)
     43     return false;
     44   if (strcmp(uname_info.sysname, "Darwin") != 0)
     45     return false;
     46 
     47   char* dot = strchr(uname_info.release, '.');
     48   if (!dot)
     49     return false;
     50 
     51   int darwin_major_version = atoi(uname_info.release);
     52   if (darwin_major_version < 6)
     53     return false;
     54 
     55   // The Darwin major version is always 4 greater than the Mac OS X minor
     56   // version for Darwin versions beginning with 6, corresponding to Mac OS X
     57   // 10.2.
     58   int mac_os_x_minor_version = darwin_major_version - 4;
     59 
     60   // Chrome is known to work on 10.6 - 10.9.
     61   return mac_os_x_minor_version >= 6 && mac_os_x_minor_version <= 9;
     62 }
     63 
     64 // Returns the pid/gid of the logged-in user, even if getuid() claims that the
     65 // current user is root.
     66 // Returns NULL on error.
     67 passwd* GetRealUserId() {
     68   CFDictionaryRef session_info_dict = CGSessionCopyCurrentDictionary();
     69   [NSMakeCollectable(session_info_dict) autorelease];
     70   if (!session_info_dict)
     71     return NULL;  // Possibly no screen plugged in.
     72 
     73   CFNumberRef ns_uid = (CFNumberRef)CFDictionaryGetValue(session_info_dict,
     74                                                          kCGSessionUserIDKey);
     75   if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
     76     return NULL;
     77 
     78   uid_t uid;
     79   BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
     80   if (!success)
     81     return NULL;
     82 
     83   return getpwuid(uid);
     84 }
     85 
     86 enum TicketKind {
     87   kSystemTicket, kUserTicket
     88 };
     89 
     90 // Replaces "~~" with |home_dir|.
     91 NSString* AdjustHomedir(NSString* s, const char* home_dir) {
     92   if (![s hasPrefix:@"~~"])
     93     return s;
     94   NSString* ns_home_dir = [NSString stringWithUTF8String:home_dir];
     95   return [ns_home_dir stringByAppendingString:[s substringFromIndex:2]];
     96 }
     97 
     98 // If |chrome_path| is not 0, |*chrome_path| is set to the path where chrome
     99 // is according to keystone. It's only set if that path exists on disk.
    100 BOOL FindChromeTicket(TicketKind kind, const passwd* user,
    101                       NSString** chrome_path) {
    102   if (chrome_path)
    103     *chrome_path = nil;
    104 
    105   // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4.
    106   NSMutableArray* keystone_paths =
    107       [NSMutableArray arrayWithObject:kSystemKsadminPath];
    108   if (kind == kUserTicket) {
    109     [keystone_paths insertObject:AdjustHomedir(kUserKsadminPath, user->pw_dir)
    110                         atIndex:0];
    111   }
    112   NSEnumerator* e = [keystone_paths objectEnumerator];
    113   id ks_path;
    114   while ((ks_path = [e nextObject])) {
    115     if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
    116       continue;
    117 
    118     NSTask* task = nil;
    119     NSString* string = nil;
    120     bool ksadmin_ran_successfully = false;
    121 
    122     @try {
    123       task = [[NSTask alloc] init];
    124       [task setLaunchPath:ks_path];
    125 
    126       NSArray* arguments = @[
    127           kind == kUserTicket ? @"--user-store" : @"--system-store",
    128           @"--print-tickets",
    129           @"--productid",
    130           @"com.google.Chrome",
    131       ];
    132       if (geteuid() == 0 && kind == kUserTicket) {
    133         NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
    134         [task setLaunchPath:@"/usr/bin/sudo"];
    135         arguments = [@[@"-u", run_as, ks_path]
    136             arrayByAddingObjectsFromArray:arguments];
    137       }
    138       [task setArguments:arguments];
    139 
    140       NSPipe* pipe = [NSPipe pipe];
    141       [task setStandardOutput:pipe];
    142 
    143       NSFileHandle* file = [pipe fileHandleForReading];
    144 
    145       [task launch];
    146 
    147       NSData* data = [file readDataToEndOfFile];
    148       [task waitUntilExit];
    149 
    150       ksadmin_ran_successfully = [task terminationStatus] == 0;
    151       string = [[[NSString alloc] initWithData:data
    152                                     encoding:NSUTF8StringEncoding] autorelease];
    153     }
    154     @catch (id exception) {
    155       // Most likely, ks_path didn't exist.
    156     }
    157     [task release];
    158 
    159     if (ksadmin_ran_successfully && [string length] > 0) {
    160       // If the user deleted chrome, it doesn't get unregistered in keystone.
    161       // Check if the path keystone thinks chrome is at still exists, and if not
    162       // treat this as "chrome isn't installed". Sniff for
    163       //   xc=<KSPathExistenceChecker:1234 path=/Applications/Google Chrome.app>
    164       // in the output. But don't mess with system tickets, since reinstalling
    165       // a user chrome on top of a system ticket produces a non-autoupdating
    166       // chrome.
    167       NSRange start = [string rangeOfString:@"\n\txc=<KSPathExistenceChecker:"];
    168       if (start.location == NSNotFound && start.length == 0)
    169         return YES;  // Err on the cautious side.
    170       string = [string substringFromIndex:start.location];
    171 
    172       start = [string rangeOfString:@"path="];
    173       if (start.location == NSNotFound && start.length == 0)
    174         return YES;  // Err on the cautious side.
    175       string = [string substringFromIndex:start.location];
    176 
    177       NSRange end = [string rangeOfString:@".app>\n\t"];
    178       if (end.location == NSNotFound && end.length == 0)
    179         return YES;
    180 
    181       string = [string substringToIndex:NSMaxRange(end) - [@">\n\t" length]];
    182       string = [string substringFromIndex:start.length];
    183 
    184       BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:string];
    185       if (exists && chrome_path)
    186         *chrome_path = string;
    187       // Don't allow reinstallation over a system ticket, even if chrome doesn't
    188       // exist on disk.
    189       if (kind == kSystemTicket)
    190         return YES;
    191       return exists;
    192     }
    193   }
    194 
    195   return NO;
    196 }
    197 
    198 // File permission mask for files created by gcapi.
    199 const mode_t kUserPermissions = 0755;
    200 const mode_t kAdminPermissions = 0775;
    201 
    202 BOOL CreatePathToFile(NSString* path, const passwd* user) {
    203   path = [path stringByDeletingLastPathComponent];
    204 
    205   // Default owner, group, permissions:
    206   // * Permissions are set according to the umask of the current process. For
    207   //   more information, see umask.
    208   // * The owner ID is set to the effective user ID of the process.
    209   // * The group ID is set to that of the parent directory.
    210   // The default group ID is fine. Owner ID is fine if creating a system path,
    211   // but when creating a user path explicitly set the owner in case euid is 0.
    212   // Do set permissions explicitly; for admin paths all admins can write, for
    213   // user paths just the owner may.
    214   NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
    215   if (user) {
    216     [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
    217                    forKey:NSFilePosixPermissions];
    218     [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
    219                    forKey:NSFileOwnerAccountID];
    220   } else {
    221     [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
    222                    forKey:NSFilePosixPermissions];
    223     [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
    224   }
    225 
    226   NSFileManager* manager = [NSFileManager defaultManager];
    227   return [manager createDirectoryAtPath:path
    228             withIntermediateDirectories:YES
    229                              attributes:attributes
    230                                   error:nil];
    231 }
    232 
    233 // Tries to write |data| at |user_path|.
    234 // Returns the path where it wrote, or nil on failure.
    235 NSString* WriteUserData(NSData* data,
    236                         NSString* user_path,
    237                         const passwd* user) {
    238   user_path = AdjustHomedir(user_path, user->pw_dir);
    239   if (CreatePathToFile(user_path, user) &&
    240       [data writeToFile:user_path atomically:YES]) {
    241     chmod([user_path fileSystemRepresentation], kUserPermissions & ~0111);
    242     chown([user_path fileSystemRepresentation], user->pw_uid, user->pw_gid);
    243     return user_path;
    244   }
    245   return nil;
    246 }
    247 
    248 // Tries to write |data| at |system_path| or if that fails at |user_path|.
    249 // Returns the path where it wrote, or nil on failure.
    250 NSString* WriteData(NSData* data,
    251                     NSString* system_path,
    252                     NSString* user_path,
    253                     const passwd* user) {
    254   // Try system first.
    255   if (CreatePathToFile(system_path, NULL) &&
    256       [data writeToFile:system_path atomically:YES]) {
    257     chmod([system_path fileSystemRepresentation], kAdminPermissions & ~0111);
    258     // Make sure the file is owned by group admin.
    259     if (group* group = getgrnam("admin"))
    260       chown([system_path fileSystemRepresentation], 0, group->gr_gid);
    261     return system_path;
    262   }
    263 
    264   // Failed, try user.
    265   return WriteUserData(data, user_path, user);
    266 }
    267 
    268 NSString* WriteBrandCode(const char* brand_code, const passwd* user) {
    269   NSDictionary* brand_dict = @{
    270       kBrandKey: [NSString stringWithUTF8String:brand_code],
    271   };
    272   NSData* contents = [NSPropertyListSerialization
    273       dataFromPropertyList:brand_dict
    274                     format:NSPropertyListBinaryFormat_v1_0
    275           errorDescription:nil];
    276 
    277   return WriteUserData(contents, kUserBrandPath, user);
    278 }
    279 
    280 BOOL WriteMasterPrefs(const char* master_prefs_contents,
    281                       size_t master_prefs_contents_size,
    282                       const passwd* user) {
    283   NSData* contents = [NSData dataWithBytes:master_prefs_contents
    284                                     length:master_prefs_contents_size];
    285   return WriteData(
    286       contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
    287 }
    288 
    289 NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
    290   NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
    291   if (!version)
    292     return nil;
    293   return [[[app_path
    294       stringByAppendingPathComponent:@"Contents/Versions"]
    295       stringByAppendingPathComponent:version]
    296       stringByAppendingPathComponent:@"Google Chrome Framework.framework"];
    297 }
    298 
    299 NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) {
    300   return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent:
    301       @"Resources/install.sh"];
    302 }
    303 
    304 bool isbrandchar(int c) {
    305   // Always four upper-case alpha chars.
    306   return c >= 'A' && c <= 'Z';
    307 }
    308 
    309 }  // namespace
    310 
    311 int GoogleChromeCompatibilityCheck(unsigned* reasons) {
    312   unsigned local_reasons = 0;
    313   @autoreleasepool {
    314     passwd* user = GetRealUserId();
    315     if (!user)
    316       return GCCC_ERROR_ACCESSDENIED;
    317 
    318     if (!IsOSXVersionSupported())
    319       local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
    320 
    321     NSString* path;
    322     if (FindChromeTicket(kSystemTicket, NULL, &path)) {
    323       local_reasons |= GCCC_ERROR_ALREADYPRESENT;
    324       if (!path)  // Ticket points to nothingness.
    325         local_reasons |= GCCC_ERROR_ACCESSDENIED;
    326     }
    327 
    328     if (FindChromeTicket(kUserTicket, user, NULL))
    329       local_reasons |= GCCC_ERROR_ALREADYPRESENT;
    330 
    331     if ([[NSFileManager defaultManager] fileExistsAtPath:kChromeInstallPath])
    332       local_reasons |= GCCC_ERROR_ALREADYPRESENT;
    333 
    334     if ((local_reasons & GCCC_ERROR_ALREADYPRESENT) == 0) {
    335       if (![[NSFileManager defaultManager]
    336               isWritableFileAtPath:@"/Applications"])
    337       local_reasons |= GCCC_ERROR_ACCESSDENIED;
    338     }
    339 
    340   }
    341   if (reasons != NULL)
    342     *reasons = local_reasons;
    343   return local_reasons == 0;
    344 }
    345 
    346 int InstallGoogleChrome(const char* source_path,
    347                         const char* brand_code,
    348                         const char* master_prefs_contents,
    349                         unsigned master_prefs_contents_size) {
    350   if (!GoogleChromeCompatibilityCheck(NULL))
    351     return 0;
    352 
    353   @autoreleasepool {
    354     passwd* user = GetRealUserId();
    355     if (!user)
    356       return 0;
    357 
    358     NSString* app_path = [NSString stringWithUTF8String:source_path];
    359     NSString* info_plist_path =
    360         [app_path stringByAppendingPathComponent:@"Contents/Info.plist"];
    361     NSDictionary* info_plist =
    362         [NSDictionary dictionaryWithContentsOfFile:info_plist_path];
    363 
    364     // Use install.sh from the Chrome app bundle to copy Chrome to its
    365     // destination.
    366     NSString* install_script = PathToInstallScript(app_path, info_plist);
    367     if (!install_script) {
    368       return 0;
    369     }
    370 
    371     @try {
    372       NSTask* task = [[[NSTask alloc] init] autorelease];
    373 
    374       // install.sh tries to make the installed app admin-writable, but
    375       // only when it's not run as root.
    376       if (geteuid() == 0) {
    377         // Use |su $(whoami)| instead of sudo -u. If the current user is in more
    378         // than 16 groups, |sudo -u $(whoami)| will drop all but the first 16
    379         // groups, which can lead to problems (e.g. if "admin" is one of the
    380         // dropped groups).
    381         // Since geteuid() is 0, su won't prompt for a password.
    382         NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
    383         [task setLaunchPath:@"/usr/bin/su"];
    384 
    385         NSString* single_quote_escape = @"'\"'\"'";
    386         NSString* install_script_quoted = [install_script
    387             stringByReplacingOccurrencesOfString:@"'"
    388                                       withString:single_quote_escape];
    389         NSString* app_path_quoted =
    390             [app_path stringByReplacingOccurrencesOfString:@"'"
    391                                                 withString:single_quote_escape];
    392         NSString* install_path_quoted = [kChromeInstallPath
    393             stringByReplacingOccurrencesOfString:@"'"
    394                                       withString:single_quote_escape];
    395 
    396         NSString* install_script_execution =
    397             [NSString stringWithFormat:@"exec '%@' '%@' '%@'",
    398                                        install_script_quoted,
    399                                        app_path_quoted,
    400                                        install_path_quoted];
    401         [task setArguments:
    402             @[run_as, @"-c", install_script_execution]];
    403       } else {
    404         [task setLaunchPath:install_script];
    405         [task setArguments:@[app_path, kChromeInstallPath]];
    406       }
    407 
    408       [task launch];
    409       [task waitUntilExit];
    410       if ([task terminationStatus] != 0) {
    411         return 0;
    412       }
    413     }
    414     @catch (id exception) {
    415       return 0;
    416     }
    417 
    418     // Set brand code. If Chrome's Info.plist contains a brand code, use that.
    419     NSString* info_plist_brand = [info_plist objectForKey:kBrandKey];
    420     if (info_plist_brand &&
    421         [info_plist_brand respondsToSelector:@selector(UTF8String)])
    422       brand_code = [info_plist_brand UTF8String];
    423 
    424     BOOL valid_brand_code = brand_code && strlen(brand_code) == 4 &&
    425         isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) &&
    426         isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]);
    427 
    428     NSString* brand_path = nil;
    429     if (valid_brand_code)
    430       brand_path = WriteBrandCode(brand_code, user);
    431 
    432     // Write master prefs.
    433     if (master_prefs_contents)
    434       WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size, user);
    435 
    436     // TODO Set default browser if requested.
    437   }
    438   return 1;
    439 }
    440 
    441 int LaunchGoogleChrome() {
    442   @autoreleasepool {
    443     passwd* user = GetRealUserId();
    444     if (!user)
    445       return 0;
    446 
    447     NSString* app_path;
    448 
    449     NSString* path;
    450     if (FindChromeTicket(kUserTicket, user, &path) && path)
    451       app_path = path;
    452     else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
    453       app_path = path;
    454     else
    455       app_path = kChromeInstallPath;
    456 
    457     // NSWorkspace launches processes as the current console owner,
    458     // even when running with euid of 0.
    459     return [[NSWorkspace sharedWorkspace] launchApplication:app_path];
    460   }
    461 }
    462