OverSight/LoginItem/AVMonitor.m

1814 lines
52 KiB
Objective-C

//
// AVMonitor.m
// OverSight
//
// Created by Patrick Wardle on 9/01/16.
// Copyright (c) 2015 Objective-See. All rights reserved.
//
#import "Consts.h"
#import "Logging.h"
#import "Utilities.h"
#import "AVMonitor.h"
#import "AppDelegate.h"
#import "../Shared/XPCProtocol.h"
@implementation AVMonitor
@synthesize mic;
@synthesize camera;
@synthesize lastEvent;
@synthesize whiteList;
@synthesize audioActive;
@synthesize rememberPopups;
@synthesize activationAlerts;
@synthesize lastNotification;
@synthesize videoMonitorThread;
//init
-(id)init
{
//init super
self = [super init];
if(nil != self)
{
//alloc
activationAlerts = [NSMutableDictionary dictionary];
//alloc
rememberPopups = [NSMutableArray array];
//load whitelist
[self loadWhitelist];
}
return self;
}
//load whitelist
-(void)loadWhitelist
{
//path
NSString* path = nil;
//init path
path = [[[@"~" stringByAppendingPathComponent:APP_SUPPORT_DIRECTORY] stringByExpandingTildeInPath] stringByAppendingPathComponent:FILE_WHITELIST];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"whitelist path %@", path]);
#endif
//check if it exists
if(YES != [[NSFileManager defaultManager] fileExistsAtPath:path])
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"nothing whitelisted yet, so won't load (file not found)");
#endif
//bail
goto bail;
}
//since file is created by priv'd XPC, it shouldn't be writeable
// ...unless somebody maliciously creates it, so we check that here
if(YES == [[NSFileManager defaultManager] isWritableFileAtPath:path])
{
//err msg
logMsg(LOG_ERR, @"whitelist is writable, so ignoring!");
//bail
goto bail;
}
//load
self.whiteList = [NSMutableArray arrayWithContentsOfFile:path];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"whitelist: %@", self.whiteList]);
#endif
//bail
bail:
return;
}
//grab first apple camera, or default
// ->saves into iVar 'camera'
-(void)findAppleCamera
{
//cameras
NSArray* cameras = nil;
//grab all cameras
cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"cameras: %@", cameras]);
#endif
//look for camera that belongs to apple
for(AVCaptureDevice* currentCamera in cameras)
{
//check if apple
if(YES == [currentCamera.manufacturer isEqualToString:@"Apple Inc."])
{
//save
self.camera = currentCamera;
//exit loop
break;
}
}
//didn't find apple
// ->grab default camera
if(nil == self.camera)
{
//get default
self.camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"didn't find apple camera, grabbed default: %@", self.camera]);
#endif
}
return;
}
//grab first apple mic
// ->saves into iVar 'mic'
-(void)findAppleMic
{
//mics
NSArray* mics = nil;
//grab all mics
mics = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"mics: %@", mics]);
#endif
//look for mic that belongs to apple
for(AVCaptureDevice* currentMic in mics)
{
//check if apple
// ->also check input source
if( (YES == [currentMic.manufacturer isEqualToString:@"Apple Inc."]) &&
(YES == [[[currentMic activeInputSource] inputSourceID] isEqualToString:@"imic"]) )
{
//save
self.mic = currentMic;
//exit loop
break;
}
}
//didn't find apple
// ->grab default camera
if(nil == self.mic)
{
//get default
self.mic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"didn't find apple 'imic', grabbed default: %@", self.mic]);
#endif
}
return;
}
//initialiaze AV notifcations/callbacks
-(BOOL)monitor
{
//return var
BOOL bRet = NO;
//status/err var
BOOL wasErrors = NO;
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//device's connection id
unsigned int connectionID = 0;
//selector for getting device id
SEL methodSelector = nil;
//array for devices + status
NSMutableArray* devices = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
//alloc device array
devices = [NSMutableArray array];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
//resume
[xpcConnection resume];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"telling XPC service to begin base-lining mach messages");
#endif
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//XPC service to begin baselining mach messages
// ->wait, since want this to compelete before doing other things!
[[xpcConnection remoteObjectProxy] initialize:^
{
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
//init selector
methodSelector = NSSelectorFromString(@"connectionID");
//find (first) apple camera
// ->saves camera into iVar, 'camera'
[self findAppleCamera];
//find (first) apple mic
// ->saves mic into iVar, 'mic'
[self findAppleMic];
//got camera
// ->grab connection ID and invoke helper functions
if( (nil != self.camera) &&
(YES == [self.camera respondsToSelector:methodSelector]) )
{
//ignore leak warning
// ->we know what we're doing via this 'performSelector'
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//grab connection ID
connectionID = (unsigned int)[self.camera performSelector:methodSelector withObject:nil];
//restore
#pragma clang diagnostic pop
//set status
// ->will set 'videoActive' iVar
[self setVideoDevStatus:connectionID];
//if video is already active
// ->set status & start monitoring thread
if(YES == self.videoActive)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"video already active, so will start polling for new video procs");
#endif
//tell XPC video is active
[[xpcConnection remoteObjectProxy] updateVideoStatus:self.videoActive reply:^{
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
//alloc
videoMonitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitor4Procs) object:nil];
//start
[self.videoMonitorThread start];
}
//save camera/status into device array
[devices addObject:@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)}];
//register for video events
if(YES != [self watchVideo:connectionID])
{
//err msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"failed to watch for video events");
#endif
//set err
wasErrors = YES;
//don't bail
// ->can still listen for audio events
}
}
//err msg
else
{
//err msg
logMsg(LOG_ERR, @"failed to find (apple) camera :(");
//don't bail
// ->can still listen for audio events
}
//watch mic
// ->grab connection ID and invoke helper function
if( (nil != self.mic) &&
(YES == [self.mic respondsToSelector:methodSelector]) )
{
//ignore leak warning
// ->we know what we're doing via this 'performSelector'
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//grab connection ID
connectionID = (unsigned int)[self.mic performSelector:NSSelectorFromString(@"connectionID") withObject:nil];
//restore
#pragma clang diagnostic pop
//set status
// ->will set 'videoActive' iVar
[self setAudioDevStatus:connectionID];
//if audio is already active
// ->tell XPC that it's active
// TODO: monitor for piggybacking?
if(YES == self.audioActive)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"audio already active");
#endif
//tell XPC audio is active
[[xpcConnection remoteObjectProxy] updateAudioStatus:self.audioActive reply:^{
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
//save mic/status into device array
[devices addObject:@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)}];
//register for audio events
if(YES != [self watchAudio:connectionID])
{
//err msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"failed to watch for audio events");
#endif
//set err
wasErrors = YES;
}
}
//err msg
else
{
//err msg
logMsg(LOG_ERR, @"failed to find (apple) mic :(");
}
//send msg to status menu
// ->update menu to show devices & their status
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:devices];
//make sure no errors occured
if(YES != wasErrors)
{
//happy
bRet = YES;
}
//cleanup XPC
if(nil != xpcConnection)
{
//close connection
[xpcConnection invalidate];
}
return bRet;
}
//determine if video is active
// ->sets 'videoActive' iVar
-(void)setVideoDevStatus:(CMIODeviceID)deviceID
{
//status var
OSStatus status = -1;
//running flag
UInt32 isRunning = -1;
//size of query flag
UInt32 propertySize = 0;
//property address struct
CMIOObjectPropertyAddress propertyStruct = {0};
//init size
propertySize = sizeof(isRunning);
//init property struct's selector
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
//init property struct's scope
propertyStruct.mScope = kCMIOObjectPropertyScopeGlobal;
//init property struct's element
propertyStruct.mElement = 0;
//query to get 'kAudioDevicePropertyDeviceIsRunningSomewhere' status
status = CMIOObjectGetPropertyData(deviceID, &propertyStruct, 0, NULL, sizeof(kAudioDevicePropertyDeviceIsRunningSomewhere), &propertySize, &isRunning);
if(noErr != status)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"getting status of video device failed with %d", status]);
//set error
isRunning = -1;
//bail
goto bail;
}
//set iVar
self.videoActive = isRunning;
//bail
bail:
return;
}
//helper function
// determines if video went active/inactive then invokes notification generator method
-(void)handleVideoNotification:(CMIOObjectID)deviceID addresses:(const CMIOObjectPropertyAddress[]) addresses
{
//event dictionary
NSMutableDictionary* event = nil;
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//devices
NSMutableArray* devices = nil;
//init dictionary for event
event = [NSMutableDictionary dictionary];
//init array for devices
devices = [NSMutableArray array];
//sync
@synchronized (self)
{
//set status
// ->sets 'videoActive' iVar
[self setVideoDevStatus:deviceID];
//add camera
if(nil != self.camera)
{
//add
[devices addObject:@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)}];
}
//add mic
if(nil != self.mic)
{
//add
[devices addObject:@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)}];
}
//update status menu
// run on main thread, since its a UI update
dispatch_async(dispatch_get_main_queue(), ^{
//update status menu
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:devices];
});
//add timestamp
event[EVENT_TIMESTAMP] = [NSDate date];
//add device
event[EVENT_DEVICE] = self.camera;
//set device status
event[EVENT_DEVICE_STATUS] = [NSNumber numberWithInt:self.videoActive];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"got video change notification; is running? %x", self.videoActive]);
#endif
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//tell XPC about video status
// ->for example, when video is active, will stop baselining
[[xpcConnection remoteObjectProxy] updateVideoStatus:self.videoActive reply:^{
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
//if video just started
// ->ask for video procs from XPC
if(YES == self.videoActive)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"video is active, so querying XPC to get video process(s)");
#endif
//set allowed classes
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSNumber class], nil]
forSelector: @selector(getVideoProcs:reply:) argumentIndex: 0 ofReply: YES];
//invoke XPC service
[[xpcConnection remoteObjectProxy] getVideoProcs:NO reply:^(NSMutableArray* videoProcesses)
{
//close connection
[xpcConnection invalidate];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"video procs from XPC: %@", videoProcesses]);
#endif
//generate notification for each process
for(NSNumber* processID in videoProcesses)
{
//set pid
event[EVENT_PROCESS_ID] = processID;
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
//if no consumer process was found
// ->still alert user that webcam was activated, but without details/ability to block
if(0 == videoProcesses.count)
{
//set pid
event[EVENT_PROCESS_ID] = @0;
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
//video deactivated
// ->close XPC connection and alert user
else
{
//close connection
[xpcConnection invalidate];
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
//poll for new video procs
// ->this thread will exit itself as its checks the 'videoActive' iVar
if(YES == self.videoActive)
{
//start monitor thread if needed
if(YES != videoMonitorThread.isExecuting)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"(re)Starting polling/monitor thread");
#endif
//alloc
videoMonitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitor4Procs) object:nil];
//start
[self.videoMonitorThread start];
}
//no need to restart
else
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"polling/monitor thread still running");
#endif
}
}
}//sync
//bail
bail:
return;
}
//register for video notifcations
// ->block will invoke method on event
-(BOOL)watchVideo:(CMIOObjectID)deviceID
{
//ret var
BOOL bRegistered = NO;
//status var
OSStatus status = -1;
//property struct
CMIOObjectPropertyAddress propertyStruct = {0};
//init property struct's selector
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
//init property struct's scope
propertyStruct.mScope = kAudioObjectPropertyScopeGlobal;
//init property struct's element
propertyStruct.mElement = kAudioObjectPropertyElementMaster;
//block
// ->invoked when video changes & just calls helper function
CMIOObjectPropertyListenerBlock listenerBlock = ^(UInt32 inNumberAddresses, const CMIOObjectPropertyAddress addresses[])
{
//on main thread?
// handle on background thread
if(YES == [NSThread isMainThread])
{
//invoke in background
// XPC stuff might be slow!
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
//handle notification
[self handleVideoNotification:deviceID addresses:addresses];
});
}
//on background thread
// just can invoke as it
else
{
//handle notification
[self handleVideoNotification:deviceID addresses:addresses];
}
};
//register (add) property block listener
status = CMIOObjectAddPropertyListenerBlock(deviceID, &propertyStruct, dispatch_get_main_queue(), listenerBlock);
if(noErr != status)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"CMIOObjectAddPropertyListenerBlock() failed with %d", status]);
//bail
goto bail;
}
//happy
bRegistered = YES;
//bail
bail:
return bRegistered;
}
//determine if audio is active
// ->sets 'audioActive' iVar
-(void)setAudioDevStatus:(AudioObjectID)deviceID
{
//status var
OSStatus status = -1;
//running flag
UInt32 isRunning = -1;
//size of query flag
UInt32 propertySize = 0;
//init size
propertySize = sizeof(isRunning);
//query to get 'kAudioDevicePropertyDeviceIsRunningSomewhere' status
status = AudioDeviceGetProperty(deviceID, 0, false, kAudioDevicePropertyDeviceIsRunningSomewhere, &propertySize, &isRunning);
if(noErr != status)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"getting status of audio device failed with %d", status]);
//set error
isRunning = -1;
//bail
goto bail;
}
//set iVar
self.audioActive = isRunning;
//bail
bail:
return;
}
//helper function
// ->determines if audio went active/inactive then invokes notification generator method
-(void)handleAudioNotification:(AudioObjectID)deviceID
{
//event dictionary
NSMutableDictionary* event = nil;
//devices
NSMutableArray* devices = nil;
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//init dictionary
event = [NSMutableDictionary dictionary];
//init array for devices
devices = [NSMutableArray array];
//sync
@synchronized (self)
{
//set status
// ->updates 'audioActive' iVar
[self setAudioDevStatus:deviceID];
//add camera
if(nil != self.camera)
{
//add
[devices addObject:@{EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:@(self.videoActive)}];
}
//add mic
if(nil != self.mic)
{
//add
[devices addObject:@{EVENT_DEVICE:self.mic, EVENT_DEVICE_STATUS:@(self.audioActive)}];
}
//update status menu
// run on main thread, since its a UI update
dispatch_async(dispatch_get_main_queue(), ^{
//update status menu
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarMenuController updateStatusItemMenu:devices];
});
//add timestamp
event[EVENT_TIMESTAMP] = [NSDate date];
//add device
event[EVENT_DEVICE] = self.mic;
//set device status
event[EVENT_DEVICE_STATUS] = [NSNumber numberWithInt:self.audioActive];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"got audio change notification; is running? %x", self.audioActive]);
#endif
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//tell XPC about audio status
// ->for example, when audio is active, will stop baselining
[[xpcConnection remoteObjectProxy] updateAudioStatus:self.audioActive reply:^{
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
//if audio just started
// ->ask for audio procs from XPC
if(YES == self.audioActive)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"audio is active, so querying XPC to get audio process(s)");
#endif
//set allowed classes
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSNumber class], nil]
forSelector: @selector(getAudioProcs:) argumentIndex: 0 ofReply: YES];
//invoke XPC service
[[xpcConnection remoteObjectProxy] getAudioProcs:^(NSMutableArray* audioProcesses)
{
//close connection
[xpcConnection invalidate];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"audio procs from XPC: %@", audioProcesses]);
#endif
//generate notification for each process
for(NSNumber* processID in audioProcesses)
{
//set pid
event[EVENT_PROCESS_ID] = processID;
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
//if no consumer process was found
// ->still alert user that webcam was activated, but without details/ability to block
if(0 == audioProcesses.count)
{
//set pid
event[EVENT_PROCESS_ID] = @0;
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
//audio deactivated
// ->close XPC connection and alert user
else
{
//close connection
[xpcConnection invalidate];
//generate notification
// do on main thread since its a UI event
dispatch_async(dispatch_get_main_queue(), ^{
//generate notification
[self generateNotification:event];
});
}
}//sync
//bail
bail:
return;
}
//register for audio notifcations
// ->block will invoke method on event
-(BOOL)watchAudio:(AudioObjectID)deviceID
{
//ret var
BOOL bRegistered = NO;
//status var
OSStatus status = -1;
//property struct
AudioObjectPropertyAddress propertyStruct = {0};
//init property struct's selector
propertyStruct.mSelector = kAudioDevicePropertyDeviceIsRunningSomewhere;
//init property struct's scope
propertyStruct.mScope = kAudioObjectPropertyScopeGlobal;
//init property struct's element
propertyStruct.mElement = kAudioObjectPropertyElementMaster;
//block
// ->invoked when audio changes & just calls helper function
AudioObjectPropertyListenerBlock listenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses)
{
//on main thread?
// handle on background thread
if(YES == [NSThread isMainThread])
{
//invoke in background
// XPC stuff might be slow!
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
//handle notification
[self handleAudioNotification:deviceID];
});
}
//on background thread
// just can invoke as it
else
{
//handle notification
[self handleAudioNotification:deviceID];
}
};
//add property listener for audio changes
status = AudioObjectAddPropertyListenerBlock(deviceID, &propertyStruct, dispatch_get_main_queue(), listenerBlock);
if(noErr != status)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"AudioObjectAddPropertyListenerBlock() failed with %d", status]);
//bail
goto bail;
}
//happy
bRegistered = YES;
//bail
bail:
return bRegistered;
}
//build and display notification
// ->handles extra logic like ignore whitelisted apps, disable alerts (if user has turned that off), etc
-(void)generateNotification:(NSMutableDictionary*)event
{
//pool
@autoreleasepool {
//notification
NSUserNotification* notification = nil;
//device
// ->audio or video
NSNumber* deviceType = nil;
//title
NSMutableString* title = nil;
//details
// ->just name of device for now
NSString* details = nil;
//process name
NSString* processName = nil;
//process path
NSString* processPath = nil;
//log msg
NSMutableString* logMessage = nil;
//preferences
NSDictionary* preferences = nil;
//flag
// ->was an activation alert shown
BOOL matchingActivationAlert = NO;
//flag
// ->is the activation alert still visible
BOOL activationAlertVisible = NO;
//alloc notificaiton
notification = [[NSUserNotification alloc] init];
//alloc title
title = [NSMutableString string];
//alloc log msg
logMessage = [NSMutableString string];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"generating notification for %@", event]);
#endif
//get process name
processName = getProcessName([event[EVENT_PROCESS_ID] intValue]);
//get process path
processPath = getProcessPath([event[EVENT_PROCESS_ID] intValue]);
if(nil == processPath)
{
//set to something
processPath = PROCESS_UNKNOWN;
}
//set device and title for audio
if(YES == [event[EVENT_DEVICE] isKindOfClass:NSClassFromString(@"AVCaptureHALDevice")])
{
//add
[title appendString:@"Audio Device"];
//set device
deviceType = SOURCE_AUDIO;
}
//set device and title for video
else
{
//add
[title appendString:@"Video Device"];
//set device
deviceType = SOURCE_VIDEO;
}
//ignore whitelisted processes
// ->for activation events, can check process path
if(YES == [DEVICE_ACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
{
//check each
// ->need match on path and device (camera || mic)
for(NSDictionary* item in self.whiteList)
{
//check path & device
if( (YES == [item[EVENT_PROCESS_PATH] isEqualToString:processPath]) &&
([item[EVENT_DEVICE] intValue] == deviceType.intValue) )
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"activation alert for process %@ is whitelisted, so ignoring", item]);
#endif
//bail
goto bail;
}
}
}
//ignore whitelisted processes
// ->for deactivation, ignore when no activation alert was shown (cuz process will have likely died, so no pid/path, etc)
if(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
{
//any active alerts?
// ->note: we make to check if the type matches
for(NSUUID* key in self.activationAlerts.allKeys)
{
//same type?
if(event[EVENT_DEVICE] == self.activationAlerts[key][EVENT_DEVICE])
{
//got match
matchingActivationAlert = YES;
//no need to check more
break;
}
}
//no match?
// ->bail, as means process was whitelisted so no activation was shown
if(YES != matchingActivationAlert)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"deactivation alert doesn't have an activation alert (whitelisted?), so ignoring");
#endif
//bail
goto bail;
}
}
//check if event is essentially a duplicate (facetime, etc)
if(nil != self.lastEvent)
{
//less than 10 second ago?
if(fabs([self.lastEvent[EVENT_TIMESTAMP] timeIntervalSinceDate:event[EVENT_TIMESTAMP]]) < 10)
{
//same process/device/action
if( (YES == [self.lastEvent[EVENT_PROCESS_ID] isEqual:event[EVENT_PROCESS_ID]]) &&
(YES == [self.lastEvent[EVENT_DEVICE] isEqual:event[EVENT_DEVICE]]) &&
(YES == [self.lastEvent[EVENT_DEVICE_STATUS] isEqual:event[EVENT_DEVICE_STATUS]]) )
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"alert for %@ would be same as previous (%@), so ignoring", event, self.lastEvent]);
#endif
//update
self.lastEvent = event;
//bail to ignore
goto bail;
}
}
}//'same' event check
//update last event
self.lastEvent = event;
//always (manually) load preferences
preferences = [NSDictionary dictionaryWithContentsOfFile:[APP_PREFERENCES stringByExpandingTildeInPath]];
//check if user wants to ingnore inactive alerts
if( (YES == [preferences[PREF_DISABLE_INACTIVE] boolValue]) &&
(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]]) )
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"user has decided to ingore 'inactive' events, so ingoring A/V going to disable state");
#endif
//bail
goto bail;
}
//add action
// ->device went inactive
if(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
{
//add
[title appendString:@" became inactive"];
}
//add action
// ->device went active
else
{
//add
[title appendString:@" became active"];
}
//set details
// ->name of device
details = ((AVCaptureDevice*)event[EVENT_DEVICE]).localizedName;
//ALERT 1:
// ->activate alert with lots of info
if( (YES == [DEVICE_ACTIVE isEqual:event[EVENT_DEVICE_STATUS]]) &&
(0 != [event[EVENT_PROCESS_ID] intValue]) )
{
//set other button title
notification.otherButtonTitle = @"allow";
//set action title
notification.actionButtonTitle = @"block";
//remove action button
notification.hasActionButton = YES;
//set pid/name/device into user info
// ->allows code to whitelist proc and/or kill proc (later) if user clicks 'block'
notification.userInfo = @{EVENT_PROCESS_ID:event[EVENT_PROCESS_ID], EVENT_PROCESS_PATH:processPath, EVENT_PROCESS_NAME:processName, EVENT_DEVICE:deviceType, EVENT_ALERT_TYPE:ALERT_ACTIVATE};
//set details
// ->name of process using it / icon too?
[notification setInformativeText:[NSString stringWithFormat:@"process: %@ (%@)", processName, event[EVENT_PROCESS_ID]]];
}
//ALERT 2:
// ->activate alert, with no process info
else if( (YES == [DEVICE_ACTIVE isEqual:event[EVENT_DEVICE_STATUS]]) &&
(0 == [event[EVENT_PROCESS_ID] intValue]) )
{
//set other button title
notification.otherButtonTitle = @"ok";
//remove action button
notification.hasActionButton = NO;
//set type
notification.userInfo = @{EVENT_DEVICE:deviceType, EVENT_ALERT_TYPE:ALERT_ACTIVATE};
}
//ALERT 3:
// ->inactive alert, with no process info
else
{
//set other button title
notification.otherButtonTitle = @"ok";
//remove action button
notification.hasActionButton = NO;
//set type
notification.userInfo = @{EVENT_DEVICE:deviceType, EVENT_ALERT_TYPE:ALERT_INACTIVE};
}
//log event?
if(YES == [preferences[PREF_LOG_ACTIVITY] boolValue])
{
//no process?
// ->just add title / details
if( (nil == processName) ||
(YES == [processName isEqualToString:PROCESS_UNKNOWN]) )
{
//add
[logMessage appendFormat:@"%@ (%@)", title, details];
}
//process
// ->add title / details / process name / process path
else
{
//add
[logMessage appendFormat:@"%@ (%@, process: %@, %@)", title, details, processName, processPath];
}
//log msg
logMsg(LOG_DEBUG|LOG_TO_FILE, logMessage);
}
//set title
[notification setTitle:title];
//set subtitle
[notification setSubtitle:details];
//set id
notification.identifier = [[NSUUID UUID] UUIDString];
//set notification
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
//deliver notification
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
//save displayed activation alerts
if(YES == [DEVICE_ACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
{
//save
self.activationAlerts[notification.identifier] = event;
}
//for 'went inactive' notification automatically close
// ->unless there still is an active alert on the screen
if(YES == [DEVICE_INACTIVE isEqual:event[EVENT_DEVICE_STATUS]])
{
//any active alerts still visible?
// ->note: we make to check if the type matches and still visible
for(NSUUID* key in self.activationAlerts.allKeys)
{
//same type
if( (event[EVENT_DEVICE] == self.activationAlerts[key][EVENT_DEVICE]) &&
(YES != [self.activationAlerts[key][EVENT_ALERT_CLOSED] boolValue]) )
{
//set flag
activationAlertVisible = YES;
//no need to check more
break;
}
}
//auto close if activation alert isn't still visible
if(YES != activationAlertVisible)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"automatically closing deactivation alert");
#endif
//delay, then close
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
//close
[NSUserNotificationCenter.defaultUserNotificationCenter removeDeliveredNotification:notification];
});
}
}
}//pool
//bail
bail:
return;
}
//always present notifications
-(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
return YES;
}
//automatically invoked when user interacts w/ the notification popup
// ->handle rule creation, blocking/killing proc, etc
-(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
//pool
@autoreleasepool {
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//process id
NSNumber* processID = nil;
//device type
NSNumber* deviceType = nil;
//preferences
NSDictionary* preferences = nil;
//log msg
NSMutableString* logMessage = nil;
//remember popup/window controller
RememberWindowController* rememberWindowController = nil;
//always (manually) load preferences
preferences = [NSDictionary dictionaryWithContentsOfFile:[APP_PREFERENCES stringByExpandingTildeInPath]];
//alloc log msg
logMessage = [NSMutableString string];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"user responded to notification: %@", notification]);
#endif
//ignore if this notification was already seen
// ->need this logic, since have to determine if 'allow' was invoke indirectly
if(nil != self.lastNotification)
{
//same?
if(YES == [self.lastNotification isEqualToString:notification.identifier])
{
//update
self.lastNotification = notification.identifier;
//ignore
goto bail;
}
}
//when it's a deactivation alert
// ->remove all activation alerts that match the same type
if(ALERT_INACTIVE.intValue == [notification.userInfo[EVENT_ALERT_TYPE] intValue])
{
//remove any active alerts
// ->note: we make sure to check if the type matches
for(NSUUID* key in self.activationAlerts.allKeys)
{
//check stored activation alert type
// ->audio
if(YES == [self.activationAlerts[key][EVENT_DEVICE] isKindOfClass:NSClassFromString(@"AVCaptureHALDevice")])
{
//set device
deviceType = SOURCE_AUDIO;
}
//check stored activation alert type
// ->video
else
{
//set device
deviceType = SOURCE_VIDEO;
}
//same type?
if([notification.userInfo[EVENT_DEVICE] intValue] == deviceType.intValue)
{
//remove
[self.activationAlerts removeObjectForKey:key];
}
}
}
//activation alert that user has interacted with
// ->this means it's going to close, to we indicate that
else
{
//set flag
self.activationAlerts[notification.identifier][EVENT_ALERT_CLOSED] = @YES;
}
//update
self.lastNotification = notification.identifier;
//for alerts without an action
// ->don't need to do anything!
if(YES != notification.hasActionButton)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"popup without an action, no need to do anything");
#endif
//bail
goto bail;
}
//log user's response?
if(YES == [preferences[PREF_LOG_ACTIVITY] boolValue])
{
//user clicked 'block'
if(notification.activationType == NSUserNotificationActivationTypeActionButtonClicked)
{
//add
[logMessage appendFormat:@"user clicked 'block' for %@", notification.userInfo];
}
//user clicked 'allow'
else
{
//add
[logMessage appendFormat:@"user clicked 'allow' for %@", notification.userInfo];
}
//log
logMsg(LOG_DEBUG|LOG_TO_FILE, logMessage);
}
//check if user clicked 'allow' via user info (since OS doesn't directly deliver this)
// ->show a popup w/ option to remember ('whitelist') the application
// don't do this for 'block' since that kills the app, so obv, that'd be bad to *always* do!
if(NSUserNotificationActivationTypeAdditionalActionClicked == [notification.userInfo[@"activationType"] integerValue])
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"user clicked 'allow'");
#endif
//can't remember process that we didn't find the path for
if(YES == [notification.userInfo[EVENT_PROCESS_PATH] isEqualToString:PROCESS_UNKNOWN])
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"don't have a process path, so not displaying whitelisting popup");
#endif
//bail
goto bail;
}
//alloc/init
rememberWindowController = [[RememberWindowController alloc] initWithWindowNibName:@"RememberPopup"];
//center window
[[rememberWindowController window] center];
//show it
[rememberWindowController showWindow:self];
//manually configure
// ->invoke here as the outlets will be set
[rememberWindowController configure:notification avMonitor:self];
//make it key window
[rememberWindowController.window makeKeyAndOrderFront:self];
//make window front
[NSApp activateIgnoringOtherApps:YES];
//save reference to window
// ->otherwise memory is freed/window not shown :/
@synchronized (self)
{
//save
[self.rememberPopups addObject:rememberWindowController];
}
}
//when user clicks 'block'
// ->kill the process to block it
else if(NSUserNotificationActivationTypeActionButtonClicked == notification.activationType)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"user clicked 'block'");
#endif
//extract process id
processID = notification.userInfo[EVENT_PROCESS_ID];
if(nil == processID)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to extract process id from notification, %@", notification.userInfo]);
//bail
goto bail;
}
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
//resume
[xpcConnection resume];
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"invoking XPC method to kill: %@", processID]);
#endif
//invoke XPC method 'killProcess' to terminate
[[xpcConnection remoteObjectProxy] killProcess:processID reply:^(BOOL wasKilled)
{
//check for err
if(YES != wasKilled)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to kill/block: %@", processID]);
}
//close connection
[xpcConnection invalidate];
}];
}//user clicked 'block'
}//pool
//bail
bail:
return;
}
//manually monitor delivered notifications to see if user closes alert
// ->can't detect 'allow' otherwise :/ (see: http://stackoverflow.com/questions/21110714/mac-os-x-nsusernotificationcenter-notification-get-dismiss-event-callback)
-(void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification
{
//flag
__block BOOL notificationStillPresent;
//user dictionary
__block NSMutableDictionary* userInfo = nil;
//monitor in background to see if alert was dismissed
// ->invokes normal 'didActivateNotification' callback when alert is dimsissed
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
//monitor all delivered notifications until it goes away
do {
//reset
notificationStillPresent = NO;
//check all delivered notifications
for (NSUserNotification *nox in [[NSUserNotificationCenter defaultUserNotificationCenter] deliveredNotifications])
{
//check
if(YES == [nox.identifier isEqualToString:notification.identifier])
{
//found!
notificationStillPresent = YES;
//exit loop
break;
}
}
//nap if notification is still there
if(YES == notificationStillPresent)
{
//nap
[NSThread sleepForTimeInterval:0.25f];
}
//keep monitoring until its gone
} while(YES == notificationStillPresent);
//alert was dismissed
// ->invoke 'didActivateNotification' to process if it was an 'allow/block' alert
dispatch_async(dispatch_get_main_queue(),
^{
//add extra info for activation alerts
// ->will allow callback to identify we delivered via this mechanism
if(YES == notification.hasActionButton)
{
//grab user info dictionary
userInfo = [notification.userInfo mutableCopy];
//add activation type
userInfo[@"activationType"] = [NSNumber numberWithInteger:NSUserNotificationActivationTypeAdditionalActionClicked];
//update
notification.userInfo = userInfo;
}
//deliver
[self userNotificationCenter:center didActivateNotification:notification];
});
});
return;
}
//monitor for new procs (video only at the moment)
// runs until video is no longer in use (set elsewhere)
-(void)monitor4Procs
{
//pool
@autoreleasepool {
//xpc connection
NSXPCConnection* xpcConnection = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"[MONITOR THREAD] video is active, so polling for new procs");
#endif
//poll while video is active
while(YES == self.videoActive)
{
//pool
@autoreleasepool {
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"[MONITOR THREAD] (re)Asking XPC for (new) video procs");
#endif
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.OverSightXPC"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCProtocol)];
//set classes
// ->arrays/numbers ok to vend
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSNumber class], nil]
forSelector: @selector(getVideoProcs:reply:) argumentIndex: 0 ofReply: YES];
//resume
[xpcConnection resume];
//invoke XPC service to get (new) video procs
// ->will generate user notifications for any new processes
[[xpcConnection remoteObjectProxy] getVideoProcs:YES reply:^(NSMutableArray* videoProcesses)
{
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"[MONITOR THREAD] found %lu new video procs: %@", (unsigned long)videoProcesses.count, videoProcesses]);
#endif
//generate a notification for each process
// ->double check video is still active though...
for(NSNumber* processID in videoProcesses)
{
//check video
if(YES != self.videoActive)
{
//exit loop
break;
}
//generate notification
[self generateNotification:[@{EVENT_TIMESTAMP:[NSDate date], EVENT_DEVICE:self.camera, EVENT_DEVICE_STATUS:DEVICE_ACTIVE, EVENT_PROCESS_ID:processID} mutableCopy]];
}
//signal sema
dispatch_semaphore_signal(waitSema);
//invalidate
[xpcConnection invalidate];
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}//pool
//nap
[NSThread sleepForTimeInterval:5.0f];
}//run until video (camera) is off
//pool
}
bail:
//dbg msg
#ifdef DEBUG
logMsg(LOG_DEBUG, @"[MONITOR THREAD] exiting polling/monitor thread since camera is off");
#endif
return;
}
@end