diff options
-rw-r--r-- | gyp/platform-ios.gypi | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLAPIClient.h | 12 | ||||
-rw-r--r-- | platform/ios/src/MGLAPIClient.m | 195 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.m | 198 |
4 files changed, 242 insertions, 165 deletions
diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index dc52db7acd..e2c840581b 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -46,6 +46,8 @@ '../platform/darwin/src/MGLMapCamera.mm', '../platform/ios/src/MGLMapboxEvents.h', '../platform/ios/src/MGLMapboxEvents.m', + '../platform/ios/src/MGLAPIClient.h', + '../platform/ios/src/MGLAPIClient.m', '../platform/ios/src/MGLMapView.mm', '../platform/ios/src/MGLAccountManager_Private.h', '../platform/ios/src/MGLAccountManager.m', diff --git a/platform/ios/src/MGLAPIClient.h b/platform/ios/src/MGLAPIClient.h new file mode 100644 index 0000000000..ef64b021b7 --- /dev/null +++ b/platform/ios/src/MGLAPIClient.h @@ -0,0 +1,12 @@ +#import <Foundation/Foundation.h> + +#import "MGLMapboxEvents.h" +#import "MGLTypes.h" + +@interface MGLAPIClient : NSObject <NSURLSessionDelegate> + +- (void)postEvents:(nonnull NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; +- (void)postEvent:(nonnull MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; +- (void)cancelAll; + +@end diff --git a/platform/ios/src/MGLAPIClient.m b/platform/ios/src/MGLAPIClient.m new file mode 100644 index 0000000000..b4f85b5631 --- /dev/null +++ b/platform/ios/src/MGLAPIClient.m @@ -0,0 +1,195 @@ +#import "MGLAPIClient.h" +#import "NSBundle+MGLAdditions.h" +#import "MGLAccountManager.h" + +static NSString * const MGLAPIClientUserAgent = @"MapboxEventsiOS/1.1"; +static NSString * const MGLAPIClientBaseURL = @"https://api.tiles.mapbox.com"; + +static NSString * const MGLAPIClientHeaderFieldUserAgentKey = @"User-Agent"; +static NSString * const MGLAPIClientHeaderFieldContentTypeKey = @"Content-Type"; +static NSString * const MGLAPIClientHeaderFieldContentTypeValue = @"application/json"; +static NSString * const MGLAPIClientHTTPMethodPost = @"POST"; + +@interface MGLAPIClient () + +@property (nonatomic, copy) NSURLSession *session; +@property (nonatomic, copy) NSString *baseURL; +@property (nonatomic, copy) NSData *digicertCert; +@property (nonatomic, copy) NSData *geoTrustCert; +@property (nonatomic, copy) NSData *testServerCert; +@property (nonatomic, copy) NSString *userAgent; +@property (nonatomic) NSMutableArray *dataTasks; +@property (nonatomic) BOOL usesTestServer; + +@end + +@implementation MGLAPIClient + +- (instancetype)init { + self = [super init]; + if (self) { + _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:self delegateQueue:nil]; + _dataTasks = [NSMutableArray array]; + [self loadCertificates]; + [self setupBaseURL]; + [self setupUserAgent]; + } + return self; +} + +#pragma mark Public API + +- (void)postEvents:(nonnull NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { + NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:[self requestForEvents:events] + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + [self.dataTasks removeObject:dataTask]; + if (completionHandler) { + completionHandler(error); + } + }]; + [dataTask resume]; + [self.dataTasks addObject:dataTask]; +} + +- (void)postEvent:(nonnull MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { + [self postEvents:@[event] completionHandler:completionHandler]; +} + +- (void)cancelAll { + [self.dataTasks makeObjectsPerformSelector:@selector(cancel)]; + [self.dataTasks removeAllObjects]; +} + +#pragma mark Utilities + +- (NSURLRequest *)requestForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { + NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", self.baseURL, [MGLAccountManager accessToken]]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + [request setValue:self.userAgent forHTTPHeaderField:MGLAPIClientHeaderFieldUserAgentKey]; + [request setValue:MGLAPIClientHeaderFieldContentTypeValue forHTTPHeaderField:MGLAPIClientHeaderFieldContentTypeKey]; + [request setHTTPMethod:MGLAPIClientHTTPMethodPost]; + NSData *jsonData = [self serializedDataForEvents:events]; + [request setHTTPBody:jsonData]; + return [request copy]; +} + +- (void)setupBaseURL { + NSString *testServerURL = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMetricsTestServerURL"]; + if (testServerURL) { + _baseURL = testServerURL; + _usesTestServer = YES; + } else { + _baseURL = MGLAPIClientBaseURL; + } +} + +- (void)loadCertificates { + NSData *certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust"]; + self.geoTrustCert = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert"]; + self.digicertCert = certificate; + [self loadCertificate:&certificate withResource:@"star_tilestream_net"]; + self.testServerCert = certificate; +} + +- (void)loadCertificate:(NSData **)certificate withResource:(NSString *)resource { + NSBundle *resourceBundle = [NSBundle mgl_frameworkBundle]; + NSString *cerPath = [resourceBundle pathForResource:resource ofType:@"der"]; + if (cerPath != nil) { + *certificate = [NSData dataWithContentsOfFile:cerPath]; + } +} + +- (void)setupUserAgent { + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString *appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + _userAgent = [NSString stringWithFormat:@"%@/%@/%@ %@", appName, appVersion, appBuildNumber, MGLAPIClientUserAgent]; +} + +#pragma mark - JSON Serialization + +- (NSData *)serializedDataForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { + return [NSJSONSerialization dataWithJSONObject:events options:0 error:nil]; +} + +#pragma mark NSURLSessionDelegate + +- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + + if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + + SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; + SecTrustResultType trustResult; + + // Validate the certificate chain with the device's trust store anyway + // This *might* give use revocation checking + SecTrustEvaluate(serverTrust, &trustResult); + if (trustResult == kSecTrustResultUnspecified) + { + // Look for a pinned certificate in the server's certificate chain + long numKeys = SecTrustGetCertificateCount(serverTrust); + + BOOL found = NO; + // Try GeoTrust Cert First + for (int lc = 0; lc < numKeys; lc++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); + NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); + + // Compare Remote Key With Local Version + if ([remoteCertificateData isEqualToData:_geoTrustCert]) { + // Found the certificate; continue connecting + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + found = YES; + break; + } + } + + if (!found) { + // Fallback to Digicert Cert + for (int lc = 0; lc < numKeys; lc++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); + NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); + + // Compare Remote Key With Local Version + if ([remoteCertificateData isEqualToData:_digicertCert]) { + // Found the certificate; continue connecting + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + found = YES; + break; + } + } + + if (!found && _usesTestServer) { + // See if this is test server + for (int lc = 0; lc < numKeys; lc++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); + NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); + + // Compare Remote Key With Local Version + if ([remoteCertificateData isEqualToData:_testServerCert]) { + // Found the certificate; continue connecting + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + found = YES; + break; + } + } + } + + if (!found) { + // The certificate wasn't found in GeoTrust nor Digicert. Cancel the connection. + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } + } + } + else + { + // Certificate chain validation failed; cancel the connection + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } + } +} + +@end diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index bec86fd399..7f4b632e8f 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -5,13 +5,12 @@ #import "NSProcessInfo+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" #import "NSException+MGLAdditions.h" +#import "MGLAPIClient.h" #include <mbgl/platform/darwin/reachability.h> #include <sys/sysctl.h> static const NSUInteger version = 1; -static NSString *const MGLMapboxEventsUserAgent = @"MapboxEventsiOS/1.1"; -static NSString *MGLMapboxEventsAPIBase = @"https://api.tiles.mapbox.com"; NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile"; NSString *const MGLEventTypeMapLoad = @"map.load"; @@ -94,16 +93,13 @@ const NSTimeInterval MGLFlushInterval = 60; @property (nonatomic) MGLMapboxEventsData *data; @property (nonatomic, copy) NSString *appBundleId; -@property (nonatomic, copy) NSString *appName; -@property (nonatomic, copy) NSString *appVersion; -@property (nonatomic, copy) NSString *appBuildNumber; @property (nonatomic, copy) NSString *instanceID; @property (nonatomic, copy) NSString *dateForDebugLogFile; @property (nonatomic, copy) NSData *digicertCert; @property (nonatomic, copy) NSData *geoTrustCert; @property (nonatomic, copy) NSData *testServerCert; @property (nonatomic) NSDateFormatter *rfc3339DateFormatter; -@property (nonatomic) NSURLSession *session; +@property (nonatomic) MGLAPIClient *apiClient; @property (nonatomic) BOOL usesTestServer; @property (nonatomic) BOOL canEnableDebugLogging; @property (nonatomic, getter=isPaused) BOOL paused; @@ -145,47 +141,16 @@ const NSTimeInterval MGLFlushInterval = 60; self = [super init]; if (self) { _appBundleId = [[NSBundle mainBundle] bundleIdentifier]; - _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; - _appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - _appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; _instanceID = [[NSUUID UUID] UUIDString]; + _apiClient = [[MGLAPIClient alloc] init]; NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - // Configure Events Infrastructure - // =============================== - - // Check for TEST Metrics URL - NSString *testURL = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMetricsTestServerURL"]; - if (testURL != nil) { - MGLMapboxEventsAPIBase = testURL; - _usesTestServer = YES; - } else { - // Explicitly Set For Clarity - _usesTestServer = NO; - } - _paused = YES; [self resumeMetricsCollection]; NSBundle *resourceBundle = [NSBundle mgl_frameworkBundle]; - // Load Local Copy of Server's Public Key - NSString *cerPath = nil; - cerPath = [resourceBundle pathForResource:@"api_mapbox_com-geotrust" ofType:@"der"]; - if (cerPath != nil) { - _geoTrustCert = [NSData dataWithContentsOfFile:cerPath]; - } - - cerPath = [resourceBundle pathForResource:@"api_mapbox_com-digicert" ofType:@"der"]; - if (cerPath != nil) { - _digicertCert = [NSData dataWithContentsOfFile:cerPath]; - } - cerPath = [resourceBundle pathForResource:@"star_tilestream_net" ofType:@"der"]; - if (cerPath != nil) { - _testServerCert = [NSData dataWithContentsOfFile:cerPath]; - } - // Events Control _eventQueue = [[NSMutableArray alloc] init]; @@ -313,8 +278,6 @@ const NSTimeInterval MGLFlushInterval = 60; self.timer = nil; [self.eventQueue removeAllObjects]; self.data = nil; - [self.session invalidateAndCancel]; - self.session = nil; [self validateUpdatingLocation]; } @@ -341,7 +304,6 @@ const NSTimeInterval MGLFlushInterval = 60; self.paused = NO; self.data = [[MGLMapboxEventsData alloc] init]; - self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil]; [self validateUpdatingLocation]; } @@ -429,16 +391,24 @@ const NSTimeInterval MGLFlushInterval = 60; return; } - NSDictionary *vevt = @{@"event" : MGLEventTypeAppUserTurnstile, - @"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]], - @"appBundleId" : strongSelf.appBundleId, - @"vendorId": vendorID, - @"version": @(version), - @"instance": strongSelf.instanceID}; - - [strongSelf.eventQueue addObject:vevt]; - [strongSelf flush]; - [strongSelf writeEventToLocalDebugLog:vevt]; + NSDictionary *turnstileEventAttributes = @{@"event" : MGLEventTypeAppUserTurnstile, + @"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]], + @"appBundleId" : strongSelf.appBundleId, + @"vendorId": vendorID, + @"version": @(version), + @"instance": strongSelf.instanceID}; + + if ([MGLAccountManager accessToken] == nil) { + return; + } + [strongSelf.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) { + if (error) { + [MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", + @"error": error}]; + return; + } + [strongSelf writeEventToLocalDebugLog:turnstileEventAttributes]; + }]; }); } @@ -471,42 +441,22 @@ const NSTimeInterval MGLFlushInterval = 60; // Called implicitly from public use of +flush. // - (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - __weak MGLMapboxEvents *weakSelf = self; - + if ([self isPaused]) { + return; + } + + __weak __typeof__(self) weakSelf = self; dispatch_async(self.serialQueue, ^{ - MGLMapboxEvents *strongSelf = weakSelf; - - if (!strongSelf) { - return; - } - - NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", MGLMapboxEventsAPIBase, [MGLAccountManager accessToken]]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; - [request setValue:strongSelf.userAgent forHTTPHeaderField:@"User-Agent"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [request setHTTPMethod:@"POST"]; - - if ([NSJSONSerialization isValidJSONObject:events]) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:events options:NSJSONWritingPrettyPrinted error:nil]; - [request setHTTPBody:jsonData]; - - if (!strongSelf.paused) { - [[strongSelf.session dataTaskWithRequest:request] resume]; - } else { - for (MGLMapboxEventAttributes *event in events) { - if ([event[@"event"] isEqualToString:MGLEventTypeAppUserTurnstile]) { - NSURLSession *temporarySession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:strongSelf - delegateQueue:nil]; - [[temporarySession dataTaskWithRequest:request] resume]; - [temporarySession finishTasksAndInvalidate]; - } - } + __strong __typeof__(weakSelf) strongSelf = weakSelf; + [self.apiClient postEvents:events completionHandler:^(NSError * _Nullable error) { + if (error) { + [MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", + @"error": error}]; + return; } - [MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post", @"debug.eventsCount": @(events.count)}]; - } + }]; }); } @@ -519,10 +469,6 @@ const NSTimeInterval MGLFlushInterval = 60; repeats:YES]; } -- (NSString *)userAgent { - return [NSString stringWithFormat:@"%@/%@/%@ %@", self.appName, self.appVersion, self.appBuildNumber, MGLMapboxEventsUserAgent]; -} - - (NSInteger)batteryLevel { return [[NSNumber numberWithFloat:100 * [UIDevice currentDevice].batteryLevel] integerValue]; } @@ -694,84 +640,6 @@ const NSTimeInterval MGLFlushInterval = 60; [self validateUpdatingLocation]; } -#pragma mark NSURLSessionDelegate - -- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - - if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { - - SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; - SecTrustResultType trustResult; - - // Validate the certificate chain with the device's trust store anyway - // This *might* give use revocation checking - SecTrustEvaluate(serverTrust, &trustResult); - if (trustResult == kSecTrustResultUnspecified) - { - // Look for a pinned certificate in the server's certificate chain - long numKeys = SecTrustGetCertificateCount(serverTrust); - - BOOL found = NO; - // Try GeoTrust Cert First - for (int lc = 0; lc < numKeys; lc++) { - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); - NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); - - // Compare Remote Key With Local Version - if ([remoteCertificateData isEqualToData:_geoTrustCert]) { - // Found the certificate; continue connecting - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - found = YES; - break; - } - } - - if (!found) { - // Fallback to Digicert Cert - for (int lc = 0; lc < numKeys; lc++) { - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); - NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); - - // Compare Remote Key With Local Version - if ([remoteCertificateData isEqualToData:_digicertCert]) { - // Found the certificate; continue connecting - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - found = YES; - break; - } - } - - if (!found && _usesTestServer) { - // See if this is test server - for (int lc = 0; lc < numKeys; lc++) { - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); - NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); - - // Compare Remote Key With Local Version - if ([remoteCertificateData isEqualToData:_testServerCert]) { - // Found the certificate; continue connecting - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - found = YES; - break; - } - } - } - - if (!found) { - // The certificate wasn't found in GeoTrust nor Digicert. Cancel the connection. - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } - } - } - else - { - // Certificate chain validation failed; cancel the connection - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } - } - -} - #pragma mark MGLMapboxEvents Debug + (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { |