FIRFADApiService.m 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebaseAppDistribution/Sources/FIRFADApiService.h"
  15. #import <Foundation/Foundation.h>
  16. #import "FirebaseAppDistribution/Sources/FIRFADLogger.h"
  17. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  18. #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
  19. NSString *const kFIRFADApiErrorDomain = @"com.firebase.appdistribution.api";
  20. NSString *const kFIRFADApiErrorDetailsKey = @"details";
  21. NSString *const kHTTPGet = @"GET";
  22. // The App Distribution Tester API endpoint used to retrieve releases
  23. NSString *const kReleasesEndpointURLTemplate =
  24. @"https://firebaseapptesters.googleapis.com/v1alpha/devices/"
  25. @"-/testerApps/%@/installations/%@/releases";
  26. NSString *const kInstallationAuthHeader = @"X-Goog-Firebase-Installations-Auth";
  27. NSString *const kApiHeaderKey = @"X-Goog-Api-Key";
  28. NSString *const kApiBundleKey = @"X-Ios-Bundle-Identifier";
  29. NSString *const kResponseReleasesKey = @"releases";
  30. @implementation FIRFADApiService
  31. + (void)generateAuthTokenWithCompletion:(FIRFADGenerateAuthTokenCompletion)completion {
  32. FIRInstallations *installations = [FIRInstallations installations];
  33. // Get a FIS Authentication Token.
  34. [installations authTokenWithCompletion:^(
  35. FIRInstallationsAuthTokenResult *_Nullable authTokenResult,
  36. NSError *_Nullable error) {
  37. if ([self handleError:&error
  38. description:@"Failed to generate Firebase installation auth token."
  39. code:FIRFADApiTokenGenerationFailure]) {
  40. FIRFADErrorLog(@"Error getting fresh auth tokens. Error: %@", [error localizedDescription]);
  41. completion(nil, nil, error);
  42. return;
  43. }
  44. [installations installationIDWithCompletion:^(NSString *__nullable identifier,
  45. NSError *__nullable error) {
  46. if ([self handleError:&error
  47. description:@"Failed to fetch Firebase Installation ID."
  48. code:FIRFADApiInstallationIdentifierError]) {
  49. FIRFADErrorLog(@"Error getting installation id. Error: %@", [error localizedDescription]);
  50. completion(nil, nil, error);
  51. return;
  52. }
  53. completion(identifier, authTokenResult, nil);
  54. }];
  55. }];
  56. }
  57. + (NSMutableURLRequest *)createHTTPRequest:(NSString *)method
  58. withUrl:(NSString *)urlString
  59. withAuthToken:(FIRInstallationsAuthTokenResult *)authTokenResult {
  60. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  61. FIRFADInfoLog(@"Requesting releases for app id - %@", [[FIRApp defaultApp] options].googleAppID);
  62. [request setURL:[NSURL URLWithString:urlString]];
  63. [request setHTTPMethod:method];
  64. [request setValue:authTokenResult.authToken forHTTPHeaderField:kInstallationAuthHeader];
  65. [request setValue:[[FIRApp defaultApp] options].APIKey forHTTPHeaderField:kApiHeaderKey];
  66. [request setValue:[NSBundle mainBundle].bundleIdentifier forHTTPHeaderField:kApiBundleKey];
  67. return request;
  68. }
  69. + (NSString *)tryParseGoogleAPIErrorFromResponse:(NSData *)data {
  70. if (!data) {
  71. return @"No data in response.";
  72. }
  73. NSError *parseError;
  74. NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data
  75. options:0
  76. error:&parseError];
  77. if (parseError) {
  78. return @"Could not parse additional details about this API error.";
  79. } else {
  80. NSDictionary *errorDict = [responseDict objectForKey:@"error"];
  81. if (!errorDict) {
  82. return @"Could not parse additional details about this API error.";
  83. }
  84. NSString *message = [errorDict objectForKey:@"message"];
  85. if (!message) {
  86. return @"Could not parse additional details about this API error.";
  87. }
  88. return message;
  89. }
  90. }
  91. + (NSArray *)handleReleaseResponse:(NSData *)data
  92. response:(NSURLResponse *)response
  93. error:(NSError **_Nullable)error {
  94. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  95. FIRFADInfoLog(@"HTTPResonse status code %ld response %@", (long)[httpResponse statusCode],
  96. httpResponse);
  97. if ([self handleHttpResponseError:httpResponse error:error]) {
  98. FIRFADErrorLog(@"App Tester API service error: %@. %@", [*error localizedDescription],
  99. [self tryParseGoogleAPIErrorFromResponse:data]);
  100. return nil;
  101. }
  102. return [self parseApiResponseWithData:data error:error];
  103. }
  104. + (void)fetchReleasesWithCompletion:(FIRFADFetchReleasesCompletion)completion {
  105. void (^executeFetch)(NSString *_Nullable, FIRInstallationsAuthTokenResult *, NSError *_Nullable) =
  106. ^(NSString *_Nullable identifier, FIRInstallationsAuthTokenResult *authTokenResult,
  107. NSError *_Nullable error) {
  108. NSString *urlString =
  109. [NSString stringWithFormat:kReleasesEndpointURLTemplate,
  110. [[FIRApp defaultApp] options].googleAppID, identifier];
  111. NSMutableURLRequest *request = [self createHTTPRequest:@"GET"
  112. withUrl:urlString
  113. withAuthToken:authTokenResult];
  114. FIRFADInfoLog(@"Url : %@, Auth token: %@ API KEY: %@", urlString, authTokenResult.authToken,
  115. [[FIRApp defaultApp] options].APIKey);
  116. NSURLSessionDataTask *listReleasesDataTask = [[NSURLSession sharedSession]
  117. dataTaskWithRequest:request
  118. completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  119. NSArray *releases = [self handleReleaseResponse:data
  120. response:response
  121. error:&error];
  122. dispatch_async(dispatch_get_main_queue(), ^{
  123. completion(releases, error);
  124. });
  125. }];
  126. [listReleasesDataTask resume];
  127. };
  128. [self generateAuthTokenWithCompletion:executeFetch];
  129. }
  130. + (BOOL)handleHttpResponseError:(NSHTTPURLResponse *)httpResponse error:(NSError **_Nullable)error {
  131. if (*error || !httpResponse) {
  132. return [self handleError:error
  133. description:@"Unknown http error occurred"
  134. code:FIRApiErrorUnknownFailure];
  135. }
  136. if ([httpResponse statusCode] != 200) {
  137. *error = [self createErrorFromStatusCode:[httpResponse statusCode]];
  138. return YES;
  139. }
  140. return NO;
  141. }
  142. + (NSError *)createErrorFromStatusCode:(NSInteger)statusCode {
  143. if (statusCode == 401) {
  144. return [self createErrorWithDescription:@"Tester not authenticated"
  145. code:FIRFADApiErrorUnauthenticated];
  146. }
  147. if (statusCode == 403 || statusCode == 400) {
  148. return [self createErrorWithDescription:@"Tester not authorized"
  149. code:FIRFADApiErrorUnauthorized];
  150. }
  151. if (statusCode == 404) {
  152. return [self createErrorWithDescription:@"Tester or releases not found"
  153. code:FIRFADApiErrorUnauthorized];
  154. }
  155. if (statusCode == 408 || statusCode == 504) {
  156. return [self createErrorWithDescription:@"Request timeout" code:FIRFADApiErrorTimeout];
  157. }
  158. FIRFADErrorLog(@"Encountered unmapped status code: %ld", (long)statusCode);
  159. NSString *description = [NSString stringWithFormat:@"Unknown status code: %ld", (long)statusCode];
  160. return [self createErrorWithDescription:description code:FIRApiErrorUnknownFailure];
  161. }
  162. + (BOOL)handleError:(NSError **_Nullable)error
  163. description:(NSString *)description
  164. code:(FIRFADApiError)code {
  165. if (*error) {
  166. *error = [self createErrorWithDescription:description code:code];
  167. return YES;
  168. }
  169. return NO;
  170. }
  171. + (NSError *)createErrorWithDescription:description code:(FIRFADApiError)code {
  172. NSDictionary *userInfo = @{NSLocalizedDescriptionKey : description};
  173. return [NSError errorWithDomain:kFIRFADApiErrorDomain code:code userInfo:userInfo];
  174. }
  175. + (NSArray *_Nullable)parseApiResponseWithData:(NSData *)data error:(NSError **_Nullable)error {
  176. NSDictionary *serializedResponse = [NSJSONSerialization JSONObjectWithData:data
  177. options:0
  178. error:error];
  179. if (*error) {
  180. FIRFADErrorLog(@"Tester API - Error deserializing json response");
  181. NSString *description = (*error).userInfo[NSLocalizedDescriptionKey]
  182. ? (*error).userInfo[NSLocalizedDescriptionKey]
  183. : @"Failed to parse response";
  184. [self handleError:error description:description code:FIRApiErrorParseFailure];
  185. return nil;
  186. }
  187. NSArray *releases = [serializedResponse objectForKey:kResponseReleasesKey];
  188. return releases;
  189. }
  190. @end