mirror of
https://github.com/sparkle-project/Sparkle.git
synced 2025-11-01 15:34:38 +00:00
561 lines
30 KiB
Objective-C
561 lines
30 KiB
Objective-C
//
|
|
// SUInstallerLauncher.m
|
|
// InstallerLauncher
|
|
//
|
|
// Created by Mayur Pawashe on 4/1/16.
|
|
// Copyright © 2016 Sparkle Project. All rights reserved.
|
|
//
|
|
|
|
#import "SUInstallerLauncher.h"
|
|
#import "SUInstallerLauncher+Private.h"
|
|
#import "SUFileManager.h"
|
|
#import "SULog.h"
|
|
#import "SPUMessageTypes.h"
|
|
#import "SUBundleIcon.h"
|
|
#import "SPULocalCacheDirectory.h"
|
|
#import "SPUInstallationType.h"
|
|
#import "SUHost.h"
|
|
#import <ImageIO/ImageIO.h>
|
|
#import <ServiceManagement/ServiceManagement.h>
|
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
#import <SystemConfiguration/SystemConfiguration.h>
|
|
|
|
|
|
#include "AppKitPrevention.h"
|
|
|
|
@implementation SUInstallerLauncher
|
|
|
|
- (BOOL)submitProgressToolAtPath:(NSString *)progressToolPath withHostBundle:(NSBundle *)hostBundle inSystemDomainForInstaller:(BOOL)inSystemDomainForInstaller SPU_OBJC_DIRECT
|
|
{
|
|
SUFileManager *fileManager = [[SUFileManager alloc] init];
|
|
|
|
NSURL *progressToolURL = [NSURL fileURLWithPath:progressToolPath];
|
|
|
|
NSError *quarantineError = nil;
|
|
if (![fileManager releaseItemFromQuarantineAtRootURL:progressToolURL error:&quarantineError]) {
|
|
// This may or may not be a fatal error depending on if the process is sandboxed or not
|
|
SULog(SULogLevelError, @"Failed to release quarantine on installer at %@ with error %@", progressToolPath, quarantineError);
|
|
}
|
|
|
|
NSString *executablePath = [[NSBundle bundleWithURL:progressToolURL] executablePath];
|
|
assert(executablePath != nil);
|
|
|
|
NSString *hostBundlePath = hostBundle.bundlePath;
|
|
assert(hostBundlePath != nil);
|
|
|
|
NSString *hostBundleIdentifier = hostBundle.bundleIdentifier;
|
|
assert(hostBundleIdentifier != nil);
|
|
|
|
NSArray<NSString *> *arguments = @[executablePath, hostBundlePath, @(inSystemDomainForInstaller).stringValue];
|
|
|
|
// The progress tool can only be ran as the logged in user, not as root
|
|
CFStringRef domain = kSMDomainUserLaunchd;
|
|
NSString *label = [NSString stringWithFormat:@"%@-sparkle-progress", hostBundleIdentifier];
|
|
|
|
AuthorizationRef auth = NULL;
|
|
Boolean submittedJob = false;
|
|
OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
|
|
if (createStatus == errAuthorizationSuccess) {
|
|
// Try to remove the job from launchd if it is already running
|
|
// We could invoke SMJobCopyDictionary() first to see if the job exists, but I'd rather avoid
|
|
// using it because the headers indicate it may be removed one day without any replacement
|
|
CFErrorRef removeError = NULL;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
if (!SMJobRemove(domain, (__bridge CFStringRef)(label), auth, true, &removeError)) {
|
|
#pragma clang diagnostic pop
|
|
if (removeError != NULL) {
|
|
// It's normal for a job to not be found, so this is not an interesting error
|
|
if (CFErrorGetCode(removeError) != kSMErrorJobNotFound) {
|
|
SULog(SULogLevelError, @"Remove error: %@", removeError);
|
|
}
|
|
CFRelease(removeError);
|
|
}
|
|
}
|
|
|
|
// If we are running as the root user, there is no need to explicitly set the UserName / GroupName keys
|
|
// because we are submitting under the user domain, which should automatically use the the console user.
|
|
NSMutableDictionary *jobDictionary = [[NSMutableDictionary alloc] init];
|
|
jobDictionary[@"Label"] = label;
|
|
jobDictionary[@"ProgramArguments"] = arguments;
|
|
jobDictionary[@"EnableTransactions"] = @NO;
|
|
jobDictionary[@"KeepAlive"] = @{@"SuccessfulExit" : @NO};
|
|
jobDictionary[@"RunAtLoad"] = @NO;
|
|
jobDictionary[@"Nice"] = @0;
|
|
jobDictionary[@"ProcessType"] = @"Interactive";
|
|
jobDictionary[@"LaunchOnlyOnce"] = @YES;
|
|
jobDictionary[@"MachServices"] = @{SPUStatusInfoServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES};
|
|
|
|
CFErrorRef submitError = NULL;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
// SMJobSubmit is deprecated but is the only way to submit a non-permanent
|
|
// helper and allows us to submit to user domain without requiring authorization
|
|
submittedJob = SMJobSubmit(domain, (__bridge CFDictionaryRef)(jobDictionary), auth, &submitError);
|
|
#pragma clang diagnostic pop
|
|
if (!submittedJob) {
|
|
if (submitError != NULL) {
|
|
SULog(SULogLevelError, @"Submit progress error: %@", submitError);
|
|
CFRelease(submitError);
|
|
}
|
|
}
|
|
|
|
AuthorizationFree(auth, kAuthorizationFlagDefaults);
|
|
}
|
|
|
|
return (submittedJob == true);
|
|
}
|
|
|
|
- (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath withHostBundle:(NSBundle *)hostBundle updaterIdentifier:(NSString *)updaterIdentifier userName:(NSString *)userName homeDirectory:(NSString *)homeDirectory authorizationPrompt:(NSString *)authorizationPrompt inSystemDomain:(BOOL)systemDomain rootUser:(BOOL)rootUser SPU_OBJC_DIRECT
|
|
{
|
|
SUFileManager *fileManager = [[SUFileManager alloc] init];
|
|
|
|
// No need to release the quarantine for this utility
|
|
// In fact, we shouldn't because the tool may be located at a path we should not be writing too.
|
|
|
|
NSString *hostBundleIdentifier = hostBundle.bundleIdentifier;
|
|
assert(hostBundleIdentifier != nil);
|
|
|
|
// The first argument has to be the path to the program, and the second is a host identifier so that the installer knows what mach services to host
|
|
// The third and forth arguments are for home directory and user name which only pkg installer scripts may need
|
|
// We intentionally do not pass any more arguments. Anything else should be done via IPC.
|
|
// This is compatible to SMJobBless() which does not allow arguments
|
|
// Even though we aren't using that function for now, it'd be wise to not decrease compatibility to it
|
|
|
|
NSArray<NSString *> *arguments = @[installerPath, hostBundleIdentifier, homeDirectory, userName];
|
|
|
|
AuthorizationRef auth = NULL;
|
|
OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
|
|
if (createStatus != errAuthorizationSuccess) {
|
|
auth = NULL;
|
|
SULog(SULogLevelError, @"Failed to create authorization reference: %d", createStatus);
|
|
}
|
|
|
|
BOOL canceledAuthorization = NO;
|
|
BOOL failedToUseSystemDomain = NO;
|
|
if (auth != NULL && systemDomain && !rootUser) {
|
|
// See Apple's 'EvenBetterAuthorizationSample' sample code and
|
|
// https://developer.apple.com/library/mac/technotes/tn2095/_index.html#//apple_ref/doc/uid/DTS10003110-CH1-SECTION7
|
|
// We can set a custom right name for authenticating as an administrator
|
|
// Using this right rather than using something like kSMRightModifySystemDaemons allows us to present a better worded prompt
|
|
// Note the right name is cached, so if we want to change the authorization
|
|
// prompt, we may need to change the right name. I have found no good way around this :|
|
|
NSString *sparkleAuthTag = @"sparkle2-auth"; // this needs to change if auth wording changes
|
|
NSString *rightNameString;
|
|
if ([hostBundleIdentifier isEqualToString:updaterIdentifier]) {
|
|
// Application bundle is likely updating itself
|
|
rightNameString = [NSString stringWithFormat:@"%@.%@", hostBundleIdentifier, sparkleAuthTag];
|
|
} else {
|
|
// Updater is likely updating a bundle that is not itself
|
|
rightNameString = [NSString stringWithFormat:@"%@.%@.%@", updaterIdentifier, hostBundleIdentifier, sparkleAuthTag];
|
|
}
|
|
|
|
const char *rightName = rightNameString.UTF8String;
|
|
assert(rightName != NULL);
|
|
|
|
OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
|
|
if (getRightResult == errAuthorizationDenied) {
|
|
if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
|
|
SULog(SULogLevelError, @"Failed to make auth right set");
|
|
}
|
|
}
|
|
|
|
AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
|
|
AuthorizationRights rights = { .count = 1, .items = &right };
|
|
|
|
AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
|
|
|
|
AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
|
|
AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
|
|
|
|
// Find a 32x32 image representation of the icon, and write out a PNG version of it to a temporary location
|
|
// Then use the icon (if one is available) for the authorization prompt
|
|
// NSImage is not used because it relies on AppKit
|
|
NSURL *tempIconDestinationURL = nil;
|
|
NSURL *iconURL = [SUBundleIcon iconURLForHost:[[SUHost alloc] initWithBundle:hostBundle]];
|
|
if (iconURL != nil) {
|
|
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)iconURL, (CFDictionaryRef)@{});
|
|
if (imageSource != NULL) {
|
|
size_t imageCount = CGImageSourceGetCount(imageSource);
|
|
for (size_t imageIndex = 0; imageIndex < imageCount; imageIndex++) {
|
|
CFDictionaryRef cfProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, imageIndex, (CFDictionaryRef)@{});
|
|
NSDictionary *properties = CFBridgingRelease(cfProperties);
|
|
|
|
NSNumber *pixelWidth = properties[(const NSString *)kCGImagePropertyPixelWidth];
|
|
NSNumber *pixelHeight = properties[(const NSString *)kCGImagePropertyPixelHeight];
|
|
|
|
// If we don't find a 32x32 image representation, then we don't provide an icon
|
|
// Don't try to make up with it by eg: converting an image representation to this size
|
|
// The app developer should be providing the icon representation.
|
|
// The authorization API may not take other size dimensions.
|
|
NSNumber *targetDimension = @32;
|
|
if ([pixelWidth isKindOfClass:[NSNumber class]] && [pixelHeight isKindOfClass:[NSNumber class]] && [pixelWidth isEqualToNumber:targetDimension] && [pixelHeight isEqualToNumber:targetDimension]) {
|
|
|
|
// Use /tmp rather than NSTemporaryDirectory() or SUFileManager's temp directory function because we want:
|
|
// a) no spaces in the path (SU/NSFileManager fails here)
|
|
// b) short file path that does not exceed a small threshold (NSTemporaryDirectory() fails here)
|
|
// These limitations only apply to older systems (eg: macOS 10.8)
|
|
// The file also needs to be placed in a system readable place such as /tmp
|
|
// See https://github.com/sparkle-project/Sparkle/issues/347#issuecomment-149523848 for more info
|
|
char pathBuffer[] = "/tmp/XXXXXX.png";
|
|
int tempIconFile = mkstemps(pathBuffer, strlen(".png"));
|
|
if (tempIconFile == -1) {
|
|
SULog(SULogLevelError, @"Failed to open temp icon from path buffer with error: %d", errno);
|
|
} else {
|
|
close(tempIconFile);
|
|
|
|
NSString *path = [[NSString alloc] initWithUTF8String:pathBuffer];
|
|
tempIconDestinationURL = [NSURL fileURLWithPath:path];
|
|
|
|
CGImageDestinationRef imageDestination = NULL;
|
|
if (@available(macOS 11, *)) {
|
|
CFStringRef uti = (__bridge CFStringRef)[UTTypePNG identifier];
|
|
imageDestination = CGImageDestinationCreateWithURL((CFURLRef)tempIconDestinationURL, uti, 1, NULL);
|
|
}
|
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_11_0
|
|
else
|
|
{
|
|
imageDestination = CGImageDestinationCreateWithURL((CFURLRef)tempIconDestinationURL, kUTTypePNG, 1, NULL);
|
|
}
|
|
#endif
|
|
if (imageDestination != NULL) {
|
|
CGImageDestinationAddImageFromSource(imageDestination, imageSource, imageIndex, (CFDictionaryRef)@{});
|
|
if (CGImageDestinationFinalize(imageDestination)) {
|
|
iconAuthorizationItem.valueLength = strlen(pathBuffer);
|
|
iconAuthorizationItem.value = pathBuffer;
|
|
|
|
authorizationEnvironment.count = 1;
|
|
authorizationEnvironment.items = &iconAuthorizationItem;
|
|
}
|
|
|
|
CFRelease(imageDestination);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
CFRelease(imageSource);
|
|
}
|
|
}
|
|
|
|
// This should prompt up the authorization dialog if necessary
|
|
OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
|
|
if (copyStatus != errAuthorizationSuccess) {
|
|
failedToUseSystemDomain = YES;
|
|
|
|
if (copyStatus == errAuthorizationCanceled) {
|
|
canceledAuthorization = YES;
|
|
} else {
|
|
SULog(SULogLevelError, @"Failed copying system domain rights: %d", copyStatus);
|
|
}
|
|
}
|
|
|
|
if (tempIconDestinationURL != nil) {
|
|
[fileManager removeItemAtURL:tempIconDestinationURL error:NULL];
|
|
}
|
|
}
|
|
|
|
Boolean submittedJob = false;
|
|
if (!canceledAuthorization && !failedToUseSystemDomain && auth != NULL) {
|
|
CFStringRef domain = (systemDomain ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
|
|
NSString *label = [NSString stringWithFormat:@"%@-sparkle-updater", hostBundleIdentifier];
|
|
|
|
// Try to remove the job from launchd if it is already running
|
|
// We could invoke SMJobCopyDictionary() first to see if the job exists, but I'd rather avoid
|
|
// using it because the headers indicate it may be removed one day without any replacement
|
|
CFErrorRef removeError = NULL;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
if (!SMJobRemove(domain, (__bridge CFStringRef)(label), auth, true, &removeError)) {
|
|
#pragma clang diagnostic pop
|
|
if (removeError != NULL) {
|
|
// It's normal for a job to not be found, so this is not an interesting error
|
|
if (CFErrorGetCode(removeError) != kSMErrorJobNotFound) {
|
|
SULog(SULogLevelError, @"Remove job error: %@", removeError);
|
|
}
|
|
CFRelease(removeError);
|
|
}
|
|
}
|
|
|
|
NSDictionary *jobDictionary = @{@"Label" : label, @"ProgramArguments" : arguments, @"EnableTransactions" : @NO, @"KeepAlive" : @{@"SuccessfulExit" : @NO}, @"RunAtLoad" : @NO, @"Nice" : @0, @"ProcessType": @"Interactive", @"LaunchOnlyOnce": @YES, @"MachServices" : @{SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES, SPUProgressAgentServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES}};
|
|
|
|
CFErrorRef submitError = NULL;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
// SMJobSubmit is deprecated but is the only way to submit a non-permanent
|
|
// helper and allows us to submit to user domain without requiring authorization
|
|
submittedJob = SMJobSubmit(domain, (__bridge CFDictionaryRef)(jobDictionary), auth, &submitError);
|
|
#pragma clang diagnostic pop
|
|
if (!submittedJob) {
|
|
if (submitError != NULL) {
|
|
SULog(SULogLevelError, @"Submit error: %@", submitError);
|
|
CFRelease(submitError);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (auth != NULL) {
|
|
AuthorizationFree(auth, kAuthorizationFlagDefaults);
|
|
}
|
|
|
|
SUInstallerLauncherStatus status;
|
|
if (submittedJob == true) {
|
|
status = SUInstallerLauncherSuccess;
|
|
} else if (canceledAuthorization) {
|
|
status = SUInstallerLauncherCanceled;
|
|
} else {
|
|
status = SUInstallerLauncherFailure;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
- (NSString *)pathForBundledTool:(NSString *)toolName extension:(NSString *)extension fromBundle:(NSBundle *)bundle SPU_OBJC_DIRECT
|
|
{
|
|
// If the path extension is empty, we don't want to add a "." at the end
|
|
NSString *nameWithExtension = (extension.length > 0) ? [toolName stringByAppendingPathExtension:extension] : toolName;
|
|
assert(nameWithExtension != nil);
|
|
|
|
NSURL *auxiliaryToolURL;
|
|
if ([bundle.bundleURL.pathExtension isEqualToString:@"xpc"]) {
|
|
// Paranoid check to get full bundle URL
|
|
NSURL *fullURL = bundle.bundleURL.URLByResolvingSymlinksInPath;
|
|
|
|
auxiliaryToolURL = [fullURL.URLByDeletingLastPathComponent.URLByDeletingLastPathComponent URLByAppendingPathComponent:nameWithExtension];
|
|
} else {
|
|
auxiliaryToolURL = [bundle URLForAuxiliaryExecutable:nameWithExtension];
|
|
}
|
|
|
|
if (auxiliaryToolURL == nil) {
|
|
SULog(SULogLevelError, @"Error: Cannot retrieve path for auxiliary tool: %@", nameWithExtension);
|
|
return nil;
|
|
}
|
|
|
|
NSURL *resolvedAuxiliaryToolURL = [auxiliaryToolURL URLByResolvingSymlinksInPath];
|
|
if (resolvedAuxiliaryToolURL == nil) {
|
|
SULog(SULogLevelError, @"Error: Cannot retrieve resolved path for auxiliary tool path: %@", auxiliaryToolURL.path);
|
|
return nil;
|
|
}
|
|
|
|
return resolvedAuxiliaryToolURL.path;
|
|
}
|
|
|
|
BOOL SPUSystemNeedsAuthorizationAccessForBundlePath(NSString *bundlePath)
|
|
{
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL hasWritability = [fileManager isWritableFileAtPath:bundlePath] && [fileManager isWritableFileAtPath:[bundlePath stringByDeletingLastPathComponent]];
|
|
|
|
BOOL needsAuthorization;
|
|
if (!hasWritability) {
|
|
needsAuthorization = YES;
|
|
} else {
|
|
// Just because we have writability access does not mean we can set the correct owner/group
|
|
// Test if we can set the owner/group on a temporarily created file
|
|
// If we can, then we can probably perform an update without authorization
|
|
|
|
NSString *tempFilename = @"permission_test" ;
|
|
|
|
SUFileManager *suFileManager = [[SUFileManager alloc] init];
|
|
NSURL *tempDirectoryURL = [suFileManager makeTemporaryDirectoryAppropriateForDirectoryURL:[NSURL fileURLWithPath:NSTemporaryDirectory()] error:NULL];
|
|
|
|
if (tempDirectoryURL == nil) {
|
|
// I don't imagine this ever happening but in case it does, requesting authorization may be the better option
|
|
needsAuthorization = YES;
|
|
} else {
|
|
NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:tempFilename];
|
|
if (![[NSData data] writeToURL:tempFileURL atomically:NO]) {
|
|
// Obvious indicator we may need authorization
|
|
needsAuthorization = YES;
|
|
} else {
|
|
needsAuthorization = ![suFileManager changeOwnerAndGroupOfItemAtRootURL:tempFileURL toMatchURL:[NSURL fileURLWithPath:bundlePath] error:NULL];
|
|
}
|
|
|
|
[suFileManager removeItemAtURL:tempDirectoryURL error:NULL];
|
|
}
|
|
}
|
|
|
|
return needsAuthorization;
|
|
}
|
|
|
|
static BOOL SPUUsesSystemDomainForBundlePath(NSString *path, BOOL rootUser
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
, NSString *installationType
|
|
#endif
|
|
)
|
|
{
|
|
if (!rootUser) {
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
if ([installationType isEqualToString:SPUInstallationTypeGuidedPackage]) {
|
|
return YES;
|
|
} else if ([installationType isEqualToString:SPUInstallationTypeInteractivePackage]) {
|
|
return NO;
|
|
} else
|
|
#endif
|
|
{
|
|
return SPUSystemNeedsAuthorizationAccessForBundlePath(path);
|
|
}
|
|
} else {
|
|
// If we are the root user we use the system domain even if we don't need escalated authorization.
|
|
// Note interactive package installations are not supported as root.
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
// Note: do not pass untrusted information such as paths to the installer and progress agent tools, when we can find them ourselves here
|
|
- (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIdentifier:(NSString *)updaterIdentifier authorizationPrompt:(NSString *)authorizationPrompt installationType:(NSString *)installationType allowingDriverInteraction:(BOOL)allowingDriverInteraction completion:(void (^)(SUInstallerLauncherStatus, BOOL))completionHandler
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// We could do a sort of preflight Authorization test instead of testing if we are running as root,
|
|
// but I think this is not necessarily a better approach. We have to chown() the launcher cache directory later on,
|
|
// and that is not necessarily related to a preflight test. It's more related to being ran under a root / different user from the active GUI session
|
|
BOOL rootUser = (geteuid() == 0);
|
|
|
|
BOOL inSystemDomain = SPUUsesSystemDomainForBundlePath(hostBundlePath, rootUser
|
|
#if SPARKLE_BUILD_PACKAGE_SUPPORT
|
|
, installationType
|
|
#endif
|
|
);
|
|
|
|
NSBundle *hostBundle = [NSBundle bundleWithPath:hostBundlePath];
|
|
if (hostBundle == nil) {
|
|
SULog(SULogLevelError, @"InstallerLauncher failed to create bundle at %@", hostBundlePath);
|
|
SULog(SULogLevelError, @"Please make sure InstallerLauncher is not sandboxed and do not sign your app by passing --deep. Check: codesign -d --entitlements :- \"%@\"", NSBundle.mainBundle.bundlePath);
|
|
SULog(SULogLevelError, @"More information regarding sandboxing: https://sparkle-project.org/documentation/sandboxing/");
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
// if we need to use the system authorization from non-root and we aren't allowed interaction, then try sometime later when interaction is allowed
|
|
if (inSystemDomain && !rootUser && !allowingDriverInteraction) {
|
|
completionHandler(SUInstallerLauncherAuthorizeLater, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
NSString *hostBundleIdentifier = hostBundle.bundleIdentifier;
|
|
assert(hostBundleIdentifier != nil);
|
|
|
|
// We could be inside the InstallerLauncher XPC bundle or in the Sparkle.framework bundle if no XPC service is used
|
|
NSBundle *ourBundle = [NSBundle bundleForClass:[self class]];
|
|
|
|
// Note we do not have to copy this tool out of the bundle it's in because it's a utility with no dependencies.
|
|
// Furthermore, we can keep the tool at a place that may not necessarily be writable.
|
|
NSString *installerPath = [self pathForBundledTool:@""SPARKLE_RELAUNCH_TOOL_NAME extension:@"" fromBundle:ourBundle];
|
|
if (installerPath == nil) {
|
|
SULog(SULogLevelError, @"Error: Cannot submit installer because the installer could not be located");
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
// We do however have to copy the progress tool app somewhere safe due to its external depedencies
|
|
NSString *progressToolResourcePath = [self pathForBundledTool:@""SPARKLE_INSTALLER_PROGRESS_TOOL_NAME extension:@"app" fromBundle:ourBundle];
|
|
|
|
if (progressToolResourcePath == nil) {
|
|
SULog(SULogLevelError, @"Error: Cannot submit progress tool because the progress tool could not be located");
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
NSString *userName;
|
|
NSString *homeDirectory;
|
|
uid_t uid = 0;
|
|
gid_t gid = 0;
|
|
if (!rootUser) {
|
|
// Normal path
|
|
homeDirectory = NSHomeDirectory();
|
|
assert(homeDirectory != nil);
|
|
|
|
userName = NSUserName();
|
|
assert(userName != nil);
|
|
} else {
|
|
// As the root user we need to obtain the user name and home directory reflecting
|
|
// the user's console session.
|
|
CFStringRef userNameRef = SCDynamicStoreCopyConsoleUser(NULL, &uid, &gid);
|
|
if (userNameRef == NULL) {
|
|
SULog(SULogLevelError, @"Failed to retrieve user name from the console user");
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
userName = (NSString *)CFBridgingRelease(userNameRef);
|
|
homeDirectory = NSHomeDirectoryForUser(userName);
|
|
if (homeDirectory == nil) {
|
|
SULog(SULogLevelError, @"Failed to retrieve home directory for user: %@", userName);
|
|
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// It may be tempting here to validate/match the signature of the installer and progress tool, however this is not very reliable
|
|
// We can't compare the signature of this framework/XPC service (depending how it's run) to the host bundle because
|
|
// they could be different (eg: take a look at sparkle-cli). We also can't easily tell if the signature of the service/framework is the same as the bundle it's inside.
|
|
// The service/framework also need not even be signed in the first place. We'll just assume for now the original bundle hasn't been tampered with
|
|
NSString *cachePath = rootUser ?
|
|
[SPULocalCacheDirectory cachePathForBundleIdentifier:hostBundleIdentifier userName:userName] :
|
|
[SPULocalCacheDirectory cachePathForBundleIdentifier:hostBundleIdentifier];
|
|
|
|
NSString *rootLauncherCachePath = [cachePath stringByAppendingPathComponent:@"Launcher"];
|
|
|
|
[SPULocalCacheDirectory removeOldItemsInDirectory:rootLauncherCachePath];
|
|
|
|
NSDictionary<NSFileAttributeKey, id> *fileAttributes = rootUser ?
|
|
@{NSFileOwnerAccountID: @(uid), NSFileGroupOwnerAccountID: @(gid)} :
|
|
nil;
|
|
|
|
NSString *launcherCachePath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootLauncherCachePath intermediateDirectoryFileAttributes:fileAttributes];
|
|
|
|
if (launcherCachePath == nil) {
|
|
SULog(SULogLevelError, @"Failed to create cache directory for progress tool in %@", rootLauncherCachePath);
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
SUFileManager *fileManager = [[SUFileManager alloc] init];
|
|
|
|
if (rootUser) {
|
|
// Ensure the console user has ownership of the launcher cache directory
|
|
// Otherwise the updater may not launch and not be able to clean up itself
|
|
NSError *changeOwnerAndGroupError = nil;
|
|
if (![fileManager changeOwnerAndGroupOfItemAtURL:[NSURL fileURLWithPath:launcherCachePath] ownerID:uid groupID:gid error:&changeOwnerAndGroupError]) {
|
|
SULog(SULogLevelError, @"Failed to change owner and group for launcher cache directory: %@", changeOwnerAndGroupError);
|
|
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
}
|
|
|
|
NSString *progressToolPath = [launcherCachePath stringByAppendingPathComponent:@""SPARKLE_INSTALLER_PROGRESS_TOOL_NAME@".app"];
|
|
|
|
NSError *copyError = nil;
|
|
// SUFileManager is more reliable for copying files around
|
|
if (![fileManager copyItemAtURL:[NSURL fileURLWithPath:progressToolResourcePath] toURL:[NSURL fileURLWithPath:progressToolPath] error:©Error]) {
|
|
SULog(SULogLevelError, @"Failed to copy progress tool to cache: %@", copyError);
|
|
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
|
|
return;
|
|
}
|
|
|
|
SUInstallerLauncherStatus installerStatus = [self submitInstallerAtPath:installerPath withHostBundle:hostBundle updaterIdentifier:updaterIdentifier userName:userName homeDirectory:homeDirectory authorizationPrompt:authorizationPrompt inSystemDomain:inSystemDomain rootUser:rootUser];
|
|
|
|
BOOL submittedProgressTool = NO;
|
|
if (installerStatus == SUInstallerLauncherSuccess) {
|
|
submittedProgressTool = [self submitProgressToolAtPath:progressToolPath withHostBundle:hostBundle inSystemDomainForInstaller:inSystemDomain];
|
|
|
|
if (!submittedProgressTool) {
|
|
SULog(SULogLevelError, @"Failed to submit progress tool job");
|
|
}
|
|
} else if (installerStatus == SUInstallerLauncherFailure) {
|
|
SULog(SULogLevelError, @"Failed to submit installer job");
|
|
SULog(SULogLevelError, @"If your application is sandboxed please follow steps at: https://sparkle-project.org/documentation/sandboxing/");
|
|
}
|
|
|
|
if (installerStatus == SUInstallerLauncherCanceled) {
|
|
completionHandler(installerStatus, inSystemDomain);
|
|
} else {
|
|
completionHandler(submittedProgressTool ? SUInstallerLauncherSuccess : SUInstallerLauncherFailure, inSystemDomain);
|
|
}
|
|
});
|
|
}
|
|
|
|
@end
|