OverSight/Application/Application/AVMonitor.m

1262 lines
38 KiB
Objective-C

//
// AVMonitor.m
// Application
//
// Created by Patrick Wardle on 4/30/21.
// Copyright © 2021 Objective-See. All rights reserved.
//
@import OSLog;
@import AVFoundation;
#import "consts.h"
#import "Client.h"
#import "AVMonitor.h"
#import "utilities.h"
#import "AppDelegate.h"
/* GLOBALS */
//log handle
extern os_log_t logHandle;
@implementation AVMonitor
//init
-(id)init
{
//action: ok
UNNotificationAction *ok = nil;
//action: allow
UNNotificationAction *allow = nil;
//action: allow
UNNotificationAction *allowAlways = nil;
//action: block
UNNotificationAction *block = nil;
//close category
UNNotificationCategory* closeCategory = nil;
//action category
UNNotificationCategory* actionCategory = nil;
//super
self = [super init];
if(nil != self)
{
//init log monitor
self.logMonitor = [[LogMonitor alloc] init];
//init audio attributions
self.audioAttributions = [NSMutableArray array];
//init camera attributions
self.cameraAttributions = [NSMutableArray array];
//init audio listener
self.audioListeners = [NSMutableDictionary dictionary];
//set up delegate
UNUserNotificationCenter.currentNotificationCenter.delegate = self;
//ask for notificaitons
[UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:(UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error)
{
//dbg msg
os_log_debug(logHandle, "permission to display notifications granted? %d (error: %@)", granted, error);
//not granted/error
if( (nil != error) ||
(YES != granted) )
{
//main thread?
if(YES == NSThread.isMainThread)
{
//show alert
showAlert(@"ERROR: OverSight not authorized to display notifications!", @"Please authorize via the \"Notifications\" pane (in System Preferences).");
}
//bg thread
// show alert on main thread
else
{
//on main thread
dispatch_async(dispatch_get_main_queue(),
^{
//show alert
showAlert(@"ERROR: OverSight not authorized to display notifications!", @"Please authorize via the \"Notifications\" pane (in System Preferences).");
});
}
}
}];
//init ok action
ok = [UNNotificationAction actionWithIdentifier:@"Ok" title:@"Ok" options:UNNotificationActionOptionNone];
//init close category
closeCategory = [UNNotificationCategory categoryWithIdentifier:CATEGORY_CLOSE actions:@[ok] intentIdentifiers:@[] options:0];
//init allow action
allow = [UNNotificationAction actionWithIdentifier:@"Allow" title:@"Allow (Once)" options:UNNotificationActionOptionNone];
//init allow action
allowAlways = [UNNotificationAction actionWithIdentifier:@"AllowAlways" title:@"Allow (Always)" options:UNNotificationActionOptionNone];
//init block action
block = [UNNotificationAction actionWithIdentifier:@"Block" title:@"Block" options:UNNotificationActionOptionNone];
//init category
actionCategory = [UNNotificationCategory categoryWithIdentifier:CATEGORY_ACTION actions:@[allow, allowAlways, block] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
//set categories
[UNUserNotificationCenter.currentNotificationCenter setNotificationCategories:[NSSet setWithObjects:closeCategory, actionCategory, nil]];
//per device events
self.deviceEvents = [NSMutableDictionary dictionary];
//init audio event queue
self.audioEventQueue = dispatch_queue_create("audio.event.timer", 0);
//init camera event queue
self.cameraEventQueue = dispatch_queue_create("camera.event.timer", 0);
//enumerate active devices
// then update status menu (on main thread)
dispatch_async(dispatch_get_main_queue(), ^{
//update status menu
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarItemController setActiveDevices:[self enumerateActiveDevices]];
});
//find built-in mic
self.builtInMic = [self findBuiltInMic];
//dbg msg
os_log_debug(logHandle, "built-in mic: %{public}@ (device ID: %d)", self.builtInMic.localizedName, [self getAVObjectID:self.builtInMic]);
//find build-in camera
self.builtInCamera = [self findBuiltInCamera];
//dbg msg
os_log_debug(logHandle, "built-in camera: %{public}@ (device ID: %d)", self.builtInCamera.localizedName, [self getAVObjectID:self.builtInCamera]);
}
return self;
}
//monitor AV
// also generate alerts as needed
-(void)start
{
//dbg msg
os_log_debug(logHandle, "starting AV monitoring");
//start log monitor
[self startLogMonitor];
return;
}
//log monitor
-(void)startLogMonitor
{
//dbg msg
os_log_debug(logHandle, "starting log monitor for AV events via w/ 'com.apple.SystemStatus'");
//start logging
[self.logMonitor start:[NSPredicate predicateWithFormat:@"subsystem=='com.apple.SystemStatus'"] level:Log_Level_Default callback:^(OSLogEvent* logEvent) {
//flags
BOOL audioAttributionsList = NO;
BOOL cameraAttributionsList = NO;
//new audio attributions
NSMutableArray* newAudioAttributions = nil;
//new camera attributions
NSMutableArray* newCameraAttributions = nil;
//dbg msg
os_log_debug(logHandle, "log message from 'com.apple.SystemStatus'");
//only interested on "Server data changed..." msgs
if(YES != [logEvent.composedMessage containsString:@"Server data changed for media domain"])
{
return;
}
//dbg msg
//os_log_debug(logHandle, "new (video) client msg: %{public}@", logEvent.composedMessage);
//split on newlines
// ...and then parse out audio/camera attributions
for(NSString* __strong line in [logEvent.composedMessage componentsSeparatedByString:@"\n"])
{
//pid
NSNumber* pid = 0;
//trim
line = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
//audioAttributions list?
if(YES == [line hasPrefix:@"audioAttributions = "])
{
//set flag
audioAttributionsList = YES;
//init
newAudioAttributions = [NSMutableArray array];
//unset (other) list
cameraAttributionsList = NO;
//next
continue;
}
//cameraAttributionsList list?
if(YES == [line hasPrefix:@"cameraAttributions = "])
{
//set flag
cameraAttributionsList = YES;
//init
newCameraAttributions = [NSMutableArray array];
//unset (other) list
audioAttributionsList = NO;
//next
continue;
}
//audit token of item?
if(YES == [line hasPrefix:@"auditToken = "])
{
//pid extraction regex
NSRegularExpression* regex = nil;
//match
NSTextCheckingResult* match = nil;
//init regex
regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=PID: )[0-9]*" options:0 error:nil];
//match/extract pid
match = [regex firstMatchInString:line options:0 range:NSMakeRange(0, line.length)];
if( (nil == match) ||
(NSNotFound == match.range.location))
{
//ignore
continue;
}
//extract pid
pid = @([[line substringWithRange:[match rangeAtIndex:0]] intValue]);
//in audio list?
if(YES == audioAttributionsList)
{
//add
[newAudioAttributions addObject:[NSNumber numberWithInt:[pid intValue]]];
}
//in camera list?
else if(YES == cameraAttributionsList)
{
//add
[newCameraAttributions addObject:[NSNumber numberWithInt:[pid intValue]]];
}
//next
continue;
}
}
//sync to process
@synchronized (self) {
//process attibutions
[self processAttributions:newAudioAttributions newCameraAttributions:newCameraAttributions];
}
//(re)enumerate active devices
// delayed need as device deactiavation
// then update status menu (on main thread)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
//update on on main thread
dispatch_async(dispatch_get_main_queue(), ^{
//update status menu
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).statusBarItemController setActiveDevices:[self enumerateActiveDevices]];
});
}); //dispatch for delay
}];
return;
}
//process attributions
// will generate (any needed) events to trigger alerts to user
-(void)processAttributions:(NSMutableArray*)newAudioAttributions newCameraAttributions:(NSMutableArray*)newCameraAttributions
{
//audio differences
NSOrderedCollectionDifference* audioDifferences = nil;
//camera differences
NSOrderedCollectionDifference* cameraDifferences = nil;
//client
__block Client* client = nil;
//event
__block Event* event = nil;
//diff audio differences
if(nil != newAudioAttributions)
{
//diff
audioDifferences = [newAudioAttributions differenceFromArray:self.audioAttributions];
}
//diff camera differences
if(nil != newCameraAttributions)
{
//diff
cameraDifferences = [newCameraAttributions differenceFromArray:self.cameraAttributions];
}
/* audio event logic */
//new audio event?
// handle (lookup mic, send event)
if(YES == audioDifferences.hasChanges)
{
//cancel prev timer
if(nil != self.audioEventTimer)
{
//cancel
dispatch_cancel(self.audioEventTimer);
self.audioEventTimer = nil;
}
//re-init timer
self.audioEventTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.audioEventQueue);
dispatch_source_set_timer(self.audioEventTimer, dispatch_walltime(NULL, 1.0 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.1 * NSEC_PER_SEC);
//set handler
dispatch_source_set_event_handler(self.audioEventTimer, ^{
//active mic
AVCaptureDevice* activeMic = nil;
//canel timer
dispatch_cancel(self.audioEventTimer);
self.audioEventTimer = nil;
//audio off?
// sent event
if(0 == audioDifferences.insertions.count)
{
//dbg msg
os_log_debug(logHandle, "audio event: off");
//init event
// process (client) and device are nil
event = [[Event alloc] init:nil device:nil deviceType:Device_Microphone state:NSControlStateValueOff];
//handle event
[self handleEvent:event];
}
//audio on?
// send event
else
{
//dbg msg
os_log_debug(logHandle, "audio event: on");
//send event for each process (attribution)
for(NSOrderedCollectionChange* audioAttribution in audioDifferences.insertions)
{
//init client from attribution
client = [[Client alloc] init];
client.pid = audioAttribution.object;
client.path = valueForStringItem(getProcessPath(client.pid.intValue));
client.name = valueForStringItem(getProcessName(client.path));
//look for active mic
for(AVCaptureDevice* microphone in [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio])
{
//off? skip
if(NSControlStateValueOn != [self getMicState:microphone])
{
//skip
continue;
}
//dbg msg
os_log_debug(logHandle, "device: %{public}@/%{public}@ is on", microphone.manufacturer, microphone.localizedName);
//save
activeMic = microphone;
//init event
// with client and (active) mic
event = [[Event alloc] init:client device:activeMic deviceType:Device_Microphone state:NSControlStateValueOn];
//handle event
[self handleEvent:event];
}
//no mic found? (e.g. headphones as input)
// show (limited) alert
if(nil == activeMic)
{
//init event
// devivce is nil
event = [[Event alloc] init:client device:nil deviceType:Device_Microphone state:NSControlStateValueOn];
//handle event
[self handleEvent:event];
}
}
}
});
//start audio event timer
dispatch_resume(self.audioEventTimer);
} //audio event
/* camera event logic */
//new camera event?
// handle (lookup camera, send event)
if(YES == cameraDifferences.hasChanges)
{
//cancel prev timer
if(nil != self.cameraEventTimer)
{
//cancel
dispatch_cancel(self.cameraEventTimer);
self.cameraEventTimer = nil;
}
//re-init timer
self.cameraEventTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.cameraEventQueue);
dispatch_source_set_timer(self.cameraEventTimer, dispatch_walltime(NULL, 1.0 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.1 * NSEC_PER_SEC);
//set handler
dispatch_source_set_event_handler(self.cameraEventTimer, ^{
//active camera
AVCaptureDevice* activeCamera = nil;
//canel timer
dispatch_cancel(self.cameraEventTimer);
self.cameraEventTimer = nil;
//camera off?
// sent event
if(0 == cameraDifferences.insertions.count)
{
//dbg msg
os_log_debug(logHandle, "camera event: off");
//init event
// process (client) and device are nil
event = [[Event alloc] init:nil device:nil deviceType:Device_Camera state:NSControlStateValueOff];
//handle event
[self handleEvent:event];
}
//camera on?
// send event
else
{
//dbg msg
os_log_debug(logHandle, "camera event: on");
//send event for each process (attribution)
for(NSOrderedCollectionChange* cameraAttribution in cameraDifferences.insertions)
{
//init client from attribution
client = [[Client alloc] init];
client.pid = cameraAttribution.object;
client.path = valueForStringItem(getProcessPath(client.pid.intValue));
client.name = valueForStringItem(getProcessName(client.path));
//look for active camera
for(AVCaptureDevice* camera in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo])
{
//off? skip
if(NSControlStateValueOn != [self getCameraState:camera])
{
//skip
continue;
}
//virtual
// TODO: is there a better way to determine this?
if(YES == [camera.localizedName containsString:@"Virtual"])
{
//skip
continue;
}
//dbg msg
os_log_debug(logHandle, "camera device: %{public}@/%{public}@ is on", camera.manufacturer, camera.localizedName);
//save
activeCamera = camera;
//init event
// with client and (active) camera
event = [[Event alloc] init:client device:activeCamera deviceType:Device_Camera state:NSControlStateValueOn];
//handle event
[self handleEvent:event];
}
//no camera found?
// show (limited) alert
if(nil == activeCamera)
{
//init event
// devivce is nil
event = [[Event alloc] init:client device:nil deviceType:Device_Camera state:NSControlStateValueOn];
//handle event
[self handleEvent:event];
}
}
}
});
//start audio timer
dispatch_resume(self.cameraEventTimer);
} //camera event
//update audio attributions
self.audioAttributions = [newAudioAttributions copy];
//update camera attributions
self.cameraAttributions = [newCameraAttributions copy];
return;
}
//enumerate active devices
-(NSMutableArray*)enumerateActiveDevices
{
//active device
NSMutableArray* activeDevices = nil;
//init
activeDevices = [NSMutableArray array];
//look for active cameras
for(AVCaptureDevice* camera in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo])
{
//skip virtual devices (e.g. OBS virtual camera)
// TODO: is there a better way to determine this?
if(YES == [camera.localizedName containsString:@"Virtual"])
{
//skip
continue;
}
//save those that are one
if(NSControlStateValueOn == [self getCameraState:camera])
{
//save
[activeDevices addObject:camera];
}
}
//look for active mic
for(AVCaptureDevice* microphone in [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio])
{
//save those that are one
if(NSControlStateValueOn == [self getMicState:microphone])
{
//save
[activeDevices addObject:microphone];
}
}
return activeDevices;
}
//get built-in mic
// looks for Apple device that's 'BuiltInMicrophoneDevice'
-(AVCaptureDevice*)findBuiltInMic
{
//mic
AVCaptureDevice* builtInMic = 0;
//built in mic appears as "BuiltInMicrophoneDevice"
for(AVCaptureDevice* currentMic in [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio])
{
//dbg msg
os_log_debug(logHandle, "device: %{public}@/%{public}@", currentMic.manufacturer, currentMic.localizedName);
//is "BuiltInMicrophoneDevice" ?
if( (YES == [currentMic.manufacturer isEqualToString:@"Apple Inc."]) &&
(YES == [currentMic.uniqueID isEqualToString:@"BuiltInMicrophoneDevice"]) )
{
//found
builtInMic = currentMic;
break;
}
}
//not found?
// grab default
if(0 == builtInMic)
{
//get mic / id
builtInMic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
//dbg msg
os_log_debug(logHandle, "Apple Mic not found, defaulting to default device: %{public}@/%{public}@)", builtInMic.manufacturer, builtInMic.localizedName);
}
return builtInMic;
}
//get built-in camera
-(AVCaptureDevice*)findBuiltInCamera
{
//camera
AVCaptureDevice* builtInCamera = 0;
//built in mic appears as "BuiltInMicrophoneDevice"
for(AVCaptureDevice* currentCamera in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo])
{
//dbg msg
os_log_debug(logHandle, "device: %{public}@/%{public}@", currentCamera.manufacturer, currentCamera.localizedName);
//check if apple && 'FaceTime HD Camera'
if( (YES == [currentCamera.manufacturer isEqualToString:@"Apple Inc."]) &&
(YES == [currentCamera.uniqueID isEqualToString:@"FaceTime HD Camera"]) )
{
//found
builtInCamera = currentCamera;
break;
}
}
//not found?
// grab default
if(0 == builtInCamera)
{
//get mic / id
builtInCamera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//dbg msg
os_log_debug(logHandle, "Apple Camera not found, defaulting to default device: %{public}@/%{public}@)", builtInCamera.manufacturer, builtInCamera.localizedName);
}
return builtInCamera;
}
//get av object's ID
-(UInt32)getAVObjectID:(AVCaptureDevice*)device
{
//object id
UInt32 objectID = 0;
//selector for getting device id
SEL methodSelector = nil;
//init selector
methodSelector = NSSelectorFromString(@"connectionID");
//sanity check
if(YES != [device respondsToSelector:methodSelector])
{
//bail
goto bail;
}
//ignore leak warning
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//grab connection ID
objectID = (UInt32)[device performSelector:methodSelector withObject:nil];
//restore
#pragma clang diagnostic pop
bail:
return objectID;
}
//determine if audio device is active
-(UInt32)getMicState:(AVCaptureDevice*)device;
{
//status var
OSStatus status = -1;
//device ID
AudioObjectID deviceID = 0;
//running flag
UInt32 isRunning = 0;
//size of query flag
UInt32 propertySize = 0;
//get device ID
deviceID = [self getAVObjectID:device];
//init size
propertySize = sizeof(isRunning);
//query to get 'kAudioDevicePropertyDeviceIsRunningSomewhere' status
status = AudioDeviceGetProperty(deviceID, 0, false, kAudioDevicePropertyDeviceIsRunningSomewhere, &propertySize, &isRunning);
if(noErr != status)
{
//err msg
os_log_error(logHandle, "ERROR: getting status of audio device failed with %d", status);
//set error
isRunning = -1;
//bail
goto bail;
}
bail:
return isRunning;
}
//check if a specified video is active
// note: on M1 this always says 'on' (smh apple)
-(UInt32)getCameraState:(AVCaptureDevice*)device
{
//status var
OSStatus status = -1;
//device ID
CMIODeviceID deviceID = 0;
//running flag
UInt32 isRunning = 0;
//size of query flag
UInt32 propertySize = 0;
//property address struct
CMIOObjectPropertyAddress propertyStruct = {0};
//get device ID
deviceID = [self getAVObjectID:device];
//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
os_log_error(logHandle, "ERROR: failed to get camera status (error: %#x)", status);
//set error
isRunning = -1;
//bail
goto bail;
}
bail:
return isRunning;
}
//should an event be shown?
-(NSUInteger)shouldShowNotification:(Event*)event
{
//result
NSUInteger result = NOTIFICATION_ERROR;
//device ID
NSNumber* deviceID = 0;
//device's last event
Event* deviceLastEvent = nil;
//get device ID
deviceID = [NSNumber numberWithInt:[self getAVObjectID:event.device]];
//extract its last event
deviceLastEvent = self.deviceEvents[deviceID];
//save this event, now as last
self.deviceEvents[deviceID] = event;
//inactive alerting off?
// ignore if event is an inactive/off
if( (NSControlStateValueOff == event.state) &&
(YES == [NSUserDefaults.standardUserDefaults boolForKey:PREF_DISABLE_INACTIVE]))
{
//set result
result = NOTIFICATION_SKIPPED;
//dbg msg
os_log_debug(logHandle, "disable inactive alerts set, so ignoring inactive/off event");
//bail
goto bail;
}
//no external devices mode?
// note: only for activation event (as we don't have device for inactivation events)
if( (NSControlStateValueOn == event.state) &&
(YES == [NSUserDefaults.standardUserDefaults boolForKey:PREF_NO_EXTERNAL_DEVICES_MODE]) &&
(YES != [self.builtInMic.uniqueID isEqualToString:event.device.uniqueID]) && (YES != [self.builtInCamera.uniqueID isEqualToString:event.device.uniqueID]) )
{
//set result
result = NOTIFICATION_SKIPPED;
//dbg msg
os_log_debug(logHandle, "ingore external devices activation is set, so ignoring external device event");
//bail
goto bail;
}
//(new) mic event?
// need extra logic, since macOS sometimes toggles / delivers 2x events :/
if(Device_Microphone == event.deviceType)
{
//ignore if device's last event was <0.5
if([event.timestamp timeIntervalSinceDate:deviceLastEvent.timestamp] < 0.5f)
{
//set result
result = NOTIFICATION_SPURIOUS;
//dbg msg
os_log_debug(logHandle, "ignoring mic event, as it happened <0.5s ");
//bail
goto bail;
}
//ignore if device's last event was same state
if( (deviceLastEvent.state == event.state) &&
([event.timestamp timeIntervalSinceDate:deviceLastEvent.timestamp] < 1.0f) )
{
//set result
result = NOTIFICATION_SPURIOUS;
//dbg msg
os_log_debug(logHandle, "ignoring mic event, as it happened <1.0 and is same state (%ld)", (long)event.state);
//bail
goto bail;
}
/*
//from same device?
// ignore if last event *just* occurred
([self getAVObjectID:event.device] == [self getAVObjectID:self.lastMicEvent.device]) )
{
//dbg msg
os_log_debug(logHandle, "ignoring mic event, as it happened <0.5s ");
//bail
goto bail;
}
//was a 2x off/on for same device
if( (nil != self.lastMicEvent) &&
(event.state == self.lastMicEvent.state) &&
([self getAVObjectID:event.device] == [self getAVObjectID:self.lastMicEvent.device]) )
{
//set result
result = NOTIFICATION_SPURIOUS;
//dbg msg
os_log_debug(logHandle, "ignoring mic event for %{public}@ as it was a 2x", event.device.localizedName);
//bail
goto bail;
}
//update
self.lastMicEvent = event;
*/
}
//client provided?
// check if its allowed
if(nil != event.client)
{
//match is simply: device and path
for(NSDictionary* allowedItem in [NSUserDefaults.standardUserDefaults objectForKey:PREFS_ALLOWED_ITEMS])
{
//match?
if( ([allowedItem[EVENT_DEVICE] intValue] == event.deviceType) &&
(YES == [allowedItem[EVENT_PROCESS_PATH] isEqualToString:event.client.path]) )
{
//set result
result = NOTIFICATION_SKIPPED;
//dbg msg
os_log_debug(logHandle, "%{public}@ is allowed to access %d, so no notification will be shown", event.client.path, event.deviceType);
//done
goto bail;
}
}
}
//set result
result = NOTIFICATION_DELIVER;
bail:
return result;
}
//handle an event
// show alert / exec user action
-(void)handleEvent:(Event*)event
{
//result
NSUInteger result = NOTIFICATION_ERROR;
//should show?
@synchronized (self) {
result = [self shouldShowNotification:event];
}
if(NOTIFICATION_DELIVER == result)
{
//deliver
[self showNotification:event];
}
//should (also) exec user action?
if( (NOTIFICATION_ERROR != result) &&
(NOTIFICATION_SPURIOUS != result) )
{
//exec
[self executeUserAction:event];
}
bail:
return;
}
//build and display notification
-(void)showNotification:(Event*)event
{
//notification content
UNMutableNotificationContent* content = nil;
//notificaito0n request
UNNotificationRequest* request = nil;
//alloc content
content = [[UNMutableNotificationContent alloc] init];
//title
NSMutableString* title = nil;
//set (default) category
content.categoryIdentifier = CATEGORY_CLOSE;
//alloc title
title = [NSMutableString string];
//set device type
(Device_Camera == event.deviceType) ? [title appendString:@"📸"] : [title appendFormat:@"🎙️"];
//set status
(NSControlStateValueOn == event.state) ? [title appendString:@" Became Active!"] : [title appendString:@" Became Inactive."];
//set title
content.title = title;
//sub-title
// device name
if(nil != event.device)
{
//set
content.subtitle = [NSString stringWithFormat:@"%@", event.device.localizedName];
}
//have client?
// use as body
if(nil != event.client)
{
//set body
content.body = [NSString stringWithFormat:@"\r\nProcess: %@ (%@)", event.client.name, (0 != event.client.pid.intValue) ? event.client.pid : @"pid: unknown"];
//set category
content.categoryIdentifier = CATEGORY_ACTION;
//set user info
content.userInfo = @{EVENT_DEVICE:@(event.deviceType), EVENT_PROCESS_ID:event.client.pid, EVENT_PROCESS_PATH:event.client.path};
}
else if(nil != event.device)
{
//set body
content.body = [NSString stringWithFormat:@"Device: %@", event.device.localizedName];
}
//init request
request = [UNNotificationRequest requestWithIdentifier:NSUUID.UUID.UUIDString content:content trigger:NULL];
//send notification
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error)
{
//error?
if(nil != error)
{
//err msg
os_log_error(logHandle, "ERROR failed to deliver notification (error: %@)", error);
}
}];
bail:
return;
}
//execute user action
-(BOOL)executeUserAction:(Event*)event
{
//flag
BOOL wasExecuted = NO;
//path to action
NSString* action = nil;
//args
NSMutableArray* args = nil;
//execute user-specified action?
if(YES != [NSUserDefaults.standardUserDefaults boolForKey:PREF_EXECUTE_ACTION])
{
//bail
goto bail;
}
//dbg msg
os_log_debug(logHandle, "executing user action");
//grab action
action = [NSUserDefaults.standardUserDefaults objectForKey:PREF_EXECUTE_PATH];
if(YES != [NSFileManager.defaultManager fileExistsAtPath:action])
{
//err msg
os_log_error(logHandle, "ERROR: action %{public}@, does not exist", action);
//bail
goto bail;
}
//pass args?
if(YES == [NSUserDefaults.standardUserDefaults boolForKey:PREF_EXECUTE_ACTION_ARGS])
{
//alloc
args = [NSMutableArray array];
//add device
[args addObject:@"-device"];
(Device_Camera == event.device) ? [args addObject:@"camera"] : [args addObject:@"microphone"];
//add event
[args addObject:@"-event"];
(NSControlStateValueOn == event.state) ? [args addObject:@"on"] : [args addObject:@"off"];
//add process
if(nil != event.client)
{
//add
[args addObject:@"-process"];
[args addObject:event.client.pid.stringValue];
}
}
//exec user specified action
execTask(action, args, NO, NO);
bail:
return wasExecuted;
}
//stop monitor
-(void)stop
{
//stop log monitoring
[self.logMonitor stop];
return;
}
# pragma mark UNNotificationCenter Delegate Methods
//handle user response to notification
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
//allowed items
NSMutableArray* allowedItems = nil;
//device
NSNumber* device = nil;
//process path
NSString* processPath = nil;
//process name
NSString* processName = nil;
//process id
NSNumber* processID = nil;
//error
int error = 0;
//dbg msg
os_log_debug(logHandle, "user response to notification: %{public}@", response);
//extract device
device = response.notification.request.content.userInfo[EVENT_DEVICE];
//extact process path
processPath = response.notification.request.content.userInfo[EVENT_PROCESS_PATH];
//extract process id
processID = response.notification.request.content.userInfo[EVENT_PROCESS_ID];
//get process name
processName = valueForStringItem(getProcessName(processPath));
//close?
// nothing to do
if(YES == [response.notification.request.content.categoryIdentifier isEqualToString:CATEGORY_CLOSE])
{
//dbg msg
os_log_debug(logHandle, "user clicked 'Ok'");
//done
goto bail;
}
//allow?
// really nothing to do
else if(YES == [response.actionIdentifier isEqualToString:@"Allow"])
{
//dbg msg
os_log_debug(logHandle, "user clicked 'Allow'");
//done
goto bail;
}
//always allow?
// added to 'allowed' items
if(YES == [response.actionIdentifier isEqualToString:@"AllowAlways"])
{
//dbg msg
os_log_debug(logHandle, "user clicked 'Allow Always'");
//load allowed items
allowedItems = [[NSUserDefaults.standardUserDefaults objectForKey:PREFS_ALLOWED_ITEMS] mutableCopy];
if(nil == allowedItems)
{
//alloc
allowedItems = [NSMutableArray array];
}
//add item
[allowedItems addObject:@{EVENT_PROCESS_PATH:processPath, EVENT_DEVICE:device}];
//save & sync
[NSUserDefaults.standardUserDefaults setObject:allowedItems forKey:PREFS_ALLOWED_ITEMS];
[NSUserDefaults.standardUserDefaults synchronize];
//dbg msg
os_log_debug(logHandle, "added %{public}@ to list of allowed items", processPath);
//broadcast
[[NSNotificationCenter defaultCenter] postNotificationName:RULES_CHANGED object:nil userInfo:nil];
//done
goto bail;
}
//block?
// kill process
if(YES == [response.actionIdentifier isEqualToString:@"Block"])
{
//dbg msg
os_log_debug(logHandle, "user clicked 'Block'");
//kill
error = kill(processID.intValue, SIGKILL);
if(0 != error)
{
//err msg
os_log_error(logHandle, "ERROR: failed to kill %@ (%@)", processName, processID);
//show an alert
showAlert([NSString stringWithFormat:@"ERROR: failed to block %@ (%@)", processName, processID], [NSString stringWithFormat:@"system error code: %d", error]);
//bail
goto bail;
}
//dbg msg
os_log_debug(logHandle, "killed %@ (%@)", processName, processID);
}
bail:
//gotta call
completionHandler();
return;
}
@end