2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import "SFAnalyticsLogger.h"
28 #import "CKKSViewManager.h"
30 #import <objc/runtime.h>
31 #import <os/variant_private.h>
32 #import <CoreFoundation/CFPriv.h>
34 NSString* const SFAnalyticsLoggerTableSuccessCount = @"success_count";
35 NSString* const SFAnalyticsLoggerColumnEventType = @"event_type";
36 NSString* const SFAnalyticsLoggerColumnSuccessCount = @"success_count";
37 NSString* const SFAnalyticsLoggerColumnHardFailureCount = @"hard_failure_count";
38 NSString* const SFAnalyticsLoggerColumnSoftFailureCount = @"soft_failure_count";
40 NSString* const SFAnalyticsLoggerTableHardFailures = @"hard_failures";
41 NSString* const SFAnalyticsLoggerTableSoftFailures = @"soft_failures";
42 NSString* const SFAnalyticsLoggerTableAllEvents = @"all_events";
43 NSString* const SFAnalyticsLoggerColumnDate = @"timestamp";
44 NSString* const SFAnalyticsLoggerColumnData = @"data";
46 NSString* const SFAnalyticsLoggerUploadDate = @"upload_date";
48 NSString* const SFAnalyticsLoggerSplunkTopic = @"topic";
49 NSString* const SFAnalyticsLoggerSplunkEventTime = @"eventTime";
50 NSString* const SFAnalyticsLoggerSplunkPostTime = @"postTime";
51 NSString* const SFAnalyticsLoggerSplunkEventType = @"eventType";
52 NSString* const SFAnalyticsLoggerSplunkEventBuild = @"build";
53 NSString* const SFAnalyticsLoggerSplunkEventProduct = @"product";
55 NSString* const SFAnalyticsLoggerMetricsBase = @"metricsBase";
56 NSString* const SFAnalyticsLoggerEventClassKey = @"eventClass";
59 NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics";
61 static NSString* const SFAnalyticsLoggerTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n"
62 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
66 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures AFTER INSERT ON hard_failures\n"
68 @"DELETE FROM hard_failures WHERE id != NEW.id AND id % 999 = NEW.id % 999;\n"
70 @"CREATE TABLE IF NOT EXISTS soft_failures (\n"
71 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
75 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures AFTER INSERT ON soft_failures\n"
77 @"DELETE FROM soft_failures WHERE id != NEW.id AND id % 999 = NEW.id % 999;\n"
79 @"CREATE TABLE IF NOT EXISTS all_events (\n"
80 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
84 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_all_events AFTER INSERT ON all_events\n"
86 @"DELETE FROM all_events WHERE id != NEW.id AND id % 10000 = NEW.id % 10000;\n"
88 @"CREATE TABLE IF NOT EXISTS success_count (\n"
89 @"event_type STRING PRIMARY KEY,\n"
90 @"success_count INTEGER,\n"
91 @"hard_failure_count INTEGER,\n"
92 @"soft_failure_count INTEGER\n"
95 #define SFANALYTICS_SPLUNK_DEV 0
96 #define SFANALYTICS_MAX_EVENTS_TO_REPORT 999
98 #define SECONDS_PER_DAY (60 * 60 * 24)
100 #if SFANALYTICS_SPLUNK_DEV
101 #define SECONDS_BETWEEN_UPLOADS_CUSTOMER 10
102 #define SECONDS_BETWEEN_UPLOADS_INTERNAL 10
104 #define SECONDS_BETWEEN_UPLOADS_CUSTOMER (3 * SECONDS_PER_DAY)
105 #define SECONDS_BETWEEN_UPLOADS_INTERNAL (SECONDS_PER_DAY)
108 typedef NS_ENUM(NSInteger, SFAnalyticsEventClass) {
109 SFAnalyticsEventClassSuccess,
110 SFAnalyticsEventClassHardFailure,
111 SFAnalyticsEventClassSoftFailure,
112 SFAnalyticsEventClassNote
115 @interface SFAnalyticsLoggerSQLiteStore : SFSQLite
117 @property (readonly, strong) NSArray* failureRecords;
118 @property (readonly, strong) NSArray* allEvents;
119 @property (readwrite, strong) NSDate* uploadDate;
121 + (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema;
123 - (void)incrementSuccessCountForEventType:(NSString*)eventType;
124 - (void)incrementHardFailureCountForEventType:(NSString*)eventType;
125 - (void)incrementSoftFailureCountForEventType:(NSString*)eventType;
126 - (NSInteger)successCountForEventType:(NSString*)eventType;
127 - (NSInteger)hardFailureCountForEventType:(NSString*)eventType;
128 - (NSInteger)softFailureCountForEventType:(NSString*)eventType;
129 - (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table;
130 - (void)clearAllData;
131 - (BOOL)tryToOpenDatabase;
133 - (NSDictionary*)summaryCounts;
137 @implementation SFAnalyticsLogger {
138 SFAnalyticsLoggerSQLiteStore* _database;
139 NSURL* _splunkUploadURL;
140 NSString* _splunkTopicName;
141 NSURL* _splunkBagURL;
142 dispatch_queue_t _queue;
143 NSInteger _secondsBetweenUploads;
144 NSDictionary* _metricsBase; // data the server provides and wants us to send back
145 NSArray* _blacklistedFields;
146 NSArray* _blacklistedEvents;
148 unsigned int _allowInsecureSplunkCert:1;
149 unsigned int _disableLogging:1;
150 unsigned int _disableUploads:1;
151 unsigned int _ignoreServersMessagesTellingUsToGoAway:1;
154 @synthesize splunkUploadURL = _splunkUploadURL;
155 @synthesize splunkBagURL = _splunkBagURL;
156 @synthesize splunkTopicName = _splunkTopicName;
157 @synthesize splunkLoggingQueue = _queue;
159 + (instancetype)logger
161 #if TARGET_OS_SIMULATOR
165 if (self == [SFAnalyticsLogger class]) {
166 secerror("attempt to instatiate abstract class SFAnalyticsLogger");
170 SFAnalyticsLogger* logger = nil;
171 @synchronized(self) {
172 logger = objc_getAssociatedObject(self, "SFAnalyticsLoggerInstance");
174 logger = [[self alloc] init];
175 objc_setAssociatedObject(self, "SFAnalyticsLoggerInstance", logger, OBJC_ASSOCIATION_RETAIN);
182 + (NSString*)databasePath
187 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
189 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
190 if (timeIntervalSinceDate < SECONDS_PER_DAY) {
193 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 7)) {
196 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 30)) {
199 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 365)) {
209 if (self = [super init]) {
210 _database = [SFAnalyticsLoggerSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsLoggerTableSchema];
211 _queue = dispatch_queue_create("com.apple.security.analytics", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
213 if (os_variant_has_internal_diagnostics("Security")) {
214 _secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_INTERNAL;
216 _secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_CUSTOMER;
219 NSDictionary* systemDefaultValues = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle bundleWithPath:@"/System/Library/Frameworks/Security.framework"] pathForResource:@"SFAnalyticsLogging" ofType:@"plist"]];
220 _splunkTopicName = systemDefaultValues[@"splunk_topic"];
221 _splunkUploadURL = [NSURL URLWithString:systemDefaultValues[@"splunk_uploadURL"]];
222 _splunkBagURL = [NSURL URLWithString:systemDefaultValues[@"splunk_bagURL"]];
223 _allowInsecureSplunkCert = [[systemDefaultValues valueForKey:@"splunk_allowInsecureCertificate"] boolValue];
224 NSString* splunkEndpoint = systemDefaultValues[@"splunk_endpointDomain"];
226 NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:SFAnalyticsUserDefaultsSuite];
227 NSString* userDefaultsSplunkTopic = [defaults stringForKey:@"splunk_topic"];
228 if (userDefaultsSplunkTopic) {
229 _splunkTopicName = userDefaultsSplunkTopic;
232 NSURL* userDefaultsSplunkUploadURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_uploadURL"]];
233 if (userDefaultsSplunkUploadURL) {
234 _splunkUploadURL = userDefaultsSplunkUploadURL;
237 NSURL* userDefaultsSplunkBagURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_bagURL"]];
238 if (userDefaultsSplunkUploadURL) {
239 _splunkBagURL = userDefaultsSplunkBagURL;
242 BOOL userDefaultsAllowInsecureSplunkCert = [defaults boolForKey:@"splunk_allowInsecureCertificate"];
243 _allowInsecureSplunkCert |= userDefaultsAllowInsecureSplunkCert;
245 NSString* userDefaultsSplunkEndpoint = [defaults stringForKey:@"splunk_endpointDomain"];
246 if (userDefaultsSplunkEndpoint) {
247 splunkEndpoint = userDefaultsSplunkEndpoint;
250 #if SFANALYTICS_SPLUNK_DEV
251 _ignoreServersMessagesTellingUsToGoAway = YES;
253 if (!_splunkUploadURL && splunkEndpoint) {
254 NSString* urlString = [NSString stringWithFormat:@"https://%@/report/2/%@", splunkEndpoint, _splunkTopicName];
255 _splunkUploadURL = [NSURL URLWithString:urlString];
258 (void)splunkEndpoint;
265 - (void)logSuccessForEventNamed:(NSString*)eventName
267 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil];
270 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
272 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes];
275 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
277 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes];
280 - (void)noteEventNamed:(NSString*)eventName
282 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil];
285 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
288 secinfo("SFAnalytics", "attempt to log an event with no name");
292 __block NSDate* uploadDate = nil;
293 __weak __typeof(self) weakSelf = self;
294 dispatch_sync(_queue, ^{
295 __strong __typeof(self) strongSelf = weakSelf;
296 if (!strongSelf || strongSelf->_disableLogging || [strongSelf->_blacklistedEvents containsObject:eventName]) {
300 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class];
301 [strongSelf->_database addEventDict:eventDict toTable:SFAnalyticsLoggerTableAllEvents];
303 if (class == SFAnalyticsEventClassHardFailure) {
304 NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
305 [strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableHardFailures];
306 [strongSelf->_database incrementHardFailureCountForEventType:eventName];
308 else if (class == SFAnalyticsEventClassSoftFailure) {
309 NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
310 [strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableSoftFailures];
311 [strongSelf->_database incrementSoftFailureCountForEventType:eventName];
313 else if (class == SFAnalyticsEventClassSuccess || class == SFAnalyticsEventClassNote) {
314 [strongSelf->_database incrementSuccessCountForEventType:eventName];
317 uploadDate = strongSelf->_database.uploadDate;
320 NSDate* nowDate = [NSDate date];
322 if ([nowDate compare:uploadDate] == NSOrderedDescending) {
323 NSError* error = nil;
324 BOOL uploadSuccess = [self forceUploadWithError:&error];
326 secinfo("SFAnalytics", "uploaded sync health data");
327 [self resetUploadDate:YES];
331 secerror("SFAnalytics: failed to upload json to analytics server with error: %@", error);
336 [self resetUploadDate:NO];
340 - (void)resetUploadDate:(BOOL)clearData
342 __weak __typeof(self) weakSelf = self;
343 dispatch_sync(_queue, ^{
344 __strong __typeof(self) strongSelf = weakSelf;
350 [strongSelf->_database clearAllData];
352 strongSelf->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:strongSelf->_secondsBetweenUploads];
356 - (NSDictionary*)eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass
358 NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary];
359 eventDict[SFAnalyticsLoggerSplunkTopic] = _splunkTopicName;
360 eventDict[SFAnalyticsLoggerSplunkEventType] = eventName;
361 eventDict[SFAnalyticsLoggerSplunkEventTime] = @([[NSDate date] timeIntervalSince1970] * 1000);
362 eventDict[SFAnalyticsLoggerEventClassKey] = @(eventClass);
364 [_metricsBase enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
365 if (!eventDict[key]) {
366 eventDict[key] = object;
373 - (NSDictionary*)eventDictWithBlacklistedFieldsStrippedFrom:(NSDictionary*)eventDict
375 NSMutableDictionary* strippedDict = eventDict.mutableCopy;
376 for (NSString* blacklistedField in _blacklistedFields) {
377 [strippedDict removeObjectForKey:blacklistedField];
382 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
384 dispatch_sync(_queue, ^{
385 [self->_database setDateProperty:date forKey:key];
389 - (NSDate*)datePropertyForKey:(NSString*)key
391 __block NSDate* result = nil;
392 dispatch_sync(_queue, ^{
393 result = [self->_database datePropertyForKey:key];
398 - (NSDictionary*)extraValuesToUploadToServer
400 return [NSDictionary dictionary];
403 // 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.
404 // TODO redo this, probably to return a dictionary.
405 - (NSURL*)splunkUploadURL
407 dispatch_assert_queue(_queue);
409 if (_splunkUploadURL) {
410 return _splunkUploadURL;
413 __weak __typeof(self) weakSelf = self;
414 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
416 __block NSError* error = nil;
417 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
419 configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/%s", SECURITY_BUILD_VERSION]};
421 NSURLSession* storeBagSession = [NSURLSession sessionWithConfiguration:configuration
425 NSURL* requestEndpoint = _splunkBagURL;
426 __block NSURL* result = nil;
427 NSURLSessionDataTask* storeBagTask = [storeBagSession dataTaskWithURL:requestEndpoint completionHandler:^(NSData * _Nullable data,
428 NSURLResponse * _Nullable __unused response,
429 NSError * _Nullable responseError) {
431 __strong __typeof(self) strongSelf = weakSelf;
436 if (data && !responseError) {
437 NSData *responseData = data; // shut up compiler
438 NSDictionary* responseDict = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
439 if([responseDict isKindOfClass:NSDictionary.class] && !error) {
440 if (!self->_ignoreServersMessagesTellingUsToGoAway) {
441 strongSelf->_disableLogging = [[responseDict valueForKey:@"disabled"] boolValue];
442 if (strongSelf->_disableLogging || [[responseDict valueForKey:@"sendDisabled"] boolValue]) {
443 // then don't upload anything right now
444 secerror("not returning a splunk URL because uploads are disabled");
445 dispatch_semaphore_signal(sem);
449 NSUInteger millisecondsBetweenUploads = [[responseDict valueForKey:@"postFrequency"] unsignedIntegerValue] / 1000;
450 if (millisecondsBetweenUploads > 0) {
451 strongSelf->_secondsBetweenUploads = millisecondsBetweenUploads;
454 strongSelf->_blacklistedEvents = responseDict[@"blacklistedEvents"];
455 strongSelf->_blacklistedFields = responseDict[@"blacklistedFields"];
458 strongSelf->_metricsBase = responseDict[@"metricsBase"];
460 NSString* metricsEndpoint = responseDict[@"metricsUrl"];
461 if([metricsEndpoint isKindOfClass:NSString.class]) {
463 NSString* endpoint = [metricsEndpoint stringByAppendingFormat:@"/2/%@", strongSelf->_splunkTopicName];
464 secnotice("ckks", "got metrics endpoint: %@", endpoint);
465 NSURL* endpointURL = [NSURL URLWithString:endpoint];
466 if([endpointURL.scheme isEqualToString:@"https"]) {
467 result = endpointURL;
473 error = responseError;
476 secnotice("ckks", "Unable to fetch splunk endpoint at URL: %@ -- error: %@", requestEndpoint, error.description);
479 secnotice("ckks", "Malformed iTunes config payload!");
482 dispatch_semaphore_signal(sem);
485 [storeBagTask resume];
486 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
491 - (BOOL)forceUploadWithError:(NSError**)error
493 __block BOOL result = NO;
494 NSData* json = [self getLoggingJSON:false error: error];
495 dispatch_sync(_queue, ^{
496 if (json && [self _onQueuePostJSON:json error:error]) {
497 secinfo("ckks", "uploading sync health data: %@", json);
499 [self->_database clearAllData];
500 self->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:self->_secondsBetweenUploads];
511 - (BOOL)_onQueuePostJSON:(NSData*)json error:(NSError**)error
513 dispatch_assert_queue(_queue);
516 * Create the NSURLSession
517 * We use the ephemeral session config because we don't need cookies or cache
519 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
521 configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/%s", SECURITY_BUILD_VERSION]};
523 NSURLSession* postSession = [NSURLSession sessionWithConfiguration:configuration
530 NSURL* postEndpoint = self.splunkUploadURL;
532 secerror("failed to get a splunk upload endpoint - not uploading");
536 NSMutableURLRequest* postRequest = [[NSMutableURLRequest alloc] init];
537 postRequest.URL = postEndpoint;
538 postRequest.HTTPMethod = @"POST";
539 postRequest.HTTPBody = json;
542 * Create the upload task.
544 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
545 __block BOOL uploadSuccess = NO;
546 NSURLSessionDataTask* uploadTask = [postSession dataTaskWithRequest:postRequest
547 completionHandler:^(NSData * _Nullable __unused data, NSURLResponse * _Nullable response, NSError * _Nullable requestError) {
549 secerror("Error in uploading the events to splunk: %@", requestError);
551 else if (![response isKindOfClass:NSHTTPURLResponse.class]){
552 Class class = response.class;
553 secerror("Received the wrong kind of response: %@", NSStringFromClass(class));
556 NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
557 if(httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
560 secnotice("ckks", "Splunk upload success");
563 secnotice("ckks", "Splunk upload unexpected status to URL: %@ -- status: %d", postEndpoint, (int)(httpResponse.statusCode));
566 dispatch_semaphore_signal(sem);
569 secnotice("ckks", "Splunk upload start");
571 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
572 return uploadSuccess;
575 - (NSString*)stringForEventClass:(SFAnalyticsEventClass)eventClass
577 if (eventClass == SFAnalyticsEventClassNote) {
580 else if (eventClass == SFAnalyticsEventClassSuccess) {
581 return @"EventSuccess";
583 else if (eventClass == SFAnalyticsEventClassHardFailure) {
584 return @"EventHardFailure";
586 else if (eventClass == SFAnalyticsEventClassSoftFailure) {
587 return @"EventSoftFailure";
590 return @"EventUnknown";
594 - (NSString*)sysdiagnoseStringForEventRecord:(NSDictionary*)eventRecord
596 NSMutableDictionary* mutableEventRecord = eventRecord.mutableCopy;
597 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkTopic];
599 NSDate* eventDate = [NSDate dateWithTimeIntervalSince1970:[[eventRecord valueForKey:SFAnalyticsLoggerSplunkEventTime] doubleValue] / 1000];
600 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventTime];
602 NSString* eventName = eventRecord[SFAnalyticsLoggerSplunkEventType];
603 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventType];
605 SFAnalyticsEventClass eventClass = [[eventRecord valueForKey:SFAnalyticsLoggerEventClassKey] integerValue];
606 NSString* eventClassString = [self stringForEventClass:eventClass];
607 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerEventClassKey];
609 NSMutableString* additionalAttributesString = [NSMutableString string];
610 if (mutableEventRecord.count > 0) {
611 [additionalAttributesString appendString:@" - Attributes: {" ];
612 __block BOOL firstAttribute = YES;
613 [mutableEventRecord enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
614 NSString* openingString = firstAttribute ? @"" : @", ";
615 [additionalAttributesString appendString:[NSString stringWithFormat:@"%@%@ : %@", openingString, key, object]];
618 [additionalAttributesString appendString:@" }"];
621 return [NSString stringWithFormat:@"%@ %@: %@%@", eventDate, eventClassString, eventName, additionalAttributesString];
624 - (NSString*)getSysdiagnoseDumpWithError:(NSError**)error
626 NSMutableString* sysdiagnose = [[NSMutableString alloc] init];
627 NSDictionary* extraValues = self.extraValuesToUploadToServer;
628 [extraValues enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
629 [sysdiagnose appendFormat:@"Key: %@, Value: %@\n", key, object];
632 [sysdiagnose appendString:@"\n"];
634 dispatch_sync(_queue, ^{
635 NSArray* allEvents = self->_database.allEvents;
636 for (NSDictionary* eventRecord in allEvents) {
637 [sysdiagnose appendFormat:@"%@\n", [self sysdiagnoseStringForEventRecord:eventRecord]];
644 + (void)addOSVersion:(NSMutableDictionary *)event
646 static dispatch_once_t onceToken;
647 static NSString *build = NULL;
648 static NSString *product = NULL;
649 dispatch_once(&onceToken, ^{
650 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
653 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
654 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
657 event[SFAnalyticsLoggerSplunkEventBuild] = build;
659 event[SFAnalyticsLoggerSplunkEventProduct] = product;
662 - (NSData*)getLoggingJSON:(bool)pretty error:(NSError**)error
664 __block NSData* json = nil;
665 NSDictionary* extraValues = self.extraValuesToUploadToServer;
666 dispatch_sync(_queue, ^{
667 if (![self->_database tryToOpenDatabase]) {
668 // we should not even be here because uploadDate was nil. But since we are, let's get out of here.
669 // Returning nil here will abort the upload (but again, the uploadDate should've done that already)
670 secerror("can't get logging JSON because database is not openable");
672 *error = [NSError errorWithDomain:@"SFAnalyticsLogger" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"could not open db to read and process metrics (device in class D?)"}];
677 NSArray* failureRecords = self->_database.failureRecords;
679 NSDictionary* successCounts = self->_database.summaryCounts;
680 NSInteger totalSuccessCount = 0;
681 NSInteger totalHardFailureCount = 0;
682 NSInteger totalSoftFailureCount = 0;
683 for (NSDictionary* perEventTypeSuccessCounts in successCounts.objectEnumerator) {
684 totalSuccessCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnSuccessCount] integerValue];
685 totalHardFailureCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnHardFailureCount] integerValue];
686 totalSoftFailureCount += [perEventTypeSuccessCounts[SFAnalyticsLoggerColumnSoftFailureCount] integerValue];
689 NSDate* now = [NSDate date];
691 NSMutableDictionary* healthSummaryEvent = extraValues ? extraValues.mutableCopy : [[NSMutableDictionary alloc] init];
692 healthSummaryEvent[SFAnalyticsLoggerSplunkTopic] = self->_splunkTopicName ?: [NSNull null];
693 healthSummaryEvent[SFAnalyticsLoggerSplunkEventTime] = @([now timeIntervalSince1970] * 1000);
694 healthSummaryEvent[SFAnalyticsLoggerSplunkEventType] = @"ckksHealthSummary";
695 healthSummaryEvent[SFAnalyticsLoggerColumnSuccessCount] = @(totalSuccessCount);
696 healthSummaryEvent[SFAnalyticsLoggerColumnHardFailureCount] = @(totalHardFailureCount);
697 healthSummaryEvent[SFAnalyticsLoggerColumnSoftFailureCount] = @(totalSoftFailureCount);
698 [SFAnalyticsLogger addOSVersion:healthSummaryEvent];
700 NSMutableArray* splunkRecords = failureRecords.mutableCopy;
701 [splunkRecords addObject:healthSummaryEvent];
703 NSDictionary* jsonDict = @{
704 SFAnalyticsLoggerSplunkPostTime : @([now timeIntervalSince1970] * 1000),
705 @"events" : splunkRecords
708 json = [NSJSONSerialization dataWithJSONObject:jsonDict
709 options:(pretty ? NSJSONWritingPrettyPrinted : 0)
716 - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
717 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
718 assert(completionHandler);
720 secnotice("ckks", "Splunk upload challenge");
721 NSURLCredential *cred = nil;
722 SecTrustResultType result = kSecTrustResultInvalid;
724 if ([challenge previousFailureCount] > 0) {
725 // Previous failures occurred, bail
726 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
728 } else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
730 * Evaluate trust for the certificate
733 SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
734 SecTrustEvaluate(serverTrust, &result);
735 if (_allowInsecureSplunkCert || (result == kSecTrustResultProceed) || (result == kSecTrustResultUnspecified)) {
737 * All is well, accept the credentials
739 if(_allowInsecureSplunkCert) {
740 secnotice("ckks", "Force Accepting Splunk Credential");
742 cred = [NSURLCredential credentialForTrust:serverTrust];
743 completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
747 * An error occurred in evaluating trust, bail
749 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
753 * Just perform the default handling
755 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
760 - (BOOL)ignoreServerDisablingMessages
762 return _ignoreServersMessagesTellingUsToGoAway;
765 - (void)setIgnoreServerDisablingMessages:(BOOL)ignoreServer
767 _ignoreServersMessagesTellingUsToGoAway = ignoreServer ? YES : NO;
770 - (BOOL)allowsInsecureSplunkCert
772 return _allowInsecureSplunkCert;
775 - (void)setAllowsInsecureSplunkCert:(BOOL)allowsInsecureSplunkCert
777 _allowInsecureSplunkCert = allowsInsecureSplunkCert ? YES : NO;
782 @implementation SFAnalyticsLoggerSQLiteStore
784 + (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema
786 SFAnalyticsLoggerSQLiteStore* store = nil;
787 @synchronized([SFAnalyticsLoggerSQLiteStore class]) {
788 static NSMutableDictionary* loggingStores = nil;
789 static dispatch_once_t onceToken;
790 dispatch_once(&onceToken, ^{
791 loggingStores = [[NSMutableDictionary alloc] init];
794 NSString* standardizedPath = path.stringByStandardizingPath;
795 store = loggingStores[standardizedPath];
797 store = [[self alloc] initWithPath:standardizedPath schema:schema];
798 loggingStores[standardizedPath] = store;
801 NSError* error = nil;
802 if (![store openWithError:&error]) {
803 secerror("SFAnalyticsLogger: could not open db at init, will try again later. Error: %@", error);
816 - (BOOL)tryToOpenDatabase
819 secwarning("SFAnalyticsLogger: db is closed, attempting to open");
820 NSError* error = nil;
821 if (![self openWithError:&error]) {
822 secerror("SFAnalyticsLogger: failed to open db with error %@", error);
829 - (NSInteger)successCountForEventType:(NSString*)eventType
831 if ([self tryToOpenDatabase]) {
832 return [[[[self select:@[SFAnalyticsLoggerColumnSuccessCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSuccessCount] integerValue];
837 - (void)incrementSuccessCountForEventType:(NSString*)eventType
839 if ([self tryToOpenDatabase]) {
840 NSInteger successCount = [self successCountForEventType:eventType];
841 NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
842 NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
843 [self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount + 1), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount)}];
847 - (NSInteger)hardFailureCountForEventType:(NSString*)eventType
849 if ([self tryToOpenDatabase]) {
850 return [[[[self select:@[SFAnalyticsLoggerColumnHardFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnHardFailureCount] integerValue];
855 - (NSInteger)softFailureCountForEventType:(NSString*)eventType
857 if ([self tryToOpenDatabase]) {
858 return [[[[self select:@[SFAnalyticsLoggerColumnSoftFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSoftFailureCount] integerValue];
863 - (void)incrementHardFailureCountForEventType:(NSString*)eventType
865 if ([self tryToOpenDatabase]) {
866 NSInteger successCount = [self successCountForEventType:eventType];
867 NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
868 NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
869 [self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount + 1), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount)}];
873 - (void)incrementSoftFailureCountForEventType:(NSString*)eventType
875 if ([self tryToOpenDatabase]) {
876 NSInteger successCount = [self successCountForEventType:eventType];
877 NSInteger hardFailureCount = [self hardFailureCountForEventType:eventType];
878 NSInteger softFailureCount = [self softFailureCountForEventType:eventType];
879 [self insertOrReplaceInto:SFAnalyticsLoggerTableSuccessCount values:@{SFAnalyticsLoggerColumnEventType : eventType, SFAnalyticsLoggerColumnSuccessCount : @(successCount), SFAnalyticsLoggerColumnHardFailureCount : @(hardFailureCount), SFAnalyticsLoggerColumnSoftFailureCount : @(softFailureCount + 1)}];
883 - (NSDictionary*)summaryCounts
885 if ([self tryToOpenDatabase]) {
886 NSMutableDictionary* successCountsDict = [NSMutableDictionary dictionary];
887 NSArray* rows = [self selectAllFrom:SFAnalyticsLoggerTableSuccessCount where:nil bindings:nil];
888 for (NSDictionary* rowDict in rows) {
889 NSString* eventName = rowDict[SFAnalyticsLoggerColumnEventType];
891 secinfo("SFAnalytics", "ignoring entry in success counts table without an event name");
895 successCountsDict[eventName] = @{SFAnalyticsLoggerTableSuccessCount : rowDict[SFAnalyticsLoggerColumnSuccessCount], SFAnalyticsLoggerColumnHardFailureCount : rowDict[SFAnalyticsLoggerColumnHardFailureCount], SFAnalyticsLoggerColumnSoftFailureCount : rowDict[SFAnalyticsLoggerColumnSoftFailureCount]};
897 return successCountsDict;
899 return [NSDictionary new];
902 - (NSArray*)failureRecords
904 if ([self tryToOpenDatabase]) {
905 NSArray* recordBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableHardFailures];
906 if (recordBlobs.count < SFANALYTICS_MAX_EVENTS_TO_REPORT) {
907 NSArray* softFailureBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableSoftFailures];
908 if (softFailureBlobs.count > 0) {
909 NSUInteger numSoftFailuresToReport = SFANALYTICS_MAX_EVENTS_TO_REPORT - recordBlobs.count;
910 if (numSoftFailuresToReport > softFailureBlobs.count)
911 numSoftFailuresToReport = softFailureBlobs.count;
913 recordBlobs = [recordBlobs arrayByAddingObjectsFromArray:[softFailureBlobs subarrayWithRange:NSMakeRange(softFailureBlobs.count - numSoftFailuresToReport, numSoftFailuresToReport)]];
917 NSMutableArray* failureRecords = [[NSMutableArray alloc] init];
918 for (NSDictionary* row in recordBlobs) {
919 NSMutableDictionary* deserializedRecord = [NSPropertyListSerialization propertyListWithData:row[SFAnalyticsLoggerColumnData] options:NSPropertyListMutableContainers format:nil error:nil];
920 [SFAnalyticsLogger addOSVersion:deserializedRecord];
921 [failureRecords addObject:deserializedRecord];
923 return failureRecords;
925 return [NSArray new];
928 - (NSArray*)allEvents
930 if ([self tryToOpenDatabase]) {
931 NSArray* recordBlobs = [self select:@[SFAnalyticsLoggerColumnData] from:SFAnalyticsLoggerTableAllEvents];
932 NSMutableArray* records = [[NSMutableArray alloc] init];
933 for (NSDictionary* row in recordBlobs) {
934 NSDictionary* deserializedRecord = [NSPropertyListSerialization propertyListWithData:row[SFAnalyticsLoggerColumnData] options:0 format:nil error:nil];
935 [records addObject:deserializedRecord];
939 return [NSArray new];
942 - (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table
944 if ([self tryToOpenDatabase]) {
945 NSError* error = nil;
946 NSData* serializedRecord = [NSPropertyListSerialization dataWithPropertyList:eventDict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
947 if(!error && serializedRecord) {
948 [self insertOrReplaceInto:table values:@{SFAnalyticsLoggerColumnDate : [NSDate date], SFAnalyticsLoggerColumnData : serializedRecord}];
950 if(error && !serializedRecord) {
951 secerror("Couldn't serialize failure record: %@", error);
956 // the other returning methods give default values in case of closed db,
957 // but this needs to be nil so the comparison to 'now' fails and we don't upload
958 - (NSDate*)uploadDate
960 if ([self tryToOpenDatabase]) {
961 return [self datePropertyForKey:SFAnalyticsLoggerUploadDate];
966 - (void)setUploadDate:(NSDate*)uploadDate
968 if ([self tryToOpenDatabase]) {
969 [self setDateProperty:uploadDate forKey:SFAnalyticsLoggerUploadDate];
975 if ([self tryToOpenDatabase]) {
976 [self deleteFrom:SFAnalyticsLoggerTableSuccessCount where:@"event_type like ?" bindings:@[@"%"]];
977 [self deleteFrom:SFAnalyticsLoggerTableHardFailures where:@"id >= 0" bindings:nil];
978 [self deleteFrom:SFAnalyticsLoggerTableSoftFailures where:@"id >= 0" bindings:nil];
979 [self deleteFrom:SFAnalyticsLoggerTableAllEvents where:@"id >= 0" bindings:nil];