]> git.saurik.com Git - apple/security.git/blob - keychain/analytics/SecMetrics.m
Security-59754.80.3.tar.gz
[apple/security.git] / keychain / analytics / SecMetrics.m
1 /*
2 * Copyright (c) 2018 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 #import <os/transaction_private.h>
25 #import <Foundation/Foundation.h>
26 #import <CFNetwork/CFNetworkPriv.h>
27 #import <Accounts/Accounts.h>
28 #import <Accounts/Accounts_Private.h>
29 #import <AppleAccount/ACAccount+AppleAccount.h>
30 #import <AppleAccount/ACAccountStore+AppleAccount.h>
31 #import <zlib.h>
32
33 #import "keychain/analytics/SecMetrics.h"
34 #import "keychain/analytics/SecEventMetric.h"
35 #import "keychain/analytics/SecEventMetric_private.h"
36 #import "keychain/analytics/SecC2DeviceInfo.h"
37
38 #import "keychain/analytics/C2Metric/SECC2MPMetric.h"
39 #import "keychain/analytics/C2Metric/SECC2MPGenericEvent.h"
40 #import "keychain/analytics/C2Metric/SECC2MPGenericEventMetric.h"
41 #import "keychain/analytics/C2Metric/SECC2MPGenericEventMetricValue.h"
42 #import "keychain/analytics/C2Metric/SECC2MPDeviceInfo.h"
43 #import <utilities/SecCoreAnalytics.h>
44
45 #import <utilities/simulatecrash_assert.h>
46
47
48
49 @interface SecMetrics () <NSURLSessionDelegate>
50 @property (strong) NSMutableDictionary<NSNumber *, SecEventMetric *> *taskMap;
51 @property (strong) NSURLSession *URLSession;
52 @property (strong) os_transaction_t transaction;
53 @property (assign) long lostEvents;
54 @end
55
56
57 static NSString *securtitydPushTopic = @"com.apple.private.alloy.keychain.metrics";
58
59 @implementation SecMetrics
60
61 + (NSURL *)c2MetricsEndpoint {
62 ACAccountStore *store = [[ACAccountStore alloc] init];
63 ACAccount* primaryAccount = [store aa_primaryAppleAccount];
64 if(!primaryAccount) {
65 return nil;
66 }
67 NSString *urlString = [primaryAccount propertiesForDataclass:ACAccountDataclassCKMetricsService][@"url"];
68 if (urlString == NULL) {
69 return nil;
70 }
71
72 NSURL *url = [[[NSURL alloc] initWithString:urlString] URLByAppendingPathComponent:@"c2"];
73
74 if (url) {
75 static dispatch_once_t onceToken;
76 dispatch_once(&onceToken, ^{
77 os_log(OS_LOG_DEFAULT, "metrics URL is: %@", url);
78 });
79 }
80
81 return url;
82 }
83
84 + (SecMetrics *)managerObject {
85 static SecMetrics *manager;
86 static dispatch_once_t onceToken;
87 dispatch_once(&onceToken, ^{
88 manager = [[SecMetrics alloc] init];
89 });
90 return manager;
91 }
92
93 - (instancetype)init
94 {
95 if ((self = [super init]) == NULL) {
96 return self;
97 }
98
99 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
100
101 self.URLSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:NULL];
102 self.taskMap = [NSMutableDictionary dictionary];
103 return self;
104 }
105
106 - (void)submitEvent:(SecEventMetric *)metric
107 {
108 [self sendEvent:metric pushTopic:securtitydPushTopic];
109 }
110
111
112 - (void)sendEvent:(SecEventMetric *)event pushTopic:(NSString *)pushTopic
113 {
114 bool tooMany = false;
115
116 @synchronized(self) {
117 if ([self.taskMap count] > 5) {
118 self.lostEvents++;
119 tooMany = true;
120 }
121 }
122 if (tooMany) {
123 os_log(OS_LOG_DEFAULT, "metrics %@ dropped on floor since too many are pending", event.eventName);
124 return;
125 }
126
127 SECC2MPGenericEvent *genericEvent = [event genericEvent];
128 if (genericEvent == NULL) {
129 return;
130 }
131
132 NSMutableURLRequest *request = [self requestForGenericEvent:genericEvent];
133 if (request == NULL) {
134 return;
135 }
136
137 NSURLSessionDataTask *task = [self.URLSession dataTaskWithRequest:request];
138 if (pushTopic) {
139 #if !TARGET_OS_BRIDGE
140 task._APSRelayTopic = pushTopic;
141 #endif
142 }
143
144 @synchronized(self) {
145 if ([self.taskMap count] == 0) {
146 self.transaction = os_transaction_create("com.apple.security.c2metric.upload");
147 }
148 self.taskMap[@(task.taskIdentifier)] = event;
149 }
150
151 [task resume];
152 }
153
154 - (SecEventMetric *)getEvent:(NSURLSessionTask *)task
155 {
156 @synchronized(self) {
157 return self.taskMap[@(task.taskIdentifier)];
158 }
159 }
160
161 //MARK: - URLSession Callbacks
162
163 - (void) URLSession:(NSURLSession *)session
164 task:(NSURLSessionTask *)task
165 didCompleteWithError:(nullable NSError *)error
166 {
167 SecEventMetric *event = [self getEvent:task];
168
169 os_log(OS_LOG_DEFAULT, "metrics %@ transfer %@ completed with: %@",
170 event.eventName, task.originalRequest.URL, error ? [error description] : @"success");
171
172 @synchronized(self) {
173 [self.taskMap removeObjectForKey:@(task.taskIdentifier)];
174 if (self.lostEvents || error) {
175 NSMutableDictionary *event = [NSMutableDictionary dictionary];
176
177 if (self.lostEvents) {
178 event[@"counter"] = @(self.lostEvents);
179 }
180 if (error) {
181 event[@"error_code"] = @(error.code);
182 event[@"error_domain"] = error.domain;
183 }
184 [SecCoreAnalytics sendEvent:@"com.apple.security.push.channel.dropped" event:event];
185 self.lostEvents = 0;
186 }
187
188 if (self.taskMap.count == 0) {
189 self.transaction = NULL;
190 }
191 }
192 }
193
194 //MARK: - FOO
195
196
197 - (NSMutableURLRequest*)requestForGenericEvent:(SECC2MPGenericEvent*)genericEvent
198 {
199 NSURL* metricURL = [[self class] c2MetricsEndpoint];
200 if (!metricURL) {
201 return nil;
202 }
203
204 NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:metricURL];
205 if (!request) {
206 return nil;
207 }
208
209 SECC2MPMetric* metrics = [[SECC2MPMetric alloc] init];
210 if (!metrics) {
211 return nil;
212 }
213 metrics.deviceInfo = [self generateDeviceInfo];
214 metrics.reportFrequency = 0;
215 metrics.reportFrequencyBase = 0;
216
217 metrics.metricType = SECC2MPMetric_Type_generic_event_type;
218 metrics.genericEvent = genericEvent;
219
220 PBDataWriter* protobufWriter = [[PBDataWriter alloc] init];
221 if (!protobufWriter) {
222 return nil;
223 }
224 [metrics writeTo:protobufWriter];
225 NSData* metricData = [protobufWriter immutableData];
226 if (!metricData) {
227 return nil;
228 }
229 NSData* compressedData = [self gzipEncode:metricData];
230 if (!compressedData) {
231 return nil;
232 }
233 [request setHTTPMethod:@"POST"];
234 [request setHTTPBody:compressedData];
235 [request setValue:@"application/protobuf" forHTTPHeaderField:@"Content-Type"];
236 [request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
237
238 return request;
239 }
240
241 #define CHUNK 1024
242
243 - (NSData*) gzipEncode:(NSData*)bodyData {
244 unsigned have;
245 unsigned char outBytes[CHUNK] = {0};
246 NSMutableData *compressedData = [NSMutableData data];
247
248 /* allocate deflate state */
249 z_stream _zlibStream;
250 _zlibStream.zalloc = Z_NULL;
251 _zlibStream.zfree = Z_NULL;
252 _zlibStream.opaque = Z_NULL;
253
254 // generate gzip header/trailer, use defaults for all other values
255 int ret = deflateInit2(&_zlibStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
256 if (ret != Z_OK) {
257 return NULL;
258 }
259
260 NS_VALID_UNTIL_END_OF_SCOPE NSData *arcSafeBodyData = bodyData;
261 _zlibStream.next_in = (Bytef *)[arcSafeBodyData bytes];
262 _zlibStream.avail_in = (unsigned int)[arcSafeBodyData length];
263 do {
264 _zlibStream.avail_out = CHUNK;
265 _zlibStream.next_out = outBytes;
266 ret = deflate(&_zlibStream, Z_FINISH);
267 assert(ret != Z_STREAM_ERROR);
268 have = CHUNK - _zlibStream.avail_out;
269 [compressedData appendBytes:outBytes length:have];
270 } while (_zlibStream.avail_out == 0);
271 assert(_zlibStream.avail_in == 0);
272 deflateEnd(&_zlibStream);
273
274 return compressedData;
275 }
276
277
278 - (SECC2MPDeviceInfo*) generateDeviceInfo {
279 SECC2MPDeviceInfo* deviceInfo = [[SECC2MPDeviceInfo alloc] init];
280 deviceInfo.productName = [SecC2DeviceInfo productName];
281 deviceInfo.productType = [SecC2DeviceInfo productType];
282 deviceInfo.productVersion = [SecC2DeviceInfo productVersion];
283 deviceInfo.productBuild = [SecC2DeviceInfo buildVersion];
284 deviceInfo.processName = [SecC2DeviceInfo processName];
285 deviceInfo.processVersion = [SecC2DeviceInfo processVersion];
286 deviceInfo.processUuid = [SecC2DeviceInfo processUUID];
287 return deviceInfo;
288 }
289
290 @end