mirror of
https://github.com/sparkle-project/Sparkle.git
synced 2025-11-01 15:34:38 +00:00
834 lines
41 KiB
Objective-C
834 lines
41 KiB
Objective-C
//
|
|
// AppInstaller.m
|
|
// Sparkle
|
|
//
|
|
// Created by Mayur Pawashe on 3/7/16.
|
|
// Copyright © 2016 Sparkle Project. All rights reserved.
|
|
//
|
|
|
|
#import "AppInstaller.h"
|
|
#import "SUInstaller.h"
|
|
#import "SUUpdateValidator.h"
|
|
#import "SULog.h"
|
|
#import "SULog+NSError.h"
|
|
#import "SUHost.h"
|
|
#import "SULocalizations.h"
|
|
#import "SUStandardVersionComparator.h"
|
|
#import "SPUMessageTypes.h"
|
|
#import "SPUSecureCoding.h"
|
|
#import "SPUInstallationInputData.h"
|
|
#import "SUUnarchiver.h"
|
|
#import "SUFileManager.h"
|
|
#import "SPUInstallationInfo.h"
|
|
#import "SUAppcastItem.h"
|
|
#import "SUErrors.h"
|
|
#import "SUInstallerCommunicationProtocol.h"
|
|
#import "AgentConnection.h"
|
|
#import "SPUInstallerAgentProtocol.h"
|
|
#import "SPUInstallationType.h"
|
|
#import "SPULocalCacheDirectory.h"
|
|
#import "SPUVerifierInformation.h"
|
|
#import "SUConstants.h"
|
|
#import "SUCodeSigningVerifier.h"
|
|
#import <os/lock.h>
|
|
|
|
|
|
#include "AppKitPrevention.h"
|
|
|
|
#define FIRST_UPDATER_MESSAGE_TIMEOUT 18ull
|
|
#define RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT 8ull
|
|
|
|
/**
|
|
* Show display progress UI after a delay from starting the final part of the installation.
|
|
* This should be long enough so that we don't show progress for very fast installations, but
|
|
* short enough so that we don't leave the user wondering why nothing is happening.
|
|
*/
|
|
static const NSTimeInterval SUDisplayProgressTimeDelay = 0.7;
|
|
|
|
@interface AppInstaller () <NSXPCListenerDelegate, SUInstallerCommunicationProtocol, AgentConnectionDelegate>
|
|
@end
|
|
|
|
@implementation AppInstaller
|
|
{
|
|
NSXPCListener* _xpcListener;
|
|
// Must be synchronized with _newConnectionLock
|
|
// Set from new connection handler, and also set/read from main thread
|
|
NSXPCConnection *_activeConnection;
|
|
|
|
id<SUInstallerCommunicationProtocol> _communicator;
|
|
AgentConnection *_agentConnection;
|
|
|
|
SUUpdateValidator *_updateValidator;
|
|
|
|
NSString *_hostBundleIdentifier;
|
|
NSString *_homeDirectory;
|
|
NSString *_userName;
|
|
SUHost *_host;
|
|
NSString *_updateDirectoryPath;
|
|
NSString *_extractionDirectory;
|
|
NSString *_downloadName;
|
|
NSString *_decryptionPassword;
|
|
SUSignatures *_signatures;
|
|
NSString *_relaunchPath;
|
|
NSString *_installationType;
|
|
SPUVerifierInformation *_verifierInformation;
|
|
|
|
id<SUInstallerProtocol> _installer;
|
|
|
|
dispatch_queue_t _installerQueue;
|
|
|
|
os_unfair_lock _newConnectionLock;
|
|
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
// Must be synchronized with _newConnectionLock
|
|
// Set from new connection handler, read from main thread
|
|
BOOL _connectionCodeSigningValidationSkipped;
|
|
#endif
|
|
|
|
BOOL _shouldRelaunch;
|
|
BOOL _shouldShowUI;
|
|
|
|
BOOL _receivedUpdaterPong;
|
|
|
|
BOOL _willCompleteInstallation;
|
|
BOOL _receivedInstallationData;
|
|
BOOL _finishedValidation;
|
|
BOOL _agentInitiatedConnection;
|
|
|
|
// Setting _performedStage1Installation on main thread must be synchronzied with reading it from new connection handler
|
|
BOOL _performedStage1Installation;
|
|
|
|
BOOL _performedStage2Installation;
|
|
BOOL _performedStage3Installation;
|
|
|
|
BOOL _targetTerminated;
|
|
}
|
|
|
|
- (instancetype)initWithHostBundleIdentifier:(NSString *)hostBundleIdentifier homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName
|
|
{
|
|
if (!(self = [super init])) {
|
|
return nil;
|
|
}
|
|
|
|
_newConnectionLock = OS_UNFAIR_LOCK_INIT;
|
|
|
|
_hostBundleIdentifier = [hostBundleIdentifier copy];
|
|
|
|
_homeDirectory = [homeDirectory copy];
|
|
_userName = [userName copy];
|
|
|
|
_xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier)];
|
|
_xpcListener.delegate = self;
|
|
|
|
_agentConnection = [[AgentConnection alloc] initWithHostBundleIdentifier:hostBundleIdentifier delegate:self];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
|
|
{
|
|
os_unfair_lock_lock(&_newConnectionLock);
|
|
{
|
|
if (_activeConnection != nil) {
|
|
os_unfair_lock_unlock(&_newConnectionLock);
|
|
|
|
SULog(SULogLevelError, @"Error: Rejecting multiple XPC connections for installer...");
|
|
|
|
[newConnection invalidate];
|
|
return NO;
|
|
}
|
|
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
BOOL connectionCodeSigningValidationSkipped = NO;
|
|
#endif
|
|
|
|
// It's safe to allow any connections once stage 1 installation is complete
|
|
// This is to allow general updaters to resume the installation.
|
|
if (!_performedStage1Installation) {
|
|
BOOL passesValidation;
|
|
NSError *validationError = nil;
|
|
SUValidateConnectionStatus status = [SUCodeSigningVerifier validateConnection:newConnection error:&validationError];
|
|
switch (status) {
|
|
case SUValidateConnectionStatusSetCodeSigningRequirementSuccess:
|
|
passesValidation = YES;
|
|
break;
|
|
case SUValidateConnectionStatusSetNoRequirementSuccess:
|
|
passesValidation = YES;
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
connectionCodeSigningValidationSkipped = YES;
|
|
#endif
|
|
break;
|
|
case SUValidateConnectionStatusAPIFailure:
|
|
case SUValidateConnectionStatusCodeSigningRequirementFailure:
|
|
case SUValidateConectionNoSupportedValidationMethodFailure:
|
|
passesValidation = NO;
|
|
break;
|
|
}
|
|
|
|
if (!passesValidation) {
|
|
os_unfair_lock_unlock(&_newConnectionLock);
|
|
|
|
SULog(SULogLevelError, @"Error: Rejecting new connection for installer due to failing validation of XPC connection with status %lu and error: %@", status, validationError.localizedDescription);
|
|
[newConnection invalidate];
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
_connectionCodeSigningValidationSkipped = connectionCodeSigningValidationSkipped;
|
|
#endif
|
|
_activeConnection = newConnection;
|
|
}
|
|
os_unfair_lock_unlock(&_newConnectionLock);
|
|
|
|
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
|
|
newConnection.exportedObject = self;
|
|
|
|
newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
newConnection.interruptionHandler = ^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
if (strongSelf != nil) {
|
|
os_unfair_lock_lock(&strongSelf->_newConnectionLock);
|
|
NSXPCConnection *activeConnection = strongSelf->_activeConnection;
|
|
os_unfair_lock_unlock(&strongSelf->_newConnectionLock);
|
|
|
|
[activeConnection invalidate];
|
|
}
|
|
});
|
|
};
|
|
|
|
newConnection.invalidationHandler = ^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
if (strongSelf != nil) {
|
|
if (strongSelf->_activeConnection != nil && !strongSelf->_willCompleteInstallation) {
|
|
[strongSelf cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Invalidation on remote port being called, and installation is not close enough to completion!" }]];
|
|
}
|
|
strongSelf->_communicator = nil;
|
|
|
|
os_unfair_lock_lock(&strongSelf->_newConnectionLock);
|
|
strongSelf->_activeConnection = nil;
|
|
os_unfair_lock_unlock(&strongSelf->_newConnectionLock);
|
|
}
|
|
});
|
|
};
|
|
|
|
// _communicator is used only on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self->_communicator = newConnection.remoteObjectProxy;
|
|
[newConnection resume];
|
|
});
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)start
|
|
{
|
|
[_xpcListener resume];
|
|
[_agentConnection startListener];
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(FIRST_UPDATER_MESSAGE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
if (!self->_receivedInstallationData) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: installation data was never received" }]];
|
|
}
|
|
|
|
if (!self->_agentConnection.connected) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: agent connection was never initiated" }]];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)extractAndInstallUpdate SPU_OBJC_DIRECT
|
|
{
|
|
[_communicator handleMessageWithIdentifier:SPUExtractionStarted data:[NSData data]];
|
|
|
|
NSString *archivePath = [_updateDirectoryPath stringByAppendingPathComponent:_downloadName];
|
|
|
|
id<SUUnarchiverProtocol> unarchiver = [SUUnarchiver unarchiverForPath:archivePath extractionDirectory:_extractionDirectory updatingHostBundlePath:_host.bundlePath decryptionPassword:_decryptionPassword expectingInstallationType:_installationType];
|
|
|
|
NSError *prevalidationError = nil;
|
|
BOOL success = NO;
|
|
if (!unarchiver) {
|
|
prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }];
|
|
|
|
success = NO;
|
|
} else {
|
|
NSError *fileAttributesError = nil;
|
|
NSDictionary<NSFileAttributeKey, id> *archiveFileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:archivePath error:&fileAttributesError];
|
|
if (archiveFileAttributes == nil) {
|
|
SULog(SULogLevelError, @"Failed to retrieve file attributes from archive: %@.", fileAttributesError);
|
|
} else {
|
|
_verifierInformation.actualContentLength = (uint64_t)(archiveFileAttributes.fileSize);
|
|
}
|
|
|
|
_updateValidator = [[SUUpdateValidator alloc] initWithDownloadPath:archivePath signatures:_signatures host:_host verifierInformation:_verifierInformation];
|
|
|
|
// More uncommon archives types (.aar, .yaa) need SUVerifyUpdateBeforeExtraction
|
|
BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey];
|
|
if (!verifyBeforeExtraction && unarchiver.needsVerifyBeforeExtractionKey) {
|
|
prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Extracting %@ archives require setting %@ to YES in the old app. Please visit https://sparkle-project.org/documentation/customization/ for more information.", archivePath.pathExtension, SUVerifyUpdateBeforeExtractionKey] }];
|
|
|
|
success = NO;
|
|
} else {
|
|
// Delta, package updates, and apps with SUVerifyUpdateBeforeExtraction will require validation before extraction
|
|
// Otherwise normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys after extraction
|
|
BOOL archiveTypeMustValidateBeforeExtraction = [[unarchiver class] mustValidateBeforeExtraction];
|
|
BOOL needsPrevalidation = verifyBeforeExtraction || archiveTypeMustValidateBeforeExtraction || ![_installationType isEqualToString:SPUInstallationTypeApplication];
|
|
|
|
if (needsPrevalidation) {
|
|
// EdDSA signing is required, so host must have public keys
|
|
if (![_updateValidator validateHostHasPublicKeys:&prevalidationError]) {
|
|
success = NO;
|
|
} else {
|
|
// Falling back on code signing for prevalidation requires SUVerifyUpdateBeforeExtraction
|
|
// and that update is a regular app update, and not a delta update
|
|
BOOL fallbackOnCodeSigning = (verifyBeforeExtraction && !archiveTypeMustValidateBeforeExtraction && [_installationType isEqualToString:SPUInstallationTypeApplication]);
|
|
|
|
success = [_updateValidator validateDownloadPathWithFallbackOnCodeSigning:fallbackOnCodeSigning error:&prevalidationError];
|
|
}
|
|
} else {
|
|
success = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!success) {
|
|
[self unarchiverDidFailWithError:prevalidationError];
|
|
} else {
|
|
[unarchiver
|
|
unarchiveWithCompletionBlock:^(NSError * _Nullable error) {
|
|
if (error != nil) {
|
|
[self unarchiverDidFailWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to unarchive %@", archivePath], NSUnderlyingErrorKey: (NSError * _Nonnull)error }]];
|
|
} else {
|
|
[self->_communicator handleMessageWithIdentifier:SPUValidationStarted data:[NSData data]];
|
|
|
|
NSError *validationError = nil;
|
|
BOOL validationSuccess = [self->_updateValidator validateWithUpdateDirectory:self->_extractionDirectory error:&validationError];
|
|
|
|
if (!validationSuccess) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Update validation was a failure", NSUnderlyingErrorKey: validationError }]];
|
|
} else {
|
|
[self->_communicator handleMessageWithIdentifier:SPUInstallationStartedStage1 data:[NSData data]];
|
|
|
|
self->_finishedValidation = YES;
|
|
if (self->_agentInitiatedConnection) {
|
|
[self retrieveProcessIdentifierAndStartInstallation];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
progressBlock:^(double progress) {
|
|
if (sizeof(progress) == sizeof(uint64_t)) {
|
|
uint64_t progressValue = CFSwapInt64HostToLittle(*(uint64_t *)&progress);
|
|
NSData *data = [NSData dataWithBytes:&progressValue length:sizeof(progressValue)];
|
|
|
|
[self->_communicator handleMessageWithIdentifier:SPUExtractedArchiveWithProgress data:data];
|
|
}
|
|
} waitForCleanup:NO];
|
|
}
|
|
}
|
|
|
|
- (void)clearUpdateDirectory SPU_OBJC_DIRECT
|
|
{
|
|
if (_updateDirectoryPath != nil) {
|
|
NSError *theError = nil;
|
|
if (![[[SUFileManager alloc] init] removeItemAtURL:[NSURL fileURLWithPath:_updateDirectoryPath] error:&theError]) {
|
|
SULog(SULogLevelError, @"Couldn't remove update folder: %@.", theError);
|
|
}
|
|
_updateDirectoryPath = nil;
|
|
}
|
|
}
|
|
|
|
- (void)unarchiverDidFailWithError:(NSError *)error SPU_OBJC_DIRECT
|
|
{
|
|
SULog(SULogLevelError, @"Failed to unarchive file");
|
|
SULogError(error);
|
|
|
|
// No longer need update validator until next possible extraction (eg: if initial delta update fails)
|
|
_updateValidator = nil;
|
|
|
|
// Client could try update again with different inputs
|
|
// Eg: one common case is if a delta update fails, client may want to fall back to regular update
|
|
// We really only need to set updateDirectoryPath to nil since that's the field we check if we've received installation data,
|
|
// but may as well set other fields to nil too
|
|
[self clearUpdateDirectory];
|
|
_downloadName = nil;
|
|
_extractionDirectory = nil;
|
|
_decryptionPassword = nil;
|
|
_signatures = nil;
|
|
_relaunchPath = nil;
|
|
_host = nil;
|
|
|
|
NSData *archivedError = SPUArchiveRootObjectSecurely(error);
|
|
[_communicator handleMessageWithIdentifier:SPUArchiveExtractionFailed data:archivedError != nil ? archivedError : [NSData data]];
|
|
}
|
|
|
|
- (void)agentConnectionDidInitiate
|
|
{
|
|
_agentInitiatedConnection = YES;
|
|
if (_finishedValidation) {
|
|
[self retrieveProcessIdentifierAndStartInstallation];
|
|
}
|
|
}
|
|
|
|
- (void)agentConnectionDidInvalidate
|
|
{
|
|
if (!_finishedValidation || !_agentInitiatedConnection || !_targetTerminated) {
|
|
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Error: Agent connection invalidated before installation began" }];
|
|
|
|
NSError *agentError = _agentConnection.invalidationError;
|
|
if (agentError != nil) {
|
|
userInfo[NSUnderlyingErrorKey] = agentError;
|
|
}
|
|
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:userInfo]];
|
|
}
|
|
}
|
|
|
|
- (void)retrieveProcessIdentifierAndStartInstallation SPU_OBJC_DIRECT
|
|
{
|
|
// We use the relaunch path for the bundle to listen for termination instead of the host path
|
|
// For a plug-in this makes a big difference; we want to wait until the app hosting the plug-in terminates
|
|
// Otherwise for an app, the relaunch path and host path should be identical
|
|
|
|
__block BOOL receivedResponse = NO;
|
|
[_agentConnection.agent registerApplicationBundlePath:_relaunchPath reply:^(BOOL targetTerminated) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
receivedResponse = YES;
|
|
|
|
if (!targetTerminated) {
|
|
[self->_agentConnection.agent listenForTerminationWithCompletion:^{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self->_targetTerminated = YES;
|
|
|
|
if (self->_performedStage1Installation) {
|
|
[self finishInstallationAfterHostTermination];
|
|
}
|
|
});
|
|
}];
|
|
} else {
|
|
self->_targetTerminated = YES;
|
|
}
|
|
|
|
[self startInstallation];
|
|
});
|
|
}];
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
if (!receivedResponse) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout error: failed to retrieve process identifier from agent" }]];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data
|
|
{
|
|
if (identifier == SPUInstallationData && _updateDirectoryPath == nil) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Mark that we have received the installation data
|
|
// Do not rely on eg: self->_updateDirectoryPath != nil because we may set it to nil again if an early stage fails (i.e, archive extraction)
|
|
self->_receivedInstallationData = YES;
|
|
|
|
SPUInstallationInputData *installationData = (data != nil) ? (SPUInstallationInputData *)SPUUnarchiveRootObjectSecurely(data, [SPUInstallationInputData class]) : nil;
|
|
if (installationData == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to unarchive input installation data" }]];
|
|
return;
|
|
}
|
|
|
|
NSString *installationType = installationData.installationType;
|
|
if (!SPUValidInstallationType(installationType)) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received invalid installation type: %@", installationType] }]];
|
|
return;
|
|
}
|
|
|
|
NSBundle *hostBundle = [NSBundle bundleWithPath:installationData.hostBundlePath];
|
|
|
|
NSString *bundleIdentifier = hostBundle.bundleIdentifier;
|
|
if (bundleIdentifier == nil || ![bundleIdentifier isEqualToString:self->_hostBundleIdentifier]) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to match host bundle identifiers %@ and %@", self->_hostBundleIdentifier, bundleIdentifier] }]];
|
|
return;
|
|
}
|
|
|
|
// This will be important later
|
|
if (installationData.relaunchPath == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to obtain relaunch path from installation data" }]];
|
|
return;
|
|
}
|
|
|
|
// This installation path is specific to sparkle and the bundle identifier
|
|
NSString *rootCacheInstallationPath = [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"Installation"];
|
|
|
|
[SPULocalCacheDirectory removeOldItemsInDirectory:rootCacheInstallationPath];
|
|
|
|
NSString *cacheInstallationPath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootCacheInstallationPath];
|
|
if (cacheInstallationPath == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation cache directory in %@", rootCacheInstallationPath] }]];
|
|
return;
|
|
}
|
|
|
|
// Resolve the bookmark data for the downloaded update
|
|
// See "Share file access between processes with URL bookmarks" in https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox
|
|
BOOL isStale = NO;
|
|
NSError *bookmarkError = nil;
|
|
NSURL *downloadURL = [NSURL URLByResolvingBookmarkData:installationData.updateURLBookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&isStale error:&bookmarkError];
|
|
if (downloadURL == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resolve bookmark data from downloaded update", NSUnderlyingErrorKey: bookmarkError }]];
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate the download URL before moving it
|
|
{
|
|
NSArray<NSString *> *downloadURLPathComponents = downloadURL.URLByResolvingSymlinksInPath.pathComponents;
|
|
if (downloadURLPathComponents == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve path components from download URL" }]];
|
|
|
|
return;
|
|
}
|
|
|
|
if ([downloadURLPathComponents containsObject:@".."]) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components contains '..' which is unsafe" }]];
|
|
|
|
return;
|
|
}
|
|
|
|
if (![downloadURLPathComponents containsObject:@SPARKLE_BUNDLE_IDENTIFIER] || ![downloadURLPathComponents containsObject:@"PersistentDownloads"]) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components does not contain PersistentDownloads or "@SPARKLE_BUNDLE_IDENTIFIER }]];
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!isStale) {
|
|
SULog(SULogLevelError, @"Error: bookmark data for update download is stale.. but still continuing.");
|
|
}
|
|
|
|
NSString *originalDownloadName = downloadURL.lastPathComponent;
|
|
if (originalDownloadName == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve download name from download URL" }]];
|
|
|
|
return;
|
|
}
|
|
|
|
// Randomize the download name if possible
|
|
// This adds better security if there are any vulnerabilities in extracting/executing archives
|
|
// which allow writing in unexpected locations. For zip/tar/dmg archives we may also extract them before
|
|
// performing signing validation (due to key rotation).
|
|
NSString *downloadName;
|
|
NSString *randomizedUUIDString = [[NSUUID UUID] UUIDString];
|
|
if (randomizedUUIDString != nil) {
|
|
// Find the real path extension of the download name
|
|
// We cannot use -[NSString pathExtension] because it may not give us the full path extension
|
|
// E.g. for "foo.tar.xz" we need "tar.xz", not "xz"
|
|
NSString *downloadPathExtension;
|
|
NSRange pathExtensionDelimiterRange = [originalDownloadName rangeOfString:@"."];
|
|
if (pathExtensionDelimiterRange.location == NSNotFound) {
|
|
downloadPathExtension = @"";
|
|
} else {
|
|
downloadPathExtension = [originalDownloadName substringFromIndex:pathExtensionDelimiterRange.location + 1];
|
|
}
|
|
|
|
NSString *randomizedDownloadName = [randomizedUUIDString stringByAppendingPathExtension:downloadPathExtension];
|
|
if (randomizedDownloadName != nil) {
|
|
downloadName = randomizedDownloadName;
|
|
} else {
|
|
downloadName = originalDownloadName;
|
|
}
|
|
} else {
|
|
downloadName = originalDownloadName;
|
|
}
|
|
|
|
// Move the download archive to somewhere where probably only we will be touching it
|
|
// This prevents eg: if a bug exists in the updater that removes files we are trying to install
|
|
// When this tool is ran as root, we are moving it into a directory that only root will have access to
|
|
|
|
NSURL *downloadDestinationURL = [[NSURL fileURLWithPath:cacheInstallationPath] URLByAppendingPathComponent:downloadName];
|
|
|
|
NSError *moveError = nil;
|
|
if (![[[SUFileManager alloc] init] moveItemAtURL:downloadURL toURL:downloadDestinationURL error:&moveError]) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to move download archive to new location", NSUnderlyingErrorKey: moveError }]];
|
|
return;
|
|
}
|
|
|
|
// Make sure the downloaded archive we moved over is a regular file and not a symbolic link placed by an attacker
|
|
NSError *attributesError = nil;
|
|
NSString *downloadDestinationPath = downloadDestinationURL.path;
|
|
if (downloadDestinationPath == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive path from %@", downloadDestinationURL] }]];
|
|
|
|
return;
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *archiveAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:downloadDestinationPath error:&attributesError];
|
|
|
|
if (archiveAttributes == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive attributes from %@", downloadDestinationPath] }]];
|
|
|
|
return;
|
|
}
|
|
|
|
if (![(NSString *)archiveAttributes[NSFileType] isEqualToString:NSFileTypeRegular]) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received bad archive file type: %@", archiveAttributes[NSFileType]] }]];
|
|
return;
|
|
}
|
|
|
|
NSString *extractionDirectory = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:cacheInstallationPath];
|
|
if (extractionDirectory == nil) {
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation extraction directory in %@", cacheInstallationPath] }]];
|
|
|
|
return;
|
|
}
|
|
|
|
// Carry these properties separately rather than using the SUInstallationInputData object
|
|
// Some of our properties may slightly differ than our input and we don't want to make the mistake of using one of those
|
|
self->_installationType = installationType;
|
|
self->_relaunchPath = installationData.relaunchPath;
|
|
self->_downloadName = downloadName;
|
|
self->_signatures = installationData.signatures;
|
|
self->_updateDirectoryPath = cacheInstallationPath;
|
|
self->_extractionDirectory = extractionDirectory;
|
|
self->_decryptionPassword = installationData.decryptionPassword;
|
|
self->_host = [[SUHost alloc] initWithBundle:hostBundle];
|
|
self->_verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:installationData.expectedVersion expectedContentLength:installationData.expectedContentLength];
|
|
|
|
[self extractAndInstallUpdate];
|
|
});
|
|
} else if (identifier == SPUSentUpdateAppcastItemData) {
|
|
SUAppcastItem *updateItem = (data != nil) ? (SUAppcastItem *)SPUUnarchiveRootObjectSecurely(data, [SUAppcastItem class]) : nil;
|
|
if (updateItem != nil) {
|
|
SPUInstallationInfo *installationInfo = [[SPUInstallationInfo alloc] initWithAppcastItem:updateItem canSilentlyInstall:[_installer canInstallSilently]];
|
|
|
|
NSData *archivedData = SPUArchiveRootObjectSecurely(installationInfo);
|
|
if (archivedData != nil) {
|
|
[_agentConnection.agent registerInstallationInfoData:archivedData];
|
|
}
|
|
}
|
|
} else if (identifier == SPUResumeInstallationToStage2 && data.length == sizeof(uint8_t) * 2) {
|
|
// Because anyone can ask us to resume the installation, it may be wise to think about backwards compatibility here if IPC changes
|
|
uint8_t relaunch = *((const uint8_t *)data.bytes);
|
|
uint8_t showsUI = *((const uint8_t *)data.bytes + 1);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// This flag has an impact on interactive type installations and showing UI progress during non-interactive installations
|
|
self->_shouldShowUI = (BOOL)showsUI;
|
|
// Don't test if the application was alive initially, leave that to the progress agent if we decide to relaunch
|
|
self->_shouldRelaunch = (BOOL)relaunch;
|
|
|
|
if (self->_performedStage1Installation) {
|
|
// Resume the installation if we aren't done with stage 2 yet, and remind the client we are prepared to relaunch
|
|
dispatch_async(self->_installerQueue, ^{
|
|
if (!self->_performedStage2Installation) {
|
|
[self performStage2Installation];
|
|
} else if (!self->_performedStage3Installation) {
|
|
// If we already performed the 2nd stage, re-purpose this request to re-try sending another termination signal
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Don't check if the target is already terminated, leave that to the progress agent
|
|
// We could be slightly off if there were multiple instances running
|
|
[self->_agentConnection.agent sendTerminationSignal];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} else if (identifier == SPUCancelInstallation) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self cleanupAndExitWithStatus:0 error:nil];
|
|
});
|
|
} else if (identifier == SPUUpdaterAlivePong) {
|
|
_receivedUpdaterPong = YES;
|
|
}
|
|
}
|
|
|
|
- (void)startInstallation SPU_OBJC_DIRECT
|
|
{
|
|
_willCompleteInstallation = YES;
|
|
|
|
dispatch_queue_attr_t queuePriority = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
|
|
|
|
_installerQueue = dispatch_queue_create("org.sparkle-project.sparkle.installer", queuePriority);
|
|
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
os_unfair_lock_lock(&_newConnectionLock);
|
|
BOOL connectionCodeSigningValidationSkipped = self->_connectionCodeSigningValidationSkipped;
|
|
os_unfair_lock_unlock(&_newConnectionLock);
|
|
#else
|
|
BOOL connectionCodeSigningValidationSkipped = NO;
|
|
#endif
|
|
|
|
dispatch_async(_installerQueue, ^{
|
|
NSError *installerError = nil;
|
|
id <SUInstallerProtocol> installer = [SUInstaller installerForHost:self->_host expectedInstallationType:self->_installationType updateDirectory:self->_extractionDirectory connectionCodeSigningValidationSkipped:connectionCodeSigningValidationSkipped homeDirectory:self->_homeDirectory userName:self->_userName error:&installerError];
|
|
|
|
if (installer == nil) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to create installer instance", NSUnderlyingErrorKey: installerError }]];
|
|
});
|
|
return;
|
|
}
|
|
|
|
NSError *firstStageError = nil;
|
|
if (![installer performInitialInstallation:&firstStageError]) {
|
|
self->_installer = nil;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to start installer", NSUnderlyingErrorKey: firstStageError }]];
|
|
});
|
|
return;
|
|
}
|
|
|
|
uint8_t canPerformSilentInstall = (uint8_t)[installer canInstallSilently];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self->_installer = installer;
|
|
|
|
os_unfair_lock_lock(&self->_newConnectionLock);
|
|
self->_performedStage1Installation = YES;
|
|
os_unfair_lock_unlock(&self->_newConnectionLock);
|
|
|
|
uint8_t sendInformation[] = {canPerformSilentInstall, (uint8_t)self->_targetTerminated};
|
|
|
|
NSData *sendData = [NSData dataWithBytes:sendInformation length:sizeof(sendInformation)];
|
|
|
|
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage1 data:sendData];
|
|
|
|
if (self->_targetTerminated) {
|
|
// Stage 2 can still be run before we finish installation
|
|
// if the updater requests for it before the app is terminated
|
|
[self finishInstallationAfterHostTermination];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
- (void)performStage2Installation SPU_OBJC_DIRECT
|
|
{
|
|
BOOL canPerformSecondStage = _shouldShowUI || [_installer canInstallSilently];
|
|
if (canPerformSecondStage) {
|
|
_performedStage2Installation = YES;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
uint8_t targetTerminated = (uint8_t)self->_targetTerminated;
|
|
|
|
NSData *sendData = [NSData dataWithBytes:&targetTerminated length:sizeof(targetTerminated)];
|
|
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage2 data:sendData];
|
|
|
|
// Don't check if the target is already terminated, leave that to the progress agent
|
|
// We could be slightly off if there were multiple instances running
|
|
[self->_agentConnection.agent sendTerminationSignal];
|
|
});
|
|
} else {
|
|
_installer = nil;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resume installer on stage 2 because installation cannot be installed silently" }]];
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)finishInstallationAfterHostTermination SPU_OBJC_DIRECT
|
|
{
|
|
assert(self->_targetTerminated);
|
|
|
|
// Show our installer progress UI tool if only after a certain amount of time passes,
|
|
// and if our installer is silent (i.e, doesn't show progress on its own)
|
|
__block BOOL shouldShowUIProgress = YES;
|
|
if (self->_shouldShowUI && [self->_installer canInstallSilently]) {
|
|
// Ask the updater if it is still alive
|
|
// If they are, we will receive a pong response back
|
|
// Reset if we received a pong just to be on the safe side
|
|
self->_receivedUpdaterPong = NO;
|
|
[self->_communicator handleMessageWithIdentifier:SPUUpdaterAlivePing data:[NSData data]];
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SUDisplayProgressTimeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
// Make sure we're still eligible for showing the installer progress
|
|
// Also if the updater process is still alive, showing the progress should not be our duty
|
|
// if the communicator object is nil, the updater definitely isn't alive. However, if it is not nil,
|
|
// this does not necessarily mean the updater is alive, so we should also check if we got a recent response back from the updater
|
|
if (shouldShowUIProgress && (!self->_receivedUpdaterPong || self->_communicator == nil)) {
|
|
[self->_agentConnection.agent showProgress];
|
|
}
|
|
});
|
|
}
|
|
|
|
dispatch_async(self->_installerQueue, ^{
|
|
if (!self->_performedStage2Installation) {
|
|
[self performStage2Installation];
|
|
}
|
|
|
|
if (!self->_performedStage2Installation) {
|
|
// We failed and we're going to exit shortly
|
|
return;
|
|
}
|
|
|
|
NSError *thirdStageError = nil;
|
|
if (![self->_installer performFinalInstallationProgressBlock:nil error:&thirdStageError]) {
|
|
[self->_installer performCleanup];
|
|
self->_installer = nil;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to finalize installation", NSUnderlyingErrorKey: thirdStageError }]];
|
|
});
|
|
return;
|
|
}
|
|
|
|
self->_performedStage3Installation = YES;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Make sure to stop our displayed progress before we move onto cleanup & relaunch
|
|
// This will also stop the agent from broadcasting the status info service, which we want to do before
|
|
// we relaunch the app because the relaunched app could check the service upon launch..
|
|
[self->_agentConnection.agent stopProgress];
|
|
shouldShowUIProgress = NO;
|
|
|
|
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage3 data:[NSData data]];
|
|
|
|
if (self->_shouldRelaunch) {
|
|
// This will also signal to the agent that it will terminate soon
|
|
[self->_agentConnection.agent relaunchApplication];
|
|
}
|
|
|
|
[self->_installer performCleanup];
|
|
|
|
[self cleanupAndExitWithStatus:EXIT_SUCCESS error:nil];
|
|
});
|
|
});
|
|
}
|
|
|
|
- (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn))
|
|
{
|
|
if (error != nil) {
|
|
SULogError(error);
|
|
|
|
NSData *errorData = SPUArchiveRootObjectSecurely((NSError * _Nonnull)error);
|
|
if (errorData != nil) {
|
|
[_communicator handleMessageWithIdentifier:SPUInstallerError data:errorData];
|
|
}
|
|
}
|
|
|
|
// It's nice to tell the other end we're invalidating
|
|
|
|
os_unfair_lock_lock(&_newConnectionLock);
|
|
|
|
NSXPCConnection *activeConnection = _activeConnection;
|
|
_activeConnection = nil;
|
|
|
|
os_unfair_lock_unlock(&_newConnectionLock);
|
|
|
|
[activeConnection invalidate];
|
|
|
|
[_xpcListener invalidate];
|
|
_xpcListener = nil;
|
|
|
|
[_agentConnection invalidate];
|
|
_agentConnection = nil;
|
|
|
|
[self clearUpdateDirectory];
|
|
|
|
exit(status);
|
|
}
|
|
|
|
@end
|