1612 lines
38 KiB
Objective-C
1612 lines
38 KiB
Objective-C
//
|
|
// file: utilities.m
|
|
// project: OverSight (shared)
|
|
// description: various helper/utility functions
|
|
//
|
|
// created by Patrick Wardle
|
|
// copyright (c) 2017 Objective-See. All rights reserved.
|
|
//
|
|
|
|
@import OSLog;
|
|
|
|
#import "consts.h"
|
|
#import "utilities.h"
|
|
|
|
#import <pwd.h>
|
|
#import <grp.h>
|
|
#import <dlfcn.h>
|
|
#import <signal.h>
|
|
#import <unistd.h>
|
|
#import <libproc.h>
|
|
#import <sys/sysctl.h>
|
|
#import <Carbon/Carbon.h>
|
|
#import <Security/Security.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <SystemConfiguration/SystemConfiguration.h>
|
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
|
|
/* GLOBALS */
|
|
|
|
//log handle
|
|
extern os_log_t logHandle;
|
|
|
|
//get app's version
|
|
// extracted from Info.plist
|
|
NSString* getAppVersion(void)
|
|
{
|
|
//read and return 'CFBundleVersion' from bundle
|
|
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
|
|
}
|
|
|
|
//given an app binary
|
|
// try get app's bundle
|
|
NSBundle* getAppBundle(NSString* binaryPath)
|
|
{
|
|
//bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//app path
|
|
NSString* appPath = nil;
|
|
|
|
//build app path
|
|
// assuming path is <blah.app>/Contents/MacOS/<blah>
|
|
appPath = [[[binaryPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
|
|
if(YES != [appPath hasSuffix:@".app"])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//try to load app's bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
if(nil == appBundle)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sanity check
|
|
// binary paths match?
|
|
if(YES != [appBundle.executablePath isEqualToString:binaryPath])
|
|
{
|
|
//unset
|
|
appBundle = nil;
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return appBundle;
|
|
}
|
|
|
|
//figure out binary's name
|
|
// either via app bundle, or from path
|
|
NSString* getBinaryName(NSString* path)
|
|
{
|
|
//name
|
|
NSString* name = nil;
|
|
|
|
//bundle
|
|
NSBundle* bundle = nil;
|
|
|
|
//try get bundle
|
|
// then extract name from 'CFBundleName'
|
|
bundle = getAppBundle(path);
|
|
if(nil != bundle)
|
|
{
|
|
//extract name
|
|
name = [bundle infoDictionary][@"CFBundleName"];
|
|
}
|
|
|
|
//no app bundle || no 'CFBundleName'?
|
|
// just use last component from the path
|
|
if(nil == name)
|
|
{
|
|
//set name
|
|
name = [path lastPathComponent];
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
//get path to (main) app of a login item
|
|
// login item is in app bundle, so parse up to get main app
|
|
NSString* getMainAppPath(void)
|
|
{
|
|
//path components
|
|
NSArray *pathComponents = nil;
|
|
|
|
//path to config (main) app
|
|
NSString* mainApp = nil;
|
|
|
|
//get path components
|
|
// then build full path to main app
|
|
pathComponents = [[[NSBundle mainBundle] bundlePath] pathComponents];
|
|
if(pathComponents.count > 4)
|
|
{
|
|
//init path to full (main) app
|
|
mainApp = [NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 4)]];
|
|
}
|
|
|
|
//when (still) nil
|
|
// use default path
|
|
if(nil == mainApp)
|
|
{
|
|
//default
|
|
mainApp = [@"/Applications" stringByAppendingPathComponent:APP_NAME];
|
|
}
|
|
|
|
return mainApp;
|
|
}
|
|
|
|
//give path to app
|
|
// get full path to its binary
|
|
NSString* getAppBinary(NSString* appPath)
|
|
{
|
|
//binary path
|
|
NSString* binaryPath = nil;
|
|
|
|
//app bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//load app bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
if(nil == appBundle)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to load app bundle for %@", appPath);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extract executable
|
|
binaryPath = appBundle.executablePath;
|
|
|
|
bail:
|
|
|
|
return binaryPath;
|
|
}
|
|
|
|
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
//get (true) parent
|
|
NSDictionary* getRealParent(pid_t pid)
|
|
{
|
|
//process info
|
|
NSDictionary* processInfo = nil;
|
|
|
|
//process serial number
|
|
ProcessSerialNumber psn = {0, kNoProcess};
|
|
|
|
//(parent) process serial number
|
|
ProcessSerialNumber ppsn = {0, kNoProcess};
|
|
|
|
//get process serial number from pid
|
|
if(noErr != GetProcessForPID(pid, &psn))
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
//get process (carbon) info
|
|
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&psn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
|
|
if(nil == processInfo)
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
//extract/convert parent ppsn
|
|
ppsn.lowLongOfPSN = [processInfo[@"ParentPSN"] longLongValue] & 0x00000000FFFFFFFFLL;
|
|
ppsn.highLongOfPSN = ([processInfo[@"ParentPSN"] longLongValue] >> 32) & 0x00000000FFFFFFFFLL;
|
|
|
|
//get parent process (carbon) info
|
|
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&ppsn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
|
|
if(nil == processInfo)
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return processInfo;
|
|
}
|
|
|
|
#pragma GCC diagnostic pop
|
|
|
|
//check if something is nil
|
|
// if so, return a default ('unknown') value
|
|
NSString* valueForStringItem(NSString* item)
|
|
{
|
|
return (nil != item) ? item : @"unknown";
|
|
}
|
|
|
|
//find 'top-level' app of binary
|
|
// useful to determine if binary (or other app) is embedded in a 'parent' app bundle
|
|
NSString* topLevelApp(NSString* binaryPath)
|
|
{
|
|
//app path
|
|
NSString* appPath = nil;
|
|
|
|
//offset of (first) '.app'
|
|
NSRange offset;
|
|
|
|
//find first instance of '.app' in path
|
|
offset = [binaryPath rangeOfString:@".app/" options:NSCaseInsensitiveSearch];
|
|
if(NSNotFound == offset.location)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extact app path
|
|
// from start, to & including '.app'
|
|
appPath = [binaryPath substringWithRange:NSMakeRange(0, offset.location+4)];
|
|
|
|
bail:
|
|
|
|
return appPath;
|
|
}
|
|
|
|
//verify that an app bundle is valid
|
|
// signed & with (our) signing auth / identifier
|
|
OSStatus verifyApp(NSString* path, NSString* signingAuth)
|
|
{
|
|
//status
|
|
OSStatus status = !noErr;
|
|
|
|
//signing req string
|
|
NSString *requirement = nil;
|
|
|
|
//code
|
|
SecStaticCodeRef staticCode = NULL;
|
|
|
|
//signing reqs
|
|
SecRequirementRef requirementRef = NULL;
|
|
|
|
//init signing req string
|
|
requirement = [NSString stringWithFormat:@"anchor apple generic and identifier \"%@\" and certificate leaf [subject.CN] = \"%@\" and info [CFBundleShortVersionString] >= \"1.0.0\"", INSTALLER_ID, signingAuth];
|
|
|
|
//create static code
|
|
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
|
|
if(noErr != status)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: SecStaticCodeCreateWithPath failed w/ %d", status);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//create req string
|
|
status = SecRequirementCreateWithString((__bridge CFStringRef _Nonnull)(requirement), kSecCSDefaultFlags, &requirementRef);
|
|
if( (noErr != status) ||
|
|
(requirementRef == NULL) )
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: SecRequirementCreateWithString failed w/ %d", status);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//check if file is signed w/ apple dev id by checking if it conforms to req string
|
|
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
|
|
if(noErr != status)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: SecStaticCodeCheckValidity failed w/ %d", status);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
status = noErr;
|
|
|
|
bail:
|
|
|
|
//free req reference
|
|
if(NULL != requirementRef)
|
|
{
|
|
//free
|
|
CFRelease(requirementRef);
|
|
requirementRef = NULL;
|
|
}
|
|
|
|
//free static code
|
|
if(NULL != staticCode)
|
|
{
|
|
//free
|
|
CFRelease(staticCode);
|
|
staticCode = NULL;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
//get name of logged in user
|
|
uid_t getConsoleUserID(void)
|
|
{
|
|
//uid
|
|
uid_t userID = 0;
|
|
|
|
//copy logged in user
|
|
if(NULL == CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, &userID, NULL)))
|
|
{
|
|
//error?
|
|
// defer to current user
|
|
userID = getuid();
|
|
}
|
|
|
|
return userID;
|
|
}
|
|
|
|
//get process name
|
|
// either via app bundle, or path
|
|
NSString* getProcessName(NSString* path)
|
|
{
|
|
//process name
|
|
NSString* processName = nil;
|
|
|
|
//app bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//try find an app bundle
|
|
appBundle = findAppBundle(path);
|
|
if(nil != appBundle)
|
|
{
|
|
//grab name from app's bundle
|
|
processName = [appBundle infoDictionary][@"CFBundleName"];
|
|
}
|
|
|
|
//still nil?
|
|
// just grab from path
|
|
if(nil == processName)
|
|
{
|
|
//from path
|
|
processName = [path lastPathComponent];
|
|
}
|
|
|
|
return processName;
|
|
}
|
|
|
|
//given a path to binary
|
|
// parse it back up to find app's bundle
|
|
NSBundle* findAppBundle(NSString* binaryPath)
|
|
{
|
|
//app's bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//app's path
|
|
NSString* appPath = nil;
|
|
|
|
//first just try full path
|
|
appPath = [[binaryPath stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
|
|
//try to find the app's bundle/info dictionary
|
|
do
|
|
{
|
|
//try to load app's bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
|
|
//check for match
|
|
// ->binary path's match
|
|
if( (nil != appBundle) &&
|
|
(YES == [appBundle.executablePath isEqualToString:binaryPath]))
|
|
{
|
|
//all done
|
|
break;
|
|
}
|
|
|
|
//always unset bundle var since it's being returned
|
|
// ->and at this point, its not a match
|
|
appBundle = nil;
|
|
|
|
//remove last part
|
|
// ->will try this next
|
|
appPath = [appPath stringByDeletingLastPathComponent];
|
|
|
|
//scan until we get to root
|
|
// ->of course, loop will exit if app info dictionary is found/loaded
|
|
} while( (nil != appPath) &&
|
|
(YES != [appPath isEqualToString:@"/"]) &&
|
|
(YES != [appPath isEqualToString:@""]) );
|
|
|
|
return appBundle;
|
|
}
|
|
|
|
//set dir's|file's group/owner
|
|
BOOL setFileOwner(NSString* path, NSNumber* groupID, NSNumber* ownerID, BOOL recursive)
|
|
{
|
|
//ret var
|
|
BOOL bSetOwner = NO;
|
|
|
|
//owner dictionary
|
|
NSDictionary* fileOwner = nil;
|
|
|
|
//sub paths
|
|
NSArray* subPaths = nil;
|
|
|
|
//full path
|
|
// ->for recursive
|
|
NSString* fullPath = nil;
|
|
|
|
//init permissions dictionary
|
|
fileOwner = @{NSFileGroupOwnerAccountID:groupID, NSFileOwnerAccountID:ownerID};
|
|
|
|
//set group/owner
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:path error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set ownership for %@ (%@)", path, fileOwner);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "set ownership for %{public}@ (%{public}@)", path, fileOwner);
|
|
|
|
//do it recursively
|
|
if(YES == recursive)
|
|
{
|
|
//sanity check
|
|
// ->make sure root starts with '/'
|
|
if(YES != [path hasSuffix:@"/"])
|
|
{
|
|
//add '/'
|
|
path = [NSString stringWithFormat:@"%@/", path];
|
|
}
|
|
|
|
//get all subpaths
|
|
subPaths = [[NSFileManager defaultManager] subpathsAtPath:path];
|
|
for(NSString *subPath in subPaths)
|
|
{
|
|
//init full path
|
|
fullPath = [path stringByAppendingString:subPath];
|
|
|
|
//set group/owner
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:fullPath error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set ownership for %{public}@ (%{public}@)", fullPath, fileOwner);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
//no errors
|
|
bSetOwner = YES;
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return bSetOwner;
|
|
}
|
|
|
|
//set permissions for file
|
|
BOOL setFilePermissions(NSString* file, int permissions, BOOL recursive)
|
|
{
|
|
//ret var
|
|
BOOL bSetPermissions = NO;
|
|
|
|
//file permissions
|
|
NSDictionary* filePermissions = nil;
|
|
|
|
//root directory
|
|
NSURL* root = nil;
|
|
|
|
//directory enumerator
|
|
NSDirectoryEnumerator* enumerator = nil;
|
|
|
|
//error
|
|
NSError* error = nil;
|
|
|
|
//init dictionary
|
|
filePermissions = @{NSFilePosixPermissions: [NSNumber numberWithInt:permissions]};
|
|
|
|
//apply file permissions recursively
|
|
if(YES == recursive)
|
|
{
|
|
//init root
|
|
root = [NSURL fileURLWithPath:file];
|
|
|
|
//init enumerator
|
|
enumerator = [[NSFileManager defaultManager] enumeratorAtURL:root includingPropertiesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] options:0 errorHandler:nil];
|
|
|
|
//set file permissions on each
|
|
for(NSURL* currentFile in enumerator)
|
|
{
|
|
//set permissions
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:currentFile.path error:&error])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set permissions for %@ (%@), %@", currentFile.path, filePermissions, error);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
//always set permissions on passed in file (or top-level directory)
|
|
// note: recursive enumerator skips root directory, so execute this always
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:file error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set permissions for %@ (%@)", file, filePermissions);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
bSetPermissions = YES;
|
|
|
|
bail:
|
|
|
|
return bSetPermissions;
|
|
}
|
|
|
|
//get process's path
|
|
NSString* getProcessPath(pid_t pid)
|
|
{
|
|
//task path
|
|
NSString* processPath = nil;
|
|
|
|
//buffer for process path
|
|
char pathBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0};
|
|
|
|
//status
|
|
int status = -1;
|
|
|
|
//'management info base' array
|
|
int mib[3] = {0};
|
|
|
|
//system's size for max args
|
|
unsigned long systemMaxArgs = 0;
|
|
|
|
//process's args
|
|
char* taskArgs = NULL;
|
|
|
|
//# of args
|
|
int numberOfArgs = 0;
|
|
|
|
//size of buffers, etc
|
|
size_t size = 0;
|
|
|
|
//reset buffer
|
|
memset(pathBuffer, 0x0, PROC_PIDPATHINFO_MAXSIZE);
|
|
|
|
//first attempt to get path via 'proc_pidpath()'
|
|
status = proc_pidpath(pid, pathBuffer, sizeof(pathBuffer));
|
|
if(0 != status)
|
|
{
|
|
//init task's name
|
|
processPath = [NSString stringWithUTF8String:pathBuffer];
|
|
}
|
|
//otherwise
|
|
// try via task's args ('KERN_PROCARGS2')
|
|
else
|
|
{
|
|
//init mib
|
|
// ->want system's size for max args
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_ARGMAX;
|
|
|
|
//set size
|
|
size = sizeof(systemMaxArgs);
|
|
|
|
//get system's size for max args
|
|
if(-1 == sysctl(mib, 2, &systemMaxArgs, &size, NULL, 0))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc space for args
|
|
taskArgs = malloc(systemMaxArgs);
|
|
if(NULL == taskArgs)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//init mib
|
|
// ->want process args
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_PROCARGS2;
|
|
mib[2] = pid;
|
|
|
|
//set size
|
|
size = (size_t)systemMaxArgs;
|
|
|
|
//get process's args
|
|
if(-1 == sysctl(mib, 3, taskArgs, &size, NULL, 0))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sanity check
|
|
// ensure buffer is somewhat sane
|
|
if(size <= sizeof(int))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extract number of args
|
|
memcpy(&numberOfArgs, taskArgs, sizeof(numberOfArgs));
|
|
|
|
//extract task's name
|
|
// follows # of args (int) and is NULL-terminated
|
|
processPath = [NSString stringWithUTF8String:taskArgs + sizeof(int)];
|
|
}
|
|
|
|
bail:
|
|
|
|
//free process args
|
|
if(NULL != taskArgs)
|
|
{
|
|
//free
|
|
free(taskArgs);
|
|
|
|
//reset
|
|
taskArgs = NULL;
|
|
}
|
|
|
|
return processPath;
|
|
}
|
|
|
|
//given a process path and user
|
|
// return array of all matching pids
|
|
NSMutableArray* getProcessIDs(NSString* processPath, int userID)
|
|
{
|
|
//status
|
|
int status = -1;
|
|
|
|
//process IDs
|
|
NSMutableArray* processIDs = nil;
|
|
|
|
//# of procs
|
|
int numberOfProcesses = 0;
|
|
|
|
//array of pids
|
|
pid_t* pids = NULL;
|
|
|
|
//process info struct
|
|
struct kinfo_proc procInfo;
|
|
|
|
//size of struct
|
|
size_t procInfoSize = sizeof(procInfo);
|
|
|
|
//mib
|
|
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, -1};
|
|
|
|
//clear buffer
|
|
memset(&procInfo, 0x0, procInfoSize);
|
|
|
|
//get # of procs
|
|
numberOfProcesses = proc_listallpids(NULL, 0);
|
|
if(-1 == numberOfProcesses)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc buffer for pids
|
|
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
|
|
|
|
//alloc
|
|
processIDs = [NSMutableArray array];
|
|
|
|
//get list of pids
|
|
status = proc_listallpids(pids, numberOfProcesses * (int)sizeof(pid_t));
|
|
if(status < 0)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//iterate over all pids
|
|
// ->get name for each process
|
|
for(int i = 0; i < (int)numberOfProcesses; i++)
|
|
{
|
|
//skip blank pids
|
|
if(0 == pids[i])
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//skip if path doesn't match
|
|
if(YES != [processPath isEqualToString:getProcessPath(pids[i])])
|
|
{
|
|
//next
|
|
continue;
|
|
}
|
|
|
|
//need to also match on user?
|
|
// caller can pass in -1 to skip this check
|
|
if(-1 != userID)
|
|
{
|
|
//init mib
|
|
mib[0x3] = pids[i];
|
|
|
|
//make syscall to get proc info for user
|
|
if( (0 != sysctl(mib, 0x4, &procInfo, &procInfoSize, NULL, 0)) ||
|
|
(0 == procInfoSize) )
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//skip if user id doesn't match
|
|
if(userID != (int)procInfo.kp_eproc.e_ucred.cr_uid)
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//got match
|
|
// add to list
|
|
[processIDs addObject:[NSNumber numberWithInt:pids[i]]];
|
|
}
|
|
|
|
bail:
|
|
|
|
//free buffer
|
|
if(NULL != pids)
|
|
{
|
|
//free
|
|
free(pids);
|
|
|
|
//reset
|
|
pids = NULL;
|
|
}
|
|
|
|
return processIDs;
|
|
}
|
|
|
|
//enable/disable a menu
|
|
void toggleMenu(NSMenu* menu, BOOL shouldEnable)
|
|
{
|
|
//disable autoenable
|
|
menu.autoenablesItems = NO;
|
|
|
|
//iterate over
|
|
// set state of each item
|
|
for(NSMenuItem* item in menu.itemArray)
|
|
{
|
|
//set state
|
|
item.enabled = shouldEnable;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//get an icon for a process
|
|
// for apps, this will be app's icon, otherwise just a standard system one
|
|
NSImage* getIconForProcess(NSString* path)
|
|
{
|
|
//icon's file name
|
|
NSString* iconFile = nil;
|
|
|
|
//icon's path
|
|
NSString* iconPath = nil;
|
|
|
|
//icon's path extension
|
|
NSString* iconExtension = nil;
|
|
|
|
//icon
|
|
NSImage* icon = nil;
|
|
|
|
//system's document icon
|
|
static NSImage* documentIcon = nil;
|
|
|
|
//bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//invalid path?
|
|
// grab a default icon and bail
|
|
if(YES != [NSFileManager.defaultManager fileExistsAtPath:path])
|
|
{
|
|
//set icon to system 'application' icon
|
|
icon = [NSWorkspace.sharedWorkspace iconForContentType:[UTType typeWithFilenameExtension:@"app"]];
|
|
|
|
//set size to 64 @2x
|
|
[icon setSize:NSMakeSize(128, 128)];
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//first try grab bundle
|
|
// ->then extact icon from this
|
|
appBundle = findAppBundle(path);
|
|
if(nil != appBundle)
|
|
{
|
|
//extract icon
|
|
icon = [NSWorkspace.sharedWorkspace iconForFile:appBundle.bundlePath];
|
|
if(nil != icon)
|
|
{
|
|
//done!
|
|
goto bail;
|
|
}
|
|
|
|
//get file
|
|
iconFile = appBundle.infoDictionary[@"CFBundleIconFile"];
|
|
if(nil != iconFile)
|
|
{
|
|
//get path extension
|
|
iconExtension = [iconFile pathExtension];
|
|
|
|
//if its blank (i.e. not specified)
|
|
// go with 'icns'
|
|
if(YES == [iconExtension isEqualTo:@""])
|
|
{
|
|
//set type
|
|
iconExtension = @"icns";
|
|
}
|
|
|
|
//set full path
|
|
iconPath = [appBundle pathForResource:[iconFile stringByDeletingPathExtension] ofType:iconExtension];
|
|
|
|
//load it
|
|
icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
|
|
}
|
|
}
|
|
|
|
//process is not an app or couldn't get icon
|
|
// try to get it via shared workspace
|
|
if( (nil == appBundle) ||
|
|
(nil == icon) )
|
|
{
|
|
//extract icon
|
|
icon = [[NSWorkspace sharedWorkspace] iconForFile:path];
|
|
|
|
//load system document icon
|
|
// static var, so only load once
|
|
if(nil == documentIcon)
|
|
{
|
|
//load
|
|
documentIcon = [NSWorkspace.sharedWorkspace iconForContentType:[UTType typeWithFilenameExtension:@"txt"]];
|
|
|
|
}
|
|
|
|
//if 'iconForFile' method doesn't find and icon, it returns the system 'document' icon
|
|
// the system 'application' icon seems more applicable, so use that here...
|
|
if(YES == [icon isEqual:documentIcon])
|
|
{
|
|
//set icon to system 'application' icon
|
|
icon = [NSWorkspace.sharedWorkspace iconForContentType:[UTType typeWithFilenameExtension:@"app"]];
|
|
}
|
|
|
|
//'iconForFileType' returns small icons
|
|
// so set size to 64 @2x
|
|
[icon setSize:NSMakeSize(128, 128)];
|
|
}
|
|
|
|
bail:
|
|
|
|
return icon;
|
|
}
|
|
|
|
//wait till window non-nil
|
|
// then make that window modal
|
|
void makeModal(NSWindowController* windowController)
|
|
{
|
|
//window
|
|
__block NSWindow* window = nil;
|
|
|
|
//wait till non-nil
|
|
// then make window modal
|
|
for(int i=0; i<20; i++)
|
|
{
|
|
//grab window
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//grab
|
|
window = windowController.window;
|
|
|
|
});
|
|
|
|
//nil?
|
|
// nap
|
|
if(nil == window)
|
|
{
|
|
//nap
|
|
[NSThread sleepForTimeInterval:0.05f];
|
|
|
|
//next
|
|
continue;
|
|
}
|
|
|
|
//have window?
|
|
// make it modal
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//modal
|
|
[[NSApplication sharedApplication] runModalForWindow:windowController.window];
|
|
|
|
});
|
|
|
|
//done
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//find a process by name
|
|
pid_t findProcess(NSString* processName)
|
|
{
|
|
//pid
|
|
pid_t processID = 0;
|
|
|
|
//status
|
|
int status = -1;
|
|
|
|
//# of procs
|
|
int numberOfProcesses = 0;
|
|
|
|
//array of pids
|
|
pid_t* pids = NULL;
|
|
|
|
//process path
|
|
NSString* processPath = nil;
|
|
|
|
//get # of procs
|
|
numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
|
if(-1 == numberOfProcesses)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc buffer for pids
|
|
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
|
|
|
|
//get list of pids
|
|
status = proc_listpids(PROC_ALL_PIDS, 0, pids, numberOfProcesses * (int)sizeof(pid_t));
|
|
if(status < 0)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//iterate over all pids
|
|
// get name for each via helper function
|
|
for(int i = 0; i < numberOfProcesses; ++i)
|
|
{
|
|
//skip blank pids
|
|
if(0 == pids[i])
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//get name
|
|
processPath = getProcessPath(pids[i]);
|
|
if( (nil == processPath) ||
|
|
(0 == processPath.length) )
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//match?
|
|
if(YES == [processPath isEqualToString:processName])
|
|
{
|
|
//save
|
|
processID = pids[i];
|
|
|
|
//pau
|
|
break;
|
|
}
|
|
|
|
}//all procs
|
|
|
|
bail:
|
|
|
|
//free buffer
|
|
if(NULL != pids)
|
|
{
|
|
//free
|
|
free(pids);
|
|
}
|
|
|
|
return processID;
|
|
}
|
|
|
|
//for login item enable/disable
|
|
// we use the launch services APIs, since replacements don't always work :(
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
//toggle login item
|
|
// either add (install) or remove (uninstall)
|
|
BOOL toggleLoginItem(NSURL* loginItem, int toggleFlag)
|
|
{
|
|
//flag
|
|
BOOL wasToggled = NO;
|
|
|
|
//flag
|
|
BOOL alreadyAdded = NO;
|
|
|
|
//login item ref
|
|
LSSharedFileListRef loginItemsRef = NULL;
|
|
|
|
//login items
|
|
CFArrayRef loginItems = NULL;
|
|
|
|
//current login item
|
|
CFURLRef currentLoginItem = NULL;
|
|
|
|
//get reference to login items
|
|
loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
|
|
|
|
//add (install)
|
|
if(ACTION_INSTALL_FLAG == toggleFlag)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "adding login item %{public}@", loginItem);
|
|
|
|
//grab existing login items
|
|
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
|
|
|
|
//already added?
|
|
for(id item in (__bridge NSArray *)loginItems)
|
|
{
|
|
//grab current item
|
|
currentLoginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, NULL);
|
|
if(NULL == currentLoginItem) continue;
|
|
|
|
//is match?
|
|
if(YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem])
|
|
{
|
|
//set flag
|
|
alreadyAdded = YES;
|
|
}
|
|
|
|
//release
|
|
CFRelease(currentLoginItem);
|
|
currentLoginItem = NULL;
|
|
|
|
//done?
|
|
if(YES == alreadyAdded) break;
|
|
}
|
|
|
|
//sanity check
|
|
if(YES == alreadyAdded)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "%{public}@ already exists as a login item", loginItem);
|
|
|
|
//happy
|
|
wasToggled = YES;
|
|
|
|
//done
|
|
goto bail;
|
|
|
|
}
|
|
|
|
//add
|
|
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL);
|
|
|
|
//release item ref
|
|
if(NULL != itemRef)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "added %{public}@/%{public}@", loginItem, itemRef);
|
|
|
|
//release
|
|
CFRelease(itemRef);
|
|
|
|
//reset
|
|
itemRef = NULL;
|
|
}
|
|
//failed
|
|
else
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to add login item");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
wasToggled = YES;
|
|
}
|
|
//remove (uninstall)
|
|
else
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "removing login item %@", loginItem);
|
|
|
|
//grab existing login items
|
|
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
|
|
|
|
//iterate over all login items
|
|
// look for self, then remove it
|
|
for(id item in (__bridge NSArray *)loginItems)
|
|
{
|
|
//get current login item
|
|
currentLoginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, NULL);
|
|
if(NULL == currentLoginItem) continue;
|
|
|
|
|
|
//current login item match self?
|
|
if(YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem])
|
|
{
|
|
//remove
|
|
if(noErr != LSSharedFileListItemRemove(loginItemsRef, (__bridge LSSharedFileListItemRef)item))
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to remove login item");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "removed login item: %@", loginItem);
|
|
|
|
//happy
|
|
wasToggled = YES;
|
|
|
|
//all done
|
|
goto bail;
|
|
}
|
|
|
|
//release
|
|
CFRelease(currentLoginItem);
|
|
|
|
//reset
|
|
currentLoginItem = NULL;
|
|
|
|
}//all login items
|
|
|
|
}//remove/uninstall
|
|
|
|
bail:
|
|
|
|
//release login items
|
|
if(NULL != loginItems)
|
|
{
|
|
//release
|
|
CFRelease(loginItems);
|
|
|
|
//reset
|
|
loginItems = NULL;
|
|
}
|
|
|
|
//release login ref
|
|
if(NULL != loginItemsRef)
|
|
{
|
|
//release
|
|
CFRelease(loginItemsRef);
|
|
|
|
//reset
|
|
loginItemsRef = NULL;
|
|
}
|
|
|
|
//release url
|
|
if(NULL != currentLoginItem)
|
|
{
|
|
//release
|
|
CFRelease(currentLoginItem);
|
|
currentLoginItem = NULL;
|
|
}
|
|
|
|
return wasToggled;
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
//check if process is alive
|
|
BOOL isProcessAlive(pid_t processID)
|
|
{
|
|
//ret var
|
|
BOOL bIsAlive = NO;
|
|
|
|
//signal status
|
|
int signalStatus = -1;
|
|
|
|
//send kill with 0 to determine if alive
|
|
signalStatus = kill(processID, 0);
|
|
|
|
//is alive?
|
|
if( (0 == signalStatus) ||
|
|
((0 != signalStatus) && (errno != ESRCH)) )
|
|
{
|
|
//alive!
|
|
bIsAlive = YES;
|
|
}
|
|
|
|
return bIsAlive;
|
|
}
|
|
|
|
|
|
//hash a file
|
|
NSMutableString* hashFile(NSString* filePath)
|
|
{
|
|
//file's contents
|
|
NSData* fileContents = nil;
|
|
|
|
//hash digest
|
|
uint8_t digestSHA256[CC_SHA256_DIGEST_LENGTH] = {0};
|
|
|
|
//hash as string
|
|
NSMutableString* sha256 = nil;
|
|
|
|
//index var
|
|
NSUInteger index = 0;
|
|
|
|
//init
|
|
sha256 = [NSMutableString string];
|
|
|
|
//load file
|
|
if(nil == (fileContents = [NSData dataWithContentsOfFile:filePath]))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sha256 it
|
|
CC_SHA256(fileContents.bytes, (unsigned int)fileContents.length, digestSHA256);
|
|
|
|
//convert to NSString
|
|
// iterate over each bytes in computed digest and format
|
|
for(index=0; index < CC_SHA256_DIGEST_LENGTH; index++)
|
|
{
|
|
//format/append
|
|
[sha256 appendFormat:@"%02lX", (unsigned long)digestSHA256[index]];
|
|
}
|
|
|
|
bail:
|
|
|
|
return sha256;
|
|
}
|
|
|
|
//exec a process with args
|
|
// if 'shouldWait' is set, wait and return stdout/in and termination status
|
|
NSMutableDictionary* execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait, BOOL grabOutput)
|
|
{
|
|
//task
|
|
NSTask* task = nil;
|
|
|
|
//output pipe for stdout
|
|
NSPipe* stdOutPipe = nil;
|
|
|
|
//output pipe for stderr
|
|
NSPipe* stdErrPipe = nil;
|
|
|
|
//read handle for stdout
|
|
NSFileHandle* stdOutReadHandle = nil;
|
|
|
|
//read handle for stderr
|
|
NSFileHandle* stdErrReadHandle = nil;
|
|
|
|
//results dictionary
|
|
NSMutableDictionary* results = nil;
|
|
|
|
//output for stdout
|
|
NSMutableData *stdOutData = nil;
|
|
|
|
//output for stderr
|
|
NSMutableData *stdErrData = nil;
|
|
|
|
//init dictionary for results
|
|
results = [NSMutableDictionary dictionary];
|
|
|
|
//init task
|
|
task = [[NSTask alloc] init];
|
|
|
|
//sanity check
|
|
// NSTask throws if path isn't found...
|
|
if(YES != [NSFileManager.defaultManager fileExistsAtPath:binaryPath])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: %{public}@ not found", binaryPath);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//only setup pipes if wait flag is set
|
|
if(YES == grabOutput)
|
|
{
|
|
//init stdout pipe
|
|
stdOutPipe = [NSPipe pipe];
|
|
|
|
//init stderr pipe
|
|
stdErrPipe = [NSPipe pipe];
|
|
|
|
//init stdout read handle
|
|
stdOutReadHandle = [stdOutPipe fileHandleForReading];
|
|
|
|
//init stderr read handle
|
|
stdErrReadHandle = [stdErrPipe fileHandleForReading];
|
|
|
|
//init stdout output buffer
|
|
stdOutData = [NSMutableData data];
|
|
|
|
//init stderr output buffer
|
|
stdErrData = [NSMutableData data];
|
|
|
|
//set task's stdout
|
|
task.standardOutput = stdOutPipe;
|
|
|
|
//set task's stderr
|
|
task.standardError = stdErrPipe;
|
|
}
|
|
|
|
//set task's path
|
|
task.launchPath = binaryPath;
|
|
|
|
//set task's args
|
|
if(nil != arguments)
|
|
{
|
|
//set
|
|
task.arguments = arguments;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "execing task %{public}@ (arguments: %{public}@)", task.launchPath, task.arguments);
|
|
|
|
//wrap task launch
|
|
@try
|
|
{
|
|
//launch
|
|
[task launch];
|
|
}
|
|
@catch(NSException *exception)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "failed to launch task (%{public}@)", exception);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//no need to wait
|
|
// can just bail w/ no output
|
|
if( (YES != shouldWait) &&
|
|
(YES != grabOutput) )
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//wait
|
|
// ...but no output
|
|
else if( (YES == shouldWait) &&
|
|
(YES != grabOutput) )
|
|
{
|
|
//wait
|
|
[task waitUntilExit];
|
|
|
|
//add exit code
|
|
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//grab output?
|
|
// even if wait not set, still will wait!
|
|
else
|
|
{
|
|
//read in stdout/stderr
|
|
while(YES == [task isRunning])
|
|
{
|
|
//accumulate stdout
|
|
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
|
|
|
//accumulate stderr
|
|
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
|
}
|
|
|
|
//grab any leftover stdout
|
|
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
|
|
|
//grab any leftover stderr
|
|
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
|
|
|
//add stdout
|
|
if(0 != stdOutData.length)
|
|
{
|
|
//add
|
|
results[STDOUT] = stdOutData;
|
|
}
|
|
|
|
//add stderr
|
|
if(0 != stdErrData.length)
|
|
{
|
|
//add
|
|
results[STDERR] = stdErrData;
|
|
}
|
|
|
|
//add exit code
|
|
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
|
}
|
|
|
|
bail:
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "task completed with %{public}@", results);
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
//loads a framework
|
|
// note: assumes it is in 'Framework' dir
|
|
NSBundle* loadFramework(NSString* name)
|
|
{
|
|
//handle
|
|
NSBundle* framework = nil;
|
|
|
|
//framework path
|
|
NSString* path = nil;
|
|
|
|
//init path
|
|
path = [NSString stringWithFormat:@"%@/../Frameworks/%@", [NSProcessInfo.processInfo.arguments[0] stringByDeletingLastPathComponent], name];
|
|
|
|
//standardize path
|
|
path = [path stringByStandardizingPath];
|
|
|
|
//init framework (bundle)
|
|
framework = [NSBundle bundleWithPath:path];
|
|
if(NULL == framework)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//load framework
|
|
if(YES != [framework loadAndReturnError:nil])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return framework;
|
|
}
|
|
|
|
//check if a file is restricted (SIP)
|
|
BOOL isFileRestricted(NSString* file)
|
|
{
|
|
//flag
|
|
BOOL restricted = NO;
|
|
|
|
//info
|
|
struct stat info = {0};
|
|
|
|
//clear
|
|
memset(&info, 0x0, sizeof(struct stat));
|
|
|
|
//get file info
|
|
if(0 == lstat(file.UTF8String, &info))
|
|
{
|
|
//check flags
|
|
restricted = (BOOL)(info.st_flags & SF_RESTRICTED);
|
|
}
|
|
|
|
return restricted;
|
|
}
|
|
|
|
//in dark mode?
|
|
BOOL isDarkMode(void)
|
|
{
|
|
return [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"];
|
|
}
|
|
|
|
|
|
//show an alert
|
|
NSModalResponse showAlert(NSString* messageText, NSString* informativeText, NSString* buttonTitle)
|
|
{
|
|
//alert
|
|
NSAlert* alert = nil;
|
|
|
|
//response
|
|
NSModalResponse response = 0;
|
|
|
|
//init alert
|
|
alert = [[NSAlert alloc] init];
|
|
|
|
//set style
|
|
alert.alertStyle = NSAlertStyleWarning;
|
|
|
|
//main text
|
|
alert.messageText = messageText;
|
|
|
|
//add details
|
|
if(nil != informativeText)
|
|
{
|
|
//details
|
|
alert.informativeText = informativeText;
|
|
}
|
|
|
|
//add button
|
|
[alert addButtonWithTitle:buttonTitle];
|
|
|
|
//make app active
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
|
|
//show
|
|
response = [alert runModal];
|
|
|
|
return response;
|
|
}
|
|
|
|
//checks if user has admin privs
|
|
// based off http://stackoverflow.com/questions/30000443/asking-for-admin-privileges-for-only-standard-accounts
|
|
BOOL hasAdminPrivileges(void)
|
|
{
|
|
//flag
|
|
BOOL isAdmin = NO;
|
|
|
|
//password entry
|
|
struct passwd* pwentry = NULL;
|
|
|
|
//admin group
|
|
struct group* adminGroup = NULL;
|
|
|
|
//get password entry for console user
|
|
pwentry = getpwuid(getConsoleUserID());
|
|
|
|
//get admin group
|
|
adminGroup = getgrnam("admin");
|
|
|
|
//iterate over entries
|
|
// ->check if current user is part of the admin group
|
|
while(*adminGroup->gr_mem != NULL)
|
|
{
|
|
//check if admin
|
|
if (strcmp(pwentry->pw_name, *adminGroup->gr_mem) == 0)
|
|
{
|
|
//yay!
|
|
isAdmin = YES;
|
|
|
|
//exit loop
|
|
break;
|
|
}
|
|
|
|
//try next
|
|
adminGroup->gr_mem++;
|
|
}
|
|
|
|
return isAdmin;
|
|
}
|
|
|
|
//get Do Not Distrub state
|
|
BOOL DNDState(void)
|
|
{
|
|
//default
|
|
NSUserDefaults* defaults = nil;
|
|
|
|
//load defaults
|
|
defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.notificationcenterui"];
|
|
|
|
//return status of DND
|
|
return [defaults boolForKey:@"doNotDisturb"];
|
|
|
|
}
|