管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "AlicloudHTTPDNS"
s.version = "3.4.1"
s.version = "1.0.0"
s.summary = "Aliyun Mobile Service HTTPDNS iOS SDK (source distribution)."
s.description = <<-DESC
HTTPDNS iOS SDK 源码分发版本,提供通过 HTTP(S) 进行域名解析、
@@ -27,6 +27,7 @@ Pod::Spec.new do |s|
s.public_header_files = [
"AlicloudHttpDNS/AlicloudHttpDNS.h",
"AlicloudHttpDNS/HttpdnsService.h",
"AlicloudHttpDNS/HttpdnsEdgeService.h",
"AlicloudHttpDNS/Model/HttpdnsResult.h",
"AlicloudHttpDNS/Model/HttpdnsRequest.h",
"AlicloudHttpDNS/Log/HttpdnsLog.h",

View File

@@ -24,6 +24,7 @@
#import <AlicloudHTTPDNS/HttpdnsService.h>
#import <AlicloudHTTPDNS/HttpdnsRequest.h>
#import <AlicloudHTTPDNS/HttpDnsResult.h>
#import <AlicloudHTTPDNS/HttpdnsEdgeService.h>
#import <AlicloudHTTPDNS/HttpdnsLoggerProtocol.h>
#import <AlicloudHTTPDNS/HttpdnsDegradationDelegate.h>
#import <AlicloudHTTPDNS/HttpdnsIpStackDetector.h>

View File

@@ -9,7 +9,7 @@
#ifndef PublicConstant_h
#define PublicConstant_h
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"3.4.1";
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"1.0.0";
#define ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY @"cn"
#define ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY @"hk"

View File

@@ -0,0 +1,36 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsEdgeResolveResult : NSObject
@property (nonatomic, copy) NSString *requestId;
@property (nonatomic, copy) NSArray<NSString *> *ipv4s;
@property (nonatomic, copy) NSArray<NSString *> *ipv6s;
@property (nonatomic, assign) NSInteger ttl;
@end
@interface HttpdnsEdgeService : NSObject
- (instancetype)initWithAppId:(NSString *)appId
primaryServiceHost:(NSString *)primaryServiceHost
backupServiceHost:(nullable NSString *)backupServiceHost
servicePort:(NSInteger)servicePort
signSecret:(nullable NSString *)signSecret;
- (void)resolveHost:(NSString *)host
queryType:(NSString *)queryType
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable result, NSError *_Nullable error))completion;
/// Connect by IP + HTTPS and keep Host header as business domain.
/// This path will not fallback to domain connect.
- (void)requestURL:(NSURL *)url
method:(NSString *)method
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body
completion:(void (^)(NSData *_Nullable data, NSHTTPURLResponse *_Nullable response, NSError *_Nullable error))completion;
@end
NS_ASSUME_NONNULL_END

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

View File

@@ -1,73 +1,58 @@
# Alicloud HTTPDNS iOS SDK
# HTTPDNS iOS SDK (SNI Hidden v1.0.0)
面向 iOS 的 HTTP/HTTPS DNS 解析 SDK提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 iOS 12.0。
## 1. Init
## 功能特性
- 鉴权请求与可选 AES 传输加密
- IPv4/IPv6 双栈解析,支持自动/同时解析
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
- 预解析、区域路由、网络切换自动刷新
- 可定制日志回调与会话追踪 `sessionId`
## 安装CocoaPods
`Podfile` 中添加:
```ruby
platform :ios, '12.0'
target 'YourApp' do
pod 'AlicloudHTTPDNS', '~> 3.4.1'
end
```
执行 `pod install` 安装依赖。
## 快速开始
ObjectiveC
```objc
#import <AlicloudHttpDNS/AlicloudHttpDNS.h>
#import <AlicloudHTTPDNS/AlicloudHTTPDNS.h>
// 使用鉴权初始化(单例);密钥请勿硬编码到仓库
HttpDnsService *service = [[HttpDnsService alloc] initWithAccountID:1000000
secretKey:@"<YOUR_SECRET>"];
[service setPersistentCacheIPEnabled:YES];
[service setPreResolveHosts:@[@"www.aliyun.com"] byIPType:HttpdnsQueryIPTypeAuto];
// 同步解析(会根据网络自动选择 v4/v6
HttpdnsResult *r = [service resolveHostSync:@"www.aliyun.com" byIpType:HttpdnsQueryIPTypeAuto];
NSLog(@"IPv4: %@", r.ips);
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
initWithAppId:@"app1f1ndpo9"
primaryServiceHost:@"httpdns-a.example.com"
backupServiceHost:@"httpdns-b.example.com"
servicePort:443
signSecret:@"your-sign-secret"]; // optional if sign is enabled
```
Swift
```swift
import AlicloudHttpDNS
## 2. Resolve
_ = HttpDnsService(accountID: 1000000, secretKey: "<YOUR_SECRET>")
let svc = HttpDnsService.sharedInstance()
let res = svc?.resolveHostSync("www.aliyun.com", byIpType: .auto)
print(res?.ips ?? [])
```objc
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
if (error != nil) {
return;
}
NSLog(@"requestId=%@ ipv4=%@ ipv6=%@ ttl=%ld", result.requestId, result.ipv4s, result.ipv6s, (long)result.ttl);
}];
```
提示
- 启动时通过 `setPreResolveHosts(_:byIPType:)` 预热热点域名。
- 如需在刷新期间容忍 TTL 过期,可开启 `setReuseExpiredIPEnabled:YES`
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障。
## 3. Official Request Adapter (IP + Host)
## 源码构建
执行 `./build_xc_framework.sh` 生成 XCFramework。脚本会从 `gitlab.alibaba-inc.com` 克隆内部构建工具;外部环境建议优先使用 CocoaPods 引入。
```objc
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
[service requestURL:url method:@"GET" headers:@{@"Accept": @"application/json"} body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
if (error != nil) {
return;
}
NSLog(@"status=%ld", (long)response.statusCode);
}];
```
## 测试
- 在 Xcode 使用 Scheme `AlicloudHttpDNSTests` 运行。
- 含 OCMock 的用例批量执行可能出现内存问题,请单个运行。
- 非 Mock 用例使用预置参数AccountID `1000000`,测试域名 `*.onlyforhttpdnstest.run.place`(每年需续期)。
Behavior is fixed:
- Resolve by `/resolve`.
- Connect to resolved IP over HTTPS.
- Keep `Host` header as business domain.
- No fallback to domain direct request.
## 依赖与链接
- iOS 12.0+;需链接 `CoreTelephony``SystemConfiguration`
- 额外库:`sqlite3.0``resolv``OTHER_LDFLAGS` 包含 `-ObjC -lz`
## 4. Public Errors
## 安全说明
- 切勿提交真实的 AccountID/SecretKey請通过本地安全配置或 CI 注入。
- 若担心设备时间偏差影响鉴权,可用 `setInternalAuthTimeBaseBySpecifyingCurrentTime:` 校正。
- `NO_IP_AVAILABLE`
- `TLS_EMPTY_SNI_FAILED`
- `HOST_ROUTE_REJECTED`
- `RESOLVE_SIGN_INVALID`
## Demo 与贡献
- 示例应用:`AlicloudHttpDNSTestDemo/`
- 贡献与提交流程请参见 `AGENTS.md`(提交信息与 PR 规范)。
## 5. Removed Public Params
Do not expose legacy public parameters:
- `accountId`
- `serviceDomain`
- `endpoint`
- `aesSecretKey`