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