summaryrefslogtreecommitdiff
path: root/platform/ios/src/MGLAPIClient.m
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/src/MGLAPIClient.m')
-rw-r--r--platform/ios/src/MGLAPIClient.m218
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