feat: sync httpdns sdk/platform updates without large binaries

This commit is contained in:
robin
2026-03-04 17:59:14 +08:00
parent 853897a6f8
commit 532891fad0
700 changed files with 6096 additions and 2712 deletions

View File

@@ -0,0 +1,277 @@
#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