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