#import "HttpdnsEdgeService.h" #import static NSString * const kHttpdnsEdgeErrorDomain = @"com.goeedge.httpdns.edge"; @implementation HttpdnsEdgeResolveResult @end @interface HttpdnsEdgeService () @property (nonatomic, copy) NSString *appId; @property (nonatomic, copy) NSString *primaryServiceHost; @property (nonatomic, copy) NSString *backupServiceHost; @property (nonatomic, assign) NSInteger servicePort; @property (nonatomic, copy) NSString *signSecret; @property (nonatomic, copy) NSString *sessionId; @end @implementation HttpdnsEdgeService - (instancetype)initWithAppId:(NSString *)appId primaryServiceHost:(NSString *)primaryServiceHost backupServiceHost:(NSString *)backupServiceHost servicePort:(NSInteger)servicePort signSecret:(NSString *)signSecret { if (self = [super init]) { _appId = [appId copy]; _primaryServiceHost = [primaryServiceHost copy]; _backupServiceHost = backupServiceHost.length > 0 ? [backupServiceHost copy] : @""; _servicePort = servicePort > 0 ? servicePort : 443; _signSecret = signSecret.length > 0 ? [signSecret copy] : @""; _sessionId = [[[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""] copy]; } return self; } - (void)resolveHost:(NSString *)host queryType:(NSString *)queryType completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable, NSError *_Nullable))completion { if (host.length == 0 || self.appId.length == 0 || self.primaryServiceHost.length == 0) { NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain code:1001 userInfo:@{NSLocalizedDescriptionKey: @"invalid init config or host"}]; completion(nil, error); return; } NSString *qtype = queryType.length > 0 ? queryType.uppercaseString : @"A"; NSArray *hosts = self.backupServiceHost.length > 0 ? @[self.primaryServiceHost, self.backupServiceHost] : @[self.primaryServiceHost]; [self requestResolveHosts:hosts index:0 host:host qtype:qtype completion:completion]; } - (void)requestURL:(NSURL *)url method:(NSString *)method headers:(NSDictionary *)headers body:(NSData *)body completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion { NSString *originHost = url.host ?: @""; if (originHost.length == 0) { NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain code:2001 userInfo:@{NSLocalizedDescriptionKey: @"invalid request host"}]; completion(nil, nil, error); return; } [self resolveHost:originHost queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) { if (error != nil || result.ipv4s.count == 0) { completion(nil, nil, error ?: [NSError errorWithDomain:kHttpdnsEdgeErrorDomain code:2002 userInfo:@{NSLocalizedDescriptionKey: @"NO_IP_AVAILABLE"}]); return; } [self requestByIPList:result.ipv4s index:0 url:url method:method headers:headers body:body completion:completion]; }]; } - (void)requestByIPList:(NSArray *)ips index:(NSInteger)index url:(NSURL *)url method:(NSString *)method headers:(NSDictionary *)headers body:(NSData *)body completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion { if (index >= ips.count) { NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain code:2003 userInfo:@{NSLocalizedDescriptionKey: @"TLS_EMPTY_SNI_FAILED"}]; completion(nil, nil, error); return; } NSString *ip = ips[index]; NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; components.host = ip; if (components.port == nil) { components.port = @(443); } NSURL *targetURL = components.URL; if (targetURL == nil) { [self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion]; return; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:targetURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:8]; request.HTTPMethod = method.length > 0 ? method : @"GET"; if (body.length > 0) { request.HTTPBody = body; } [request setValue:url.host forHTTPHeaderField:@"Host"]; [headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { if ([key.lowercaseString isEqualToString:@"host"]) { return; } [request setValue:obj forHTTPHeaderField:key]; }]; NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; config.timeoutIntervalForRequest = 8; config.timeoutIntervalForResource = 8; NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; [[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [session finishTasksAndInvalidate]; if (error != nil) { [self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion]; return; } completion(data, (NSHTTPURLResponse *)response, nil); }] resume]; } - (void)requestResolveHosts:(NSArray *)hosts index:(NSInteger)index host:(NSString *)host qtype:(NSString *)qtype completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable result, NSError *_Nullable error))completion { if (index >= hosts.count) { NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain code:1002 userInfo:@{NSLocalizedDescriptionKey: @"resolve failed on all service hosts"}]; completion(nil, error); return; } NSString *serviceHost = hosts[index]; NSURLComponents *components = [NSURLComponents new]; components.scheme = @"https"; components.host = serviceHost; components.port = @(self.servicePort); components.path = @"/resolve"; NSMutableArray *items = [NSMutableArray arrayWithArray:@[ [NSURLQueryItem queryItemWithName:@"appId" value:self.appId], [NSURLQueryItem queryItemWithName:@"dn" value:host], [NSURLQueryItem queryItemWithName:@"qtype" value:qtype], [NSURLQueryItem queryItemWithName:@"sid" value:self.sessionId], [NSURLQueryItem queryItemWithName:@"sdk_version" value:@"ios-native-1.0.0"], [NSURLQueryItem queryItemWithName:@"os" value:@"ios"], ]]; if (self.signSecret.length > 0) { NSString *exp = [NSString stringWithFormat:@"%ld", (long)([[NSDate date] timeIntervalSince1970] + 600)]; NSString *nonce = [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""]; NSString *raw = [NSString stringWithFormat:@"%@|%@|%@|%@|%@", self.appId, host.lowercaseString, qtype.uppercaseString, exp, nonce]; NSString *sign = [self hmacSha256Hex:raw secret:self.signSecret]; [items addObject:[NSURLQueryItem queryItemWithName:@"exp" value:exp]]; [items addObject:[NSURLQueryItem queryItemWithName:@"nonce" value:nonce]]; [items addObject:[NSURLQueryItem queryItemWithName:@"sign" value:sign]]; } components.queryItems = items; NSURL *url = components.URL; if (url == nil) { [self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion]; return; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:6]; request.HTTPMethod = @"GET"; NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; config.timeoutIntervalForRequest = 6; config.timeoutIntervalForResource = 6; NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; [[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [session finishTasksAndInvalidate]; if (error != nil || data.length == 0) { [self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion]; return; } NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if (![json isKindOfClass:NSDictionary.class]) { [self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion]; return; } NSString *code = [NSString stringWithFormat:@"%@", json[@"code"] ?: @""]; if (![code.uppercaseString isEqualToString:@"SUCCESS"]) { [self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion]; return; } NSDictionary *dataJSON = [json[@"data"] isKindOfClass:NSDictionary.class] ? json[@"data"] : @{}; NSArray *records = [dataJSON[@"records"] isKindOfClass:NSArray.class] ? dataJSON[@"records"] : @[]; NSMutableArray *ipv4 = [NSMutableArray array]; NSMutableArray *ipv6 = [NSMutableArray array]; for (NSDictionary *row in records) { NSString *type = [NSString stringWithFormat:@"%@", row[@"type"] ?: @""]; NSString *ip = [NSString stringWithFormat:@"%@", row[@"ip"] ?: @""]; if (ip.length == 0) { continue; } if ([type.uppercaseString isEqualToString:@"AAAA"]) { [ipv6 addObject:ip]; } else { [ipv4 addObject:ip]; } } HttpdnsEdgeResolveResult *result = [HttpdnsEdgeResolveResult new]; result.requestId = [NSString stringWithFormat:@"%@", json[@"requestId"] ?: @""]; result.ipv4s = ipv4; result.ipv6s = ipv6; result.ttl = [dataJSON[@"ttl"] integerValue]; completion(result, nil); }] resume]; } - (NSString *)hmacSha256Hex:(NSString *)data secret:(NSString *)secret { NSData *keyData = [secret dataUsingEncoding:NSUTF8StringEncoding]; NSData *messageData = [data dataUsingEncoding:NSUTF8StringEncoding]; unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA256, keyData.bytes, keyData.length, messageData.bytes, messageData.length, cHMAC); NSMutableString *result = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { [result appendFormat:@"%02x", cHMAC[i]]; } return result; } @end