Skip to content

Commit

Permalink
sync service: add support for APNS
Browse files Browse the repository at this point in the history
  • Loading branch information
tburgin committed Dec 6, 2024
1 parent 82fddd3 commit eed0782
Show file tree
Hide file tree
Showing 20 changed files with 374 additions and 38 deletions.
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];
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 {
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 {
LOGE(@"Failed to register with APNS: %@", error);
}

- (void)application:(NSApplication *)application
didReceiveRemoteNotification:(NSDictionary<NSString *, id> *)message {
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

0 comments on commit eed0782

Please sign in to comment.