]> git.saurik.com Git - apple/security.git/blob - Analytics/SFAnalyticsLogger.m
Security-58286.41.2.tar.gz
[apple/security.git] / Analytics / SFAnalyticsLogger.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if __OBJC2__
25
26 #import "SFAnalyticsLogger.h"
27 #import "SFSQLite.h"
28 #import "CKKSViewManager.h"
29 #import "debugging.h"
30 #import <objc/runtime.h>
31 #import <os/variant_private.h>
32 #import <CoreFoundation/CFPriv.h>
33
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";
39
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";
45
46 NSString* const SFAnalyticsLoggerUploadDate = @"upload_date";
47
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";
54
55 NSString* const SFAnalyticsLoggerMetricsBase = @"metricsBase";
56 NSString* const SFAnalyticsLoggerEventClassKey = @"eventClass";
57
58
59 NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics";
60
61 static NSString* const SFAnalyticsLoggerTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n"
62 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
63 @"timestamp REAL,"
64 @"data BLOB\n"
65 @");\n"
66 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures AFTER INSERT ON hard_failures\n"
67 @"BEGIN\n"
68 @"DELETE FROM hard_failures WHERE id != NEW.id AND id % 999 = NEW.id % 999;\n"
69 @"END;\n"
70 @"CREATE TABLE IF NOT EXISTS soft_failures (\n"
71 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
72 @"timestamp REAL,"
73 @"data BLOB\n"
74 @");\n"
75 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures AFTER INSERT ON soft_failures\n"
76 @"BEGIN\n"
77 @"DELETE FROM soft_failures WHERE id != NEW.id AND id % 999 = NEW.id % 999;\n"
78 @"END;\n"
79 @"CREATE TABLE IF NOT EXISTS all_events (\n"
80 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
81 @"timestamp REAL,"
82 @"data BLOB\n"
83 @");\n"
84 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_all_events AFTER INSERT ON all_events\n"
85 @"BEGIN\n"
86 @"DELETE FROM all_events WHERE id != NEW.id AND id % 10000 = NEW.id % 10000;\n"
87 @"END;\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"
93 @");\n";
94
95 #define SFANALYTICS_SPLUNK_DEV 0
96 #define SFANALYTICS_MAX_EVENTS_TO_REPORT 999
97
98 #define SECONDS_PER_DAY (60 * 60 * 24)
99
100 #if SFANALYTICS_SPLUNK_DEV
101 #define SECONDS_BETWEEN_UPLOADS_CUSTOMER 10
102 #define SECONDS_BETWEEN_UPLOADS_INTERNAL 10
103 #else
104 #define SECONDS_BETWEEN_UPLOADS_CUSTOMER (3 * SECONDS_PER_DAY)
105 #define SECONDS_BETWEEN_UPLOADS_INTERNAL (SECONDS_PER_DAY)
106 #endif
107
108 typedef NS_ENUM(NSInteger, SFAnalyticsEventClass) {
109 SFAnalyticsEventClassSuccess,
110 SFAnalyticsEventClassHardFailure,
111 SFAnalyticsEventClassSoftFailure,
112 SFAnalyticsEventClassNote
113 };
114
115 @interface SFAnalyticsLoggerSQLiteStore : SFSQLite
116
117 @property (readonly, strong) NSArray* failureRecords;
118 @property (readonly, strong) NSArray* allEvents;
119 @property (readwrite, strong) NSDate* uploadDate;
120
121 + (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema;
122
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;
132
133 - (NSDictionary*)summaryCounts;
134
135 @end
136
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;
147
148 unsigned int _allowInsecureSplunkCert:1;
149 unsigned int _disableLogging:1;
150 unsigned int _disableUploads:1;
151 unsigned int _ignoreServersMessagesTellingUsToGoAway:1;
152 }
153
154 @synthesize splunkUploadURL = _splunkUploadURL;
155 @synthesize splunkBagURL = _splunkBagURL;
156 @synthesize splunkTopicName = _splunkTopicName;
157 @synthesize splunkLoggingQueue = _queue;
158
159 + (instancetype)logger
160 {
161 #if TARGET_OS_SIMULATOR
162 return nil;
163 #else
164
165 if (self == [SFAnalyticsLogger class]) {
166 secerror("attempt to instatiate abstract class SFAnalyticsLogger");
167 return nil;
168 }
169
170 SFAnalyticsLogger* logger = nil;
171 @synchronized(self) {
172 logger = objc_getAssociatedObject(self, "SFAnalyticsLoggerInstance");
173 if (!logger) {
174 logger = [[self alloc] init];
175 objc_setAssociatedObject(self, "SFAnalyticsLoggerInstance", logger, OBJC_ASSOCIATION_RETAIN);
176 }
177 }
178 return logger;
179 #endif
180 }
181
182 + (NSString*)databasePath
183 {
184 return nil;
185 }
186
187 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
188 {
189 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
190 if (timeIntervalSinceDate < SECONDS_PER_DAY) {
191 return 0;
192 }
193 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 7)) {
194 return 1;
195 }
196 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 30)) {
197 return 7;
198 }
199 else if (timeIntervalSinceDate < (SECONDS_PER_DAY * 365)) {
200 return 30;
201 }
202 else {
203 return 365;
204 }
205 }
206
207 - (instancetype)init
208 {
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);
212
213 if (os_variant_has_internal_diagnostics("Security")) {
214 _secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_INTERNAL;
215 } else {
216 _secondsBetweenUploads = SECONDS_BETWEEN_UPLOADS_CUSTOMER;
217 }
218
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"];
225
226 NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:SFAnalyticsUserDefaultsSuite];
227 NSString* userDefaultsSplunkTopic = [defaults stringForKey:@"splunk_topic"];
228 if (userDefaultsSplunkTopic) {
229 _splunkTopicName = userDefaultsSplunkTopic;
230 }
231
232 NSURL* userDefaultsSplunkUploadURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_uploadURL"]];
233 if (userDefaultsSplunkUploadURL) {
234 _splunkUploadURL = userDefaultsSplunkUploadURL;
235 }
236
237 NSURL* userDefaultsSplunkBagURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_bagURL"]];
238 if (userDefaultsSplunkUploadURL) {
239 _splunkBagURL = userDefaultsSplunkBagURL;
240 }
241
242 BOOL userDefaultsAllowInsecureSplunkCert = [defaults boolForKey:@"splunk_allowInsecureCertificate"];
243 _allowInsecureSplunkCert |= userDefaultsAllowInsecureSplunkCert;
244
245 NSString* userDefaultsSplunkEndpoint = [defaults stringForKey:@"splunk_endpointDomain"];
246 if (userDefaultsSplunkEndpoint) {
247 splunkEndpoint = userDefaultsSplunkEndpoint;
248 }
249
250 #if SFANALYTICS_SPLUNK_DEV
251 _ignoreServersMessagesTellingUsToGoAway = YES;
252
253 if (!_splunkUploadURL && splunkEndpoint) {
254 NSString* urlString = [NSString stringWithFormat:@"https://%@/report/2/%@", splunkEndpoint, _splunkTopicName];
255 _splunkUploadURL = [NSURL URLWithString:urlString];
256 }
257 #else
258 (void)splunkEndpoint;
259 #endif
260 }
261
262 return self;
263 }
264
265 - (void)logSuccessForEventNamed:(NSString*)eventName
266 {
267 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil];
268 }
269
270 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
271 {
272 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes];
273 }
274
275 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
276 {
277 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes];
278 }
279
280 - (void)noteEventNamed:(NSString*)eventName
281 {
282 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil];
283 }
284
285 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
286 {
287 if (!eventName) {
288 secinfo("SFAnalytics", "attempt to log an event with no name");
289 return;
290 }
291
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]) {
297 return;
298 }
299
300 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class];
301 [strongSelf->_database addEventDict:eventDict toTable:SFAnalyticsLoggerTableAllEvents];
302
303 if (class == SFAnalyticsEventClassHardFailure) {
304 NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
305 [strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableHardFailures];
306 [strongSelf->_database incrementHardFailureCountForEventType:eventName];
307 }
308 else if (class == SFAnalyticsEventClassSoftFailure) {
309 NSDictionary* strippedDict = [self eventDictWithBlacklistedFieldsStrippedFrom:eventDict];
310 [strongSelf->_database addEventDict:strippedDict toTable:SFAnalyticsLoggerTableSoftFailures];
311 [strongSelf->_database incrementSoftFailureCountForEventType:eventName];
312 }
313 else if (class == SFAnalyticsEventClassSuccess || class == SFAnalyticsEventClassNote) {
314 [strongSelf->_database incrementSuccessCountForEventType:eventName];
315 }
316
317 uploadDate = strongSelf->_database.uploadDate;
318 });
319
320 NSDate* nowDate = [NSDate date];
321 if (uploadDate) {
322 if ([nowDate compare:uploadDate] == NSOrderedDescending) {
323 NSError* error = nil;
324 BOOL uploadSuccess = [self forceUploadWithError:&error];
325 if (uploadSuccess) {
326 secinfo("SFAnalytics", "uploaded sync health data");
327 [self resetUploadDate:YES];
328 }
329
330 if (error) {
331 secerror("SFAnalytics: failed to upload json to analytics server with error: %@", error);
332 }
333 }
334 }
335 else {
336 [self resetUploadDate:NO];
337 }
338 }
339
340 - (void)resetUploadDate:(BOOL)clearData
341 {
342 __weak __typeof(self) weakSelf = self;
343 dispatch_sync(_queue, ^{
344 __strong __typeof(self) strongSelf = weakSelf;
345 if (!strongSelf) {
346 return;
347 }
348
349 if (clearData) {
350 [strongSelf->_database clearAllData];
351 }
352 strongSelf->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:strongSelf->_secondsBetweenUploads];
353 });
354 }
355
356 - (NSDictionary*)eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass
357 {
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);
363
364 [_metricsBase enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
365 if (!eventDict[key]) {
366 eventDict[key] = object;
367 }
368 }];
369
370 return eventDict;
371 }
372
373 - (NSDictionary*)eventDictWithBlacklistedFieldsStrippedFrom:(NSDictionary*)eventDict
374 {
375 NSMutableDictionary* strippedDict = eventDict.mutableCopy;
376 for (NSString* blacklistedField in _blacklistedFields) {
377 [strippedDict removeObjectForKey:blacklistedField];
378 }
379 return strippedDict;
380 }
381
382 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
383 {
384 dispatch_sync(_queue, ^{
385 [self->_database setDateProperty:date forKey:key];
386 });
387 }
388
389 - (NSDate*)datePropertyForKey:(NSString*)key
390 {
391 __block NSDate* result = nil;
392 dispatch_sync(_queue, ^{
393 result = [self->_database datePropertyForKey:key];
394 });
395 return result;
396 }
397
398 - (NSDictionary*)extraValuesToUploadToServer
399 {
400 return [NSDictionary dictionary];
401 }
402
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
406 {
407 dispatch_assert_queue(_queue);
408
409 if (_splunkUploadURL) {
410 return _splunkUploadURL;
411 }
412
413 __weak __typeof(self) weakSelf = self;
414 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
415
416 __block NSError* error = nil;
417 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
418
419 configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/%s", SECURITY_BUILD_VERSION]};
420
421 NSURLSession* storeBagSession = [NSURLSession sessionWithConfiguration:configuration
422 delegate:self
423 delegateQueue:nil];
424
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) {
430
431 __strong __typeof(self) strongSelf = weakSelf;
432 if (!strongSelf) {
433 return;
434 }
435
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);
446 return;
447 }
448
449 NSUInteger millisecondsBetweenUploads = [[responseDict valueForKey:@"postFrequency"] unsignedIntegerValue] / 1000;
450 if (millisecondsBetweenUploads > 0) {
451 strongSelf->_secondsBetweenUploads = millisecondsBetweenUploads;
452 }
453
454 strongSelf->_blacklistedEvents = responseDict[@"blacklistedEvents"];
455 strongSelf->_blacklistedFields = responseDict[@"blacklistedFields"];
456 }
457
458 strongSelf->_metricsBase = responseDict[@"metricsBase"];
459
460 NSString* metricsEndpoint = responseDict[@"metricsUrl"];
461 if([metricsEndpoint isKindOfClass:NSString.class]) {
462 /* Lives our URL */
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;
468 }
469 }
470 }
471 }
472 else {
473 error = responseError;
474 }
475 if(error) {
476 secnotice("ckks", "Unable to fetch splunk endpoint at URL: %@ -- error: %@", requestEndpoint, error.description);
477 }
478 else if(!result) {
479 secnotice("ckks", "Malformed iTunes config payload!");
480 }
481
482 dispatch_semaphore_signal(sem);
483 }];
484
485 [storeBagTask resume];
486 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
487
488 return result;
489 }
490
491 - (BOOL)forceUploadWithError:(NSError**)error
492 {
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);
498
499 [self->_database clearAllData];
500 self->_database.uploadDate = [NSDate dateWithTimeIntervalSinceNow:self->_secondsBetweenUploads];
501 result = YES;
502 }
503 else {
504 result = NO;
505 }
506 });
507
508 return result;
509 }
510
511 - (BOOL)_onQueuePostJSON:(NSData*)json error:(NSError**)error
512 {
513 dispatch_assert_queue(_queue);
514
515 /*
516 * Create the NSURLSession
517 * We use the ephemeral session config because we don't need cookies or cache
518 */
519 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
520
521 configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/%s", SECURITY_BUILD_VERSION]};
522
523 NSURLSession* postSession = [NSURLSession sessionWithConfiguration:configuration
524 delegate:self
525 delegateQueue:nil];
526
527 /*
528 * Create the request
529 */
530 NSURL* postEndpoint = self.splunkUploadURL;
531 if (!postEndpoint) {
532 secerror("failed to get a splunk upload endpoint - not uploading");
533 return NO;
534 }
535
536 NSMutableURLRequest* postRequest = [[NSMutableURLRequest alloc] init];
537 postRequest.URL = postEndpoint;
538 postRequest.HTTPMethod = @"POST";
539 postRequest.HTTPBody = json;
540
541 /*
542 * Create the upload task.
543 */
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) {
548 if(requestError) {
549 secerror("Error in uploading the events to splunk: %@", requestError);
550 }
551 else if (![response isKindOfClass:NSHTTPURLResponse.class]){
552 Class class = response.class;
553 secerror("Received the wrong kind of response: %@", NSStringFromClass(class));
554 }
555 else {
556 NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
557 if(httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
558 /* Success */
559 uploadSuccess = YES;
560 secnotice("ckks", "Splunk upload success");
561 }
562 else {
563 secnotice("ckks", "Splunk upload unexpected status to URL: %@ -- status: %d", postEndpoint, (int)(httpResponse.statusCode));
564 }
565 }
566 dispatch_semaphore_signal(sem);
567 }];
568
569 secnotice("ckks", "Splunk upload start");
570 [uploadTask resume];
571 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
572 return uploadSuccess;
573 }
574
575 - (NSString*)stringForEventClass:(SFAnalyticsEventClass)eventClass
576 {
577 if (eventClass == SFAnalyticsEventClassNote) {
578 return @"EventNote";
579 }
580 else if (eventClass == SFAnalyticsEventClassSuccess) {
581 return @"EventSuccess";
582 }
583 else if (eventClass == SFAnalyticsEventClassHardFailure) {
584 return @"EventHardFailure";
585 }
586 else if (eventClass == SFAnalyticsEventClassSoftFailure) {
587 return @"EventSoftFailure";
588 }
589 else {
590 return @"EventUnknown";
591 }
592 }
593
594 - (NSString*)sysdiagnoseStringForEventRecord:(NSDictionary*)eventRecord
595 {
596 NSMutableDictionary* mutableEventRecord = eventRecord.mutableCopy;
597 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkTopic];
598
599 NSDate* eventDate = [NSDate dateWithTimeIntervalSince1970:[[eventRecord valueForKey:SFAnalyticsLoggerSplunkEventTime] doubleValue] / 1000];
600 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventTime];
601
602 NSString* eventName = eventRecord[SFAnalyticsLoggerSplunkEventType];
603 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerSplunkEventType];
604
605 SFAnalyticsEventClass eventClass = [[eventRecord valueForKey:SFAnalyticsLoggerEventClassKey] integerValue];
606 NSString* eventClassString = [self stringForEventClass:eventClass];
607 [mutableEventRecord removeObjectForKey:SFAnalyticsLoggerEventClassKey];
608
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]];
616 firstAttribute = NO;
617 }];
618 [additionalAttributesString appendString:@" }"];
619 }
620
621 return [NSString stringWithFormat:@"%@ %@: %@%@", eventDate, eventClassString, eventName, additionalAttributesString];
622 }
623
624 - (NSString*)getSysdiagnoseDumpWithError:(NSError**)error
625 {
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];
630 }];
631
632 [sysdiagnose appendString:@"\n"];
633
634 dispatch_sync(_queue, ^{
635 NSArray* allEvents = self->_database.allEvents;
636 for (NSDictionary* eventRecord in allEvents) {
637 [sysdiagnose appendFormat:@"%@\n", [self sysdiagnoseStringForEventRecord:eventRecord]];
638 }
639 });
640
641 return sysdiagnose;
642 }
643
644 + (void)addOSVersion:(NSMutableDictionary *)event
645 {
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());
651 if (version == NULL)
652 return;
653 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
654 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
655 });
656 if (build)
657 event[SFAnalyticsLoggerSplunkEventBuild] = build;
658 if (product)
659 event[SFAnalyticsLoggerSplunkEventProduct] = product;
660 }
661
662 - (NSData*)getLoggingJSON:(bool)pretty error:(NSError**)error
663 {
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");
671 if (error) {
672 *error = [NSError errorWithDomain:@"SFAnalyticsLogger" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"could not open db to read and process metrics (device in class D?)"}];
673 }
674 return;
675 }
676
677 NSArray* failureRecords = self->_database.failureRecords;
678
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];
687 }
688
689 NSDate* now = [NSDate date];
690
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];
699
700 NSMutableArray* splunkRecords = failureRecords.mutableCopy;
701 [splunkRecords addObject:healthSummaryEvent];
702
703 NSDictionary* jsonDict = @{
704 SFAnalyticsLoggerSplunkPostTime : @([now timeIntervalSince1970] * 1000),
705 @"events" : splunkRecords
706 };
707
708 json = [NSJSONSerialization dataWithJSONObject:jsonDict
709 options:(pretty ? NSJSONWritingPrettyPrinted : 0)
710 error:error];
711 });
712
713 return json;
714 }
715
716 - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
717 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
718 assert(completionHandler);
719 (void)session;
720 secnotice("ckks", "Splunk upload challenge");
721 NSURLCredential *cred = nil;
722 SecTrustResultType result = kSecTrustResultInvalid;
723
724 if ([challenge previousFailureCount] > 0) {
725 // Previous failures occurred, bail
726 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
727
728 } else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
729 /*
730 * Evaluate trust for the certificate
731 */
732
733 SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
734 SecTrustEvaluate(serverTrust, &result);
735 if (_allowInsecureSplunkCert || (result == kSecTrustResultProceed) || (result == kSecTrustResultUnspecified)) {
736 /*
737 * All is well, accept the credentials
738 */
739 if(_allowInsecureSplunkCert) {
740 secnotice("ckks", "Force Accepting Splunk Credential");
741 }
742 cred = [NSURLCredential credentialForTrust:serverTrust];
743 completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
744
745 } else {
746 /*
747 * An error occurred in evaluating trust, bail
748 */
749 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
750 }
751 } else {
752 /*
753 * Just perform the default handling
754 */
755 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
756 }
757
758 }
759
760 - (BOOL)ignoreServerDisablingMessages
761 {
762 return _ignoreServersMessagesTellingUsToGoAway;
763 }
764
765 - (void)setIgnoreServerDisablingMessages:(BOOL)ignoreServer
766 {
767 _ignoreServersMessagesTellingUsToGoAway = ignoreServer ? YES : NO;
768 }
769
770 - (BOOL)allowsInsecureSplunkCert
771 {
772 return _allowInsecureSplunkCert;
773 }
774
775 - (void)setAllowsInsecureSplunkCert:(BOOL)allowsInsecureSplunkCert
776 {
777 _allowInsecureSplunkCert = allowsInsecureSplunkCert ? YES : NO;
778 }
779
780 @end
781
782 @implementation SFAnalyticsLoggerSQLiteStore
783
784 + (instancetype)storeWithPath:(NSString*)path schema:(NSString*)schema
785 {
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];
792 });
793
794 NSString* standardizedPath = path.stringByStandardizingPath;
795 store = loggingStores[standardizedPath];
796 if (!store) {
797 store = [[self alloc] initWithPath:standardizedPath schema:schema];
798 loggingStores[standardizedPath] = store;
799 }
800
801 NSError* error = nil;
802 if (![store openWithError:&error]) {
803 secerror("SFAnalyticsLogger: could not open db at init, will try again later. Error: %@", error);
804 }
805
806 }
807
808 return store;
809 }
810
811 - (void)dealloc
812 {
813 [self close];
814 }
815
816 - (BOOL)tryToOpenDatabase
817 {
818 if (!self.isOpen) {
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);
823 return NO;
824 }
825 }
826 return YES;
827 }
828
829 - (NSInteger)successCountForEventType:(NSString*)eventType
830 {
831 if ([self tryToOpenDatabase]) {
832 return [[[[self select:@[SFAnalyticsLoggerColumnSuccessCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSuccessCount] integerValue];
833 }
834 return 0;
835 }
836
837 - (void)incrementSuccessCountForEventType:(NSString*)eventType
838 {
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)}];
844 }
845 }
846
847 - (NSInteger)hardFailureCountForEventType:(NSString*)eventType
848 {
849 if ([self tryToOpenDatabase]) {
850 return [[[[self select:@[SFAnalyticsLoggerColumnHardFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnHardFailureCount] integerValue];
851 }
852 return 0;
853 }
854
855 - (NSInteger)softFailureCountForEventType:(NSString*)eventType
856 {
857 if ([self tryToOpenDatabase]) {
858 return [[[[self select:@[SFAnalyticsLoggerColumnSoftFailureCount] from:SFAnalyticsLoggerTableSuccessCount where:@"event_type = ?" bindings:@[eventType]] firstObject] valueForKey:SFAnalyticsLoggerColumnSoftFailureCount] integerValue];
859 }
860 return 0;
861 }
862
863 - (void)incrementHardFailureCountForEventType:(NSString*)eventType
864 {
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)}];
870 }
871 }
872
873 - (void)incrementSoftFailureCountForEventType:(NSString*)eventType
874 {
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)}];
880 }
881 }
882
883 - (NSDictionary*)summaryCounts
884 {
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];
890 if (!eventName) {
891 secinfo("SFAnalytics", "ignoring entry in success counts table without an event name");
892 continue;
893 }
894
895 successCountsDict[eventName] = @{SFAnalyticsLoggerTableSuccessCount : rowDict[SFAnalyticsLoggerColumnSuccessCount], SFAnalyticsLoggerColumnHardFailureCount : rowDict[SFAnalyticsLoggerColumnHardFailureCount], SFAnalyticsLoggerColumnSoftFailureCount : rowDict[SFAnalyticsLoggerColumnSoftFailureCount]};
896 }
897 return successCountsDict;
898 }
899 return [NSDictionary new];
900 }
901
902 - (NSArray*)failureRecords
903 {
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;
912
913 recordBlobs = [recordBlobs arrayByAddingObjectsFromArray:[softFailureBlobs subarrayWithRange:NSMakeRange(softFailureBlobs.count - numSoftFailuresToReport, numSoftFailuresToReport)]];
914 }
915 }
916
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];
922 }
923 return failureRecords;
924 }
925 return [NSArray new];
926 }
927
928 - (NSArray*)allEvents
929 {
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];
936 }
937 return records;
938 }
939 return [NSArray new];
940 }
941
942 - (void)addEventDict:(NSDictionary*)eventDict toTable:(NSString*)table
943 {
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}];
949 }
950 if(error && !serializedRecord) {
951 secerror("Couldn't serialize failure record: %@", error);
952 }
953 }
954 }
955
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
959 {
960 if ([self tryToOpenDatabase]) {
961 return [self datePropertyForKey:SFAnalyticsLoggerUploadDate];
962 }
963 return nil;
964 }
965
966 - (void)setUploadDate:(NSDate*)uploadDate
967 {
968 if ([self tryToOpenDatabase]) {
969 [self setDateProperty:uploadDate forKey:SFAnalyticsLoggerUploadDate];
970 }
971 }
972
973 - (void)clearAllData
974 {
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];
980 }
981 }
982
983 @end
984
985 #endif // __OBJC2__