mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
* sync: Improve logging when connection restored, avoid retries. Don't retry requests if the error is that the machine is offline
422 lines
15 KiB
Objective-C
422 lines
15 KiB
Objective-C
/// Copyright 2016 Google 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/SNTSyncManager.h"
|
|
|
|
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
|
|
#import <MOLXPCConnection/MOLXPCConnection.h>
|
|
#import <Network/Network.h>
|
|
|
|
#import "Source/common/SNTCommonEnums.h"
|
|
#import "Source/common/SNTConfigurator.h"
|
|
#import "Source/common/SNTLogging.h"
|
|
#import "Source/common/SNTStoredEvent.h"
|
|
#import "Source/common/SNTStrengthify.h"
|
|
#import "Source/common/SNTSyncConstants.h"
|
|
#import "Source/common/SNTXPCControlInterface.h"
|
|
#import "Source/santasyncservice/SNTPushNotifications.h"
|
|
#import "Source/santasyncservice/SNTSyncEventUpload.h"
|
|
#import "Source/santasyncservice/SNTSyncLogging.h"
|
|
#import "Source/santasyncservice/SNTSyncPostflight.h"
|
|
#import "Source/santasyncservice/SNTSyncPreflight.h"
|
|
#import "Source/santasyncservice/SNTSyncRuleDownload.h"
|
|
#import "Source/santasyncservice/SNTSyncState.h"
|
|
|
|
static const uint8_t kMaxEnqueuedSyncs = 2;
|
|
|
|
@interface SNTSyncManager () <SNTPushNotificationsDelegate>
|
|
|
|
@property(nonatomic) dispatch_source_t fullSyncTimer;
|
|
@property(nonatomic) dispatch_source_t ruleSyncTimer;
|
|
|
|
@property(nonatomic, readonly) dispatch_queue_t syncQueue;
|
|
@property(nonatomic, readonly) dispatch_semaphore_t syncLimiter;
|
|
|
|
@property(nonatomic) MOLXPCConnection *daemonConn;
|
|
|
|
@property(nonatomic) BOOL reachable;
|
|
@property nw_path_monitor_t pathMonitor;
|
|
|
|
@property SNTPushNotifications *pushNotifications;
|
|
|
|
@property NSUInteger eventBatchSize;
|
|
|
|
@property NSString *xsrfToken;
|
|
@property NSString *xsrfTokenHeader;
|
|
|
|
@end
|
|
|
|
@implementation SNTSyncManager
|
|
|
|
#pragma mark init
|
|
|
|
- (instancetype)initWithDaemonConnection:(MOLXPCConnection *)daemonConn {
|
|
self = [super init];
|
|
if (self) {
|
|
_daemonConn = daemonConn;
|
|
_pushNotifications = [[SNTPushNotifications alloc] init];
|
|
_pushNotifications.delegate = self;
|
|
_fullSyncTimer = [self createSyncTimerWithBlock:^{
|
|
[self rescheduleTimerQueue:self.fullSyncTimer
|
|
secondsFromNow:_pushNotifications.pushNotificationsFullSyncInterval];
|
|
[self syncType:SNTSyncTypeNormal withReply:NULL];
|
|
}];
|
|
_ruleSyncTimer = [self createSyncTimerWithBlock:^{
|
|
dispatch_source_set_timer(self.ruleSyncTimer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER,
|
|
0);
|
|
[self ruleSyncImpl];
|
|
}];
|
|
_syncQueue = dispatch_queue_create("com.google.santa.syncservice", DISPATCH_QUEUE_SERIAL);
|
|
_syncLimiter = dispatch_semaphore_create(kMaxEnqueuedSyncs);
|
|
|
|
_eventBatchSize = kDefaultEventBatchSize;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark SNTSyncServiceXPC methods
|
|
|
|
- (void)postEventsToSyncServer:(NSArray<SNTStoredEvent *> *)events fromBundle:(BOOL)isFromBundle {
|
|
SNTSyncStatusType status = SNTSyncStatusTypeUnknown;
|
|
SNTSyncState *syncState = [self createSyncStateWithStatus:&status];
|
|
if (!syncState) {
|
|
LOGE(@"Events upload failed to create sync state: %ld", status);
|
|
return;
|
|
}
|
|
if (isFromBundle) syncState.eventBatchSize = self.eventBatchSize;
|
|
SNTSyncEventUpload *p = [[SNTSyncEventUpload alloc] initWithState:syncState];
|
|
if (events && [p uploadEvents:events]) {
|
|
LOGD(@"Events upload complete");
|
|
} else {
|
|
LOGE(@"Events upload failed. Will retry again once %@ is reachable",
|
|
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
|
|
[self startReachability];
|
|
}
|
|
self.xsrfToken = syncState.xsrfToken;
|
|
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
|
}
|
|
|
|
- (void)postBundleEventToSyncServer:(SNTStoredEvent *)event
|
|
reply:(void (^)(SNTBundleEventAction))reply {
|
|
if (!event) {
|
|
reply(SNTBundleEventActionDropEvents);
|
|
return;
|
|
}
|
|
SNTSyncStatusType status = SNTSyncStatusTypeUnknown;
|
|
SNTSyncState *syncState = [self createSyncStateWithStatus:&status];
|
|
if (!syncState) {
|
|
LOGE(@"Bundle event upload failed to create sync state: %ld", status);
|
|
reply(SNTBundleEventActionDropEvents);
|
|
return;
|
|
}
|
|
SNTSyncEventUpload *p = [[SNTSyncEventUpload alloc] initWithState:syncState];
|
|
if ([p uploadEvents:@[ event ]]) {
|
|
if ([syncState.bundleBinaryRequests containsObject:event.fileBundleHash]) {
|
|
reply(SNTBundleEventActionSendEvents);
|
|
LOGD(@"Needs related events");
|
|
} else {
|
|
reply(SNTBundleEventActionDropEvents);
|
|
LOGD(@"Bundle event upload complete");
|
|
}
|
|
} else {
|
|
// Related bundle events will be stored and eventually synced, whether the server actually
|
|
// wanted them or not. If they weren't needed the server will simply ignore them.
|
|
reply(SNTBundleEventActionStoreEvents);
|
|
LOGE(@"Bundle event upload failed. Will retry again once %@ is reachable",
|
|
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
|
|
[self startReachability];
|
|
}
|
|
self.xsrfToken = syncState.xsrfToken;
|
|
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
|
}
|
|
|
|
- (void)isFCMListening:(void (^)(BOOL))reply {
|
|
reply(self.pushNotifications.isConnected);
|
|
}
|
|
|
|
#pragma mark sync control / SNTPushNotificationsDelegate methods
|
|
|
|
- (void)sync {
|
|
[self syncSecondsFromNow:0];
|
|
}
|
|
|
|
- (void)syncSecondsFromNow:(uint64_t)seconds {
|
|
[self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:seconds];
|
|
}
|
|
|
|
- (void)syncType:(SNTSyncType)syncType withReply:(void (^)(SNTSyncStatusType))reply {
|
|
if (dispatch_semaphore_wait(self.syncLimiter, DISPATCH_TIME_NOW)) {
|
|
if (reply) reply(SNTSyncStatusTypeTooManySyncsInProgress);
|
|
return;
|
|
}
|
|
dispatch_async(self.syncQueue, ^() {
|
|
SLOGI(@"Starting sync...");
|
|
if (syncType != SNTSyncTypeNormal) {
|
|
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
[[self.daemonConn remoteObjectProxy] setSyncTypeRequired:syncType
|
|
reply:^() {
|
|
dispatch_semaphore_signal(sema);
|
|
}];
|
|
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) {
|
|
SLOGE(@"Timeout waiting for daemon");
|
|
if (reply) reply(SNTSyncStatusTypeDaemonTimeout);
|
|
return;
|
|
}
|
|
}
|
|
if (reply) reply(SNTSyncStatusTypeSyncStarted);
|
|
SNTSyncStatusType status = [self preflight];
|
|
if (reply) reply(status);
|
|
dispatch_semaphore_signal(self.syncLimiter);
|
|
});
|
|
}
|
|
|
|
- (void)ruleSync {
|
|
[self ruleSyncSecondsFromNow:0];
|
|
}
|
|
|
|
- (void)ruleSyncSecondsFromNow:(uint64_t)seconds {
|
|
[self rescheduleTimerQueue:self.ruleSyncTimer secondsFromNow:seconds];
|
|
}
|
|
|
|
- (void)rescheduleTimerQueue:(dispatch_source_t)timerQueue secondsFromNow:(uint64_t)seconds {
|
|
uint64_t interval = seconds * NSEC_PER_SEC;
|
|
uint64_t leeway = (seconds * 0.5) * NSEC_PER_SEC;
|
|
dispatch_source_set_timer(timerQueue, dispatch_walltime(NULL, interval), interval, leeway);
|
|
}
|
|
- (void)ruleSyncImpl {
|
|
// Rule only syncs are exclusivly scheduled by self.ruleSyncTimer. We do not need to worry about
|
|
// using self.syncLimiter here. However we do want to do the work on self.syncQueue so we do not
|
|
// overlap with a full sync.
|
|
dispatch_async(self.syncQueue, ^() {
|
|
if (![[SNTConfigurator configurator] syncBaseURL]) return;
|
|
SNTSyncStatusType status = SNTSyncStatusTypeUnknown;
|
|
SNTSyncState *syncState = [self createSyncStateWithStatus:&status];
|
|
if (!syncState) {
|
|
LOGE(@"Rule sync failed to create sync state: %ld", status);
|
|
return;
|
|
}
|
|
SNTSyncRuleDownload *p = [[SNTSyncRuleDownload alloc] initWithState:syncState];
|
|
BOOL ret = [p sync];
|
|
LOGD(@"Rule download %@", ret ? @"complete" : @"failed");
|
|
self.xsrfToken = syncState.xsrfToken;
|
|
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
|
});
|
|
}
|
|
|
|
- (void)preflightSync {
|
|
[self preflightOnly:YES];
|
|
}
|
|
|
|
#pragma mark syncing chain
|
|
|
|
- (SNTSyncStatusType)preflight {
|
|
return [self preflightOnly:NO];
|
|
}
|
|
|
|
- (SNTSyncStatusType)preflightOnly:(BOOL)preflightOnly {
|
|
SNTSyncStatusType status = SNTSyncStatusTypeUnknown;
|
|
SNTSyncState *syncState = [self createSyncStateWithStatus:&status];
|
|
if (!syncState) {
|
|
return status;
|
|
}
|
|
|
|
SLOGD(@"Preflight starting");
|
|
SNTSyncPreflight *p = [[SNTSyncPreflight alloc] initWithState:syncState];
|
|
if ([p sync]) {
|
|
SLOGD(@"Preflight complete");
|
|
self.xsrfToken = syncState.xsrfToken;
|
|
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
|
|
|
// Clean up reachability if it was started for a non-network error
|
|
[self stopReachability];
|
|
|
|
self.eventBatchSize = syncState.eventBatchSize;
|
|
|
|
// Start listening for push notifications with a full sync every
|
|
// pushNotificationsFullSyncInterval.
|
|
if ([SNTConfigurator configurator].fcmEnabled) {
|
|
[self.pushNotifications listenWithSyncState:syncState];
|
|
} else {
|
|
LOGD(@"Push notifications are not enabled. Sync every %lu min.",
|
|
syncState.fullSyncInterval / 60);
|
|
[self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:syncState.fullSyncInterval];
|
|
}
|
|
|
|
if (preflightOnly) return SNTSyncStatusTypeSuccess;
|
|
return [self eventUploadWithSyncState:syncState];
|
|
}
|
|
|
|
SLOGE(@"Preflight failed, will try again once %@ is reachable",
|
|
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
|
|
[self startReachability];
|
|
return SNTSyncStatusTypePreflightFailed;
|
|
}
|
|
|
|
- (SNTSyncStatusType)eventUploadWithSyncState:(SNTSyncState *)syncState {
|
|
SLOGD(@"Event upload starting");
|
|
SNTSyncEventUpload *p = [[SNTSyncEventUpload alloc] initWithState:syncState];
|
|
if ([p sync]) {
|
|
SLOGD(@"Event upload complete");
|
|
return [self ruleDownloadWithSyncState:syncState];
|
|
}
|
|
|
|
SLOGE(@"Event upload failed, aborting run");
|
|
return SNTSyncStatusTypeEventUploadFailed;
|
|
}
|
|
|
|
- (SNTSyncStatusType)ruleDownloadWithSyncState:(SNTSyncState *)syncState {
|
|
SLOGD(@"Rule download starting");
|
|
SNTSyncRuleDownload *p = [[SNTSyncRuleDownload alloc] initWithState:syncState];
|
|
if ([p sync]) {
|
|
SLOGD(@"Rule download complete");
|
|
return [self postflightWithSyncState:syncState];
|
|
}
|
|
|
|
SLOGE(@"Rule download failed, aborting run");
|
|
return SNTSyncStatusTypeRuleDownloadFailed;
|
|
}
|
|
|
|
- (SNTSyncStatusType)postflightWithSyncState:(SNTSyncState *)syncState {
|
|
SLOGD(@"Postflight starting");
|
|
SNTSyncPostflight *p = [[SNTSyncPostflight alloc] initWithState:syncState];
|
|
if ([p sync]) {
|
|
SLOGD(@"Postflight complete");
|
|
SLOGI(@"Sync completed successfully");
|
|
return SNTSyncStatusTypeSuccess;
|
|
}
|
|
SLOGE(@"Postflight failed");
|
|
return SNTSyncStatusTypePostflightFailed;
|
|
}
|
|
|
|
#pragma mark internal helpers
|
|
|
|
- (dispatch_source_t)createSyncTimerWithBlock:(void (^)(void))block {
|
|
dispatch_source_t timerQueue =
|
|
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
|
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
|
dispatch_source_set_event_handler(timerQueue, block);
|
|
dispatch_resume(timerQueue);
|
|
return timerQueue;
|
|
}
|
|
|
|
- (SNTSyncState *)createSyncStateWithStatus:(SNTSyncStatusType *)status {
|
|
// Gather some data needed during some sync stages
|
|
SNTSyncState *syncState = [[SNTSyncState alloc] init];
|
|
SNTConfigurator *config = [SNTConfigurator configurator];
|
|
|
|
syncState.syncBaseURL = config.syncBaseURL;
|
|
if (syncState.syncBaseURL.absoluteString.length == 0) {
|
|
SLOGE(@"Missing SyncBaseURL. Can't sync without it.");
|
|
if (*status) *status = SNTSyncStatusTypeMissingSyncBaseURL;
|
|
return nil;
|
|
} else if (![syncState.syncBaseURL.scheme isEqual:@"https"]) {
|
|
SLOGW(@"SyncBaseURL is not over HTTPS!");
|
|
}
|
|
|
|
syncState.machineID = config.machineID;
|
|
if (syncState.machineID.length == 0) {
|
|
SLOGE(@"Missing Machine ID. Can't sync without it.");
|
|
if (*status) *status = SNTSyncStatusTypeMissingMachineID;
|
|
return nil;
|
|
}
|
|
|
|
syncState.machineOwner = config.machineOwner;
|
|
if (syncState.machineOwner.length == 0) {
|
|
syncState.machineOwner = @"";
|
|
SLOGW(@"Missing Machine Owner.");
|
|
}
|
|
|
|
syncState.xsrfToken = self.xsrfToken;
|
|
syncState.xsrfTokenHeader = self.xsrfTokenHeader;
|
|
|
|
NSURLSessionConfiguration *sessConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
sessConfig.connectionProxyDictionary = [[SNTConfigurator configurator] syncProxyConfig];
|
|
|
|
MOLAuthenticatingURLSession *authURLSession =
|
|
[[MOLAuthenticatingURLSession alloc] initWithSessionConfiguration:sessConfig];
|
|
authURLSession.userAgent = @"santactl-sync/";
|
|
NSString *santactlVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
|
if (santactlVersion) {
|
|
authURLSession.userAgent = [authURLSession.userAgent stringByAppendingString:santactlVersion];
|
|
}
|
|
authURLSession.refusesRedirects = YES;
|
|
authURLSession.serverHostname = syncState.syncBaseURL.host;
|
|
authURLSession.loggingBlock = ^(NSString *line) {
|
|
SLOGD(@"%@", line);
|
|
};
|
|
|
|
// Configure server auth
|
|
if ([config syncServerAuthRootsFile]) {
|
|
authURLSession.serverRootsPemFile = [config syncServerAuthRootsFile];
|
|
} else if ([config syncServerAuthRootsData]) {
|
|
authURLSession.serverRootsPemData = [config syncServerAuthRootsData];
|
|
}
|
|
|
|
// Configure client auth
|
|
if ([config syncClientAuthCertificateFile]) {
|
|
authURLSession.clientCertFile = [config syncClientAuthCertificateFile];
|
|
authURLSession.clientCertPassword = [config syncClientAuthCertificatePassword];
|
|
} else if ([config syncClientAuthCertificateCn]) {
|
|
authURLSession.clientCertCommonName = [config syncClientAuthCertificateCn];
|
|
} else if ([config syncClientAuthCertificateIssuer]) {
|
|
authURLSession.clientCertIssuerCn = [config syncClientAuthCertificateIssuer];
|
|
}
|
|
|
|
syncState.session = [authURLSession session];
|
|
syncState.daemonConn = self.daemonConn;
|
|
syncState.contentEncoding = config.syncClientContentEncoding;
|
|
syncState.pushNotificationsToken = self.pushNotifications.token;
|
|
|
|
return syncState;
|
|
}
|
|
|
|
#pragma mark reachability methods
|
|
|
|
- (void)setReachable:(BOOL)reachable {
|
|
_reachable = reachable;
|
|
if (reachable) {
|
|
LOGD(@"Internet connection has been restored, triggering a new sync.");
|
|
[self stopReachability];
|
|
[self sync];
|
|
}
|
|
}
|
|
|
|
// Start listening for network state changes.
|
|
- (void)startReachability {
|
|
if (self.pathMonitor) return;
|
|
self.pathMonitor = nw_path_monitor_create();
|
|
// Put the callback on the main thread to ensure serial access.
|
|
nw_path_monitor_set_queue(self.pathMonitor, dispatch_get_main_queue());
|
|
nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) {
|
|
// Only call the setter when there is a change. This will filter out the redundant calls to
|
|
// this callback whenever the network interface states change.
|
|
int reachable = nw_path_get_status(path) == nw_path_status_satisfied;
|
|
if (self.reachable != reachable) {
|
|
self.reachable = reachable;
|
|
}
|
|
});
|
|
nw_path_monitor_set_cancel_handler(self.pathMonitor, ^{
|
|
self.pathMonitor = nil;
|
|
});
|
|
nw_path_monitor_start(self.pathMonitor);
|
|
}
|
|
|
|
// Stop listening for network state changes
|
|
- (void)stopReachability {
|
|
if (!self.pathMonitor) return;
|
|
nw_path_monitor_cancel(self.pathMonitor);
|
|
}
|
|
|
|
@end
|