阿里sdk
This commit is contained in:
29
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/AlicloudHttpDNS.h
Normal file
29
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/AlicloudHttpDNS.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <AlicloudHTTPDNS/HttpdnsLog.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsPublicConstant.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsService.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsRequest.h>
|
||||
#import <AlicloudHTTPDNS/HttpDnsResult.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsLoggerProtocol.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsDegradationDelegate.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsIpStackDetector.h>
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// HttpdnsInternalConstant.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/03/10.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef HTTPDNS_INTERNAL_CONSTANT_H
|
||||
#define HTTPDNS_INTERNAL_CONSTANT_H
|
||||
|
||||
static const int HTTPDNS_MAX_REQUEST_RETRY_TIME = 1;
|
||||
|
||||
static const int HTTPDNS_MAX_MANAGE_HOST_NUM = 100;
|
||||
|
||||
static const int HTTPDNS_PRE_RESOLVE_BATCH_SIZE = 5;
|
||||
|
||||
static const int HTTPDNS_DEFAULT_REQUEST_TIMEOUT_INTERVAL = 3;
|
||||
|
||||
static const NSUInteger HTTPDNS_DEFAULT_AUTH_TIMEOUT_INTERVAL = 10 * 60;
|
||||
|
||||
static NSString *const ALICLOUD_HTTPDNS_VALID_SERVER_CERTIFICATE_IP = @"203.107.1.1";
|
||||
|
||||
// 在iOS14和iOS16,网络信息的获取权限受到越来越紧的限制
|
||||
// 除非用户主动声明需要相关entitlement,不然只能拿到空信息
|
||||
// 考虑大多数用户并不会申请这些权限,我们放弃针对细节的网络信息做缓存粒度隔离
|
||||
// 出于兼容性考虑,网络运营商只有default一种类型
|
||||
#define HTTPDNS_DEFAULT_NETWORK_CARRIER_NAME @"default"
|
||||
|
||||
// 调度地址示例:http://106.11.90.200/sc/httpdns_config?account_id=153519&platform=ios&sdk_version=1.6.1
|
||||
static NSString *const ALICLOUD_HTTPDNS_SCHEDULE_CENTER_REQUEST_HOST = @"httpdns-sc.aliyuncs.com";
|
||||
|
||||
static NSString *const ALICLOUD_HTTPDNS_ERROR_MESSAGE_KEY = @"ErrorMessage";
|
||||
|
||||
static NSString *const kAlicloudHttpdnsRegionConfigV4HostKey = @"service_ip";
|
||||
static NSString *const kAlicloudHttpdnsRegionConfigV6HostKey = @"service_ipv6";
|
||||
|
||||
static NSString *const kAlicloudHttpdnsRegionKey = @"HttpdnsRegion";
|
||||
|
||||
#define SECONDS_OF_ONE_YEAR 365 * 24 * 60 * 60
|
||||
|
||||
static NSString *const ALICLOUD_HTTPDNS_ERROR_DOMAIN = @"HttpdnsErrorDomain";
|
||||
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE = 10003;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTP_COMMON_ERROR_CODE = 10004;
|
||||
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTPS_TIMEOUT_ERROR_CODE = 10005;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTP_TIMEOUT_ERROR_CODE = 10006;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTP_OPEN_STREAM_ERROR_CODE = 10007;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_HTTPS_NO_DATA_ERROR_CODE = 10008;
|
||||
|
||||
static NSInteger const ALICLOUD_HTTP_UNSUPPORTED_STATUS_CODE = 10013;
|
||||
static NSInteger const ALICLOUD_HTTP_PARSE_JSON_FAILED = 10014;
|
||||
|
||||
// 加密错误码
|
||||
static NSInteger const ALICLOUD_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE = 10021;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_ENCRYPT_RANDOM_IV_ERROR_CODE = 10022;
|
||||
static NSInteger const ALICLOUD_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE = 10023;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// HttpdnsPublicConstant.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/6/16.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef PublicConstant_h
|
||||
#define PublicConstant_h
|
||||
|
||||
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"3.4.1";
|
||||
|
||||
#define ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY @"cn"
|
||||
#define ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY @"hk"
|
||||
#define ALICLOUD_HTTPDNS_SINGAPORE_REGION_KEY @"sg"
|
||||
#define ALICLOUD_HTTPDNS_GERMANY_REGION_KEY @"de"
|
||||
#define ALICLOUD_HTTPDNS_AMERICA_REGION_KEY @"us"
|
||||
|
||||
|
||||
#endif /* PublicConstant_h */
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// HttpdnsRegionConfigLoader.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/6/16.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsRegionConfigLoader : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
+ (NSArray<NSString *> *)getAvailableRegionList;
|
||||
|
||||
- (NSArray *)getSeriveV4HostList:(NSString *)region;
|
||||
|
||||
- (NSArray *)getUpdateV4FallbackHostList:(NSString *)region;
|
||||
|
||||
- (NSArray *)getSeriveV6HostList:(NSString *)region;
|
||||
|
||||
- (NSArray *)getUpdateV6FallbackHostList:(NSString *)region;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// HttpdnsRegionConfigLoader.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/6/16.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsRegionConfigLoader.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
|
||||
static NSString *const kServiceV4Key = @"ALICLOUD_HTTPDNS_SERVICE_HOST_V4_KEY";
|
||||
static NSString *const kUpdateV4FallbackHostKey = @"ALICLOUD_HTTPDNS_UPDATE_HOST_V4_KEY";
|
||||
static NSString *const kServiceV6Key = @"ALICLOUD_HTTPDNS_SERVICE_HOST_V6_KEY";
|
||||
static NSString *const kUpdateV6FallbackHostKey = @"ALICLOUD_HTTPDNS_UPDATE_HOST_V6_KEY";
|
||||
|
||||
static NSArray<NSString *> *ALICLOUD_HTTPDNS_AVAILABLE_REGION_LIST = nil;
|
||||
|
||||
@interface HttpdnsRegionConfigLoader ()
|
||||
|
||||
@property (nonatomic, strong) NSDictionary *regionConfig;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsRegionConfigLoader
|
||||
|
||||
+ (void)initialize {
|
||||
ALICLOUD_HTTPDNS_AVAILABLE_REGION_LIST = @[
|
||||
ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY,
|
||||
ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY,
|
||||
ALICLOUD_HTTPDNS_SINGAPORE_REGION_KEY,
|
||||
ALICLOUD_HTTPDNS_GERMANY_REGION_KEY,
|
||||
ALICLOUD_HTTPDNS_AMERICA_REGION_KEY
|
||||
];
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static HttpdnsRegionConfigLoader *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[HttpdnsRegionConfigLoader alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[self loadRegionConfig];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)getAvailableRegionList {
|
||||
return ALICLOUD_HTTPDNS_AVAILABLE_REGION_LIST;
|
||||
}
|
||||
|
||||
- (void)loadRegionConfig {
|
||||
self.regionConfig = @{
|
||||
ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY: @{
|
||||
kServiceV4Key: @[@"203.107.1.1", @"203.107.1.97", @"203.107.1.100", @"203.119.238.240", @"106.11.25.239", @"59.82.99.47"],
|
||||
kUpdateV4FallbackHostKey: @[@"resolvers-cn.httpdns.aliyuncs.com"],
|
||||
kServiceV6Key: @[@"2401:b180:7001::31d", @"2401:b180:2000:30::1c", @"2401:b180:2000:20::10", @"2401:b180:2000:30::1c"],
|
||||
kUpdateV6FallbackHostKey: @[@"resolvers-cn.httpdns.aliyuncs.com"]
|
||||
},
|
||||
ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY: @{
|
||||
kServiceV4Key: @[@"47.56.234.194", @"47.56.119.115"],
|
||||
kUpdateV4FallbackHostKey: @[@"resolvers-hk.httpdns.aliyuncs.com"],
|
||||
kServiceV6Key: @[@"240b:4000:f10::178", @"240b:4000:f10::188"],
|
||||
kUpdateV6FallbackHostKey: @[@"resolvers-hk.httpdns.aliyuncs.com"]
|
||||
},
|
||||
ALICLOUD_HTTPDNS_SINGAPORE_REGION_KEY: @{
|
||||
kServiceV4Key: @[@"161.117.200.122", @"47.74.222.190"],
|
||||
kUpdateV4FallbackHostKey: @[@"resolvers-sg.httpdns.aliyuncs.com"],
|
||||
kServiceV6Key: @[@"240b:4000:f10::178", @"240b:4000:f10::188"],
|
||||
kUpdateV6FallbackHostKey: @[@"resolvers-sg.httpdns.aliyuncs.com"]
|
||||
},
|
||||
ALICLOUD_HTTPDNS_GERMANY_REGION_KEY: @{
|
||||
kServiceV4Key: @[@"47.89.80.182", @"47.246.146.77"],
|
||||
kUpdateV4FallbackHostKey: @[@"resolvers-de.httpdns.aliyuncs.com"],
|
||||
kServiceV6Key: @[@"2404:2280:3000::176", @"2404:2280:3000::188"],
|
||||
kUpdateV6FallbackHostKey: @[@"resolvers-de.httpdns.aliyuncs.com"]
|
||||
},
|
||||
ALICLOUD_HTTPDNS_AMERICA_REGION_KEY: @{
|
||||
kServiceV4Key: @[@"47.246.131.175", @"47.246.131.141"],
|
||||
kUpdateV4FallbackHostKey: @[@"resolvers-us.httpdns.aliyuncs.com"],
|
||||
kServiceV6Key: @[@"2404:2280:4000::2bb", @"2404:2280:4000::23e"],
|
||||
kUpdateV6FallbackHostKey: @[@"resolvers-us.httpdns.aliyuncs.com"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
- (NSArray *)getSeriveV4HostList:(NSString *)region {
|
||||
NSDictionary *regionConfing = [self.regionConfig objectForKey:region];
|
||||
return [regionConfing objectForKey:kServiceV4Key];
|
||||
}
|
||||
|
||||
- (NSArray *)getUpdateV4FallbackHostList:(NSString *)region {
|
||||
NSDictionary *regionConfing = [self.regionConfig objectForKey:region];
|
||||
return [regionConfing objectForKey:kUpdateV4FallbackHostKey];
|
||||
}
|
||||
|
||||
- (NSArray *)getSeriveV6HostList:(NSString *)region {
|
||||
NSDictionary *regionConfing = [self.regionConfig objectForKey:region];
|
||||
return [regionConfing objectForKey:kServiceV6Key];
|
||||
}
|
||||
|
||||
- (NSArray *)getUpdateV6FallbackHostList:(NSString *)region {
|
||||
NSDictionary *regionConfing = [self.regionConfig objectForKey:region];
|
||||
return [regionConfing objectForKey:kUpdateV6FallbackHostKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#ifndef HttpdnsDegradationDelegate_h
|
||||
#define HttpdnsDegradationDelegate_h
|
||||
|
||||
__attribute__((deprecated("不再建议通过设置此回调实现降级逻辑,而是自行在调用HTTPDNS解析域名前做判断")))
|
||||
@protocol HttpDNSDegradationDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldDegradeHTTPDNS:(NSString *)hostName;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HttpdnsDegradationDelegate_h */
|
||||
21
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsLocalResolver.h
Normal file
21
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsLocalResolver.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// HttpdnsLocalResolver.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/16.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsRequest.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsLocalResolver : NSObject
|
||||
|
||||
- (HttpdnsHostObject *)resolve:(HttpdnsRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
150
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsLocalResolver.m
Normal file
150
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsLocalResolver.m
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// HttpdnsLocalResolver.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/16.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsLocalResolver.h"
|
||||
#import <netdb.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#import <sys/socket.h>
|
||||
|
||||
#import "HttpdnsService.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
|
||||
@implementation HttpdnsLocalResolver
|
||||
|
||||
- (HttpdnsHostObject *)resolve:(HttpdnsRequest *)request {
|
||||
// 1. 验证输入参数
|
||||
NSString *host = request.host;
|
||||
if (host.length == 0) {
|
||||
return nil; // 没有主机名可解析
|
||||
}
|
||||
|
||||
HttpDnsService *service = [HttpDnsService getInstanceByAccountId:request.accountId];
|
||||
if (!service) {
|
||||
service = [HttpDnsService sharedInstance];
|
||||
}
|
||||
|
||||
// 2. 准备DNS解析配置
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC; // 同时支持IPv4和IPv6
|
||||
hints.ai_socktype = SOCK_STREAM; // TCP (对DNS解析来说通常不重要)
|
||||
|
||||
// 3. 执行getaddrinfo解析
|
||||
struct addrinfo *res = NULL;
|
||||
int ret = getaddrinfo([host UTF8String], NULL, &hints, &res);
|
||||
if (ret != 0 || res == NULL) {
|
||||
// DNS解析失败
|
||||
if (res) {
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 4. 收集所有IPv4和IPv6地址
|
||||
NSMutableArray<NSString *> *ipv4Array = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *ipv6Array = [NSMutableArray array];
|
||||
|
||||
for (struct addrinfo *p = res; p != NULL; p = p->ai_next) {
|
||||
if (p->ai_family == AF_INET || p->ai_family == AF_INET6) {
|
||||
char hostBuffer[NI_MAXHOST];
|
||||
memset(hostBuffer, 0, sizeof(hostBuffer));
|
||||
|
||||
if (getnameinfo(p->ai_addr, (socklen_t)p->ai_addrlen,
|
||||
hostBuffer, sizeof(hostBuffer),
|
||||
NULL, 0, NI_NUMERICHOST) == 0) {
|
||||
NSString *ipString = [NSString stringWithUTF8String:hostBuffer];
|
||||
if (p->ai_family == AF_INET) {
|
||||
[ipv4Array addObject:ipString];
|
||||
} else {
|
||||
[ipv6Array addObject:ipString];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
|
||||
// 5. 根据queryIpType确定保留哪些IP类型
|
||||
BOOL wantIPv4 = NO;
|
||||
BOOL wantIPv6 = NO;
|
||||
|
||||
switch (request.queryIpType) {
|
||||
case HttpdnsQueryIPTypeAuto:
|
||||
// Auto模式:如果有IPv4则始终返回,如果有IPv6则也包含
|
||||
// 无条件设置wantIPv4为YES
|
||||
wantIPv4 = YES;
|
||||
// 如果DNS返回了IPv6地址,则也包含IPv6
|
||||
wantIPv6 = (ipv6Array.count > 0);
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeIpv4:
|
||||
wantIPv4 = YES;
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeIpv6:
|
||||
wantIPv6 = YES;
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeBoth:
|
||||
wantIPv4 = YES;
|
||||
wantIPv6 = YES;
|
||||
break;
|
||||
}
|
||||
|
||||
// 6. 构建最终的HttpdnsIpObject数组
|
||||
NSMutableArray<HttpdnsIpObject *> *v4IpObjects = [NSMutableArray array];
|
||||
NSMutableArray<HttpdnsIpObject *> *v6IpObjects = [NSMutableArray array];
|
||||
|
||||
if (wantIPv4) {
|
||||
for (NSString *ipStr in ipv4Array) {
|
||||
HttpdnsIpObject *ipObj = [[HttpdnsIpObject alloc] init];
|
||||
[ipObj setIp:ipStr]; // ipObj.ip = ipStr
|
||||
// connectedRT默认为0
|
||||
[v4IpObjects addObject:ipObj];
|
||||
}
|
||||
}
|
||||
if (wantIPv6) {
|
||||
for (NSString *ipStr in ipv6Array) {
|
||||
HttpdnsIpObject *ipObj = [[HttpdnsIpObject alloc] init];
|
||||
[ipObj setIp:ipStr];
|
||||
[v6IpObjects addObject:ipObj];
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 创建并填充HttpdnsHostObject
|
||||
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
|
||||
[hostObject setHostName:host]; // hostName = request.host
|
||||
[hostObject setV4Ips:v4IpObjects];
|
||||
[hostObject setV6Ips:v6IpObjects];
|
||||
|
||||
// IPv4和IPv6的默认TTL为60秒
|
||||
[hostObject setV4TTL:60];
|
||||
[hostObject setV6TTL:60];
|
||||
|
||||
// 自定义ttl
|
||||
[HttpdnsUtil processCustomTTL:hostObject forHost:host service:service];
|
||||
|
||||
// 当前时间(自1970年以来的秒数)
|
||||
int64_t now = (int64_t)[[NSDate date] timeIntervalSince1970];
|
||||
|
||||
// 更新最后查询时间
|
||||
[hostObject setLastIPv4LookupTime:now];
|
||||
[hostObject setLastIPv6LookupTime:now];
|
||||
|
||||
// 标记是否没有IPv4或IPv6记录
|
||||
[hostObject setHasNoIpv4Record:(v4IpObjects.count == 0)];
|
||||
[hostObject setHasNoIpv6Record:(v6IpObjects.count == 0)];
|
||||
|
||||
// 如果需要,可以在这里设置clientIp或额外字段
|
||||
// 现在保留为默认值/空
|
||||
|
||||
return hostObject;
|
||||
}
|
||||
|
||||
@end
|
||||
28
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRemoteResolver.h
Normal file
28
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRemoteResolver.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@interface HttpdnsRemoteResolver : NSObject
|
||||
|
||||
- (NSArray<HttpdnsHostObject *> *)resolve:(HttpdnsRequest *)request error:(NSError **)error;
|
||||
|
||||
@end
|
||||
710
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRemoteResolver.m
Normal file
710
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRemoteResolver.m
Normal file
@@ -0,0 +1,710 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
#import "HttpdnsRequest.h"
|
||||
#import "HttpdnsRemoteResolver.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsReachability.h"
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
#import <stdint.h>
|
||||
|
||||
|
||||
static dispatch_queue_t _streamOperateSyncQueue = 0;
|
||||
|
||||
@interface HttpdnsRemoteResolver () <NSStreamDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSRunLoop *runloop;
|
||||
@property (nonatomic, strong) NSError *networkError;
|
||||
@property (nonatomic, weak) HttpDnsService *service;
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPClient *httpClient;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HttpdnsRemoteResolver {
|
||||
NSMutableData *_resultData;
|
||||
NSInputStream *_inputStream;
|
||||
BOOL _responseResolved;
|
||||
BOOL _compeleted;
|
||||
NSTimer *_timeoutTimer;
|
||||
NSDictionary *_httpJSONDict;
|
||||
}
|
||||
|
||||
#pragma mark init
|
||||
|
||||
+ (void)initialize {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_streamOperateSyncQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.runloopOperateQueue.HttpdnsRequest", DISPATCH_QUEUE_SERIAL);
|
||||
});
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_resultData = [NSMutableData data];
|
||||
_httpJSONDict = nil;
|
||||
self.networkError = nil;
|
||||
_responseResolved = NO;
|
||||
_compeleted = NO;
|
||||
_httpClient = [HttpdnsNWHTTPClient sharedInstance];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark LookupIpAction
|
||||
|
||||
- (NSArray<HttpdnsHostObject *> *)parseHttpdnsResponse:(NSDictionary *)json withQueryIpType:(HttpdnsQueryIPType)queryIpType {
|
||||
if (!json) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 验证响应码
|
||||
if (![self validateResponseCode:json]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 获取数据内容
|
||||
id data = [self extractDataContent:json];
|
||||
if (!data) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 获取所有答案
|
||||
NSArray *answers = [self getAnswersFromData:data];
|
||||
if (!answers) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 创建主机对象数组
|
||||
NSMutableArray<HttpdnsHostObject *> *hostObjects = [NSMutableArray array];
|
||||
for (NSDictionary *answer in answers) {
|
||||
HttpdnsHostObject *hostObject = [self createHostObjectFromAnswer:answer];
|
||||
if (hostObject) {
|
||||
[hostObjects addObject:hostObject];
|
||||
}
|
||||
}
|
||||
|
||||
return hostObjects;
|
||||
}
|
||||
|
||||
// 验证响应码
|
||||
- (BOOL)validateResponseCode:(NSDictionary *)json {
|
||||
NSString *code = [json objectForKey:@"code"];
|
||||
if (![code isEqualToString:@"success"]) {
|
||||
HttpdnsLogDebug("Response code is not success: %@", code);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
// 获取并处理解密数据内容
|
||||
- (id)extractDataContent:(NSDictionary *)json {
|
||||
// 获取mode,判断是否需要解密
|
||||
NSInteger mode = [[json objectForKey:@"mode"] integerValue];
|
||||
id data = [json objectForKey:@"data"];
|
||||
|
||||
if (mode == 1) { // 只处理AES-CBC模式
|
||||
// 需要解密
|
||||
data = [self decryptData:data withMode:mode];
|
||||
} else if (mode != 0) {
|
||||
// 不支持的加密模式(如AES-GCM)
|
||||
HttpdnsLogDebug("Unsupported encryption mode: %ld", (long)mode);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![data isKindOfClass:[NSDictionary class]]) {
|
||||
HttpdnsLogDebug("Data is not a dictionary");
|
||||
return nil;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
- (id)decryptData:(id)data withMode:(NSInteger)mode {
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
NSString *aesSecretKey = service.aesSecretKey;
|
||||
|
||||
if (![HttpdnsUtil isNotEmptyString:aesSecretKey]) {
|
||||
HttpdnsLogDebug("Response is encrypted but no AES key is provided");
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![data isKindOfClass:[NSString class]]) {
|
||||
HttpdnsLogDebug("Encrypted data is not a string");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 将Base64字符串转为NSData
|
||||
NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:data options:0];
|
||||
if (!encryptedData || encryptedData.length <= 16) {
|
||||
HttpdnsLogDebug("Invalid encrypted data");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 从secretKey转换为二进制密钥
|
||||
NSData *keyData = [HttpdnsUtil dataFromHexString:aesSecretKey];
|
||||
if (!keyData) {
|
||||
HttpdnsLogDebug("Invalid AES key format");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 使用工具类解密
|
||||
NSError *decryptError = nil;
|
||||
NSData *decryptedData = [HttpdnsUtil decryptDataAESCBC:encryptedData withKey:keyData error:&decryptError];
|
||||
|
||||
if (decryptError || !decryptedData) {
|
||||
HttpdnsLogDebug("Failed to decrypt data: %@", decryptError);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 将解密后的JSON数据解析为字典
|
||||
NSError *jsonError;
|
||||
id decodedData = [NSJSONSerialization JSONObjectWithData:decryptedData options:0 error:&jsonError];
|
||||
|
||||
if (jsonError) {
|
||||
HttpdnsLogDebug("Failed to parse decrypted JSON: %@", jsonError);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
// 从数据中获取答案数组
|
||||
- (NSArray *)getAnswersFromData:(NSDictionary *)data {
|
||||
NSArray *answers = [data objectForKey:@"answers"];
|
||||
if (![answers isKindOfClass:[NSArray class]] || answers.count == 0) {
|
||||
HttpdnsLogDebug("No answers in response");
|
||||
return nil;
|
||||
}
|
||||
return answers;
|
||||
}
|
||||
|
||||
// 从答案创建主机对象
|
||||
- (HttpdnsHostObject *)createHostObjectFromAnswer:(NSDictionary *)answer {
|
||||
// 获取域名
|
||||
NSString *host = [answer objectForKey:@"dn"];
|
||||
if (![HttpdnsUtil isNotEmptyString:host]) {
|
||||
HttpdnsLogDebug("Missing domain name in answer");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 创建并填充HostObject
|
||||
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
|
||||
[hostObject setHostName:host];
|
||||
|
||||
// 处理IPv4信息
|
||||
[self processIPv4Info:answer forHostObject:hostObject];
|
||||
|
||||
// 处理IPv6信息
|
||||
[self processIPv6Info:answer forHostObject:hostObject];
|
||||
|
||||
// 自定义ttl
|
||||
[HttpdnsUtil processCustomTTL:hostObject forHost:host service:self.service];
|
||||
|
||||
// 设置客户端IP
|
||||
NSString *clientIp = [[answer objectForKey:@"data"] objectForKey:@"cip"];
|
||||
if ([HttpdnsUtil isNotEmptyString:clientIp]) {
|
||||
[hostObject setClientIp:clientIp];
|
||||
}
|
||||
|
||||
return hostObject;
|
||||
}
|
||||
|
||||
// 处理IPv4信息
|
||||
- (void)processIPv4Info:(NSDictionary *)answer forHostObject:(HttpdnsHostObject *)hostObject {
|
||||
NSDictionary *v4Data = [answer objectForKey:@"v4"];
|
||||
if (![v4Data isKindOfClass:[NSDictionary class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *ip4s = [v4Data objectForKey:@"ips"];
|
||||
if ([ip4s isKindOfClass:[NSArray class]] && ip4s.count > 0) {
|
||||
// 处理IPv4地址
|
||||
[self setIpArrayToHostObject:hostObject fromIpsArray:ip4s forIPv6:NO];
|
||||
|
||||
// 设置IPv4的TTL
|
||||
[self setTTLForHostObject:hostObject fromData:v4Data forIPv6:NO];
|
||||
|
||||
// 处理v4的extra字段,优先使用
|
||||
[self processExtraInfo:v4Data forHostObject:hostObject];
|
||||
|
||||
// 检查是否有no_ip_code字段,表示无IPv4记录
|
||||
if ([[v4Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
|
||||
hostObject.hasNoIpv4Record = YES;
|
||||
}
|
||||
} else {
|
||||
// 没有IPv4地址但有v4节点,可能是无记录
|
||||
hostObject.hasNoIpv4Record = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理IPv6信息
|
||||
- (void)processIPv6Info:(NSDictionary *)answer forHostObject:(HttpdnsHostObject *)hostObject {
|
||||
NSDictionary *v6Data = [answer objectForKey:@"v6"];
|
||||
if (![v6Data isKindOfClass:[NSDictionary class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *ip6s = [v6Data objectForKey:@"ips"];
|
||||
if ([ip6s isKindOfClass:[NSArray class]] && ip6s.count > 0) {
|
||||
// 处理IPv6地址
|
||||
[self setIpArrayToHostObject:hostObject fromIpsArray:ip6s forIPv6:YES];
|
||||
|
||||
// 设置IPv6的TTL
|
||||
[self setTTLForHostObject:hostObject fromData:v6Data forIPv6:YES];
|
||||
|
||||
// 只有在没有v4 extra的情况下才使用v6的extra
|
||||
if (![hostObject getExtra]) {
|
||||
[self processExtraInfo:v6Data forHostObject:hostObject];
|
||||
}
|
||||
|
||||
// 检查是否有no_ip_code字段,表示无IPv6记录
|
||||
if ([[v6Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
|
||||
hostObject.hasNoIpv6Record = YES;
|
||||
}
|
||||
} else {
|
||||
// 没有IPv6地址但有v6节点,可能是无记录
|
||||
hostObject.hasNoIpv6Record = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置IP数组到主机对象
|
||||
- (void)setIpArrayToHostObject:(HttpdnsHostObject *)hostObject fromIpsArray:(NSArray *)ips forIPv6:(BOOL)isIPv6 {
|
||||
NSMutableArray *ipArray = [NSMutableArray array];
|
||||
for (NSString *ip in ips) {
|
||||
if ([HttpdnsUtil isEmptyString:ip]) {
|
||||
continue;
|
||||
}
|
||||
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
|
||||
[ipObject setIp:ip];
|
||||
[ipArray addObject:ipObject];
|
||||
}
|
||||
|
||||
if (isIPv6) {
|
||||
[hostObject setV6Ips:ipArray];
|
||||
} else {
|
||||
[hostObject setV4Ips:ipArray];
|
||||
}
|
||||
}
|
||||
|
||||
// 设置TTL
|
||||
- (void)setTTLForHostObject:(HttpdnsHostObject *)hostObject fromData:(NSDictionary *)data forIPv6:(BOOL)isIPv6 {
|
||||
NSNumber *ttl = [data objectForKey:@"ttl"];
|
||||
if (ttl) {
|
||||
if (isIPv6) {
|
||||
hostObject.v6ttl = [ttl longLongValue];
|
||||
hostObject.lastIPv6LookupTime = [NSDate date].timeIntervalSince1970;
|
||||
} else {
|
||||
hostObject.v4ttl = [ttl longLongValue];
|
||||
hostObject.lastIPv4LookupTime = [NSDate date].timeIntervalSince1970;
|
||||
}
|
||||
} else {
|
||||
if (isIPv6) {
|
||||
hostObject.v6ttl = 0;
|
||||
} else {
|
||||
hostObject.v4ttl = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理额外信息
|
||||
- (void)processExtraInfo:(NSDictionary *)data forHostObject:(HttpdnsHostObject *)hostObject {
|
||||
id extra = [data objectForKey:@"extra"];
|
||||
if (extra) {
|
||||
NSString *convertedExtra = [self convertExtraToString:extra];
|
||||
if (convertedExtra) {
|
||||
[hostObject setExtra:convertedExtra];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)constructHttpdnsResolvingUrl:(HttpdnsRequest *)request forV4Net:(BOOL)isV4 {
|
||||
// 获取基础信息
|
||||
NSString *serverIp = [self getServerIpForNetwork:isV4];
|
||||
HttpDnsService *service = self.service;
|
||||
if (![HttpdnsUtil isNotEmptyString:serverIp]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 准备签名和加密参数
|
||||
NSDictionary *paramsToSign = [self prepareSigningParams:request forEncryption:[self shouldUseEncryption]];
|
||||
|
||||
// 计算签名
|
||||
NSString *signature = [self calculateSignatureForParams:paramsToSign withSecretKey:service.secretKey];
|
||||
|
||||
// 构建URL
|
||||
NSString *url = [NSString stringWithFormat:@"%@/v2/d", serverIp];
|
||||
|
||||
// 添加所有参数并构建最终URL
|
||||
NSString *finalUrl = [self buildFinalUrlWithBase:url
|
||||
params:paramsToSign
|
||||
isEncrypted:[self shouldUseEncryption]
|
||||
signature:signature
|
||||
request:request];
|
||||
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
// 获取当前应使用的服务器IP
|
||||
- (NSString *)getServerIpForNetwork:(BOOL)isV4 {
|
||||
HttpdnsScheduleCenter *scheduleCenter = self.service.scheduleCenter;
|
||||
if (!scheduleCenter) {
|
||||
return nil;
|
||||
}
|
||||
return isV4 ? [scheduleCenter currentActiveServiceServerV4Host] : [scheduleCenter currentActiveServiceServerV6Host];
|
||||
}
|
||||
|
||||
// 检查是否应该使用加密
|
||||
- (BOOL)shouldUseEncryption {
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
return [HttpdnsUtil isNotEmptyString:service.aesSecretKey];
|
||||
}
|
||||
|
||||
// 准备需要进行签名的参数
|
||||
- (NSDictionary *)prepareSigningParams:(HttpdnsRequest *)request forEncryption:(BOOL)useEncryption {
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
NSInteger accountId = service.accountID;
|
||||
|
||||
// 构建参与签名的参数字典
|
||||
NSMutableDictionary *paramsToSign = [NSMutableDictionary dictionary];
|
||||
|
||||
// 构建需要加密的参数字典
|
||||
NSMutableDictionary *paramsToEncrypt = [NSMutableDictionary dictionary];
|
||||
|
||||
// 账号ID,参与签名但不加密
|
||||
[paramsToSign setObject:[NSString stringWithFormat:@"%ld", accountId] forKey:@"id"];
|
||||
|
||||
// 决定加密模式
|
||||
NSString *mode = useEncryption ? @"1" : @"0"; // 0: 明文模式, 1: AES-CBC加密模式
|
||||
[paramsToSign setObject:mode forKey:@"m"];
|
||||
|
||||
// 版本号,参与签名但不加密
|
||||
[paramsToSign setObject:@"1.0" forKey:@"v"];
|
||||
|
||||
// 域名参数,参与签名并加密
|
||||
[paramsToEncrypt setObject:request.host forKey:@"dn"];
|
||||
|
||||
// 查询类型,参与签名并加密
|
||||
[paramsToEncrypt setObject:[self getQueryTypeString:request.queryIpType] forKey:@"q"];
|
||||
|
||||
// SDNS参数,参与签名并加密
|
||||
[self addSdnsParams:request.sdnsParams toParams:paramsToEncrypt];
|
||||
|
||||
// 签名过期时间,参与签名但不加密
|
||||
long expiredTimestamp = [self calculateExpiredTimestamp];
|
||||
NSString *expiredTimestampString = [NSString stringWithFormat:@"%ld", expiredTimestamp];
|
||||
[paramsToSign setObject:expiredTimestampString forKey:@"exp"];
|
||||
|
||||
// 处理加密
|
||||
if (useEncryption) {
|
||||
NSString *encryptedHexString = [self encryptParams:paramsToEncrypt];
|
||||
if (encryptedHexString) {
|
||||
[paramsToSign setObject:encryptedHexString forKey:@"enc"];
|
||||
}
|
||||
} else {
|
||||
// 明文模式下,加密参数也放入签名参数中
|
||||
[paramsToSign addEntriesFromDictionary:paramsToEncrypt];
|
||||
}
|
||||
|
||||
return paramsToSign;
|
||||
}
|
||||
|
||||
// 获取查询类型字符串
|
||||
- (NSString *)getQueryTypeString:(HttpdnsQueryIPType)queryIpType {
|
||||
if ((queryIpType & HttpdnsQueryIPTypeIpv4) && (queryIpType & HttpdnsQueryIPTypeIpv6)) {
|
||||
return @"4,6";
|
||||
} else if (queryIpType & HttpdnsQueryIPTypeIpv6) {
|
||||
return @"6";
|
||||
}
|
||||
return @"4";
|
||||
}
|
||||
|
||||
// 添加SDNS参数
|
||||
- (void)addSdnsParams:(NSDictionary *)sdnsParams toParams:(NSMutableDictionary *)paramsToEncrypt {
|
||||
if ([HttpdnsUtil isNotEmptyDictionary:sdnsParams]) {
|
||||
[sdnsParams enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
NSString *sdnsKey = [NSString stringWithFormat:@"sdns-%@", key];
|
||||
[paramsToEncrypt setObject:obj forKey:sdnsKey];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// 计算过期时间戳
|
||||
- (long)calculateExpiredTimestamp {
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
long localTimestampOffset = (long)service.authTimeOffset;
|
||||
long localTimestamp = (long)[[NSDate date] timeIntervalSince1970];
|
||||
if (localTimestampOffset != 0) {
|
||||
localTimestamp = localTimestamp + localTimestampOffset;
|
||||
}
|
||||
return localTimestamp + HTTPDNS_DEFAULT_AUTH_TIMEOUT_INTERVAL;
|
||||
}
|
||||
|
||||
// 加密参数
|
||||
- (NSString *)encryptParams:(NSDictionary *)paramsToEncrypt {
|
||||
NSError *error = nil;
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
|
||||
// 将待加密参数转为JSON
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:paramsToEncrypt options:0 error:&error];
|
||||
if (error) {
|
||||
HttpdnsLogDebug("Failed to serialize params to JSON: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 从secretKey转换为二进制密钥
|
||||
NSData *keyData = [HttpdnsUtil dataFromHexString:service.aesSecretKey];
|
||||
if (!keyData) {
|
||||
HttpdnsLogDebug("Invalid AES key format");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 目前在OC中没有比较好的实现AES-GCM的方式,因此这里选择AES-CBC加密
|
||||
NSData *encryptedData = [HttpdnsUtil encryptDataAESCBC:jsonData withKey:keyData error:&error];
|
||||
if (error) {
|
||||
HttpdnsLogDebug("Failed to encrypt data: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 将加密结果转为十六进制字符串
|
||||
return [HttpdnsUtil hexStringFromData:encryptedData];
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
- (NSString *)calculateSignatureForParams:(NSDictionary *)params withSecretKey:(NSString *)secretKey {
|
||||
if (![HttpdnsUtil isNotEmptyString:secretKey]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 按照签名要求对参数进行排序并生成签名内容
|
||||
NSArray *sortedKeys = [[params allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSMutableArray *signParts = [NSMutableArray array];
|
||||
|
||||
for (NSString *key in sortedKeys) {
|
||||
[signParts addObject:[NSString stringWithFormat:@"%@=%@", key, [params objectForKey:key]]];
|
||||
}
|
||||
|
||||
// 组合签名字符串
|
||||
NSString *signContent = [signParts componentsJoinedByString:@"&"];
|
||||
|
||||
// 计算HMAC-SHA256签名
|
||||
return [HttpdnsUtil hmacSha256:signContent key:secretKey];
|
||||
}
|
||||
|
||||
// 构建最终URL
|
||||
- (NSString *)buildFinalUrlWithBase:(NSString *)baseUrl
|
||||
params:(NSDictionary *)params
|
||||
isEncrypted:(BOOL)useEncryption
|
||||
signature:(NSString *)signature
|
||||
request:(HttpdnsRequest *)request {
|
||||
|
||||
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
|
||||
NSMutableString *finalUrl = [NSMutableString stringWithString:baseUrl];
|
||||
[finalUrl appendString:@"?"];
|
||||
|
||||
// 首先添加必要参数
|
||||
[finalUrl appendFormat:@"id=%ld", service.accountID];
|
||||
[finalUrl appendFormat:@"&m=%@", [params objectForKey:@"m"]];
|
||||
[finalUrl appendFormat:@"&exp=%@", [params objectForKey:@"exp"]];
|
||||
[finalUrl appendFormat:@"&v=%@", [params objectForKey:@"v"]];
|
||||
|
||||
if (useEncryption) {
|
||||
// 加密模式下,添加enc参数
|
||||
[finalUrl appendFormat:@"&enc=%@", [params objectForKey:@"enc"]];
|
||||
} else {
|
||||
// 明文模式下,添加所有参数
|
||||
NSMutableDictionary *paramsForPlainText = [NSMutableDictionary dictionaryWithDictionary:params];
|
||||
[paramsForPlainText removeObjectForKey:@"id"];
|
||||
[paramsForPlainText removeObjectForKey:@"m"];
|
||||
[paramsForPlainText removeObjectForKey:@"exp"];
|
||||
[paramsForPlainText removeObjectForKey:@"v"];
|
||||
|
||||
for (NSString *key in paramsForPlainText) {
|
||||
// 跳过已添加的参数
|
||||
if ([key isEqualToString:@"id"] || [key isEqualToString:@"m"] ||
|
||||
[key isEqualToString:@"exp"] || [key isEqualToString:@"v"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *value = [paramsForPlainText objectForKey:key];
|
||||
[finalUrl appendFormat:@"&%@=%@", [HttpdnsUtil URLEncodedString:key], [HttpdnsUtil URLEncodedString:value]];
|
||||
}
|
||||
}
|
||||
|
||||
// 添加签名(如果有)
|
||||
if ([HttpdnsUtil isNotEmptyString:signature]) {
|
||||
[finalUrl appendFormat:@"&s=%@", signature];
|
||||
}
|
||||
|
||||
// 添加不参与签名的其他参数
|
||||
[self appendAdditionalParams:finalUrl];
|
||||
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
// 添加额外的不参与签名的参数
|
||||
- (void)appendAdditionalParams:(NSMutableString *)url {
|
||||
// sessionId
|
||||
NSString *sessionId = [HttpdnsUtil generateSessionID];
|
||||
if ([HttpdnsUtil isNotEmptyString:sessionId]) {
|
||||
[url appendFormat:@"&sid=%@", sessionId];
|
||||
}
|
||||
|
||||
// 网络类型
|
||||
NSString *netType = [[HttpdnsReachability sharedInstance] currentReachabilityString];
|
||||
if ([HttpdnsUtil isNotEmptyString:netType]) {
|
||||
[url appendFormat:@"&net=%@", netType];
|
||||
}
|
||||
|
||||
// SDK版本
|
||||
NSString *versionInfo = [NSString stringWithFormat:@"ios_%@", HTTPDNS_IOS_SDK_VERSION];
|
||||
[url appendFormat:@"&sdk=%@", versionInfo];
|
||||
}
|
||||
|
||||
- (NSArray<HttpdnsHostObject *> *)resolve:(HttpdnsRequest *)request error:(NSError **)error {
|
||||
HttpdnsLogDebug("lookupHostFromServer, request: %@", request);
|
||||
|
||||
HttpDnsService *service = [HttpDnsService getInstanceByAccountId:request.accountId];
|
||||
if (!service) {
|
||||
HttpdnsLogDebug("Missing service for accountId: %ld; ensure request.accountId is set and service initialized", (long)request.accountId);
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE userInfo:@{NSLocalizedDescriptionKey: @"HttpDnsService not found for accountId"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
self.service = service;
|
||||
|
||||
NSString *url = [self constructHttpdnsResolvingUrl:request forV4Net:YES];
|
||||
|
||||
HttpdnsQueryIPType queryIPType = request.queryIpType;
|
||||
|
||||
NSArray<HttpdnsHostObject *> *hostObjects = [self sendRequest:url queryIpType:queryIPType error:error];
|
||||
|
||||
if (!(*error)) {
|
||||
return hostObjects;
|
||||
}
|
||||
|
||||
@try {
|
||||
HttpdnsIPStackType stackType = [[HttpdnsIpStackDetector sharedInstance] currentIpStack];
|
||||
// 由于上面默认只用ipv4请求,这里判断如果是ipv6-only环境,那就用v6的ip再试一次
|
||||
if (stackType == kHttpdnsIpv6Only) {
|
||||
url = [self constructHttpdnsResolvingUrl:request forV4Net:NO];
|
||||
HttpdnsLogDebug("lookupHostFromServer by ipv4 server failed, construct ipv6 backup url: %@", url);
|
||||
hostObjects = [self sendRequest:url queryIpType:queryIPType error:error];
|
||||
|
||||
if (!(*error)) {
|
||||
return hostObjects;
|
||||
}
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
HttpdnsLogDebug("lookupHostFromServer failed again by ipv6 server, exception: %@", exception.reason);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<HttpdnsHostObject *> *)sendRequest:(NSString *)urlStr queryIpType:(HttpdnsQueryIPType)queryIpType error:(NSError **)error {
|
||||
if (![HttpdnsUtil isNotEmptyString:urlStr]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Empty resolve URL due to missing scheduler"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
HttpDnsService *httpdnsService = self.service;
|
||||
NSString *scheme = httpdnsService.enableHttpsRequest ? @"https" : @"http";
|
||||
NSString *fullUrlStr = [NSString stringWithFormat:@"%@://%@", scheme, urlStr];
|
||||
|
||||
NSTimeInterval timeout = httpdnsService.timeoutInterval > 0 ? httpdnsService.timeoutInterval : 10.0;
|
||||
NSString *userAgent = [HttpdnsUtil generateUserAgent];
|
||||
HttpdnsNWHTTPClientResponse *httpResponse = [self.httpClient performRequestWithURLString:fullUrlStr
|
||||
userAgent:userAgent
|
||||
timeout:timeout
|
||||
error:error];
|
||||
if (!httpResponse) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (httpResponse.statusCode != 200) {
|
||||
if (error) {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Unsupported http status code: %ld", (long)httpResponse.statusCode];
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_UNSUPPORTED_STATUS_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *jsonError = nil;
|
||||
id jsonValue = [NSJSONSerialization JSONObjectWithData:httpResponse.body options:kNilOptions error:&jsonError];
|
||||
if (jsonError) {
|
||||
if (error) {
|
||||
*error = jsonError;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *json = [HttpdnsUtil getValidDictionaryFromJson:jsonValue];
|
||||
if (!json) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse JSON response"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self parseHttpdnsResponse:json withQueryIpType:queryIpType];
|
||||
}
|
||||
|
||||
#pragma mark - Helper Functions
|
||||
// 将extra字段转换为NSString类型
|
||||
- (NSString *)convertExtraToString:(id)extra {
|
||||
if (!extra) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([extra isKindOfClass:[NSString class]]) {
|
||||
// 已经是字符串,直接返回
|
||||
return extra;
|
||||
} else {
|
||||
// 非字符串,尝试转换为JSON字符串
|
||||
NSError *error = nil;
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:extra options:0 error:&error];
|
||||
if (!error && jsonData) {
|
||||
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
return jsonString;
|
||||
} else {
|
||||
HttpdnsLogDebug("Failed to convert extra to JSON string: %@", error);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
66
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRequestManager.h
Normal file
66
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRequestManager.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@class HttpDnsService;
|
||||
|
||||
|
||||
@class HttpdnsHostObject;
|
||||
|
||||
@interface HttpdnsRequestManager : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) NSInteger accountId;
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId ownerService:(HttpDnsService *)service;
|
||||
|
||||
- (void)setExpiredIPEnabled:(BOOL)enable;
|
||||
|
||||
- (void)setCachedIPEnabled:(BOOL)enable discardRecordsHasExpiredFor:(NSTimeInterval)duration;
|
||||
|
||||
- (void)setDegradeToLocalDNSEnabled:(BOOL)enable;
|
||||
|
||||
- (void)setPreResolveAfterNetworkChanged:(BOOL)enable;
|
||||
|
||||
- (void)preResolveHosts:(NSArray *)hosts queryType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
- (HttpdnsHostObject *)resolveHost:(HttpdnsRequest *)request;
|
||||
|
||||
// 内部缓存开关,不触发加载DB到内存的操作
|
||||
- (void)setPersistentCacheIpEnabled:(BOOL)enable;
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfHostArray:(NSArray<NSString *> *)hostArray;
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfAllHosts;
|
||||
|
||||
|
||||
#pragma mark - Expose to Testcases
|
||||
|
||||
- (HttpdnsHostObject *)mergeLookupResultToManager:(HttpdnsHostObject *)result host:host cacheKey:(NSString *)cacheKey underQueryIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
- (HttpdnsHostObject *)executeRequest:(HttpdnsRequest *)request retryCount:(int)hasRetryedCount;
|
||||
|
||||
- (NSString *)showMemoryCache;
|
||||
|
||||
- (void)cleanAllHostMemoryCache;
|
||||
|
||||
- (void)syncLoadCacheFromDbToMemory;
|
||||
|
||||
@end
|
||||
645
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRequestManager.m
Normal file
645
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsRequestManager.m
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsRemoteResolver.h"
|
||||
#import "HttpdnsLocalResolver.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsReachability.h"
|
||||
#import "HttpdnsHostRecord.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpDnsLocker.h"
|
||||
#import "HttpdnsRequest_Internal.h"
|
||||
#import "HttpdnsHostObjectInMemoryCache.h"
|
||||
#import "HttpdnsIPQualityDetector.h"
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
#import "HttpdnsDB.h"
|
||||
|
||||
|
||||
static dispatch_queue_t _persistentCacheConcurrentQueue = NULL;
|
||||
static dispatch_queue_t _asyncResolveHostQueue = NULL;
|
||||
|
||||
typedef struct {
|
||||
BOOL isResultUsable;
|
||||
BOOL isResolvingRequired;
|
||||
} HostObjectExamingResult;
|
||||
|
||||
@interface HttpdnsRequestManager()
|
||||
|
||||
@property (nonatomic, strong) dispatch_queue_t cacheQueue;
|
||||
@property (nonatomic, assign, readwrite) NSInteger accountId;
|
||||
@property (nonatomic, weak) HttpDnsService *ownerService;
|
||||
|
||||
@property (atomic, setter=setPersistentCacheIpEnabled:, assign) BOOL persistentCacheIpEnabled;
|
||||
@property (atomic, setter=setDegradeToLocalDNSEnabled:, assign) BOOL degradeToLocalDNSEnabled;
|
||||
@property (atomic, assign) BOOL atomicExpiredIPEnabled;
|
||||
@property (atomic, assign) BOOL atomicPreResolveAfterNetworkChanged;
|
||||
|
||||
@property (atomic, assign) NSTimeInterval lastUpdateTimestamp;
|
||||
@property (atomic, assign) HttpdnsNetworkStatus lastNetworkStatus;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsRequestManager {
|
||||
HttpdnsHostObjectInMemoryCache *_hostObjectInMemoryCache;
|
||||
HttpdnsDB *_httpdnsDB;
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_persistentCacheConcurrentQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.persistentCacheOperationQueue", DISPATCH_QUEUE_CONCURRENT);
|
||||
_asyncResolveHostQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.asyncResolveHostQueue", DISPATCH_QUEUE_CONCURRENT);
|
||||
});
|
||||
}
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId ownerService:(HttpDnsService *)service {
|
||||
if (self = [super init]) {
|
||||
_accountId = accountId;
|
||||
_ownerService = service;
|
||||
|
||||
HttpdnsReachability *reachability = [HttpdnsReachability sharedInstance];
|
||||
self.atomicExpiredIPEnabled = NO;
|
||||
self.atomicPreResolveAfterNetworkChanged = NO;
|
||||
_hostObjectInMemoryCache = [[HttpdnsHostObjectInMemoryCache alloc] init];
|
||||
_httpdnsDB = [[HttpdnsDB alloc] initWithAccountId:accountId];
|
||||
[[HttpdnsIpStackDetector sharedInstance] redetectIpStack];
|
||||
|
||||
_lastNetworkStatus = reachability.currentReachabilityStatus;
|
||||
_lastUpdateTimestamp = [NSDate date].timeIntervalSince1970;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleReachabilityNotification:)
|
||||
name:kHttpdnsReachabilityChangedNotification
|
||||
object:reachability];
|
||||
[reachability startNotifier];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setExpiredIPEnabled:(BOOL)enable {
|
||||
self.atomicExpiredIPEnabled = enable;
|
||||
}
|
||||
|
||||
- (void)setCachedIPEnabled:(BOOL)enable discardRecordsHasExpiredFor:(NSTimeInterval)duration {
|
||||
// 开启允许持久化缓存
|
||||
[self setPersistentCacheIpEnabled:enable];
|
||||
|
||||
if (enable) {
|
||||
dispatch_async(_persistentCacheConcurrentQueue, ^{
|
||||
// 先清理过期时间超过阈值的缓存结果
|
||||
[self->_httpdnsDB cleanRecordAlreadExpiredAt:[[NSDate date] timeIntervalSince1970] - duration];
|
||||
|
||||
// 再读取持久化缓存中的历史记录,加载到内存缓存里
|
||||
[self loadCacheFromDbToMemory];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPreResolveAfterNetworkChanged:(BOOL)enable {
|
||||
self.atomicPreResolveAfterNetworkChanged = enable;
|
||||
}
|
||||
|
||||
- (void)preResolveHosts:(NSArray *)hosts queryType:(HttpdnsQueryIPType)queryType {
|
||||
if (![HttpdnsUtil isNotEmptyArray:hosts]) {
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_async(_asyncResolveHostQueue, ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([strongSelf isHostsNumberLimitReached]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 分批处理,每批最多5个域名
|
||||
NSUInteger totalCount = hosts.count;
|
||||
for (NSUInteger i = 0; i < totalCount; i += HTTPDNS_PRE_RESOLVE_BATCH_SIZE) {
|
||||
NSUInteger length = MIN(HTTPDNS_PRE_RESOLVE_BATCH_SIZE, totalCount - i);
|
||||
NSArray *batch = [hosts subarrayWithRange:NSMakeRange(i, length)];
|
||||
|
||||
NSString *combinedHostString = [batch componentsJoinedByString:@","];
|
||||
HttpdnsLogDebug("Pre resolve host by async lookup, hosts: %@", combinedHostString);
|
||||
|
||||
HttpdnsRequest *request = [[HttpdnsRequest alloc] initWithHost:combinedHostString queryIpType:queryType];
|
||||
request.accountId = strongSelf.accountId;
|
||||
[request becomeNonBlockingRequest];
|
||||
|
||||
dispatch_async(_asyncResolveHostQueue, ^{
|
||||
[strongSelf executePreResolveRequest:request retryCount:0];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - core method for all public query API
|
||||
- (HttpdnsHostObject *)resolveHost:(HttpdnsRequest *)request {
|
||||
HttpdnsLogDebug("resolveHost, request: %@", request);
|
||||
|
||||
NSString *host = request.host;
|
||||
NSString *cacheKey = request.cacheKey;
|
||||
|
||||
if (request.accountId == 0 || request.accountId != self.accountId) {
|
||||
request.accountId = self.accountId;
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isEmptyString:host]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
HttpdnsHostObject *result = [_hostObjectInMemoryCache getHostObjectByCacheKey:cacheKey createIfNotExists:^id _Nonnull {
|
||||
HttpdnsLogDebug("No cache for cacheKey: %@", cacheKey);
|
||||
HttpdnsHostObject *newObject = [HttpdnsHostObject new];
|
||||
newObject.hostName = host;
|
||||
newObject.v4Ips = @[];
|
||||
newObject.v6Ips = @[];
|
||||
return newObject;
|
||||
}];
|
||||
|
||||
HostObjectExamingResult examingResult = [self examineHttpdnsHostObject:result underQueryType:request.queryIpType];
|
||||
BOOL isCachedResultUsable = examingResult.isResultUsable;
|
||||
BOOL isResolvingRequired = examingResult.isResolvingRequired;
|
||||
|
||||
if (isCachedResultUsable) {
|
||||
if (isResolvingRequired) {
|
||||
// 缓存结果可用,但是需要请求,因为缓存结果已经过期
|
||||
// 这种情况异步去解析就可以了
|
||||
[self determineResolvingHostNonBlocking:request];
|
||||
}
|
||||
// 缓存是以cacheKey为准,这里返回前,要把host替换成用户请求的这个
|
||||
result.hostName = host;
|
||||
HttpdnsLogDebug("Reuse available cache for cacheKey: %@, result: %@", cacheKey, result);
|
||||
// 因为缓存结果可用,可以立即返回
|
||||
return result;
|
||||
}
|
||||
|
||||
if (request.isBlockingRequest) {
|
||||
// 缓存结果不可用,且是同步请求,需要等待结果
|
||||
return [self determineResolveHostBlocking:request];
|
||||
} else {
|
||||
// 缓存结果不可用,且是异步请求,不需要等待结果
|
||||
[self determineResolvingHostNonBlocking:request];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)determineResolvingHostNonBlocking:(HttpdnsRequest *)request {
|
||||
dispatch_async(_asyncResolveHostQueue, ^{
|
||||
HttpDnsLocker *locker = [HttpDnsLocker sharedInstance];
|
||||
if ([locker tryLock:request.cacheKey queryType:request.queryIpType]) {
|
||||
@try {
|
||||
[self executeRequest:request retryCount:0];
|
||||
} @catch (NSException *exception) {
|
||||
HttpdnsLogDebug("determineResolvingHostNonBlocking host: %@, exception: %@", request.host, exception);
|
||||
} @finally {
|
||||
[locker unlock:request.cacheKey queryType:request.queryIpType];
|
||||
}
|
||||
} else {
|
||||
HttpdnsLogDebug("determineResolvingHostNonBlocking skipped due to concurrent limitation, host: %@", request.host);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)determineResolveHostBlocking:(HttpdnsRequest *)request {
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block HttpdnsHostObject *result = nil;
|
||||
dispatch_async(_asyncResolveHostQueue, ^{
|
||||
HttpDnsLocker *locker = [HttpDnsLocker sharedInstance];
|
||||
@try {
|
||||
[locker lock:request.cacheKey queryType:request.queryIpType];
|
||||
|
||||
result = [self->_hostObjectInMemoryCache getHostObjectByCacheKey:request.cacheKey];
|
||||
if (result && ![result isExpiredUnderQueryIpType:request.queryIpType]) {
|
||||
// 存在且未过期,意味着其他线程已经解析到了新的结果
|
||||
return;
|
||||
}
|
||||
|
||||
result = [self executeRequest:request retryCount:0];
|
||||
} @catch (NSException *exception) {
|
||||
HttpdnsLogDebug("determineResolveHostBlocking host: %@, exception: %@", request.host, exception);
|
||||
} @finally {
|
||||
[locker unlock:request.cacheKey queryType:request.queryIpType];
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}
|
||||
});
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(request.resolveTimeoutInSecond * NSEC_PER_SEC)));
|
||||
return result;
|
||||
}
|
||||
|
||||
- (HostObjectExamingResult)examineHttpdnsHostObject:(HttpdnsHostObject *)hostObject underQueryType:(HttpdnsQueryIPType)queryType {
|
||||
if (!hostObject) {
|
||||
return (HostObjectExamingResult){NO, YES};
|
||||
}
|
||||
|
||||
if ([hostObject isIpEmptyUnderQueryIpType:queryType]) {
|
||||
return (HostObjectExamingResult){NO, YES};
|
||||
}
|
||||
|
||||
if ([hostObject isExpiredUnderQueryIpType:queryType]) {
|
||||
if (self.atomicExpiredIPEnabled || [hostObject isLoadFromDB]) {
|
||||
// 只有开启了允许过期缓存,和开启持久化缓存情况下启动后加载到内存中的缓存,才可以直接复用过期结果
|
||||
HttpdnsLogDebug("The ips is expired, but we accept it, host: %@, queryType: %ld, expiredIpEnabled: %d, isLoadFromDB: %d",
|
||||
hostObject.hostName, queryType, self.atomicExpiredIPEnabled, [hostObject isLoadFromDB]);
|
||||
// 复用过期结果,同时也需要发起新的解析请求
|
||||
return (HostObjectExamingResult){YES, YES};
|
||||
}
|
||||
|
||||
// 只要过期了就肯定需要请求
|
||||
return (HostObjectExamingResult){NO, YES};
|
||||
}
|
||||
|
||||
return (HostObjectExamingResult){YES, NO};
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)executeRequest:(HttpdnsRequest *)request retryCount:(int)hasRetryedCount {
|
||||
NSString *host = request.host;
|
||||
NSString *cacheKey = request.cacheKey;
|
||||
HttpdnsQueryIPType queryIPType = request.queryIpType;
|
||||
HttpdnsHostObject *result = nil;
|
||||
|
||||
BOOL isDegradationResult = NO;
|
||||
|
||||
if (hasRetryedCount <= HTTPDNS_MAX_REQUEST_RETRY_TIME) {
|
||||
HttpdnsLogDebug("Internal request starts, host: %@, request: %@", host, request);
|
||||
|
||||
NSError *error = nil;
|
||||
NSArray<HttpdnsHostObject *> *resultArray = [[HttpdnsRemoteResolver new] resolve:request error:&error];
|
||||
|
||||
if (error) {
|
||||
HttpdnsLogDebug("Internal request error, host: %@, error: %@", host, error);
|
||||
|
||||
HttpdnsScheduleCenter *scheduleCenter = self.ownerService.scheduleCenter;
|
||||
[scheduleCenter rotateServiceServerHost];
|
||||
|
||||
// 确保一定的重试间隔
|
||||
hasRetryedCount++;
|
||||
[NSThread sleepForTimeInterval:hasRetryedCount * 0.25];
|
||||
|
||||
return [self executeRequest:request retryCount:hasRetryedCount];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isEmptyArray:resultArray]) {
|
||||
HttpdnsLogDebug("Internal request get empty result array, host: %@", host);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 这个路径里,host只会有一个,所以直接取第一个处理就行
|
||||
result = resultArray.firstObject;
|
||||
} else {
|
||||
if (!self.degradeToLocalDNSEnabled) {
|
||||
HttpdnsLogDebug("Internal remote request retry count exceed limit, host: %@", host);
|
||||
return nil;
|
||||
}
|
||||
|
||||
result = [[HttpdnsLocalResolver new] resolve:request];
|
||||
if (!result) {
|
||||
HttpdnsLogDebug("Fallback to local dns resolver, but still get no result, host: %@", host);
|
||||
return nil;
|
||||
}
|
||||
|
||||
isDegradationResult = YES;
|
||||
}
|
||||
|
||||
HttpdnsLogDebug("Internal request finished, host: %@, cacheKey: %@, isDegradationResult: %d, result: %@ ",
|
||||
host, cacheKey, isDegradationResult, result);
|
||||
|
||||
// merge之后,返回的应当是存储在缓存中的实际对象,而非请求过程中构造出来的对象
|
||||
HttpdnsHostObject *lookupResult = [self mergeLookupResultToManager:result host:host cacheKey:cacheKey underQueryIpType:queryIPType];
|
||||
// 返回一个快照,避免进行中的一些缓存调整影响返回去的结果
|
||||
return [lookupResult copy];
|
||||
}
|
||||
|
||||
- (void)executePreResolveRequest:(HttpdnsRequest *)request retryCount:(int)hasRetryedCount {
|
||||
NSString *host = request.host;
|
||||
HttpdnsQueryIPType queryIPType = request.queryIpType;
|
||||
|
||||
BOOL isDegradationResult = NO;
|
||||
|
||||
if (hasRetryedCount > HTTPDNS_MAX_REQUEST_RETRY_TIME) {
|
||||
HttpdnsLogDebug("PreResolve remote request retry count exceed limit, host: %@", host);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpdnsLogDebug("PreResolve request starts, host: %@, request: %@", host, request);
|
||||
|
||||
NSError *error = nil;
|
||||
NSArray<HttpdnsHostObject *> *resultArray = [[HttpdnsRemoteResolver new] resolve:request error:&error];
|
||||
|
||||
if (error) {
|
||||
HttpdnsLogDebug("PreResolve request error, host: %@, error: %@", host, error);
|
||||
|
||||
HttpdnsScheduleCenter *scheduleCenter = self.ownerService.scheduleCenter;
|
||||
[scheduleCenter rotateServiceServerHost];
|
||||
|
||||
// 确保一定的重试间隔
|
||||
hasRetryedCount++;
|
||||
[NSThread sleepForTimeInterval:hasRetryedCount * 0.25];
|
||||
|
||||
// 预解析重试需保持“多域名预解析”的语义,不能误用单域名执行路径
|
||||
[self executePreResolveRequest:request retryCount:hasRetryedCount];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isEmptyArray:resultArray]) {
|
||||
HttpdnsLogDebug("PreResolve request get empty result array, host: %@", host);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpdnsLogDebug("PreResolve request finished, host: %@, isDegradationResult: %d, result: %@ ",
|
||||
host, isDegradationResult, resultArray);
|
||||
|
||||
for (HttpdnsHostObject *result in resultArray) {
|
||||
// merge之后,返回的应当是存储在缓存中的实际对象,而非请求过程中构造出来的对象
|
||||
// 预解析不支持SDNS,所以cacheKey只能是单独的每一个hostName
|
||||
[self mergeLookupResultToManager:result host:result.hostName cacheKey:result.hostName underQueryIpType:queryIPType];
|
||||
}
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)mergeLookupResultToManager:(HttpdnsHostObject *)result host:host cacheKey:(NSString *)cacheKey underQueryIpType:(HttpdnsQueryIPType)queryIpType {
|
||||
if (!result) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray<HttpdnsIpObject *> *v4IpObjects = [result getV4Ips];
|
||||
NSArray<HttpdnsIpObject *> *v6IpObjects = [result getV6Ips];
|
||||
NSString* extra = [result getExtra];
|
||||
|
||||
BOOL hasNoIpv4Record = NO;
|
||||
BOOL hasNoIpv6Record = NO;
|
||||
if (queryIpType & HttpdnsQueryIPTypeIpv4 && [HttpdnsUtil isEmptyArray:v4IpObjects]) {
|
||||
hasNoIpv4Record = YES;
|
||||
}
|
||||
if (queryIpType & HttpdnsQueryIPTypeIpv6 && [HttpdnsUtil isEmptyArray:v6IpObjects]) {
|
||||
hasNoIpv6Record = YES;
|
||||
}
|
||||
|
||||
HttpdnsHostObject *cachedHostObject = [_hostObjectInMemoryCache getHostObjectByCacheKey:cacheKey];
|
||||
if (!cachedHostObject) {
|
||||
HttpdnsLogDebug("Create new hostObject for cache, cacheKey: %@, host: %@", cacheKey, host);
|
||||
cachedHostObject = [[HttpdnsHostObject alloc] init];
|
||||
}
|
||||
|
||||
[cachedHostObject setCacheKey:cacheKey];
|
||||
[cachedHostObject setClientIp:result.clientIp];
|
||||
|
||||
[cachedHostObject setHostName:host];
|
||||
[cachedHostObject setIsLoadFromDB:NO];
|
||||
[cachedHostObject setHasNoIpv4Record:hasNoIpv4Record];
|
||||
[cachedHostObject setHasNoIpv6Record:hasNoIpv6Record];
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v4IpObjects]) {
|
||||
[cachedHostObject setV4Ips:v4IpObjects];
|
||||
[cachedHostObject setV4TTL:result.getV4TTL];
|
||||
[cachedHostObject setLastIPv4LookupTime:result.lastIPv4LookupTime];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6IpObjects]) {
|
||||
[cachedHostObject setV6Ips:v6IpObjects];
|
||||
[cachedHostObject setV6TTL:result.getV6TTL];
|
||||
[cachedHostObject setLastIPv6LookupTime:result.lastIPv6LookupTime];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyString:extra]) {
|
||||
[cachedHostObject setExtra:extra];
|
||||
}
|
||||
|
||||
HttpdnsLogDebug("Updated hostObject to cached, cacheKey: %@, host: %@", cacheKey, host);
|
||||
|
||||
// 由于从缓存中读取到的是拷贝出来的新对象,字段赋值不会影响缓存中的值对象,因此这里无论如何都要放回缓存
|
||||
[_hostObjectInMemoryCache setHostObject:cachedHostObject forCacheKey:cacheKey];
|
||||
|
||||
[self persistToDB:cacheKey hostObject:cachedHostObject];
|
||||
|
||||
NSArray *ipv4StrArray = [cachedHostObject getV4IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:ipv4StrArray]) {
|
||||
[self initiateQualityDetectionForIP:ipv4StrArray forHost:host cacheKey:cacheKey];
|
||||
}
|
||||
|
||||
NSArray *ipv6StrArray = [cachedHostObject getV6IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:ipv6StrArray]) {
|
||||
[self initiateQualityDetectionForIP:ipv6StrArray forHost:host cacheKey:cacheKey];
|
||||
}
|
||||
return cachedHostObject;
|
||||
}
|
||||
|
||||
- (void)initiateQualityDetectionForIP:(NSArray *)ipArray forHost:(NSString *)host cacheKey:(NSString *)cacheKey {
|
||||
HttpDnsService *service = self.ownerService ?: [HttpDnsService sharedInstance];
|
||||
NSDictionary<NSString *, NSNumber *> *dataSource = [service getIPRankingDatasource];
|
||||
if (!dataSource || ![dataSource objectForKey:host]) {
|
||||
return;
|
||||
}
|
||||
NSNumber *port = [dataSource objectForKey:host];
|
||||
for (NSString *ip in ipArray) {
|
||||
[[HttpdnsIPQualityDetector sharedInstance] scheduleIPQualityDetection:cacheKey
|
||||
ip:ip
|
||||
port:port
|
||||
callback:^(NSString * _Nonnull cacheKey, NSString * _Nonnull ip, NSInteger costTime) {
|
||||
[self->_hostObjectInMemoryCache updateQualityForCacheKey:cacheKey forIp:ip withConnectedRT:costTime];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isHostsNumberLimitReached {
|
||||
if ([_hostObjectInMemoryCache count] >= HTTPDNS_MAX_MANAGE_HOST_NUM) {
|
||||
HttpdnsLogDebug("Can't handle more than %d hosts due to the software configuration.", HTTPDNS_MAX_MANAGE_HOST_NUM);
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)handleReachabilityNotification:(NSNotification *)notification {
|
||||
[self networkChanged];
|
||||
}
|
||||
|
||||
- (void)networkChanged {
|
||||
HttpdnsNetworkStatus currentStatus = [[HttpdnsReachability sharedInstance] currentReachabilityStatus];
|
||||
NSString *currentStatusString = [[HttpdnsReachability sharedInstance] currentReachabilityString];
|
||||
|
||||
// 重新检测协议栈代价小,所以只要网络切换就发起检测
|
||||
// 但考虑到网络切换后不稳定,还是延迟1秒才发起
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
|
||||
[[HttpdnsIpStackDetector sharedInstance] redetectIpStack];
|
||||
});
|
||||
|
||||
NSTimeInterval currentTimestamp = [NSDate date].timeIntervalSince1970;
|
||||
BOOL statusChanged = (_lastNetworkStatus != currentStatus);
|
||||
|
||||
// 仅在以下情况下响应网络变化去尝试更新缓存:
|
||||
// - 距离上次处理事件至少过去了较长时间,或
|
||||
// - 网络状态发生变化且至少过去了较短时间
|
||||
NSTimeInterval elapsedTime = currentTimestamp - _lastUpdateTimestamp;
|
||||
if (elapsedTime >= 5 || (statusChanged && elapsedTime >= 1)) {
|
||||
HttpdnsLogDebug("Processing network change: oldStatus: %ld, newStatus: %ld(%@), elapsedTime=%.2f seconds",
|
||||
_lastNetworkStatus, currentStatus, currentStatusString, elapsedTime);
|
||||
|
||||
// 更新调度
|
||||
// 网络在切换过程中可能不稳定,所以发送请求前等待2秒
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
|
||||
HttpdnsScheduleCenter *scheduleCenter = self.ownerService.scheduleCenter;
|
||||
[scheduleCenter asyncUpdateRegionScheduleConfig];
|
||||
});
|
||||
|
||||
// 复杂逻辑(仅注释关键点,避免冗余):
|
||||
// 网络切换后只预解析“cacheKey 等于 hostName”的条目,
|
||||
// 这些条目代表标准域名缓存;SDNS 等使用自定义 cacheKey 的记录不在此批次处理。
|
||||
NSArray *allKeys = [_hostObjectInMemoryCache allCacheKeys];
|
||||
NSMutableArray<NSString *> *hostArray = [NSMutableArray array];
|
||||
for (NSString *key in allKeys) {
|
||||
HttpdnsHostObject *obj = [self->_hostObjectInMemoryCache getHostObjectByCacheKey:key];
|
||||
if (!obj) {
|
||||
continue;
|
||||
}
|
||||
NSString *cacheKey = [obj getCacheKey];
|
||||
NSString *hostName = [obj getHostName];
|
||||
if (cacheKey && hostName && [cacheKey isEqualToString:hostName]) {
|
||||
[hostArray addObject:hostName];
|
||||
}
|
||||
}
|
||||
|
||||
// 预解析
|
||||
// 网络在切换过程中可能不稳定,所以在清理缓存和发送请求前等待3秒
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
|
||||
// 仅清理“hostName 键”的缓存,保留 SDNS 等自定义 cacheKey 的记录
|
||||
for (NSString *host in hostArray) {
|
||||
[self->_hostObjectInMemoryCache removeHostObjectByCacheKey:host];
|
||||
}
|
||||
|
||||
if (self.atomicPreResolveAfterNetworkChanged && hostArray.count > 0) {
|
||||
HttpdnsLogDebug("Network changed, pre resolve for host-key entries: %@", hostArray);
|
||||
[self preResolveHosts:hostArray queryType:HttpdnsQueryIPTypeAuto];
|
||||
}
|
||||
});
|
||||
|
||||
// 更新时间戳和状态
|
||||
_lastNetworkStatus = currentStatus;
|
||||
_lastUpdateTimestamp = currentTimestamp;
|
||||
} else {
|
||||
HttpdnsLogDebug("Ignoring network change event: oldStatus: %ld, newStatus: %ld(%@), elapsedTime=%.2f seconds",
|
||||
_lastNetworkStatus, currentStatus, currentStatusString, elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark - disable status Setter and Getter Method
|
||||
|
||||
- (dispatch_queue_t)cacheQueue {
|
||||
if (!_cacheQueue) {
|
||||
_cacheQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.cacheDisableStatusQueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
return _cacheQueue;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark - Flag for Disable and Sniffer Method
|
||||
|
||||
- (void)loadCacheFromDbToMemory {
|
||||
NSArray<HttpdnsHostRecord *> *hostRecords = [self->_httpdnsDB getAllRecords];
|
||||
|
||||
if ([HttpdnsUtil isEmptyArray:hostRecords]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (HttpdnsHostRecord *hostRecord in hostRecords) {
|
||||
NSString *hostName = hostRecord.hostName;
|
||||
NSString *cacheKey = hostRecord.cacheKey;
|
||||
|
||||
HttpdnsHostObject *hostObject = [HttpdnsHostObject fromDBRecord:hostRecord];
|
||||
|
||||
// 从持久层加载到内存的缓存,需要做个标记,App启动后从缓存使用结果时,根据标记做特殊处理
|
||||
[hostObject setIsLoadFromDB:YES];
|
||||
|
||||
[self->_hostObjectInMemoryCache setHostObject:hostObject forCacheKey:cacheKey];
|
||||
|
||||
NSArray *v4IpStrArr = [hostObject getV4IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:v4IpStrArr]) {
|
||||
[self initiateQualityDetectionForIP:v4IpStrArr forHost:hostName cacheKey:cacheKey];
|
||||
}
|
||||
NSArray *v6IpStrArr = [hostObject getV6IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6IpStrArr]) {
|
||||
[self initiateQualityDetectionForIP:v6IpStrArr forHost:hostName cacheKey:cacheKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfHostArray:(NSArray<NSString *> *)hostArray {
|
||||
for (NSString *host in hostArray) {
|
||||
if ([HttpdnsUtil isNotEmptyString:host]) {
|
||||
[_hostObjectInMemoryCache removeHostObjectByCacheKey:host];
|
||||
}
|
||||
}
|
||||
|
||||
// 清空数据库数据
|
||||
dispatch_async(_persistentCacheConcurrentQueue, ^{
|
||||
[self->_httpdnsDB deleteByHostNameArr:hostArray];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfAllHosts {
|
||||
[_hostObjectInMemoryCache removeAllHostObjects];
|
||||
|
||||
// 清空数据库数据
|
||||
dispatch_async(_persistentCacheConcurrentQueue, ^{
|
||||
[self->_httpdnsDB deleteAll];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)persistToDB:(NSString *)cacheKey hostObject:(HttpdnsHostObject *)hostObject {
|
||||
if (!_persistentCacheIpEnabled) {
|
||||
return;
|
||||
}
|
||||
dispatch_async(_persistentCacheConcurrentQueue, ^{
|
||||
HttpdnsHostRecord *hostRecord = [hostObject toDBRecord];
|
||||
[self->_httpdnsDB createOrUpdate:hostRecord];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark - 以下函数仅用于测试目的
|
||||
|
||||
- (NSString *)showMemoryCache {
|
||||
NSString *cacheDes;
|
||||
cacheDes = [NSString stringWithFormat:@"%@", _hostObjectInMemoryCache];
|
||||
return cacheDes;
|
||||
}
|
||||
|
||||
- (void)cleanAllHostMemoryCache {
|
||||
[_hostObjectInMemoryCache removeAllHostObjects];
|
||||
}
|
||||
|
||||
- (void)syncLoadCacheFromDbToMemory {
|
||||
[self loadCacheFromDbToMemory];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
@end
|
||||
481
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsService.h
Normal file
481
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsService.h
Normal file
@@ -0,0 +1,481 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
// 头文件包含需使用相对目录,确保通过 CocoaPods 安装后能被模块化编译找到
|
||||
// #import "HttpdnsRequest.h"
|
||||
// #import "HttpdnsResult.h"
|
||||
// #import "HttpdnsDegradationDelegate.h"
|
||||
// #import "HttpdnsLoggerProtocol.h"
|
||||
|
||||
#import <AlicloudHTTPDNS/HttpdnsRequest.h>
|
||||
#import <AlicloudHTTPDNS/HttpDnsResult.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsLoggerProtocol.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsDegradationDelegate.h>
|
||||
|
||||
#define ALICLOUD_HTTPDNS_DEPRECATED(explain) __attribute__((deprecated(explain)))
|
||||
|
||||
|
||||
#ifndef ALICLOUDHDNS_STACK_KEY
|
||||
#define ALICLOUDHDNS_STACK_KEY
|
||||
|
||||
#define ALICLOUDHDNS_IPV4 @"ALICLOUDHDNS_IPV4"
|
||||
#define ALICLOUDHDNS_IPV6 @"ALICLOUDHDNS_IPV6"
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol HttpdnsTTLDelegate <NSObject>
|
||||
|
||||
|
||||
/// 自定义HOST的TTL时长
|
||||
/// @return 返回需要自定义的TTL时长
|
||||
/// @param host 域名
|
||||
/// @param ipType 当前查询的IP类型
|
||||
/// @param ttl 当次域名解析返回的TTL
|
||||
- (int64_t)httpdnsHost:(NSString * _Nonnull)host ipType:(AlicloudHttpDNS_IPType)ipType ttl:(int64_t)ttl;
|
||||
|
||||
@end
|
||||
|
||||
@interface HttpDnsService: NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) NSInteger accountID;
|
||||
|
||||
@property (nonatomic, copy, readonly, nullable) NSString *secretKey;
|
||||
|
||||
@property (nonatomic, copy, readonly, nullable) NSString *aesSecretKey;
|
||||
|
||||
@property (nonatomic, weak, setter=setDelegateForDegradationFilter:) id<HttpDNSDegradationDelegate> delegate ALICLOUD_HTTPDNS_DEPRECATED("不再建议通过设置此回调实现降级逻辑,而是自行在调用HTTPDNS解析域名前做判断");
|
||||
|
||||
@property (nonatomic, weak) id<HttpdnsTTLDelegate> ttlDelegate;
|
||||
|
||||
+ (nonnull instancetype)sharedInstance;
|
||||
|
||||
/// 获取指定账号对应的 HttpDnsService 实例
|
||||
/// @param accountID 账号 ID
|
||||
/// @return 已初始化的实例,若账号尚未注册则返回 nil
|
||||
+ (nullable instancetype)getInstanceByAccountId:(NSInteger)accountID;
|
||||
|
||||
/*!
|
||||
* @brief 无需鉴权功能的初始化接口
|
||||
* @details 初始化,设置 HTTPDNS 服务 Account ID。使用本接口初始化,请求将无任何签名保护,请谨慎使用。
|
||||
* 您可以从控制台获取您的 Account ID 。
|
||||
* 此方法会初始化为单例。
|
||||
* 注意:本接口从3.2.1起废弃,后续将进行移除。
|
||||
* @param accountID 您的 HTTPDNS Account ID
|
||||
*/
|
||||
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. This method will be removed in the future. Use -[HttpDnsService initWithAccountID:secretKey:] instead.");
|
||||
|
||||
/*!
|
||||
* @brief 启用鉴权功能的初始化接口
|
||||
* @details 初始化、开启鉴权功能,并设置 HTTPDNS 服务 Account ID,鉴权功能对应的 secretKey。
|
||||
* 您可以从控制台获取您的 Account ID 、secretKey信息。
|
||||
* 此方法会初始化为单例。
|
||||
* @param accountID 您的 HTTPDNS Account ID
|
||||
* @param secretKey 鉴权对应的 secretKey
|
||||
*/
|
||||
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID secretKey:(NSString * _Nonnull)secretKey;
|
||||
|
||||
/*!
|
||||
* @brief 启用鉴权功能、加密功能的初始化接口
|
||||
* @details 初始化、开启鉴权功能、开启AES加密,并设置 HTTPDNS 服务 Account ID,鉴权功能对应的 secretKey,加密功能对应的 aesSecretKey。
|
||||
* 您可以从控制台获取您的 Account ID 、secretKey、aesSecretKey 信息。
|
||||
* 此方法会初始化为单例。
|
||||
* @param accountID 您的 HTTPDNS Account ID
|
||||
* @param secretKey 鉴权对应的 secretKey
|
||||
* @param aesSecretKey 加密功能对应的 aesSecretKey
|
||||
*/
|
||||
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID secretKey:(NSString * _Nonnull)secretKey aesSecretKey:(NSString * _Nullable)aesSecretKey;
|
||||
|
||||
|
||||
/// 开启鉴权功能后,鉴权的签名计算默认读取设备当前时间。若担心设备时间不准确导致签名不准确,可以使用此接口校正 APP 内鉴权计算使用的时间值
|
||||
/// 注意,校正操作在 APP 的一个生命周期内生效,APP 重启后需要重新设置才能重新生效
|
||||
/// @param authCurrentTime 用于校正的时间戳,单位为秒
|
||||
- (void)setAuthCurrentTime:(NSUInteger)authCurrentTime ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setInternalAuthTimeBaseBySpecifyingCurrentTime:] instead.");
|
||||
|
||||
|
||||
/// 开启鉴权功能后,鉴权的签名计算默认读取设备当前时间。若担心设备时间不准确导致签名不准确,可以使用此接口校正 APP 内鉴权计算使用的时间值
|
||||
/// 注意,校正操作在 APP 的一个生命周期内生效,APP 重启后需要重新设置才能重新生效
|
||||
/// @param currentTime 用于校正的时间戳,单位为秒
|
||||
- (void)setInternalAuthTimeBaseBySpecifyingCurrentTime:(NSTimeInterval)currentTime;
|
||||
|
||||
|
||||
/// 设置持久化缓存功能
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setCachedIPEnabled:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setPersistentCacheIPEnabled:] instead.");
|
||||
|
||||
|
||||
/// 设置持久化缓存功能
|
||||
/// 开启后,每次解析会将结果持久化缓存到本地,当下次应用启动时,可以从本地加载缓存解析结果,提高应用启动时获取解析结果的速度
|
||||
/// 加载时,会丢弃已经过期的解析结果
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setPersistentCacheIPEnabled:(BOOL)enable;
|
||||
|
||||
/// 设置持久化缓存功能
|
||||
/// 开启后,每次解析会将结果持久化缓存到本地,当下次应用启动时,可以从本地加载缓存解析结果,提高应用启动时获取解析结果的速度
|
||||
/// 加载时,会丢弃过期时间已经超过指定值的解析结果
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
/// @param duration 决定丢弃IP的过期时间阈值,单位为秒,过期超过这个时间范围的IP会被丢弃,取值范围为0-1年。这个值仅在开启持久化缓存功能时才有意义
|
||||
- (void)setPersistentCacheIPEnabled:(BOOL)enable discardRecordsHasExpiredFor:(NSTimeInterval)duration;
|
||||
|
||||
/// 是否允许 HTTPDNS 返回 TTL 过期域名的 ip ,建议允许(默认不允许)
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setExpiredIPEnabled:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setReuseExpiredIPEnabled:] instead.");
|
||||
|
||||
|
||||
/// 是否允许 HTTPDNS 返回 TTL 过期域名的 ip ,建议允许(默认不允许)
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setReuseExpiredIPEnabled:(BOOL)enable;
|
||||
|
||||
|
||||
/// 设置 HTTPDNS 域名解析请求类型 ( HTTP / HTTPS )
|
||||
/// 若不调用该接口,默认为 HTTP 请求。
|
||||
/// HTTP 请求基于底层 CFNetwork 实现,不受 ATS 限制;
|
||||
/// @param enable YES: HTTPS请求 NO: HTTP请求
|
||||
- (void)setHTTPSRequestEnabled:(BOOL)enable;
|
||||
|
||||
|
||||
/// 声明App是否配置了ATS为AllowsArbitraryLoads,默认认为没有配置
|
||||
/// 若做了声明,则当指定走HTTP方式解析域名时,解析链路会走系统NSURLSession逻辑
|
||||
/// 否则,会走定制的CFHTTP链路,避免被ATS拦截请求
|
||||
- (void)setHasAllowedArbitraryLoadsInATS:(BOOL)hasAllowedArbitraryLoadsInATS;
|
||||
|
||||
|
||||
/// 设置底层HTTPDNS网络请求超时时间,单位为秒
|
||||
/// 需要注意,这个值只决定底层解析请求的网络超时时间,而非同步解析接口、异步解析接口的最长阻塞或者等待时间
|
||||
/// 同步解析接口、异步解析接口的最长阻塞或者等待时间,需要调用接口时设置request参数中的`resolveTimeoutInSecond`决定
|
||||
/// @param timeoutInterval 超时时间,单位为秒
|
||||
- (void)setNetworkingTimeoutInterval:(NSTimeInterval)timeoutInterval;
|
||||
|
||||
|
||||
/// 指定region,指定后会读取该region对应配置作为初始化配置
|
||||
/// 一般情况下无需设置,SDK内部会默认路由全球范围内最近的接入点
|
||||
/// @param region 需要指定的region,缺省为中国大陆
|
||||
- (void)setRegion:(NSString *)region;
|
||||
|
||||
|
||||
/// 域名预解析 (默认解析双栈记录)
|
||||
/// 通常用于启动后立即向SDK设置您后续可能会使用到的热点域名,以便SDK提前解析,减少后续解析域名时请求的时延
|
||||
/// 如果是在运行过程中调用,SDK也会立即解析设置的域名数组中的域名,刷新这些域名的解析结果
|
||||
///
|
||||
/// @param hosts 预解析列表数组
|
||||
- (void)setPreResolveHosts:(NSArray *)hosts;
|
||||
|
||||
|
||||
/// 域名预解析,可以指定预解析auto、ipv4、ipv6、both
|
||||
/// 通常用于启动后立即向SDK设置您后续可能会使用到的热点域名,以便SDK提前解析,减少后续解析域名时请求的时延
|
||||
/// 如果是在运行过程中调用,SDK也会立即解析设置的域名数组中的域名,刷新这些域名的解析结果
|
||||
///
|
||||
/// @param hosts 预解析列表数组
|
||||
/// @param ipType 指定预解析记录类型
|
||||
- (void)setPreResolveHosts:(NSArray *)hosts byIPType:(HttpdnsQueryIPType)ipType;
|
||||
|
||||
|
||||
/// 域名预解析
|
||||
/// @param hosts 域名
|
||||
/// @param ipType 4: ipv4; 6: ipv6; 64: ipv4+ipv6
|
||||
- (void)setPreResolveHosts:(NSArray *)hosts queryIPType:(AlicloudHttpDNS_IPType)ipType ALICLOUD_HTTPDNS_DEPRECATED("Deprecated, this method will be removed in the future. Use -[HttpDnsService setPreResolveHosts:byIPType:] instead.");
|
||||
|
||||
|
||||
/// 本地日志 log 开关
|
||||
/// @param enable YES: 打开 NO: 关闭
|
||||
- (void)setLogEnabled:(BOOL)enable;
|
||||
|
||||
|
||||
/// 设置网络切换时是否自动更新所有域名解析结果
|
||||
/// 如果打开此开关,在网络切换时,会自动刷新所有域名的解析结果,但会产生一定流量消耗
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setPreResolveAfterNetworkChanged:(BOOL)enable;
|
||||
|
||||
|
||||
/// 设置当httpdns解析失败时是否降级到localDNS尝试解析
|
||||
/// 降级生效时,SDNS参数不生效,降级逻辑只解析域名,返回的结果默认使用60秒(若未指定该域名自定义TTL)作为TTL值
|
||||
/// 降级请求也不会再对ip进行优先排序
|
||||
/// 默认关闭,不会自动降级
|
||||
/// @param enable YES:自动降级 NO:不自动降级
|
||||
- (void)setDegradeToLocalDNSEnabled:(BOOL)enable;
|
||||
|
||||
|
||||
/// 设置IP排优规则
|
||||
/// @param IPRankingDatasource 设置对应域名的端口号
|
||||
/// @{host: port}
|
||||
- (void)setIPRankingDatasource:(NSDictionary<NSString *, NSNumber *> *)IPRankingDatasource;
|
||||
|
||||
|
||||
/// 设置是否 开启 IPv6 结果解析。只有开启状态下,对域名的解析才会尝试解析v6记录并返回v6的结果
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)enableIPv6:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setIPv6Enabled:] instead.");
|
||||
|
||||
|
||||
/// 设置是否 开启 IPv6 结果解析。只有开启状态下,对域名的解析才会尝试解析v6记录并返回v6的结果
|
||||
/// 已弃用。默认支持IPv6。如果不需要IPv6类型的结果,只需在请求时指定`queryIpType`为`HttpdnsQueryIPTypeIpv4`
|
||||
/// @param enable YES: 开启 NO: 关闭
|
||||
- (void)setIPv6Enabled:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. If ipv6 is unnecessary, you can set the `queryIpType` as HttpdnsQueryIPTypeIpv4 when resolving domain.");
|
||||
|
||||
|
||||
/// 是否允许通过 CNCopyCurrentNetworkInfo 获取wifi ssid bssid
|
||||
/// @param enable YES: 开启 NO: 关闭 ,默认关闭
|
||||
- (void)enableNetworkInfo:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. We do not utilize network information anymore");
|
||||
|
||||
|
||||
/// 是否允许通过 CNCopyCurrentNetworkInfo 获取wifi ssid bssid
|
||||
/// @param enable YES: 开启 NO: 关闭 ,默认关闭
|
||||
- (void)setReadNetworkInfoEnabled:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. We do not utilize network information anymore.");
|
||||
|
||||
|
||||
/// 是否开启IP探测功能
|
||||
/// @param enable YES: 开启 NO: 关闭 默认打开
|
||||
- (void)enableCustomIPRank:(BOOL)enable ALICLOUD_HTTPDNS_DEPRECATED("Deprecated, will be removed in the future.");
|
||||
|
||||
|
||||
/// 设置软件自定义解析全局默认参数,设置后,调用软件自定义解析时,每个请求默认都会带上这里配置的参数
|
||||
/// @param params 全局默认参数
|
||||
- (void)setSdnsGlobalParams:(NSDictionary<NSString *, NSString *> *)params;
|
||||
|
||||
|
||||
/// 设置日志输出回调,以实现自定义日志输出方式
|
||||
/// @param logHandler 日志输出回调实现实例
|
||||
- (void)setLogHandler:(id<HttpdnsLoggerProtocol>)logHandler;
|
||||
|
||||
|
||||
/// 获取用于用户追踪的 sessionId
|
||||
/// sessionId为随机生成,长度为 12 位,App 生命周期内保持不变
|
||||
/// 为了排查可能的解析问题,需要您将 sessionId 和解析出的 IP 一起记录在日志中
|
||||
/// 请参考: 解析异常排查之 “会话追踪方案” https://help.aliyun.com/document_detail/100530.html
|
||||
- (NSString *)getSessionId;
|
||||
|
||||
/// 同步解析域名,会阻塞当前线程,直到从缓存中获取到有效解析结果,或者从服务器拿到最新解析结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// 为了防止在主线程中误用本接口导致APP卡顿,本接口会做检测,若发现调用线程是主线程,则自动降级到resolveHostSyncNonBlocking接口的实现逻辑。
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
/// 同步解析域名,会阻塞当前线程,直到从缓存中获取到有效解析结果,或者从服务器拿到最新解析结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// 为了防止在主线程中误用本接口导致APP卡顿,本接口会做检测,若发现调用线程是主线程,则自动降级到resolveHostSyncNonBlocking接口的实现逻辑。
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(NSString *)cacheKey;
|
||||
|
||||
/// 同步解析域名,会阻塞当前线程,直到从缓存中获取到有效解析结果,或者从服务器拿到最新解析结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// 为了防止在主线程中误用本接口导致APP卡顿,本接口会做检测,若发现调用线程是主线程,则自动降级到resolveHostSyncNonBlocking接口的实现逻辑。
|
||||
/// @param request 请求参数对象
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(HttpdnsRequest *)request;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(nullable NSString *)cacheKey completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结果
|
||||
/// @param request 请求参数对象
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(HttpdnsRequest *)request completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为空
|
||||
/// 先查询缓存,缓存中存在有效结果(未过期,或者过期但配置了可以复用过期解析结果),则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为空
|
||||
/// 先查询缓存,缓存中存在有效结果(未过期,或者过期但配置了可以复用过期解析结果),则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(nullable NSString *)cacheKey;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为空
|
||||
/// 先查询缓存,缓存中存在有效结果(未过期,或者过期但配置了可以复用过期解析结果),则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param request 请求参数对象
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(HttpdnsRequest *)request;
|
||||
|
||||
|
||||
/// 获取域名对应的IP,单IP
|
||||
/// @param host 域名
|
||||
- (NSString *)getIpByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的IP数组,多IP
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIpsByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的ipv6, 单IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSString *)getIPv6ByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的ipv6数组, 多IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIPv6sByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 同时获取ipv4 ipv6的IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// @param host 域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
- (NSDictionary <NSString *, NSArray *>*)getIPv4_v6ByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址组
|
||||
/// 使用此API 需要确保 enableIPv6 开关已打开
|
||||
/// 设备网络 返回域名IP
|
||||
/// IPv4 Only IPv4
|
||||
/// IPv6 Only IPv6 (如果没有Pv6返回空)
|
||||
/// 双栈 IPv4/IPV6
|
||||
/// @param host 要解析的域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
-(NSDictionary <NSString *, NSArray *>*)autoGetIpsByHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的IPv4地址,单IPv4
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSString *)getIPv4ForHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的IP数组,多IP
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIPv4ListForHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 获取IPv4地址列表,同步接口,必须在子线程中执行,否则会转变为异步接口
|
||||
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限为5s,
|
||||
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大于5s,同步接口也最多阻塞当前线程5s
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起同步解析请求
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIPv4ListForHostSync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的ipv6, 单IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSString *)getIPv6ForHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应的ipv6数组, 多IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIPv6ListForHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 获取IPv6地址列表,同步接口,必须在子线程中执行,否则会转变为异步接口
|
||||
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限为5s,
|
||||
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大于5s,同步接口也最多阻塞当前线程5s
|
||||
/// @param host 域名
|
||||
- (NSArray *)getIPv6ListForHostSync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,获取域名对应格式化后的IP (针对ipv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
- (NSString *)getIpByHostAsyncInURLFormat:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
|
||||
/// 异步接口,首次结果可能为空,同时获取ipv4 ipv6的IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
- (NSDictionary <NSString *, NSArray *>*)getHttpDnsResultHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// NOTE: 同步接口,必须在子线程中执行,否则会转变为异步接口
|
||||
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限为5s,
|
||||
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大于5s,同步接口也最多阻塞当前线程5s
|
||||
/// 同时获取ipv4 + ipv6的IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
- (NSDictionary <NSString *, NSArray *>*)getHttpDnsResultHostSync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
|
||||
|
||||
/// 异步接口,首次结果可能为空,根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址组
|
||||
/// 使用此API 需要确保 enableIPv6 开关已打开
|
||||
/// 设备网络 返回域名IP
|
||||
/// IPv4 Only IPv4
|
||||
/// IPv6 Only IPv6 (如果没有Pv6返回空)
|
||||
/// 双栈 IPv4/IPV6
|
||||
/// @param host 要解析的域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
-(NSDictionary <NSString *, NSArray *>*)autoGetHttpDnsResultForHostAsync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
|
||||
|
||||
/// 根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址组,同步接口,必须在子线程中执行,否则会转变为异步接口
|
||||
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限为5s,
|
||||
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大于5s,同步接口也最多阻塞当前线程5s
|
||||
/// 根据当前网络栈自动获取ipv4 ipv6的IP (需要开启ipv6 开关 enableIPv6)
|
||||
/// 先查询缓存,缓存中存在未过期的结果,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 域名
|
||||
/// @result 返回字典类型结构
|
||||
/// {
|
||||
/// ALICLOUDHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
|
||||
/// ALICLOUDHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
|
||||
/// }
|
||||
- (NSDictionary <NSString *, NSArray *>*)autoGetHttpDnsResultForHostSync:(NSString *)host ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
|
||||
|
||||
/// 软件自定义解析接口
|
||||
- (NSDictionary *)getIpsByHostAsync:(NSString *)host withParams:(NSDictionary<NSString *, NSString *> *)params withCacheKey:(NSString *)cacheKey ALICLOUD_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:withSdnsParams:sdnsCacheKey:] instead.");
|
||||
|
||||
|
||||
/// 清除指定host缓存(内存+沙盒数据库)
|
||||
/// @param hostArray 需要清除的host域名数组。如果需要清空全部数据传nil或者空数组即可
|
||||
- (void)cleanHostCache:(nullable NSArray<NSString *> *)hostArray;
|
||||
|
||||
/// 清除当前所有host缓存 (内存+沙盒数据库)
|
||||
- (void)cleanAllHostCache;
|
||||
|
||||
/// 清理已经配置的软件自定义解析全局参数
|
||||
- (void)clearSdnsGlobalParams;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
1250
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsService.m
Normal file
1250
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsService.m
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsService.h"
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
@class HttpdnsScheduleCenter;
|
||||
|
||||
|
||||
@interface HttpDnsService()
|
||||
|
||||
@property (nonatomic, strong) HttpdnsRequestManager *requestManager;
|
||||
@property (nonatomic, strong) HttpdnsScheduleCenter *scheduleCenter;
|
||||
|
||||
@property (atomic, assign) NSTimeInterval authTimeOffset;
|
||||
|
||||
@property (nonatomic, copy) NSDictionary<NSString *, NSNumber *> *IPRankingDataSource;
|
||||
|
||||
@property (atomic, assign) NSTimeInterval timeoutInterval;
|
||||
|
||||
@property (atomic, assign) BOOL enableHttpsRequest;
|
||||
|
||||
@property (atomic, assign) BOOL allowedArbitraryLoadsInATS;
|
||||
|
||||
@property (atomic, assign) BOOL enableDegradeToLocalDNS;
|
||||
|
||||
- (NSString *)getIpByHost:(NSString *)host;
|
||||
|
||||
- (NSArray *)getIpsByHost:(NSString *)host;
|
||||
|
||||
- (NSString *)getIpByHostInURLFormat:(NSString *)host;
|
||||
|
||||
- (NSDictionary<NSString *, NSNumber *> *)getIPRankingDatasource;
|
||||
|
||||
@end
|
||||
26
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Info.plist
Normal file
26
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// HttpdnsIpStackDetector.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/16.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* IP 协议栈类型
|
||||
*/
|
||||
typedef enum {
|
||||
kHttpdnsIpUnknown = 0, // 未知协议栈
|
||||
kHttpdnsIpv4Only = 1, // IPv4-only
|
||||
kHttpdnsIpv6Only = 2, // IPv6-only
|
||||
kHttpdnsIpDual = 3 // 双栈
|
||||
} HttpdnsIPStackType;
|
||||
|
||||
@interface HttpdnsIpStackDetector : NSObject
|
||||
|
||||
/**
|
||||
* 返回HttpdnsIpStackDetector的共享实例
|
||||
* @return HttpdnsIpStackDetector实例
|
||||
*/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/**
|
||||
* 返回当前缓存的IP协议栈类型,不执行检测
|
||||
* @return HttpdnsIPStackType
|
||||
*/
|
||||
- (HttpdnsIPStackType)currentIpStack;
|
||||
|
||||
/**
|
||||
* 返回当前是否是IPv6-only网络
|
||||
* @return BOOL
|
||||
*/
|
||||
- (BOOL)isIpv6OnlyNetwork;
|
||||
|
||||
/**
|
||||
* 强制重新检测IP协议栈类型
|
||||
*/
|
||||
- (void)redetectIpStack;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// HttpdnsIpStackDetector.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/16.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#include <strings.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
typedef union httpdns_sockaddr_union {
|
||||
struct sockaddr httpdns_generic;
|
||||
struct sockaddr_in httpdns_in;
|
||||
struct sockaddr_in6 httpdns_in6;
|
||||
} httpdns_sockaddr_union;
|
||||
|
||||
/*
|
||||
* 连接UDP套接字到指定的单播地址。这不会产生网络流量,
|
||||
* 但如果系统对目标没有或有限的可达性(例如,没有IPv4地址,没有IPv6默认路由等),
|
||||
* 将快速失败。
|
||||
*/
|
||||
static const unsigned int kMaxLoopCount = 10;
|
||||
|
||||
static int httpdns_test_connect(int pf, struct sockaddr * addr, size_t addrlen) {
|
||||
int s = socket(pf, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s < 0) {
|
||||
return 0;
|
||||
}
|
||||
int ret;
|
||||
unsigned int loop_count = 0;
|
||||
do {
|
||||
ret = connect(s, addr, (socklen_t)addrlen);
|
||||
} while (ret < 0 && errno == EINTR && loop_count++ < kMaxLoopCount);
|
||||
if (loop_count >= kMaxLoopCount) {
|
||||
HttpdnsLogDebug("connect error. loop_count = %d", loop_count);
|
||||
}
|
||||
int success = (ret == 0);
|
||||
loop_count = 0;
|
||||
do {
|
||||
ret = close(s);
|
||||
} while (ret < 0 && errno == EINTR && loop_count++ < kMaxLoopCount);
|
||||
if (loop_count >= kMaxLoopCount) {
|
||||
HttpdnsLogDebug("close error. loop_count = %d", loop_count);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/*
|
||||
* 以下函数用于确定IPv4或IPv6连接是否可用,以实现AI_ADDRCONFIG。
|
||||
*
|
||||
* 严格来说,AI_ADDRCONFIG不应该检查连接是否可用,
|
||||
* 而是检查指定协议族的地址是否"在本地系统上配置"。
|
||||
* 然而,bionic目前不支持getifaddrs,
|
||||
* 所以检查连接是下一个最佳选择。
|
||||
*/
|
||||
static int httpdns_have_ipv6(void) {
|
||||
static struct sockaddr_in6 sin6_test = {0};
|
||||
sin6_test.sin6_family = AF_INET6;
|
||||
sin6_test.sin6_port = 80;
|
||||
sin6_test.sin6_flowinfo = 0;
|
||||
sin6_test.sin6_scope_id = 0;
|
||||
bzero(sin6_test.sin6_addr.s6_addr, sizeof(sin6_test.sin6_addr.s6_addr));
|
||||
sin6_test.sin6_addr.s6_addr[0] = 0x20;
|
||||
// union
|
||||
httpdns_sockaddr_union addr = {.httpdns_in6 = sin6_test};
|
||||
return httpdns_test_connect(PF_INET6, &addr.httpdns_generic, sizeof(addr.httpdns_in6));
|
||||
}
|
||||
|
||||
static int httpdns_have_ipv4(void) {
|
||||
static struct sockaddr_in sin_test = {0};
|
||||
sin_test.sin_family = AF_INET;
|
||||
sin_test.sin_port = 80;
|
||||
sin_test.sin_addr.s_addr = htonl(0x08080808L); // 8.8.8.8
|
||||
// union
|
||||
httpdns_sockaddr_union addr = {.httpdns_in = sin_test};
|
||||
return httpdns_test_connect(PF_INET, &addr.httpdns_generic, sizeof(addr.httpdns_in));
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于IPv4和IPv6连接检测当前IP协议栈类型
|
||||
*/
|
||||
static HttpdnsIPStackType detectIpStack(void) {
|
||||
int hasIPv4 = httpdns_have_ipv4();
|
||||
int hasIPv6 = httpdns_have_ipv6();
|
||||
|
||||
HttpdnsLogDebug("IP stack detection: IPv4=%d, IPv6=%d", hasIPv4, hasIPv6);
|
||||
|
||||
if (hasIPv4 && hasIPv6) {
|
||||
return kHttpdnsIpDual;
|
||||
} else if (hasIPv4) {
|
||||
return kHttpdnsIpv4Only;
|
||||
} else if (hasIPv6) {
|
||||
return kHttpdnsIpv6Only;
|
||||
} else {
|
||||
return kHttpdnsIpUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
@implementation HttpdnsIpStackDetector {
|
||||
HttpdnsIPStackType _lastDetectedIpStack;
|
||||
dispatch_queue_t _detectSerialQueue; // 用于控制检测操作的串行队列
|
||||
dispatch_queue_t _updateSerialQueue;
|
||||
BOOL _isDetecting; // 标记是否正在进行检测
|
||||
NSTimeInterval _lastDetectionTime; // 上次检测的时间戳
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static HttpdnsIpStackDetector *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lastDetectedIpStack = kHttpdnsIpUnknown;
|
||||
_isDetecting = NO;
|
||||
_lastDetectionTime = 0;
|
||||
// 创建串行队列用于控制IP栈检测的并发
|
||||
_detectSerialQueue = dispatch_queue_create("com.aliyun.httpdns.ipstack.detect", DISPATCH_QUEUE_SERIAL);
|
||||
_updateSerialQueue = dispatch_queue_create("com.aliyun.httpdns.ipstack.update", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (HttpdnsIPStackType)currentIpStack {
|
||||
// 如果不在主队列,同步获取主队列中的值以确保线程安全
|
||||
__block HttpdnsIPStackType result;
|
||||
dispatch_sync(_updateSerialQueue, ^{
|
||||
result = self->_lastDetectedIpStack;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isIpv6OnlyNetwork {
|
||||
return [self currentIpStack] == kHttpdnsIpv6Only;
|
||||
}
|
||||
|
||||
- (void)redetectIpStack {
|
||||
// 完全异步执行,将检查逻辑放在串行队列中
|
||||
dispatch_async(_detectSerialQueue, ^{
|
||||
// 如果已经在检测中,直接返回
|
||||
if (self->_isDetecting) {
|
||||
HttpdnsLogDebug("IP stack detection already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否满足最小时间间隔要求(1秒)
|
||||
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
|
||||
NSTimeInterval timeSinceLastDetection = currentTime - self->_lastDetectionTime;
|
||||
if (timeSinceLastDetection < 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后检测时间
|
||||
self->_lastDetectionTime = currentTime;
|
||||
|
||||
// 标记为正在检测并执行检测
|
||||
self->_isDetecting = YES;
|
||||
|
||||
// 执行实际的检测操作(已经在串行队列中,无需再次异步)
|
||||
HttpdnsIPStackType detectedStack = detectIpStack();
|
||||
|
||||
// 在主队列中更新结果,确保线程安全
|
||||
dispatch_async(self->_updateSerialQueue, ^{
|
||||
self->_lastDetectedIpStack = detectedStack;
|
||||
|
||||
// 重置检测状态(已经在串行队列的异步块中,完成后再次异步到串行队列)
|
||||
dispatch_async(self->_detectSerialQueue, ^{
|
||||
self->_isDetecting = NO;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
30
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Log/HttpdnsLog.h
Normal file
30
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Log/HttpdnsLog.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsLog : NSObject
|
||||
|
||||
+ (void)enableLog;
|
||||
|
||||
+ (void)disableLog;
|
||||
|
||||
+ (BOOL)isEnabled;
|
||||
|
||||
@end
|
||||
73
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Log/HttpdnsLog.m
Normal file
73
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Log/HttpdnsLog.m
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
|
||||
static BOOL HttpdnsLogIsEnabled = NO;
|
||||
|
||||
static id<HttpdnsLoggerProtocol> sLogHandler;
|
||||
|
||||
@implementation HttpdnsLog
|
||||
|
||||
+ (void)enableLog {
|
||||
HttpdnsLogIsEnabled = YES;
|
||||
}
|
||||
|
||||
+ (void)disableLog {
|
||||
HttpdnsLogIsEnabled = NO;
|
||||
}
|
||||
|
||||
+ (BOOL)isEnabled {
|
||||
return HttpdnsLogIsEnabled;
|
||||
}
|
||||
|
||||
+ (void)setLogHandler:(id<HttpdnsLoggerProtocol>)handler {
|
||||
SEL sel = NSSelectorFromString(@"log:");
|
||||
if (handler && [handler respondsToSelector:sel]) {
|
||||
sLogHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)unsetLogHandler {
|
||||
sLogHandler = nil;
|
||||
}
|
||||
|
||||
+ (BOOL)validLogHandler {
|
||||
return (sLogHandler != nil);
|
||||
}
|
||||
|
||||
+ (void)outputToLogHandler:(NSString *)logStr {
|
||||
if (sLogHandler) {
|
||||
[sLogHandler log:logStr];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)getFormattedDateTimeStr {
|
||||
static NSDateFormatter *dateFormatter;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [[NSDateFormatter alloc] init];
|
||||
});
|
||||
|
||||
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
|
||||
return [dateFormatter stringFromDate:[NSDate date]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// HttpdnsLog_Internal.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by junmo on 2018/12/19.
|
||||
// Copyright © 2018年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsLog.h"
|
||||
#import "HttpdnsLoggerProtocol.h"
|
||||
#import <pthread/pthread.h>
|
||||
|
||||
// logHandler输出日志,不受日志开关影响
|
||||
#define HttpdnsLogDebug(frmt, ...) \
|
||||
if ([HttpdnsLog validLogHandler]) { \
|
||||
@try { \
|
||||
uint64_t tid = 0; \
|
||||
pthread_threadid_np(NULL, &tid); \
|
||||
NSString *logFormat = [NSString stringWithFormat:@"%s", frmt]; \
|
||||
NSString *logStr = [NSString stringWithFormat:@"[%llu] %@", tid, [NSString stringWithFormat:logFormat, ##__VA_ARGS__, nil]]; \
|
||||
[HttpdnsLog outputToLogHandler:logStr]; \
|
||||
} @catch (NSException *exception){ \
|
||||
} \
|
||||
} \
|
||||
if ([HttpdnsLog isEnabled]) { \
|
||||
@try { \
|
||||
uint64_t tid = 0; \
|
||||
pthread_threadid_np(NULL, &tid); \
|
||||
NSLog((@"%@ HTTPDNSSDKLOG [%llu] - " frmt), [HttpdnsLog getFormattedDateTimeStr], tid, ##__VA_ARGS__); \
|
||||
} @catch (NSException *exception){ \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
@interface HttpdnsLog ()
|
||||
|
||||
+ (void)setLogHandler:(id<HttpdnsLoggerProtocol>)handler;
|
||||
+ (void)unsetLogHandler;
|
||||
+ (BOOL)validLogHandler;
|
||||
+ (void)outputToLogHandler:(NSString *)logStr;
|
||||
|
||||
+ (NSString *)getFormattedDateTimeStr;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// HttpdnsLoggerProtocol.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by junmo on 2018/12/19.
|
||||
// Copyright © 2018年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef HttpdnsLoggerProtocol_h
|
||||
#define HttpdnsLoggerProtocol_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol HttpdnsLoggerProtocol <NSObject>
|
||||
|
||||
- (void)log:(NSString *)logStr;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HttpdnsLoggerProtocol_h */
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@class HttpdnsHostRecord;
|
||||
@class HttpdnsIPRecord;
|
||||
@class HttpdnsServerIpObject;
|
||||
|
||||
|
||||
@interface HttpdnsIpObject: NSObject<NSCoding, NSCopying>
|
||||
|
||||
@property (nonatomic, copy, getter=getIpString, setter=setIp:) NSString *ip;
|
||||
@property (nonatomic, assign) NSInteger connectedRT;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface HttpdnsHostObject : NSObject<NSCoding, NSCopying>
|
||||
|
||||
@property (nonatomic, copy, setter=setCacheKey:, getter=getCacheKey) NSString *cacheKey;
|
||||
@property (nonatomic, copy, setter=setHostName:, getter=getHostName) NSString *hostName;
|
||||
|
||||
@property (nonatomic, copy, setter=setClientIp:, getter=getClientIp) NSString *clientIp;
|
||||
|
||||
@property (nonatomic, strong, setter=setV4Ips:, getter=getV4Ips) NSArray<HttpdnsIpObject *> *v4Ips;
|
||||
@property (nonatomic, strong, setter=setV6Ips:, getter=getV6Ips) NSArray<HttpdnsIpObject *> *v6Ips;
|
||||
|
||||
// 虽然当前后端接口的设计里ttl并没有区分v4、v6,但原则是应该要分开
|
||||
// v4 ttl
|
||||
@property (nonatomic, setter=setV4TTL:, getter=getV4TTL) int64_t v4ttl;
|
||||
@property (nonatomic, assign) int64_t lastIPv4LookupTime;
|
||||
|
||||
// v6 ttl
|
||||
@property (nonatomic, setter=setV6TTL:, getter=getV6TTL) int64_t v6ttl;
|
||||
@property (nonatomic, assign) int64_t lastIPv6LookupTime;
|
||||
|
||||
// 用来标记该域名为配置v4记录或v6记录的情况,避免如双栈网络下因为某个协议查不到record需要重复请求
|
||||
// 这个信息不用持久化,一次APP启动周期内使用是合适的
|
||||
@property (nonatomic, assign) BOOL hasNoIpv4Record;
|
||||
@property (nonatomic, assign) BOOL hasNoIpv6Record;
|
||||
|
||||
@property (nonatomic, strong, setter=setExtra:, getter=getExtra) NSString *extra;
|
||||
|
||||
// 标识是否从持久化缓存加载
|
||||
@property (nonatomic, assign, setter=setIsLoadFromDB:, getter=isLoadFromDB) BOOL isLoadFromDB;
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
- (BOOL)isIpEmptyUnderQueryIpType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
- (BOOL)isExpiredUnderQueryIpType:(HttpdnsQueryIPType)queryIPType;
|
||||
|
||||
+ (instancetype)fromDBRecord:(HttpdnsHostRecord *)IPRecord;
|
||||
|
||||
/**
|
||||
* 将当前对象转换为数据库记录对象
|
||||
* @return 数据库记录对象
|
||||
*/
|
||||
- (HttpdnsHostRecord *)toDBRecord;
|
||||
|
||||
- (NSArray<NSString *> *)getV4IpStrings;
|
||||
|
||||
- (NSArray<NSString *> *)getV6IpStrings;
|
||||
|
||||
/**
|
||||
* 更新指定IP的connectedRT值并重新排序IP列表
|
||||
* @param ip 需要更新的IP地址
|
||||
* @param connectedRT 检测到的RT值,-1表示不可达
|
||||
*/
|
||||
- (void)updateConnectedRT:(NSInteger)connectedRT forIP:(NSString *)ip;
|
||||
|
||||
@end
|
||||
306
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsHostObject.m
Normal file
306
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsHostObject.m
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsHostRecord.h"
|
||||
#import "HttpdnsIPQualityDetector.h"
|
||||
|
||||
|
||||
@implementation HttpdnsIpObject
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
// 初始化connectedRT为最大整数值
|
||||
self.connectedRT = NSIntegerMax;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder {
|
||||
if (self = [super init]) {
|
||||
self.ip = [aDecoder decodeObjectForKey:@"ip"];
|
||||
self.connectedRT = [aDecoder decodeIntegerForKey:@"connectedRT"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeObject:self.ip forKey:@"ip"];
|
||||
[aCoder encodeInteger:self.connectedRT forKey:@"connectedRT"];
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
HttpdnsIpObject *copy = [[[self class] allocWithZone:zone] init];
|
||||
if (copy) {
|
||||
copy.ip = [self.ip copyWithZone:zone];
|
||||
copy.connectedRT = self.connectedRT;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
+ (NSArray<HttpdnsIpObject *> *)IPObjectsFromIPs:(NSArray<NSString *> *)IPs {
|
||||
NSMutableArray *IPObjects = [NSMutableArray arrayWithCapacity:IPs.count];
|
||||
for (NSString *IP in IPs) {
|
||||
HttpdnsIpObject *IPObject = [HttpdnsIpObject new];
|
||||
IPObject.ip = IP;
|
||||
IPObject.connectedRT = NSIntegerMax;
|
||||
[IPObjects addObject:IPObject];
|
||||
}
|
||||
return [IPObjects copy];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
if (self.connectedRT == NSIntegerMax) {
|
||||
return [NSString stringWithFormat:@"ip: %@", self.ip];
|
||||
} else {
|
||||
return [NSString stringWithFormat:@"ip: %@, connectedRT: %ld", self.ip, self.connectedRT];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HttpdnsHostObject
|
||||
|
||||
- (instancetype)init {
|
||||
_hostName = nil;
|
||||
_cacheKey = nil;
|
||||
_clientIp = nil;
|
||||
_v4ttl = -1;
|
||||
_lastIPv4LookupTime = 0;
|
||||
_v6ttl = -1;
|
||||
_lastIPv6LookupTime = 0;
|
||||
_isLoadFromDB = NO;
|
||||
_v4Ips = nil;
|
||||
_v6Ips = nil;
|
||||
_extra = nil;
|
||||
_hasNoIpv4Record = NO;
|
||||
_hasNoIpv6Record = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
if (self = [super init]) {
|
||||
_cacheKey = [aDecoder decodeObjectForKey:@"cacheKey"];
|
||||
_hostName = [aDecoder decodeObjectForKey:@"hostName"];
|
||||
_clientIp = [aDecoder decodeObjectForKey:@"clientIp"];
|
||||
_v4Ips = [aDecoder decodeObjectForKey:@"v4ips"];
|
||||
_v4ttl = [aDecoder decodeInt64ForKey:@"v4ttl"];
|
||||
_lastIPv4LookupTime = [aDecoder decodeInt64ForKey:@"lastIPv4LookupTime"];
|
||||
_v6Ips = [aDecoder decodeObjectForKey:@"v6ips"];
|
||||
_v6ttl = [aDecoder decodeInt64ForKey:@"v6ttl"];
|
||||
_lastIPv6LookupTime = [aDecoder decodeInt64ForKey:@"lastIPv6LookupTime"];
|
||||
_extra = [aDecoder decodeObjectForKey:@"extra"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeObject:_cacheKey forKey:@"cacheKey"];
|
||||
[aCoder encodeObject:_hostName forKey:@"hostName"];
|
||||
[aCoder encodeObject:_clientIp forKey:@"clientIp"];
|
||||
[aCoder encodeObject:_v4Ips forKey:@"v4ips"];
|
||||
[aCoder encodeInt64:_v4ttl forKey:@"v4ttl"];
|
||||
[aCoder encodeInt64:_lastIPv4LookupTime forKey:@"lastIPv4LookupTime"];
|
||||
[aCoder encodeObject:_v6Ips forKey:@"v6ips"];
|
||||
[aCoder encodeInt64:_v6ttl forKey:@"v6ttl"];
|
||||
[aCoder encodeInt64:_lastIPv6LookupTime forKey:@"lastIPv6LookupTime"];
|
||||
[aCoder encodeObject:_extra forKey:@"extra"];
|
||||
}
|
||||
|
||||
- (BOOL)isIpEmptyUnderQueryIpType:(HttpdnsQueryIPType)queryType {
|
||||
if (queryType & HttpdnsQueryIPTypeIpv4) {
|
||||
// 注意,_hasNoIpv4Record为true时,说明域名没有配置ipv4ip,不是需要去请求的情况
|
||||
if ([HttpdnsUtil isEmptyArray:[self getV4Ips]] && !_hasNoIpv4Record) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
} else if (queryType & HttpdnsQueryIPTypeIpv6 && !_hasNoIpv6Record) {
|
||||
// 注意,_hasNoIpv6Record为true时,说明域名没有配置ipv6ip,不是需要去请求的情况
|
||||
if ([HttpdnsUtil isEmptyArray:[self getV6Ips]] && !_hasNoIpv6Record) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
HttpdnsHostObject *copy = [[[self class] allocWithZone:zone] init];
|
||||
if (copy) {
|
||||
copy.cacheKey = [self.cacheKey copyWithZone:zone];
|
||||
copy.hostName = [self.hostName copyWithZone:zone];
|
||||
copy.clientIp = [self.clientIp copyWithZone:zone];
|
||||
copy.v4Ips = [[NSArray allocWithZone:zone] initWithArray:self.v4Ips copyItems:YES];
|
||||
copy.v6Ips = [[NSArray allocWithZone:zone] initWithArray:self.v6Ips copyItems:YES];
|
||||
copy.v4ttl = self.v4ttl;
|
||||
copy.lastIPv4LookupTime = self.lastIPv4LookupTime;
|
||||
copy.v6ttl = self.v6ttl;
|
||||
copy.lastIPv6LookupTime = self.lastIPv6LookupTime;
|
||||
copy.hasNoIpv4Record = self.hasNoIpv4Record;
|
||||
copy.hasNoIpv6Record = self.hasNoIpv6Record;
|
||||
copy.extra = [self.extra copyWithZone:zone];
|
||||
copy.isLoadFromDB = self.isLoadFromDB;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
- (BOOL)isExpiredUnderQueryIpType:(HttpdnsQueryIPType)queryIPType {
|
||||
int64_t currentEpoch = (int64_t)[[[NSDate alloc] init] timeIntervalSince1970];
|
||||
if ((queryIPType & HttpdnsQueryIPTypeIpv4)
|
||||
&& !_hasNoIpv4Record
|
||||
&& _lastIPv4LookupTime + _v4ttl <= currentEpoch) {
|
||||
return YES;
|
||||
}
|
||||
if ((queryIPType & HttpdnsQueryIPTypeIpv6)
|
||||
&& !_hasNoIpv6Record
|
||||
&& _lastIPv6LookupTime + _v6ttl <= currentEpoch) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (instancetype)fromDBRecord:(HttpdnsHostRecord *)hostRecord {
|
||||
HttpdnsHostObject *hostObject = [HttpdnsHostObject new];
|
||||
[hostObject setCacheKey:hostRecord.cacheKey];
|
||||
[hostObject setHostName:hostRecord.hostName];
|
||||
[hostObject setClientIp:hostRecord.clientIp];
|
||||
NSArray *v4ips = hostRecord.v4ips;
|
||||
NSArray *v6ips = hostRecord.v6ips;
|
||||
if ([HttpdnsUtil isNotEmptyArray:v4ips]) {
|
||||
[hostObject setV4Ips:[HttpdnsIpObject IPObjectsFromIPs:v4ips]];
|
||||
[hostObject setV4TTL:hostRecord.v4ttl];
|
||||
[hostObject setLastIPv4LookupTime:hostRecord.v4LookupTime];
|
||||
|
||||
}
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6ips]) {
|
||||
[hostObject setV6Ips:[HttpdnsIpObject IPObjectsFromIPs:v6ips]];
|
||||
[hostObject setV6TTL:hostRecord.v6ttl];
|
||||
[hostObject setLastIPv6LookupTime:hostRecord.v6LookupTime];
|
||||
}
|
||||
[hostObject setExtra:hostRecord.extra];
|
||||
[hostObject setIsLoadFromDB:YES];
|
||||
return hostObject;
|
||||
}
|
||||
|
||||
- (HttpdnsHostRecord *)toDBRecord {
|
||||
// 将IP对象数组转换为IP字符串数组
|
||||
NSArray<NSString *> *v4IpStrings = [self getV4IpStrings];
|
||||
NSArray<NSString *> *v6IpStrings = [self getV6IpStrings];
|
||||
|
||||
// 创建当前时间作为modifyAt
|
||||
NSDate *currentDate = [NSDate date];
|
||||
|
||||
// 使用hostName作为cacheKey,保持与fromDBRecord方法的一致性
|
||||
return [[HttpdnsHostRecord alloc] initWithId:0 // 数据库会自动分配ID
|
||||
cacheKey:self.cacheKey
|
||||
hostName:self.hostName
|
||||
createAt:currentDate
|
||||
modifyAt:currentDate
|
||||
clientIp:self.clientIp
|
||||
v4ips:v4IpStrings
|
||||
v4ttl:self.v4ttl
|
||||
v4LookupTime:self.lastIPv4LookupTime
|
||||
v6ips:v6IpStrings
|
||||
v6ttl:self.v6ttl
|
||||
v6LookupTime:self.lastIPv6LookupTime
|
||||
extra:self.extra];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)getV4IpStrings {
|
||||
NSArray<HttpdnsIpObject *> *ipv4Records = [self getV4Ips];
|
||||
NSMutableArray<NSString *> *ipv4Strings = [NSMutableArray arrayWithCapacity:ipv4Records.count];
|
||||
for (HttpdnsIpObject *IPObject in ipv4Records) {
|
||||
[ipv4Strings addObject:[IPObject getIpString]];
|
||||
}
|
||||
return [ipv4Strings copy];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)getV6IpStrings {
|
||||
NSArray<HttpdnsIpObject *> *ipv6Records = [self getV6Ips];
|
||||
NSMutableArray<NSString *> *ipv6sString = [NSMutableArray arrayWithCapacity:ipv6Records.count];
|
||||
for (HttpdnsIpObject *ipObject in ipv6Records) {
|
||||
[ipv6sString addObject:[ipObject getIpString]];
|
||||
}
|
||||
return [ipv6sString copy];
|
||||
}
|
||||
|
||||
- (void)updateConnectedRT:(NSInteger)connectedRT forIP:(NSString *)ip {
|
||||
if ([HttpdnsUtil isEmptyString:ip]) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isIPv6 = [HttpdnsUtil isIPv6Address:ip];
|
||||
|
||||
NSArray<HttpdnsIpObject *> *ipObjects = isIPv6 ? [self getV6Ips] : [self getV4Ips];
|
||||
if ([HttpdnsUtil isEmptyArray:ipObjects]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找匹配的IP对象并更新connectedRT
|
||||
BOOL found = NO;
|
||||
NSMutableArray<HttpdnsIpObject *> *mutableIpObjects = [ipObjects mutableCopy];
|
||||
|
||||
for (HttpdnsIpObject *ipObject in ipObjects) {
|
||||
if ([ipObject.ip isEqualToString:ip]) {
|
||||
ipObject.connectedRT = connectedRT;
|
||||
found = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据connectedRT值对IP列表进行排序,-1值放在最后
|
||||
[mutableIpObjects sortUsingComparator:^NSComparisonResult(HttpdnsIpObject *obj1, HttpdnsIpObject *obj2) {
|
||||
// 如果obj1的connectedRT为-1,将其排在后面
|
||||
if (obj1.connectedRT == -1) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
// 如果obj2的connectedRT为-1,将其排在后面
|
||||
if (obj2.connectedRT == -1) {
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
// 否则按照connectedRT值从小到大排序
|
||||
return obj1.connectedRT > obj2.connectedRT ? NSOrderedDescending : (obj1.connectedRT < obj2.connectedRT ? NSOrderedAscending : NSOrderedSame);
|
||||
}];
|
||||
|
||||
// 更新排序后的IP列表
|
||||
if (isIPv6) {
|
||||
[self setV6Ips:[mutableIpObjects copy]];
|
||||
} else {
|
||||
[self setV4Ips:[mutableIpObjects copy]];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
if (![HttpdnsUtil isNotEmptyArray:_v6Ips]) {
|
||||
return [NSString stringWithFormat:@"Host = %@ v4ips = %@ v4ttl = %lld v4LastLookup = %lld extra = %@",
|
||||
_hostName, _v4Ips, _v4ttl, _lastIPv4LookupTime, _extra];
|
||||
} else {
|
||||
return [NSString stringWithFormat:@"Host = %@ v4ips = %@ v4ttl = %lld v4LastLookup = %lld v6ips = %@ v6ttl = %lld v6LastLookup = %lld extra = %@",
|
||||
_hostName, _v4Ips, _v4ttl, _lastIPv4LookupTime, _v6Ips, _v6ttl, _lastIPv6LookupTime, _extra];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// HttpdnsHostRecord.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/5/3.
|
||||
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsHostRecord : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger id;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *cacheKey;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *hostName;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSDate *createAt;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSDate *modifyAt;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *clientIp;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSArray<NSString *> *v4ips;
|
||||
|
||||
@property (nonatomic, assign, readonly) int64_t v4ttl;
|
||||
|
||||
@property (nonatomic, assign, readonly) int64_t v4LookupTime;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSArray<NSString *> *v6ips;
|
||||
|
||||
@property (nonatomic, assign, readonly) int64_t v6ttl;
|
||||
|
||||
@property (nonatomic, assign, readonly) int64_t v6LookupTime;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *extra;
|
||||
|
||||
- (instancetype)initWithId:(NSUInteger)id
|
||||
cacheKey:(NSString *)cacheKey
|
||||
hostName:(NSString *)hostName
|
||||
createAt:(NSDate *)createAt
|
||||
modifyAt:(NSDate *)modifyAt
|
||||
clientIp:(NSString *)clientIp
|
||||
v4ips:(NSArray<NSString *> *)v4ips
|
||||
v4ttl:(int64_t)v4ttl
|
||||
v4LookupTime:(int64_t)v4LookupTime
|
||||
v6ips:(NSArray<NSString *> *)v6ips
|
||||
v6ttl:(int64_t)v6ttl
|
||||
v6LookupTime:(int64_t)v6LookupTime
|
||||
extra:(NSString *)extra;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// HttpdnsHostRecord.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/5/3.
|
||||
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsHostRecord.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
@interface HttpdnsHostRecord()
|
||||
|
||||
@property (nonatomic, assign) NSUInteger id;
|
||||
|
||||
@property (nonatomic, copy) NSString *cacheKey;
|
||||
|
||||
@property (nonatomic, copy) NSString *hostName;
|
||||
|
||||
@property (nonatomic, strong) NSDate *createAt;
|
||||
|
||||
@property (nonatomic, strong) NSDate *modifyAt;
|
||||
|
||||
@property (nonatomic, copy) NSString *clientIp;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *v4ips;
|
||||
|
||||
@property (nonatomic, assign) int64_t v4ttl;
|
||||
|
||||
@property (nonatomic, assign) int64_t v4LookupTime;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *v6ips;
|
||||
|
||||
@property (nonatomic, assign) int64_t v6ttl;
|
||||
|
||||
@property (nonatomic, assign) int64_t v6LookupTime;
|
||||
|
||||
@property (nonatomic, copy) NSString *extra;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HttpdnsHostRecord
|
||||
|
||||
- (instancetype)initWithId:(NSUInteger)id
|
||||
cacheKey:(NSString *)cacheKey
|
||||
hostName:(NSString *)hostName
|
||||
createAt:(NSDate *)createAt
|
||||
modifyAt:(NSDate *)modifyAt
|
||||
clientIp:(NSString *)clientIp
|
||||
v4ips:(NSArray<NSString *> *)v4ips
|
||||
v4ttl:(int64_t)v4ttl
|
||||
v4LookupTime:(int64_t)v4LookupTime
|
||||
v6ips:(NSArray<NSString *> *)v6ips
|
||||
v6ttl:(int64_t)v6ttl
|
||||
v6LookupTime:(int64_t)v6LookupTime
|
||||
extra:(NSString *)extra {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_id = id;
|
||||
_cacheKey = [cacheKey copy];
|
||||
_hostName = [hostName copy];
|
||||
_createAt = createAt;
|
||||
_modifyAt = modifyAt;
|
||||
_clientIp = [clientIp copy];
|
||||
_v4ips = [v4ips copy] ?: @[];
|
||||
_v4ttl = v4ttl;
|
||||
_v4LookupTime = v4LookupTime;
|
||||
_v6ips = [v6ips copy] ?: @[];
|
||||
_v6ttl = v6ttl;
|
||||
_v6LookupTime = v6LookupTime;
|
||||
_extra = [extra copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSString *hostName = self.hostName;
|
||||
if (self.cacheKey) {
|
||||
hostName = [NSString stringWithFormat:@"%@(%@)", hostName, self.cacheKey];
|
||||
}
|
||||
if ([HttpdnsUtil isEmptyArray:_v6ips]) {
|
||||
return [NSString stringWithFormat:@"hostName = %@, v4ips = %@, v4ttl = %lld v4LastLookup = %lld extra = %@",
|
||||
hostName, _v4ips, _v4ttl, _v4LookupTime, _extra];
|
||||
} else {
|
||||
return [NSString stringWithFormat:@"hostName = %@, v4ips = %@, v4ttl = %lld v4LastLookup = %lld v6ips = %@ v6ttl = %lld v6LastLookup = %lld extra = %@",
|
||||
hostName, _v4ips, _v4ttl, _v4LookupTime, _v6ips, _v6ttl, _v6LookupTime, _extra];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
64
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsRequest.h
Normal file
64
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsRequest.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// HttpdnsRequest.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/5/19.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef ALICLOUDHTTPDNSQUERYIPTYPE
|
||||
#define ALICLOUDHTTPDNSQUERYIPTYPE
|
||||
|
||||
typedef enum {
|
||||
AlicloudHttpDNS_IPTypeV4 = 0, //ipv4
|
||||
AlicloudHttpDNS_IPTypeV6 = 1, //ipv6
|
||||
AlicloudHttpDNS_IPTypeV64 = 2, //ipv4 + ipv6
|
||||
} AlicloudHttpDNS_IPType;
|
||||
|
||||
typedef NS_OPTIONS(NSUInteger, HttpdnsQueryIPType) {
|
||||
HttpdnsQueryIPTypeAuto NS_SWIFT_NAME(auto) = 0,
|
||||
HttpdnsQueryIPTypeIpv4 = 1 << 0,
|
||||
HttpdnsQueryIPTypeIpv6 = 1 << 1,
|
||||
HttpdnsQueryIPTypeBoth = HttpdnsQueryIPTypeIpv4 | HttpdnsQueryIPTypeIpv6,
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@interface HttpdnsRequest : NSObject
|
||||
|
||||
/// 需要解析的域名
|
||||
@property (nonatomic, copy) NSString *host;
|
||||
|
||||
/// 解析超时时间,对于同步接口,即为最大等待时间,对于异步接口,即为最大等待回调时间
|
||||
/// 默认值2秒,取值必须在0.5秒 - 5秒之间
|
||||
@property (nonatomic, assign) double resolveTimeoutInSecond;
|
||||
|
||||
/// 查询IP类型
|
||||
/// 默认为HttpdnsQueryIPTypeAuto,此类型下,SDK至少会请求解析ipv4地址,若判断到当前网络环境支持ipv6,则还会请求解析ipv6地址
|
||||
/// HttpdnsQueryIPTypeIpv4,只请求解析ipv4
|
||||
/// HttpdnsQueryIPTypeIpv6,只请求解析ipv6
|
||||
/// HttpdnsQueryIPTypeBoth,不管当前网络环境是什么,会尝试同时请求解析ipv4地址和ipv6地址,这种用法,通常需要拿到结果之后自行判断网络环境决定使用哪个结果
|
||||
@property (nonatomic, assign) HttpdnsQueryIPType queryIpType;
|
||||
|
||||
/// SDNS参数,针对软件自定义解析场景使用
|
||||
@property (nonatomic, copy, nullable) NSDictionary<NSString *, NSString *> *sdnsParams;
|
||||
|
||||
/// 缓存Key,针对软件自定义解析场景使用
|
||||
@property (nonatomic, copy, nullable) NSString *cacheKey;
|
||||
|
||||
/// 请求所属的账号ID,用于在多账号场景下定位实例
|
||||
@property (nonatomic, assign) NSInteger accountId;
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType sdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams cacheKey:(nullable NSString *)cacheKey;
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType sdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams cacheKey:(nullable NSString *)cacheKey resolveTimeout:(double)timeoutInSecond;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
76
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsRequest.m
Normal file
76
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsRequest.m
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// HttpdnsRequest.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/5/19.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsRequest.h"
|
||||
#import "HttpdnsRequest_Internal.h"
|
||||
|
||||
static double const RESOLVE_HOST_DEFAULT_TIMEOUT_IN_SEC = 2;
|
||||
static double const RESOLVE_HOST_MIN_TIMEOUT_IN_SEC = 0.5;
|
||||
static double const RESOLVE_HOST_MAX_TIMEOUT_IN_SEC = 5;
|
||||
|
||||
|
||||
@implementation HttpdnsRequest
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType {
|
||||
return [self initWithHost:host queryIpType:queryIpType sdnsParams:nil cacheKey:host];
|
||||
}
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType sdnsParams:(NSDictionary<NSString *, NSString *> *)sdnsParams cacheKey:(NSString *)cacheKey {
|
||||
return [self initWithHost:host queryIpType:queryIpType sdnsParams:sdnsParams cacheKey:cacheKey resolveTimeout:RESOLVE_HOST_DEFAULT_TIMEOUT_IN_SEC];
|
||||
}
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host queryIpType:(HttpdnsQueryIPType)queryIpType sdnsParams:(NSDictionary<NSString *,NSString *> *)sdnsParams cacheKey:(NSString *)cacheKey resolveTimeout:(double)timeoutInSecond {
|
||||
if (self = [super init]) {
|
||||
_host = host;
|
||||
_queryIpType = queryIpType;
|
||||
_sdnsParams = sdnsParams;
|
||||
|
||||
if (cacheKey) {
|
||||
_cacheKey = cacheKey;
|
||||
} else {
|
||||
_cacheKey = host;
|
||||
}
|
||||
|
||||
_resolveTimeoutInSecond = timeoutInSecond;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_queryIpType = HttpdnsQueryIPTypeAuto;
|
||||
_resolveTimeoutInSecond = RESOLVE_HOST_DEFAULT_TIMEOUT_IN_SEC;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)becomeBlockingRequest {
|
||||
_isBlockingRequest = YES;
|
||||
}
|
||||
|
||||
- (void)becomeNonBlockingRequest {
|
||||
_isBlockingRequest = NO;
|
||||
}
|
||||
|
||||
- (void)ensureResolveTimeoutInReasonableRange {
|
||||
if (_resolveTimeoutInSecond == 0) {
|
||||
_resolveTimeoutInSecond = RESOLVE_HOST_DEFAULT_TIMEOUT_IN_SEC;
|
||||
} else if (_resolveTimeoutInSecond < RESOLVE_HOST_MIN_TIMEOUT_IN_SEC) {
|
||||
_resolveTimeoutInSecond = RESOLVE_HOST_MIN_TIMEOUT_IN_SEC;
|
||||
} else if (_resolveTimeoutInSecond > RESOLVE_HOST_MAX_TIMEOUT_IN_SEC) {
|
||||
_resolveTimeoutInSecond = RESOLVE_HOST_MAX_TIMEOUT_IN_SEC;
|
||||
} else {
|
||||
// 在范围内的正常值
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"Host: %@, isBlockingRequest: %d, queryIpType: %ld, sdnsParams: %@, cacheKey: %@", self.host, self.isBlockingRequest, self.queryIpType, self.sdnsParams, self.cacheKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// HttpdnsRequest_Internal.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/6/19.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef HttpdnsRequest_Internal_h
|
||||
#define HttpdnsRequest_Internal_h
|
||||
|
||||
|
||||
@interface HttpdnsRequest ()
|
||||
|
||||
@property (nonatomic, assign) BOOL isBlockingRequest;
|
||||
|
||||
- (void)becomeBlockingRequest;
|
||||
|
||||
- (void)becomeNonBlockingRequest;
|
||||
|
||||
- (void)ensureResolveTimeoutInReasonableRange;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HttpdnsRequest_Internal_h */
|
||||
42
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsResult.h
Normal file
42
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsResult.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// HttpdnsResult.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/5/15.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsResult : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *host;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ips;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv6s;
|
||||
|
||||
// 最后一次ipv4地址更新时间戳,Unix时间,单位秒
|
||||
@property (nonatomic, assign) NSTimeInterval lastUpdatedTimeInterval;
|
||||
|
||||
// 最后一次ipv6地址更新时间戳,Unix时间,单位秒
|
||||
@property (nonatomic, assign) NSTimeInterval v6LastUpdatedTimeInterval;
|
||||
|
||||
// 对应ipv4的ttl,单位秒
|
||||
@property (nonatomic, assign) NSTimeInterval ttl;
|
||||
|
||||
// 对应ipv6的ttl,单位秒
|
||||
@property (nonatomic, assign) NSTimeInterval v6ttl;
|
||||
|
||||
- (BOOL)hasIpv4Address;
|
||||
|
||||
- (BOOL)hasIpv6Address;
|
||||
|
||||
- (nullable NSString *)firstIpv4Address;
|
||||
|
||||
- (nullable NSString *)firstIpv6Address;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
53
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsResult.m
Normal file
53
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Model/HttpdnsResult.m
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// HttpdnsResult.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/5/15.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsResult.h"
|
||||
|
||||
@implementation HttpdnsResult
|
||||
|
||||
- (BOOL)hasIpv4Address {
|
||||
return self.ips.count > 0;
|
||||
}
|
||||
|
||||
- (BOOL)hasIpv6Address {
|
||||
return self.ipv6s.count > 0;
|
||||
}
|
||||
|
||||
- (nullable NSString *)firstIpv4Address {
|
||||
if (self.ips.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
return self.ips.firstObject;
|
||||
}
|
||||
|
||||
- (nullable NSString *)firstIpv6Address {
|
||||
if (self.ipv6s.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
return self.ipv6s.firstObject;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSMutableString *result = [NSMutableString stringWithFormat:@"Host: %@", self.host];
|
||||
|
||||
if ([self hasIpv4Address]) {
|
||||
[result appendFormat:@", ipv4 Addresses: %@", [self.ips componentsJoinedByString:@", "]];
|
||||
} else {
|
||||
[result appendString:@", ipv4 Addresses: None"];
|
||||
}
|
||||
|
||||
if ([self hasIpv6Address]) {
|
||||
[result appendFormat:@", ipv6 Addresses: %@", [self.ipv6s componentsJoinedByString:@", "]];
|
||||
} else {
|
||||
[result appendString:@", ipv6 Addresses: None"];
|
||||
}
|
||||
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class HttpdnsNWReusableConnection;
|
||||
|
||||
@interface HttpdnsNWHTTPClientResponse : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSInteger statusCode;
|
||||
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *headers;
|
||||
@property (nonatomic, strong) NSData *body;
|
||||
|
||||
@end
|
||||
|
||||
@interface HttpdnsNWHTTPClient : NSObject
|
||||
|
||||
/// 全局共享实例,复用底层连接池;线程安全
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (nullable HttpdnsNWHTTPClientResponse *)performRequestWithURLString:(NSString *)urlString
|
||||
userAgent:(NSString *)userAgent
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#if DEBUG
|
||||
@interface HttpdnsNWHTTPClient (TestInspection)
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger connectionCreationCount;
|
||||
@property (nonatomic, assign, readonly) NSUInteger connectionReuseCount;
|
||||
|
||||
- (NSUInteger)connectionPoolCountForKey:(NSString *)key;
|
||||
- (NSArray<NSString *> *)allConnectionPoolKeys;
|
||||
- (NSUInteger)totalConnectionCount;
|
||||
- (void)resetPoolStatistics;
|
||||
- (NSArray<HttpdnsNWReusableConnection *> *)connectionsInPoolForKey:(NSString *)key;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,834 @@
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
#import "HttpdnsNWReusableConnection.h"
|
||||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||||
|
||||
#import <Network/Network.h>
|
||||
#import <Security/SecCertificate.h>
|
||||
#import <Security/SecPolicy.h>
|
||||
#import <Security/SecTrust.h>
|
||||
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClientResponse ()
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClientResponse
|
||||
@end
|
||||
|
||||
static const NSUInteger kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey = 4;
|
||||
static const NSTimeInterval kHttpdnsNWHTTPClientIdleConnectionTimeout = 30.0;
|
||||
static const NSTimeInterval kHttpdnsNWHTTPClientDefaultTimeout = 10.0;
|
||||
|
||||
// decoupled reusable connection implementation moved to HttpdnsNWReusableConnection.{h,m}
|
||||
|
||||
@interface HttpdnsNWHTTPClient ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<HttpdnsNWReusableConnection *> *> *connectionPool;
|
||||
@property (nonatomic, strong) dispatch_queue_t poolQueue;
|
||||
|
||||
#if DEBUG
|
||||
// 测试专用统计计数器
|
||||
@property (atomic, assign) NSUInteger connectionCreationCount;
|
||||
@property (atomic, assign) NSUInteger connectionReuseCount;
|
||||
#endif
|
||||
|
||||
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS;
|
||||
- (HttpdnsNWReusableConnection *)dequeueConnectionForHost:(NSString *)host
|
||||
port:(NSString *)port
|
||||
useTLS:(BOOL)useTLS
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error;
|
||||
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
|
||||
forKey:(NSString *)key
|
||||
shouldClose:(BOOL)shouldClose;
|
||||
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool
|
||||
referenceDate:(NSDate *)referenceDate;
|
||||
- (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent;
|
||||
- (BOOL)parseHTTPResponseData:(NSData *)data
|
||||
statusCode:(NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||||
body:(NSData *__autoreleasing *)body
|
||||
error:(NSError **)error;
|
||||
- (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data
|
||||
headerEndIndex:(NSUInteger *)headerEndIndex
|
||||
statusCode:(NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||||
error:(NSError **)error;
|
||||
- (HttpdnsHTTPChunkParseResult)checkChunkedBodyCompletionInData:(NSData *)data
|
||||
headerEndIndex:(NSUInteger)headerEndIndex
|
||||
error:(NSError **)error;
|
||||
- (NSData *)decodeChunkedBody:(NSData *)bodyData error:(NSError **)error;
|
||||
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain;
|
||||
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
// 使用 dispatch_once 保证单例创建的线程安全与唯一性;复用连接池降低连接开销
|
||||
static dispatch_once_t onceToken;
|
||||
static HttpdnsNWHTTPClient *instance = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_poolQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.network.pool", DISPATCH_QUEUE_SERIAL);
|
||||
_connectionPool = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable HttpdnsNWHTTPClientResponse *)performRequestWithURLString:(NSString *)urlString
|
||||
userAgent:(NSString *)userAgent
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error {
|
||||
HttpdnsLogDebug("Send Network.framework request URL: %@", urlString);
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
if (!url) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid resolve URL"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSTimeInterval requestTimeout = timeout > 0 ? timeout : kHttpdnsNWHTTPClientDefaultTimeout;
|
||||
|
||||
NSString *host = url.host;
|
||||
if (![HttpdnsUtil isNotEmptyString:host]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Missing host in resolve URL"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL useTLS = [[url.scheme lowercaseString] isEqualToString:@"https"];
|
||||
NSString *portString = url.port ? url.port.stringValue : (useTLS ? @"443" : @"80");
|
||||
|
||||
NSString *requestString = [self buildHTTPRequestStringWithURL:url userAgent:userAgent];
|
||||
NSData *requestData = [requestString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if (!requestData) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to encode HTTP request"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *connectionError = nil;
|
||||
HttpdnsNWReusableConnection *connection = [self dequeueConnectionForHost:host
|
||||
port:portString
|
||||
useTLS:useTLS
|
||||
timeout:requestTimeout
|
||||
error:&connectionError];
|
||||
if (!connection) {
|
||||
if (error) {
|
||||
*error = connectionError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Unable to obtain network connection"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *poolKey = [self connectionPoolKeyForHost:host port:portString useTLS:useTLS];
|
||||
BOOL remoteClosed = NO;
|
||||
NSError *exchangeError = nil;
|
||||
NSData *rawResponse = [connection sendRequestData:requestData
|
||||
timeout:requestTimeout
|
||||
remoteConnectionClosed:&remoteClosed
|
||||
error:&exchangeError];
|
||||
|
||||
if (!rawResponse) {
|
||||
[self returnConnection:connection forKey:poolKey shouldClose:YES];
|
||||
if (error) {
|
||||
*error = exchangeError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Network request failed"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSInteger statusCode = 0;
|
||||
NSDictionary<NSString *, NSString *> *headers = nil;
|
||||
NSData *bodyData = nil;
|
||||
NSError *parseError = nil;
|
||||
if (![self parseHTTPResponseData:rawResponse statusCode:&statusCode headers:&headers body:&bodyData error:&parseError]) {
|
||||
[self returnConnection:connection forKey:poolKey shouldClose:YES];
|
||||
if (error) {
|
||||
*error = parseError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse HTTP response"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL shouldClose = remoteClosed;
|
||||
NSString *connectionHeader = headers[@"connection"];
|
||||
if ([HttpdnsUtil isNotEmptyString:connectionHeader] && [connectionHeader rangeOfString:@"close" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||||
shouldClose = YES;
|
||||
}
|
||||
NSString *proxyConnectionHeader = headers[@"proxy-connection"];
|
||||
if (!shouldClose && [HttpdnsUtil isNotEmptyString:proxyConnectionHeader] && [proxyConnectionHeader rangeOfString:@"close" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||||
shouldClose = YES;
|
||||
}
|
||||
|
||||
[self returnConnection:connection forKey:poolKey shouldClose:shouldClose];
|
||||
|
||||
HttpdnsNWHTTPClientResponse *response = [HttpdnsNWHTTPClientResponse new];
|
||||
response.statusCode = statusCode;
|
||||
response.headers = headers ?: @{};
|
||||
response.body = bodyData ?: [NSData data];
|
||||
return response;
|
||||
}
|
||||
|
||||
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS {
|
||||
NSString *safeHost = host ?: @"";
|
||||
NSString *safePort = port ?: @"";
|
||||
return [NSString stringWithFormat:@"%@:%@:%@", safeHost, safePort, useTLS ? @"tls" : @"tcp"];
|
||||
}
|
||||
|
||||
- (HttpdnsNWReusableConnection *)dequeueConnectionForHost:(NSString *)host
|
||||
port:(NSString *)port
|
||||
useTLS:(BOOL)useTLS
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error {
|
||||
NSString *key = [self connectionPoolKeyForHost:host port:port useTLS:useTLS];
|
||||
NSDate *now = [NSDate date];
|
||||
__block HttpdnsNWReusableConnection *connection = nil;
|
||||
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
if (!pool) {
|
||||
pool = [NSMutableArray array];
|
||||
self.connectionPool[key] = pool;
|
||||
}
|
||||
[self pruneConnectionPool:pool referenceDate:now];
|
||||
for (HttpdnsNWReusableConnection *candidate in pool) {
|
||||
if (!candidate.inUse && [candidate isViable]) {
|
||||
candidate.inUse = YES;
|
||||
candidate.lastUsedDate = now;
|
||||
connection = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (connection) {
|
||||
#if DEBUG
|
||||
self.connectionReuseCount++;
|
||||
#endif
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpdnsNWReusableConnection *newConnection = [[HttpdnsNWReusableConnection alloc] initWithClient:self
|
||||
host:host
|
||||
port:port
|
||||
useTLS:useTLS];
|
||||
if (!newConnection) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to create network connection"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![newConnection openWithTimeout:timeout error:error]) {
|
||||
[newConnection invalidate];
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
self.connectionCreationCount++;
|
||||
#endif
|
||||
|
||||
newConnection.inUse = YES;
|
||||
newConnection.lastUsedDate = now;
|
||||
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
if (!pool) {
|
||||
pool = [NSMutableArray array];
|
||||
self.connectionPool[key] = pool;
|
||||
}
|
||||
[pool addObject:newConnection];
|
||||
[self pruneConnectionPool:pool referenceDate:[NSDate date]];
|
||||
});
|
||||
|
||||
return newConnection;
|
||||
}
|
||||
|
||||
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
|
||||
forKey:(NSString *)key
|
||||
shouldClose:(BOOL)shouldClose {
|
||||
if (!connection || !key) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSDate *now = [NSDate date];
|
||||
dispatch_async(self.poolQueue, ^{
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
if (!pool) {
|
||||
pool = [NSMutableArray array];
|
||||
self.connectionPool[key] = pool;
|
||||
}
|
||||
|
||||
if (shouldClose || connection.isInvalidated) {
|
||||
[connection invalidate];
|
||||
[pool removeObject:connection];
|
||||
} else {
|
||||
connection.inUse = NO;
|
||||
connection.lastUsedDate = now;
|
||||
if (![pool containsObject:connection]) {
|
||||
[pool addObject:connection];
|
||||
}
|
||||
[self pruneConnectionPool:pool referenceDate:now];
|
||||
}
|
||||
|
||||
if (pool.count == 0) {
|
||||
[self.connectionPool removeObjectForKey:key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool referenceDate:(NSDate *)referenceDate {
|
||||
if (!pool || pool.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval idleLimit = kHttpdnsNWHTTPClientIdleConnectionTimeout;
|
||||
for (NSInteger idx = (NSInteger)pool.count - 1; idx >= 0; idx--) {
|
||||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)idx];
|
||||
if (!candidate) {
|
||||
[pool removeObjectAtIndex:(NSUInteger)idx];
|
||||
continue;
|
||||
}
|
||||
NSDate *lastUsed = candidate.lastUsedDate ?: [NSDate distantPast];
|
||||
BOOL expired = !candidate.inUse && referenceDate && [referenceDate timeIntervalSinceDate:lastUsed] > idleLimit;
|
||||
if (candidate.isInvalidated || expired) {
|
||||
[candidate invalidate];
|
||||
[pool removeObjectAtIndex:(NSUInteger)idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (pool.count <= kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey) {
|
||||
NSInteger removeIndex = NSNotFound;
|
||||
NSDate *oldestDate = nil;
|
||||
for (NSInteger idx = 0; idx < (NSInteger)pool.count; idx++) {
|
||||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)idx];
|
||||
if (candidate.inUse) {
|
||||
continue;
|
||||
}
|
||||
NSDate *candidateDate = candidate.lastUsedDate ?: [NSDate distantPast];
|
||||
if (!oldestDate || [candidateDate compare:oldestDate] == NSOrderedAscending) {
|
||||
oldestDate = candidateDate;
|
||||
removeIndex = idx;
|
||||
}
|
||||
}
|
||||
if (removeIndex == NSNotFound) {
|
||||
break;
|
||||
}
|
||||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)removeIndex];
|
||||
[candidate invalidate];
|
||||
[pool removeObjectAtIndex:(NSUInteger)removeIndex];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent {
|
||||
NSString *pathComponent = url.path.length > 0 ? url.path : @"/";
|
||||
NSMutableString *path = [NSMutableString stringWithString:pathComponent];
|
||||
if (url.query.length > 0) {
|
||||
[path appendFormat:@"?%@", url.query];
|
||||
}
|
||||
|
||||
BOOL isTLS = [[url.scheme lowercaseString] isEqualToString:@"https"];
|
||||
NSInteger portValue = url.port ? url.port.integerValue : (isTLS ? 443 : 80);
|
||||
BOOL isDefaultPort = (!url.port) || (isTLS && portValue == 443) || (!isTLS && portValue == 80);
|
||||
|
||||
NSMutableString *hostHeader = [NSMutableString stringWithString:url.host ?: @""];
|
||||
if (!isDefaultPort && url.port) {
|
||||
[hostHeader appendFormat:@":%@", url.port];
|
||||
}
|
||||
|
||||
NSMutableString *request = [NSMutableString stringWithFormat:@"GET %@ HTTP/1.1\r\n", path];
|
||||
[request appendFormat:@"Host: %@\r\n", hostHeader];
|
||||
if ([HttpdnsUtil isNotEmptyString:userAgent]) {
|
||||
[request appendFormat:@"User-Agent: %@\r\n", userAgent];
|
||||
}
|
||||
[request appendString:@"Accept: application/json\r\n"];
|
||||
[request appendString:@"Accept-Encoding: identity\r\n"];
|
||||
[request appendString:@"Connection: keep-alive\r\n\r\n"];
|
||||
return request;
|
||||
}
|
||||
|
||||
- (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data
|
||||
headerEndIndex:(NSUInteger *)headerEndIndex
|
||||
statusCode:(NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||||
error:(NSError **)error {
|
||||
if (!data || data.length == 0) {
|
||||
return HttpdnsHTTPHeaderParseResultIncomplete;
|
||||
}
|
||||
|
||||
const uint8_t *bytes = data.bytes;
|
||||
NSUInteger length = data.length;
|
||||
NSUInteger headerEnd = NSNotFound;
|
||||
for (NSUInteger idx = 0; idx + 3 < length; idx++) {
|
||||
if (bytes[idx] == '\r' && bytes[idx + 1] == '\n' && bytes[idx + 2] == '\r' && bytes[idx + 3] == '\n') {
|
||||
headerEnd = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerEnd == NSNotFound) {
|
||||
return HttpdnsHTTPHeaderParseResultIncomplete;
|
||||
}
|
||||
|
||||
if (headerEndIndex) {
|
||||
*headerEndIndex = headerEnd;
|
||||
}
|
||||
|
||||
NSData *headerData = [data subdataWithRange:NSMakeRange(0, headerEnd)];
|
||||
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
||||
if (![HttpdnsUtil isNotEmptyString:headerString]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to decode HTTP headers"}];
|
||||
}
|
||||
return HttpdnsHTTPHeaderParseResultError;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
||||
if (lines.count == 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Missing HTTP status line"}];
|
||||
}
|
||||
return HttpdnsHTTPHeaderParseResultError;
|
||||
}
|
||||
|
||||
NSString *statusLine = lines.firstObject;
|
||||
NSArray<NSString *> *statusParts = [statusLine componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
NSMutableArray<NSString *> *filteredParts = [NSMutableArray array];
|
||||
for (NSString *component in statusParts) {
|
||||
if (component.length > 0) {
|
||||
[filteredParts addObject:component];
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredParts.count < 2) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid HTTP status line"}];
|
||||
}
|
||||
return HttpdnsHTTPHeaderParseResultError;
|
||||
}
|
||||
|
||||
NSInteger localStatus = [filteredParts[1] integerValue];
|
||||
if (localStatus <= 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid HTTP status code"}];
|
||||
}
|
||||
return HttpdnsHTTPHeaderParseResultError;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, NSString *> *headerDict = [NSMutableDictionary dictionary];
|
||||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
for (NSUInteger idx = 1; idx < lines.count; idx++) {
|
||||
NSString *line = lines[idx];
|
||||
if (line.length == 0) {
|
||||
continue;
|
||||
}
|
||||
NSRange colonRange = [line rangeOfString:@":"];
|
||||
if (colonRange.location == NSNotFound) {
|
||||
continue;
|
||||
}
|
||||
NSString *key = [[line substringToIndex:colonRange.location] stringByTrimmingCharactersInSet:trimSet];
|
||||
NSString *value = [[line substringFromIndex:colonRange.location + 1] stringByTrimmingCharactersInSet:trimSet];
|
||||
if (key.length > 0) {
|
||||
headerDict[[key lowercaseString]] = value ?: @"";
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode) {
|
||||
*statusCode = localStatus;
|
||||
}
|
||||
if (headers) {
|
||||
*headers = [headerDict copy];
|
||||
}
|
||||
return HttpdnsHTTPHeaderParseResultSuccess;
|
||||
}
|
||||
|
||||
- (HttpdnsHTTPChunkParseResult)checkChunkedBodyCompletionInData:(NSData *)data
|
||||
headerEndIndex:(NSUInteger)headerEndIndex
|
||||
error:(NSError **)error {
|
||||
if (!data || headerEndIndex == NSNotFound) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
|
||||
NSUInteger length = data.length;
|
||||
NSUInteger cursor = headerEndIndex + 4;
|
||||
if (cursor > length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
|
||||
const uint8_t *bytes = data.bytes;
|
||||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
|
||||
while (cursor < length) {
|
||||
NSUInteger lineEnd = cursor;
|
||||
while (lineEnd + 1 < length && !(bytes[lineEnd] == '\r' && bytes[lineEnd + 1] == '\n')) {
|
||||
lineEnd++;
|
||||
}
|
||||
if (lineEnd + 1 >= length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
|
||||
NSData *sizeData = [data subdataWithRange:NSMakeRange(cursor, lineEnd - cursor)];
|
||||
NSString *sizeString = [[NSString alloc] initWithData:sizeData encoding:NSUTF8StringEncoding];
|
||||
if (![HttpdnsUtil isNotEmptyString:sizeString]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||||
}
|
||||
return HttpdnsHTTPChunkParseResultError;
|
||||
}
|
||||
|
||||
NSString *trimmed = [[sizeString componentsSeparatedByString:@";"] firstObject];
|
||||
trimmed = [trimmed stringByTrimmingCharactersInSet:trimSet];
|
||||
const char *cStr = trimmed.UTF8String;
|
||||
char *endPtr = NULL;
|
||||
unsigned long long chunkSize = strtoull(cStr, &endPtr, 16);
|
||||
if (endPtr == NULL || endPtr == cStr) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||||
}
|
||||
return HttpdnsHTTPChunkParseResultError;
|
||||
}
|
||||
|
||||
if (chunkSize > NSUIntegerMax - cursor) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Chunk size overflow"}];
|
||||
}
|
||||
return HttpdnsHTTPChunkParseResultError;
|
||||
}
|
||||
|
||||
cursor = lineEnd + 2;
|
||||
if (chunkSize == 0) {
|
||||
NSUInteger trailerCursor = cursor;
|
||||
while (YES) {
|
||||
if (trailerCursor + 1 >= length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
NSUInteger trailerLineEnd = trailerCursor;
|
||||
while (trailerLineEnd + 1 < length && !(bytes[trailerLineEnd] == '\r' && bytes[trailerLineEnd + 1] == '\n')) {
|
||||
trailerLineEnd++;
|
||||
}
|
||||
if (trailerLineEnd + 1 >= length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
if (trailerLineEnd == trailerCursor) {
|
||||
return HttpdnsHTTPChunkParseResultSuccess;
|
||||
}
|
||||
trailerCursor = trailerLineEnd + 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor + (NSUInteger)chunkSize > length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
cursor += (NSUInteger)chunkSize;
|
||||
if (cursor + 1 >= length) {
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
if (bytes[cursor] != '\r' || bytes[cursor + 1] != '\n') {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk terminator"}];
|
||||
}
|
||||
return HttpdnsHTTPChunkParseResultError;
|
||||
}
|
||||
cursor += 2;
|
||||
}
|
||||
|
||||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||||
}
|
||||
|
||||
- (BOOL)parseHTTPResponseData:(NSData *)data
|
||||
statusCode:(NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||||
body:(NSData *__autoreleasing *)body
|
||||
error:(NSError **)error {
|
||||
if (!data || data.length == 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Empty HTTP response"}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSUInteger headerEnd = NSNotFound;
|
||||
NSInteger localStatus = 0;
|
||||
NSDictionary<NSString *, NSString *> *headerDict = nil;
|
||||
NSError *headerError = nil;
|
||||
HttpdnsHTTPHeaderParseResult headerResult = [self tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEnd
|
||||
statusCode:&localStatus
|
||||
headers:&headerDict
|
||||
error:&headerError];
|
||||
if (headerResult != HttpdnsHTTPHeaderParseResultSuccess) {
|
||||
if (error) {
|
||||
if (headerResult == HttpdnsHTTPHeaderParseResultIncomplete) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Missing HTTP header terminator"}];
|
||||
} else {
|
||||
*error = headerError;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSUInteger bodyStart = headerEnd + 4;
|
||||
NSData *bodyData = bodyStart <= data.length ? [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)] : [NSData data];
|
||||
|
||||
NSString *transferEncoding = headerDict[@"transfer-encoding"];
|
||||
if ([HttpdnsUtil isNotEmptyString:transferEncoding] && [transferEncoding rangeOfString:@"chunked" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||||
NSError *chunkError = nil;
|
||||
NSData *decoded = [self decodeChunkedBody:bodyData error:&chunkError];
|
||||
if (!decoded) {
|
||||
HttpdnsLogDebug("Chunked decode failed, fallback to raw body, error: %@", chunkError);
|
||||
decoded = bodyData;
|
||||
}
|
||||
bodyData = decoded;
|
||||
} else {
|
||||
NSString *contentLengthValue = headerDict[@"content-length"];
|
||||
if ([HttpdnsUtil isNotEmptyString:contentLengthValue]) {
|
||||
long long expected = [contentLengthValue longLongValue];
|
||||
if (expected >= 0 && (NSUInteger)expected != bodyData.length) {
|
||||
HttpdnsLogDebug("Content-Length mismatch, expected: %lld, actual: %lu", expected, (unsigned long)bodyData.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode) {
|
||||
*statusCode = localStatus;
|
||||
}
|
||||
if (headers) {
|
||||
*headers = headerDict ?: @{};
|
||||
}
|
||||
if (body) {
|
||||
*body = bodyData;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSData *)decodeChunkedBody:(NSData *)bodyData error:(NSError **)error {
|
||||
if (!bodyData) {
|
||||
return [NSData data];
|
||||
}
|
||||
|
||||
const uint8_t *bytes = bodyData.bytes;
|
||||
NSUInteger length = bodyData.length;
|
||||
NSUInteger cursor = 0;
|
||||
NSMutableData *decoded = [NSMutableData data];
|
||||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
|
||||
while (cursor < length) {
|
||||
NSUInteger lineEnd = cursor;
|
||||
while (lineEnd + 1 < length && !(bytes[lineEnd] == '\r' && bytes[lineEnd + 1] == '\n')) {
|
||||
lineEnd++;
|
||||
}
|
||||
if (lineEnd + 1 >= length) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunked encoding"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *sizeData = [bodyData subdataWithRange:NSMakeRange(cursor, lineEnd - cursor)];
|
||||
NSString *sizeString = [[NSString alloc] initWithData:sizeData encoding:NSUTF8StringEncoding];
|
||||
if (![HttpdnsUtil isNotEmptyString:sizeString]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSString *trimmed = [[sizeString componentsSeparatedByString:@";"] firstObject];
|
||||
trimmed = [trimmed stringByTrimmingCharactersInSet:trimSet];
|
||||
const char *cStr = trimmed.UTF8String;
|
||||
char *endPtr = NULL;
|
||||
unsigned long chunkSize = strtoul(cStr, &endPtr, 16);
|
||||
// 检查是否是无效的十六进制字符串
|
||||
if (endPtr == cStr) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size format"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
cursor = lineEnd + 2;
|
||||
if (chunkSize == 0) {
|
||||
if (cursor + 1 < length) {
|
||||
cursor += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cursor + chunkSize > length) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Chunk size exceeds buffer"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
[decoded appendBytes:bytes + cursor length:chunkSize];
|
||||
cursor += chunkSize;
|
||||
if (cursor + 1 >= length || bytes[cursor] != '\r' || bytes[cursor + 1] != '\n') {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk terminator"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
cursor += 2;
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
|
||||
// 测试专用:通过环境变量跳过 TLS 验证
|
||||
// 仅在设置 HTTPDNS_SKIP_TLS_VERIFY 环境变量时生效(用于本地 mock server 测试)
|
||||
if (getenv("HTTPDNS_SKIP_TLS_VERIFY") != NULL) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// 生产环境标准 TLS 验证流程
|
||||
NSMutableArray *policies = [NSMutableArray array];
|
||||
if (domain) {
|
||||
[policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
|
||||
} else {
|
||||
[policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
|
||||
}
|
||||
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef) policies);
|
||||
SecTrustResultType result;
|
||||
SecTrustEvaluate(serverTrust, &result);
|
||||
if (result == kSecTrustResultRecoverableTrustFailure) {
|
||||
CFDataRef errDataRef = SecTrustCopyExceptions(serverTrust);
|
||||
SecTrustSetExceptions(serverTrust, errDataRef);
|
||||
SecTrustEvaluate(serverTrust, &result);
|
||||
}
|
||||
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
|
||||
}
|
||||
|
||||
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description {
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
if ([HttpdnsUtil isNotEmptyString:description]) {
|
||||
userInfo[NSLocalizedDescriptionKey] = description;
|
||||
}
|
||||
if (nwError) {
|
||||
CFErrorRef cfError = nw_error_copy_cf_error(nwError);
|
||||
if (cfError) {
|
||||
NSError *underlyingError = CFBridgingRelease(cfError);
|
||||
if (underlyingError) {
|
||||
userInfo[NSUnderlyingErrorKey] = underlyingError;
|
||||
if (!userInfo[NSLocalizedDescriptionKey] && underlyingError.localizedDescription) {
|
||||
userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!userInfo[NSLocalizedDescriptionKey]) {
|
||||
userInfo[NSLocalizedDescriptionKey] = @"Network operation failed";
|
||||
}
|
||||
return [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:userInfo];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#if DEBUG
|
||||
// 测试专用:连接池检查 API 实现
|
||||
@implementation HttpdnsNWHTTPClient (TestInspection)
|
||||
|
||||
- (NSUInteger)connectionPoolCountForKey:(NSString *)key {
|
||||
__block NSUInteger count = 0;
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
count = pool ? pool.count : 0;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)allConnectionPoolKeys {
|
||||
__block NSArray<NSString *> *keys = nil;
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
keys = [self.connectionPool.allKeys copy];
|
||||
});
|
||||
return keys ?: @[];
|
||||
}
|
||||
|
||||
- (NSUInteger)totalConnectionCount {
|
||||
__block NSUInteger total = 0;
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
for (NSMutableArray *pool in self.connectionPool.allValues) {
|
||||
total += pool.count;
|
||||
}
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
- (void)resetPoolStatistics {
|
||||
self.connectionCreationCount = 0;
|
||||
self.connectionReuseCount = 0;
|
||||
}
|
||||
|
||||
// 获取指定池中的所有连接(用于状态检查)
|
||||
- (NSArray<HttpdnsNWReusableConnection *> *)connectionsInPoolForKey:(NSString *)key {
|
||||
__block NSArray<HttpdnsNWReusableConnection *> *connections = nil;
|
||||
dispatch_sync(self.poolQueue, ^{
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
connections = pool ? [pool copy] : @[];
|
||||
});
|
||||
return connections;
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
// Internal helpers for NW HTTP client
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Network/Network.h>
|
||||
#import <Security/SecTrust.h>
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, HttpdnsHTTPHeaderParseResult) {
|
||||
HttpdnsHTTPHeaderParseResultIncomplete = 0,
|
||||
HttpdnsHTTPHeaderParseResultSuccess,
|
||||
HttpdnsHTTPHeaderParseResultError,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, HttpdnsHTTPChunkParseResult) {
|
||||
HttpdnsHTTPChunkParseResultIncomplete = 0,
|
||||
HttpdnsHTTPChunkParseResultSuccess,
|
||||
HttpdnsHTTPChunkParseResultError,
|
||||
};
|
||||
|
||||
@interface HttpdnsNWHTTPClient (Internal)
|
||||
|
||||
// TLS 验证
|
||||
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain;
|
||||
|
||||
// HTTP 头部解析
|
||||
- (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data
|
||||
headerEndIndex:(nullable NSUInteger *)headerEndIndex
|
||||
statusCode:(nullable NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing _Nullable * _Nullable)headers
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
// Chunked 编码检查
|
||||
- (HttpdnsHTTPChunkParseResult)checkChunkedBodyCompletionInData:(NSData *)data
|
||||
headerEndIndex:(NSUInteger)headerEndIndex
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
// Chunked 编码解码
|
||||
- (nullable NSData *)decodeChunkedBody:(NSData *)bodyData error:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
// 完整 HTTP 响应解析
|
||||
- (BOOL)parseHTTPResponseData:(NSData *)data
|
||||
statusCode:(nullable NSInteger *)statusCode
|
||||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing _Nullable * _Nullable)headers
|
||||
body:(NSData *__autoreleasing _Nullable * _Nullable)body
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
// HTTP 请求构建
|
||||
- (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent;
|
||||
|
||||
// 连接池 key 生成
|
||||
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS;
|
||||
|
||||
// 错误转换
|
||||
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description;
|
||||
|
||||
@end
|
||||
|
||||
#if DEBUG
|
||||
// 测试专用:连接池检查 API
|
||||
@interface HttpdnsNWHTTPClient (TestInspection)
|
||||
|
||||
// 获取指定 pool key 的连接数量
|
||||
- (NSUInteger)connectionPoolCountForKey:(NSString *)key;
|
||||
|
||||
// 获取所有连接池 keys
|
||||
- (NSArray<NSString *> *)allConnectionPoolKeys;
|
||||
|
||||
// 获取连接池总连接数
|
||||
- (NSUInteger)totalConnectionCount;
|
||||
|
||||
// 连接创建计数(用于验证连接复用)
|
||||
@property (atomic, assign) NSUInteger connectionCreationCount;
|
||||
|
||||
// 连接复用计数(用于验证连接复用)
|
||||
@property (atomic, assign) NSUInteger connectionReuseCount;
|
||||
|
||||
// 重置统计计数器(每个测试开始前调用)
|
||||
- (void)resetPoolStatistics;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,48 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class HttpdnsNWHTTPClient;
|
||||
|
||||
@interface HttpdnsNWReusableConnection : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSDate *lastUsedDate;
|
||||
@property (nonatomic, assign) BOOL inUse;
|
||||
@property (nonatomic, assign, getter=isInvalidated, readonly) BOOL invalidated;
|
||||
|
||||
- (instancetype)initWithClient:(HttpdnsNWHTTPClient *)client
|
||||
host:(NSString *)host
|
||||
port:(NSString *)port
|
||||
useTLS:(BOOL)useTLS NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (BOOL)openWithTimeout:(NSTimeInterval)timeout error:(NSError **)error;
|
||||
- (nullable NSData *)sendRequestData:(NSData *)requestData
|
||||
timeout:(NSTimeInterval)timeout
|
||||
remoteConnectionClosed:(BOOL *)remoteConnectionClosed
|
||||
error:(NSError **)error;
|
||||
- (BOOL)isViable;
|
||||
- (void)invalidate;
|
||||
|
||||
@end
|
||||
|
||||
#if DEBUG
|
||||
// 测试专用:连接状态检查与操作
|
||||
@interface HttpdnsNWReusableConnection (DebugInspection)
|
||||
|
||||
// 状态检查(这些属性已在主接口暴露,这里仅为文档明确)
|
||||
// @property lastUsedDate - 可读写
|
||||
// @property inUse - 可读写
|
||||
// @property invalidated - 只读
|
||||
|
||||
// 测试辅助方法
|
||||
- (void)debugSetLastUsedDate:(nullable NSDate *)date;
|
||||
- (void)debugSetInUse:(BOOL)inUse;
|
||||
- (void)debugInvalidate;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
#import "HttpdnsNWReusableConnection.h"
|
||||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||||
|
||||
#import <Network/Network.h>
|
||||
#import <Security/SecCertificate.h>
|
||||
#import <Security/SecPolicy.h>
|
||||
#import <Security/SecTrust.h>
|
||||
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
@class HttpdnsNWHTTPClient;
|
||||
|
||||
// 只在此实现文件内可见的交换对象,承载一次请求/响应数据与状态
|
||||
@interface HttpdnsNWHTTPExchange : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSMutableData *buffer;
|
||||
@property (nonatomic, strong, readonly) dispatch_semaphore_t semaphore;
|
||||
@property (nonatomic, assign) BOOL finished;
|
||||
@property (nonatomic, assign) BOOL remoteClosed;
|
||||
@property (nonatomic, strong) NSError *error;
|
||||
@property (nonatomic, assign) NSUInteger headerEndIndex;
|
||||
@property (nonatomic, assign) BOOL headerParsed;
|
||||
@property (nonatomic, assign) BOOL chunked;
|
||||
@property (nonatomic, assign) long long contentLength;
|
||||
@property (nonatomic, assign) NSInteger statusCode;
|
||||
@property (nonatomic, strong) dispatch_block_t timeoutBlock;
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPExchange
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_buffer = [NSMutableData data];
|
||||
_semaphore = dispatch_semaphore_create(0);
|
||||
_headerEndIndex = NSNotFound;
|
||||
_contentLength = -1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface HttpdnsNWReusableConnection ()
|
||||
|
||||
@property (nonatomic, weak, readonly) HttpdnsNWHTTPClient *client;
|
||||
@property (nonatomic, copy, readonly) NSString *host;
|
||||
@property (nonatomic, copy, readonly) NSString *port;
|
||||
@property (nonatomic, assign, readonly) BOOL useTLS;
|
||||
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
|
||||
|
||||
#if OS_OBJECT_USE_OBJC
|
||||
@property (nonatomic, strong) nw_connection_t connectionHandle;
|
||||
#else
|
||||
@property (nonatomic, assign) nw_connection_t connectionHandle;
|
||||
#endif
|
||||
@property (nonatomic, strong) dispatch_semaphore_t stateSemaphore;
|
||||
@property (nonatomic, assign) nw_connection_state_t state;
|
||||
@property (nonatomic, strong) NSError *stateError;
|
||||
@property (nonatomic, assign) BOOL started;
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPExchange *currentExchange;
|
||||
@property (nonatomic, assign, readwrite, getter=isInvalidated) BOOL invalidated;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWReusableConnection
|
||||
|
||||
- (void)dealloc {
|
||||
if (_connectionHandle) {
|
||||
nw_connection_set_state_changed_handler(_connectionHandle, NULL);
|
||||
nw_connection_cancel(_connectionHandle);
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
nw_release(_connectionHandle);
|
||||
#endif
|
||||
_connectionHandle = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithClient:(HttpdnsNWHTTPClient *)client
|
||||
host:(NSString *)host
|
||||
port:(NSString *)port
|
||||
useTLS:(BOOL)useTLS {
|
||||
NSParameterAssert(client);
|
||||
NSParameterAssert(host);
|
||||
NSParameterAssert(port);
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_client = client;
|
||||
_host = [host copy];
|
||||
_port = [port copy];
|
||||
_useTLS = useTLS;
|
||||
_queue = dispatch_queue_create("com.alibaba.sdk.httpdns.network.connection.reuse", DISPATCH_QUEUE_SERIAL);
|
||||
_stateSemaphore = dispatch_semaphore_create(0);
|
||||
_state = nw_connection_state_invalid;
|
||||
_lastUsedDate = [NSDate date];
|
||||
|
||||
nw_endpoint_t endpoint = nw_endpoint_create_host(_host.UTF8String, _port.UTF8String);
|
||||
if (!endpoint) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
nw_parameters_t parameters = NULL;
|
||||
if (useTLS) {
|
||||
parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tlsOptions) {
|
||||
if (!tlsOptions) {
|
||||
return;
|
||||
}
|
||||
sec_protocol_options_t secOptions = nw_tls_copy_sec_protocol_options(tlsOptions);
|
||||
if (!secOptions) {
|
||||
return;
|
||||
}
|
||||
if (![HttpdnsUtil isIPv4Address:host] && ![HttpdnsUtil isIPv6Address:host]) {
|
||||
sec_protocol_options_set_tls_server_name(secOptions, host.UTF8String);
|
||||
}
|
||||
#if defined(__IPHONE_13_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0)
|
||||
if (@available(iOS 13.0, *)) {
|
||||
sec_protocol_options_add_tls_application_protocol(secOptions, "http/1.1");
|
||||
}
|
||||
#endif
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
sec_protocol_options_set_verify_block(secOptions, ^(sec_protocol_metadata_t metadata, sec_trust_t secTrust, sec_protocol_verify_complete_t complete) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
BOOL isValid = NO;
|
||||
if (secTrust && strongSelf) {
|
||||
SecTrustRef trustRef = sec_trust_copy_ref(secTrust);
|
||||
if (trustRef) {
|
||||
NSString *validIP = ALICLOUD_HTTPDNS_VALID_SERVER_CERTIFICATE_IP;
|
||||
isValid = [strongSelf.client evaluateServerTrust:trustRef forDomain:validIP];
|
||||
if (!isValid && [HttpdnsUtil isNotEmptyString:strongSelf.host]) {
|
||||
isValid = [strongSelf.client evaluateServerTrust:trustRef forDomain:strongSelf.host];
|
||||
}
|
||||
if (!isValid && !strongSelf.stateError) {
|
||||
strongSelf.stateError = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"TLS trust validation failed"}];
|
||||
}
|
||||
CFRelease(trustRef);
|
||||
}
|
||||
}
|
||||
complete(isValid);
|
||||
}, strongSelf.queue);
|
||||
}, ^(nw_protocol_options_t tcpOptions) {
|
||||
nw_tcp_options_set_no_delay(tcpOptions, true);
|
||||
});
|
||||
} else {
|
||||
parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, ^(nw_protocol_options_t tcpOptions) {
|
||||
nw_tcp_options_set_no_delay(tcpOptions, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (!parameters) {
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
nw_release(endpoint);
|
||||
#endif
|
||||
return nil;
|
||||
}
|
||||
|
||||
nw_connection_t connection = nw_connection_create(endpoint, parameters);
|
||||
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
nw_release(endpoint);
|
||||
nw_release(parameters);
|
||||
#endif
|
||||
|
||||
if (!connection) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_connectionHandle = connection;
|
||||
|
||||
nw_connection_set_queue(_connectionHandle, _queue);
|
||||
nw_connection_set_state_changed_handler(_connectionHandle, ^(nw_connection_state_t state, nw_error_t stateError) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
[strongSelf handleStateChange:state error:stateError];
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleStateChange:(nw_connection_state_t)state error:(nw_error_t)error {
|
||||
_state = state;
|
||||
if (error) {
|
||||
_stateError = [HttpdnsNWHTTPClient errorFromNWError:error description:@"Connection state error"];
|
||||
}
|
||||
if (state == nw_connection_state_ready) {
|
||||
dispatch_semaphore_signal(_stateSemaphore);
|
||||
return;
|
||||
}
|
||||
if (state == nw_connection_state_failed || state == nw_connection_state_cancelled) {
|
||||
self.invalidated = YES;
|
||||
if (!_stateError && error) {
|
||||
_stateError = [HttpdnsNWHTTPClient errorFromNWError:error description:@"Connection failed"];
|
||||
}
|
||||
dispatch_semaphore_signal(_stateSemaphore);
|
||||
HttpdnsNWHTTPExchange *exchange = self.currentExchange;
|
||||
if (exchange && !exchange.finished) {
|
||||
if (!exchange.error) {
|
||||
exchange.error = _stateError ?: [HttpdnsNWHTTPClient errorFromNWError:error description:@"Connection failed"];
|
||||
}
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)openWithTimeout:(NSTimeInterval)timeout error:(NSError **)error {
|
||||
if (self.invalidated) {
|
||||
if (error) {
|
||||
*error = _stateError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection invalid"}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (!_started) {
|
||||
_started = YES;
|
||||
nw_connection_start(_connectionHandle);
|
||||
}
|
||||
|
||||
dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
|
||||
long waitResult = dispatch_semaphore_wait(_stateSemaphore, deadline);
|
||||
if (waitResult != 0) {
|
||||
self.invalidated = YES;
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection setup timed out"}];
|
||||
}
|
||||
nw_connection_cancel(_connectionHandle);
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_state == nw_connection_state_ready) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
*error = _stateError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection failed to become ready"}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isViable {
|
||||
return !self.invalidated && _state == nw_connection_state_ready;
|
||||
}
|
||||
|
||||
- (void)invalidate {
|
||||
if (self.invalidated) {
|
||||
return;
|
||||
}
|
||||
self.invalidated = YES;
|
||||
if (_connectionHandle) {
|
||||
nw_connection_cancel(_connectionHandle);
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)sendRequestData:(NSData *)requestData
|
||||
timeout:(NSTimeInterval)timeout
|
||||
remoteConnectionClosed:(BOOL *)remoteConnectionClosed
|
||||
error:(NSError **)error {
|
||||
if (!requestData || requestData.length == 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Empty HTTP request"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self isViable] || self.currentExchange) {
|
||||
if (error) {
|
||||
*error = _stateError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection not ready"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
HttpdnsNWHTTPExchange *exchange = [HttpdnsNWHTTPExchange new];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
|
||||
dispatch_sync(_queue, ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
exchange.error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection released unexpectedly"}];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
return;
|
||||
}
|
||||
if (strongSelf.invalidated || strongSelf.currentExchange) {
|
||||
exchange.error = strongSelf.stateError ?: [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection is busy"}];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
return;
|
||||
}
|
||||
strongSelf.currentExchange = exchange;
|
||||
dispatch_data_t payload = dispatch_data_create(requestData.bytes, requestData.length, NULL, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
|
||||
dispatch_block_t timeoutBlock = dispatch_block_create(0, ^{
|
||||
if (exchange.finished) {
|
||||
return;
|
||||
}
|
||||
exchange.error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Request timed out"}];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
nw_connection_cancel(strongSelf.connectionHandle);
|
||||
});
|
||||
exchange.timeoutBlock = timeoutBlock;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), strongSelf.queue, timeoutBlock);
|
||||
|
||||
nw_connection_send(strongSelf.connectionHandle, payload, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t sendError) {
|
||||
__strong typeof(strongSelf) innerSelf = strongSelf;
|
||||
if (!innerSelf) {
|
||||
return;
|
||||
}
|
||||
if (sendError) {
|
||||
dispatch_async(innerSelf.queue, ^{
|
||||
if (!exchange.finished) {
|
||||
exchange.error = [HttpdnsNWHTTPClient errorFromNWError:sendError description:@"Send failed"];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
nw_connection_cancel(innerSelf.connectionHandle);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
[innerSelf startReceiveLoopForExchange:exchange];
|
||||
});
|
||||
});
|
||||
|
||||
dispatch_time_t deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
|
||||
long waitResult = dispatch_semaphore_wait(exchange.semaphore, deadline);
|
||||
|
||||
dispatch_sync(_queue, ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (exchange.timeoutBlock) {
|
||||
dispatch_block_cancel(exchange.timeoutBlock);
|
||||
exchange.timeoutBlock = nil;
|
||||
}
|
||||
if (strongSelf && strongSelf.currentExchange == exchange) {
|
||||
strongSelf.currentExchange = nil;
|
||||
}
|
||||
});
|
||||
|
||||
if (waitResult != 0) {
|
||||
if (!exchange.error) {
|
||||
exchange.error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Request wait timed out"}];
|
||||
}
|
||||
[self invalidate];
|
||||
if (error) {
|
||||
*error = exchange.error;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (exchange.error) {
|
||||
[self invalidate];
|
||||
if (error) {
|
||||
*error = exchange.error;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (remoteConnectionClosed) {
|
||||
*remoteConnectionClosed = exchange.remoteClosed;
|
||||
}
|
||||
|
||||
self.lastUsedDate = [NSDate date];
|
||||
return [exchange.buffer copy];
|
||||
}
|
||||
|
||||
- (void)startReceiveLoopForExchange:(HttpdnsNWHTTPExchange *)exchange {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
__block void (^receiveBlock)(dispatch_data_t, nw_content_context_t, bool, nw_error_t);
|
||||
__block __weak void (^weakReceiveBlock)(dispatch_data_t, nw_content_context_t, bool, nw_error_t);
|
||||
|
||||
receiveBlock = ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t receiveError) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
if (exchange.finished) {
|
||||
return;
|
||||
}
|
||||
if (receiveError) {
|
||||
exchange.error = [HttpdnsNWHTTPClient errorFromNWError:receiveError description:@"Receive failed"];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
return;
|
||||
}
|
||||
if (content) {
|
||||
dispatch_data_apply(content, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
|
||||
if (buffer && size > 0) {
|
||||
[exchange.buffer appendBytes:buffer length:size];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
[strongSelf evaluateExchangeCompletion:exchange isRemoteComplete:is_complete];
|
||||
if (exchange.finished) {
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
return;
|
||||
}
|
||||
if (is_complete) {
|
||||
exchange.remoteClosed = YES;
|
||||
if (!exchange.finished) {
|
||||
exchange.error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Connection closed before response completed"}];
|
||||
exchange.finished = YES;
|
||||
dispatch_semaphore_signal(exchange.semaphore);
|
||||
}
|
||||
return;
|
||||
}
|
||||
void (^callback)(dispatch_data_t, nw_content_context_t, bool, nw_error_t) = weakReceiveBlock;
|
||||
if (callback && !exchange.finished) {
|
||||
nw_connection_receive(strongSelf.connectionHandle, 1, UINT32_MAX, callback);
|
||||
}
|
||||
};
|
||||
|
||||
weakReceiveBlock = receiveBlock;
|
||||
nw_connection_receive(_connectionHandle, 1, UINT32_MAX, receiveBlock);
|
||||
}
|
||||
|
||||
- (void)evaluateExchangeCompletion:(HttpdnsNWHTTPExchange *)exchange isRemoteComplete:(bool)isComplete {
|
||||
if (exchange.finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
// 远端已经发送完并关闭,需要提前标记,避免提前返回时漏记连接状态
|
||||
exchange.remoteClosed = YES;
|
||||
}
|
||||
|
||||
if (!exchange.headerParsed) {
|
||||
NSUInteger headerEnd = NSNotFound;
|
||||
NSInteger statusCode = 0;
|
||||
NSDictionary<NSString *, NSString *> *headers = nil;
|
||||
NSError *headerError = nil;
|
||||
HttpdnsHTTPHeaderParseResult headerResult = [self.client tryParseHTTPHeadersInData:exchange.buffer
|
||||
headerEndIndex:&headerEnd
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&headerError];
|
||||
if (headerResult == HttpdnsHTTPHeaderParseResultError) {
|
||||
exchange.error = headerError;
|
||||
exchange.finished = YES;
|
||||
return;
|
||||
}
|
||||
if (headerResult == HttpdnsHTTPHeaderParseResultIncomplete) {
|
||||
return;
|
||||
}
|
||||
exchange.headerParsed = YES;
|
||||
exchange.headerEndIndex = headerEnd;
|
||||
exchange.statusCode = statusCode;
|
||||
NSString *contentLengthValue = headers[@"content-length"];
|
||||
if ([HttpdnsUtil isNotEmptyString:contentLengthValue]) {
|
||||
exchange.contentLength = [contentLengthValue longLongValue];
|
||||
}
|
||||
NSString *transferEncodingValue = headers[@"transfer-encoding"];
|
||||
if ([HttpdnsUtil isNotEmptyString:transferEncodingValue] && [transferEncodingValue rangeOfString:@"chunked" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||||
exchange.chunked = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exchange.headerParsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSUInteger bodyOffset = exchange.headerEndIndex == NSNotFound ? 0 : exchange.headerEndIndex + 4;
|
||||
NSUInteger currentBodyLength = exchange.buffer.length > bodyOffset ? exchange.buffer.length - bodyOffset : 0;
|
||||
|
||||
if (exchange.chunked) {
|
||||
NSError *chunkError = nil;
|
||||
HttpdnsHTTPChunkParseResult chunkResult = [self.client checkChunkedBodyCompletionInData:exchange.buffer
|
||||
headerEndIndex:exchange.headerEndIndex
|
||||
error:&chunkError];
|
||||
if (chunkResult == HttpdnsHTTPChunkParseResultError) {
|
||||
exchange.error = chunkError;
|
||||
exchange.finished = YES;
|
||||
return;
|
||||
}
|
||||
if (chunkResult == HttpdnsHTTPChunkParseResultSuccess) {
|
||||
exchange.finished = YES;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (exchange.contentLength >= 0) {
|
||||
if ((long long)currentBodyLength >= exchange.contentLength) {
|
||||
exchange.finished = YES;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
exchange.remoteClosed = YES;
|
||||
exchange.finished = YES;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#if DEBUG
|
||||
// 测试专用:连接状态操作实现
|
||||
@implementation HttpdnsNWReusableConnection (DebugInspection)
|
||||
|
||||
- (void)debugSetLastUsedDate:(nullable NSDate *)date {
|
||||
self.lastUsedDate = date;
|
||||
}
|
||||
|
||||
- (void)debugSetInUse:(BOOL)inUse {
|
||||
self.inUse = inUse;
|
||||
}
|
||||
|
||||
- (void)debugInvalidate {
|
||||
[self invalidate];
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
75
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Persistent/HttpdnsDB.h
Normal file
75
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Persistent/HttpdnsDB.h
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// HttpdnsDB.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/15.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsHostRecord.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* SQLite3数据库操作类,用于持久化存储HttpDNS缓存记录
|
||||
*/
|
||||
@interface HttpdnsDB : NSObject
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
* @param accountId 账户ID
|
||||
* @return 数据库实例
|
||||
*/
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId;
|
||||
|
||||
/**
|
||||
* 创建或更新记录
|
||||
* @param record 主机记录
|
||||
* @return 是否成功
|
||||
*/
|
||||
- (BOOL)createOrUpdate:(HttpdnsHostRecord *)record;
|
||||
|
||||
/**
|
||||
* 根据缓存键查询记录
|
||||
* @param cacheKey 缓存键
|
||||
* @return 查询到的记录,如果不存在则返回nil
|
||||
*/
|
||||
- (nullable HttpdnsHostRecord *)selectByCacheKey:(NSString *)cacheKey;
|
||||
|
||||
/**
|
||||
* 根据缓存键删除记录
|
||||
* @param cacheKey 缓存键
|
||||
* @return 是否成功
|
||||
*/
|
||||
- (BOOL)deleteByCacheKey:(NSString *)cacheKey;
|
||||
|
||||
/**
|
||||
* 根据主机名数组批量删除记录
|
||||
* @param hostNameArr 主机名数组
|
||||
* @return 成功删除的记录数量
|
||||
*/
|
||||
- (NSInteger)deleteByHostNameArr:(NSArray<NSString *> *)hostNameArr;
|
||||
|
||||
/**
|
||||
* 获取所有缓存记录
|
||||
* @return 所有缓存记录数组
|
||||
*/
|
||||
- (NSArray<HttpdnsHostRecord *> *)getAllRecords;
|
||||
|
||||
/**
|
||||
* 清理指定时间点已过期的记录
|
||||
* @param specifiedTime 指定的时间点(epoch时间)
|
||||
* @return 清理的记录数量
|
||||
*/
|
||||
- (NSInteger)cleanRecordAlreadExpiredAt:(NSTimeInterval)specifiedTime;
|
||||
|
||||
/**
|
||||
* 删除所有记录
|
||||
* @return 是否成功
|
||||
*/
|
||||
- (BOOL)deleteAll;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
608
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Persistent/HttpdnsDB.m
Normal file
608
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Persistent/HttpdnsDB.m
Normal file
@@ -0,0 +1,608 @@
|
||||
//
|
||||
// HttpdnsDB.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/15.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsDB.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
// 表名
|
||||
static NSString *const kTableName = @"httpdns_cache_table";
|
||||
|
||||
// 列名
|
||||
static NSString *const kColumnId = @"id";
|
||||
static NSString *const kColumnCacheKey = @"cache_key";
|
||||
static NSString *const kColumnHostName = @"host_name";
|
||||
static NSString *const kColumnCreateAt = @"create_at";
|
||||
static NSString *const kColumnModifyAt = @"modify_at";
|
||||
static NSString *const kColumnClientIp = @"client_ip";
|
||||
static NSString *const kColumnV4Ips = @"v4_ips";
|
||||
static NSString *const kColumnV4Ttl = @"v4_ttl";
|
||||
static NSString *const kColumnV4LookupTime = @"v4_lookup_time";
|
||||
static NSString *const kColumnV6Ips = @"v6_ips";
|
||||
static NSString *const kColumnV6Ttl = @"v6_ttl";
|
||||
static NSString *const kColumnV6LookupTime = @"v6_lookup_time";
|
||||
static NSString *const kColumnExtra = @"extra";
|
||||
|
||||
@interface HttpdnsDB ()
|
||||
|
||||
@property (nonatomic, assign) sqlite3 *db;
|
||||
@property (nonatomic, copy) NSString *dbPath;
|
||||
@property (nonatomic, strong) dispatch_queue_t dbQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsDB
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// 创建数据库目录
|
||||
NSString *dbDir = [HttpdnsPersistenceUtils httpdnsDataDirectory];
|
||||
|
||||
// 设置数据库路径
|
||||
_dbPath = [dbDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%ld_v20250406.db", (long)accountId]];
|
||||
|
||||
// 创建专用队列确保线程安全
|
||||
_dbQueue = dispatch_queue_create("com.aliyun.httpdns.db", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
// 打开数据库
|
||||
__block BOOL success = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
success = [self openDB];
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_db) {
|
||||
sqlite3_close(_db);
|
||||
_db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (BOOL)createOrUpdate:(HttpdnsHostRecord *)record {
|
||||
if (!record || !record.cacheKey) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__block BOOL result = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
// 检查记录是否存在,以便正确处理createAt
|
||||
HttpdnsHostRecord *existingRecord = [self selectByCacheKeyInternal:record.cacheKey];
|
||||
|
||||
// 准备要保存的记录
|
||||
NSDate *now = [NSDate date];
|
||||
HttpdnsHostRecord *recordToSave;
|
||||
|
||||
if (existingRecord) {
|
||||
// 更新记录,保留原始的createAt,更新modifyAt为当前时间
|
||||
recordToSave = [[HttpdnsHostRecord alloc] initWithId:record.id
|
||||
cacheKey:record.cacheKey
|
||||
hostName:record.hostName
|
||||
createAt:existingRecord.createAt // 保留原始createAt
|
||||
modifyAt:now // 更新modifyAt
|
||||
clientIp:record.clientIp
|
||||
v4ips:record.v4ips
|
||||
v4ttl:record.v4ttl
|
||||
v4LookupTime:record.v4LookupTime
|
||||
v6ips:record.v6ips
|
||||
v6ttl:record.v6ttl
|
||||
v6LookupTime:record.v6LookupTime
|
||||
extra:record.extra];
|
||||
} else {
|
||||
// 新记录,设置createAt和modifyAt为当前时间
|
||||
recordToSave = [[HttpdnsHostRecord alloc] initWithId:record.id
|
||||
cacheKey:record.cacheKey
|
||||
hostName:record.hostName
|
||||
createAt:now // 新记录的createAt
|
||||
modifyAt:now // 新记录的modifyAt
|
||||
clientIp:record.clientIp
|
||||
v4ips:record.v4ips
|
||||
v4ttl:record.v4ttl
|
||||
v4LookupTime:record.v4LookupTime
|
||||
v6ips:record.v6ips
|
||||
v6ttl:record.v6ttl
|
||||
v6LookupTime:record.v6LookupTime
|
||||
extra:record.extra];
|
||||
}
|
||||
|
||||
// 使用INSERT OR REPLACE语法保存记录
|
||||
result = [self saveRecord:recordToSave];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (nullable HttpdnsHostRecord *)selectByCacheKey:(NSString *)cacheKey {
|
||||
if (!cacheKey) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block HttpdnsHostRecord *record = nil;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
record = [self selectByCacheKeyInternal:cacheKey];
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
- (BOOL)deleteByCacheKey:(NSString *)cacheKey {
|
||||
if (!cacheKey) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__block BOOL result = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, [cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
result = (sqlite3_step(stmt) == SQLITE_DONE);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSInteger)deleteByHostNameArr:(NSArray<NSString *> *)hostNameArr {
|
||||
if (!hostNameArr || hostNameArr.count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 过滤掉空值
|
||||
NSMutableArray *validHostNames = [NSMutableArray array];
|
||||
for (NSString *hostname in hostNameArr) {
|
||||
if (hostname && hostname.length > 0) {
|
||||
[validHostNames addObject:hostname];
|
||||
}
|
||||
}
|
||||
|
||||
if (validHostNames.count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__block NSInteger deletedCount = 0;
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
// 构建IN子句的占位符
|
||||
NSMutableString *placeholders = [NSMutableString string];
|
||||
for (NSUInteger i = 0; i < validHostNames.count; i++) {
|
||||
[placeholders appendString:@"?"];
|
||||
if (i < validHostNames.count - 1) {
|
||||
[placeholders appendString:@","];
|
||||
}
|
||||
}
|
||||
|
||||
// 构建SQL语句
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (%@)",
|
||||
kTableName, kColumnHostName, placeholders];
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
// 绑定所有参数
|
||||
for (NSUInteger i = 0; i < validHostNames.count; i++) {
|
||||
sqlite3_bind_text(stmt, (int)(i + 1), [validHostNames[i] UTF8String], -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
deletedCount = sqlite3_changes(_db);
|
||||
} else {
|
||||
NSLog(@"Failed to delete records: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
} else {
|
||||
NSLog(@"Failed to prepare delete statement: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
});
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
- (BOOL)deleteAll {
|
||||
__block BOOL result = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@", kTableName];
|
||||
char *errMsg;
|
||||
|
||||
result = (sqlite3_exec(_db, [sql UTF8String], NULL, NULL, &errMsg) == SQLITE_OK);
|
||||
|
||||
if (errMsg) {
|
||||
NSLog(@"Failed to delete all records: %s", errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<HttpdnsHostRecord *> *)getAllRecords {
|
||||
__block NSMutableArray<HttpdnsHostRecord *> *records = [NSMutableArray array];
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@", kTableName];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
HttpdnsHostRecord *record = [self recordFromStatement:stmt];
|
||||
if (record) {
|
||||
[records addObject:record];
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
} else {
|
||||
NSLog(@"Failed to prepare getAllRecords statement: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
});
|
||||
|
||||
return [records copy];
|
||||
}
|
||||
|
||||
- (NSInteger)cleanRecordAlreadExpiredAt:(NSTimeInterval)specifiedTime {
|
||||
__block NSInteger cleanedCount = 0;
|
||||
|
||||
// 获取所有记录
|
||||
NSArray<HttpdnsHostRecord *> *allRecords = [self getAllRecords];
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
for (HttpdnsHostRecord *record in allRecords) {
|
||||
BOOL v4Expired = NO;
|
||||
BOOL v6Expired = NO;
|
||||
|
||||
// 检查IPv4记录是否过期
|
||||
if (record.v4LookupTime + record.v4ttl <= specifiedTime) {
|
||||
v4Expired = YES;
|
||||
}
|
||||
|
||||
// 检查IPv6记录是否过期
|
||||
if (record.v6LookupTime + record.v6ttl <= specifiedTime) {
|
||||
v6Expired = YES;
|
||||
}
|
||||
|
||||
// 如果两种IP类型都过期,删除整条记录
|
||||
if (v4Expired && v6Expired) {
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
// 如果只有一种IP类型过期,更新记录
|
||||
else if (v4Expired || v6Expired) {
|
||||
NSMutableArray<NSString *> *v4ips = [NSMutableArray arrayWithArray:record.v4ips];
|
||||
NSMutableArray<NSString *> *v6ips = [NSMutableArray arrayWithArray:record.v6ips];
|
||||
|
||||
// 如果IPv4过期,清空IPv4记录
|
||||
if (v4Expired) {
|
||||
[v4ips removeAllObjects];
|
||||
}
|
||||
|
||||
// 如果IPv6过期,清空IPv6记录
|
||||
if (v6Expired) {
|
||||
[v6ips removeAllObjects];
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"UPDATE %@ SET %@ = ?, %@ = ? WHERE %@ = ?",
|
||||
kTableName,
|
||||
kColumnV4Ips,
|
||||
kColumnV6Ips,
|
||||
kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
// 绑定v4ips
|
||||
if (v4ips.count > 0) {
|
||||
NSString *v4ipsStr = [v4ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, 1, [v4ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, 1);
|
||||
}
|
||||
|
||||
// 绑定v6ips
|
||||
if (v6ips.count > 0) {
|
||||
NSString *v6ipsStr = [v6ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, 2, [v6ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, 2);
|
||||
}
|
||||
|
||||
// 绑定cacheKey
|
||||
sqlite3_bind_text(stmt, 3, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (BOOL)openDB {
|
||||
if (sqlite3_open([_dbPath UTF8String], &_db) != SQLITE_OK) {
|
||||
NSLog(@"Failed to open database: %s", sqlite3_errmsg(_db));
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 创建表
|
||||
return [self createTableIfNeeded];
|
||||
}
|
||||
|
||||
- (BOOL)createTableIfNeeded {
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"CREATE TABLE IF NOT EXISTS %@ ("
|
||||
@"%@ INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
@"%@ TEXT UNIQUE NOT NULL, "
|
||||
@"%@ TEXT NOT NULL, "
|
||||
@"%@ REAL, "
|
||||
@"%@ REAL, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ TEXT"
|
||||
@")",
|
||||
kTableName,
|
||||
kColumnId,
|
||||
kColumnCacheKey,
|
||||
kColumnHostName,
|
||||
kColumnCreateAt,
|
||||
kColumnModifyAt,
|
||||
kColumnClientIp,
|
||||
kColumnV4Ips,
|
||||
kColumnV4Ttl,
|
||||
kColumnV4LookupTime,
|
||||
kColumnV6Ips,
|
||||
kColumnV6Ttl,
|
||||
kColumnV6LookupTime,
|
||||
kColumnExtra];
|
||||
|
||||
char *errMsg;
|
||||
if (sqlite3_exec(_db, [sql UTF8String], NULL, NULL, &errMsg) != SQLITE_OK) {
|
||||
NSLog(@"Failed to create table: %s", errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)saveRecord:(HttpdnsHostRecord *)record {
|
||||
// 使用INSERT OR REPLACE语法,如果记录存在则更新,不存在则插入
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"INSERT OR REPLACE INTO %@ ("
|
||||
@"%@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@) "
|
||||
@"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
kTableName,
|
||||
kColumnCacheKey,
|
||||
kColumnHostName,
|
||||
kColumnCreateAt,
|
||||
kColumnModifyAt,
|
||||
kColumnClientIp,
|
||||
kColumnV4Ips,
|
||||
kColumnV4Ttl,
|
||||
kColumnV4LookupTime,
|
||||
kColumnV6Ips,
|
||||
kColumnV6Ttl,
|
||||
kColumnV6LookupTime,
|
||||
kColumnExtra];
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
|
||||
NSLog(@"Failed to prepare save statement: %s", sqlite3_errmsg(_db));
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 绑定参数
|
||||
int index = 1;
|
||||
|
||||
// 绑定cacheKey (唯一键)
|
||||
sqlite3_bind_text(stmt, index++, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
// 绑定hostName
|
||||
sqlite3_bind_text(stmt, index++, [record.hostName UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
// 绑定createAt
|
||||
if (record.createAt) {
|
||||
sqlite3_bind_double(stmt, index++, [record.createAt timeIntervalSince1970]);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定modifyAt
|
||||
if (record.modifyAt) {
|
||||
sqlite3_bind_double(stmt, index++, [record.modifyAt timeIntervalSince1970]);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定clientIp
|
||||
if (record.clientIp) {
|
||||
sqlite3_bind_text(stmt, index++, [record.clientIp UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v4ips
|
||||
if (record.v4ips.count > 0) {
|
||||
NSString *v4ipsStr = [record.v4ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, index++, [v4ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v4ttl
|
||||
sqlite3_bind_int64(stmt, index++, record.v4ttl);
|
||||
|
||||
// 绑定v4LookupTime
|
||||
sqlite3_bind_int64(stmt, index++, record.v4LookupTime);
|
||||
|
||||
// 绑定v6ips
|
||||
if (record.v6ips.count > 0) {
|
||||
NSString *v6ipsStr = [record.v6ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, index++, [v6ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v6ttl
|
||||
sqlite3_bind_int64(stmt, index++, record.v6ttl);
|
||||
|
||||
// 绑定v6LookupTime
|
||||
sqlite3_bind_int64(stmt, index++, record.v6LookupTime);
|
||||
|
||||
// 绑定extra
|
||||
if (record.extra) {
|
||||
sqlite3_bind_text(stmt, index++, [record.extra UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
BOOL result = (sqlite3_step(stmt) == SQLITE_DONE);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (HttpdnsHostRecord *)selectByCacheKeyInternal:(NSString *)cacheKey {
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
|
||||
NSLog(@"Failed to prepare query statement: %s", sqlite3_errmsg(_db));
|
||||
return nil;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, [cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
HttpdnsHostRecord *record = nil;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
record = [self recordFromStatement:stmt];
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return record;
|
||||
}
|
||||
|
||||
- (HttpdnsHostRecord *)recordFromStatement:(sqlite3_stmt *)stmt {
|
||||
// 获取id
|
||||
NSUInteger recordId = (NSUInteger)sqlite3_column_int64(stmt, 0);
|
||||
|
||||
// 获取cacheKey
|
||||
const char *cacheKeyChars = (const char *)sqlite3_column_text(stmt, 1);
|
||||
NSString *cacheKey = cacheKeyChars ? [NSString stringWithUTF8String:cacheKeyChars] : nil;
|
||||
|
||||
// 获取hostName
|
||||
const char *hostNameChars = (const char *)sqlite3_column_text(stmt, 2);
|
||||
NSString *hostName = hostNameChars ? [NSString stringWithUTF8String:hostNameChars] : nil;
|
||||
|
||||
// 获取createAt
|
||||
NSDate *createAt = nil;
|
||||
if (sqlite3_column_type(stmt, 3) != SQLITE_NULL) {
|
||||
double createAtTimestamp = sqlite3_column_double(stmt, 3);
|
||||
createAt = [NSDate dateWithTimeIntervalSince1970:createAtTimestamp];
|
||||
}
|
||||
|
||||
// 获取modifyAt
|
||||
NSDate *modifyAt = nil;
|
||||
if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
|
||||
double modifyAtTimestamp = sqlite3_column_double(stmt, 4);
|
||||
modifyAt = [NSDate dateWithTimeIntervalSince1970:modifyAtTimestamp];
|
||||
}
|
||||
|
||||
// 获取clientIp
|
||||
const char *clientIpChars = (const char *)sqlite3_column_text(stmt, 5);
|
||||
NSString *clientIp = clientIpChars ? [NSString stringWithUTF8String:clientIpChars] : nil;
|
||||
|
||||
// 获取v4ips
|
||||
NSArray<NSString *> *v4ips = nil;
|
||||
const char *v4ipsChars = (const char *)sqlite3_column_text(stmt, 6);
|
||||
if (v4ipsChars) {
|
||||
NSString *v4ipsStr = [NSString stringWithUTF8String:v4ipsChars];
|
||||
v4ips = [v4ipsStr componentsSeparatedByString:@","];
|
||||
} else {
|
||||
v4ips = @[];
|
||||
}
|
||||
|
||||
// 获取v4ttl
|
||||
int64_t v4ttl = sqlite3_column_int64(stmt, 7);
|
||||
|
||||
// 获取v4LookupTime
|
||||
int64_t v4LookupTime = sqlite3_column_int64(stmt, 8);
|
||||
|
||||
// 获取v6ips
|
||||
NSArray<NSString *> *v6ips = nil;
|
||||
const char *v6ipsChars = (const char *)sqlite3_column_text(stmt, 9);
|
||||
if (v6ipsChars) {
|
||||
NSString *v6ipsStr = [NSString stringWithUTF8String:v6ipsChars];
|
||||
v6ips = [v6ipsStr componentsSeparatedByString:@","];
|
||||
} else {
|
||||
v6ips = @[];
|
||||
}
|
||||
|
||||
// 获取v6ttl
|
||||
int64_t v6ttl = sqlite3_column_int64(stmt, 10);
|
||||
|
||||
// 获取v6LookupTime
|
||||
int64_t v6LookupTime = sqlite3_column_int64(stmt, 11);
|
||||
|
||||
// 获取extra
|
||||
NSString *extra = nil;
|
||||
const char *extraChars = (const char *)sqlite3_column_text(stmt, 12);
|
||||
if (extraChars) {
|
||||
extra = [NSString stringWithUTF8String:extraChars];
|
||||
}
|
||||
|
||||
// 创建记录对象
|
||||
return [[HttpdnsHostRecord alloc] initWithId:recordId
|
||||
cacheKey:cacheKey
|
||||
hostName:hostName
|
||||
createAt:createAt
|
||||
modifyAt:modifyAt
|
||||
clientIp:clientIp
|
||||
v4ips:v4ips
|
||||
v4ttl:v4ttl
|
||||
v4LookupTime:v4LookupTime
|
||||
v6ips:v6ips
|
||||
v6ttl:v6ttl
|
||||
v6LookupTime:v6LookupTime
|
||||
extra:extra];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsPersistenceUtils : NSObject
|
||||
|
||||
+ (NSString *)httpdnsDataDirectory;
|
||||
+ (NSString *)scheduleCenterResultDirectory;
|
||||
/// 多账号隔离:返回指定账号的调度结果目录
|
||||
+ (NSString *)scheduleCenterResultDirectoryForAccount:(NSInteger)accountId;
|
||||
|
||||
+ (BOOL)saveJSON:(id)JSON toPath:(NSString *)path;
|
||||
+ (id)getJSONFromPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
static NSString *const ALICLOUD_HTTPDNS_ROOT_DIR_NAME = @"HTTPDNS";
|
||||
static NSString *const ALICLOUD_HTTPDNS_HOST_CACHE_DIR_NAME = @"HostCache";
|
||||
|
||||
static dispatch_queue_t _fileCacheQueue = 0;
|
||||
|
||||
@implementation HttpdnsPersistenceUtils
|
||||
|
||||
#pragma mark - Base Path
|
||||
|
||||
+ (void)initialize {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_fileCacheQueue = dispatch_queue_create("com.alibaba.sdk.httpdns.fileCacheQueue", DISPATCH_QUEUE_SERIAL);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - File Utils
|
||||
|
||||
+ (BOOL)saveJSON:(id)JSON toPath:(NSString *)path {
|
||||
if (![HttpdnsUtil isNotEmptyString:path]) {
|
||||
return NO;
|
||||
}
|
||||
BOOL isValid = [HttpdnsUtil isValidJSON:JSON];
|
||||
if (isValid) {
|
||||
__block BOOL saveSucceed = NO;
|
||||
@try {
|
||||
[self removeFile:path];
|
||||
dispatch_sync(_fileCacheQueue, ^{
|
||||
saveSucceed = [NSKeyedArchiver archiveRootObject:JSON toFile:path];
|
||||
});
|
||||
|
||||
} @catch (NSException *exception) {}
|
||||
|
||||
|
||||
return saveSucceed;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (id)getJSONFromPath:(NSString *)path {
|
||||
if (![HttpdnsUtil isNotEmptyString:path]) {
|
||||
return nil;
|
||||
}
|
||||
__block id JSON = nil;
|
||||
@try {
|
||||
dispatch_sync(_fileCacheQueue, ^{
|
||||
JSON = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
|
||||
});
|
||||
BOOL isValid = [HttpdnsUtil isValidJSON:JSON];
|
||||
|
||||
if (isValid) {
|
||||
return JSON;
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
//deal with the previous file version
|
||||
if ([[exception name] isEqualToString:NSInvalidArgumentException]) {
|
||||
JSON = [NSMutableDictionary dictionaryWithContentsOfFile:path];
|
||||
|
||||
if (!JSON) {
|
||||
JSON = [NSMutableArray arrayWithContentsOfFile:path];
|
||||
}
|
||||
}
|
||||
}
|
||||
return JSON;
|
||||
}
|
||||
|
||||
+ (BOOL)removeFile:(NSString *)path {
|
||||
if (![HttpdnsUtil isNotEmptyString:path]) {
|
||||
return NO;
|
||||
}
|
||||
__block NSError * error = nil;
|
||||
__block BOOL ret = NO;
|
||||
dispatch_sync(_fileCacheQueue, ^{
|
||||
ret = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
+ (void)createDirectoryIfNeeded:(NSString *)path {
|
||||
if (![HttpdnsUtil isNotEmptyString:path]) {
|
||||
return;
|
||||
}
|
||||
dispatch_sync(_fileCacheQueue, ^{
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:path
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:NULL];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - ~/Libraray/Private Documents
|
||||
|
||||
/// Base path, all paths depend it
|
||||
+ (NSString *)homeDirectoryPath {
|
||||
return NSHomeDirectory();
|
||||
}
|
||||
|
||||
// ~/Library
|
||||
+ (NSString *)libraryDirectory {
|
||||
static NSString *path = nil;
|
||||
if (!path) {
|
||||
path = [[self homeDirectoryPath] stringByAppendingPathComponent:@"Library"];
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// ~/Library/Private Documents/HTTPDNS
|
||||
+ (NSString *)httpdnsDataDirectory {
|
||||
NSString *directory = [[HttpdnsPersistenceUtils libraryDirectory] stringByAppendingPathComponent:@"Private Documents/HTTPDNS"];
|
||||
[self createDirectoryIfNeeded:directory];
|
||||
return directory;
|
||||
}
|
||||
|
||||
//Library/Private Documents/HTTPDNS/scheduleCenterResult
|
||||
+ (NSString *)scheduleCenterResultDirectory {
|
||||
NSString *directory = [[HttpdnsPersistenceUtils httpdnsDataDirectory] stringByAppendingPathComponent:@"scheduleCenterResult"];
|
||||
[self createDirectoryIfNeeded:directory];
|
||||
return directory;
|
||||
}
|
||||
|
||||
//Library/Private Documents/HTTPDNS/scheduleCenterResult/<accountId>
|
||||
+ (NSString *)scheduleCenterResultDirectoryForAccount:(NSInteger)accountId {
|
||||
NSString *base = [self scheduleCenterResultDirectory];
|
||||
NSString *directory = [base stringByAppendingPathComponent:[NSString stringWithFormat:@"%ld", (long)accountId]];
|
||||
[self createDirectoryIfNeeded:directory];
|
||||
return directory;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsScheduleCenter : NSObject
|
||||
|
||||
/// 针对多账号场景的调度中心构造方法
|
||||
/// 注意:若无需多账号隔离,可继续使用 sharedInstance
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId;
|
||||
|
||||
- (void)initRegion:(NSString *)region;
|
||||
|
||||
- (void)resetRegion:(NSString *)region;
|
||||
|
||||
- (void)asyncUpdateRegionScheduleConfig;
|
||||
|
||||
- (void)rotateServiceServerHost;
|
||||
|
||||
- (NSString *)currentActiveServiceServerV4Host;
|
||||
|
||||
- (NSString *)currentActiveServiceServerV6Host;
|
||||
|
||||
|
||||
#pragma mark - Expose to Testcases
|
||||
|
||||
- (void)asyncUpdateRegionScheduleConfigAtRetry:(int)retryCount;
|
||||
|
||||
- (NSString *)getActiveUpdateServerHost;
|
||||
|
||||
- (NSArray<NSString *> *)currentUpdateServerV4HostList;
|
||||
|
||||
- (NSArray<NSString *> *)currentServiceServerV4HostList;
|
||||
|
||||
- (NSArray<NSString *> *)currentUpdateServerV6HostList;
|
||||
|
||||
- (NSArray<NSString *> *)currentServiceServerV6HostList;
|
||||
|
||||
- (int)currentActiveUpdateServerHostIndex;
|
||||
|
||||
- (int)currentActiveServiceServerHostIndex;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsScheduleExecutor.h"
|
||||
#import "HttpdnsRemoteResolver.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsRegionConfigLoader.h"
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
|
||||
static NSString *const kLastUpdateUnixTimestampKey = @"last_update_unix_timestamp";
|
||||
static NSString *const kScheduleRegionConfigLocalCacheFileName = @"schedule_center_result";
|
||||
|
||||
static int const MAX_UPDATE_RETRY_COUNT = 2;
|
||||
|
||||
@interface HttpdnsScheduleCenter ()
|
||||
|
||||
// 为了简单,无论v4还是v6,都只共同维护1个下标
|
||||
// 一般而言,下标当前在哪里并不是那么重要,重要的是轮询的能力
|
||||
@property (nonatomic, assign) int currentActiveServiceHostIndex;
|
||||
@property (nonatomic, assign) int currentActiveUpdateHostIndex;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv4ServiceServerHostList;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv6ServiceServerHostList;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv4UpdateServerHostList;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv6UpdateServerHostList;
|
||||
|
||||
@property (nonatomic, strong) dispatch_queue_t scheduleFetchConfigAsyncQueue;
|
||||
@property (nonatomic, strong) dispatch_queue_t scheduleConfigLocalOperationQueue;
|
||||
|
||||
@property (nonatomic, copy) NSString *scheduleCenterResultPath;
|
||||
@property (nonatomic, copy) NSDate *lastScheduleCenterConnectDate;
|
||||
|
||||
@property (nonatomic, copy) NSString *currentRegion;
|
||||
|
||||
@property (nonatomic, assign) NSInteger accountId;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsScheduleCenter
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId {
|
||||
if (self = [self init]) {
|
||||
_accountId = accountId;
|
||||
// 针对多账号隔离:根据账号ID切换本地调度结果存储路径
|
||||
NSString *dir = [HttpdnsPersistenceUtils scheduleCenterResultDirectoryForAccount:accountId];
|
||||
_scheduleCenterResultPath = [dir stringByAppendingPathComponent:kScheduleRegionConfigLocalCacheFileName];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_scheduleFetchConfigAsyncQueue = dispatch_queue_create("com.aliyun.httpdns.scheduleFetchConfigAsyncQueue", DISPATCH_QUEUE_CONCURRENT);
|
||||
_scheduleConfigLocalOperationQueue = dispatch_queue_create("com.aliyun.httpdns.scheduleConfigLocalOperationQueue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self->_currentActiveUpdateHostIndex = 0;
|
||||
self->_currentActiveServiceHostIndex = 0;
|
||||
});
|
||||
|
||||
_scheduleCenterResultPath = [[HttpdnsPersistenceUtils scheduleCenterResultDirectory]
|
||||
stringByAppendingPathComponent:kScheduleRegionConfigLocalCacheFileName];
|
||||
|
||||
// 上次更新日期默认设置为1天前,这样如果缓存没有记录,就会立即更新
|
||||
_lastScheduleCenterConnectDate = [NSDate dateWithTimeIntervalSinceNow:(- 24 * 60 * 60)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initRegion:(NSString *)region {
|
||||
if (![[HttpdnsRegionConfigLoader getAvailableRegionList] containsObject:region]) {
|
||||
region = ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY;
|
||||
}
|
||||
|
||||
// 先用默认region初始化
|
||||
// 如果用户主动调用了设置region接口,会按照用户设置的再来一次
|
||||
[self initServerListByRegion:region];
|
||||
|
||||
// 再从本地缓存读取之前缓存过的配置
|
||||
[self loadRegionConfigFromLocalCache];
|
||||
}
|
||||
|
||||
- (void)resetRegion:(NSString *)region {
|
||||
[self initServerListByRegion:region];
|
||||
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self.currentActiveServiceHostIndex = 0;
|
||||
self.currentActiveUpdateHostIndex = 0;
|
||||
});
|
||||
|
||||
// 重置region之后马上发起一次更新
|
||||
[self asyncUpdateRegionScheduleConfig];
|
||||
}
|
||||
|
||||
- (void)loadRegionConfigFromLocalCache {
|
||||
dispatch_async(self.scheduleFetchConfigAsyncQueue, ^{
|
||||
id obj = [HttpdnsPersistenceUtils getJSONFromPath:self.scheduleCenterResultPath];
|
||||
if (![obj isKindOfClass:[NSDictionary class]]) {
|
||||
// 本地缓存可能被旧版本或其他组件写入非字典类型,直接忽略以避免非法对象释放
|
||||
return;
|
||||
}
|
||||
NSDictionary *scheduleCenterResult = (NSDictionary *)obj;
|
||||
|
||||
// 兼容时间戳为NSNumber/NSString,屏蔽NSNull等异常输入
|
||||
id ts = [scheduleCenterResult objectForKey:kLastUpdateUnixTimestampKey];
|
||||
if ([ts respondsToSelector:@selector(doubleValue)]) {
|
||||
NSDate *lastUpdateDate = [NSDate dateWithTimeIntervalSince1970:[ts doubleValue]];
|
||||
self->_lastScheduleCenterConnectDate = lastUpdateDate;
|
||||
}
|
||||
[self updateRegionConfig:scheduleCenterResult];
|
||||
});
|
||||
}
|
||||
|
||||
// 根据指定的时间间隔检查是否需要更新
|
||||
- (void)asyncUpdateRegionConfigAfterAtLeast:(NSTimeInterval)interval {
|
||||
__block BOOL shouldUpdate = NO;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
NSDate *now = [NSDate date];
|
||||
if ([now timeIntervalSinceDate:self->_lastScheduleCenterConnectDate] > interval) {
|
||||
self->_lastScheduleCenterConnectDate = now;
|
||||
shouldUpdate = YES;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldUpdate) {
|
||||
[self asyncUpdateRegionScheduleConfig];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)asyncUpdateRegionScheduleConfig {
|
||||
[self asyncUpdateRegionScheduleConfigAtRetry:0];
|
||||
}
|
||||
|
||||
- (void)asyncUpdateRegionScheduleConfigAtRetry:(int)retryCount {
|
||||
if (retryCount > MAX_UPDATE_RETRY_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(_scheduleFetchConfigAsyncQueue, ^(void) {
|
||||
NSTimeInterval timeout = [HttpDnsService getInstanceByAccountId:self.accountId].timeoutInterval;
|
||||
HttpdnsScheduleExecutor *scheduleCenterExecutor = [[HttpdnsScheduleExecutor alloc] initWithAccountId:self.accountId timeout:timeout];
|
||||
|
||||
NSError *error = nil;
|
||||
NSString *updateHost = [self getActiveUpdateServerHost];
|
||||
NSDictionary *scheduleCenterResult = [scheduleCenterExecutor fetchRegionConfigFromServer:updateHost error:&error];
|
||||
if (error || !scheduleCenterResult) {
|
||||
HttpdnsLogDebug("Update region config failed, error: %@", error);
|
||||
|
||||
// 只有报错了就尝试选择新的调度服务器
|
||||
[self rotateUpdateServerHost];
|
||||
|
||||
// 3秒之后重试
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((retryCount + 1) * NSEC_PER_SEC)), self->_scheduleFetchConfigAsyncQueue, ^{
|
||||
[self asyncUpdateRegionScheduleConfigAtRetry:retryCount + 1];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary *toSave = [scheduleCenterResult mutableCopy];
|
||||
toSave[kLastUpdateUnixTimestampKey] = @([[NSDate date] timeIntervalSince1970]);
|
||||
|
||||
BOOL saveSuccess = [HttpdnsPersistenceUtils saveJSON:toSave toPath:self.scheduleCenterResultPath];
|
||||
HttpdnsLogDebug("Save region config to local cache %@", saveSuccess ? @"successfully" : @"failed");
|
||||
|
||||
[self updateRegionConfig:scheduleCenterResult];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateRegionConfig:(NSDictionary *)scheduleCenterResult {
|
||||
NSArray *v4Result = [scheduleCenterResult objectForKey:kAlicloudHttpdnsRegionConfigV4HostKey];
|
||||
NSArray *v6Result = [scheduleCenterResult objectForKey:kAlicloudHttpdnsRegionConfigV6HostKey];
|
||||
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
HttpdnsRegionConfigLoader *regionConfigLoader = [HttpdnsRegionConfigLoader sharedInstance];
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v4Result]) {
|
||||
self->_ipv4ServiceServerHostList = [v4Result copy];
|
||||
|
||||
// 调度server列表总是服务server列表加上兜底域名
|
||||
self->_ipv4UpdateServerHostList = [HttpdnsUtil joinArrays:v4Result
|
||||
withArray:[regionConfigLoader getUpdateV4FallbackHostList:self->_currentRegion]];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6Result]) {
|
||||
self->_ipv6ServiceServerHostList = [v6Result copy];
|
||||
|
||||
// 调度server列表总是服务server列表加上兜底域名
|
||||
self->_ipv6UpdateServerHostList = [HttpdnsUtil joinArrays:v6Result
|
||||
withArray:[regionConfigLoader getUpdateV6FallbackHostList:self->_currentRegion]];
|
||||
}
|
||||
|
||||
self->_currentActiveUpdateHostIndex = 0;
|
||||
self->_currentActiveServiceHostIndex = 0;
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)getActiveUpdateServerHost {
|
||||
HttpdnsIPStackType currentStack = [[HttpdnsIpStackDetector sharedInstance] currentIpStack];
|
||||
if (currentStack == kHttpdnsIpv6Only) {
|
||||
NSString *v6Host = [self currentActiveUpdateServerV6Host];
|
||||
if ([HttpdnsUtil isIPv6Address:v6Host]) {
|
||||
return [NSString stringWithFormat:@"[%@]", v6Host];
|
||||
}
|
||||
return v6Host;
|
||||
}
|
||||
|
||||
return [self currentActiveUpdateServerV4Host];
|
||||
}
|
||||
|
||||
- (void)initServerListByRegion:(NSString *)region {
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self->_currentRegion = region;
|
||||
|
||||
HttpdnsRegionConfigLoader *regionConfigLoader = [HttpdnsRegionConfigLoader sharedInstance];
|
||||
|
||||
self.ipv4ServiceServerHostList = [regionConfigLoader getSeriveV4HostList:region];
|
||||
self.ipv4UpdateServerHostList = [HttpdnsUtil joinArrays:[regionConfigLoader getSeriveV4HostList:region]
|
||||
withArray:[regionConfigLoader getUpdateV4FallbackHostList:region]];
|
||||
|
||||
self.ipv6ServiceServerHostList = [regionConfigLoader getSeriveV6HostList:region];
|
||||
self.ipv6UpdateServerHostList = [HttpdnsUtil joinArrays:[regionConfigLoader getSeriveV6HostList:region]
|
||||
withArray:[regionConfigLoader getUpdateV6FallbackHostList:region]];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)rotateServiceServerHost {
|
||||
__block int timeToUpdate = NO;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self.currentActiveServiceHostIndex++;
|
||||
|
||||
int total = (int)self.ipv4ServiceServerHostList.count + (int)self.ipv6ServiceServerHostList.count;
|
||||
if (self.currentActiveServiceHostIndex % total == 0) {
|
||||
timeToUpdate = YES;
|
||||
}
|
||||
});
|
||||
|
||||
if (timeToUpdate) {
|
||||
// 每次服务server列表轮转之后,尝试1个至少间隔30秒的更新
|
||||
[self asyncUpdateRegionConfigAfterAtLeast:30];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rotateUpdateServerHost {
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self.currentActiveUpdateHostIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)currentActiveUpdateServerV4Host {
|
||||
__block NSString *host = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
int count = (int)self.ipv4UpdateServerHostList.count;
|
||||
if (count == 0) {
|
||||
HttpdnsLogDebug("Severe error: update v4 ip list is empty, it should never happen");
|
||||
return;
|
||||
}
|
||||
int index = self.currentActiveUpdateHostIndex % count;
|
||||
host = self.ipv4UpdateServerHostList[index];
|
||||
});
|
||||
return host;
|
||||
}
|
||||
|
||||
- (NSString *)currentActiveServiceServerV4Host {
|
||||
// 每次读取时都检查是否需要更新,相当于实现一个懒加载的机制
|
||||
// 因为当前httpdns的初始化方式,没有一个统一的初始化入口,所以需要这样处理
|
||||
[self asyncUpdateRegionConfigAfterAtLeast:(24 * 60 * 60)];
|
||||
|
||||
// 检查是否存在HTTPDNS_DEBUG_V4_SERVICE_IP环境变量
|
||||
NSString *debugV4ServiceIP = [[[NSProcessInfo processInfo] environment] objectForKey:@"HTTPDNS_DEBUG_V4_SERVICE_IP"];
|
||||
if ([HttpdnsUtil isNotEmptyString:debugV4ServiceIP]) {
|
||||
HttpdnsLogDebug("Using debug v4 service IP from environment: %@", debugV4ServiceIP);
|
||||
return debugV4ServiceIP;
|
||||
}
|
||||
|
||||
__block NSString *host = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
int count = (int)self.ipv4ServiceServerHostList.count;
|
||||
if (count == 0) {
|
||||
HttpdnsLogDebug("Severe error: service v4 ip list is empty, it should never happen");
|
||||
return;
|
||||
}
|
||||
int index = self.currentActiveServiceHostIndex % count;
|
||||
host = self.ipv4ServiceServerHostList[index];
|
||||
});
|
||||
return host;
|
||||
}
|
||||
|
||||
- (NSString *)currentActiveUpdateServerV6Host {
|
||||
__block NSString *host = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
int count = (int)self.ipv6UpdateServerHostList.count;
|
||||
if (count == 0) {
|
||||
HttpdnsLogDebug("Severe error: update v6 ip list is empty, it should never happen");
|
||||
return;
|
||||
}
|
||||
int index = self.currentActiveUpdateHostIndex % count;
|
||||
host = self.ipv6UpdateServerHostList[index];
|
||||
});
|
||||
return host;
|
||||
}
|
||||
|
||||
- (NSString *)currentActiveServiceServerV6Host {
|
||||
// 同上
|
||||
[self asyncUpdateRegionConfigAfterAtLeast:(24 * 60 * 60)];
|
||||
|
||||
// 检查是否存在HTTPDNS_DEBUG_V6_SERVICE_IP环境变量
|
||||
NSString *debugV6ServiceIP = [[[NSProcessInfo processInfo] environment] objectForKey:@"HTTPDNS_DEBUG_V6_SERVICE_IP"];
|
||||
if ([HttpdnsUtil isNotEmptyString:debugV6ServiceIP]) {
|
||||
HttpdnsLogDebug("Using debug v6 service IP from environment: %@", debugV6ServiceIP);
|
||||
return debugV6ServiceIP;
|
||||
}
|
||||
|
||||
__block NSString *host = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
int count = (int)self.ipv6ServiceServerHostList.count;
|
||||
if (count == 0) {
|
||||
HttpdnsLogDebug("Severe error: service v6 ip list is empty, it should never happen");
|
||||
return;
|
||||
}
|
||||
int index = self.currentActiveServiceHostIndex % count;
|
||||
host = self.ipv6ServiceServerHostList[index];
|
||||
});
|
||||
|
||||
if ([HttpdnsUtil isIPv6Address:host]) {
|
||||
host = [NSString stringWithFormat:@"[%@]", host];
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
#pragma mark - For Test Only
|
||||
|
||||
- (NSArray<NSString *> *)currentUpdateServerV4HostList {
|
||||
__block NSArray<NSString *> *list = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
list = [self.ipv4UpdateServerHostList copy];
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)currentServiceServerV4HostList {
|
||||
__block NSArray<NSString *> *list = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
list = [self.ipv4ServiceServerHostList copy];
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)currentUpdateServerV6HostList {
|
||||
__block NSArray<NSString *> *list = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
list = [self.ipv6UpdateServerHostList copy];
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)currentServiceServerV6HostList {
|
||||
__block NSArray<NSString *> *list = nil;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
list = [self.ipv6ServiceServerHostList copy];
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
- (int)currentActiveUpdateServerHostIndex {
|
||||
__block int index = 0;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
index = self.currentActiveUpdateHostIndex;
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
- (int)currentActiveServiceServerHostIndex {
|
||||
__block int index = 0;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
index = self.currentActiveServiceHostIndex;
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// HttpdnsScheduleExecutor.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/11.
|
||||
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsScheduleExecutor : NSObject
|
||||
|
||||
- (NSDictionary *)fetchRegionConfigFromServer:(NSString *)updateHost error:(NSError **)pError;
|
||||
|
||||
// 多账号隔离:允许携带账号与超时初始化,避免依赖全局单例
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId timeout:(NSTimeInterval)timeoutInterval;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// HttpdnsScheduleExecutor.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/11.
|
||||
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsScheduleExecutor.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsReachability.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
|
||||
@interface HttpdnsScheduleExecutor ()
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPClient *httpClient;
|
||||
@end
|
||||
|
||||
@implementation HttpdnsScheduleExecutor {
|
||||
NSInteger _accountId;
|
||||
NSTimeInterval _timeoutInterval;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (!(self = [super init])) {
|
||||
return nil;
|
||||
}
|
||||
// 兼容旧路径:使用全局单例读取,但多账号场景下建议使用新init接口
|
||||
_accountId = [HttpDnsService sharedInstance].accountID;
|
||||
_timeoutInterval = [HttpDnsService sharedInstance].timeoutInterval;
|
||||
_httpClient = [HttpdnsNWHTTPClient sharedInstance];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId timeout:(NSTimeInterval)timeoutInterval {
|
||||
if (!(self = [self init])) {
|
||||
return nil;
|
||||
}
|
||||
_accountId = accountId;
|
||||
_timeoutInterval = timeoutInterval;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接 URL
|
||||
* 2024.6.12今天起,调度服务由后端就近调度,不再需要传入region参数,但为了兼容不传region默认就是国内region的逻辑,默认都传入region=global
|
||||
* https://203.107.1.1/100000/ss?region=global&platform=ios&sdk_version=3.1.7&sid=LpmJIA2CUoi4&net=wifi
|
||||
*/
|
||||
- (NSString *)constructRequestURLWithUpdateHost:(NSString *)updateHost {
|
||||
NSString *urlPath = [NSString stringWithFormat:@"%ld/ss?region=global&platform=ios&sdk_version=%@", (long)_accountId, HTTPDNS_IOS_SDK_VERSION];
|
||||
urlPath = [self urlFormatSidNetBssid:urlPath];
|
||||
urlPath = [urlPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];
|
||||
return [NSString stringWithFormat:@"https://%@/%@", updateHost, urlPath];
|
||||
}
|
||||
|
||||
// url 添加 sid net
|
||||
- (NSString *)urlFormatSidNetBssid:(NSString *)url {
|
||||
NSString *sessionId = [HttpdnsUtil generateSessionID];
|
||||
if ([HttpdnsUtil isNotEmptyString:sessionId]) {
|
||||
url = [NSString stringWithFormat:@"%@&sid=%@", url, sessionId];
|
||||
}
|
||||
|
||||
NSString *netType = [[HttpdnsReachability sharedInstance] currentReachabilityString];
|
||||
if ([HttpdnsUtil isNotEmptyString:netType]) {
|
||||
url = [NSString stringWithFormat:@"%@&net=%@", url, netType];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fetchRegionConfigFromServer:(NSString *)updateHost error:(NSError **)pError {
|
||||
NSString *fullUrlStr = [self constructRequestURLWithUpdateHost:updateHost];
|
||||
HttpdnsLogDebug("ScRequest URL: %@", fullUrlStr);
|
||||
NSTimeInterval timeout = _timeoutInterval > 0 ? _timeoutInterval : HTTPDNS_DEFAULT_REQUEST_TIMEOUT_INTERVAL;
|
||||
NSString *userAgent = [HttpdnsUtil generateUserAgent];
|
||||
|
||||
NSError *requestError = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.httpClient performRequestWithURLString:fullUrlStr
|
||||
userAgent:userAgent
|
||||
timeout:timeout
|
||||
error:&requestError];
|
||||
if (!response) {
|
||||
if (pError) {
|
||||
*pError = requestError;
|
||||
HttpdnsLogDebug("ScRequest failed with url: %@, error: %@", fullUrlStr, requestError);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
NSDictionary *dict = @{@"ResponseCode": [NSString stringWithFormat:@"%ld", (long)response.statusCode]};
|
||||
if (pError) {
|
||||
*pError = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_HTTPS_NO_DATA_ERROR_CODE
|
||||
userInfo:dict];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *jsonError = nil;
|
||||
id jsonValue = [NSJSONSerialization JSONObjectWithData:response.body options:kNilOptions error:&jsonError];
|
||||
if (jsonError) {
|
||||
if (pError) {
|
||||
*pError = jsonError;
|
||||
HttpdnsLogDebug("ScRequest JSON parse error, url: %@, error: %@", fullUrlStr, jsonError);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *result = [HttpdnsUtil getValidDictionaryFromJson:jsonValue];
|
||||
if (result) {
|
||||
HttpdnsLogDebug("ScRequest get response: %@", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (pError) {
|
||||
*pError = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTP_PARSE_JSON_FAILED
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse JSON response"}];
|
||||
}
|
||||
if (pError != NULL) {
|
||||
HttpdnsLogDebug("ScRequest failed with url: %@, response body invalid", fullUrlStr);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
28
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpDnsLocker.h
Normal file
28
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpDnsLocker.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// HttpDnsLocker.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by 王贇 on 2023/8/16.
|
||||
// Copyright © 2023 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef HttpDnsLocker_h
|
||||
#define HttpDnsLocker_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@interface HttpDnsLocker : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (void)lock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
- (BOOL)tryLock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
- (void)unlock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#endif /* HttpDnsLocker_h */
|
||||
91
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpDnsLocker.m
Normal file
91
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpDnsLocker.m
Normal file
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// HttpDnsLocker.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by 王贇 on 2023/8/16.
|
||||
// Copyright © 2023 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpDnsLocker.h"
|
||||
#import "HttpdnsService.h"
|
||||
|
||||
@implementation HttpDnsLocker {
|
||||
NSMutableDictionary<NSString*, NSLock*> *_v4LockMap;
|
||||
NSMutableDictionary<NSString*, NSLock*> *_v6LockMap;
|
||||
NSMutableDictionary<NSString*, NSLock*> *_v4v6LockMap;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static HttpDnsLocker *locker = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
locker = [[HttpDnsLocker alloc] init];
|
||||
});
|
||||
return locker;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_v4LockMap = [NSMutableDictionary dictionary];
|
||||
_v6LockMap = [NSMutableDictionary dictionary];
|
||||
_v4v6LockMap = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)lock:(NSString *)host queryType:(HttpdnsQueryIPType)queryIpType {
|
||||
NSLock *condition = [self getLock:host queryType:queryIpType];
|
||||
|
||||
if (condition) {
|
||||
[condition lock];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tryLock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType {
|
||||
NSLock *condition = [self getLock:host queryType:queryType];
|
||||
|
||||
if (condition) {
|
||||
return [condition tryLock];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)unlock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType {
|
||||
NSLock *condition = [self getLock:host queryType:queryType];
|
||||
if (condition) {
|
||||
[condition unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSLock *)getLock:(NSString *)host queryType:(HttpdnsQueryIPType)queryType {
|
||||
if (queryType == HttpdnsQueryIPTypeIpv4) {
|
||||
@synchronized (_v4LockMap) {
|
||||
NSLock *lock = [_v4LockMap objectForKey:host];
|
||||
if (!lock) {
|
||||
lock = [[NSLock alloc] init];
|
||||
[_v4LockMap setObject:lock forKey:host];
|
||||
}
|
||||
return lock;
|
||||
}
|
||||
} else if (queryType == HttpdnsQueryIPTypeIpv6) {
|
||||
@synchronized (_v6LockMap) {
|
||||
NSLock *condition = [_v6LockMap objectForKey:host];
|
||||
if (!condition) {
|
||||
condition = [[NSLock alloc] init];
|
||||
[_v6LockMap setObject:condition forKey:host];
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
} else {
|
||||
@synchronized (_v4v6LockMap) {
|
||||
NSLock *condition = [_v4v6LockMap objectForKey:host];
|
||||
if (!condition) {
|
||||
condition = [[NSLock alloc] init];
|
||||
[_v4v6LockMap setObject:condition forKey:host];
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// HttpdnsHostObjectInMemoryCache.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/9/28.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsHostObject.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// 这个字典在HTTPDNS中只用于存储HttpdnsHostObject对象,这个对象是整个框架的核心对象,用于缓存和处理域名解析结果
|
||||
// 通常从缓存中获得这个对象之后,会根据不同场景改变一些字段的值,而且很可能发生在不同线程中
|
||||
// 而不同线程从缓存中直接读取共享对象的话,很有可能发生线程竞争的情况,多线程访问某个对象的同一个字段,在swift环境有较高概率发生crash
|
||||
// 因此,除了确保字典操作的线程安全,拿出对象的时候,也直接copy一个复制对象返回(HttpdnsHostObject对象实现了NSCopying协议)
|
||||
@interface HttpdnsHostObjectInMemoryCache : NSObject
|
||||
|
||||
- (void)setHostObject:(HttpdnsHostObject *)object forCacheKey:(NSString *)key;
|
||||
|
||||
- (HttpdnsHostObject *)getHostObjectByCacheKey:(NSString *)key;
|
||||
|
||||
- (HttpdnsHostObject *)getHostObjectByCacheKey:(NSString *)key createIfNotExists:(HttpdnsHostObject *(^)(void))objectProducer;
|
||||
|
||||
- (void)updateQualityForCacheKey:(NSString *)key forIp:(NSString *)ip withConnectedRT:(NSInteger)connectedRT;
|
||||
|
||||
- (void)removeHostObjectByCacheKey:(NSString *)key;
|
||||
|
||||
- (void)removeAllHostObjects;
|
||||
|
||||
- (NSInteger)count;
|
||||
|
||||
- (NSArray *)allCacheKeys;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// HttpdnsHostObjectInMemoryCache.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/9/28.
|
||||
// Copyright © 2024 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsHostObjectInMemoryCache.h"
|
||||
|
||||
@interface HttpdnsHostObjectInMemoryCache ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, HttpdnsHostObject *> *cacheDict;
|
||||
@property (nonatomic, strong) NSLock *lock;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsHostObjectInMemoryCache
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_cacheDict = [NSMutableDictionary dictionary];
|
||||
_lock = [[NSLock alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setHostObject:(HttpdnsHostObject *)object forCacheKey:(NSString *)key {
|
||||
[_lock lock];
|
||||
_cacheDict[key] = object;
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)getHostObjectByCacheKey:(NSString *)key {
|
||||
[_lock lock];
|
||||
@try {
|
||||
HttpdnsHostObject *object = _cacheDict[key];
|
||||
return [object copy];
|
||||
} @finally {
|
||||
[_lock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)getHostObjectByCacheKey:(NSString *)key createIfNotExists:(HttpdnsHostObject *(^)(void))objectProducer {
|
||||
[_lock lock];
|
||||
HttpdnsHostObject *object = _cacheDict[key];
|
||||
@try {
|
||||
if (!object) {
|
||||
object = objectProducer();
|
||||
_cacheDict[key] = object;
|
||||
}
|
||||
return [object copy];
|
||||
} @finally {
|
||||
[_lock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateQualityForCacheKey:(NSString *)key forIp:(NSString *)ip withConnectedRT:(NSInteger)connectedRT {
|
||||
[_lock lock];
|
||||
HttpdnsHostObject *object = _cacheDict[key];
|
||||
if (object) {
|
||||
[object updateConnectedRT:connectedRT forIP:ip];
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)removeHostObjectByCacheKey:(NSString *)key {
|
||||
[_lock lock];
|
||||
[_cacheDict removeObjectForKey:key];
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)removeAllHostObjects {
|
||||
[_lock lock];
|
||||
[_cacheDict removeAllObjects];
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (NSInteger)count {
|
||||
[_lock lock];
|
||||
NSInteger count = _cacheDict.count;
|
||||
[_lock unlock];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSArray *)allCacheKeys {
|
||||
[_lock lock];
|
||||
NSArray *keys = [_cacheDict allKeys];
|
||||
[_lock unlock];
|
||||
return keys;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// HttpdnsIPQualityDetector.h
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/13.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* IP质量检测回调
|
||||
* @param cacheKey 缓存键
|
||||
* @param ip IP地址
|
||||
* @param costTime 连接耗时(毫秒),-1表示连接失败
|
||||
*/
|
||||
typedef void(^HttpdnsIPQualityCallback)(NSString *cacheKey, NSString *ip, NSInteger costTime);
|
||||
|
||||
@interface HttpdnsIPQualityDetector : NSObject
|
||||
|
||||
/**
|
||||
* 单例方法
|
||||
*/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/**
|
||||
* 获取当前等待队列中的任务数量
|
||||
*/
|
||||
- (NSUInteger)pendingTasksCount;
|
||||
|
||||
/**
|
||||
* 调度一个IP连接质量检测任务,不会阻塞当前线程
|
||||
* @param cacheKey 缓存键,通常是域名
|
||||
* @param ip 要检测的IP地址
|
||||
* @param port 连接端口,如果为nil则默认使用80
|
||||
* @param callback 检测完成后的回调
|
||||
*/
|
||||
- (void)scheduleIPQualityDetection:(NSString *)cacheKey
|
||||
ip:(NSString *)ip
|
||||
port:(nullable NSNumber *)port
|
||||
callback:(HttpdnsIPQualityCallback)callback;
|
||||
|
||||
#pragma mark - Methods exposed for testing
|
||||
|
||||
/**
|
||||
* 执行IP连接质量检测
|
||||
* @param cacheKey 缓存键,通常是域名
|
||||
* @param ip 要检测的IP地址
|
||||
* @param port 连接端口,如果为nil则默认使用80
|
||||
* @param callback 检测完成后的回调
|
||||
* @note 此方法主要用于测试
|
||||
*/
|
||||
- (void)executeDetection:(NSString *)cacheKey
|
||||
ip:(NSString *)ip
|
||||
port:(nullable NSNumber *)port
|
||||
callback:(HttpdnsIPQualityCallback)callback;
|
||||
|
||||
/**
|
||||
* 建立TCP连接并测量连接时间
|
||||
* @param ip 要连接的IP地址
|
||||
* @param port 连接端口
|
||||
* @return 连接耗时(毫秒),-1表示连接失败
|
||||
* @note 此方法主要用于测试
|
||||
*/
|
||||
- (NSInteger)tcpConnectToIP:(NSString *)ip port:(int)port;
|
||||
|
||||
/**
|
||||
* 添加待处理任务
|
||||
* @param cacheKey 缓存键,通常是域名
|
||||
* @param ip 要检测的IP地址
|
||||
* @param port 连接端口
|
||||
* @param callback 检测完成后的回调
|
||||
* @note 此方法主要用于测试
|
||||
*/
|
||||
- (void)addPendingTask:(NSString *)cacheKey
|
||||
ip:(NSString *)ip
|
||||
port:(nullable NSNumber *)port
|
||||
callback:(HttpdnsIPQualityCallback)callback;
|
||||
|
||||
/**
|
||||
* 处理待处理任务队列
|
||||
* @note 此方法主要用于测试
|
||||
*/
|
||||
- (void)processPendingTasksIfNeeded;
|
||||
|
||||
/**
|
||||
* 处理所有待处理任务
|
||||
* @note 此方法主要用于测试
|
||||
*/
|
||||
- (void)processPendingTasks;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// HttpdnsIPQualityDetector.m
|
||||
// AlicloudHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2025/3/13.
|
||||
// Copyright © 2025 alibaba-inc.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsIPQualityDetector.h"
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <netdb.h>
|
||||
#import <unistd.h>
|
||||
#import <sys/time.h>
|
||||
#import <fcntl.h>
|
||||
#import <errno.h>
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
// 定义任务类,替代之前的结构体,确保正确的内存管理
|
||||
@interface HttpdnsDetectionTask : NSObject
|
||||
@property (nonatomic, copy) NSString *cacheKey;
|
||||
@property (nonatomic, copy) NSString *ip;
|
||||
@property (nonatomic, strong) NSNumber *port;
|
||||
@property (nonatomic, copy) HttpdnsIPQualityCallback callback;
|
||||
@end
|
||||
|
||||
@implementation HttpdnsDetectionTask
|
||||
@end
|
||||
|
||||
@interface HttpdnsIPQualityDetector ()
|
||||
|
||||
@property (nonatomic, strong) dispatch_queue_t detectQueue;
|
||||
@property (nonatomic, strong) dispatch_semaphore_t concurrencySemaphore;
|
||||
@property (nonatomic, strong) NSMutableArray<HttpdnsDetectionTask *> *pendingTasks;
|
||||
@property (nonatomic, strong) NSLock *pendingTasksLock;
|
||||
@property (nonatomic, assign) BOOL isProcessingPendingTasks;
|
||||
|
||||
/**
|
||||
* 最大并发检测数量,默认为10
|
||||
*/
|
||||
@property (nonatomic, assign) NSUInteger maxConcurrentDetections;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsIPQualityDetector
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static HttpdnsIPQualityDetector *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[HttpdnsIPQualityDetector alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_detectQueue = dispatch_queue_create("com.aliyun.httpdns.ipqualitydetector", DISPATCH_QUEUE_CONCURRENT);
|
||||
_maxConcurrentDetections = 10;
|
||||
_concurrencySemaphore = dispatch_semaphore_create(_maxConcurrentDetections);
|
||||
_pendingTasks = [NSMutableArray array];
|
||||
_pendingTasksLock = [[NSLock alloc] init];
|
||||
_isProcessingPendingTasks = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)scheduleIPQualityDetection:(NSString *)cacheKey
|
||||
ip:(NSString *)ip
|
||||
port:(NSNumber *)port
|
||||
callback:(HttpdnsIPQualityCallback)callback {
|
||||
if (!cacheKey || !ip || !callback) {
|
||||
HttpdnsLogDebug("IPQualityDetector invalid parameters for detection: cacheKey=%@, ip=%@", cacheKey, ip);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试获取信号量,如果获取不到,说明已达到最大并发数
|
||||
if (dispatch_semaphore_wait(_concurrencySemaphore, DISPATCH_TIME_NOW) != 0) {
|
||||
// 将任务加入等待队列
|
||||
[self addPendingTask:cacheKey ip:ip port:port callback:callback];
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取到信号量,可以执行检测
|
||||
[self executeDetection:cacheKey ip:ip port:port callback:callback];
|
||||
}
|
||||
|
||||
- (void)addPendingTask:(NSString *)cacheKey ip:(NSString *)ip port:(NSNumber *)port callback:(HttpdnsIPQualityCallback)callback {
|
||||
// 创建任务对象,ARC会自动管理内存
|
||||
HttpdnsDetectionTask *task = [[HttpdnsDetectionTask alloc] init];
|
||||
task.cacheKey = cacheKey;
|
||||
task.ip = ip;
|
||||
task.port = port;
|
||||
task.callback = callback;
|
||||
|
||||
// 加锁添加任务
|
||||
[_pendingTasksLock lock];
|
||||
[_pendingTasks addObject:task];
|
||||
[_pendingTasksLock unlock];
|
||||
|
||||
// 如果没有正在处理等待队列,则开始处理
|
||||
[self processPendingTasksIfNeeded];
|
||||
}
|
||||
|
||||
- (NSUInteger)pendingTasksCount {
|
||||
[_pendingTasksLock lock];
|
||||
NSUInteger count = _pendingTasks.count;
|
||||
[_pendingTasksLock unlock];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (void)processPendingTasksIfNeeded {
|
||||
[_pendingTasksLock lock];
|
||||
BOOL shouldProcess = !_isProcessingPendingTasks && _pendingTasks.count > 0;
|
||||
if (shouldProcess) {
|
||||
_isProcessingPendingTasks = YES;
|
||||
}
|
||||
[_pendingTasksLock unlock];
|
||||
|
||||
if (shouldProcess) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self processPendingTasks];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processPendingTasks {
|
||||
while (1) {
|
||||
// 尝试获取信号量
|
||||
if (dispatch_semaphore_wait(_concurrencySemaphore, DISPATCH_TIME_NOW) != 0) {
|
||||
// 无法获取信号量,等待一段时间后重试
|
||||
[NSThread sleepForTimeInterval:0.1];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取到信号量,取出一个等待任务
|
||||
HttpdnsDetectionTask *task = nil;
|
||||
|
||||
[_pendingTasksLock lock];
|
||||
if (_pendingTasks.count > 0) {
|
||||
task = _pendingTasks.firstObject;
|
||||
[_pendingTasks removeObjectAtIndex:0];
|
||||
} else {
|
||||
// 没有等待任务了,结束处理
|
||||
_isProcessingPendingTasks = NO;
|
||||
[_pendingTasksLock unlock];
|
||||
// 释放多余的信号量
|
||||
dispatch_semaphore_signal(_concurrencySemaphore);
|
||||
break;
|
||||
}
|
||||
[_pendingTasksLock unlock];
|
||||
|
||||
// 执行任务
|
||||
[self executeDetection:task.cacheKey ip:task.ip port:task.port callback:task.callback];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)executeDetection:(NSString *)cacheKey ip:(NSString *)ip port:(NSNumber *)port callback:(HttpdnsIPQualityCallback)callback {
|
||||
// 创建强引用以确保在异步操作期间对象不会被释放
|
||||
HttpdnsIPQualityCallback strongCallback = [callback copy];
|
||||
|
||||
// 使用后台队列进行检测,避免阻塞主线程
|
||||
dispatch_async(self.detectQueue, ^{
|
||||
NSInteger costTime = [self tcpConnectToIP:ip port:port ? [port intValue] : 80];
|
||||
|
||||
// 在后台线程回调结果
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
strongCallback(cacheKey, ip, costTime);
|
||||
|
||||
// 释放信号量,允许执行下一个任务
|
||||
dispatch_semaphore_signal(self->_concurrencySemaphore);
|
||||
|
||||
// 检查是否有等待的任务需要处理
|
||||
[self processPendingTasksIfNeeded];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (NSInteger)tcpConnectToIP:(NSString *)ip port:(int)port {
|
||||
if (!ip || port <= 0) {
|
||||
return -1;
|
||||
}
|
||||
int socketFd;
|
||||
struct sockaddr_in serverAddr;
|
||||
struct sockaddr_in6 serverAddr6;
|
||||
void *serverAddrPtr;
|
||||
socklen_t serverAddrLen;
|
||||
BOOL isIPv6 = [HttpdnsUtil isIPv6Address:ip];
|
||||
BOOL isIpv4 = [HttpdnsUtil isIPv4Address:ip];
|
||||
|
||||
// 创建socket
|
||||
if (isIPv6) {
|
||||
socketFd = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
if (socketFd < 0) {
|
||||
HttpdnsLogDebug("IPQualityDetector failed to create IPv6 socket: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&serverAddr6, 0, sizeof(serverAddr6));
|
||||
serverAddr6.sin6_family = AF_INET6;
|
||||
serverAddr6.sin6_port = htons(port);
|
||||
inet_pton(AF_INET6, [ip UTF8String], &serverAddr6.sin6_addr);
|
||||
|
||||
serverAddrPtr = &serverAddr6;
|
||||
serverAddrLen = sizeof(serverAddr6);
|
||||
} else if (isIpv4) {
|
||||
socketFd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (socketFd < 0) {
|
||||
HttpdnsLogDebug("IPQualityDetector failed to create IPv4 socket: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&serverAddr, 0, sizeof(serverAddr));
|
||||
serverAddr.sin_family = AF_INET;
|
||||
serverAddr.sin_port = htons(port);
|
||||
inet_pton(AF_INET, [ip UTF8String], &serverAddr.sin_addr);
|
||||
|
||||
serverAddrPtr = &serverAddr;
|
||||
serverAddrLen = sizeof(serverAddr);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 设置非阻塞模式
|
||||
int flags = fcntl(socketFd, F_GETFL, 0);
|
||||
fcntl(socketFd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// 开始计时
|
||||
struct timeval startTime, endTime;
|
||||
gettimeofday(&startTime, NULL);
|
||||
|
||||
// 尝试连接
|
||||
int connectResult = connect(socketFd, serverAddrPtr, serverAddrLen);
|
||||
|
||||
if (connectResult < 0) {
|
||||
if (errno == EINPROGRESS) {
|
||||
// 连接正在进行中,使用select等待
|
||||
fd_set fdSet;
|
||||
struct timeval timeout;
|
||||
|
||||
FD_ZERO(&fdSet);
|
||||
FD_SET(socketFd, &fdSet);
|
||||
|
||||
// 设置超时时间为2秒
|
||||
// 更长的超时时间不是很有必要,因为建连超过2秒的IP,已经没有优选必要了
|
||||
timeout.tv_sec = 2;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
int selectResult = select(socketFd + 1, NULL, &fdSet, NULL, &timeout);
|
||||
|
||||
if (selectResult <= 0) {
|
||||
// 超时或错误
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ timed out or error: %s", ip, strerror(errno));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
} else {
|
||||
// 检查连接是否成功
|
||||
int error;
|
||||
socklen_t errorLen = sizeof(error);
|
||||
if (getsockopt(socketFd, SOL_SOCKET, SO_ERROR, &error, &errorLen) < 0 || error != 0) {
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ failed after select: %s", ip, strerror(error));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 其他错误
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ failed: %s", ip, strerror(errno));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 结束计时
|
||||
gettimeofday(&endTime, NULL);
|
||||
|
||||
// 关闭socket
|
||||
close(socketFd);
|
||||
|
||||
// 计算耗时(毫秒)
|
||||
long seconds = endTime.tv_sec - startTime.tv_sec;
|
||||
long microseconds = endTime.tv_usec - startTime.tv_usec;
|
||||
NSInteger costTime = (seconds * 1000) + (microseconds / 1000);
|
||||
|
||||
return costTime;
|
||||
}
|
||||
|
||||
@end
|
||||
106
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsReachability.h
Normal file
106
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsReachability.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
//! Project version number for MacOSReachability.
|
||||
FOUNDATION_EXPORT double ReachabilityVersionNumber;
|
||||
|
||||
//! Project version string for MacOSReachability.
|
||||
FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
|
||||
|
||||
/**
|
||||
* Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X.
|
||||
*
|
||||
* @see http://nshipster.com/ns_enum-ns_options/
|
||||
**/
|
||||
#ifndef NS_ENUM
|
||||
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
|
||||
#endif
|
||||
|
||||
extern NSString *const kHttpdnsReachabilityChangedNotification;
|
||||
|
||||
typedef NS_ENUM(NSInteger, HttpdnsNetworkStatus) {
|
||||
// Apple NetworkStatus Compatible Names.
|
||||
HttpdnsNotReachable = 0,
|
||||
HttpdnsReachableViaWiFi = 1,
|
||||
HttpdnsReachableVia2G = 2,
|
||||
HttpdnsReachableVia3G = 3,
|
||||
HttpdnsReachableVia4G = 4,
|
||||
HttpdnsReachableVia5G = 5
|
||||
};
|
||||
|
||||
@class HttpdnsReachability;
|
||||
|
||||
typedef void (^NetworkReachable)(HttpdnsReachability * reachability);
|
||||
typedef void (^NetworkUnreachable)(HttpdnsReachability * reachability);
|
||||
typedef void (^NetworkReachability)(HttpdnsReachability * reachability, SCNetworkConnectionFlags flags);
|
||||
|
||||
|
||||
@interface HttpdnsReachability : NSObject
|
||||
|
||||
@property (nonatomic, copy) NetworkReachable reachableBlock;
|
||||
@property (nonatomic, copy) NetworkUnreachable unreachableBlock;
|
||||
@property (nonatomic, copy) NetworkReachability reachabilityBlock;
|
||||
|
||||
@property (nonatomic, assign) BOOL reachableOnWWAN;
|
||||
|
||||
+(instancetype)sharedInstance;
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname;
|
||||
// This is identical to the function above, but is here to maintain
|
||||
//compatibility with Apples original code. (see .m)
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname;
|
||||
+(instancetype)reachabilityForInternetConnection;
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress;
|
||||
+(instancetype)reachabilityForLocalWiFi;
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url;
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref;
|
||||
|
||||
-(BOOL)startNotifier;
|
||||
-(void)stopNotifier;
|
||||
|
||||
-(BOOL)isReachable;
|
||||
-(BOOL)isReachableViaWWAN;
|
||||
-(BOOL)isReachableViaWiFi;
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired; // Identical DDG variant.
|
||||
-(BOOL)connectionRequired; // Apple's routine.
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand;
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired;
|
||||
|
||||
-(HttpdnsNetworkStatus)currentReachabilityStatus;
|
||||
-(SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
-(NSString*)currentReachabilityString;
|
||||
-(NSString*)currentReachabilityFlags;
|
||||
|
||||
@end
|
||||
559
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsReachability.m
Normal file
559
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsReachability.m
Normal file
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "HttpdnsReachability.h"
|
||||
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
#import <netinet6/in6.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#import <netdb.h>
|
||||
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
|
||||
|
||||
NSString *const kHttpdnsReachabilityChangedNotification = @"kHttpdnsReachabilityChangedNotification";
|
||||
|
||||
static CTTelephonyNetworkInfo *networkInfo;
|
||||
|
||||
@interface HttpdnsReachability ()
|
||||
|
||||
@property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef;
|
||||
@property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue;
|
||||
@property (nonatomic, strong) id reachabilityObject;
|
||||
|
||||
-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags;
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags)
|
||||
{
|
||||
return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c",
|
||||
#if TARGET_OS_IPHONE
|
||||
(flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-',
|
||||
#else
|
||||
'X',
|
||||
#endif
|
||||
(flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-'];
|
||||
}
|
||||
|
||||
// Start listening for reachability notifications on the current run loop
|
||||
static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
|
||||
{
|
||||
#pragma unused (target)
|
||||
|
||||
HttpdnsReachability *reachability = ((__bridge HttpdnsReachability*)info);
|
||||
|
||||
// We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool,
|
||||
// but what the heck eh?
|
||||
@autoreleasepool
|
||||
{
|
||||
[reachability reachabilityChanged:flags];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@implementation HttpdnsReachability
|
||||
|
||||
#pragma mark - Class Constructor Methods
|
||||
+(instancetype)sharedInstance
|
||||
{
|
||||
static HttpdnsReachability *instance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
networkInfo = [[CTTelephonyNetworkInfo alloc] init];
|
||||
instance = [HttpdnsReachability reachabilityWithHostname:@"www.taobao.com"];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname
|
||||
{
|
||||
return [HttpdnsReachability reachabilityWithHostname:hostname];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForInternetConnection
|
||||
{
|
||||
struct sockaddr_in zeroAddress;
|
||||
bzero(&zeroAddress, sizeof(zeroAddress));
|
||||
zeroAddress.sin_len = sizeof(zeroAddress);
|
||||
zeroAddress.sin_family = AF_INET;
|
||||
|
||||
return [self reachabilityWithAddress:&zeroAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForLocalWiFi
|
||||
{
|
||||
struct sockaddr_in localWifiAddress;
|
||||
bzero(&localWifiAddress, sizeof(localWifiAddress));
|
||||
localWifiAddress.sin_len = sizeof(localWifiAddress);
|
||||
localWifiAddress.sin_family = AF_INET;
|
||||
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
|
||||
localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM);
|
||||
|
||||
return [self reachabilityWithAddress:&localWifiAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url
|
||||
{
|
||||
id reachability;
|
||||
|
||||
NSString *host = url.host;
|
||||
BOOL isIpAddress = [self isIpAddress:host];
|
||||
|
||||
if (isIpAddress)
|
||||
{
|
||||
NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80);
|
||||
|
||||
struct sockaddr_in address;
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons([port intValue]);
|
||||
address.sin_addr.s_addr = inet_addr([host UTF8String]);
|
||||
|
||||
reachability = [self reachabilityWithAddress:&address];
|
||||
}
|
||||
else
|
||||
{
|
||||
reachability = [self reachabilityWithHostname:host];
|
||||
}
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
+(BOOL)isIpAddress:(NSString*)host
|
||||
{
|
||||
struct in_addr pin;
|
||||
return 1 == inet_aton([host UTF8String], &pin);
|
||||
}
|
||||
|
||||
|
||||
// Initialization methods
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
self.reachableOnWWAN = YES;
|
||||
self.reachabilityRef = ref;
|
||||
|
||||
// We need to create a serial queue.
|
||||
// We allocate this once for the lifetime of the notifier.
|
||||
|
||||
self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)dealloc
|
||||
{
|
||||
[self stopNotifier];
|
||||
|
||||
if(self.reachabilityRef)
|
||||
{
|
||||
CFRelease(self.reachabilityRef);
|
||||
self.reachabilityRef = nil;
|
||||
}
|
||||
|
||||
self.reachableBlock = nil;
|
||||
self.unreachableBlock = nil;
|
||||
self.reachabilityBlock = nil;
|
||||
self.reachabilitySerialQueue = nil;
|
||||
}
|
||||
|
||||
#pragma mark - Notifier Methods
|
||||
|
||||
// Notifier
|
||||
// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD
|
||||
// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS.
|
||||
// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want)
|
||||
|
||||
-(BOOL)startNotifier
|
||||
{
|
||||
// allow start notifier to be called multiple times
|
||||
if(self.reachabilityObject && (self.reachabilityObject == self))
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL };
|
||||
context.info = (__bridge void *)self;
|
||||
|
||||
if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context))
|
||||
{
|
||||
// Set it as our reachability queue, which will retain the queue
|
||||
if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue))
|
||||
{
|
||||
// this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves
|
||||
// woah
|
||||
self.reachabilityObject = self;
|
||||
return YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
|
||||
// UH OH - FAILURE - stop any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
}
|
||||
|
||||
// if we get here we fail at the internet
|
||||
self.reachabilityObject = nil;
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(void)stopNotifier
|
||||
{
|
||||
// First stop, any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
|
||||
// Unregister target from the GCD serial dispatch queue.
|
||||
SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL);
|
||||
|
||||
self.reachabilityObject = nil;
|
||||
}
|
||||
|
||||
#pragma mark - reachability tests
|
||||
|
||||
// This is for the case where you flick the airplane mode;
|
||||
// you end up getting something like this:
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
// We treat this as 4 UNREACHABLE triggers - really apple should do better than this
|
||||
|
||||
#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
|
||||
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags
|
||||
{
|
||||
BOOL connectionUP = YES;
|
||||
|
||||
if(!(flags & kSCNetworkReachabilityFlagsReachable))
|
||||
connectionUP = NO;
|
||||
|
||||
if( (flags & testcase) == testcase )
|
||||
connectionUP = NO;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
// We're on 3G.
|
||||
if(!self.reachableOnWWAN)
|
||||
{
|
||||
// We don't want to connect when on 3G.
|
||||
connectionUP = NO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return connectionUP;
|
||||
}
|
||||
|
||||
-(BOOL)isReachable
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
return NO;
|
||||
|
||||
return [self isReachableWithFlags:flags];
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWWAN
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're REACHABLE
|
||||
if(flags & kSCNetworkReachabilityFlagsReachable)
|
||||
{
|
||||
// Now, check we're on WWAN
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWiFi
|
||||
{
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're reachable
|
||||
if((flags & kSCNetworkReachabilityFlagsReachable))
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
// Check we're NOT on WWAN
|
||||
if((flags & kSCNetworkReachabilityFlagsIsWWAN))
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired
|
||||
{
|
||||
return [self connectionRequired];
|
||||
}
|
||||
|
||||
-(BOOL)connectionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand)));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - reachability status stuff
|
||||
|
||||
-(HttpdnsNetworkStatus)currentReachabilityStatus
|
||||
{
|
||||
if([self isReachable])
|
||||
{
|
||||
if([self isReachableViaWiFi])
|
||||
{
|
||||
return HttpdnsReachableViaWiFi;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
NSString *nettype = networkInfo.currentRadioAccessTechnology;
|
||||
if (nettype)
|
||||
{
|
||||
if ([CTRadioAccessTechnologyGPRS isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyEdge isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMA1x isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia2G;
|
||||
}
|
||||
else if ([CTRadioAccessTechnologyLTE isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia4G;
|
||||
}
|
||||
else if ([CTRadioAccessTechnologyWCDMA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyHSDPA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyHSUPA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORev0 isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORevA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORevB isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyeHRPD isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia3G;
|
||||
}
|
||||
else
|
||||
{
|
||||
return HttpdnsReachableVia5G;
|
||||
}
|
||||
}
|
||||
// 默认以使用最广泛的4G兜底
|
||||
return HttpdnsReachableVia4G;
|
||||
#endif
|
||||
}
|
||||
|
||||
return HttpdnsNotReachable;
|
||||
}
|
||||
|
||||
-(SCNetworkReachabilityFlags)reachabilityFlags
|
||||
{
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return flags;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
-(NSString*)currentReachabilityString
|
||||
{
|
||||
HttpdnsNetworkStatus temp = [self currentReachabilityStatus];
|
||||
|
||||
if(temp == HttpdnsReachableVia2G)
|
||||
{
|
||||
return NSLocalizedString(@"2G", @"");
|
||||
}
|
||||
if(temp == HttpdnsReachableVia3G)
|
||||
{
|
||||
return NSLocalizedString(@"3G", @"");
|
||||
}
|
||||
if(temp == HttpdnsReachableVia4G)
|
||||
{
|
||||
return NSLocalizedString(@"4G", @"");
|
||||
}
|
||||
if(temp == HttpdnsReachableVia5G)
|
||||
{
|
||||
return NSLocalizedString(@"5G", @"");
|
||||
}
|
||||
if (temp == HttpdnsReachableViaWiFi)
|
||||
{
|
||||
// 旧版本上报的wifi标识为全小写,要保持兼容
|
||||
return NSLocalizedString(@"wifi", @"");
|
||||
}
|
||||
return NSLocalizedString(@"unknown", @"");
|
||||
}
|
||||
|
||||
-(NSString*)currentReachabilityFlags
|
||||
{
|
||||
return reachabilityFlags([self reachabilityFlags]);
|
||||
}
|
||||
|
||||
#pragma mark - Callback function calls this method
|
||||
|
||||
-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags
|
||||
{
|
||||
if([self isReachableWithFlags:flags])
|
||||
{
|
||||
if(self.reachableBlock)
|
||||
{
|
||||
self.reachableBlock(self);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(self.unreachableBlock)
|
||||
{
|
||||
self.unreachableBlock(self);
|
||||
}
|
||||
}
|
||||
|
||||
if(self.reachabilityBlock)
|
||||
{
|
||||
self.reachabilityBlock(self, flags);
|
||||
}
|
||||
|
||||
// this makes sure the change notification happens on the MAIN THREAD
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kHttpdnsReachabilityChangedNotification
|
||||
object:self];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Debug Description
|
||||
|
||||
- (NSString *) description
|
||||
{
|
||||
NSString *description = [NSString stringWithFormat:@"<%@: %p (%@)>",
|
||||
NSStringFromClass([self class]), self, [self currentReachabilityFlags]];
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
81
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsUtil.h
Normal file
81
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsUtil.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class HttpDnsService;
|
||||
|
||||
@class HttpdnsHostObject;
|
||||
|
||||
@interface HttpdnsUtil : NSObject
|
||||
|
||||
+ (BOOL)isIPv4Address:(NSString *)addr;
|
||||
|
||||
+ (BOOL)isIPv6Address:(NSString *)addr;
|
||||
|
||||
+ (BOOL)isAnIP:(NSString *)candidate;
|
||||
|
||||
+ (BOOL)isAHost:(NSString *)host;
|
||||
|
||||
+ (void)warnMainThreadIfNecessary;
|
||||
|
||||
+ (NSDictionary *)getValidDictionaryFromJson:(id)jsonValue;
|
||||
|
||||
+ (BOOL)isEmptyArray:(NSArray *)inputArr;
|
||||
|
||||
+ (BOOL)isNotEmptyArray:(NSArray *)inputArr;
|
||||
|
||||
+ (BOOL)isEmptyString:(NSString *)inputStr;
|
||||
|
||||
+ (BOOL)isNotEmptyString:(NSString *)inputStr;
|
||||
|
||||
+ (BOOL)isValidJSON:(id)JSON;
|
||||
|
||||
+ (BOOL)isEmptyDictionary:(NSDictionary *)inputDict;
|
||||
|
||||
+ (BOOL)isNotEmptyDictionary:(NSDictionary *)inputDict;
|
||||
|
||||
+ (NSArray *)joinArrays:(NSArray *)array1 withArray:(NSArray *)array2;
|
||||
|
||||
+ (NSString *)getMD5StringFrom:(NSString *)originString;
|
||||
|
||||
+ (NSString *)URLEncodedString:(NSString *)str;
|
||||
|
||||
+ (NSString *)generateSessionID;
|
||||
|
||||
+ (NSString *)generateUserAgent;
|
||||
|
||||
+ (NSData *)encryptDataAESCBC:(NSData *)plaintext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error;
|
||||
|
||||
+ (NSData *)decryptDataAESCBC:(NSData *)ciphertext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error;
|
||||
|
||||
+ (NSString *)hexStringFromData:(NSData *)data;
|
||||
|
||||
+ (NSData *)dataFromHexString:(NSString *)hexString;
|
||||
|
||||
+ (NSString *)hmacSha256:(NSString *)data key:(NSString *)key;
|
||||
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host;
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host service:(HttpDnsService *)service;
|
||||
|
||||
@end
|
||||
471
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsUtil.m
Normal file
471
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/Utils/HttpdnsUtil.m
Normal file
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "CommonCrypto/CommonCrypto.h"
|
||||
#import "arpa/inet.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "httpdnsReachability.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsService.h"
|
||||
|
||||
@implementation HttpdnsUtil
|
||||
|
||||
+ (BOOL)isIPv4Address:(NSString *)addr {
|
||||
if (!addr) {
|
||||
return NO;
|
||||
}
|
||||
struct in_addr dst;
|
||||
return inet_pton(AF_INET, [addr UTF8String], &(dst.s_addr)) == 1;
|
||||
}
|
||||
|
||||
+ (BOOL)isIPv6Address:(NSString *)addr {
|
||||
if (!addr) {
|
||||
return NO;
|
||||
}
|
||||
struct in6_addr dst6;
|
||||
return inet_pton(AF_INET6, [addr UTF8String], &dst6) == 1;
|
||||
}
|
||||
|
||||
+ (BOOL)isAnIP:(NSString *)candidate {
|
||||
if ([self isIPv4Address:candidate]) {
|
||||
return YES;
|
||||
}
|
||||
if ([self isIPv6Address:candidate]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)isAHost:(NSString *)host {
|
||||
static dispatch_once_t once_token;
|
||||
static NSRegularExpression *hostExpression = nil;
|
||||
dispatch_once(&once_token, ^{
|
||||
hostExpression = [[NSRegularExpression alloc] initWithPattern:@"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
|
||||
if (!host.length) {
|
||||
return NO;
|
||||
}
|
||||
NSTextCheckingResult *checkResult = [hostExpression firstMatchInString:host options:0 range:NSMakeRange(0, [host length])];
|
||||
if (checkResult.range.length == [host length]) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)warnMainThreadIfNecessary {
|
||||
if ([NSThread isMainThread]) {
|
||||
HttpdnsLogDebug("Warning: A long-running Paas operation is being executed on the main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSDictionary *)getValidDictionaryFromJson:(id)jsonValue {
|
||||
NSDictionary *dictionaryValueFromJson = nil;
|
||||
if ([jsonValue isKindOfClass:[NSDictionary class]]) {
|
||||
if ([(NSDictionary *)jsonValue allKeys].count > 0) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:jsonValue];
|
||||
@try {
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
} @catch (NSException *exception) {}
|
||||
dictionaryValueFromJson = [jsonValue copy];
|
||||
}
|
||||
}
|
||||
return dictionaryValueFromJson;
|
||||
}
|
||||
|
||||
+ (void)removeNSNullValueFromArray:(NSMutableArray *)array {
|
||||
NSMutableArray *objToRemove = nil;
|
||||
NSMutableIndexSet *indexToReplace = [[NSMutableIndexSet alloc] init];
|
||||
NSMutableArray *objForReplace = [[NSMutableArray alloc] init];
|
||||
for (int i = 0; i < array.count; ++i) {
|
||||
id value = [array objectAtIndex:i];
|
||||
if ([value isKindOfClass:[NSNull class]]) {
|
||||
if (!objToRemove) {
|
||||
objToRemove = [[NSMutableArray alloc] init];
|
||||
}
|
||||
[objToRemove addObject:value];
|
||||
} else if ([value isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:value];
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
[indexToReplace addIndex:i];
|
||||
[objForReplace addObject:mutableDict];
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *v = [value mutableCopy];
|
||||
[self removeNSNullValueFromArray:v];
|
||||
[indexToReplace addIndex:i];
|
||||
[objForReplace addObject:v];
|
||||
}
|
||||
}
|
||||
[array replaceObjectsAtIndexes:indexToReplace withObjects:objForReplace];
|
||||
if (objToRemove) {
|
||||
[array removeObjectsInArray:objToRemove];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)removeNSNullValueFromDictionary:(NSMutableDictionary *)dict {
|
||||
for (id key in [dict allKeys]) {
|
||||
id value = [dict objectForKey:key];
|
||||
if ([value isKindOfClass:[NSNull class]]) {
|
||||
[dict removeObjectForKey:key];
|
||||
} else if ([value isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:value];
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
[dict setObject:mutableDict forKey:key];
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *v = [value mutableCopy];
|
||||
[self removeNSNullValueFromArray:v];
|
||||
[dict setObject:v forKey:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyArray:(NSArray *)inputArr {
|
||||
if (!inputArr) {
|
||||
return YES;
|
||||
}
|
||||
return [inputArr count] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyArray:(NSArray *)inputArr {
|
||||
return ![self isEmptyArray:inputArr];
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyString:(NSString *)inputStr {
|
||||
if (!inputStr) {
|
||||
return YES;
|
||||
}
|
||||
return [inputStr length] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyString:(NSString *)inputStr {
|
||||
return ![self isEmptyString:inputStr];
|
||||
}
|
||||
|
||||
+ (BOOL)isValidJSON:(id)JSON {
|
||||
BOOL isValid;
|
||||
@try {
|
||||
isValid = ([JSON isKindOfClass:[NSDictionary class]] || [JSON isKindOfClass:[NSArray class]]);
|
||||
} @catch (NSException *ignore) {
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyDictionary:(NSDictionary *)inputDict {
|
||||
if (!inputDict) {
|
||||
return YES;
|
||||
}
|
||||
return [inputDict count] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyDictionary:(NSDictionary *)inputDict {
|
||||
return ![self isEmptyDictionary:inputDict];
|
||||
}
|
||||
|
||||
+ (NSArray *)joinArrays:(NSArray *)array1 withArray:(NSArray *)array2 {
|
||||
NSMutableArray *resultArray = [array1 mutableCopy];
|
||||
[resultArray addObjectsFromArray:array2];
|
||||
return [resultArray copy];
|
||||
}
|
||||
|
||||
+ (id)convertJsonDataToObject:(NSData *)jsonData {
|
||||
if (jsonData) {
|
||||
NSError *error;
|
||||
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];
|
||||
if (!error) {
|
||||
return jsonObj;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (NSString *)getMD5StringFrom:(NSString *)originString {
|
||||
const char * pointer = [originString UTF8String];
|
||||
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
|
||||
|
||||
CC_MD5(pointer, (CC_LONG)strlen(pointer), md5Buffer);
|
||||
|
||||
NSMutableString *string = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
|
||||
[string appendFormat:@"%02x",md5Buffer[i]];
|
||||
|
||||
return [string copy];
|
||||
}
|
||||
|
||||
+ (NSString *)URLEncodedString:(NSString *)str {
|
||||
if (str) {
|
||||
return [str stringByAddingPercentEncodingWithAllowedCharacters:
|
||||
[NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]\""].invertedSet];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/**
|
||||
生成sessionId
|
||||
App打开生命周期只生成一次,不做持久化
|
||||
sessionId为12位,采用base62编码
|
||||
*/
|
||||
+ (NSString *)generateSessionID {
|
||||
static NSString *sessionId = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
NSUInteger length = alphabet.length;
|
||||
if (![HttpdnsUtil isNotEmptyString:sessionId]) {
|
||||
NSMutableString *mSessionId = [NSMutableString string];
|
||||
for (int i = 0; i < 12; i++) {
|
||||
[mSessionId appendFormat:@"%@", [alphabet substringWithRange:NSMakeRange(arc4random() % length, 1)]];
|
||||
}
|
||||
sessionId = [mSessionId copy];
|
||||
}
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
+ (NSString *)generateUserAgent {
|
||||
UIDevice *device = [UIDevice currentDevice];
|
||||
NSString *systemName = [device systemName];
|
||||
NSString *systemVersion = [device systemVersion];
|
||||
NSString *model = [device model];
|
||||
|
||||
NSString *userAgent = [NSString stringWithFormat:@"HttpdnsSDK/%@ (%@; iOS %@; %@)", HTTPDNS_IOS_SDK_VERSION, model, systemVersion, systemName];
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
+ (NSData *)encryptDataAESCBC:(NSData *)plaintext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error {
|
||||
// 检查输入参数
|
||||
if (plaintext == nil || [plaintext length] == 0 || key == nil || [key length] != kCCKeySizeAES128) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid input parameters"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 为CBC模式生成128bit(16字节)的随机IV
|
||||
NSMutableData *iv = [NSMutableData dataWithLength:kCCBlockSizeAES128];
|
||||
int result = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, iv.mutableBytes);
|
||||
if (result != 0) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_ENCRYPT_RANDOM_IV_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to generate random IV"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 计算加密后的数据长度 (可能需要填充)
|
||||
size_t bufferSize = [plaintext length] + kCCBlockSizeAES128;
|
||||
size_t encryptedSize = 0;
|
||||
|
||||
// 创建输出缓冲区
|
||||
NSMutableData *cipherData = [NSMutableData dataWithLength:bufferSize];
|
||||
|
||||
// 执行加密
|
||||
// AES中PKCS5Padding与PKCS7Padding相同
|
||||
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
|
||||
kCCAlgorithmAES,
|
||||
kCCOptionPKCS7Padding,
|
||||
[key bytes],
|
||||
[key length],
|
||||
[iv bytes],
|
||||
[plaintext bytes],
|
||||
[plaintext length],
|
||||
[cipherData mutableBytes],
|
||||
[cipherData length],
|
||||
&encryptedSize);
|
||||
|
||||
if (cryptStatus != kCCSuccess) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Encryption failed with status: %d", cryptStatus]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 调整加密数据的长度,只保留实际加密内容
|
||||
[cipherData setLength:encryptedSize];
|
||||
|
||||
// 将IV和加密数据合并在一起
|
||||
NSMutableData *resultData = [NSMutableData dataWithData:iv];
|
||||
[resultData appendData:cipherData];
|
||||
|
||||
return resultData;
|
||||
}
|
||||
|
||||
+ (NSString *)hexStringFromData:(NSData *)data {
|
||||
if (!data || data.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableString *hexString = [NSMutableString stringWithCapacity:data.length * 2];
|
||||
const unsigned char *bytes = data.bytes;
|
||||
|
||||
for (NSInteger i = 0; i < data.length; i++) {
|
||||
[hexString appendFormat:@"%02x", bytes[i]];
|
||||
}
|
||||
|
||||
return [hexString copy];
|
||||
}
|
||||
|
||||
+ (NSData *)dataFromHexString:(NSString *)hexString {
|
||||
if (!hexString || hexString.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 移除可能存在的空格
|
||||
NSString *cleanedString = [hexString stringByReplacingOccurrencesOfString:@" " withString:@""];
|
||||
|
||||
// 确保字符串长度为偶数
|
||||
if (cleanedString.length % 2 != 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableData *data = [NSMutableData dataWithCapacity:cleanedString.length / 2];
|
||||
for (NSUInteger i = 0; i < cleanedString.length; i += 2) {
|
||||
NSString *byteString = [cleanedString substringWithRange:NSMakeRange(i, 2)];
|
||||
NSScanner *scanner = [NSScanner scannerWithString:byteString];
|
||||
|
||||
unsigned int byteValue;
|
||||
if (![scanner scanHexInt:&byteValue]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint8_t byte = (uint8_t)byteValue;
|
||||
[data appendBytes:&byte length:1];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (NSString *)hmacSha256:(NSString *)data key:(NSString *)key {
|
||||
if (!data || !key) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 将十六进制密钥转换为NSData
|
||||
NSData *keyData = [self dataFromHexString:key];
|
||||
if (!keyData) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 数据转换
|
||||
NSData *dataToSign = [data dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
// 计算HMAC
|
||||
uint8_t digestBytes[CC_SHA256_DIGEST_LENGTH];
|
||||
CCHmac(kCCHmacAlgSHA256, keyData.bytes, keyData.length, dataToSign.bytes, dataToSign.length, digestBytes);
|
||||
|
||||
// 创建结果数据
|
||||
NSData *hmacData = [NSData dataWithBytes:digestBytes length:CC_SHA256_DIGEST_LENGTH];
|
||||
|
||||
// 转换为十六进制字符串
|
||||
return [self hexStringFromData:hmacData];
|
||||
}
|
||||
|
||||
+ (NSData *)decryptDataAESCBC:(NSData *)ciphertext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error {
|
||||
// 检查输入参数
|
||||
if (ciphertext == nil || [ciphertext length] <= kCCBlockSizeAES128 || key == nil || [key length] != kCCKeySizeAES128) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid input parameters for decryption"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 提取IV(前16字节)和实际的密文
|
||||
NSData *iv = [ciphertext subdataWithRange:NSMakeRange(0, kCCBlockSizeAES128)];
|
||||
NSData *actualCiphertext = [ciphertext subdataWithRange:NSMakeRange(kCCBlockSizeAES128, ciphertext.length - kCCBlockSizeAES128)];
|
||||
|
||||
// 计算解密后可能的缓冲区大小
|
||||
size_t bufferSize = actualCiphertext.length + kCCBlockSizeAES128;
|
||||
size_t decryptedSize = 0;
|
||||
|
||||
// 创建输出缓冲区
|
||||
NSMutableData *decryptedData = [NSMutableData dataWithLength:bufferSize];
|
||||
|
||||
// 执行解密
|
||||
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
|
||||
kCCAlgorithmAES,
|
||||
kCCOptionPKCS7Padding,
|
||||
[key bytes],
|
||||
[key length],
|
||||
[iv bytes],
|
||||
[actualCiphertext bytes],
|
||||
[actualCiphertext length],
|
||||
[decryptedData mutableBytes],
|
||||
[decryptedData length],
|
||||
&decryptedSize);
|
||||
|
||||
if (cryptStatus != kCCSuccess) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:ALICLOUD_HTTPDNS_ERROR_DOMAIN
|
||||
code:ALICLOUD_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Decryption failed with status: %d", cryptStatus]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 调整解密数据的长度,只保留实际解密内容
|
||||
[decryptedData setLength:decryptedSize];
|
||||
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host {
|
||||
[self processCustomTTL:hostObject forHost:host service:[HttpDnsService sharedInstance]];
|
||||
}
|
||||
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host service:(HttpDnsService *)service {
|
||||
if (!hostObject || !host) {
|
||||
return;
|
||||
}
|
||||
|
||||
HttpDnsService *dnsService = service ?: [HttpDnsService sharedInstance];
|
||||
|
||||
if (dnsService.ttlDelegate && [dnsService.ttlDelegate respondsToSelector:@selector(httpdnsHost:ipType:ttl:)]) {
|
||||
if ([self isNotEmptyArray:[hostObject getV4Ips]]) {
|
||||
int64_t customV4TTL = [dnsService.ttlDelegate httpdnsHost:host ipType:AlicloudHttpDNS_IPTypeV4 ttl:hostObject.v4ttl];
|
||||
if (customV4TTL > 0) {
|
||||
hostObject.v4ttl = customV4TTL;
|
||||
}
|
||||
}
|
||||
|
||||
if ([self isNotEmptyArray:[hostObject getV6Ips]]) {
|
||||
int64_t customV6TTL = [dnsService.ttlDelegate httpdnsHost:host ipType:AlicloudHttpDNS_IPTypeV6 ttl:hostObject.v6ttl];
|
||||
if (customV6TTL > 0) {
|
||||
hostObject.v6ttl = customV6TTL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user