Files
waf-platform/HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsEdgeService.m
2026-03-05 16:59:19 +08:00

298 lines
13 KiB
Objective-C

#import "HttpdnsEdgeService.h"
#import <CommonCrypto/CommonCrypto.h>
static NSString * const kHttpdnsEdgeErrorDomain = @"com.goeedge.httpdns.edge";
@interface HttpdnsEdgeTLSDelegate : NSObject <NSURLSessionDelegate>
@property (nonatomic, copy) NSString *host;
@end
@implementation HttpdnsEdgeTLSDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef trust = challenge.protectionSpace.serverTrust;
if (trust) {
SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)self.host);
SecTrustSetPolicies(trust, policy);
SecTrustResultType result;
SecTrustEvaluate(trust, &result);
if (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified) {
completionHandler(NSURLSessionAuthChallengeUseCredential, [[NSURLCredential alloc] initWithTrust:trust]);
return;
}
}
}
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
@end
@implementation HttpdnsEdgeResolveResult
@end
@interface HttpdnsEdgeService ()
@property (nonatomic, copy) NSString *appId;
@property (nonatomic, copy) NSArray<NSString *> *endpoints;
@property (nonatomic, copy) NSString *signSecret;
@property (nonatomic, copy) NSString *sessionId;
@end
@implementation HttpdnsEdgeService
- (instancetype)initWithAppId:(NSString *)appId
apiUrl:(NSString *)apiUrl
signSecret:(NSString *)signSecret {
if (self = [super init]) {
_appId = [appId copy];
_signSecret = signSecret.length > 0 ? [signSecret copy] : @"";
_sessionId = [[[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""] copy];
_endpoints = apiUrl.length > 0 ? @[[apiUrl 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.endpoints.count == 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";
[self requestResolveHosts:self.endpoints 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;
HttpdnsEdgeTLSDelegate *delegate = [[HttpdnsEdgeTLSDelegate alloc] init];
delegate.host = url.host;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil];
[[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 *baseUrl = hosts[index];
NSURLComponents *components = [NSURLComponents componentsWithString:baseUrl];
if (!components) {
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
return;
}
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