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