SFAnalyticsLogger.m [plain text]
/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#if __OBJC2__
#import "SFAnalyticsLogger.h"
#import "SFSQLite.h"
#import "CKKSViewManager.h"
#import "debugging.h"
#import <objc/runtime.h>
#import <os/variant_private.h>
#import <CoreFoundation/CFPriv.h>
NSString* const SFAnalyticsLoggerTableSuccessCount = @"success_count";
NSString* const SFAnalyticsLoggerColumnEventType = @"event_type";
NSString* const SFAnalyticsLoggerColumnSuccessCount = @"success_count";
NSString* const SFAnalyticsLoggerColumnHardFailureCount = @"hard_failure_count";
NSString* const SFAnalyticsLoggerColumnSoftFailureCount = @"soft_failure_count";
NSString* const SFAnalyticsLoggerTableHardFailures = @"hard_failures";
NSString* const SFAnalyticsLoggerTableSoftFailures = @"soft_failures";
NSString* const SFAnalyticsLoggerTableAllEvents = @"all_events";
NSString* const SFAnalyticsLoggerColumnDate = @"timestamp";
NSString* const SFAnalyticsLoggerColumnData = @"data";
NSString* const SFAnalyticsLoggerUploadDate = @"upload_date";
NSString* const SFAnalyticsLoggerSplunkTopic = @"topic";
NSString* const SFAnalyticsLoggerSplunkEventTime = @"eventTime";
NSString* const SFAnalyticsLoggerSplunkPostTime = @"postTime";
NSString* const SFAnalyticsLoggerSplunkEventType = @"eventType";
NSString* const SFAnalyticsLoggerSplunkEventBuild = @"build";
NSString* const SFAnalyticsLoggerSplunkEventProduct = @"product";
NSString* const SFAnalyticsLoggerMetricsBase = @"metricsBase";
NSString* const SFAnalyticsLoggerEventClassKey = @"eventClass";
NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics";
static NSString* const SFAnalyticsLoggerTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n"
@"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
@"timestamp REAL,"
@"data BLOB\n"
@");\n"
@"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures AFTER INSERT ON hard_failures\n"
@"BEGIN\n"
@"DELETE FROM hard_failures WHERE id != NEW.id AND id @"END;\n"
@"CREATE TABLE IF NOT EXISTS soft_failures (\n"
@"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
@"timestamp REAL,"
@"data BLOB\n"
@");\n"
@"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures AFTER INSERT ON soft_failures\n"
@"BEGIN\n"
@"DELETE FROM soft_failures WHERE id != NEW.id AND id @"END;\n"
@"CREATE TABLE IF NOT EXISTS all_events (\n"
@"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
@"timestamp REAL,"
@"data BLOB\n"
@");\n"
@"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_all_events AFTER INSERT ON all_events\n"
@"BEGIN\n"
@"DELETE FROM all_events WHERE id != NEW.id AND id @"END;\n"
@"CREATE TABLE IF NOT EXISTS success_count (\n"
@"event_type STRING PRIMARY KEY,\n"
@"success_count INTEGER,\n"
@"hard_failure_count INTEGER,\n"
@"soft_failure_count INTEGER\n"
@");\n";
#define SFANALYTICS_SPLUNK_DEV 0
#define SFANALYTICS_MAX_EVENTS_TO_REPORT 999
#define SECONDS_PER_DAY (60 * 60 * 24)
#if SFANALYTICS_SPLUNK_DEV
#define SECONDS_BETWEEN_UPLOADS_CUSTOMER 10
#define SECONDS_BETWEEN_UPLOADS_INTERNAL 10
#else
#define SECONDS_BETWEEN_UPLOADS_CUSTOMER (3 * SECONDS_PER_DAY)
#define SECONDS_BETWEEN_UPLOADS_INTERNAL (SECONDS_PER_DAY)
#endif
typedef NS_ENUM(NSInteger, SFAnalyticsEventClass) {
SFAnalyticsEventClassSuccess,
SFAnalyticsEventClassHardFailure,
SFAnalyticsEventClassSoftFailure,
SFAnalyticsEventClassNote
};
@interface SFAnalyticsLoggerSQLiteStore : SFSQLite
@property (readonly, strong) NSArray* failureRecords;
@property (readonly, strong) NSArray* allEvents;
@property (readwrite, strong) NSDate* uploadDate;
+ (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema;
- (void)incrementSuccessCountForEventType:(NSString*)eventType;
- (void)incrementHardFailureCountForEventType:(NSString*)eventType;
- (void)incrementSoftFailureCountForEventType:(NSString*)eventType;
- (NSInteger)successCountForEventType:(NSString*)eventType;
- (NSInteger)hardFailureCountForEventType:(NSString*)eventType;
- (NSInteger)softFailureCountForEventType:(NSString*)eventType;
- (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table;
- (void)clearAllData;
- (BOOL)tryToOpenDatabase;
- (NSDictionary*)summaryCounts;
@end
@implementation SFAnalyticsLogger {
SFAnalyticsLoggerSQLiteStore* _database;
NSURL* _splunkUploadURL;
NSString* _splunkTopicName;
NSURL* _splunkBagURL;
dispatch_queue_t _queue;
NSInteger _secondsBetweenUploads;
NSDictionary* _metricsBase; // data the server provides and wants us to send back
NSArray* _blacklistedFields;
NSArray* _blacklistedEvents;
unsigned int _allowInsecureSplunkCert:1;
unsigned int _disableLogging:1;
unsigned int _disableUploads:1;
unsigned int _ignoreServersMessagesTellingUsToGoAway:1;
}
@synthesize splunkUploadURL = _splunkUploadURL;
@synthesize splunkBagURL = _splunkBagURL;
@synthesize splunkTopicName = _splunkTopicName;
@synthesize splunkLoggingQueue = _queue;
+ (instancetype)logger
{
#if TARGET_OS_SIMULATOR
return nil;
#else
if (self == [SFAnalyticsLogger class]) {
secerror("attempt to instatiate abstract class SFAnalyticsLogger");
return nil;
}
SFAnalyticsLogger* logger = nil;
@synchronized(self) {
logger = objc_getAssociatedObject(self, "SFAnalyticsLoggerInstance");
if (!logger) {
logger = [[self alloc] init];
objc_setAssociatedObject(self, "SFAnalyticsLoggerInstance", logger, OBJC_ASSOCIATION_RETAIN);
}
}
return logger;
#endif
}
+ (NSString*)databasePath
{
return nil;
}
+ (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
{
NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
if (timeIntervalSinceDate < SECONDS_PER_DAY) {
return 0;
}
else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 7)) {
return 1;
}
else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 30)) {
return 7;
}
else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 365)) {
return 30;
}
else {
return 365;
}
}
- (instancetype)init
{
if (self = [super init]) {
_database = [SFAnalyticsLoggerSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsLoggerTableSchema];
_queue = dispatch_queue_create("com.apple.security.analytics", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
if (os_variant_has_internal_diagnostics("Security")) {
_secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_INTERNAL;
} else {
_secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_CUSTOMER;
}
NSDictionary* systemDefaultValues = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle bundleWithPath:@"/System/Library/Frameworks/Security.framework"] pathForResource:@"SFAnalyticsLogging" ofType:@"plist"]];
_splunkTopicName = systemDefaultValues[@"splunk_topic"];
_splunkUploadURL = [NSURL URLWithString:systemDefaultValues[@"splunk_uploadURL"]];
_splunkBagURL = [NSURL URLWithString:systemDefaultValues[@"splunk_bagURL"]];
_allowInsecureSplunkCert = [[systemDefaultValues valueForKey:@"splunk_allowInsecureCertificate"] boolValue];
NSString* splunkEndpoint = systemDefaultValues[@"splunk_endpointDomain"];
NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:SFAnalyticsUserDefaultsSuite];
NSString* userDefaultsSplunkTopic = [defaults stringForKey:@"splunk_topic"];
if (userDefaultsSplunkTopic) {
_splunkTopicName = userDefaultsSplunkTopic;
}
NSURL* userDefaultsSplunkUploadURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_uploadURL"]];
if (userDefaultsSplunkUploadURL) {
_splunkUploadURL = userDefaultsSplunkUploadURL;
}
NSURL* userDefaultsSplunkBagURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_bagURL"]];
if (userDefaultsSplunkUploadURL) {
_splunkBagURL = userDefaultsSplunkBagURL;
}
BOOL userDefaultsAllowInsecureSplunkCert = [defaults boolForKey:@"splunk_allowInsecureCertificate"];
_allowInsecureSplunkCert |= userDefaultsAllowInsecureSplunkCert;
NSString* userDefaultsSplunkEndpoint = [defaults stringForKey:@"splunk_endpointDomain"];
if (userDefaultsSplunkEndpoint) {
splunkEndpoint = userDefaultsSplunkEndpoint;
}
#if SFANALYTICS_SPLUNK_DEV
_ignoreServersMessagesTellingUsToGoAway = YES;
if (!_splunkUploadURL && splunkEndpoint) {
NSString* urlString = [NSString stringWithFormat:@"https://%@/report/2/ _splunkUploadURL = [NSURL URLWithString:urlString];
}
#else
(void)splunkEndpoint;
#endif
}
return self;
}
- (void)logSuccessForEventNamed:(NSString*)eventName
{
[self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil];
}
- (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
{
[self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes];
}
- (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
{
[self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes];
}
- (void)noteEventNamed:(NSString*)eventName
{
[self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil];
}
- (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
{
if (!eventName) {
secinfo("SFAnalytics", "attempt to log an event with no name");
return;
}
__block NSDate* uploadDate = nil;
__weak __typeof(self) weakSelf = self;
dispatch_sync(_queue, ^{
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf || strongSelf->_disableLogging || [strongSelf->_blacklistedEvents containsObject:eventName]) {
return;
}
NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class];
[strongSelf->_database addEventDict:eventDict toTable:SFAnalyticsLoggerTableAllEvents];
if (class == SFAnalyticsEventClassHardFailure) {
NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
[strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableHardFailures];
[strongSelf->_database incrementHardFailureCountForEventType:eventName];
}
else if (class == SFAnalyticsEventClassSoftFailure) {
NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
[strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableSoftFailures];
[strongSelf->_database incrementSoftFailureCountForEventType:eventName];
}
else if (class == SFAnalyticsEventClassSuccess || class == SFAnalyticsEventClassNote) {
[strongSelf->_database incrementSuccessCountForEventType:eventName];
}
uploadDate = strongSelf->_database.uploadDate;
});
NSDate* nowDate = [NSDate date];
if (uploadDate) {
if ([nowDate compare:uploadDate] == NSOrderedDescending) {
NSError* error = nil;
BOOL uploadSuccess = [self forceUploadWithError:&error];
if (uploadSuccess) {
secinfo("SFAnalytics", "uploaded sync health data");
[self resetUploadDate:YES];
}
if (error) {
secerror("SFAnalytics: failed to upload json to analytics server with error: }
}
}
else {
[self resetUploadDate:NO];
}
}
- (void)resetUploadDate:(BOOL)clearData
{
__weak __typeof(self) weakSelf = self;
dispatch_sync(_queue, ^{
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (clearData) {
[strongSelf->_database clearAllData];
}
strongSelf->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:strongSelf->_secondsBetweenUploads];
});
}
- (NSDictionary*)eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass
{
NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary];
eventDict[SFAnalyticsLoggerSplunkTopic] = _splunkTopicName;
eventDict[SFAnalyticsLoggerSplunkEventType] = eventName;
eventDict[SFAnalyticsLoggerSplunkEventTime] = @([[NSDate date] timeIntervalSince1970] * 1000);
eventDict[SFAnalyticsLoggerEventClassKey] = @(eventClass);
[_metricsBase enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
if (!eventDict[key]) {
eventDict[key] = object;
}
}];
return eventDict;
}
- (NSDictionary*)eventDictWithBlacklistedFieldsStrippedFrom:(NSDictionary*)eventDict
{
NSMutableDictionary* strippedDict = eventDict.mutableCopy;
for (NSString* blacklistedField in _blacklistedFields) {
[strippedDict removeObjectForKey:blacklistedField];
}
return strippedDict;
}
- (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
{
dispatch_sync(_queue, ^{
[self->_database setDateProperty:date forKey:key];
});
}
- (NSDate*)datePropertyForKey:(NSString*)key
{
__block NSDate* result = nil;
dispatch_sync(_queue, ^{
result = [self->_database datePropertyForKey:key];
});
return result;
}
- (NSDictionary*)extraValuesToUploadToServer
{
return [NSDictionary dictionary];
}
// this method is kind of evil for the fact that it has side-effects in pulling other things besides the metricsURL from the server, and as such should NOT be memoized.
// TODO redo this, probably to return a dictionary.
- (NSURL*)splunkUploadURL
{
dispatch_assert_queue(_queue);
if (_splunkUploadURL) {
return _splunkUploadURL;
}
__weak __typeof(self) weakSelf = self;
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block NSError* error = nil;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/
NSURLSession* storeBagSession = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
NSURL* requestEndpoint = _splunkBagURL;
__block NSURL* result = nil;
NSURLSessionDataTask* storeBagTask = [storeBagSession dataTaskWithURL:requestEndpoint completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable __unused response,
NSError * _Nullable responseError) {
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (data && !responseError) {
NSData *responseData = data; // shut up compiler
NSDictionary* responseDict = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
if([responseDict isKindOfClass:NSDictionary.class] && !error) {
if (!self->_ignoreServersMessagesTellingUsToGoAway) {
strongSelf->_disableLogging = [[responseDict valueForKey:@"disabled"] boolValue];
if (strongSelf->_disableLogging || [[responseDict valueForKey:@"sendDisabled"] boolValue]) {
// then don't upload anything right now
secerror("not returning a splunk URL because uploads are disabled");
dispatch_semaphore_signal(sem);
return;
}
NSUInteger millisecondsBetweenUploads = [[responseDict valueForKey:@"postFrequency"] unsignedIntegerValue] / 1000;
if (millisecondsBetweenUploads > 0) {
strongSelf->_secondsBetweenUploads = millisecondsBetweenUploads;
}
strongSelf->_blacklistedEvents = responseDict[@"blacklistedEvents"];
strongSelf->_blacklistedFields = responseDict[@"blacklistedFields"];
}
strongSelf->_metricsBase = responseDict[@"metricsBase"];
NSString* metricsEndpoint = responseDict[@"metricsUrl"];
if([metricsEndpoint isKindOfClass:NSString.class]) {
/* Lives our URL */
NSString* endpoint = [metricsEndpoint stringByAppendingFormat:@"/2/ secnotice("ckks", "got metrics endpoint: NSURL* endpointURL = [NSURL URLWithString:endpoint];
if([endpointURL.scheme isEqualToString:@"https"]) {
result = endpointURL;
}
}
}
}
else {
error = responseError;
}
if(error) {
secnotice("ckks", "Unable to fetch splunk endpoint at URL: }
else if(!result) {
secnotice("ckks", "Malformed iTunes config payload!");
}
dispatch_semaphore_signal(sem);
}];
[storeBagTask resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
return result;
}
- (BOOL)forceUploadWithError:(NSError**)error
{
__block BOOL result = NO;
NSData* json = [self getLoggingJSON:false error: error];
dispatch_sync(_queue, ^{
if (json && [self _onQueuePostJSON:json error:error]) {
secinfo("ckks", "uploading sync health data:
[self->_database clearAllData];
self->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:self->_secondsBetweenUploads];
result = YES;
}
else {
result = NO;
}
});
return result;
}
- (BOOL)_onQueuePostJSON:(NSData*)json error:(NSError**)error
{
dispatch_assert_queue(_queue);
/*
* Create the NSURLSession
* We use the ephemeral session config because we don't need cookies or cache
*/
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/
NSURLSession* postSession = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
/*
* Create the request
*/
NSURL* postEndpoint = self.splunkUploadURL;
if (!postEndpoint) {
secerror("failed to get a splunk upload endpoint - not uploading");
return NO;
}
NSMutableURLRequest* postRequest = [[NSMutableURLRequest alloc] init];
postRequest.URL = postEndpoint;
postRequest.HTTPMethod = @"POST";
postRequest.HTTPBody = json;
/*
* Create the upload task.
*/
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block BOOL uploadSuccess = NO;
NSURLSessionDataTask* uploadTask = [postSession dataTaskWithRequest:postRequest
completionHandler:^(NSData * _Nullable __unused data, NSURLResponse * _Nullable response, NSError * _Nullable requestError) {
if(requestError) {
secerror("Error in uploading the events to splunk: }
else if (![response isKindOfClass:NSHTTPURLResponse.class]){
Class class = response.class;
secerror("Received the wrong kind of response: }
else {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
if(httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
/* Success */
uploadSuccess = YES;
secnotice("ckks", "Splunk upload success");
}
else {
secnotice("ckks", "Splunk upload unexpected status to URL: }
}
dispatch_semaphore_signal(sem);
}];
secnotice("ckks", "Splunk upload start");
[uploadTask resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
return uploadSuccess;
}
- (NSString*)stringForEventClass:(SFAnalyticsEventClass)eventClass
{
if (eventClass == SFAnalyticsEventClassNote) {
return @"EventNote";
}
else if (eventClass == SFAnalyticsEventClassSuccess) {
return @"EventSuccess";
}
else if (eventClass == SFAnalyticsEventClassHardFailure) {
return @"EventHardFailure";
}
else if (eventClass == SFAnalyticsEventClassSoftFailure) {
return @"EventSoftFailure";
}
else {
return @"EventUnknown";
}
}
- (NSString*)sysdiagnoseStringForEventRecord:(NSDictionary*)eventRecord
{
NSMutableDictionary* mutableEventRecord = eventRecord.mutableCopy;
[mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkTopic];
NSDate* eventDate = [NSDate dateWithTimeIntervalSince1970:[[eventRecord valueForKey:SFAnalyticsLoggerSplunkEventTime] doubleValue] / 1000];
[mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventTime];
NSString* eventName = eventRecord[SFAnalyticsLoggerSplunkEventType];
[mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventType];
SFAnalyticsEventClass eventClass = [[eventRecord valueForKey:SFAnalyticsLoggerEventClassKey] integerValue];
NSString* eventClassString = [self stringForEventClass:eventClass];
[mutableEventRecord removeObjectForKey:SFAnalyticsLoggerEventClassKey];
NSMutableString* additionalAttributesString = [NSMutableString string];
if (mutableEventRecord.count > 0) {
[additionalAttributesString appendString:@" - Attributes: {" ];
__block BOOL firstAttribute = YES;
[mutableEventRecord enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
NSString* openingString = firstAttribute ? @"" : @", ";
[additionalAttributesString appendString:[NSString stringWithFormat:@" firstAttribute = NO;
}];
[additionalAttributesString appendString:@" }"];
}
return [NSString stringWithFormat:@"}
- (NSString*)getSysdiagnoseDumpWithError:(NSError**)error
{
NSMutableString* sysdiagnose = [[NSMutableString alloc] init];
NSDictionary* extraValues = self.extraValuesToUploadToServer;
[extraValues enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
[sysdiagnose appendFormat:@"Key: }];
[sysdiagnose appendString:@"\n"];
dispatch_sync(_queue, ^{
NSArray* allEvents = self->_database.allEvents;
for (NSDictionary* eventRecord in allEvents) {
[sysdiagnose appendFormat:@" }
});
return sysdiagnose;
}
+ (void)addOSVersion:(NSMutableDictionary *)event
{
static dispatch_once_t onceToken;
static NSString *build = NULL;
static NSString *product = NULL;
dispatch_once(&onceToken, ^{
NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
if (version == NULL)
return;
build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
});
if (build)
event[SFAnalyticsLoggerSplunkEventBuild] = build;
if (product)
event[SFAnalyticsLoggerSplunkEventProduct] = product;
}
- (NSData*)getLoggingJSON:(bool)pretty error:(NSError**)error
{
__block NSData* json = nil;
NSDictionary* extraValues = self.extraValuesToUploadToServer;
dispatch_sync(_queue, ^{
if (![self->_database tryToOpenDatabase]) {
// we should not even be here because uploadDate was nil. But since we are, let's get out of here.
// Returning nil here will abort the upload (but again, the uploadDate should've done that already)
secerror("can't get logging JSON because database is not openable");
if (error) {
*error = [NSError errorWithDomain:@"SFAnalyticsLogger" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"could not open db to read and process metrics (device in class D?)"}];
}
return;
}
NSArray* failureRecords = self->_database.failureRecords;
NSDictionary* successCounts = self->_database.summaryCounts;
NSInteger totalSuccessCount = 0;
NSInteger totalHardFailureCount = 0;
NSInteger totalSoftFailureCount = 0;
for (NSDictionary* perEventTypeSuccessCounts in successCounts.objectEnumerator) {
totalSuccessCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnSuccessCount] integerValue];
totalHardFailureCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnHardFailureCount] integerValue];
totalSoftFailureCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnSoftFailureCount] integerValue];
}
NSDate* now = [NSDate date];
NSMutableDictionary* healthSummaryEvent = extraValues ? extraValues.mutableCopy : [[NSMutableDictionary alloc] init];
healthSummaryEvent[SFAnalyticsLoggerSplunkTopic] = self->_splunkTopicName ?: [NSNull null];
healthSummaryEvent[SFAnalyticsLoggerSplunkEventTime] = @([now timeIntervalSince1970] * 1000);
healthSummaryEvent[SFAnalyticsLoggerSplunkEventType] = @"ckksHealthSummary";
healthSummaryEvent[SFAnalyticsLoggerColumnSuccessCount] = @(totalSuccessCount);
healthSummaryEvent[SFAnalyticsLoggerColumnHardFailureCount] = @(totalHardFailureCount);
healthSummaryEvent[SFAnalyticsLoggerColumnSoftFailureCount] = @(totalSoftFailureCount);
[SFAnalyticsLogger addOSVersion:healthSummaryEvent];
NSMutableArray* splunkRecords = failureRecords.mutableCopy;
[splunkRecords addObject:healthSummaryEvent];
NSDictionary* jsonDict = @{
SFAnalyticsLoggerSplunkPostTime : @([now timeIntervalSince1970] * 1000),
@"events" : splunkRecords
};
json = [NSJSONSerialization dataWithJSONObject:jsonDict
options:(pretty ? NSJSONWritingPrettyPrinted : 0)
error:error];
});
return json;
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
assert(completionHandler);
(void)session;
secnotice("ckks", "Splunk upload challenge");
NSURLCredential *cred = nil;
SecTrustResultType result = kSecTrustResultInvalid;
if ([challenge previousFailureCount] > 0) {
// Previous failures occurred, bail
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
} else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
/*
* Evaluate trust for the certificate
*/
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecTrustEvaluate(serverTrust, &result);
if (_allowInsecureSplunkCert || (result == kSecTrustResultProceed) || (result == kSecTrustResultUnspecified)) {
/*
* All is well, accept the credentials
*/
if(_allowInsecureSplunkCert) {
secnotice("ckks", "Force Accepting Splunk Credential");
}
cred = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
/*
* An error occurred in evaluating trust, bail
*/
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
/*
* Just perform the default handling
*/
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
- (BOOL)ignoreServerDisablingMessages
{
return _ignoreServersMessagesTellingUsToGoAway;
}
- (void)setIgnoreServerDisablingMessages:(BOOL)ignoreServer
{
_ignoreServersMessagesTellingUsToGoAway = ignoreServer ? YES : NO;
}
- (BOOL)allowsInsecureSplunkCert
{
return _allowInsecureSplunkCert;
}
- (void)setAllowsInsecureSplunkCert:(BOOL)allowsInsecureSplunkCert
{
_allowInsecureSplunkCert = allowsInsecureSplunkCert ? YES : NO;
}
@end
@implementation SFAnalyticsLoggerSQLiteStore
+ (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema
{
SFAnalyticsLoggerSQLiteStore* store = nil;
@synchronized([SFAnalyticsLoggerSQLiteStore class]) {
static NSMutableDictionary* loggingStores = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
loggingStores = [[NSMutableDictionary alloc] init];
});
NSString* standardizedPath = path.stringByStandardizingPath;
store = loggingStores[standardizedPath];
if (!store) {
store = [[self alloc] initWithPath:standardizedPath schema:schema];
loggingStores[standardizedPath] = store;
}
NSError* error = nil;
if (![store openWithError:&error]) {
secerror("SFAnalyticsLogger: could not open db at init, will try again later. Error: }
}
return store;
}
- (void)dealloc
{
[self close];
}
- (BOOL)tryToOpenDatabase
{
if (!self.isOpen) {
secwarning("SFAnalyticsLogger: db is closed, attempting to open");
NSError* error = nil;
if (![self openWithError:&error]) {
secerror("SFAnalyticsLogger: failed to open db with error return NO;
}
}
return YES;
}
- (NSInteger)successCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
return [[[[self select:@[SFAnalyticsLoggerColumnSuccessCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSuccessCount] integerValue];
}
return 0;
}
- (void)incrementSuccessCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
NSInteger successCount = [self successCountForEventType:eventType];
NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
[self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount + 1), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount)}];
}
}
- (NSInteger)hardFailureCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
return [[[[self select:@[SFAnalyticsLoggerColumnHardFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnHardFailureCount] integerValue];
}
return 0;
}
- (NSInteger)softFailureCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
return [[[[self select:@[SFAnalyticsLoggerColumnSoftFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSoftFailureCount] integerValue];
}
return 0;
}
- (void)incrementHardFailureCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
NSInteger successCount = [self successCountForEventType:eventType];
NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
[self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount + 1), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount)}];
}
}
- (void)incrementSoftFailureCountForEventType:(NSString*)eventType
{
if ([self tryToOpenDatabase]) {
NSInteger successCount = [self successCountForEventType:eventType];
NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
[self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount + 1)}];
}
}
- (NSDictionary*)summaryCounts
{
if ([self tryToOpenDatabase]) {
NSMutableDictionary* successCountsDict = [NSMutableDictionary dictionary];
NSArray* rows = [self selectAllFrom:SFAnalyticsLoggerTableSuccessCount where:nil bindings:nil];
for (NSDictionary* rowDict in rows) {
NSString* eventName = rowDict[SFAnalyticsLoggerColumnEventType];
if (!eventName) {
secinfo("SFAnalytics", "ignoring entry in success counts table without an event name");
continue;
}
successCountsDict[eventName] = @{SFAnalyticsLoggerTableSuccessCount : rowDict[SFAnalyticsLoggerColumnSuccessCount], SFAnalyticsLoggerColumnHardFailureCount : rowDict[SFAnalyticsLoggerColumnHardFailureCount], SFAnalyticsLoggerColumnSoftFailureCount : rowDict[SFAnalyticsLoggerColumnSoftFailureCount]};
}
return successCountsDict;
}
return [NSDictionary new];
}
- (NSArray*)failureRecords
{
if ([self tryToOpenDatabase]) {
NSArray* recordBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableHardFailures];
if (recordBlobs.count < SFANALYTICS_MAX_EVENTS_TO_REPORT) {
NSArray* softFailureBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableSoftFailures];
if (softFailureBlobs.count > 0) {
NSUInteger numSoftFailuresToReport = SFANALYTICS_MAX_EVENTS_TO_REPORT - recordBlobs.count;
if (numSoftFailuresToReport > softFailureBlobs.count)
numSoftFailuresToReport = softFailureBlobs.count;
recordBlobs = [recordBlobs arrayByAddingObjectsFromArray:[softFailureBlobs subarrayWithRange:NSMakeRange(softFailureBlobs.count - numSoftFailuresToReport, numSoftFailuresToReport)]];
}
}
NSMutableArray* failureRecords = [[NSMutableArray alloc] init];
for (NSDictionary* row in recordBlobs) {
NSMutableDictionary* deserializedRecord = [NSPropertyListSerialization propertyListWithData:row[SFAnalyticsLoggerColumnData] options:NSPropertyListMutableContainers format:nil error:nil];
[SFAnalyticsLogger addOSVersion:deserializedRecord];
[failureRecords addObject:deserializedRecord];
}
return failureRecords;
}
return [NSArray new];
}
- (NSArray*)allEvents
{
if ([self tryToOpenDatabase]) {
NSArray* recordBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableAllEvents];
NSMutableArray* records = [[NSMutableArray alloc] init];
for (NSDictionary* row in recordBlobs) {
NSDictionary* deserializedRecord = [NSPropertyListSerialization propertyListWithData:row[SFAnalyticsLoggerColumnData] options:0 format:nil error:nil];
[records addObject:deserializedRecord];
}
return records;
}
return [NSArray new];
}
- (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table
{
if ([self tryToOpenDatabase]) {
NSError* error = nil;
NSData* serializedRecord = [NSPropertyListSerialization dataWithPropertyList:eventDict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
if(!error && serializedRecord) {
[self insertOrReplaceInto:table values:@{SFAnalyticsLoggerColumnDate : [NSDate date], SFAnalyticsLoggerColumnData : serializedRecord}];
}
if(error && !serializedRecord) {
secerror("Couldn't serialize failure record: }
}
}
// the other returning methods give default values in case of closed db,
// but this needs to be nil so the comparison to 'now' fails and we don't upload
- (NSDate*)uploadDate
{
if ([self tryToOpenDatabase]) {
return [self datePropertyForKey:SFAnalyticsLoggerUploadDate];
}
return nil;
}
- (void)setUploadDate:(NSDate*)uploadDate
{
if ([self tryToOpenDatabase]) {
[self setDateProperty:uploadDate forKey:SFAnalyticsLoggerUploadDate];
}
}
- (void)clearAllData
{
if ([self tryToOpenDatabase]) {
[self deleteFrom:SFAnalyticsLoggerTableSuccessCount where:@"event_type like ?" bindings:@[@" [self deleteFrom:SFAnalyticsLoggerTableHardFailures where:@"id >= 0" bindings:nil];
[self deleteFrom:SFAnalyticsLoggerTableSoftFailures where:@"id >= 0" bindings:nil];
[self deleteFrom:SFAnalyticsLoggerTableAllEvents where:@"id >= 0" bindings:nil];
}
}
@end
#endif // __OBJC2__