Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sync service: add support for APNS #158

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Source/common/SNTConfigurator.h
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,12 @@
///
@property(readonly, nonatomic) BOOL fcmEnabled;

///
/// Set to true to use APNS. Defaults to false.
/// If fcmEnabled and enableAPNS are both enabled, only enableAPNS will be used.
///
@property(readonly, nonatomic) BOOL enableAPNS;

///
/// True if metricsFormat and metricsURL are set. False otherwise.
///
Expand Down
13 changes: 13 additions & 0 deletions Source/common/SNTConfigurator.m
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ @implementation SNTConfigurator
static NSString *const kFCMEntity = @"FCMEntity";
static NSString *const kFCMAPIKey = @"FCMAPIKey";

static NSString *const kEnableAPNS = @"EnableAPNS";

static NSString *const kEntitlementsPrefixFilterKey = @"EntitlementsPrefixFilter";
static NSString *const kEntitlementsTeamIDFilterKey = @"EntitlementsTeamIDFilter";

Expand Down Expand Up @@ -279,6 +281,7 @@ - (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath
kFCMProject : string,
kFCMEntity : string,
kFCMAPIKey : string,
kEnableAPNS : number,
kMetricFormat : string,
kMetricURL : string,
kMetricExportInterval : number,
Expand Down Expand Up @@ -597,6 +600,10 @@ + (NSSet *)keyPathsForValuesAffectingFcmEnabled {
return [self configStateSet];
}

+ (NSSet *)keyPathsForValuesAffectingEnableAPNS {
return [self configStateSet];
}

+ (NSSet *)keyPathsForValuesAffectingEnableBadSignatureProtection {
return [self configStateSet];
}
Expand Down Expand Up @@ -1113,6 +1120,12 @@ - (BOOL)fcmEnabled {
return (self.fcmProject.length && self.fcmEntity.length && self.fcmAPIKey.length);
}

- (BOOL)enableAPNS {
// TODO: Consider supporting enablement from the sync server.
NSNumber *number = self.configState[kEnableAPNS];
tburgin marked this conversation as resolved.
Show resolved Hide resolved
return [number boolValue];
}

- (void)setBlockUSBMount:(BOOL)enabled {
[self updateSyncStateForKey:kBlockUSBMountKey value:@(enabled)];
}
Expand Down
1 change: 1 addition & 0 deletions Source/common/SNTXPCControlInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
/// Syncd Ops
///
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message reply:(void (^)(void))reply;
- (void)requestAPNSToken:(void (^)(NSString *))reply;

///
/// Control Ops
Expand Down
1 change: 1 addition & 0 deletions Source/common/SNTXPCNotifierInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
binaryCount:(uint64_t)binaryCount
fileCount:(uint64_t)fileCount
hashedCount:(uint64_t)hashedCount;
- (void)requestAPNSToken:(void (^)(NSString *))reply;
@end

@interface SNTXPCNotifierInterface : NSObject
Expand Down
16 changes: 15 additions & 1 deletion Source/common/SNTXPCSyncServiceInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
- (void)postEventsToSyncServer:(NSArray<SNTStoredEvent *> *)events fromBundle:(BOOL)fromBundle;
- (void)postBundleEventToSyncServer:(SNTStoredEvent *)event
reply:(void (^)(SNTBundleEventAction))reply;
- (void)isFCMListening:(void (^)(BOOL))reply;
- (void)isPushConnected:(void (^)(BOOL))reply;

// The syncservice regularly syncs with a configured sync server. Use this method to sync out of
// band. The syncservice ensures syncs do not run concurrently.
Expand All @@ -51,6 +51,20 @@
// A new connection to the syncservice will bring it back up. This allows us to avoid running
// the syncservice needlessly when there is no configured sync server.
- (void)spindown;

// The GUI process registers with APNS. The token is then retrieved by the sync service. However,
// tokens are unique per-{device, app, and logged in user}. During fast user switching, a second
// GUI process spins up and registers with APNS. The sync service should then start using that APNS
// token. This method is called when the token has changed, letting the sync service know it needs
// to go and fetch the updated token. Why does the GUI process not send the token to the sync
// service when it changes? A few reasons. First, if the sync service restarts for any reason, it
// will be left without a token. Second, the "active" GUI process is already being negotiated
// between the GUIs and the daemon. Having the sync service fetch the token, via the daemon,
// utilizes the already negotiated active GUI process for token retrieval.
- (void)APNSTokenChanged;

// The GUI process forwards APNS messages to the sync service.
- (void)handleAPNSMessage:(NSDictionary *)message;
@end

@interface SNTXPCSyncServiceInterface : NSObject
Expand Down
1 change: 1 addition & 0 deletions Source/gui/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ objc_library(
"//Source/common:SNTSyncConstants",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCNotifierInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"@MOLCertificate",
"@MOLCodesignChecker",
"@MOLXPCConnection",
Expand Down
36 changes: 36 additions & 0 deletions Source/gui/SNTAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/SNTXPCSyncServiceInterface.h"
#import "Source/gui/SNTAboutWindowController.h"
#import "Source/gui/SNTNotificationManager.h"

Expand All @@ -34,6 +35,10 @@ @implementation SNTAppDelegate
#pragma mark App Delegate methods

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if ([SNTConfigurator configurator].enableAPNS) {
[NSApp registerForRemoteNotifications];
}

[self setupMenu];
self.notificationManager = [[SNTNotificationManager alloc] init];

Expand Down Expand Up @@ -100,6 +105,10 @@ - (void)createDaemonConnection {
// Now wait for the connection to come in.
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
[self attemptDaemonReconnection];
} else {
// Let the sync service know the APNS token may have changed. The sync service will call back on
// the above listener to get the updated token.
[self.notificationManager APNSTokenChanged];
}
}

Expand All @@ -125,4 +134,31 @@ - (void)setupMenu {
[NSApp setMainMenu:mainMenu];
}

#pragma mark Push Notifications

- (void)application:(NSApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)tokenData {
tburgin marked this conversation as resolved.
Show resolved Hide resolved
NSMutableString *deviceToken = [NSMutableString stringWithCapacity:tokenData.length * 2];
const unsigned char *bytes = tokenData.bytes;
for (NSUInteger i = 0; i < tokenData.length; ++i) {
[deviceToken appendFormat:@"%02x", bytes[i]];
}
LOGD(@"APNS Token: %@", deviceToken);
[self.notificationManager didRegisterForAPNS:deviceToken];
}

- (void)application:(NSApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
tburgin marked this conversation as resolved.
Show resolved Hide resolved
LOGE(@"Failed to register with APNS: %@", error);
}

- (void)application:(NSApplication *)application
didReceiveRemoteNotification:(NSDictionary<NSString *, id> *)message {
tburgin marked this conversation as resolved.
Show resolved Hide resolved
LOGD(@"APNS Message: %@", message);
MOLXPCConnection *syncConn = [SNTXPCSyncServiceInterface configuredConnection];
[syncConn resume];
[[syncConn remoteObjectProxy] handleAPNSMessage:message];
[syncConn invalidate];
}

@end
3 changes: 2 additions & 1 deletion Source/gui/SNTNotificationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
@interface SNTNotificationManager : NSObject <SNTMessageWindowControllerDelegate, SNTNotifierXPC>

@property NSXPCListenerEndpoint *notificationListener;

- (void)didRegisterForAPNS:(NSString *)deviceToken;
- (void)APNSTokenChanged;
@end
48 changes: 48 additions & 0 deletions Source/gui/SNTNotificationManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/SNTXPCSyncServiceInterface.h"
#import "Source/gui/SNTAppDelegate.h"
#import "Source/gui/SNTBinaryMessageWindowController.h"
#import "Source/gui/SNTBinaryMessageWindowView-Swift.h"
#import "Source/gui/SNTDeviceMessageWindowController.h"
Expand All @@ -46,6 +48,17 @@ @interface SNTNotificationManager ()
// A serial queue for holding hashBundleBinaries requests
@property dispatch_queue_t hashBundleBinariesQueue;

// The APNS device token. If configured, the GUI app registers with APNS. Once the registration is
// complete, the app delegate will notify this class. Any pending requests for the token will then
// be processed.
@property(atomic) NSString *APNSDeviceToken;

// A queue for serializing APNS token requests.
@property dispatch_queue_t APNSQueue;

// Stores APNS token requests, while waiting for APNS registration.
@property NSMutableArray<void (^)(NSString *)> *APNSTokenRequests;

@end

@implementation SNTNotificationManager
Expand All @@ -58,6 +71,8 @@ - (instancetype)init {
_pendingNotifications = [[NSMutableArray alloc] init];
_hashBundleBinariesQueue = dispatch_queue_create("com.northpolesec.santagui.hashbundlebinaries",
DISPATCH_QUEUE_SERIAL);
_APNSQueue = dispatch_queue_create("com.northpolesec.santagui.apns", DISPATCH_QUEUE_SERIAL);
_APNSTokenRequests = [NSMutableArray array];
}
return self;
}
Expand Down Expand Up @@ -400,6 +415,39 @@ - (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
[self queueMessage:pendingMsg];
}

