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