BTHTTP.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. #import "BTHTTP.h"
  2. #import "Braintree-Version.h"
  3. #import "BTAPIPinnedCertificates.h"
  4. #import "BTLogger_Internal.h"
  5. #import "BTCacheDateValidator_Internal.h"
  6. #include <sys/sysctl.h>
  7. #if __has_include(<Braintree/BraintreeCore.h>)
  8. #import <Braintree/BTClientToken.h>
  9. #import <Braintree/BTHTTPErrors.h>
  10. #import <Braintree/BTJSON.h>
  11. #import <Braintree/BTURLUtils.h>
  12. #else
  13. #import <BraintreeCore/BTClientToken.h>
  14. #import <BraintreeCore/BTHTTPErrors.h>
  15. #import <BraintreeCore/BTJSON.h>
  16. #import <BraintreeCore/BTURLUtils.h>
  17. #endif
  18. @interface BTHTTP () <NSURLSessionDelegate>
  19. @property (nonatomic, strong) NSURL *baseURL;
  20. @property (nonatomic, copy) NSString *authorizationFingerprint;
  21. @property (nonatomic, copy) NSString *tokenizationKey;
  22. @end
  23. @implementation BTHTTP
  24. - (instancetype)init {
  25. return nil;
  26. }
  27. - (instancetype)initWithBaseURL:(NSURL *)URL {
  28. self = [super init];
  29. if (self) {
  30. self.baseURL = URL;
  31. self.cacheDateValidator = [[BTCacheDateValidator alloc] init];
  32. }
  33. return self;
  34. }
  35. - (instancetype)initWithBaseURL:(NSURL *)URL authorizationFingerprint:(NSString *)authorizationFingerprint {
  36. self = [self initWithBaseURL:URL];
  37. if (self) {
  38. NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
  39. configuration.HTTPAdditionalHeaders = self.defaultHeaders;
  40. NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
  41. delegateQueue.name = @"com.braintreepayments.BTHTTP";
  42. delegateQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
  43. self.authorizationFingerprint = authorizationFingerprint;
  44. self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:delegateQueue];
  45. self.pinnedCertificates = [BTAPIPinnedCertificates trustedCertificates];
  46. }
  47. return self;
  48. }
  49. - (instancetype)initWithBaseURL:(nonnull NSURL *)URL tokenizationKey:(nonnull NSString *)tokenizationKey {
  50. if (self = [self initWithBaseURL:URL]) {
  51. NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
  52. configuration.HTTPAdditionalHeaders = self.defaultHeaders;
  53. NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
  54. delegateQueue.name = @"com.braintreepayments.BTHTTP";
  55. delegateQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
  56. self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:delegateQueue];
  57. self.pinnedCertificates = [BTAPIPinnedCertificates trustedCertificates];
  58. self.tokenizationKey = tokenizationKey;
  59. }
  60. return self;
  61. }
  62. - (instancetype)initWithClientToken:(BTClientToken *)clientToken {
  63. return [self initWithBaseURL:[clientToken.json[@"clientApiUrl"] asURL] authorizationFingerprint:clientToken.authorizationFingerprint];
  64. }
  65. - (instancetype)copyWithZone:(NSZone *)zone {
  66. BTHTTP *copiedHTTP;
  67. if (self.authorizationFingerprint) {
  68. copiedHTTP = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL authorizationFingerprint:self.authorizationFingerprint];
  69. } else {
  70. copiedHTTP = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL tokenizationKey:self.tokenizationKey];
  71. }
  72. copiedHTTP.pinnedCertificates = [_pinnedCertificates copy];
  73. return copiedHTTP;
  74. }
  75. - (void)setSession:(NSURLSession *)session {
  76. if (_session) {
  77. // If we already have a session, we need to invalidate it so that the session delegate is released to prevent a retain cycle
  78. [_session invalidateAndCancel];
  79. }
  80. _session = session;
  81. }
  82. #pragma mark - HTTP Methods
  83. - (void)GET:(NSString *)aPath completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  84. [self GET:aPath parameters:nil completion:completionBlock];
  85. }
  86. - (void)GET:(NSString *)aPath parameters:(NSDictionary *)parameters shouldCache:(BOOL)shouldCache completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  87. if (shouldCache) {
  88. [self httpRequestWithCaching:@"GET" path:aPath parameters:parameters completion:completionBlock];
  89. } else {
  90. [self httpRequest:@"GET" path:aPath parameters:parameters completion:completionBlock];
  91. }
  92. }
  93. - (void)GET:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  94. [self httpRequest:@"GET" path:aPath parameters:parameters completion:completionBlock];
  95. }
  96. - (void)POST:(NSString *)aPath completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  97. [self POST:aPath parameters:nil completion:completionBlock];
  98. }
  99. - (void)POST:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  100. [self httpRequest:@"POST" path:aPath parameters:parameters completion:completionBlock];
  101. }
  102. - (void)PUT:(NSString *)aPath completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  103. [self PUT:aPath parameters:nil completion:completionBlock];
  104. }
  105. - (void)PUT:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  106. [self httpRequest:@"PUT" path:aPath parameters:parameters completion:completionBlock];
  107. }
  108. - (void)DELETE:(NSString *)aPath completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  109. [self DELETE:aPath parameters:nil completion:completionBlock];
  110. }
  111. - (void)DELETE:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  112. [self httpRequest:@"DELETE" path:aPath parameters:parameters completion:completionBlock];
  113. }
  114. #pragma mark - Underlying HTTP
  115. - (void)httpRequestWithCaching:(NSString *)method path:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  116. [self createRequest:method path:aPath parameters:parameters completion:^(NSURLRequest *request, NSError *error) {
  117. if (error != nil) {
  118. [self handleRequestCompletion:nil request:nil shouldCache:NO response:nil error:error completionBlock:completionBlock];
  119. return;
  120. }
  121. NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
  122. if ([self.cacheDateValidator isCacheInvalid:cachedResponse]) {
  123. [[NSURLCache sharedURLCache] removeAllCachedResponses];
  124. cachedResponse = nil;
  125. }
  126. // The increase in speed of API calls with cached configuration caused an increase in "network connection lost" errors.
  127. // Adding this delay allows us to throttle the network requests slightly to reduce load on the servers and decrease connection lost errors.
  128. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
  129. if (cachedResponse != nil) {
  130. [self handleRequestCompletion:cachedResponse.data request:nil shouldCache:NO response:cachedResponse.response error:nil completionBlock:completionBlock];
  131. } else {
  132. NSURLSessionTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  133. [self handleRequestCompletion:data request:request shouldCache:YES response:response error:error completionBlock:completionBlock];
  134. }];
  135. [task resume];
  136. }
  137. });
  138. }];
  139. }
  140. - (void)httpRequest:(NSString *)method path:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  141. [self createRequest:method path:aPath parameters:parameters completion:^(NSURLRequest *request, NSError *error) {
  142. if (error != nil) {
  143. [self handleRequestCompletion:nil request:nil shouldCache:NO response:nil error:error completionBlock:completionBlock];
  144. return;
  145. }
  146. NSURLSessionTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  147. [self handleRequestCompletion:data request:request shouldCache:NO response:response error:error completionBlock:completionBlock];
  148. }];
  149. [task resume];
  150. }];
  151. }
  152. - (void)createRequest:(NSString *)method path:(NSString *)aPath parameters:(NSDictionary *)parameters completion:(void(^)(NSURLRequest *request, NSError *error))completionBlock {
  153. BOOL hasHttpPrefix = aPath != nil && [aPath hasPrefix:@"http"];
  154. if (!hasHttpPrefix && (!self.baseURL || [self.baseURL.absoluteString isEqualToString:@""])) {
  155. NSMutableDictionary *errorUserInfo = [NSMutableDictionary new];
  156. if (method) errorUserInfo[@"method"] = method;
  157. if (aPath) errorUserInfo[@"path"] = aPath;
  158. if (parameters) errorUserInfo[@"parameters"] = parameters;
  159. completionBlock(nil, [NSError errorWithDomain:BTHTTPErrorDomain code:BTHTTPErrorCodeMissingBaseURL userInfo:errorUserInfo]);
  160. return;
  161. }
  162. BOOL isNotDataURL = ![self.baseURL.scheme isEqualToString:@"data"];
  163. NSURL *fullPathURL;
  164. if (aPath && isNotDataURL) {
  165. if (hasHttpPrefix) {
  166. fullPathURL = [NSURL URLWithString:aPath];
  167. } else {
  168. fullPathURL = [self.baseURL URLByAppendingPathComponent:aPath];
  169. }
  170. } else {
  171. fullPathURL = self.baseURL;
  172. }
  173. if (parameters == nil) {
  174. parameters = [NSDictionary dictionary];
  175. }
  176. NSMutableDictionary *mutableParameters = [NSMutableDictionary dictionaryWithDictionary:parameters];
  177. if (self.authorizationFingerprint) {
  178. mutableParameters[@"authorization_fingerprint"] = self.authorizationFingerprint;
  179. }
  180. parameters = [mutableParameters copy];
  181. if (!fullPathURL) {
  182. // baseURL can be non-nil (e.g. an empty string) and still return nil for -URLByAppendingPathComponent:
  183. // causing a crash when NSURLComponents.componentsWithString is called with nil.
  184. NSMutableDictionary *errorUserInfo = [NSMutableDictionary new];
  185. if (method) errorUserInfo[@"method"] = method;
  186. if (aPath) errorUserInfo[@"path"] = aPath;
  187. if (parameters) errorUserInfo[@"parameters"] = parameters;
  188. errorUserInfo[NSLocalizedFailureReasonErrorKey] = @"fullPathURL was nil";
  189. completionBlock(nil, [NSError errorWithDomain:BTHTTPErrorDomain code:BTHTTPErrorCodeMissingBaseURL userInfo:errorUserInfo]);
  190. return;
  191. }
  192. NSURLComponents *components = [NSURLComponents componentsWithString:fullPathURL.absoluteString];
  193. NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:self.defaultHeaders];
  194. NSMutableURLRequest *request;
  195. if ([method isEqualToString:@"GET"] || [method isEqualToString:@"DELETE"]) {
  196. if (isNotDataURL) {
  197. components.percentEncodedQuery = [BTURLUtils queryStringWithDictionary:parameters];
  198. }
  199. request = [NSMutableURLRequest requestWithURL:components.URL];
  200. } else {
  201. request = [NSMutableURLRequest requestWithURL:components.URL];
  202. NSError *jsonSerializationError;
  203. NSData *bodyData;
  204. if ([parameters isKindOfClass:[NSDictionary class]]) {
  205. bodyData = [NSJSONSerialization dataWithJSONObject:parameters
  206. options:0
  207. error:&jsonSerializationError];
  208. }
  209. if (jsonSerializationError != nil) {
  210. completionBlock(nil, jsonSerializationError);
  211. return;
  212. }
  213. [request setHTTPBody:bodyData];
  214. headers[@"Content-Type"] = @"application/json; charset=utf-8";
  215. }
  216. if (self.tokenizationKey) {
  217. headers[@"Client-Key"] = self.tokenizationKey;
  218. }
  219. [request setAllHTTPHeaderFields:headers];
  220. [request setHTTPMethod:method];
  221. completionBlock(request, nil);
  222. }
  223. - (void)handleRequestCompletion:(NSData *)data request:(NSURLRequest *)request shouldCache:(BOOL)shouldCache response:(NSURLResponse *)response error:(NSError *)error completionBlock:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock {
  224. // Handle errors for which the response is irrelevant
  225. // e.g. SSL, unavailable network, etc.
  226. if (error != nil) {
  227. [self callCompletionBlock:completionBlock body:nil response:nil error:error];
  228. return;
  229. }
  230. NSHTTPURLResponse *httpResponse;
  231. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  232. httpResponse = (NSHTTPURLResponse *)response;
  233. } else if ([response.URL.scheme isEqualToString:@"data"]) {
  234. httpResponse = [[NSHTTPURLResponse alloc] initWithURL:response.URL statusCode:200 HTTPVersion:nil headerFields:nil];
  235. }
  236. NSString *responseContentType = [response MIMEType];
  237. NSMutableDictionary *errorUserInfo = [NSMutableDictionary new];
  238. errorUserInfo[BTHTTPURLResponseKey] = httpResponse;
  239. if (httpResponse.statusCode >= 400) {
  240. errorUserInfo[NSLocalizedFailureReasonErrorKey] = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
  241. BTJSON *json;
  242. if ([responseContentType isEqualToString:@"application/json"]) {
  243. json = (data.length == 0) ? [BTJSON new] : [[BTJSON alloc] initWithData:data];
  244. if (!json.isError) {
  245. errorUserInfo[BTHTTPJSONResponseBodyKey] = json;
  246. NSString *errorResponseMessage = [json[@"error"][@"developer_message"] isString] ? [json[@"error"][@"developer_message"] asString] : [json[@"error"][@"message"] asString];
  247. if (errorResponseMessage) {
  248. errorUserInfo[NSLocalizedDescriptionKey] = errorResponseMessage;
  249. }
  250. }
  251. }
  252. BTHTTPErrorCode errorCode = httpResponse.statusCode >= 500 ? BTHTTPErrorCodeServerError : BTHTTPErrorCodeClientError;
  253. if (httpResponse.statusCode == 429) {
  254. errorCode = BTHTTPErrorCodeRateLimitError;
  255. errorUserInfo[NSLocalizedDescriptionKey] = @"You are being rate-limited.";
  256. errorUserInfo[NSLocalizedRecoverySuggestionErrorKey] = @"Please try again in a few minutes.";
  257. } else if (httpResponse.statusCode >= 500) {
  258. errorUserInfo[NSLocalizedRecoverySuggestionErrorKey] = @"Please try again later.";
  259. }
  260. NSError *error = [NSError errorWithDomain:BTHTTPErrorDomain
  261. code:errorCode
  262. userInfo:[errorUserInfo copy]];
  263. [self callCompletionBlock:completionBlock body:json response:httpResponse error:error];
  264. return;
  265. }
  266. // Empty response is valid
  267. BTJSON *json = (data.length == 0) ? [BTJSON new] : [[BTJSON alloc] initWithData:data];
  268. if (json.isError) {
  269. if (![responseContentType isEqualToString:@"application/json"]) {
  270. // Return error for unsupported response type
  271. errorUserInfo[NSLocalizedFailureReasonErrorKey] = [NSString stringWithFormat:@"BTHTTP only supports application/json responses, received Content-Type: %@", responseContentType];
  272. NSError *returnedError = [NSError errorWithDomain:BTHTTPErrorDomain
  273. code:BTHTTPErrorCodeResponseContentTypeNotAcceptable
  274. userInfo:[errorUserInfo copy]];
  275. [self callCompletionBlock:completionBlock body:nil response:nil error:returnedError];
  276. } else {
  277. [self callCompletionBlock:completionBlock body:nil response:nil error:json.asError];
  278. }
  279. return;
  280. }
  281. // We should only cache the response if we do not have an error and status code is 2xx
  282. BOOL successStatusCode = httpResponse.statusCode >= 200 && httpResponse.statusCode < 300;
  283. if (request != nil && shouldCache && successStatusCode) {
  284. NSCachedURLResponse *cachedURLResponse = [[NSCachedURLResponse alloc]initWithResponse:response data:data];
  285. [[NSURLCache sharedURLCache] storeCachedResponse:cachedURLResponse forRequest:request];
  286. }
  287. [self callCompletionBlock:completionBlock body:json response:httpResponse error:nil];
  288. }
  289. - (void)callCompletionBlock:(void(^)(BTJSON *body, NSHTTPURLResponse *response, NSError *error))completionBlock
  290. body:(BTJSON *)jsonBody
  291. response:(NSHTTPURLResponse *)response
  292. error:(NSError *)error {
  293. if (completionBlock) {
  294. dispatch_async(self.dispatchQueue, ^{
  295. completionBlock(jsonBody, response, error);
  296. });
  297. }
  298. }
  299. - (dispatch_queue_t)dispatchQueue {
  300. return _dispatchQueue ?: dispatch_get_main_queue();
  301. }
  302. #pragma mark - Default Headers
  303. - (NSDictionary *)defaultHeaders {
  304. return @{ @"User-Agent": [self userAgentString],
  305. @"Accept": [self acceptString],
  306. @"Accept-Language": [self acceptLanguageString] };
  307. }
  308. - (NSString *)userAgentString {
  309. return [NSString stringWithFormat:@"Braintree/iOS/%@", BRAINTREE_VERSION];
  310. }
  311. - (NSString *)platformString {
  312. size_t size = 128;
  313. char *hwModel = alloca(size);
  314. if (sysctlbyname("hw.model", hwModel, &size, NULL, 0) != 0) {
  315. return nil;
  316. }
  317. NSString *hwModelString = [NSString stringWithCString:hwModel encoding:NSUTF8StringEncoding];
  318. #if TARGET_IPHONE_SIMULATOR
  319. hwModelString = [hwModelString stringByAppendingString:@"(simulator)"];
  320. #endif
  321. return hwModelString;
  322. }
  323. - (NSString *)architectureString {
  324. size_t size = 128;
  325. char *hwMachine = alloca(size);
  326. if (sysctlbyname("hw.machine", hwMachine, &size, NULL, 0) != 0) {
  327. return nil;
  328. }
  329. return [NSString stringWithCString:hwMachine encoding:NSUTF8StringEncoding];
  330. }
  331. - (NSString *)acceptString {
  332. return @"application/json";
  333. }
  334. - (NSString *)acceptLanguageString {
  335. NSLocale *locale = [NSLocale currentLocale];
  336. return [NSString stringWithFormat:@"%@-%@",
  337. [locale objectForKey:NSLocaleLanguageCode],
  338. [locale objectForKey:NSLocaleCountryCode]];
  339. }
  340. #pragma mark - Helpers
  341. - (NSArray *)pinnedCertificateData {
  342. NSMutableArray *pinnedCertificates = [NSMutableArray array];
  343. for (NSData *certificateData in self.pinnedCertificates) {
  344. [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
  345. }
  346. return pinnedCertificates;
  347. }
  348. - (void)URLSession:(__unused NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
  349. if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]) {
  350. NSString *domain = challenge.protectionSpace.host;
  351. SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
  352. NSArray *policies = @[(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
  353. SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
  354. SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)self.pinnedCertificateData);
  355. CFErrorRef error;
  356. BOOL trusted = SecTrustEvaluateWithError(serverTrust, &error);
  357. if (trusted && error == nil) {
  358. NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
  359. completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
  360. } else {
  361. completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, NULL);
  362. }
  363. } else {
  364. completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
  365. }
  366. }
  367. - (BOOL)isEqualToHTTP:(BTHTTP *)http {
  368. return [self.baseURL isEqual:http.baseURL] && [self.authorizationFingerprint isEqualToString:http.authorizationFingerprint];
  369. }
  370. - (BOOL)isEqual:(id)object {
  371. if (self == object) {
  372. return YES;
  373. }
  374. if ([object isKindOfClass:[BTHTTP class]]) {
  375. return [self isEqualToHTTP:object];
  376. }
  377. return NO;
  378. }
  379. @end