278 lines
12 KiB
Objective-C
278 lines
12 KiB
Objective-C
#import "HttpdnsEdgeService.h"
|
|
|
|
#import <CommonCrypto/CommonCrypto.h>
|
|
|
|
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<NSString *> *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<NSString *,NSString *> *)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<NSString *> *)ips
|
|
index:(NSInteger)index
|
|
url:(NSURL *)url
|
|
method:(NSString *)method
|
|
headers:(NSDictionary<NSString *, NSString *> *)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<NSString *> *)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<NSURLQueryItem *> *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<NSString *> *ipv4 = [NSMutableArray array];
|
|
NSMutableArray<NSString *> *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
|