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