/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 <Security/Security.h>
#import "http_ios_constant.h"
#import "http_ios_request.h"
@implementation http_ios_param
@end
@interface QueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;
- (instancetype)initWithField:(id)field value:(id)value;
- (NSString*)URLEncodedStringValue;
@end
@implementation QueryStringPair
- (instancetype)initWithField:(id)field value:(id)value
{
self = [super init];
if (!self) {
return nil;
}
self.field = field;
self.value = value;
return self;
}
- (NSString*)URLEncodedStringValue
{
if (!self.value || [self.value isEqual:[NSNull null]]) {
return PercentEscapedStringFromString([self.field description]);
} else {
NSString* key = PercentEscapedStringFromString([self.field description]);
NSString* value = PercentEscapedStringFromString([self.value description]);
return [NSString stringWithFormat:@"%@=%@", key, value];
}
}
NSString* PercentEscapedStringFromString(NSString* string) {
static NSString* const kCharactersGeneralDelimitersToEncode = @":#[]@";
static NSString* const kCharactersSubToEncode = @"!$&'()*+,;=";
NSMutableCharacterSet* allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
NSString* encodeString = [kCharactersGeneralDelimitersToEncode stringByAppendingString:kCharactersSubToEncode];
[allowedCharacterSet removeCharactersInString:encodeString];
static NSUInteger const batchSize = 50;
NSUInteger index = 0;
NSMutableString* escaped = @"".mutableCopy;
while (index < string.length) {
NSUInteger length = MIN(string.length - index, batchSize);
NSRange range = NSMakeRange(index, length);
range = [string rangeOfComposedCharacterSequencesForRange:range];
NSString* substring = [string substringWithRange:range];
NSString* encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
[escaped appendString:encoded];
index += range.length;
}
return escaped;
}
@end
@interface http_ios_request () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableData* mutableData;
@property (nonatomic) void* context;
@property (nonatomic, strong) http_ios_param* requestParam;
@property (nonatomic, strong) NSURLSession* urlSession;
@property (nonatomic, strong) NSMutableURLRequest* request;
@end
@implementation http_ios_request
+ (NSString *)getBaseCachePath
{
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString * path = [NSString stringWithFormat:@"%@/http",cachePath];
if ([self isExistFileForPath:path]) {
return path;
}
if ([self createDirectoryForPath:path]) {
NSString * filePath = [NSString stringWithFormat:@"%@/cache.json",path];
if ([self isExistFileForPath:filePath]) {
return filePath;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
return filePath;
}
return @"";
}
- (NSURLSessionDataTask*)sendRequestWith:(http_ios_param*)requestParam
{
_mutableData = [NSMutableData data];
_requestParam = requestParam;
self.request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestParam.urlPath]];
self.request.HTTPMethod = requestParam.method;
if (requestParam.headerJson) {
for (NSString* key in requestParam.headerJson) {
[self.request setValue:requestParam.headerJson[key] forHTTPHeaderField:key];
}
}
self.context = requestParam.context;
NSString* acceptEncoding = @"gzip";
[self.request setValue:acceptEncoding forHTTPHeaderField:@"Accept-Encoding"];
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = requestParam.connectTimeout;
sessionConfig.timeoutIntervalForResource = requestParam.readTimeout;
sessionConfig.HTTPShouldUsePipelining = true;
if (requestParam.usingHttpProxyType) {
if (requestParam.proxyhost && requestParam.proxyhost.length != 0 ) {
sessionConfig.connectionProxyDictionary =
[self setproxyId:requestParam.proxyhost proxyPort:requestParam.proxyport];
}
}
self.urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
if (requestParam.bodyOrQueryConfigured) {
[self serializingBodyAndQueryParams];
} else {
[self serializingQueryParams];
}
NSURLSessionDataTask* task = [self.urlSession dataTaskWithRequest:self.request];
task.priority = requestParam.priority;
[task resume];
return task;
}
#pragma mark - delegate
- (void)URLSession:(NSURLSession*)session task:(NSURLSessionTask*)task didCompleteWithError:(NSError*)error
{
NSHTTPURLResponse* response = (NSHTTPURLResponse*)task.response;
if (error) {
if (self.failBlock) {
NSInteger errorCode;
if ([ErrorCodeMap.allKeys containsObject:@(error.code)]) {
NSNumber* num = ErrorCodeMap[@(error.code)];
errorCode = num.integerValue;
} else {
errorCode = error.code;
}
self.failBlock(errorCode, error.localizedDescription, self.context);
}
} else {
if (self.responseBlock) {
self.responseBlock(task, response, self.mutableData, self.context);
}
}
}
- (void)URLSession:(NSURLSession*)session dataTask:(NSURLSessionDataTask*)dataTask didReceiveData:(NSData*)data
{
[self.mutableData appendData:data];
if (self.downloadProgress) {
self.downloadProgress(self.context, dataTask.countOfBytesExpectedToReceive, dataTask.countOfBytesReceived);
}
if (self.memoryBodyBlock) {
self.memoryBodyBlock(data, self.context);
}
}
- (void)URLSession:(NSURLSession*)session dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(nonnull NSURLResponse*)response
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler
{
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
if (self.memoryHeaderBlock) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSDictionary* headers = [httpResponse allHeaderFields];
self.memoryHeaderBlock(headers, self.context);
}
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession*)session task:(NSURLSessionTask*)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
if (self.uploadProgress) {
self.uploadProgress(self.context, totalBytesExpectedToSend, totalBytesSent);
}
}
- (void)URLSession:(nonnull NSURLSession*)session
downloadTask:(nonnull NSURLSessionDownloadTask*)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if (self.downloadProgress) {
self.downloadProgress(self.context, totalBytesWritten, totalBytesExpectedToWrite);
}
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
newRequest:(NSURLRequest*)request
completionHandler:(void (^)(NSURLRequest* _Nullable))completionHandler
{
NSURLRequest* redirectRequest = request;
if (self.redirectionBlock) {
redirectRequest = self.redirectionBlock(session, task, response, request);
}
if (completionHandler) {
completionHandler(redirectRequest);
}
}
- (void)URLSession:(NSURLSession*)session
didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential* _Nullable))completionHandler
{
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
NSURLCredential* credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, credential);
}
} else if ([challenge.protectionSpace.authenticationMethod
isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
NSData *p12Data = [NSData dataWithContentsOfFile:self.requestParam.ca];
if (p12Data) {
CFDataRef inP12Data = (__bridge CFDataRef)p12Data;
NSDictionary *options = @{(__bridge id)kSecImportExportPassphrase: self.requestParam.password};
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus status = SecPKCS12Import(inP12Data, (__bridge CFDictionaryRef)options, &items);
if (status == errSecSuccess) {
CFDictionaryRef identityDict = (CFDictionaryRef)CFArrayGetValueAtIndex(items, 0);
SecIdentityRef identityRef = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
SecCertificateRef certificateRef;
SecIdentityCopyCertificate(identityRef, &certificateRef);
NSArray *certificates = @[(__bridge id)certificateRef];
NSURLCredential *credential = [NSURLCredential
credentialWithIdentity:identityRef
certificates:certificates
persistence:NSURLCredentialPersistencePermanent];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
CFRelease(certificateRef);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
CFRelease(items);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
#pragma mark - setter
- (void)setResponseBlock:(HTTPRequestSuccessBlock)responseBlock
{
_responseBlock = responseBlock;
}
- (void)setFailBlock:(HTTPRequestFailureBlock)failBlock
{
_failBlock = failBlock;
}
- (void)setUploadProgress:(HTTPRequestProgressBlock)uploadProgress
{
_uploadProgress = uploadProgress;
}
- (void)setDownloadProgress:(HTTPRequestProgressBlock)downloadProgress
{
_downloadProgress = downloadProgress;
}
- (void)setMemoryBodyBlock:(HTTPRequestMemoryBodyBlock)memoryBodyBlock
{
_memoryBodyBlock = memoryBodyBlock;
}
- (void)setMemoryHeaderBlock:(HTTPRequestMemoryHeaderBlock)memoryHeaderBlock
{
_memoryHeaderBlock = memoryHeaderBlock;
}
- (void)setRedirectionBlock:(HTTPRequestWillRedirectionBlock)redirectionBlock
{
_redirectionBlock = redirectionBlock;
}
#pragma mark - private
- (void)serializingBodyAndQueryParams
{
if (!self.requestParam.bodyParam) {
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=1, bodyParam=nil, method=%@", self.requestParam.method);
return;
}
if ([self.requestParam.bodyParam isKindOfClass:[NSString class]]) {
NSData* bodyData = [self.requestParam.bodyParam dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=1, type=NSString, method=%@, bodyLen=%lu",
self.requestParam.method, (unsigned long)bodyData.length);
if (bodyData) {
[self.request setHTTPBody:bodyData];
}
} else if ([self.requestParam.bodyParam isKindOfClass:[NSDictionary class]]) {
NSString* contentType = [self.request valueForHTTPHeaderField:@"Content-Type"];
if (contentType.length == 0) {
contentType = @"application/json";
[self.request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
}
NSMutableArray* mutablePairs = [NSMutableArray array];
for (QueryStringPair* pair in queryStringPairsFromDictionary(self.requestParam.bodyParam)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
NSString* query = [mutablePairs componentsJoinedByString:@"&"];
NSData* bodyData = queryDataFromParameters(contentType, query, self.requestParam.bodyParam);
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=1, type=NSDictionary, method=%@, ct=%@, bodyLen=%lu",
self.requestParam.method, contentType, (unsigned long)bodyData.length);
if (bodyData) {
[self.request setHTTPBody:bodyData];
}
}
}
- (void)serializingQueryParams
{
if (!self.requestParam.bodyParam) {
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=0, bodyParam=nil, method=%@", self.requestParam.method);
return;
}
NSDictionary* parameters = nil;
if ([self.requestParam.bodyParam isKindOfClass:[NSDictionary class]]) {
parameters = self.requestParam.bodyParam;
} else {
parameters = [self convertDictionaryWithJSONString:self.requestParam.bodyParam];
}
NSString* query = nil;
if (parameters) {
NSMutableArray* mutablePairs = [NSMutableArray array];
for (QueryStringPair* pair in queryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
query = [mutablePairs componentsJoinedByString:@"&"];
}
NSSet<NSString*>* methodSet = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
if ([methodSet containsObject:[self.requestParam.method uppercaseString]]) {
if (query && query.length > 0) {
NSString* absoluteString = [self.request.URL absoluteString];
NSString* string = [absoluteString stringByAppendingFormat:self.request.URL.query ?@"&%@":@"?%@",query];
self.request.URL = [NSURL URLWithString:string];
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=0, type=GET, method=%@", self.requestParam.method);
}
} else {
NSString * contentType = [self.request valueForHTTPHeaderField:@"Content-Type"];
//default application/json
if (contentType.length == 0) {
contentType = @"application/json";
[self.request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
}
NSData * bodyData = queryDataFromParameters(contentType, query, parameters);
if (!bodyData && [self.requestParam.bodyParam isKindOfClass:[NSString class]]) {
bodyData = [self.requestParam.bodyParam dataUsingEncoding:NSUTF8StringEncoding];
}
if (bodyData) {
[self.request setHTTPBody:bodyData];
}
NSLog(@"[HTTP_DEBUG] bodyOrQueryConfigured=0, type=POST, method=%@, bodyLen=%lu",
self.requestParam.method, (unsigned long)bodyData.length);
}
}
NSData * queryDataFromParameters(NSString* contentType, NSString* query,NSDictionary* parameters)
{
NSData *bodyData = nil;
if ([contentType isEqualToString:@"application/x-plist"]) {
if (parameters) {
NSError *error = nil;
bodyData = [NSPropertyListSerialization dataWithPropertyList:parameters
format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
if (error) {
NSLog(@"JSONSerialization fail:%@", error.localizedDescription);
}
}
} else if ([contentType isEqualToString:@"application/x-www-form-urlencoded"]) {
if (query) {
bodyData = [query dataUsingEncoding:NSUTF8StringEncoding];
}
}else {
if (parameters) {
NSError *error = nil;
bodyData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&error];
if (error) {
NSLog(@"JSONSerialization fail:%@", error.localizedDescription);
}
}
}
return bodyData;
}
NSString* queryStringFromParameters(NSDictionary* parameters)
{
NSMutableArray* mutablePairs = [NSMutableArray array];
for (QueryStringPair* pair in queryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
return [mutablePairs componentsJoinedByString:@"&"];
}
NSArray* queryStringPairsFromDictionary(NSDictionary* dictionary)
{
return queryStringPairsFromKeyAndValue(nil, dictionary);
}
NSArray* queryStringPairsFromKeyAndValue(NSString* key, id value)
{
NSMutableArray* mutableQueryStringComponents = [NSMutableArray array];
NSSortDescriptor* sortDescriptor =
[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];
if ([value isKindOfClass:[NSDictionary class]]) {
NSDictionary* dictionary = value;
for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[sortDescriptor]]) {
id nestedValue = dictionary[nestedKey];
if (nestedValue) {
NSString* nestedString = [NSString stringWithFormat:@"%@[%@]", key, nestedKey];
NSArray* queryArray = queryStringPairsFromKeyAndValue((key ? nestedString : nestedKey), nestedValue);
[mutableQueryStringComponents addObjectsFromArray: queryArray];
}
}
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray* array = value;
for (id nestedValue in array) {
NSArray* qArray = queryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue);
[mutableQueryStringComponents addObjectsFromArray:qArray];
}
} else if ([value isKindOfClass:[NSSet class]]) {
NSSet* set = value;
for (id obj in [set sortedArrayUsingDescriptors:@[sortDescriptor]]) {
[mutableQueryStringComponents addObjectsFromArray:queryStringPairsFromKeyAndValue(key, obj)];
}
} else {
[mutableQueryStringComponents addObject:[[QueryStringPair alloc] initWithField:key value:value]];
}
return mutableQueryStringComponents;
}
- (NSDictionary*)convertDictionaryWithJSONString:(NSString*)jsonString
{
if (!jsonString || jsonString.length == 0) {
return nil;
}
NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError* err;
NSDictionary* dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
if (err) {
NSLog(@"%@", err);
return nil;
}
return dic;
}
- (NSDictionary*)setproxyId:(NSString*)proxyId proxyPort:(NSInteger)proxyPort
{
NSDictionary* proxyDic = [[NSDictionary alloc] init];
proxyDic = @{@"HTTPSEnable": @YES,
@"HTTPSProxy": proxyId,
@"HTTPSPort": [NSString stringWithFormat:@"%ld", (long)proxyPort],
@"HTTPEnable": @YES,
@"HTTPProxy": proxyId,
@"HTTPPort": [NSString stringWithFormat:@"%ld", (long)proxyPort]};
return proxyDic;
}
- (void)cleanTasks
{
[self.urlSession getTasksWithCompletionHandler:^(NSArray* dataTasks,
NSArray* uploadTasks,
NSArray* downloadTasks) {
NSArray<NSURLSessionTask*>* tasks;
if (dataTasks.count != 0) {
tasks = dataTasks;
}
if (dataTasks.count != 0) {
tasks = uploadTasks;
}
if (dataTasks.count != 0) {
tasks = downloadTasks;
}
for (NSURLSessionTask* task in tasks) {
[task cancel];
}
}];
}
- (void)deInitialize
{
_responseBlock = nil;
_memoryBodyBlock = nil;
_memoryHeaderBlock = nil;
_uploadProgress = nil;
_downloadProgress = nil;
_failBlock = nil;
if (self.urlSession) {
[self cleanTasks];
[self.urlSession invalidateAndCancel];
self.urlSession = nil;
}
self.mutableData = nil;
}
- (void)dealloc
{
[self deInitialize];
#if __has_feature(objc_mrc)
[super dealloc];
#endif
}
+ (BOOL)isExistFileForPath:(NSString*)strFilePath
{
if (strFilePath.length < 1) {
return NO;
}
NSFileManager *fileMgr = [NSFileManager defaultManager];
BOOL bDir = NO;
BOOL bExist = [fileMgr fileExistsAtPath:strFilePath isDirectory:&bDir];
if (!bDir && bExist) {
return YES;
}
return NO;
}
+ (BOOL)isExistDirectoryForPath:(NSString*)strDirPath
{
if (strDirPath.length < 1) {
return NO;
}
NSFileManager* fileMgr = [NSFileManager defaultManager];
BOOL bDir = NO;
BOOL bExist = [fileMgr fileExistsAtPath:strDirPath isDirectory:&bDir];
if (bDir && bExist) {
return YES;
}
return NO;
}
+ (BOOL)createDirectoryForPath:(NSString*)strDirPath
{
if (strDirPath.length < 1) {
return NO;
}
if ([self isExistDirectoryForPath:strDirPath]) {
return YES;
}
NSFileManager* fileMgr = [NSFileManager defaultManager];
BOOL result = [fileMgr createDirectoryAtPath:strDirPath withIntermediateDirectories:YES attributes:nil error:nil];
return result;
}
@end