BTHTTP.m 17 KB

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