// XPC handler. The sync service requests the APNS token, by way of the daemon.
- (void)requestAPNSToken:(void (^)(NSString *))reply {
if (self.APNSDeviceToken.length) {
reply(self.APNSDeviceToken);
return;
}

// If APNS is enabled, `-[NSApp registerForRemoteNotifications]` is run when the application
// finishes launching at startup. If APNS is enabled after startup, register now.
[NSApp registerForRemoteNotifications];
dispatch_async(self.APNSQueue, ^{
[self.APNSTokenRequests addObject:reply];
});
}

- (void)didRegisterForAPNS:(NSString *)deviceToken {
self.APNSDeviceToken = deviceToken;
[self APNSTokenChanged];
dispatch_async(self.APNSQueue, ^{
for (void (^reply)(NSString *) in self.APNSTokenRequests) {
reply(deviceToken);
}
[self.APNSTokenRequests removeAllObjects];
});
}

- (void)APNSTokenChanged {
MOLXPCConnection *syncConn = [SNTXPCSyncServiceInterface configuredConnection];
[syncConn resume];
[[syncConn remoteObjectProxy] APNSTokenChanged];
[syncConn invalidate];
}

#pragma mark SNTBundleNotifierXPC protocol methods

- (void)updateCountsForEvent:(SNTStoredEvent *)event
Expand Down
36 changes: 36 additions & 0 deletions Source/gui/SNTNotificationManagerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,40 @@ - (void)testPostBlockNotificationSendsDistributedNotification {
deliverImmediately:YES]);
}

