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 #import "chrome/browser/mac/keystone_glue.h" 6 7 #include <sys/mount.h> 8 #include <sys/param.h> 9 #include <sys/stat.h> 10 #include <sys/sysctl.h> 11 #include <sys/types.h> 12 13 #include <vector> 14 15 #include "base/bind.h" 16 #include "base/location.h" 17 #include "base/logging.h" 18 #include "base/mac/authorization_util.h" 19 #include "base/mac/bundle_locations.h" 20 #include "base/mac/mac_logging.h" 21 #include "base/mac/mac_util.h" 22 #include "base/mac/scoped_nsautorelease_pool.h" 23 #include "base/mac/scoped_nsexception_enabler.h" 24 #include "base/memory/ref_counted.h" 25 #include "base/strings/sys_string_conversions.h" 26 #include "base/threading/worker_pool.h" 27 #include "build/build_config.h" 28 #import "chrome/browser/mac/keystone_registration.h" 29 #include "chrome/common/chrome_constants.h" 30 #include "chrome/common/chrome_version_info.h" 31 #include "grit/chromium_strings.h" 32 #include "grit/generated_resources.h" 33 #include "ui/base/l10n/l10n_util.h" 34 #include "ui/base/l10n/l10n_util_mac.h" 35 36 namespace { 37 38 namespace ksr = keystone_registration; 39 40 // Constants for the brand file (uses an external file so it can survive 41 // updates to Chrome.) 42 43 #if defined(GOOGLE_CHROME_BUILD) 44 #define kBrandFileName @"Google Chrome Brand.plist"; 45 #elif defined(CHROMIUM_BUILD) 46 #define kBrandFileName @"Chromium Brand.plist"; 47 #else 48 #error Unknown branding 49 #endif 50 51 // These directories are hardcoded in Keystone promotion preflight and the 52 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used 53 // since the scripts couldn't use anything like that. 54 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName; 55 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName; 56 57 NSString* UserBrandFilePath() { 58 return [kBrandUserFile stringByStandardizingPath]; 59 } 60 NSString* SystemBrandFilePath() { 61 return [kBrandSystemFile stringByStandardizingPath]; 62 } 63 64 // Adaptor for scheduling an Objective-C method call on a |WorkerPool| 65 // thread. 66 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> { 67 public: 68 69 // Call |sel| on |target| with |arg| in a WorkerPool thread. 70 // |target| and |arg| are retained, |arg| may be |nil|. 71 static void PostPerform(id target, SEL sel, id arg) { 72 DCHECK(target); 73 DCHECK(sel); 74 75 scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg); 76 base::WorkerPool::PostTask( 77 FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true); 78 } 79 80 // Convenience for the no-argument case. 81 static void PostPerform(id target, SEL sel) { 82 PostPerform(target, sel, nil); 83 } 84 85 private: 86 // Allow RefCountedThreadSafe<> to delete. 87 friend class base::RefCountedThreadSafe<PerformBridge>; 88 89 PerformBridge(id target, SEL sel, id arg) 90 : target_([target retain]), 91 sel_(sel), 92 arg_([arg retain]) { 93 } 94 95 ~PerformBridge() {} 96 97 // Happens on a WorkerPool thread. 98 void Run() { 99 base::mac::ScopedNSAutoreleasePool pool; 100 [target_ performSelector:sel_ withObject:arg_]; 101 } 102 103 base::scoped_nsobject<id> target_; 104 SEL sel_; 105 base::scoped_nsobject<id> arg_; 106 }; 107 108 } // namespace 109 110 @interface KeystoneGlue (Private) 111 112 // Returns the path to the application's Info.plist file. This returns the 113 // outer application bundle's Info.plist, not the framework's Info.plist. 114 - (NSString*)appInfoPlistPath; 115 116 // Returns a dictionary containing parameters to be used for a KSRegistration 117 // -registerWithParameters: or -promoteWithParameters:authorization: call. 118 - (NSDictionary*)keystoneParameters; 119 120 // Called when Keystone registration completes. 121 - (void)registrationComplete:(NSNotification*)notification; 122 123 // Called periodically to announce activity by pinging the Keystone server. 124 - (void)markActive:(NSTimer*)timer; 125 126 // Called when an update check or update installation is complete. Posts the 127 // kAutoupdateStatusNotification notification to the default notification 128 // center. 129 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; 130 131 // Returns the version of the currently-installed application on disk. 132 - (NSString*)currentlyInstalledVersion; 133 134 // These three methods are used to determine the version of the application 135 // currently installed on disk, compare that to the currently-running version, 136 // decide whether any updates have been installed, and call 137 // -updateStatus:version:. 138 // 139 // In order to check the version on disk, the installed application's 140 // Info.plist dictionary must be read; in order to see changes as updates are 141 // applied, the dictionary must be read each time, bypassing any caches such 142 // as the one that NSBundle might be maintaining. Reading files can be a 143 // blocking operation, and blocking operations are to be avoided on the main 144 // thread. I'm not quite sure what jank means, but I bet that a blocked main 145 // thread would cause some of it. 146 // 147 // -determineUpdateStatusAsync is called on the main thread to initiate the 148 // operation. It performs initial set-up work that must be done on the main 149 // thread and arranges for -determineUpdateStatus to be called on a work queue 150 // thread managed by WorkerPool. 151 // -determineUpdateStatus then reads the Info.plist, gets the version from the 152 // CFBundleShortVersionString key, and performs 153 // -determineUpdateStatusForVersion: on the main thread. 154 // -determineUpdateStatusForVersion: does the actual comparison of the version 155 // on disk with the running version and calls -updateStatus:version: with the 156 // results of its analysis. 157 - (void)determineUpdateStatusAsync; 158 - (void)determineUpdateStatus; 159 - (void)determineUpdateStatusForVersion:(NSString*)version; 160 161 // Returns YES if registration_ is definitely on a user ticket. If definitely 162 // on a system ticket, or uncertain of ticket type (due to an older version 163 // of Keystone being used), returns NO. 164 - (BOOL)isUserTicket; 165 166 // Returns YES if Keystone is definitely installed at the system level, 167 // determined by the presence of an executable ksadmin program at the expected 168 // system location. 169 - (BOOL)isSystemKeystone; 170 171 // Returns YES if on a system ticket but system Keystone is not present. 172 // Returns NO otherwise. The "doomed" condition will result in the 173 // registration framework appearing to have registered Chrome, but no updates 174 // ever actually taking place. 175 - (BOOL)isSystemTicketDoomed; 176 177 // Called when ticket promotion completes. 178 - (void)promotionComplete:(NSNotification*)notification; 179 180 // Changes the application's ownership and permissions so that all files are 181 // owned by root:wheel and all files and directories are writable only by 182 // root, but readable and executable as needed by everyone. 183 // -changePermissionsForPromotionAsync is called on the main thread by 184 // -promotionComplete. That routine calls 185 // -changePermissionsForPromotionWithTool: on a work queue thread. When done, 186 // -changePermissionsForPromotionComplete is called on the main thread. 187 - (void)changePermissionsForPromotionAsync; 188 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath; 189 - (void)changePermissionsForPromotionComplete; 190 191 // Returns the brand file path to use for Keystone. 192 - (NSString*)brandFilePath; 193 194 // YES if the system's CPU is 32-bit-only, NO if it's 64-bit-capable. 195 - (BOOL)has32BitOnlyCPU; 196 197 // YES if no update installation has succeeded since a binary diff patch 198 // installation failed. This signals the need to attempt a full installer 199 // which does not depend on applying a patch to existing files. 200 - (BOOL)wantsFullInstaller; 201 202 // Returns an NSString* suitable for appending to a Chrome Keystone tag value 203 // or tag key. If the system has a 32-bit-only CPU, the tag suffix will 204 // contain the string "-32bit". If a full installer (as opposed to a binary 205 // diff/delta patch) is required, the tag suffix will contain the string 206 // "-full". If no special treatment is required, the tag suffix will be an 207 // empty string. 208 - (NSString*)tagSuffix; 209 210 @end // @interface KeystoneGlue (Private) 211 212 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification"; 213 NSString* const kAutoupdateStatusStatus = @"status"; 214 NSString* const kAutoupdateStatusVersion = @"version"; 215 216 namespace { 217 218 NSString* const kChannelKey = @"KSChannelID"; 219 NSString* const kBrandKey = @"KSBrandID"; 220 NSString* const kVersionKey = @"KSVersion"; 221 222 } // namespace 223 224 @implementation KeystoneGlue 225 226 + (id)defaultKeystoneGlue { 227 static bool sTriedCreatingDefaultKeystoneGlue = false; 228 // TODO(jrg): use base::SingletonObjC<KeystoneGlue> 229 static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked 230 231 if (!sTriedCreatingDefaultKeystoneGlue) { 232 sTriedCreatingDefaultKeystoneGlue = true; 233 234 sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; 235 [sDefaultKeystoneGlue loadParameters]; 236 if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { 237 [sDefaultKeystoneGlue release]; 238 sDefaultKeystoneGlue = nil; 239 } 240 } 241 return sDefaultKeystoneGlue; 242 } 243 244 - (id)init { 245 if ((self = [super init])) { 246 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 247 248 [center addObserver:self 249 selector:@selector(registrationComplete:) 250 name:ksr::KSRegistrationDidCompleteNotification 251 object:nil]; 252 253 [center addObserver:self 254 selector:@selector(promotionComplete:) 255 name:ksr::KSRegistrationPromotionDidCompleteNotification 256 object:nil]; 257 258 [center addObserver:self 259 selector:@selector(checkForUpdateComplete:) 260 name:ksr::KSRegistrationCheckForUpdateNotification 261 object:nil]; 262 263 [center addObserver:self 264 selector:@selector(installUpdateComplete:) 265 name:ksr::KSRegistrationStartUpdateNotification 266 object:nil]; 267 } 268 269 return self; 270 } 271 272 - (void)dealloc { 273 [productID_ release]; 274 [appPath_ release]; 275 [url_ release]; 276 [version_ release]; 277 [channel_ release]; 278 [registration_ release]; 279 [[NSNotificationCenter defaultCenter] removeObserver:self]; 280 [super dealloc]; 281 } 282 283 - (NSDictionary*)infoDictionary { 284 // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier 285 // and path, not the framework's. For auto-update, the application is 286 // what's significant here: it's used to locate the outermost part of the 287 // application for the existence checker and other operations that need to 288 // see the entire application bundle. 289 return [base::mac::OuterBundle() infoDictionary]; 290 } 291 292 - (void)loadParameters { 293 NSBundle* appBundle = base::mac::OuterBundle(); 294 NSDictionary* infoDictionary = [self infoDictionary]; 295 296 NSString* productID = [infoDictionary objectForKey:@"KSProductID"]; 297 if (productID == nil) { 298 productID = [appBundle bundleIdentifier]; 299 } 300 301 NSString* appPath = [appBundle bundlePath]; 302 NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; 303 NSString* version = [infoDictionary objectForKey:kVersionKey]; 304 305 if (!productID || !appPath || !url || !version) { 306 // If parameters required for Keystone are missing, don't use it. 307 return; 308 } 309 310 NSString* channel = [infoDictionary objectForKey:kChannelKey]; 311 // The stable channel has no tag. If updating to stable, remove the 312 // dev and beta tags since we've been "promoted". 313 if (channel == nil) 314 channel = ksr::KSRegistrationRemoveExistingTag; 315 316 productID_ = [productID retain]; 317 appPath_ = [appPath retain]; 318 url_ = [url retain]; 319 version_ = [version retain]; 320 channel_ = [channel retain]; 321 } 322 323 - (NSString*)brandFilePath { 324 DCHECK(version_ != nil) << "-loadParameters must be called first"; 325 326 if (brandFileType_ == kBrandFileTypeNotDetermined) { 327 328 NSFileManager* fm = [NSFileManager defaultManager]; 329 NSString* userBrandFile = UserBrandFilePath(); 330 NSString* systemBrandFile = SystemBrandFilePath(); 331 332 // Default to none. 333 brandFileType_ = kBrandFileTypeNone; 334 335 // Only the stable channel has a brand code. 336 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 337 338 if (channel == chrome::VersionInfo::CHANNEL_DEV || 339 channel == chrome::VersionInfo::CHANNEL_BETA) { 340 341 // If on the dev or beta channel, this installation may have replaced 342 // an older system-level installation. Check for a user brand file and 343 // nuke it if present. Don't try to remove the system brand file, there 344 // wouldn't be any permission to do so. 345 // 346 // Don't do this on the canary channel. The canary can run side-by-side 347 // with another Google Chrome installation whose brand code, if any, 348 // should remain intact. 349 350 if ([fm fileExistsAtPath:userBrandFile]) { 351 [fm removeItemAtPath:userBrandFile error:NULL]; 352 } 353 354 } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) { 355 356 // If there is a system brand file, use it. 357 if ([fm fileExistsAtPath:systemBrandFile]) { 358 // System 359 360 // Use the system file that is there. 361 brandFileType_ = kBrandFileTypeSystem; 362 363 // Clean up any old user level file. 364 if ([fm fileExistsAtPath:userBrandFile]) { 365 [fm removeItemAtPath:userBrandFile error:NULL]; 366 } 367 368 } else { 369 // User 370 371 NSDictionary* infoDictionary = [self infoDictionary]; 372 NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey]; 373 374 NSString* storedBrandID = nil; 375 if ([fm fileExistsAtPath:userBrandFile]) { 376 NSDictionary* storedBrandDict = 377 [NSDictionary dictionaryWithContentsOfFile:userBrandFile]; 378 storedBrandID = [storedBrandDict objectForKey:kBrandKey]; 379 } 380 381 if ((appBundleBrandID != nil) && 382 (![storedBrandID isEqualTo:appBundleBrandID])) { 383 // App and store don't match, update store and use it. 384 NSDictionary* storedBrandDict = 385 [NSDictionary dictionaryWithObject:appBundleBrandID 386 forKey:kBrandKey]; 387 // If Keystone hasn't been installed yet, the location the brand file 388 // is written to won't exist, so manually create the directory. 389 NSString *userBrandFileDirectory = 390 [userBrandFile stringByDeletingLastPathComponent]; 391 if (![fm fileExistsAtPath:userBrandFileDirectory]) { 392 if (![fm createDirectoryAtPath:userBrandFileDirectory 393 withIntermediateDirectories:YES 394 attributes:nil 395 error:NULL]) { 396 LOG(ERROR) << "Failed to create the directory for the brand file"; 397 } 398 } 399 if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) { 400 brandFileType_ = kBrandFileTypeUser; 401 } 402 } else if (storedBrandID) { 403 // Had stored brand, use it. 404 brandFileType_ = kBrandFileTypeUser; 405 } 406 } 407 } 408 409 } 410 411 NSString* result = nil; 412 switch (brandFileType_) { 413 case kBrandFileTypeUser: 414 result = UserBrandFilePath(); 415 break; 416 417 case kBrandFileTypeSystem: 418 result = SystemBrandFilePath(); 419 break; 420 421 case kBrandFileTypeNotDetermined: 422 NOTIMPLEMENTED(); 423 // Fall through 424 case kBrandFileTypeNone: 425 // Clear the value. 426 result = @""; 427 break; 428 429 } 430 return result; 431 } 432 433 - (BOOL)loadKeystoneRegistration { 434 if (!productID_ || !appPath_ || !url_ || !version_) 435 return NO; 436 437 // Load the KeystoneRegistration framework bundle if present. It lives 438 // inside the framework, so use base::mac::FrameworkBundle(); 439 NSString* ksrPath = 440 [[base::mac::FrameworkBundle() privateFrameworksPath] 441 stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; 442 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath]; 443 [ksrBundle load]; 444 445 // Harness the KSRegistration class. 446 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; 447 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; 448 if (!ksr) 449 return NO; 450 451 registration_ = [ksr retain]; 452 return YES; 453 } 454 455 - (NSString*)appInfoPlistPath { 456 // NSBundle ought to have a way to access this path directly, but it 457 // doesn't. 458 return [[appPath_ stringByAppendingPathComponent:@"Contents"] 459 stringByAppendingPathComponent:@"Info.plist"]; 460 } 461 462 - (NSDictionary*)keystoneParameters { 463 NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker]; 464 NSNumber* preserveTTToken = [NSNumber numberWithBool:YES]; 465 NSString* appInfoPlistPath = [self appInfoPlistPath]; 466 NSString* brandKey = kBrandKey; 467 NSString* brandPath = [self brandFilePath]; 468 469 if ([brandPath length] == 0) { 470 // Brand path and brand key must be cleared together or ksadmin seems 471 // to throw an error. 472 brandKey = @""; 473 } 474 475 // Note that channel_ is permitted to be an empty string, but it must not be 476 // nil. 477 DCHECK(channel_); 478 NSString* tagSuffix = [self tagSuffix]; 479 NSString* tagValue = [channel_ stringByAppendingString:tagSuffix]; 480 NSString* tagKey = [kChannelKey stringByAppendingString:tagSuffix]; 481 482 return [NSDictionary dictionaryWithObjectsAndKeys: 483 version_, ksr::KSRegistrationVersionKey, 484 appInfoPlistPath, ksr::KSRegistrationVersionPathKey, 485 kVersionKey, ksr::KSRegistrationVersionKeyKey, 486 xcType, ksr::KSRegistrationExistenceCheckerTypeKey, 487 appPath_, ksr::KSRegistrationExistenceCheckerStringKey, 488 url_, ksr::KSRegistrationServerURLStringKey, 489 preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey, 490 tagValue, ksr::KSRegistrationTagKey, 491 appInfoPlistPath, ksr::KSRegistrationTagPathKey, 492 tagKey, ksr::KSRegistrationTagKeyKey, 493 brandPath, ksr::KSRegistrationBrandPathKey, 494 brandKey, ksr::KSRegistrationBrandKeyKey, 495 nil]; 496 } 497 498 - (void)registerWithKeystone { 499 [self updateStatus:kAutoupdateRegistering version:nil]; 500 501 NSDictionary* parameters = [self keystoneParameters]; 502 BOOL result; 503 { 504 // TODO(shess): Allows Keystone to throw an exception when 505 // /usr/bin/python does not exist (really!). 506 // http://crbug.com/86221 and http://crbug.com/87931 507 base::mac::ScopedNSExceptionEnabler enabler; 508 result = [registration_ registerWithParameters:parameters]; 509 } 510 if (!result) { 511 [self updateStatus:kAutoupdateRegisterFailed version:nil]; 512 return; 513 } 514 515 // Upon completion, ksr::KSRegistrationDidCompleteNotification will be 516 // posted, and -registrationComplete: will be called. 517 518 // Mark an active RIGHT NOW; don't wait an hour for the first one. 519 [registration_ setActive]; 520 521 // Set up hourly activity pings. 522 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour 523 target:self 524 selector:@selector(markActive:) 525 userInfo:registration_ 526 repeats:YES]; 527 } 528 529 - (void)registrationComplete:(NSNotification*)notification { 530 NSDictionary* userInfo = [notification userInfo]; 531 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 532 if ([self isSystemTicketDoomed]) { 533 [self updateStatus:kAutoupdateNeedsPromotion version:nil]; 534 } else { 535 [self updateStatus:kAutoupdateRegistered version:nil]; 536 } 537 } else { 538 // Dump registration_? 539 [self updateStatus:kAutoupdateRegisterFailed version:nil]; 540 } 541 } 542 543 - (void)stopTimer { 544 [timer_ invalidate]; 545 } 546 547 - (void)markActive:(NSTimer*)timer { 548 KSRegistration* ksr = [timer userInfo]; 549 [ksr setActive]; 550 } 551 552 - (void)checkForUpdate { 553 DCHECK(![self asyncOperationPending]); 554 555 if (!registration_) { 556 [self updateStatus:kAutoupdateCheckFailed version:nil]; 557 return; 558 } 559 560 [self updateStatus:kAutoupdateChecking version:nil]; 561 562 [registration_ checkForUpdate]; 563 564 // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be 565 // posted, and -checkForUpdateComplete: will be called. 566 } 567 568 - (void)checkForUpdateComplete:(NSNotification*)notification { 569 NSDictionary* userInfo = [notification userInfo]; 570 571 if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey] 572 boolValue]) { 573 [self updateStatus:kAutoupdateCheckFailed version:nil]; 574 } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 575 // If an update is known to be available, go straight to 576 // -updateStatus:version:. It doesn't matter what's currently on disk. 577 NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey]; 578 [self updateStatus:kAutoupdateAvailable version:version]; 579 } else { 580 // If no updates are available, check what's on disk, because an update 581 // may have already been installed. This check happens on another thread, 582 // and -updateStatus:version: will be called on the main thread when done. 583 [self determineUpdateStatusAsync]; 584 } 585 } 586 587 - (void)installUpdate { 588 DCHECK(![self asyncOperationPending]); 589 590 if (!registration_) { 591 [self updateStatus:kAutoupdateInstallFailed version:nil]; 592 return; 593 } 594 595 [self updateStatus:kAutoupdateInstalling version:nil]; 596 597 [registration_ startUpdate]; 598 599 // Upon completion, ksr::KSRegistrationStartUpdateNotification will be 600 // posted, and -installUpdateComplete: will be called. 601 } 602 603 - (void)installUpdateComplete:(NSNotification*)notification { 604 NSDictionary* userInfo = [notification userInfo]; 605 606 // http://crbug.com/160308 and b/7517358: when using system Keystone and on 607 // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update 608 // was installed correctly, so don't check it. It should be redudnant when 609 // KSUpdateCheckSuccessfullyInstalledKey is checked. 610 if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey] 611 intValue]) { 612 [self updateStatus:kAutoupdateInstallFailed version:nil]; 613 } else { 614 updateSuccessfullyInstalled_ = YES; 615 616 // Nothing in the notification dictionary reports the version that was 617 // installed. Figure it out based on what's on disk. 618 [self determineUpdateStatusAsync]; 619 } 620 } 621 622 - (NSString*)currentlyInstalledVersion { 623 NSString* appInfoPlistPath = [self appInfoPlistPath]; 624 NSDictionary* infoPlist = 625 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; 626 return [infoPlist objectForKey:@"CFBundleShortVersionString"]; 627 } 628 629 // Runs on the main thread. 630 - (void)determineUpdateStatusAsync { 631 DCHECK([NSThread isMainThread]); 632 633 PerformBridge::PostPerform(self, @selector(determineUpdateStatus)); 634 } 635 636 // Runs on a thread managed by WorkerPool. 637 - (void)determineUpdateStatus { 638 DCHECK(![NSThread isMainThread]); 639 640 NSString* version = [self currentlyInstalledVersion]; 641 642 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) 643 withObject:version 644 waitUntilDone:NO]; 645 } 646 647 // Runs on the main thread. 648 - (void)determineUpdateStatusForVersion:(NSString*)version { 649 DCHECK([NSThread isMainThread]); 650 651 AutoupdateStatus status; 652 if (updateSuccessfullyInstalled_) { 653 // If an update was successfully installed and this object saw it happen, 654 // then don't even bother comparing versions. 655 status = kAutoupdateInstalled; 656 } else { 657 NSString* currentVersion = 658 [NSString stringWithUTF8String:chrome::kChromeVersion]; 659 if (!version) { 660 // If the version on disk could not be determined, assume that 661 // whatever's running is current. 662 version = currentVersion; 663 status = kAutoupdateCurrent; 664 } else if ([version isEqualToString:currentVersion]) { 665 status = kAutoupdateCurrent; 666 } else { 667 // If the version on disk doesn't match what's currently running, an 668 // update must have been applied in the background, without this app's 669 // direct participation. Leave updateSuccessfullyInstalled_ alone 670 // because there's no direct knowledge of what actually happened. 671 status = kAutoupdateInstalled; 672 } 673 } 674 675 [self updateStatus:status version:version]; 676 } 677 678 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { 679 NSNumber* statusNumber = [NSNumber numberWithInt:status]; 680 NSMutableDictionary* dictionary = 681 [NSMutableDictionary dictionaryWithObject:statusNumber 682 forKey:kAutoupdateStatusStatus]; 683 if (version) { 684 [dictionary setObject:version forKey:kAutoupdateStatusVersion]; 685 } 686 687 NSNotification* notification = 688 [NSNotification notificationWithName:kAutoupdateStatusNotification 689 object:self 690 userInfo:dictionary]; 691 recentNotification_.reset([notification retain]); 692 693 [[NSNotificationCenter defaultCenter] postNotification:notification]; 694 } 695 696 - (NSNotification*)recentNotification { 697 return [[recentNotification_ retain] autorelease]; 698 } 699 700 - (AutoupdateStatus)recentStatus { 701 NSDictionary* dictionary = [recentNotification_ userInfo]; 702 return static_cast<AutoupdateStatus>( 703 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); 704 } 705 706 - (BOOL)asyncOperationPending { 707 AutoupdateStatus status = [self recentStatus]; 708 return status == kAutoupdateRegistering || 709 status == kAutoupdateChecking || 710 status == kAutoupdateInstalling || 711 status == kAutoupdatePromoting; 712 } 713 714 - (BOOL)isUserTicket { 715 return [registration_ ticketType] == ksr::kKSRegistrationUserTicket; 716 } 717 718 - (BOOL)isSystemKeystone { 719 struct stat statbuf; 720 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" 721 "Contents/MacOS/ksadmin", 722 &statbuf) != 0) { 723 return NO; 724 } 725 726 if (!(statbuf.st_mode & S_IXUSR)) { 727 return NO; 728 } 729 730 return YES; 731 } 732 733 - (BOOL)isSystemTicketDoomed { 734 BOOL isSystemTicket = ![self isUserTicket]; 735 return isSystemTicket && ![self isSystemKeystone]; 736 } 737 738 - (BOOL)isOnReadOnlyFilesystem { 739 const char* appPathC = [appPath_ fileSystemRepresentation]; 740 struct statfs statfsBuf; 741 742 if (statfs(appPathC, &statfsBuf) != 0) { 743 PLOG(ERROR) << "statfs"; 744 // Be optimistic about the filesystem's writability. 745 return NO; 746 } 747 748 return (statfsBuf.f_flags & MNT_RDONLY) != 0; 749 } 750 751 - (BOOL)needsPromotion { 752 // Don't promote when on a read-only filesystem. 753 if ([self isOnReadOnlyFilesystem]) { 754 return NO; 755 } 756 757 // Promotion is required when a system ticket is present but system Keystone 758 // is not. 759 if ([self isSystemTicketDoomed]) { 760 return YES; 761 } 762 763 // If on a system ticket and system Keystone is present, promotion is not 764 // required. 765 if (![self isUserTicket]) { 766 return NO; 767 } 768 769 // Check the outermost bundle directory, the main executable path, and the 770 // framework directory. It may be enough to just look at the outermost 771 // bundle directory, but checking an interior file and directory can be 772 // helpful in case permissions are set differently only on the outermost 773 // directory. An interior file and directory are both checked because some 774 // file operations, such as Snow Leopard's Finder's copy operation when 775 // authenticating, may actually result in different ownership being applied 776 // to files and directories. 777 NSFileManager* fileManager = [NSFileManager defaultManager]; 778 NSString* executablePath = [base::mac::OuterBundle() executablePath]; 779 NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath]; 780 return ![fileManager isWritableFileAtPath:appPath_] || 781 ![fileManager isWritableFileAtPath:executablePath] || 782 ![fileManager isWritableFileAtPath:frameworkPath]; 783 } 784 785 - (BOOL)wantsPromotion { 786 if ([self needsPromotion]) { 787 return YES; 788 } 789 790 // These are the same unpromotable cases as in -needsPromotion. 791 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) { 792 return NO; 793 } 794 795 return [appPath_ hasPrefix:@"/Applications/"]; 796 } 797 798 - (void)promoteTicket { 799 if ([self asyncOperationPending] || ![self wantsPromotion]) { 800 // Because there are multiple ways of reaching promoteTicket that might 801 // not lock each other out, it may be possible to arrive here while an 802 // asynchronous operation is pending, or even after promotion has already 803 // occurred. Just quietly return without doing anything. 804 return; 805 } 806 807 NSString* prompt = l10n_util::GetNSStringFWithFixup( 808 IDS_PROMOTE_AUTHENTICATION_PROMPT, 809 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); 810 base::mac::ScopedAuthorizationRef authorization( 811 base::mac::AuthorizationCreateToRunAsRoot( 812 base::mac::NSToCFCast(prompt))); 813 if (!authorization.get()) { 814 return; 815 } 816 817 [self promoteTicketWithAuthorization:authorization.release() synchronous:NO]; 818 } 819 820 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg 821 synchronous:(BOOL)synchronous { 822 base::mac::ScopedAuthorizationRef authorization(authorization_arg); 823 authorization_arg = NULL; 824 825 if ([self asyncOperationPending]) { 826 // Starting a synchronous operation while an asynchronous one is pending 827 // could be trouble. 828 return; 829 } 830 if (!synchronous && ![self wantsPromotion]) { 831 // If operating synchronously, the call came from the installer, which 832 // means that a system ticket is required. Otherwise, only allow 833 // promotion if it's wanted. 834 return; 835 } 836 837 synchronousPromotion_ = synchronous; 838 839 [self updateStatus:kAutoupdatePromoting version:nil]; 840 841 // TODO(mark): Remove when able! 842 // 843 // keystone_promote_preflight will copy the current brand information out to 844 // the system level so all users can share the data as part of the ticket 845 // promotion. 846 // 847 // It will also ensure that the Keystone system ticket store is in a usable 848 // state for all users on the system. Ideally, Keystone's installer or 849 // another part of Keystone would handle this. The underlying problem is 850 // http://b/2285921, and it causes http://b/2289908, which this workaround 851 // addresses. 852 // 853 // This is run synchronously, which isn't optimal, but 854 // -[KSRegistration promoteWithParameters:authorization:] is currently 855 // synchronous too, and this operation needs to happen before that one. 856 // 857 // TODO(mark): Make asynchronous. That only makes sense if the promotion 858 // operation itself is asynchronous too. http://b/2290009. Hopefully, 859 // the Keystone promotion code will just be changed to do what preflight 860 // now does, and then the preflight script can be removed instead. 861 // However, preflight operation (and promotion) should only be asynchronous 862 // if the synchronous parameter is NO. 863 NSString* preflightPath = 864 [base::mac::FrameworkBundle() 865 pathForResource:@"keystone_promote_preflight" 866 ofType:@"sh"]; 867 const char* preflightPathC = [preflightPath fileSystemRepresentation]; 868 const char* userBrandFile = NULL; 869 const char* systemBrandFile = NULL; 870 if (brandFileType_ == kBrandFileTypeUser) { 871 // Running with user level brand file, promote to the system level. 872 userBrandFile = [UserBrandFilePath() fileSystemRepresentation]; 873 systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation]; 874 } 875 const char* arguments[] = {userBrandFile, systemBrandFile, NULL}; 876 877 int exit_status; 878 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( 879 authorization, 880 preflightPathC, 881 kAuthorizationFlagDefaults, 882 arguments, 883 NULL, // pipe 884 &exit_status); 885 if (status != errAuthorizationSuccess) { 886 OSSTATUS_LOG(ERROR, status) 887 << "AuthorizationExecuteWithPrivileges preflight"; 888 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 889 return; 890 } 891 if (exit_status != 0) { 892 LOG(ERROR) << "keystone_promote_preflight status " << exit_status; 893 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 894 return; 895 } 896 897 // Hang on to the AuthorizationRef so that it can be used once promotion is 898 // complete. Do this before asking Keystone to promote the ticket, because 899 // -promotionComplete: may be called from inside the Keystone promotion 900 // call. 901 authorization_.swap(authorization); 902 903 NSDictionary* parameters = [self keystoneParameters]; 904 905 // If the brand file is user level, update parameters to point to the new 906 // system level file during promotion. 907 if (brandFileType_ == kBrandFileTypeUser) { 908 NSMutableDictionary* temp_parameters = 909 [[parameters mutableCopy] autorelease]; 910 [temp_parameters setObject:SystemBrandFilePath() 911 forKey:ksr::KSRegistrationBrandPathKey]; 912 parameters = temp_parameters; 913 } 914 915 if (![registration_ promoteWithParameters:parameters 916 authorization:authorization_]) { 917 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 918 authorization_.reset(); 919 return; 920 } 921 922 // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will 923 // be posted, and -promotionComplete: will be called. 924 925 // If synchronous, see to it that this happens immediately. Give it a 926 // 10-second deadline. 927 if (synchronous) { 928 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); 929 } 930 } 931 932 - (void)promotionComplete:(NSNotification*)notification { 933 NSDictionary* userInfo = [notification userInfo]; 934 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) { 935 if (synchronousPromotion_) { 936 // Short-circuit: if performing a synchronous promotion, the promotion 937 // came from the installer, which already set the permissions properly. 938 // Rather than run a duplicate permission-changing operation, jump 939 // straight to "done." 940 [self changePermissionsForPromotionComplete]; 941 } else { 942 [self changePermissionsForPromotionAsync]; 943 } 944 } else { 945 authorization_.reset(); 946 [self updateStatus:kAutoupdatePromoteFailed version:nil]; 947 } 948 949 if (synchronousPromotion_) { 950 // The run loop doesn't need to wait for this any longer. 951 CFRunLoopRef runLoop = CFRunLoopGetCurrent(); 952 CFRunLoopStop(runLoop); 953 CFRunLoopWakeUp(runLoop); 954 } 955 } 956 957 - (void)changePermissionsForPromotionAsync { 958 // NSBundle is not documented as being thread-safe. Do NSBundle operations 959 // on the main thread before jumping over to a WorkerPool-managed 960 // thread to run the tool. 961 DCHECK([NSThread isMainThread]); 962 963 SEL selector = @selector(changePermissionsForPromotionWithTool:); 964 NSString* toolPath = 965 [base::mac::FrameworkBundle() 966 pathForResource:@"keystone_promote_postflight" 967 ofType:@"sh"]; 968 969 PerformBridge::PostPerform(self, selector, toolPath); 970 } 971 972 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath { 973 const char* toolPathC = [toolPath fileSystemRepresentation]; 974 975 const char* appPathC = [appPath_ fileSystemRepresentation]; 976 const char* arguments[] = {appPathC, NULL}; 977 978 int exit_status; 979 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( 980 authorization_, 981 toolPathC, 982 kAuthorizationFlagDefaults, 983 arguments, 984 NULL, // pipe 985 &exit_status); 986 if (status != errAuthorizationSuccess) { 987 OSSTATUS_LOG(ERROR, status) 988 << "AuthorizationExecuteWithPrivileges postflight"; 989 } else if (exit_status != 0) { 990 LOG(ERROR) << "keystone_promote_postflight status " << exit_status; 991 } 992 993 SEL selector = @selector(changePermissionsForPromotionComplete); 994 [self performSelectorOnMainThread:selector 995 withObject:nil 996 waitUntilDone:NO]; 997 } 998 999 - (void)changePermissionsForPromotionComplete { 1000 authorization_.reset(); 1001 1002 [self updateStatus:kAutoupdatePromoted version:nil]; 1003 } 1004 1005 - (void)setAppPath:(NSString*)appPath { 1006 if (appPath != appPath_) { 1007 [appPath_ release]; 1008 appPath_ = [appPath copy]; 1009 } 1010 } 1011 1012 - (BOOL)has32BitOnlyCPU { 1013 #if defined(ARCH_CPU_64_BITS) 1014 return NO; 1015 #else // ARCH_CPU_64_BITS 1016 int value; 1017 size_t valueSize = sizeof(value); 1018 if (sysctlbyname("hw.cpu64bit_capable", &value, &valueSize, NULL, 0) != 0) { 1019 return YES; 1020 } 1021 return value == 0; 1022 #endif // ARCH_CPU_64_BITS 1023 } 1024 1025 - (BOOL)wantsFullInstaller { 1026 // It's difficult to check the tag prior to Keystone registration, and 1027 // performing registration replaces the tag. keystone_install.sh 1028 // communicates a need for a full installer with Chrome in this file, 1029 // .want_full_installer. 1030 NSString* wantFullInstallerPath = 1031 [appPath_ stringByAppendingPathComponent:@".want_full_installer"]; 1032 NSString* wantFullInstallerContents = 1033 [NSString stringWithContentsOfFile:wantFullInstallerPath 1034 encoding:NSUTF8StringEncoding 1035 error:NULL]; 1036 if (!wantFullInstallerContents) { 1037 return NO; 1038 } 1039 1040 NSString* wantFullInstallerVersion = 1041 [wantFullInstallerContents stringByTrimmingCharactersInSet: 1042 [NSCharacterSet newlineCharacterSet]]; 1043 return [wantFullInstallerVersion isEqualToString:version_]; 1044 } 1045 1046 - (NSString*)tagSuffix { 1047 // Tag suffix components are not entirely arbitrary: all possible tag keys 1048 // must be present in the application's Info.plist, there must be 1049 // server-side agreement on the processing and meaning of tag suffix 1050 // components, and other code that manipulates tag values (such as the 1051 // Keystone update installation script) must be tag suffix-aware. To reduce 1052 // the number of tag suffix combinations that need to be listed in 1053 // Info.plist, tag suffix components should only be appended to the tag 1054 // suffix in ASCII sort order. 1055 NSString* tagSuffix = @""; 1056 if ([self has32BitOnlyCPU]) { 1057 tagSuffix = [tagSuffix stringByAppendingString:@"-32bit"]; 1058 } 1059 if ([self wantsFullInstaller]) { 1060 tagSuffix = [tagSuffix stringByAppendingString:@"-full"]; 1061 } 1062 return tagSuffix; 1063 } 1064 1065 @end // @implementation KeystoneGlue 1066 1067 namespace { 1068 1069 std::string BrandCodeInternal() { 1070 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue]; 1071 NSString* brand_path = [keystone_glue brandFilePath]; 1072 1073 if (![brand_path length]) 1074 return std::string(); 1075 1076 NSDictionary* dict = 1077 [NSDictionary dictionaryWithContentsOfFile:brand_path]; 1078 NSString* brand_code = 1079 base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]); 1080 if (brand_code) 1081 return [brand_code UTF8String]; 1082 1083 return std::string(); 1084 } 1085 1086 } // namespace 1087 1088 namespace keystone_glue { 1089 1090 std::string BrandCode() { 1091 // |s_brand_code| is leaked. 1092 static std::string* s_brand_code = new std::string(BrandCodeInternal()); 1093 return *s_brand_code; 1094 } 1095 1096 bool KeystoneEnabled() { 1097 return [KeystoneGlue defaultKeystoneGlue] != nil; 1098 } 1099 1100 base::string16 CurrentlyInstalledVersion() { 1101 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; 1102 NSString* version = [keystoneGlue currentlyInstalledVersion]; 1103 return base::SysNSStringToUTF16(version); 1104 } 1105 1106 } // namespace keystone_glue 1107