diff options
Diffstat (limited to 'platform/ios/src/MGLAPIClient.m')
-rw-r--r-- | platform/ios/src/MGLAPIClient.m | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/platform/ios/src/MGLAPIClient.m b/platform/ios/src/MGLAPIClient.m new file mode 100644 index 0000000000..68e78835c3 --- /dev/null +++ b/platform/ios/src/MGLAPIClient.m @@ -0,0 +1,218 @@ +#import "MGLAPIClient.h" +#import "NSBundle+MGLAdditions.h" +#import "NSData+MGLAdditions.h" +#import "MGLAccountManager.h" +#import "MGLNetworkConfiguration.h" + +static NSString * const MGLAPIClientUserAgentBase = @"MapboxEventsiOS"; +static NSString * const MGLAPIClientBaseURL = @"https://events.mapbox.com"; +static NSString * const MGLAPIClientChinaBaseURL = @"https://events.mapbox.cn"; +static NSString * const MGLAPIClientEventsPath = @"events/v2"; + +static NSString * const MGLAPIClientHeaderFieldUserAgentKey = @"User-Agent"; +static NSString * const MGLAPIClientHeaderFieldContentTypeKey = @"Content-Type"; +static NSString * const MGLAPIClientHeaderFieldContentTypeValue = @"application/json"; +static NSString * const MGLAPIClientHeaderFieldContentEncodingKey = @"Content-Encoding"; +static NSString * const MGLAPIClientHTTPMethodPost = @"POST"; + +@interface MGLAPIClient () + +@property (nonatomic, copy) NSURLSession *session; +@property (nonatomic, copy) NSURL *baseURL; +@property (nonatomic, copy) NSData *digicertCert_2016; +@property (nonatomic, copy) NSData *geoTrustCert_2016; +@property (nonatomic, copy) NSData *digicertCert_2017; +@property (nonatomic, copy) NSData *geoTrustCert_2017; +@property (nonatomic, copy) NSData *digicertCert_cn_2018; +@property (nonatomic, copy) NSData *geoTrustCert_cn_2018; +@property (nonatomic, copy) NSData *testServerCert; +@property (nonatomic, copy) NSString *userAgent; +@property (nonatomic) BOOL usesTestServer; + +@end + +@implementation MGLAPIClient + +- (instancetype)init { + self = [super init]; + if (self) { + _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:self delegateQueue:nil]; + [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 { + __block NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:[self requestForEvents:events] + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSError *statusError = nil; + if (httpResponse.statusCode >= 400) { + NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_DESC", nil, nil, @"The session data task failed. Original request was: %@", nil), dataTask.originalRequest]; + NSString *reason = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_REASON", nil, nil, @"The status code was %ld", nil), (long)httpResponse.statusCode]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description, + NSLocalizedFailureReasonErrorKey: reason}; + statusError = [NSError errorWithDomain:MGLErrorDomain code:1 userInfo:userInfo]; + } + if (completionHandler) { + error = error ?: statusError; + completionHandler(error); + } + dataTask = nil; + }]; + [dataTask resume]; +} + +- (void)postEvent:(nonnull MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { + [self postEvents:@[event] completionHandler:completionHandler]; +} + +#pragma mark Utilities + +- (NSURLRequest *)requestForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { + NSString *path = [NSString stringWithFormat:@"%@?access_token=%@", MGLAPIClientEventsPath, [MGLAccountManager accessToken]]; + NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + [request setValue:self.userAgent forHTTPHeaderField:MGLAPIClientHeaderFieldUserAgentKey]; + [request setValue:MGLAPIClientHeaderFieldContentTypeValue forHTTPHeaderField:MGLAPIClientHeaderFieldContentTypeKey]; + [request setHTTPMethod:MGLAPIClientHTTPMethodPost]; + + NSData *jsonData = [self serializedDataForEvents:events]; + + // Compressing less than 3 events can have a negative impact on the size. + if (events.count > 2) { + NSData *compressedData = [jsonData mgl_compressedData]; + [request setValue:@"deflate" forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; + [request setHTTPBody:compressedData]; + } + + // Set JSON data if events.count were less than 3 or something went wrong with compressing HTTP body data. + if (!request.HTTPBody) { + [request setValue:nil forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; + [request setHTTPBody:jsonData]; + } + + return [request copy]; +} + +- (void)setupBaseURL { + NSString *testServerURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MGLTelemetryTestServerURL"]; + NSURL *testServerURL = [NSURL URLWithString:testServerURLString]; + if (testServerURL && [testServerURL.scheme isEqualToString:@"https"]) { + self.baseURL = testServerURL; + self.usesTestServer = YES; + } else if ([[[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxAPIBaseURL"] isEqualToString:MGLChinaMapboxAPIBaseURL]) { + self.baseURL = [NSURL URLWithString:MGLAPIClientChinaBaseURL]; + } else { + self.baseURL = [NSURL URLWithString:MGLAPIClientBaseURL]; + } +} + +- (void)loadCertificates { + NSData *certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2016"]; + self.geoTrustCert_2016 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2016"]; + self.digicertCert_2016 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2017"]; + self.geoTrustCert_2017 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2017"]; + self.digicertCert_2017 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_cn-geotrust_2018"]; + self.geoTrustCert_cn_2018 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_cn-digicert_2018"]; + self.digicertCert_cn_2018 = certificate; + [self loadCertificate:&certificate withResource:@"api_mapbox_staging"]; + self.testServerCert = certificate; +} + +- (void)loadCertificate:(NSData **)certificate withResource:(NSString *)resource { + NSBundle *frameworkBundle = [NSBundle mgl_frameworkBundle]; + NSString *cerPath = [frameworkBundle pathForResource:resource ofType:@"der"]; + if (cerPath != nil) { + *certificate = [NSData dataWithContentsOfFile:cerPath]; + } +} + +- (void)setupUserAgent { + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString *appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; + NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; + NSString *sdkVersion = semanticVersion ?: shortVersion; + _userAgent = [NSString stringWithFormat:@"%@/%@/%@ %@/%@", appName, appVersion, appBuildNumber, MGLAPIClientUserAgentBase, sdkVersion]; +} + +#pragma mark - JSON Serialization + +- (NSData *)serializedDataForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { + return [NSJSONSerialization dataWithJSONObject:events options:0 error:nil]; +} + +#pragma mark NSURLSessionDelegate + +- (BOOL)evaluateCertificateWithCertificateData:(NSData *)certificateData keyCount:(CFIndex)keyCount serverTrust:(SecTrustRef)serverTrust challenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + for (int lc = 0; lc < keyCount; lc++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); + NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); + if ([remoteCertificateData isEqualToData:certificateData]) { + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + return YES; + } + } + return NO; +} + +- (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* use revocation checking + SecTrustEvaluate(serverTrust, &trustResult); + + BOOL found = NO; // For clarity; we start in a state where the challange has not been completed and no certificate has been found + + if (trustResult == kSecTrustResultUnspecified) { + // Look for a pinned certificate in the server's certificate chain + CFIndex numKeys = SecTrustGetCertificateCount(serverTrust); + + // Check certs in the following order: digicert 2016, digicert 2017, digicert CN 2018, geotrust 2016, geotrust 2017, geotrust CN 2018 + found = [self evaluateCertificateWithCertificateData:self.digicertCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + if (!found) { + found = [self evaluateCertificateWithCertificateData:self.digicertCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + if (!found) { + found = [self evaluateCertificateWithCertificateData:self.digicertCert_cn_2018 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + if (!found) { + found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + if (!found) { + found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + if (!found) { + found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_cn_2018 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + + // If challenge can't be completed with any of the above certs, then try the test server if the app is configured to use the test server + if (!found && _usesTestServer) { + found = [self evaluateCertificateWithCertificateData:self.testServerCert keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; + } + } + + if (!found) { + // No certificate was found so cancel the connection. + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } + } +} + +@end |