// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/installer/gcapi_mac/gcapi.h" #import #include #include #include #include #include #include namespace { // The "~~" prefixes are replaced with the home directory of the // console owner (i.e. not the home directory of the euid). NSString* const kChromeInstallPath = @"/Applications/Google Chrome.app"; NSString* const kBrandKey = @"KSBrandID"; NSString* const kUserBrandPath = @"~~/Library/Google/Google Chrome Brand.plist"; NSString* const kSystemKsadminPath = @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" "Contents/MacOS/ksadmin"; NSString* const kUserKsadminPath = @"~~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" "Contents/MacOS/ksadmin"; NSString* const kSystemMasterPrefsPath = @"/Library/Google/Google Chrome Master Preferences"; NSString* const kUserMasterPrefsPath = @"~~/Library/Application Support/Google/Chrome/" "Google Chrome Master Preferences"; // Condensed from chromium's base/mac/mac_util.mm. bool IsOSXVersionSupported() { // On 10.6, Gestalt() was observed to be able to spawn threads (see // http://crbug.com/53200). Don't call Gestalt(). struct utsname uname_info; if (uname(&uname_info) != 0) return false; if (strcmp(uname_info.sysname, "Darwin") != 0) return false; char* dot = strchr(uname_info.release, '.'); if (!dot) return false; int darwin_major_version = atoi(uname_info.release); if (darwin_major_version < 6) return false; // The Darwin major version is always 4 greater than the Mac OS X minor // version for Darwin versions beginning with 6, corresponding to Mac OS X // 10.2. int mac_os_x_minor_version = darwin_major_version - 4; // Chrome is known to work on 10.9 - 10.11. return mac_os_x_minor_version >= 9 && mac_os_x_minor_version <= 11; } // Returns the pid/gid of the logged-in user, even if getuid() claims that the // current user is root. // Returns NULL on error. passwd* GetRealUserId() { CFDictionaryRef session_info_dict = CGSessionCopyCurrentDictionary(); [NSMakeCollectable(session_info_dict) autorelease]; if (!session_info_dict) return NULL; // Possibly no screen plugged in. CFNumberRef ns_uid = (CFNumberRef)CFDictionaryGetValue(session_info_dict, kCGSessionUserIDKey); if (CFGetTypeID(ns_uid) != CFNumberGetTypeID()) return NULL; uid_t uid; BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid); if (!success) return NULL; return getpwuid(uid); } enum TicketKind { kSystemTicket, kUserTicket }; // Replaces "~~" with |home_dir|. NSString* AdjustHomedir(NSString* s, const char* home_dir) { if (![s hasPrefix:@"~~"]) return s; NSString* ns_home_dir = [NSString stringWithUTF8String:home_dir]; return [ns_home_dir stringByAppendingString:[s substringFromIndex:2]]; } // If |chrome_path| is not 0, |*chrome_path| is set to the path where chrome // is according to keystone. It's only set if that path exists on disk. BOOL FindChromeTicket(TicketKind kind, const passwd* user, NSString** chrome_path) { if (chrome_path) *chrome_path = nil; // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4. NSMutableArray* keystone_paths = [NSMutableArray arrayWithObject:kSystemKsadminPath]; if (kind == kUserTicket) { [keystone_paths insertObject:AdjustHomedir(kUserKsadminPath, user->pw_dir) atIndex:0]; } NSEnumerator* e = [keystone_paths objectEnumerator]; id ks_path; while ((ks_path = [e nextObject])) { if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path]) continue; NSTask* task = nil; NSString* string = nil; bool ksadmin_ran_successfully = false; @try { task = [[NSTask alloc] init]; [task setLaunchPath:ks_path]; NSArray* arguments = @[ kind == kUserTicket ? @"--user-store" : @"--system-store", @"--print-tickets", @"--productid", @"com.google.Chrome", ]; if (geteuid() == 0 && kind == kUserTicket) { NSString* run_as = [NSString stringWithUTF8String:user->pw_name]; [task setLaunchPath:@"/usr/bin/sudo"]; arguments = [@[@"-u", run_as, ks_path] arrayByAddingObjectsFromArray:arguments]; } [task setArguments:arguments]; NSPipe* pipe = [NSPipe pipe]; [task setStandardOutput:pipe]; NSFileHandle* file = [pipe fileHandleForReading]; [task launch]; NSData* data = [file readDataToEndOfFile]; [task waitUntilExit]; ksadmin_ran_successfully = [task terminationStatus] == 0; string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; } @catch (id exception) { // Most likely, ks_path didn't exist. } [task release]; if (ksadmin_ran_successfully && [string length] > 0) { // If the user deleted chrome, it doesn't get unregistered in keystone. // Check if the path keystone thinks chrome is at still exists, and if not // treat this as "chrome isn't installed". Sniff for // xc= // in the output. But don't mess with system tickets, since reinstalling // a user chrome on top of a system ticket produces a non-autoupdating // chrome. NSRange start = [string rangeOfString:@"\n\txc=\n\t"]; if (end.location == NSNotFound && end.length == 0) return YES; string = [string substringToIndex:NSMaxRange(end) - [@">\n\t" length]]; string = [string substringFromIndex:start.length]; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:string]; if (exists && chrome_path) *chrome_path = string; // Don't allow reinstallation over a system ticket, even if chrome doesn't // exist on disk. if (kind == kSystemTicket) return YES; return exists; } } return NO; } // File permission mask for files created by gcapi. const mode_t kUserPermissions = 0755; const mode_t kAdminPermissions = 0775; BOOL CreatePathToFile(NSString* path, const passwd* user) { path = [path stringByDeletingLastPathComponent]; // Default owner, group, permissions: // * Permissions are set according to the umask of the current process. For // more information, see umask. // * The owner ID is set to the effective user ID of the process. // * The group ID is set to that of the parent directory. // The default group ID is fine. Owner ID is fine if creating a system path, // but when creating a user path explicitly set the owner in case euid is 0. // Do set permissions explicitly; for admin paths all admins can write, for // user paths just the owner may. NSMutableDictionary* attributes = [NSMutableDictionary dictionary]; if (user) { [attributes setObject:[NSNumber numberWithShort:kUserPermissions] forKey:NSFilePosixPermissions]; [attributes setObject:[NSNumber numberWithInt:user->pw_uid] forKey:NSFileOwnerAccountID]; } else { [attributes setObject:[NSNumber numberWithShort:kAdminPermissions] forKey:NSFilePosixPermissions]; [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName]; } NSFileManager* manager = [NSFileManager defaultManager]; return [manager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:attributes error:nil]; } // Tries to write |data| at |user_path|. // Returns the path where it wrote, or nil on failure. NSString* WriteUserData(NSData* data, NSString* user_path, const passwd* user) { user_path = AdjustHomedir(user_path, user->pw_dir); if (CreatePathToFile(user_path, user) && [data writeToFile:user_path atomically:YES]) { chmod([user_path fileSystemRepresentation], kUserPermissions & ~0111); chown([user_path fileSystemRepresentation], user->pw_uid, user->pw_gid); return user_path; } return nil; } // Tries to write |data| at |system_path| or if that fails at |user_path|. // Returns the path where it wrote, or nil on failure. NSString* WriteData(NSData* data, NSString* system_path, NSString* user_path, const passwd* user) { // Try system first. if (CreatePathToFile(system_path, NULL) && [data writeToFile:system_path atomically:YES]) { chmod([system_path fileSystemRepresentation], kAdminPermissions & ~0111); // Make sure the file is owned by group admin. if (group* group = getgrnam("admin")) chown([system_path fileSystemRepresentation], 0, group->gr_gid); return system_path; } // Failed, try user. return WriteUserData(data, user_path, user); } NSString* WriteBrandCode(const char* brand_code, const passwd* user) { NSDictionary* brand_dict = @{ kBrandKey: [NSString stringWithUTF8String:brand_code], }; NSData* contents = [NSPropertyListSerialization dataFromPropertyList:brand_dict format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil]; return WriteUserData(contents, kUserBrandPath, user); } BOOL WriteMasterPrefs(const char* master_prefs_contents, size_t master_prefs_contents_size, const passwd* user) { NSData* contents = [NSData dataWithBytes:master_prefs_contents length:master_prefs_contents_size]; return WriteData( contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil; } NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) { NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"]; if (!version) return nil; return [[[app_path stringByAppendingPathComponent:@"Contents/Versions"] stringByAppendingPathComponent:version] stringByAppendingPathComponent:@"Google Chrome Framework.framework"]; } NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) { return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent: @"Resources/install.sh"]; } bool isbrandchar(int c) { // Always four upper-case alpha chars. return c >= 'A' && c <= 'Z'; } } // namespace int GoogleChromeCompatibilityCheck(unsigned* reasons) { unsigned local_reasons = 0; @autoreleasepool { passwd* user = GetRealUserId(); if (!user) return GCCC_ERROR_ACCESSDENIED; if (!IsOSXVersionSupported()) local_reasons |= GCCC_ERROR_OSNOTSUPPORTED; NSString* path; if (FindChromeTicket(kSystemTicket, NULL, &path)) { local_reasons |= GCCC_ERROR_ALREADYPRESENT; if (!path) // Ticket points to nothingness. local_reasons |= GCCC_ERROR_ACCESSDENIED; } if (FindChromeTicket(kUserTicket, user, NULL)) local_reasons |= GCCC_ERROR_ALREADYPRESENT; if ([[NSFileManager defaultManager] fileExistsAtPath:kChromeInstallPath]) local_reasons |= GCCC_ERROR_ALREADYPRESENT; if ((local_reasons & GCCC_ERROR_ALREADYPRESENT) == 0) { if (![[NSFileManager defaultManager] isWritableFileAtPath:@"/Applications"]) local_reasons |= GCCC_ERROR_ACCESSDENIED; } } if (reasons != NULL) *reasons = local_reasons; return local_reasons == 0; } int InstallGoogleChrome(const char* source_path, const char* brand_code, const char* master_prefs_contents, unsigned master_prefs_contents_size) { if (!GoogleChromeCompatibilityCheck(NULL)) return 0; @autoreleasepool { passwd* user = GetRealUserId(); if (!user) return 0; NSString* app_path = [NSString stringWithUTF8String:source_path]; NSString* info_plist_path = [app_path stringByAppendingPathComponent:@"Contents/Info.plist"]; NSDictionary* info_plist = [NSDictionary dictionaryWithContentsOfFile:info_plist_path]; // Use install.sh from the Chrome app bundle to copy Chrome to its // destination. NSString* install_script = PathToInstallScript(app_path, info_plist); if (!install_script) { return 0; } @try { NSTask* task = [[[NSTask alloc] init] autorelease]; // install.sh tries to make the installed app admin-writable, but // only when it's not run as root. if (geteuid() == 0) { // Use |su $(whoami)| instead of sudo -u. If the current user is in more // than 16 groups, |sudo -u $(whoami)| will drop all but the first 16 // groups, which can lead to problems (e.g. if "admin" is one of the // dropped groups). // Since geteuid() is 0, su won't prompt for a password. NSString* run_as = [NSString stringWithUTF8String:user->pw_name]; [task setLaunchPath:@"/usr/bin/su"]; NSString* single_quote_escape = @"'\"'\"'"; NSString* install_script_quoted = [install_script stringByReplacingOccurrencesOfString:@"'" withString:single_quote_escape]; NSString* app_path_quoted = [app_path stringByReplacingOccurrencesOfString:@"'" withString:single_quote_escape]; NSString* install_path_quoted = [kChromeInstallPath stringByReplacingOccurrencesOfString:@"'" withString:single_quote_escape]; NSString* install_script_execution = [NSString stringWithFormat:@"exec '%@' '%@' '%@'", install_script_quoted, app_path_quoted, install_path_quoted]; [task setArguments: @[run_as, @"-c", install_script_execution]]; } else { [task setLaunchPath:install_script]; [task setArguments:@[app_path, kChromeInstallPath]]; } [task launch]; [task waitUntilExit]; if ([task terminationStatus] != 0) { return 0; } } @catch (id exception) { return 0; } // Set brand code. If Chrome's Info.plist contains a brand code, use that. NSString* info_plist_brand = [info_plist objectForKey:kBrandKey]; if (info_plist_brand && [info_plist_brand respondsToSelector:@selector(UTF8String)]) brand_code = [info_plist_brand UTF8String]; BOOL valid_brand_code = brand_code && strlen(brand_code) == 4 && isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) && isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]); NSString* brand_path = nil; if (valid_brand_code) brand_path = WriteBrandCode(brand_code, user); // Write master prefs. if (master_prefs_contents) WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size, user); // TODO Set default browser if requested. } return 1; } int LaunchGoogleChrome() { @autoreleasepool { passwd* user = GetRealUserId(); if (!user) return 0; NSString* app_path; NSString* path; if (FindChromeTicket(kUserTicket, user, &path) && path) app_path = path; else if (FindChromeTicket(kSystemTicket, NULL, &path) && path) app_path = path; else app_path = kChromeInstallPath; // NSWorkspace launches processes as the current console owner, // even when running with euid of 0. return [[NSWorkspace sharedWorkspace] launchApplication:app_path]; } }