// // Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). // You may not use this file except in compliance with the License. // A copy of the License is located at // // http://aws.amazon.com/apache2.0 // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. // #import "AWSAPIGatewayClient.h" #import <AWSCore/AWSCore.h> NSString *const AWSAPIGatewayErrorDomain = @"com.amazonaws.AWSAPIGatewayErrorDomain"; NSString *const AWSAPIGatewayErrorHTTPBodyKey = @"HTTPBody"; NSString *const AWSAPIGatewayErrorHTTPHeaderFieldsKey = @"HTTPHeaderFields"; static NSString *const AWSAPIGatewayAPIKeyHeader = @"x-api-key"; NSString *const AWSAPIGatewaySDKVersion = @"2.33.3"; static int defaultChunkSize = 1024; @interface AWSAPIGatewayClient() // Networking @property (nonatomic, strong) NSURLSession *session; @end @interface AWSAPIGatewayRequest() @property (nonatomic, strong) NSString *HTTPMethod; @property (nonatomic, strong) NSString *URLString; @property (nonatomic, strong) NSDictionary *queryParameters; @property (nonatomic, strong) NSDictionary *headerParameters; @property (nonatomic, strong) id HTTPBody; @end @interface AWSAPIGatewayResponse() @property (nonatomic, readwrite) NSDictionary *headers; @property (nonatomic, readwrite) NSData *responseData; @property (nonatomic, readwrite) NSURLResponse *rawResponse; @property (nonatomic, readwrite) NSInteger statusCode; - (instancetype)initWithHeaders:(NSDictionary *)headers responseData:(NSData *)responseData NSURLResponseObject:(NSURLResponse *)NSURLResponseObject statusCode:(NSInteger)statusCode; @end @implementation AWSAPIGatewayClient + (void)initialize { [super initialize]; if (![AWSiOSSDKVersion isEqualToString:AWSAPIGatewaySDKVersion]) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"AWSCore and AWSAPIGateway versions need to match. Check your SDK installation. AWSCore: %@ AWSAPIGateway: %@", AWSiOSSDKVersion, AWSAPIGatewaySDKVersion] userInfo:nil]; } } - (instancetype)init { if (self = [super init]) { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; }); _session = session; } return self; } - (AWSTask<AWSAPIGatewayResponse *> *)invoke:(AWSAPIGatewayRequest *)apiRequest { if(!apiRequest) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"AWSAPIGatewayRequest cannot be nil"] userInfo:nil]; } NSURL *URL = [self requestURL:[apiRequest.URLString aws_stringWithURLEncodingPath] query:apiRequest.queryParameters URLPathComponentsDictionary:nil]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; request.HTTPMethod = apiRequest.HTTPMethod; request.allHTTPHeaderFields = [self finalizeRequestHeaders:apiRequest.headerParameters]; AWSTask *task = [AWSTask taskWithResult:nil]; task = [task continueWithSuccessBlock:^id(AWSTask *task) { NSError *error = nil; if (apiRequest.HTTPBody != nil) { if ([apiRequest.HTTPBody isKindOfClass:[NSString class]]) { NSString *body = (NSString *)apiRequest.HTTPBody; NSDictionary *bodyParameters = [NSJSONSerialization JSONObjectWithData:[body dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; request.HTTPBody = [NSJSONSerialization dataWithJSONObject:bodyParameters options:0 error:&error]; } else if ([apiRequest.HTTPBody isKindOfClass:[NSDictionary class]]) { request.HTTPBody = [NSJSONSerialization dataWithJSONObject:apiRequest.HTTPBody options:0 error:&error]; } else if ([apiRequest.HTTPBody isKindOfClass:[NSInputStream class]]) { NSInputStream *iStream = (NSInputStream *)apiRequest.HTTPBody; NSOutputStream *oStream = [[NSOutputStream alloc] initToMemory]; [iStream open]; [oStream open]; int len = defaultChunkSize; uint8_t buf[len]; while (YES) { if ([oStream hasSpaceAvailable]) { NSInteger bytesRead = [iStream read:buf maxLength:len]; if ([oStream write:(const uint8_t *)buf maxLength:bytesRead] == -1) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"Error occurred while writing input stream to output stream."] userInfo:nil]; break; } if (defaultChunkSize != bytesRead) { break; } } } NSData *data = [oStream propertyForKey: NSStreamDataWrittenToMemoryStreamKey]; if (!data) { AWSDDLogVerbose(@"No data written to memory!"); } else { request.HTTPBody = data; } [oStream close]; oStream = nil; } else { request.HTTPBody = apiRequest.HTTPBody; } if (!request.HTTPBody && ![apiRequest.HTTPBody isKindOfClass:[NSInputStream class]]) { AWSDDLogError(@"Failed to set a request body. %@", error); } } return nil; }]; // Refreshes credentials if necessary task = [task continueWithSuccessBlock:^id(AWSTask *task) { id signer = [self.configuration.requestInterceptors lastObject]; if (signer) { if ([signer respondsToSelector:@selector(credentialsProvider)]) { id<AWSCredentialsProvider> credentialsProvider = [signer performSelector:@selector(credentialsProvider)]; return [credentialsProvider credentials]; } } return nil; }]; // Signs the request for (id<AWSNetworkingRequestInterceptor> interceptor in self.configuration.requestInterceptors) { task = [task continueWithSuccessBlock:^id(AWSTask *task) { return [interceptor interceptRequest:request]; }]; } return [task continueWithSuccessBlock:^id(AWSTask *task) { AWSTaskCompletionSource *completionSource = [AWSTaskCompletionSource new]; void (^completionHandler)(NSData *data, NSURLResponse *response, NSError *error) = ^(NSData *data, NSURLResponse *response, NSError *error) { // Networking errors if (error) { [completionSource setError:error]; } else { NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; NSDictionary *HTTPHeaderFields = HTTPResponse.allHeaderFields; NSInteger HTTPStatusCode = HTTPResponse.statusCode; [completionSource setResult:[[AWSAPIGatewayResponse alloc] initWithHeaders:HTTPHeaderFields responseData:data NSURLResponseObject:response statusCode:HTTPStatusCode]]; } }; AWSDDLogVerbose(@"%@",request); NSURLSessionDataTask *sessionTask = [self.session dataTaskWithRequest:request completionHandler:completionHandler]; [sessionTask resume]; return completionSource.task; }]; } - (AWSTask *)invokeHTTPRequest:(NSString *)HTTPMethod URLString:(NSString *)URLString pathParameters:(NSDictionary *)pathParameters queryParameters:(NSDictionary *)queryParameters headerParameters:(NSDictionary *)headerParameters body:(id)body responseClass:(Class)responseClass { NSURL *URL = [self requestURL:URLString query:queryParameters URLPathComponentsDictionary:pathParameters]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; request.HTTPMethod = HTTPMethod; request.allHTTPHeaderFields = [self finalizeRequestHeaders:headerParameters]; NSError *error = nil; if (body != nil) { NSDictionary *bodyParameters = [[AWSMTLJSONAdapter JSONDictionaryFromModel:body] aws_removeNullValues]; request.HTTPBody = [NSJSONSerialization dataWithJSONObject:bodyParameters options:0 error:&error]; if (!request.HTTPBody) { AWSDDLogError(@"Failed to serialize a request body. %@", error); } } // Refreshes credentials if necessary AWSTask *task = [AWSTask taskWithResult:nil]; task = [task continueWithSuccessBlock:^id(AWSTask *task) { id signer = [self.configuration.requestInterceptors lastObject]; if (signer) { if ([signer respondsToSelector:@selector(credentialsProvider)]) { id<AWSCredentialsProvider> credentialsProvider = [signer performSelector:@selector(credentialsProvider)]; return [credentialsProvider credentials]; } } return nil; }]; // Signs the request for (id<AWSNetworkingRequestInterceptor> interceptor in self.configuration.requestInterceptors) { task = [task continueWithSuccessBlock:^id(AWSTask *task) { return [interceptor interceptRequest:request]; }]; } return [task continueWithSuccessBlock:^id(AWSTask *task) { AWSTaskCompletionSource *completionSource = [AWSTaskCompletionSource new]; void (^completionHandler)(NSData *data, NSURLResponse *response, NSError *error) = ^(NSData *data, NSURLResponse *response, NSError *error) { // Networking errors if (error) { [completionSource setError:error]; return; } // Serializes the HTTP body id JSONObject = nil; if (data && [data length] > 0) { JSONObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error]; if (!JSONObject) { NSString *bodyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if ([bodyString length] > 0) { AWSDDLogError(@"The body is not in JSON format. Body: %@\nError: %@", bodyString, error); } [completionSource setError:error]; return; } } // Handles developer defined errors NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; NSDictionary *HTTPHeaderFields = HTTPResponse.allHeaderFields; NSInteger HTTPStatusCode = HTTPResponse.statusCode; if (HTTPStatusCode/100 == 4 || HTTPStatusCode/100 == 5) { NSMutableDictionary *userInfo = [NSMutableDictionary new]; if (JSONObject) { userInfo[AWSAPIGatewayErrorHTTPBodyKey] = JSONObject; } if (HTTPHeaderFields) { userInfo[AWSAPIGatewayErrorHTTPHeaderFieldsKey] = HTTPHeaderFields; } if (HTTPStatusCode/100 == 4) { NSError *clientError = [NSError errorWithDomain:AWSAPIGatewayErrorDomain code:AWSAPIGatewayErrorTypeClient userInfo:userInfo]; [completionSource setError:clientError]; } if (HTTPStatusCode/100 == 5) { NSError *clientError = [NSError errorWithDomain:AWSAPIGatewayErrorDomain code:AWSAPIGatewayErrorTypeService userInfo:userInfo]; [completionSource setError:clientError]; } return; } // Maps a serialized JSON object to an Objective-C object if (JSONObject) { if (responseClass && responseClass != [NSDictionary class]) { if ([JSONObject isKindOfClass:[NSDictionary class]]) { NSError *responseSerializationError = nil; JSONObject = [AWSMTLJSONAdapter modelOfClass:responseClass fromJSONDictionary:JSONObject error:&responseSerializationError]; if (!JSONObject) { AWSDDLogError(@"Failed to serialize the body JSON. %@", responseSerializationError); } } if ([JSONObject isKindOfClass:[NSArray class]]) { NSError *responseSerializationError = nil; NSMutableArray *models = [NSMutableArray new]; for (id object in JSONObject) { id model = [AWSMTLJSONAdapter modelOfClass:responseClass fromJSONDictionary:object error:&responseSerializationError]; [models addObject:model]; if (!JSONObject) { AWSDDLogError(@"Failed to serialize the body JSON. %@", responseSerializationError); } } JSONObject = models; } } [completionSource setResult:JSONObject]; } else { [completionSource setResult:nil]; } }; NSURLSessionDataTask *sessionTask = [self.session dataTaskWithRequest:request completionHandler:completionHandler]; [sessionTask resume]; return completionSource.task; }]; } - (NSDictionary *)finalizeRequestHeaders:(NSDictionary *)requestHeaders { NSMutableDictionary *finalizedDictionary = [NSMutableDictionary dictionaryWithDictionary:requestHeaders]; if ([requestHeaders valueForKey:@"Cache-Control"] == nil) { [finalizedDictionary setValue:@"no-store" forKey:@"Cache-Control"]; } if (self.APIKey) { [finalizedDictionary setValue:self.APIKey forKey:AWSAPIGatewayAPIKeyHeader]; } return finalizedDictionary; } - (NSURL *)requestURL:(NSString *)URLString query:(NSDictionary *)query URLPathComponentsDictionary:(NSDictionary * _Nullable)URLPathComponentsDictionary { NSMutableString *mutableURLString = [NSMutableString stringWithString:URLString]; // Constructs the URL path components if (URLPathComponentsDictionary) { for (NSString *key in URLPathComponentsDictionary) { [mutableURLString replaceOccurrencesOfString:[NSString stringWithFormat:@"{%@}", key] withString:[self encodeQueryStringValue:URLPathComponentsDictionary[key]] options:NSLiteralSearch range:NSMakeRange(0, [mutableURLString length])]; } } // Adds query string NSMutableString *queryString = [NSMutableString new]; [self processParameters:query queryString:queryString]; if ([queryString length] > 0) { [mutableURLString appendFormat:@"?%@", queryString]; } NSString *urlString = [NSString stringWithFormat:@"%@%@", self.configuration.baseURL, mutableURLString]; return [NSURL URLWithString:urlString]; } // TODO: merge it with - (void)processParameters:(NSDictionary *)parameters queryString:(NSMutableString *)queryString in AWSURLRequestSerialization.m - (void)processParameters:(NSDictionary *)parameters queryString:(NSMutableString *)queryString { for (NSString *key in parameters) { id obj = parameters[key]; if ([obj isKindOfClass:[NSDictionary class]]) { [self processParameters:obj queryString:queryString]; } else { if ([queryString length] > 0) { [queryString appendString:@"&"]; } [queryString appendString:[self generateQueryStringWithKey:key value:obj]]; } } } - (NSString *)generateQueryStringWithKey:(NSString *)key value:(id)value { NSMutableString *queryString = [NSMutableString new]; [queryString appendString:[key aws_stringWithURLEncoding]]; [queryString appendString:@"="]; [queryString appendString:[self encodeQueryStringValue:value]]; return queryString; } - (NSString *)encodeQueryStringValue:(id)value { if ([value isKindOfClass:[NSString class]]) { return [value aws_stringWithURLEncoding]; } if ([value isKindOfClass:[NSNumber class]]) { return [[value stringValue] aws_stringWithURLEncoding]; } if ([value isKindOfClass:[NSArray class]]) { NSMutableString *mutableString = [NSMutableString new]; for (id obj in value) { if ([mutableString length] > 0) { [mutableString appendString:@","]; } [mutableString appendString:[self encodeQueryStringValue:obj]]; } return mutableString; } AWSDDLogError(@"value[%@] is invalid.", value); return [[value description] aws_stringWithURLEncoding]; } @end