- (void)testDidRegisterForAPNS {
SNTNotificationManager *nm = [[SNTNotificationManager alloc] init];
__block NSString *token;
XCTestExpectation *exp = [self expectationWithDescription:@"Wait for APNS token"];
[nm requestAPNSToken:^(NSString *reply) {
token = reply;
[exp fulfill];
}];
NSString *wantToken = @"123";
[nm didRegisterForAPNS:wantToken];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqualObjects(token, wantToken);

// Subsequent requests should be handled by the cache.
token = nil;
[nm requestAPNSToken:^(NSString *reply) {
token = reply;
}];
XCTAssertEqualObjects(token, wantToken);

// Ensure multiple in-flight requests are handled.
nm = [[SNTNotificationManager alloc] init];
int wantCount = 5;
__block int count = 0;
for (int i = 0; i < wantCount; ++i) {
exp = [self expectationWithDescription:@"Wait for multiple requests for the APNS token"];
[nm requestAPNSToken:^(NSString *reply) {
++count;
[exp fulfill];
}];
}
[nm didRegisterForAPNS:@"hello"];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqual(count, wantCount);
}

@end
7 changes: 6 additions & 1 deletion Source/santad/SNTDaemonControlController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,15 @@ - (void)setNotificationListener:(NSXPCListenerEndpoint *)listener {
self.notQueue.notifierConnection = c;
}

- (void)requestAPNSToken:(void (^)(NSString *))reply {
// Simply forward request to the active GUI (if any).
[self.notQueue.notifierConnection.remoteObjectProxy requestAPNSToken:reply];
}

#pragma mark syncd Ops

- (void)pushNotifications:(void (^)(BOOL))reply {
[self.syncdQueue.syncConnection.remoteObjectProxy isFCMListening:^(BOOL response) {
[self.syncdQueue.syncConnection.remoteObjectProxy isPushConnected:^(BOOL response) {
reply(response);
}];
}
Expand Down
10 changes: 8 additions & 2 deletions Source/santasyncservice/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ objc_library(
srcs = [
"NSData+Zlib.h",
"NSData+Zlib.m",
"SNTPushClientAPNS.h",
"SNTPushClientAPNS.m",
"SNTPushClientFCM.h",
"SNTPushClientFCM.m",
"SNTPushNotifications.h",
"SNTPushNotifications.m",
"SNTPushNotificationsTracker.h",
"SNTPushNotificationsTracker.m",
"SNTSyncEventUpload.h",
Expand Down Expand Up @@ -76,8 +79,11 @@ santa_unit_test(
srcs = [
"NSData+Zlib.h",
"NSData+Zlib.m",
"SNTPushClientAPNS.h",
"SNTPushClientAPNS.m",
"SNTPushClientFCM.h",
"SNTPushClientFCM.m",
"SNTPushNotifications.h",
"SNTPushNotifications.m",
"SNTPushNotificationsTracker.h",
"SNTPushNotificationsTracker.m",
"SNTSyncEventUpload.h",
Expand Down
21 changes: 21 additions & 0 deletions Source/santasyncservice/SNTPushClientAPNS.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Copyright 2024 North Pole Security, Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License

#import "Source/santasyncservice/SNTPushNotifications.h"

@interface SNTPushClientAPNS : NSObject <SNTPushNotificationsClientDelegate>
- (instancetype)initWithSyncDelegate:(id<SNTPushNotificationsSyncDelegate>)syncDelegate;
- (void)APNSTokenChanged;
- (void)handleAPNSMessage:(NSDictionary *)message;
@end
Loading
Loading