feat: sync httpdns sdk/platform updates without large binaries

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

View File

@@ -0,0 +1,183 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Test Commands
```bash
# Install dependencies
pod install
# Build SDK (Release configuration)
xcodebuild -workspace NewHttpDNS.xcworkspace -scheme NewHttpDNS -configuration Release build
# Run all unit tests
xcodebuild test -workspace NewHttpDNS.xcworkspace -scheme NewHttpDNSTests -destination 'platform=iOS Simulator,name=iPhone 15'
# Build distributable XCFramework
sh build_xc_framework.sh
```
**Note:** After creating new Xcode files, wait for the user to add them to the appropriate target before running builds or tests.
## Architecture Overview
This is an iOS HTTPDNS SDK with a multi-layered architecture designed for secure, cached, and resilient DNS resolution over HTTP/HTTPS.
### Core Architecture Layers
**1. Public API Layer (`HttpdnsService`)**
- Singleton facade providing three resolution modes:
- `resolveHostSync:` - Blocking with timeout
- `resolveHostAsync:` - Non-blocking with callbacks
- `resolveHostSyncNonBlocking:` - Returns cache immediately, refreshes async
- Manages multi-account instances (one per account ID)
- Configuration entry point for all SDK features
**2. Request Management Layer (`HttpdnsRequestManager`)**
- Orchestrates cache lookups before triggering network requests
- Manages two-tier caching:
- Memory cache (`HttpdnsHostObjectInMemoryCache`)
- Persistent SQLite cache (`HttpdnsDB`)
- Handles TTL validation and expired IP reuse policy
- Coordinates retry logic and degradation to local DNS
**3. Network Transport Layer (`HttpdnsNWHTTPClient`)**
- Low-level HTTP/HTTPS transport using Apple's Network framework
- Singleton with persistent connection pooling (max 4 idle connections per host:port:scheme)
- Manages reusable connections (`HttpdnsNWReusableConnection`) with automatic idle timeout (30s)
- Thread-safe concurrent request handling via serial pool queue
- Custom HTTP header parser supporting chunked transfer encoding
- TLS certificate validation with configurable trust evaluation
- Exposed by `HttpdnsRemoteResolver` for DNS resolution requests
**4. DNS Resolution Layer**
- **`HttpdnsRemoteResolver`**: HTTPS/HTTP requests to New servers
- Builds authenticated requests with HMAC-SHA256 signatures
- Optional AES-CBC encryption for sensitive parameters
- Parses JSON responses into `HttpdnsHostObject` (IPv4/IPv6)
- Uses `HttpdnsNWHTTPClient` for actual HTTP transport
- **`HttpdnsLocalResolver`**: Fallback to system DNS when remote fails
**5. Scheduling & Service Discovery (`HttpdnsScheduleCenter`)**
- Maintains regional service endpoint pools (CN, HK, SG, US, DE)
- Rotates between endpoints on failure for load balancing
- Separates IPv4 and IPv6 endpoint lists
- Per-account endpoint isolation
**6. Data Flow (Synchronous Resolution)**
```
User Request
<20><>?Validate & wrap in HttpdnsRequest
<20><>?Check memory cache (valid? return)
<20><>?Load from SQLite DB (valid? return)
<20><>?HttpdnsRemoteResolver
- Build URL with auth (HMAC-SHA256)
- Encrypt params if enabled (AES-CBC)
- Send to service endpoint
- Parse JSON response
- Decrypt if needed
<20><>?Cache in memory + DB
<20><>?Return HttpdnsResult
On Failure:
<20><>?Retry with different endpoint (max 1 retry)
<20><>?Return expired IP (if setReuseExpiredIPEnabled:YES)
<20><>?Fall back to local DNS (if setDegradeToLocalDNSEnabled:YES)
<20><>?Return nil
```
### Authentication & Encryption
**Request Signing:**
- All sensitive params signed with HMAC-SHA256
- Signature includes: account ID, expiration timestamp, domain, query type
- Params sorted alphabetically before signing
- Expiration: current_time + 10 minutes
**Request Encryption (Optional):**
- Domain name, query type, and SDNS params encrypted with AES-CBC
- Encrypted blob included as `enc` parameter
- Only encrypted when `aesSecretKey` provided at init
### Key Internal Components
- **`HttpdnsHostObject`**: Internal model with separate IPv4/IPv6 arrays, TTLs, timestamps
- **`HttpdnsResult`**: Public-facing result model (simplified view)
- **`HttpdnsHostRecord`**: Serializable model for SQLite persistence
- **`HttpdnsNWHTTPClient`**: Singleton HTTP transport layer with connection pooling
- **`HttpdnsNWReusableConnection`**: Wrapper for Network framework connections with idle timeout tracking
- **`HttpdnsIpStackDetector`**: Detects network stack type (IPv4/IPv6 capability)
- **`HttpdnsReachability`**: Monitors network changes, triggers pre-resolution
- **`HttpdnsUtil`**: Crypto utilities (HMAC, AES), IP validation, encoding
### Concurrency Model
- Concurrent queues for async user requests and DNS resolution
- Serial pool queue in `HttpdnsNWHTTPClient` for connection pool management
- `dispatch_semaphore_t` for blocking synchronous calls
- `HttpDnsLocker` prevents duplicate concurrent resolution of same domain
- Network framework handles underlying I/O asynchronously
## Coding Conventions
**Style:**
- 4-space indentation, no trailing whitespace
- Braces on same line as control statements; body starts on next line
- Always use braces for control statement bodies, even single statements
- Comments in Chinese, only for complex logic explaining WHY
**Naming:**
- Types/files: `UpperCamelCase` (e.g., `NewHttpDNSClient.h`)
- Methods/variables: `lowerCamelCase`
- Constants: `kAC...` prefix
- Internal headers: `+Internal.h` suffix
**Commit Messages:**
- Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`, `config:`
- Write in Chinese
- After `git add`, run: `/Users/xuyecan/.macconfig/script/strip-trailing-ws-in-diff --staged`
## Testing Notes
- Test target: `NewHttpDNSTests`
- OCMock-based tests may have memory issues when run in batch - run individually if needed
- Non-mock tests use predefined credentials:
- Account ID: `1000000`
- Test domains: `*.onlyforhttpdnstest.run.place` (renewed annually)
- Never commit real production Account IDs or Secret Keys
- Test file naming mirrors class under test (e.g., `NewHttpDNSClientTests.m`)
- Network layer integration tests organized into 5 focused modules:
- `HttpdnsNWHTTPClient_BasicIntegrationTests.m`: Basic HTTP/HTTPS requests
- `HttpdnsNWHTTPClient_ConcurrencyTests.m`: Thread safety and concurrent access
- `HttpdnsNWHTTPClient_PoolManagementTests.m`: Connection pooling and reuse
- `HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m`: Timeout and error handling
- `HttpdnsNWHTTPClient_StateMachineTests.m`: Connection lifecycle state transitions
## SDK-Specific Notes
**Multi-Account Support:**
- Each account ID gets isolated singleton instance
- Separate endpoint pools, caches, and configurations per account
**Public vs Internal Headers:**
- Public headers listed in `NewHTTPDNS.podspec` under `public_header_files`
- Internal headers use `+Internal.h` suffix and are not exposed
- Umbrella header: `NewHttpDNS.h` imports all public APIs
**Required System Frameworks:**
- `CoreTelephony`, `SystemConfiguration`, `Network` (for HTTP transport)
- Libraries: `sqlite3.0`, `resolv`, `z`
- Linker flags: `-ObjC -lz`
- Minimum deployment target: iOS 12.0+ (required for Network framework)
**Pre-Resolution Strategy:**
- Call `setPreResolveHosts:byIPType:` at app startup for hot domains
- Automatically re-triggered on network changes (WiFi <20><>?cellular)
- Batch requests combine multiple hosts in single HTTP call
**Persistence & Cache:**
- SQLite DB per account in isolated directory
- Enable with `setPersistentCacheIPEnabled:YES`
- Automatic expiration cleanup
- Speeds up cold starts with pre-cached DNS results

View File

@@ -0,0 +1,46 @@
Pod::Spec.new do |s|
s.name = "NewHTTPDNS"
s.version = "1.0.0"
s.summary = "New Mobile Service HTTPDNS iOS SDK (source distribution)."
s.description = <<-DESC
HTTPDNS iOS SDK 源码分发版本,提供通过 HTTP(S) 进行域名解析<E8A7A3><E69E90>? IPv4/IPv6 支持、鉴权签名、可选参数加密、内存与 SQLite 持久化缓存<E7BC93><E5AD98>? 区域调度与降级到本地解析等能力<E883BD><E58A9B>? DESC
s.homepage = "https://www.aliyun.com/product/httpdns"
s.authors = { "zhouzhuo" => "yecan.xyc@alibaba-inc.com" }
# 注意:发布到 Specs 仓库前,请将 git 地址指向正式仓库并按版本<E78988><E69CAC>?tag
s.source = { :git => "https://github.com/aliyun/alibabacloud-httpdns-ios-sdk", :tag => s.version.to_s }
s.platform = :ios, "12.0"
s.requires_arc = true
# 以源码方式集成仅收<E4BB85><E694B6>?SDK 源码目录
s.source_files = "NewHttpDNS/**/*.{h,m}"
# 资源隐私清<E7A781><E6B885>? s.resources = "resource/PrivacyInfo.xcprivacy"
# 公开头文件<E4BBB6><EFBC88>?umbrella 头导入的稳定接口<E68EA5><E58FA3>? s.public_header_files = [
"NewHttpDNS/NewHttpDNS.h",
"NewHttpDNS/HttpdnsService.h",
"NewHttpDNS/HttpdnsEdgeService.h",
"NewHttpDNS/Model/HttpdnsResult.h",
"NewHttpDNS/Model/HttpdnsRequest.h",
"NewHttpDNS/Log/HttpdnsLog.h",
"NewHttpDNS/Log/HttpdnsLoggerProtocol.h",
"NewHttpDNS/HttpdnsDegradationDelegate.h",
"NewHttpDNS/Config/HttpdnsPublicConstant.h",
"NewHttpDNS/IpStack/HttpdnsIpStackDetector.h"
]
# 系统库与框架
s.frameworks = ["CoreTelephony", "SystemConfiguration"]
s.libraries = ["sqlite3.0", "resolv", "z"]
# 链接器参数:保留 ObjectiveC 分类
s.pod_target_xcconfig = {
'OTHER_LDFLAGS' => '$(inherited) -ObjC'
}
s.user_target_xcconfig = {
'OTHER_LDFLAGS' => '$(inherited) -ObjC'
}
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA361BC79A4500BDB65B"
BuildableName = "NewHttpDNS.framework"
BlueprintName = "NewHttpDNS"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA361BC79A4500BDB65B"
BuildableName = "NewHttpDNS.framework"
BlueprintName = "NewHttpDNS"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA361BC79A4500BDB65B"
BuildableName = "NewHttpDNS.framework"
BlueprintName = "NewHttpDNS"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4AF4AB5F211439A600D712DF"
BuildableName = "NewHttpDNSTestDemo.app"
BlueprintName = "NewHttpDNSTestDemo"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4AF4AB5F211439A600D712DF"
BuildableName = "NewHttpDNSTestDemo.app"
BlueprintName = "NewHttpDNSTestDemo"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4AF4AB5F211439A600D712DF"
BuildableName = "NewHttpDNSTestDemo.app"
BlueprintName = "NewHttpDNSTestDemo"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA401BC79A4500BDB65B"
BuildableName = "NewHttpDNS.xctest"
BlueprintName = "NewHttpDNSTests"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA401BC79A4500BDB65B"
BuildableName = "NewHttpDNS.xctest"
BlueprintName = "NewHttpDNSTests"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2197CA401BC79A4500BDB65B"
BuildableName = "NewHttpDNS.xctest"
BlueprintName = "NewHttpDNSTests"
ReferencedContainer = "container:NewHttpDNS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
customArchiveName = "NewHttpDNSTests"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,60 @@
//
// HttpdnsInternalConstant.h
// TrustHttpDNS
//
// Created by xuyecan on 2025/03/10.
// Copyright © 2024 trustapp.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 Trust_HTTPDNS_VALID_SERVER_CERTIFICATE_IP = @"203.107.1.1";
// 在iOS14和iOS16网络信息的获取权限受到越来越紧的限<E79A84><E99990>?
// 除非用户主动声明需要相关entitlement不然只能拿到空信息
// 考虑大多数用户并不会申请这些权限我们放弃针对细节的网络信息做缓存粒度隔<E5BAA6><E99A94>?
// 出于兼容性考虑网络运营商只有default一种类<E7A78D><E7B1BB>?
#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 Trust_HTTPDNS_SCHEDULE_CENTER_REQUEST_HOST = @"httpdns-sc.TrustAPPcs.com";
static NSString *const Trust_HTTPDNS_ERROR_MESSAGE_KEY = @"ErrorMessage";
static NSString *const kTrustHttpdnsRegionConfigV4HostKey = @"service_ip";
static NSString *const kTrustHttpdnsRegionConfigV6HostKey = @"service_ipv6";
static NSString *const kTrustHttpdnsRegionKey = @"HttpdnsRegion";
#define SECONDS_OF_ONE_YEAR 365 * 24 * 60 * 60
static NSString *const Trust_HTTPDNS_ERROR_DOMAIN = @"HttpdnsErrorDomain";
static NSInteger const Trust_HTTPDNS_HTTPS_COMMON_ERROR_CODE = 10003;
static NSInteger const Trust_HTTPDNS_HTTP_COMMON_ERROR_CODE = 10004;
static NSInteger const Trust_HTTPDNS_HTTPS_TIMEOUT_ERROR_CODE = 10005;
static NSInteger const Trust_HTTPDNS_HTTP_TIMEOUT_ERROR_CODE = 10006;
static NSInteger const Trust_HTTPDNS_HTTP_OPEN_STREAM_ERROR_CODE = 10007;
static NSInteger const Trust_HTTPDNS_HTTPS_NO_DATA_ERROR_CODE = 10008;
static NSInteger const Trust_HTTP_UNSUPPORTED_STATUS_CODE = 10013;
static NSInteger const Trust_HTTP_PARSE_JSON_FAILED = 10014;
// 加密错误<E99499><E8AFAF>?
static NSInteger const Trust_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE = 10021;
static NSInteger const Trust_HTTPDNS_ENCRYPT_RANDOM_IV_ERROR_CODE = 10022;
static NSInteger const Trust_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE = 10023;
#endif

View File

@@ -0,0 +1,21 @@
//
// HttpdnsPublicConstant.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/6/16.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#ifndef PublicConstant_h
#define PublicConstant_h
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"1.0.0";
#define Trust_HTTPDNS_DEFAULT_REGION_KEY @"cn"
#define Trust_HTTPDNS_HONGKONG_REGION_KEY @"hk"
#define Trust_HTTPDNS_SINGAPORE_REGION_KEY @"sg"
#define Trust_HTTPDNS_GERMANY_REGION_KEY @"de"
#define Trust_HTTPDNS_AMERICA_REGION_KEY @"us"
#endif /* PublicConstant_h */

View File

@@ -0,0 +1,29 @@
//
// HttpdnsRegionConfigLoader.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/6/16.
// Copyright © 2024 trustapp.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

View File

@@ -0,0 +1,112 @@
//
// HttpdnsRegionConfigLoader.m
// TrustHttpDNS
//
// Created by xuyecan on 2024/6/16.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import "HttpdnsRegionConfigLoader.h"
#import "HttpdnsPublicConstant.h"
static NSString *const kServiceV4Key = @"Trust_HTTPDNS_SERVICE_HOST_V4_KEY";
static NSString *const kUpdateV4FallbackHostKey = @"Trust_HTTPDNS_UPDATE_HOST_V4_KEY";
static NSString *const kServiceV6Key = @"Trust_HTTPDNS_SERVICE_HOST_V6_KEY";
static NSString *const kUpdateV6FallbackHostKey = @"Trust_HTTPDNS_UPDATE_HOST_V6_KEY";
static NSArray<NSString *> *Trust_HTTPDNS_AVAILABLE_REGION_LIST = nil;
@interface HttpdnsRegionConfigLoader ()
@property (nonatomic, strong) NSDictionary *regionConfig;
@end
@implementation HttpdnsRegionConfigLoader
+ (void)initialize {
Trust_HTTPDNS_AVAILABLE_REGION_LIST = @[
Trust_HTTPDNS_DEFAULT_REGION_KEY,
Trust_HTTPDNS_HONGKONG_REGION_KEY,
Trust_HTTPDNS_SINGAPORE_REGION_KEY,
Trust_HTTPDNS_GERMANY_REGION_KEY,
Trust_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 Trust_HTTPDNS_AVAILABLE_REGION_LIST;
}
- (void)loadRegionConfig {
self.regionConfig = @{
Trust_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.TrustAPPcs.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.TrustAPPcs.com"]
},
Trust_HTTPDNS_HONGKONG_REGION_KEY: @{
kServiceV4Key: @[@"47.56.234.194", @"47.56.119.115"],
kUpdateV4FallbackHostKey: @[@"resolvers-hk.httpdns.TrustAPPcs.com"],
kServiceV6Key: @[@"240b:4000:f10::178", @"240b:4000:f10::188"],
kUpdateV6FallbackHostKey: @[@"resolvers-hk.httpdns.TrustAPPcs.com"]
},
Trust_HTTPDNS_SINGAPORE_REGION_KEY: @{
kServiceV4Key: @[@"161.117.200.122", @"47.74.222.190"],
kUpdateV4FallbackHostKey: @[@"resolvers-sg.httpdns.TrustAPPcs.com"],
kServiceV6Key: @[@"240b:4000:f10::178", @"240b:4000:f10::188"],
kUpdateV6FallbackHostKey: @[@"resolvers-sg.httpdns.TrustAPPcs.com"]
},
Trust_HTTPDNS_GERMANY_REGION_KEY: @{
kServiceV4Key: @[@"47.89.80.182", @"47.246.146.77"],
kUpdateV4FallbackHostKey: @[@"resolvers-de.httpdns.TrustAPPcs.com"],
kServiceV6Key: @[@"2404:2280:3000::176", @"2404:2280:3000::188"],
kUpdateV6FallbackHostKey: @[@"resolvers-de.httpdns.TrustAPPcs.com"]
},
Trust_HTTPDNS_AMERICA_REGION_KEY: @{
kServiceV4Key: @[@"47.246.131.175", @"47.246.131.141"],
kUpdateV4FallbackHostKey: @[@"resolvers-us.httpdns.TrustAPPcs.com"],
kServiceV6Key: @[@"2404:2280:4000::2bb", @"2404:2280:4000::23e"],
kUpdateV6FallbackHostKey: @[@"resolvers-us.httpdns.TrustAPPcs.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

View 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.
*/
#ifndef HttpdnsDegradationDelegate_h
#define HttpdnsDegradationDelegate_h
__attribute__((deprecated("不再建议通过设置此回调实现降级逻辑而是自行在调用HTTPDNS解析域名前做判断")))
@protocol HttpDNSDegradationDelegate <NSObject>
- (BOOL)shouldDegradeHTTPDNS:(NSString *)hostName;
@end
#endif /* HttpdnsDegradationDelegate_h */

View File

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

View File

@@ -0,0 +1,277 @@
#import "HttpdnsEdgeService.h"
#import <CommonCrypto/CommonCrypto.h>
static NSString * const kHttpdnsEdgeErrorDomain = @"com.goeedge.httpdns.edge";
@implementation HttpdnsEdgeResolveResult
@end
@interface HttpdnsEdgeService ()
@property (nonatomic, copy) NSString *appId;
@property (nonatomic, copy) NSString *primaryServiceHost;
@property (nonatomic, copy) NSString *backupServiceHost;
@property (nonatomic, assign) NSInteger servicePort;
@property (nonatomic, copy) NSString *signSecret;
@property (nonatomic, copy) NSString *sessionId;
@end
@implementation HttpdnsEdgeService
- (instancetype)initWithAppId:(NSString *)appId
primaryServiceHost:(NSString *)primaryServiceHost
backupServiceHost:(NSString *)backupServiceHost
servicePort:(NSInteger)servicePort
signSecret:(NSString *)signSecret {
if (self = [super init]) {
_appId = [appId copy];
_primaryServiceHost = [primaryServiceHost copy];
_backupServiceHost = backupServiceHost.length > 0 ? [backupServiceHost copy] : @"";
_servicePort = servicePort > 0 ? servicePort : 443;
_signSecret = signSecret.length > 0 ? [signSecret copy] : @"";
_sessionId = [[[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""] copy];
}
return self;
}
- (void)resolveHost:(NSString *)host
queryType:(NSString *)queryType
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable, NSError *_Nullable))completion {
if (host.length == 0 || self.appId.length == 0 || self.primaryServiceHost.length == 0) {
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
code:1001
userInfo:@{NSLocalizedDescriptionKey: @"invalid init config or host"}];
completion(nil, error);
return;
}
NSString *qtype = queryType.length > 0 ? queryType.uppercaseString : @"A";
NSArray<NSString *> *hosts = self.backupServiceHost.length > 0
? @[self.primaryServiceHost, self.backupServiceHost]
: @[self.primaryServiceHost];
[self requestResolveHosts:hosts index:0 host:host qtype:qtype completion:completion];
}
- (void)requestURL:(NSURL *)url
method:(NSString *)method
headers:(NSDictionary<NSString *,NSString *> *)headers
body:(NSData *)body
completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion {
NSString *originHost = url.host ?: @"";
if (originHost.length == 0) {
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
code:2001
userInfo:@{NSLocalizedDescriptionKey: @"invalid request host"}];
completion(nil, nil, error);
return;
}
[self resolveHost:originHost queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
if (error != nil || result.ipv4s.count == 0) {
completion(nil, nil, error ?: [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
code:2002
userInfo:@{NSLocalizedDescriptionKey: @"NO_IP_AVAILABLE"}]);
return;
}
[self requestByIPList:result.ipv4s
index:0
url:url
method:method
headers:headers
body:body
completion:completion];
}];
}
- (void)requestByIPList:(NSArray<NSString *> *)ips
index:(NSInteger)index
url:(NSURL *)url
method:(NSString *)method
headers:(NSDictionary<NSString *, NSString *> *)headers
body:(NSData *)body
completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion {
if (index >= ips.count) {
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
code:2003
userInfo:@{NSLocalizedDescriptionKey: @"TLS_EMPTY_SNI_FAILED"}];
completion(nil, nil, error);
return;
}
NSString *ip = ips[index];
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
components.host = ip;
if (components.port == nil) {
components.port = @(443);
}
NSURL *targetURL = components.URL;
if (targetURL == nil) {
[self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion];
return;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:targetURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:8];
request.HTTPMethod = method.length > 0 ? method : @"GET";
if (body.length > 0) {
request.HTTPBody = body;
}
[request setValue:url.host forHTTPHeaderField:@"Host"];
[headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
if ([key.lowercaseString isEqualToString:@"host"]) {
return;
}
[request setValue:obj forHTTPHeaderField:key];
}];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
config.timeoutIntervalForRequest = 8;
config.timeoutIntervalForResource = 8;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[session finishTasksAndInvalidate];
if (error != nil) {
[self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion];
return;
}
completion(data, (NSHTTPURLResponse *)response, nil);
}] resume];
}
- (void)requestResolveHosts:(NSArray<NSString *> *)hosts
index:(NSInteger)index
host:(NSString *)host
qtype:(NSString *)qtype
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable result, NSError *_Nullable error))completion {
if (index >= hosts.count) {
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
code:1002
userInfo:@{NSLocalizedDescriptionKey: @"resolve failed on all service hosts"}];
completion(nil, error);
return;
}
NSString *serviceHost = hosts[index];
NSURLComponents *components = [NSURLComponents new];
components.scheme = @"https";
components.host = serviceHost;
components.port = @(self.servicePort);
components.path = @"/resolve";
NSMutableArray<NSURLQueryItem *> *items = [NSMutableArray arrayWithArray:@[
[NSURLQueryItem queryItemWithName:@"appId" value:self.appId],
[NSURLQueryItem queryItemWithName:@"dn" value:host],
[NSURLQueryItem queryItemWithName:@"qtype" value:qtype],
[NSURLQueryItem queryItemWithName:@"sid" value:self.sessionId],
[NSURLQueryItem queryItemWithName:@"sdk_version" value:@"ios-native-1.0.0"],
[NSURLQueryItem queryItemWithName:@"os" value:@"ios"],
]];
if (self.signSecret.length > 0) {
NSString *exp = [NSString stringWithFormat:@"%ld", (long)([[NSDate date] timeIntervalSince1970] + 600)];
NSString *nonce = [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""];
NSString *raw = [NSString stringWithFormat:@"%@|%@|%@|%@|%@",
self.appId,
host.lowercaseString,
qtype.uppercaseString,
exp,
nonce];
NSString *sign = [self hmacSha256Hex:raw secret:self.signSecret];
[items addObject:[NSURLQueryItem queryItemWithName:@"exp" value:exp]];
[items addObject:[NSURLQueryItem queryItemWithName:@"nonce" value:nonce]];
[items addObject:[NSURLQueryItem queryItemWithName:@"sign" value:sign]];
}
components.queryItems = items;
NSURL *url = components.URL;
if (url == nil) {
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
return;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:6];
request.HTTPMethod = @"GET";
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
config.timeoutIntervalForRequest = 6;
config.timeoutIntervalForResource = 6;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[session finishTasksAndInvalidate];
if (error != nil || data.length == 0) {
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
return;
}
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json isKindOfClass:NSDictionary.class]) {
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
return;
}
NSString *code = [NSString stringWithFormat:@"%@", json[@"code"] ?: @""];
if (![code.uppercaseString isEqualToString:@"SUCCESS"]) {
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
return;
}
NSDictionary *dataJSON = [json[@"data"] isKindOfClass:NSDictionary.class] ? json[@"data"] : @{};
NSArray *records = [dataJSON[@"records"] isKindOfClass:NSArray.class] ? dataJSON[@"records"] : @[];
NSMutableArray<NSString *> *ipv4 = [NSMutableArray array];
NSMutableArray<NSString *> *ipv6 = [NSMutableArray array];
for (NSDictionary *row in records) {
NSString *type = [NSString stringWithFormat:@"%@", row[@"type"] ?: @""];
NSString *ip = [NSString stringWithFormat:@"%@", row[@"ip"] ?: @""];
if (ip.length == 0) {
continue;
}
if ([type.uppercaseString isEqualToString:@"AAAA"]) {
[ipv6 addObject:ip];
} else {
[ipv4 addObject:ip];
}
}
HttpdnsEdgeResolveResult *result = [HttpdnsEdgeResolveResult new];
result.requestId = [NSString stringWithFormat:@"%@", json[@"requestId"] ?: @""];
result.ipv4s = ipv4;
result.ipv6s = ipv6;
result.ttl = [dataJSON[@"ttl"] integerValue];
completion(result, nil);
}] resume];
}
- (NSString *)hmacSha256Hex:(NSString *)data secret:(NSString *)secret {
NSData *keyData = [secret dataUsingEncoding:NSUTF8StringEncoding];
NSData *messageData = [data dataUsingEncoding:NSUTF8StringEncoding];
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256,
keyData.bytes,
keyData.length,
messageData.bytes,
messageData.length,
cHMAC);
NSMutableString *result = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[result appendFormat:@"%02x", cHMAC[i]];
}
return result;
}
@end

View File

@@ -0,0 +1,21 @@
//
// HttpdnsLocalResolver.h
// TrustHttpDNS
//
// Created by xuyecan on 2025/3/16.
// Copyright © 2025 trustapp.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

View File

@@ -0,0 +1,150 @@
//
// HttpdnsLocalResolver.m
// TrustHttpDNS
//
// Created by xuyecan on 2025/3/16.
// Copyright © 2025 trustapp.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; // IPv4IPv6
hints.ai_socktype = SOCK_STREAM; // TCP (DNS<EFBFBD><EFBFBD>?
// 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. IPv4IPv6
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. queryIpTypeIP
BOOL wantIPv4 = NO;
BOOL wantIPv6 = NO;
switch (request.queryIpType) {
case HttpdnsQueryIPTypeAuto:
// AutoIPv4IPv6
// wantIPv4YES
wantIPv4 = YES;
// DNSIPv6IPv6
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<EFBFBD><EFBFBD>?
[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];
// IPv4IPv6TTL<EFBFBD><EFBFBD>?0<EFBFBD><EFBFBD>?
[hostObject setV4TTL:60];
[hostObject setV6TTL:60];
// ttl
[HttpdnsUtil processCustomTTL:hostObject forHost:host service:service];
// (<EFBFBD><EFBFBD>?970)
int64_t now = (int64_t)[[NSDate date] timeIntervalSince1970];
// <EFBFBD><EFBFBD>?
[hostObject setLastIPv4LookupTime:now];
[hostObject setLastIPv6LookupTime:now];
// IPv4IPv6
[hostObject setHasNoIpv4Record:(v4IpObjects.count == 0)];
[hostObject setHasNoIpv6Record:(v6IpObjects.count == 0)];
// clientIp<EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
return hostObject;
}
@end

View 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

View 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.Trust.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;
}
// <EFBFBD><EFBFBD>?
if (![self validateResponseCode:json]) {
return nil;
}
//
id data = [self extractDataContent:json];
if (!data) {
return nil;
}
// <EFBFBD><EFBFBD>?
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;
}
// <EFBFBD><EFBFBD>?
- (BOOL)validateResponseCode:(NSDictionary *)json {
NSString *code = [json objectForKey:@"code"];
if (![code isEqualToString:@"success"]) {
HttpdnsLogDebug("Response code is not success: %@", code);
return NO;
}
return YES;
}
// <EFBFBD><EFBFBD>?
- (id)extractDataContent:(NSDictionary *)json {
// mode<EFBFBD><EFBFBD>?
NSInteger mode = [[json objectForKey:@"mode"] integerValue];
id data = [json objectForKey:@"data"];
if (mode == 1) { // AES-CBC
// <EFBFBD><EFBFBD>?
data = [self decryptData:data withMode:mode];
} else if (mode != 0) {
// AES-GCM<EFBFBD><EFBFBD>?
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;
}
// Base64NSData
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;
}
// 使<EFBFBD><EFBFBD>?
NSError *decryptError = nil;
NSData *decryptedData = [HttpdnsUtil decryptDataAESCBC:encryptedData withKey:keyData error:&decryptError];
if (decryptError || !decryptedData) {
HttpdnsLogDebug("Failed to decrypt data: %@", decryptError);
return nil;
}
// JSON<EFBFBD><EFBFBD>?
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;
}
// <EFBFBD><EFBFBD>?
- (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];
// IPv4TTL
[self setTTLForHostObject:hostObject fromData:v4Data forIPv6:NO];
// v4extra使<EFBFBD><EFBFBD>?
[self processExtraInfo:v4Data forHostObject:hostObject];
// no_ip_codeIPv4
if ([[v4Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
hostObject.hasNoIpv4Record = YES;
}
} else {
// IPv4v4<EFBFBD><EFBFBD>?
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];
// IPv6TTL
[self setTTLForHostObject:hostObject fromData:v6Data forIPv6:YES];
// v4 extra使v6extra
if (![hostObject getExtra]) {
[self processExtraInfo:v6Data forHostObject:hostObject];
}
// no_ip_codeIPv6
if ([[v6Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
hostObject.hasNoIpv6Record = YES;
}
} else {
// IPv6v6<EFBFBD><EFBFBD>?
hostObject.hasNoIpv6Record = YES;
}
}
// IP<EFBFBD><EFBFBD>?
- (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;
}
// <EFBFBD><EFBFBD>?
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];
}
// 使<EFBFBD><EFBFBD>?
- (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;
// <EFBFBD><EFBFBD>?
NSMutableDictionary *paramsToSign = [NSMutableDictionary dictionary];
//
NSMutableDictionary *paramsToEncrypt = [NSMutableDictionary dictionary];
// ID<EFBFBD><EFBFBD>?
[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];
// <EFBFBD><EFBFBD>?
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;
}
// <EFBFBD><EFBFBD>?
- (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];
}];
}
}
// <EFBFBD><EFBFBD>?
- (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;
}
// OCAES-GCMAES-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]]];
}
// <EFBFBD><EFBFBD>?
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 {
// <EFBFBD><EFBFBD>?
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]];
}
}
// <EFBFBD><EFBFBD>?
if ([HttpdnsUtil isNotEmptyString:signature]) {
[finalUrl appendFormat:@"&s=%@", signature];
}
//
[self appendAdditionalParams:finalUrl];
return finalUrl;
}
// <EFBFBD><EFBFBD>?
- (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:Trust_HTTPDNS_ERROR_DOMAIN code:Trust_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再试一<E8AF95><E4B880>?
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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTP_PARSE_JSON_FAILED
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse JSON response"}];
}
return nil;
}
return [self parseHttpdnsResponse:json withQueryIpType:queryIpType];
}
#pragma mark - Helper Functions
// extraNSString
- (NSString *)convertExtraToString:(id)extra {
if (!extra) {
return nil;
}
if ([extra isKindOfClass:[NSString class]]) {
// <EFBFBD><EFBFBD>?
return extra;
} else {
// JSON<EFBFBD><EFBFBD>?
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

View 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

View 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.Trust.sdk.httpdns.persistentCacheOperationQueue", DISPATCH_QUEUE_CONCURRENT);
_asyncResolveHostQueue = dispatch_queue_create("com.Trust.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];
// <EFBFBD><EFBFBD>?
[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;
}
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
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) {
//
// <EFBFBD><EFBFBD>?
[self determineResolvingHostNonBlocking:request];
}
// cacheKeyhost
result.hostName = host;
HttpdnsLogDebug("Reuse available cache for cacheKey: %@, result: %@", cacheKey, result);
// <EFBFBD><EFBFBD>?
return result;
}
if (request.isBlockingRequest) {
// <EFBFBD><EFBFBD>?
return [self determineResolveHostBlocking:request];
} else {
// <EFBFBD><EFBFBD>?
[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]) {
// <EFBFBD><EFBFBD>?
HttpdnsLogDebug("The ips is expired, but we accept it, host: %@, queryType: %ld, expiredIpEnabled: %d, isLoadFromDB: %d",
hostObject.hostName, queryType, self.atomicExpiredIPEnabled, [hostObject isLoadFromDB]);
// <EFBFBD><EFBFBD>?
return (HostObjectExamingResult){YES, YES};
}
// <EFBFBD><EFBFBD>?
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<EFBFBD><EFBFBD>?
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];
// <EFBFBD><EFBFBD>?
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
// SDNScacheKeyhostName
[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];
// <EFBFBD><EFBFBD>?
// 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);
// :
// - <EFBFBD><EFBFBD>?
// - <EFBFBD><EFBFBD>?
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<EFBFBD><EFBFBD>?
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<EFBFBD><EFBFBD>?
// SDNS 使 cacheKey <EFBFBD><EFBFBD>?
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];
}
}
// <EFBFBD><EFBFBD>?
// 3<EFBFBD><EFBFBD>?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
// hostName <EFBFBD><EFBFBD>?SDNS cacheKey <EFBFBD><EFBFBD>?
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];
}
});
// <EFBFBD><EFBFBD>?
_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.Trust.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使<EFBFBD><EFBFBD>?
[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];
}
}
// <EFBFBD><EFBFBD>?
dispatch_async(_persistentCacheConcurrentQueue, ^{
[self->_httpdnsDB deleteByHostNameArr:hostArray];
});
}
- (void)cleanMemoryAndPersistentCacheOfAllHosts {
[_hostObjectInMemoryCache removeAllHostObjects];
// <EFBFBD><EFBFBD>?
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 - <EFBFBD><EFBFBD>?
- (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

View 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 <TrustHTTPDNS/HttpdnsRequest.h>
#import <TrustHTTPDNS/HttpDnsResult.h>
#import <TrustHTTPDNS/HttpdnsLoggerProtocol.h>
#import <TrustHTTPDNS/HttpdnsDegradationDelegate.h>
#define Trust_HTTPDNS_DEPRECATED(explain) __attribute__((deprecated(explain)))
#ifndef TrustHDNS_STACK_KEY
#define TrustHDNS_STACK_KEY
#define TrustHDNS_IPV4 @"TrustHDNS_IPV4"
#define TrustHDNS_IPV6 @"TrustHDNS_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:(TrustHttpDNS_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 Trust_HTTPDNS_DEPRECATED("不再建议通过设置此回调实现降级逻辑而是自行在调用HTTPDNS解析域名前做判断");
@property (nonatomic, weak) id<HttpdnsTTLDelegate> ttlDelegate;
+ (nonnull instancetype)sharedInstance;
/// 获取指定账号对应<E5AFB9><E5BA94>?HttpDnsService 实例
/// @param accountID 账号 ID
/// @return 已初始化的实例,若账号尚未注册则返回 nil
+ (nullable instancetype)getInstanceByAccountId:(NSInteger)accountID;
/*!
* @brief 无需鉴权功能的初始化接口
* @details 初始化,设置 HTTPDNS 服务 Account ID。使用本接口初始化请求将无任何签名保护请谨慎使用<E4BDBF><E794A8>?
* 您可以从控制台获取您<E58F96><E682A8>?Account ID <20><>?
* 此方法会初始化为单例<E58D95><E4BE8B>?
* 注意本接口<E68EA5><E58FA3>?.2.1起废弃后续将进行移除<EFBFBD><EFBFBD>?
* @param accountID 您的 HTTPDNS Account ID
*/
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID Trust_HTTPDNS_DEPRECATED("Deprecated. This method will be removed in the future. Use -[HttpDnsService initWithAccountID:secretKey:] instead.");
/*!
* @brief 启用鉴权功能的初始化接口
* @details 初始化、开启鉴权功能并设<E5B9B6><E8AEBE>?HTTPDNS 服务 Account ID鉴权功能对应的 secretKey<65><79>?
* 您可以从控制台获取您<E58F96><E682A8>?Account ID 、secretKey信息<E4BFA1><E681AF>?
* 此方法会初始化为单例<E58D95><E4BE8B>?
* @param accountID 您的 HTTPDNS Account ID
* @param secretKey 鉴权对应<E5AFB9><E5BA94>?secretKey
*/
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID secretKey:(NSString * _Nonnull)secretKey;
/*!
* @brief 启用鉴权功能、加密功能的初始化接<E58C96><E68EA5>?
* @details 初始化、开启鉴权功能、开启AES加密并设置 HTTPDNS 服务 Account ID鉴权功能对应的 secretKey加密功能对应的 aesSecretKey<65><79>?
* 您可以从控制台获取您<E58F96><E682A8>?Account ID 、secretKey、aesSecretKey 信息<E4BFA1><E681AF>?
* 此方法会初始化为单例<E58D95><E4BE8B>?
* @param accountID 您的 HTTPDNS Account ID
* @param secretKey 鉴权对应<E5AFB9><E5BA94>?secretKey
* @param aesSecretKey 加密功能对应<E5AFB9><E5BA94>?aesSecretKey
*/
- (nonnull instancetype)initWithAccountID:(NSInteger)accountID secretKey:(NSString * _Nonnull)secretKey aesSecretKey:(NSString * _Nullable)aesSecretKey;
/// 开启鉴权功能后,鉴权的签名计算默认读取设备当前时间。若担心设备时间不准确导致签名不准确,可以使用此接口校正 APP 内鉴权计算使用的时间<E697B6><E997B4>?
/// 注意,校正操作在 APP 的一个生命周期内生效APP 重启后需要重新设置才能重新生<E696B0><E7949F>?
/// @param authCurrentTime 用于校正的时间戳单位为<E4BD8D><E4B8BA>?
- (void)setAuthCurrentTime:(NSUInteger)authCurrentTime Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setInternalAuthTimeBaseBySpecifyingCurrentTime:] instead.");
/// 开启鉴权功能后,鉴权的签名计算默认读取设备当前时间。若担心设备时间不准确导致签名不准确,可以使用此接口校正 APP 内鉴权计算使用的时间<E697B6><E997B4>?
/// 注意,校正操作在 APP 的一个生命周期内生效APP 重启后需要重新设置才能重新生<E696B0><E7949F>?
/// @param currentTime 用于校正的时间戳单位为<E4BD8D><E4B8BA>?
- (void)setInternalAuthTimeBaseBySpecifyingCurrentTime:(NSTimeInterval)currentTime;
/// 设置持久化缓存功<E5AD98><E58A9F>?
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setCachedIPEnabled:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setPersistentCacheIPEnabled:] instead.");
/// 设置持久化缓存功<E5AD98><E58A9F>?
/// 开启后,每次解析会将结果持久化缓存到本地,当下次应用启动时,可以从本地加载缓存解析结果,提高应用启动时获取解析结果的速度
/// 加载时,会丢弃已经过期的解析结果
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setPersistentCacheIPEnabled:(BOOL)enable;
/// 设置持久化缓存功<E5AD98><E58A9F>?
/// 开启后,每次解析会将结果持久化缓存到本地,当下次应用启动时,可以从本地加载缓存解析结果,提高应用启动时获取解析结果的速度
/// 加载时,会丢弃过期时间已经超过指定值的解析结果
/// @param enable YES: 开<><E5BC80>?NO: 关闭
/// @param duration 决定丢弃IP的过期时间阈值单位为秒过期超过这个时间范围的IP会被丢弃取值范围为0-1年。这个值仅在开启持久化缓存功能时才有意<E69C89><E6848F>?
- (void)setPersistentCacheIPEnabled:(BOOL)enable discardRecordsHasExpiredFor:(NSTimeInterval)duration;
/// 是否允许 HTTPDNS 返回 TTL 过期域名<E59F9F><E5908D>?ip ,建议允许(默认不允许)
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setExpiredIPEnabled:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setReuseExpiredIPEnabled:] instead.");
/// 是否允许 HTTPDNS 返回 TTL 过期域名<E59F9F><E5908D>?ip ,建议允许(默认不允许)
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setReuseExpiredIPEnabled:(BOOL)enable;
/// 设置 HTTPDNS 域名解析请求类型 ( HTTP / HTTPS )
/// 若不调用该接口默认<E9BB98><E8AEA4>?HTTP 请求<E8AFB7><E6B182>?
/// HTTP 请求基于底层 CFNetwork 实现<EFBC8C><E4B88D>?ATS 限制<E99990><E588B6>?
/// @param enable YES: HTTPS请求 NO: HTTP请求
- (void)setHTTPSRequestEnabled:(BOOL)enable;
/// 声明App是否配置了ATS为AllowsArbitraryLoads默认认为没有配<E69C89><E9858D>?
/// 若做了声明则当指定走HTTP方式解析域名时解析链路会走系统NSURLSession逻辑
/// 否则会走定制的CFHTTP链路避免被ATS拦截请求
- (void)setHasAllowedArbitraryLoadsInATS:(BOOL)hasAllowedArbitraryLoadsInATS;
/// 设置底层HTTPDNS网络请求超时时间单位为<E4BD8D><E4B8BA>?
/// 需要注意这个值只决定底层解析请求的网络超时时间而非同步解析接口、异步解析接口的最长阻塞或者等待时<E5BE85><E697B6>?
/// 同步解析接口、异步解析接口的最长阻塞或者等待时间需要调用接口时设置request参数中的`resolveTimeoutInSecond`决定
/// @param timeoutInterval 超时时间单位为<E4BD8D><E4B8BA>?
- (void)setNetworkingTimeoutInterval:(NSTimeInterval)timeoutInterval;
/// 指定region指定后会读取该region对应配置作为初始化配<E58C96><E9858D>?
/// 一般情况下无需设置SDK内部会默认路由全球范围内最近的接入<E68EA5><E585A5>?
/// @param region 需要指定的region缺省为中国大陆
- (void)setRegion:(NSString *)region;
/// 域名预解<E9A284><E8A7A3>?(默认解析双栈记录)
/// 通常用于启动后立即向SDK设置您后续可能会使用到的热点域名以便SDK提前解析减少后续解析域名时请求的时<E79A84><E697B6>?
/// 如果是在运行过程中调用SDK也会立即解析设置的域名数组中的域名刷新这些域名的解析结<E69E90><E7BB93>?
///
/// @param hosts 预解析列表数<E8A1A8><E695B0>?
- (void)setPreResolveHosts:(NSArray *)hosts;
/// 域名预解析可以指定预解析auto、ipv4、ipv6、both
/// 通常用于启动后立即向SDK设置您后续可能会使用到的热点域名以便SDK提前解析减少后续解析域名时请求的时<E79A84><E697B6>?
/// 如果是在运行过程中调用SDK也会立即解析设置的域名数组中的域名刷新这些域名的解析结<E69E90><E7BB93>?
///
/// @param hosts 预解析列表数<E8A1A8><E695B0>?
/// @param ipType 指定预解析记录类<E5BD95><E7B1BB>?
- (void)setPreResolveHosts:(NSArray *)hosts byIPType:(HttpdnsQueryIPType)ipType;
/// 域名预解<E9A284><E8A7A3>?
/// @param hosts 域名
/// @param ipType 4: ipv4; 6: ipv6; 64: ipv4+ipv6
- (void)setPreResolveHosts:(NSArray *)hosts queryIPType:(TrustHttpDNS_IPType)ipType Trust_HTTPDNS_DEPRECATED("Deprecated, this method will be removed in the future. Use -[HttpDnsService setPreResolveHosts:byIPType:] instead.");
/// 本地日志 log 开<><E5BC80>?
/// @param enable YES: 打开 NO: 关闭
- (void)setLogEnabled:(BOOL)enable;
/// 设置网络切换时是否自动更新所有域名解析结<E69E90><E7BB93>?
/// 如果打开此开关在网络切换时会自动刷新所有域名的解析结果但会产生一定流量消<E9878F><E6B688>?
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setPreResolveAfterNetworkChanged:(BOOL)enable;
/// 设置当httpdns解析失败时是否降级到localDNS尝试解析
/// 降级生效时SDNS参数不生效降级逻辑只解析域名返回的结果默认使<E8AEA4><E4BDBF>?0<><30>?若未指定该域名自定义TTL)作为TTL<54><4C>?
/// 降级请求也不会再对ip进行优先排序
/// 默认关闭不会自动降<E58AA8><E9998D>?
/// @param enable YES自动降<E58AA8><E9998D>?NO不自动降级
- (void)setDegradeToLocalDNSEnabled:(BOOL)enable;
/// 设置IP排优规则
/// @param IPRankingDatasource 设置对应域名的端口号
/// @{host: port}
- (void)setIPRankingDatasource:(NSDictionary<NSString *, NSNumber *> *)IPRankingDatasource;
/// 设置是否 开<><E5BC80>?IPv6 结果解析。只有开启状态下对域名的解析才会尝试解析v6记录并返回v6的结<E79A84><E7BB93>?
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)enableIPv6:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService setIPv6Enabled:] instead.");
/// 设置是否 开<><E5BC80>?IPv6 结果解析。只有开启状态下对域名的解析才会尝试解析v6记录并返回v6的结<E79A84><E7BB93>?
/// 已弃用。默认支持IPv6。如果不需要IPv6类型的结果只需在请求时指定`queryIpType`为`HttpdnsQueryIPTypeIpv4`
/// @param enable YES: 开<><E5BC80>?NO: 关闭
- (void)setIPv6Enabled:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. If ipv6 is unnecessary, you can set the `queryIpType` as HttpdnsQueryIPTypeIpv4 when resolving domain.");
/// 是否允许通过 CNCopyCurrentNetworkInfo 获取wifi ssid bssid
/// @param enable YES: 开<><E5BC80>?NO: 关闭 默认关<E8AEA4><E585B3>?
- (void)enableNetworkInfo:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. We do not utilize network information anymore");
/// 是否允许通过 CNCopyCurrentNetworkInfo 获取wifi ssid bssid
/// @param enable YES: 开<><E5BC80>?NO: 关闭 默认关<E8AEA4><E585B3>?
- (void)setReadNetworkInfoEnabled:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated. We do not utilize network information anymore.");
/// 是否开启IP探测功能
/// @param enable YES: 开<><E5BC80>?NO: 关闭 默认打开
- (void)enableCustomIPRank:(BOOL)enable Trust_HTTPDNS_DEPRECATED("Deprecated, will be removed in the future.");
/// 设置软件自定义解析全局默认参数设置后调用软件自定义解析时每个请求默认都会带上这里配置的参<E79A84><E58F82>?
/// @param params 全局默认参数
- (void)setSdnsGlobalParams:(NSDictionary<NSString *, NSString *> *)params;
/// 设置日志输出回调以实现自定义日志输出方<E587BA><E696B9>?
/// @param logHandler 日志输出回调实现实例
- (void)setLogHandler:(id<HttpdnsLoggerProtocol>)logHandler;
/// 获取用于用户追踪<E8BFBD><E8B8AA>?sessionId
/// sessionId为随机生成长度<E995BF><E5BAA6>?12 位App 生命周期内保持不<E68C81><E4B88D>?
/// 为了排查可能的解析问题需要您<E8A681><E682A8>?sessionId 和解析出<E69E90><E587BA>?IP 一起记录在日志<E697A5><E5BF97>?
/// 请参<E8AFB7><E58F82>? 解析异常排查<E68E92><E69FA5>?“会话追踪方案<E696B9><E6A188>?https://help.TrustAPP.com/document_detail/100530.html
- (NSString *)getSessionId;
/// 同步解析域名会阻塞当前线程直到从缓存中获取到有效解析结果或者从服务器拿到最新解析结<E69E90><E7BB93>?
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// 为了防止在主线程中误用本接口导致APP卡顿本接口会做检测若发现调用线程是主线程则自动降级到resolveHostSyncNonBlocking接口的实现逻辑<E980BB><E8BE91>?
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析ipv4或ipv6
/// @return 解析结果
- (nullable HttpdnsResult *)resolveHostSync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
/// 同步解析域名会阻塞当前线程直到从缓存中获取到有效解析结果或者从服务器拿到最新解析结<E69E90><E7BB93>?
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// 为了防止在主线程中误用本接口导致APP卡顿本接口会做检测若发现调用线程是主线程则自动降级到resolveHostSyncNonBlocking接口的实现逻辑<E980BB><E8BE91>?
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析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;
/// 同步解析域名会阻塞当前线程直到从缓存中获取到有效解析结果或者从服务器拿到最新解析结<E69E90><E7BB93>?
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// 为了防止在主线程中误用本接口导致APP卡顿本接口会做检测若发现调用线程是主线程则自动降级到resolveHostSyncNonBlocking接口的实现逻辑<E980BB><E8BE91>?
/// @param request 请求参数对象
/// @return 解析结果
- (nullable HttpdnsResult *)resolveHostSync:(HttpdnsRequest *)request;
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先在回调中返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析ipv4或ipv6
/// @handler 解析结果回调
- (void)resolveHostAsync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType completionHandler:(void (^)(HttpdnsResult * nullable))handler;
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先在回调中返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析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;
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
/// 如果允许复用过期的解析结果且存在过期结果的情况下会先在回调中返回这个结果然后启动后台线程去更新解析结<E69E90><E7BB93>?
/// @param request 请求参数对象
/// @handler 解析结果回调
- (void)resolveHostAsync:(HttpdnsRequest *)request completionHandler:(void (^)(HttpdnsResult * nullable))handler;
/// 伪异步解析域名不会阻塞当前线程首次解析结果可能为<E883BD><E4B8BA>?
/// 先查询缓存缓存中存在有效结<E69588><E7BB93>?未过期或者过期但配置了可以复用过期解析结<E69E90><E7BB93>?,则直接返回结果,如果缓存未命中,则发起异步解析请求
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析ipv4或ipv6
/// @return 解析结果
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
/// 伪异步解析域名不会阻塞当前线程首次解析结果可能为<E883BD><E4B8BA>?
/// 先查询缓存缓存中存在有效结<E69588><E7BB93>?未过期或者过期但配置了可以复用过期解析结<E69E90><E7BB93>?,则直接返回结果,如果缓存未命中,则发起异步解析请求
/// @param host 需要解析的域名
/// @param queryIpType 可设置为自动选择ipv4ipv6. 设置为自动选择时会自动根据当前所处网络环境选择解析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;
/// 伪异步解析域名不会阻塞当前线程首次解析结果可能为<E883BD><E4B8BA>?
/// 先查询缓存缓存中存在有效结<E69588><E7BB93>?未过期或者过期但配置了可以复用过期解析结<E69E90><E7BB93>?,则直接返回结果,如果缓存未命中,则发起异步解析请求
/// @param request 请求参数对象
/// @return 解析结果
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(HttpdnsRequest *)request;
/// 获取域名对应的IP单IP
/// @param host 域名
- (NSString *)getIpByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的IP数组多IP
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSArray *)getIpsByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的ipv6, 单IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSString *)getIPv6ByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的ipv6数组, 多IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSArray *)getIPv6sByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 同时获取ipv4 ipv6的IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// @param host 域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
- (NSDictionary <NSString *, NSArray *>*)getIPv4_v6ByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址<E59CB0><E59D80>?
/// 使用此API 需要确<E8A681><E7A1AE>?enableIPv6 开关已打开
/// 设备网络 返回域名IP
/// IPv4 Only IPv4
/// IPv6 Only IPv6 如果没有Pv6返回空
/// 双栈 IPv4/IPV6
/// @param host 要解析的域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
-(NSDictionary <NSString *, NSArray *>*)autoGetIpsByHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的IPv4地址单IPv4
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSString *)getIPv4ForHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的IP数组多IP
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSArray *)getIPv4ListForHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 获取IPv4地址列表同步接口必须在子线程中执行否则会转变为异步接口
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限<E4B88A><E99990>?s<><73>?
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大<E997B4><E5A4A7>?s同步接口也最多阻塞当前线<E5898D><E7BABF>?s
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起同步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSArray *)getIPv4ListForHostSync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的ipv6, 单IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSString *)getIPv6ForHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应的ipv6数组, 多IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSArray *)getIPv6ListForHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 获取IPv6地址列表同步接口必须在子线程中执行否则会转变为异步接口
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限<E4B88A><E99990>?s<><73>?
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大<E997B4><E5A4A7>?s同步接口也最多阻塞当前线<E5898D><E7BABF>?s
/// @param host 域名
- (NSArray *)getIPv6ListForHostSync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
/// 异步接口首次结果可能为空获取域名对应格式化后的IP (针对ipv6)
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
- (NSString *)getIpByHostAsyncInURLFormat:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 异步接口首次结果可能为空同时获取ipv4 ipv6的IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
- (NSDictionary <NSString *, NSArray *>*)getHttpDnsResultHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// NOTE: 同步接口必须在子线程中执行否则会转变为异步接<E6ADA5><E68EA5>?
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限<E4B88A><E99990>?s<><73>?
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大<E997B4><E5A4A7>?s同步接口也最多阻塞当前线<E5898D><E7BABF>?s
/// 同时获取ipv4 + ipv6的IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
- (NSDictionary <NSString *, NSArray *>*)getHttpDnsResultHostSync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
/// 异步接口,首次结果可能为空,根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址<E59CB0><E59D80>?
/// 使用此API 需要确<E8A681><E7A1AE>?enableIPv6 开关已打开
/// 设备网络 返回域名IP
/// IPv4 Only IPv4
/// IPv6 Only IPv6 如果没有Pv6返回空
/// 双栈 IPv4/IPV6
/// @param host 要解析的域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
-(NSDictionary <NSString *, NSArray *>*)autoGetHttpDnsResultForHostAsync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:] instead.");
/// 根据当前设备的网络状态自动返回域名对应的 IPv4/IPv6地址组同步接口必须在子线程中执行否则会转变为异步接<E6ADA5><E68EA5>?
/// 同步接口有超时机制,超时时间为[HttpDnsService sharedInstance].timeoutInterval, 但是超时上限<E4B88A><E99990>?s<><73>?
/// 即使[HttpDnsService sharedInstance].timeoutInterval设置的时间大<E997B4><E5A4A7>?s同步接口也最多阻塞当前线<E5898D><E7BABF>?s
/// 根据当前网络栈自动获取ipv4 ipv6的IP 需要开启ipv6 开<><E5BC80>?enableIPv6<76><36>?
/// 先查询缓存缓存中存在未过期的结果则直接返回结果如果缓存未命中则发起异步解析请<E69E90><E8AFB7>?
/// @param host 域名
/// @result 返回字典类型结构
/// {
/// TrustHDNS_IPV4: ['xxx.xxx.xxx.xxx', 'xxx.xxx.xxx.xxx'],
/// TrustHDNS_IPV6: ['xx:xx:xx:xx:xx:xx:xx:xx', 'xx:xx:xx:xx:xx:xx:xx:xx']
/// }
- (NSDictionary <NSString *, NSArray *>*)autoGetHttpDnsResultForHostSync:(NSString *)host Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSync:byIpType:] instead.");
/// 软件自定义解析接<E69E90><E68EA5>?
- (NSDictionary *)getIpsByHostAsync:(NSString *)host withParams:(NSDictionary<NSString *, NSString *> *)params withCacheKey:(NSString *)cacheKey Trust_HTTPDNS_DEPRECATED("Deprecated. Use -[HttpDnsService resolveHostSyncNonBlocking:byIpType:withSdnsParams:sdnsCacheKey:] instead.");
/// 清除指定host缓存<EFBC88><E58685>?沙盒数据库)
/// @param hostArray 需要清除的host域名数组。如果需要清空全部数据传nil或者空数组即可
- (void)cleanHostCache:(nullable NSArray<NSString *> *)hostArray;
/// 清除当前所有host缓存 (内存+沙盒数据<E695B0><E68DAE>?
- (void)cleanAllHostCache;
/// 清理已经配置的软件自定义解析全局参数
- (void)clearSdnsGlobalParams;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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>

View File

@@ -0,0 +1,50 @@
//
// HttpdnsIpStackDetector.h
// TrustHttpDNS
//
// Created by xuyecan on 2025/3/16.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* IP 协议栈类<E6A088><E7B1BB>?
*/
typedef enum {
kHttpdnsIpUnknown = 0, // 未知协议<E58D8F><E8AEAE>?
kHttpdnsIpv4Only = 1, // IPv4-only
kHttpdnsIpv6Only = 2, // IPv6-only
kHttpdnsIpDual = 3 // 双栈
} HttpdnsIPStackType;
@interface HttpdnsIpStackDetector : NSObject
/**
* 返回HttpdnsIpStackDetector的共享实<E4BAAB><E5AE9E>?
* @return HttpdnsIpStackDetector实例
*/
+ (instancetype)sharedInstance;
/**
* 返回当前缓存的IP协议栈类型不执行检<E8A18C><E6A380>?
* @return HttpdnsIPStackType
*/
- (HttpdnsIPStackType)currentIpStack;
/**
* 返回当前是否是IPv6-only网络
* @return BOOL
*/
- (BOOL)isIpv6OnlyNetwork;
/**
* 强制重新检测IP协议栈类<E6A088><E7B1BB>?
*/
- (void)redetectIpStack;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,185 @@
//
// HttpdnsIpStackDetector.m
// NewHttpDNS
//
// Created by xuyecan on 2025/3/16.
// Copyright © 2025 new-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
* IPv4IPv6
*
*/
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;
}
/*
* IPv4IPv6AI_ADDRCONFIG
*
* AI_ADDRCONFIG
* "在本地系统上配置"
* bionicgetifaddrs
*
*/
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));
}
/**
* IPv4IPv6IP
*/
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.new.httpdns.ipstack.detect", DISPATCH_QUEUE_SERIAL);
_updateSerialQueue = dispatch_queue_create("com.new.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

View 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

View 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

View File

@@ -0,0 +1,45 @@
//
// HttpdnsLog_Internal.h
// TrustHttpDNS
//
// Created by junmo on 2018/12/19.
// Copyright © 2018<31><38>?trustapp.com. All rights reserved.
//
#import "HttpdnsLog.h"
#import "HttpdnsLoggerProtocol.h"
#import <pthread/pthread.h>
// logHandler输出日志不受日志开关影<E585B3><E5BDB1>?
#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

View File

@@ -0,0 +1,20 @@
//
// HttpdnsLoggerProtocol.h
// TrustHttpDNS
//
// Created by junmo on 2018/12/19.
// Copyright © 2018<31><38>?trustapp.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 */

View File

@@ -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需要重复请<E5A48D><E8AFB7>?
// 这个信息不用持久化一次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;
/**
* 将当前对象转换为数据库记录对<E5BD95><E5AFB9>?
* @return 数据库记录对<E5BD95><E5AFB9>?
*/
- (HttpdnsHostRecord *)toDBRecord;
- (NSArray<NSString *> *)getV4IpStrings;
- (NSArray<NSString *> *)getV6IpStrings;
/**
* 更新指定IP的connectedRT值并重新排序IP列表
* @param ip 需要更新的IP地址
* @param connectedRT 检测到的RT值-1表示不可<E4B88D><E58FAF>?
*/
- (void)updateConnectedRT:(NSInteger)connectedRT forIP:(NSString *)ip;
@end

View 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<EFBFBD><EFBFBD>?
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) {
// _hasNoIpv4Recordtrueipv4ip<EFBFBD><EFBFBD>?
if ([HttpdnsUtil isEmptyArray:[self getV4Ips]] && !_hasNoIpv4Record) {
return YES;
}
} else if (queryType & HttpdnsQueryIPTypeIpv6 && !_hasNoIpv6Record) {
// _hasNoIpv6Recordtrueipv6ip<EFBFBD><EFBFBD>?
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 {
// IPIP<EFBFBD><EFBFBD>?
NSArray<NSString *> *v4IpStrings = [self getV4IpStrings];
NSArray<NSString *> *v6IpStrings = [self getV6IpStrings];
// modifyAt
NSDate *currentDate = [NSDate date];
// 使hostNamecacheKeyfromDBRecord<EFBFBD><EFBFBD>?
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;
}
// IPconnectedRT
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;
}
// connectedRTIP<EFBFBD><EFBFBD>?1<EFBFBD><EFBFBD>?
[mutableIpObjects sortUsingComparator:^NSComparisonResult(HttpdnsIpObject *obj1, HttpdnsIpObject *obj2) {
// obj1connectedRT<EFBFBD><EFBFBD>?1<EFBFBD><EFBFBD>?
if (obj1.connectedRT == -1) {
return NSOrderedDescending;
}
// obj2connectedRT<EFBFBD><EFBFBD>?1<EFBFBD><EFBFBD>?
if (obj2.connectedRT == -1) {
return NSOrderedAscending;
}
// connectedRT<EFBFBD><EFBFBD>?
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

View File

@@ -0,0 +1,53 @@
//
// HttpdnsHostRecord.h
// TrustHttpDNS
//
// Created by ElonChan地风 on 2017/5/3.
// Copyright © 2017<31><37>?trustapp.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

View File

@@ -0,0 +1,91 @@
//
// HttpdnsHostRecord.m
// TrustHttpDNS
//
// Created by ElonChan on 2017/5/3.
// Copyright © 2017<EFBFBD><EFBFBD>?trustapp.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

View File

@@ -0,0 +1,64 @@
//
// HttpdnsRequest.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/5/19.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef TrustHTTPDNSQUERYIPTYPE
#define TrustHTTPDNSQUERYIPTYPE
typedef enum {
TrustHttpDNS_IPTypeV4 = 0, //ipv4
TrustHttpDNS_IPTypeV6 = 1, //ipv6
TrustHttpDNS_IPTypeV64 = 2, //ipv4 + ipv6
} TrustHttpDNS_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;
/// 解析超时时间对于同步接口即为最大等待时间对于异步接口即为最大等待回调时<E8B083><E697B6>?
/// 默认<E9BB98><E8AEA4>?秒取值必须在0.5<EFBFBD><EFBFBD>?- 5秒之<E7A792><E4B98B>?
@property (nonatomic, assign) double resolveTimeoutInSecond;
/// 查询IP类型
/// 默认为HttpdnsQueryIPTypeAuto此类型下SDK至少会请求解析ipv4地址若判断到当前网络环境支持ipv6则还会请求解析ipv6地址
/// HttpdnsQueryIPTypeIpv4只请求解析ipv4
/// HttpdnsQueryIPTypeIpv6只请求解析ipv6
/// HttpdnsQueryIPTypeBoth不管当前网络环境是什么会尝试同时请求解析ipv4地址和ipv6地址这种用法通常需要拿到结果之后自行判断网络环境决定使用哪个结<E4B8AA><E7BB93>?
@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

View File

@@ -0,0 +1,76 @@
//
// HttpdnsRequest.m
// TrustHttpDNS
//
// Created by xuyecan on 2024/5/19.
// Copyright © 2024 trustapp.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 {
// <EFBFBD><EFBFBD>?
}
}
- (NSString *)description {
return [NSString stringWithFormat:@"Host: %@, isBlockingRequest: %d, queryIpType: %ld, sdnsParams: %@, cacheKey: %@", self.host, self.isBlockingRequest, self.queryIpType, self.sdnsParams, self.cacheKey];
}
@end

View File

@@ -0,0 +1,25 @@
//
// HttpdnsRequest_Internal.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/6/19.
// Copyright © 2024 trustapp.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 */

View File

@@ -0,0 +1,42 @@
//
// HttpdnsResult.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/5/15.
// Copyright © 2024 trustapp.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

View File

@@ -0,0 +1,53 @@
//
// HttpdnsResult.m
// TrustHttpDNS
//
// Created by xuyecan on 2024/5/15.
// Copyright © 2024 trustapp.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

View File

@@ -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
/// 全局共享实例复用底层连接池线程安<E7A88B><E5AE89>?
+ (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

View File

@@ -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
// <EFBFBD><EFBFBD>?
@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.Trust.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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTP_PARSE_JSON_FAILED
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
}
return HttpdnsHTTPChunkParseResultError;
}
if (chunkSize > NSUIntegerMax - cursor) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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 <EFBFBD><EFBFBD>?
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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_HTTPS_COMMON_ERROR_CODE
userInfo:userInfo];
}
@end
#if DEBUG
// <EFBFBD><EFBFBD>?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

View File

@@ -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 编码检<E7A081><E6A380>?
- (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;
// 连接<E8BF9E><E68EA5>?key 生成
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS;
// 错误转换
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description;
@end
#if DEBUG
// 测试专用连接池检<E6B1A0><E6A380>?API
@interface HttpdnsNWHTTPClient (TestInspection)
// 获取指定 pool key 的连接数<E68EA5><E695B0>?
- (NSUInteger)connectionPoolCountForKey:(NSString *)key;
// 获取所有连接池 keys
- (NSArray<NSString *> *)allConnectionPoolKeys;
// 获取连接池总连接数
- (NSUInteger)totalConnectionCount;
// 连接创建计数(用于验证连接复用)
@property (atomic, assign) NSUInteger connectionCreationCount;
// 连接复用计数(用于验证连接复用)
@property (atomic, assign) NSUInteger connectionReuseCount;
// 重置统计计数器每个测试开始前调用<E8B083><E794A8>?
- (void)resetPoolStatistics;
@end
#endif
NS_ASSUME_NONNULL_END

View File

@@ -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 - 可读<E58FAF><E8AFBB>?
// @property inUse - 可读<E58FAF><E8AFBB>?
// @property invalidated - 只读
// 测试辅助方法
- (void)debugSetLastUsedDate:(nullable NSDate *)date;
- (void)debugSetInUse:(BOOL)inUse;
- (void)debugInvalidate;
@end
#endif
NS_ASSUME_NONNULL_END

View File

@@ -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;
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
@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.Trust.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 = Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_HTTPS_COMMON_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: @"Empty HTTP request"}];
}
return nil;
}
if (![self isViable] || self.currentExchange) {
if (error) {
*error = _stateError ?: [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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) {
// <EFBFBD><EFBFBD>?
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
// <EFBFBD><EFBFBD>?
@implementation HttpdnsNWReusableConnection (DebugInspection)
- (void)debugSetLastUsedDate:(nullable NSDate *)date {
self.lastUsedDate = date;
}
- (void)debugSetInUse:(BOOL)inUse {
self.inUse = inUse;
}
- (void)debugInvalidate {
[self invalidate];
}
@end
#endif

View 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>
#import <TrustHTTPDNS/HttpdnsLog.h>
#import <TrustHTTPDNS/HttpdnsPublicConstant.h>
#import <TrustHTTPDNS/HttpdnsService.h>
#import <TrustHTTPDNS/HttpdnsRequest.h>
#import <TrustHTTPDNS/HttpDnsResult.h>
#import <TrustHTTPDNS/HttpdnsEdgeService.h>
#import <TrustHTTPDNS/HttpdnsLoggerProtocol.h>
#import <TrustHTTPDNS/HttpdnsDegradationDelegate.h>
#import <TrustHTTPDNS/HttpdnsIpStackDetector.h>

View File

@@ -0,0 +1,75 @@
//
// HttpdnsDB.h
// TrustHttpDNS
//
// Created by xuyecan on 2025/3/15.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpdnsHostRecord.h"
NS_ASSUME_NONNULL_BEGIN
/**
* SQLite3数据库操作类用于持久化存储HttpDNS缓存记录
*/
@interface HttpdnsDB : NSObject
/**
* 初始化数据库
* @param accountId 账户ID
* @return 数据库实<E5BA93><E5AE9E>?
*/
- (instancetype)initWithAccountId:(NSInteger)accountId;
/**
* 创建或更新记<E696B0><E8AEB0>?
* @param record 主机记录
* @return 是否成功
*/
- (BOOL)createOrUpdate:(HttpdnsHostRecord *)record;
/**
* 根据缓存键查询记<E8AFA2><E8AEB0>?
* @param cacheKey 缓存<E7BC93><E5AD98>?
* @return 查询到的记录如果不存在则返回nil
*/
- (nullable HttpdnsHostRecord *)selectByCacheKey:(NSString *)cacheKey;
/**
* 根据缓存键删除记<E999A4><E8AEB0>?
* @param cacheKey 缓存<E7BC93><E5AD98>?
* @return 是否成功
*/
- (BOOL)deleteByCacheKey:(NSString *)cacheKey;
/**
* 根据主机名数组批量删除记<E999A4><E8AEB0>?
* @param hostNameArr 主机名数<E5908D><E695B0>?
* @return 成功删除的记录数<E5BD95><E695B0>?
*/
- (NSInteger)deleteByHostNameArr:(NSArray<NSString *> *)hostNameArr;
/**
* 获取所有缓存记<E5AD98><E8AEB0>?
* @return 所有缓存记录数<E5BD95><E695B0>?
*/
- (NSArray<HttpdnsHostRecord *> *)getAllRecords;
/**
* 清理指定时间点已过期的记<E79A84><E8AEB0>?
* @param specifiedTime 指定的时间点epoch时间<E697B6><E997B4>?
* @return 清理的记录数<E5BD95><E695B0>?
*/
- (NSInteger)cleanRecordAlreadExpiredAt:(NSTimeInterval)specifiedTime;
/**
* 删除所有记<E69C89><E8AEB0>?
* @return 是否成功
*/
- (BOOL)deleteAll;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,608 @@
//
// HttpdnsDB.m
// NewHttpDNS
//
// Created by xuyecan on 2025/3/15.
// Copyright © 2025 new-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.new.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) {
// createAtmodifyAt
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 {
// createAtmodifyAt
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];
// IPv4IPv4
if (v4Expired) {
[v4ips removeAllObjects];
}
// IPv6IPv6
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

View File

@@ -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;
/// 多账号隔离返回指定账号的调度结果目<E69E9C><E79BAE>?
+ (NSString *)scheduleCenterResultDirectoryForAccount:(NSInteger)accountId;
+ (BOOL)saveJSON:(id)JSON toPath:(NSString *)path;
+ (id)getJSONFromPath:(NSString *)path;
@end

View File

@@ -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 Trust_HTTPDNS_ROOT_DIR_NAME = @"HTTPDNS";
static NSString *const Trust_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.Trust.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

View File

@@ -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
/// 针对多账号场景的调度中心构造方<E980A0><E696B9>?
/// 注意若无需多账号隔离可继续使<E7BBAD><E4BDBF>?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

View File

@@ -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 ()
// v4v6<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?
@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.new.httpdns.scheduleFetchConfigAsyncQueue", DISPATCH_QUEUE_CONCURRENT);
_scheduleConfigLocalOperationQueue = dispatch_queue_create("com.new.httpdns.scheduleConfigLocalOperationQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
self->_currentActiveUpdateHostIndex = 0;
self->_currentActiveServiceHostIndex = 0;
});
_scheduleCenterResultPath = [[HttpdnsPersistenceUtils scheduleCenterResultDirectory]
stringByAppendingPathComponent:kScheduleRegionConfigLocalCacheFileName];
// <EFBFBD><EFBFBD>?
_lastScheduleCenterConnectDate = [NSDate dateWithTimeIntervalSinceNow:(- 24 * 60 * 60)];
}
return self;
}
- (void)initRegion:(NSString *)region {
if (![[HttpdnsRegionConfigLoader getAvailableRegionList] containsObject:region]) {
region = NEW_HTTPDNS_DEFAULT_REGION_KEY;
}
// region<EFBFBD><EFBFBD>?
// region<EFBFBD><EFBFBD>?
[self initServerListByRegion:region];
//
[self loadRegionConfigFromLocalCache];
}
- (void)resetRegion:(NSString *)region {
[self initServerListByRegion:region];
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
self.currentActiveServiceHostIndex = 0;
self.currentActiveUpdateHostIndex = 0;
});
// region<EFBFBD><EFBFBD>?
[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/NSStringNSNull<EFBFBD><EFBFBD>?
id ts = [scheduleCenterResult objectForKey:kLastUpdateUnixTimestampKey];
if ([ts respondsToSelector:@selector(doubleValue)]) {
NSDate *lastUpdateDate = [NSDate dateWithTimeIntervalSince1970:[ts doubleValue]];
self->_lastScheduleCenterConnectDate = lastUpdateDate;
}
[self updateRegionConfig:scheduleCenterResult];
});
}
// <EFBFBD><EFBFBD>?
- (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);
// <EFBFBD><EFBFBD>?
[self rotateUpdateServerHost];
// 3<EFBFBD><EFBFBD>?
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:kNewHttpdnsRegionConfigV4HostKey];
NSArray *v6Result = [scheduleCenterResult objectForKey:kNewHttpdnsRegionConfigV6HostKey];
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
HttpdnsRegionConfigLoader *regionConfigLoader = [HttpdnsRegionConfigLoader sharedInstance];
if ([HttpdnsUtil isNotEmptyArray:v4Result]) {
self->_ipv4ServiceServerHostList = [v4Result copy];
// serverserver
self->_ipv4UpdateServerHostList = [HttpdnsUtil joinArrays:v4Result
withArray:[regionConfigLoader getUpdateV4FallbackHostList:self->_currentRegion]];
}
if ([HttpdnsUtil isNotEmptyArray:v6Result]) {
self->_ipv6ServiceServerHostList = [v6Result copy];
// serverserver
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<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?0
[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 {
// <EFBFBD><EFBFBD>?
// httpdns<EFBFBD><EFBFBD>?
[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

View File

@@ -0,0 +1,18 @@
//
// HttpdnsScheduleExecutor.h
// TrustHttpDNS
//
// Created by ElonChan地风 on 2017/4/11.
// Copyright © 2017<31><37>?trustapp.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

View File

@@ -0,0 +1,131 @@
//
// HttpdnsScheduleExecutor.m
// TrustHttpDNS
//
// Created by ElonChan on 2017/4/11.
// Copyright © 2017<EFBFBD><EFBFBD>?trustapp.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.12regionregionregionregion=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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_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

View File

@@ -0,0 +1,28 @@
//
// HttpDnsLocker.h
// TrustHttpDNS
//
// Created by 王贇 on 2023/8/16.
// Copyright © 2023 trustapp.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 */

View File

@@ -0,0 +1,91 @@
//
// HttpDnsLocker.m
// TrustHttpDNS
//
// Created by on 2023/8/16.
// Copyright © 2023 trustapp.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

View File

@@ -0,0 +1,38 @@
//
// HttpdnsHostObjectInMemoryCache.h
// TrustHttpDNS
//
// Created by xuyecan on 2024/9/28.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpdnsHostObject.h"
NS_ASSUME_NONNULL_BEGIN
// 这个字典在HTTPDNS中只用于存储HttpdnsHostObject对象这个对象是整个框架的核心对象用于缓存和处理域名解析结<E69E90><E7BB93>?
// 通常从缓存中获得这个对象之后会根据不同场景改变一些字段的值而且很可能发生在不同线程<E7BABF><E7A88B>?
// 而不同线程从缓存中直接读取共享对象的话很有可能发生线程竞争的情况多线程访问某个对象的同一个字段在swift环境有较高概率发生crash
// 因此除了确保字典操作的线程安全拿出对象的时候也直接copy一个复制对象返<E8B1A1><E8BF94>?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

View File

@@ -0,0 +1,94 @@
//
// HttpdnsHostObjectInMemoryCache.m
// TrustHttpDNS
//
// Created by xuyecan on 2024/9/28.
// Copyright © 2024 trustapp.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

View File

@@ -0,0 +1,96 @@
//
// HttpdnsIPQualityDetector.h
// TrustHttpDNS
//
// Created by xuyecan on 2025/3/13.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* IP质量检测回<E6B58B><E59B9E>?
* @param cacheKey 缓存<E7BC93><E5AD98>?
* @param ip IP地址
* @param costTime 连接耗时毫秒<E7A792><EFBC89>?1表示连接失败
*/
typedef void(^HttpdnsIPQualityCallback)(NSString *cacheKey, NSString *ip, NSInteger costTime);
@interface HttpdnsIPQualityDetector : NSObject
/**
* 单例方法
*/
+ (instancetype)sharedInstance;
/**
* 获取当前等待队列中的任务数量
*/
- (NSUInteger)pendingTasksCount;
/**
* 调度一个IP连接质量检测任务不会阻塞当前线程
* @param cacheKey 缓存键通常是域<E698AF><E59F9F>?
* @param ip 要检测的IP地址
* @param port 连接端口如果为nil则默认使<E8AEA4><E4BDBF>?0
* @param callback 检测完成后的回<E79A84><E59B9E>?
*/
- (void)scheduleIPQualityDetection:(NSString *)cacheKey
ip:(NSString *)ip
port:(nullable NSNumber *)port
callback:(HttpdnsIPQualityCallback)callback;
#pragma mark - Methods exposed for testing
/**
* 执行IP连接质量检<E9878F><E6A380>?
* @param cacheKey 缓存键通常是域<E698AF><E59F9F>?
* @param ip 要检测的IP地址
* @param port 连接端口如果为nil则默认使<E8AEA4><E4BDBF>?0
* @param callback 检测完成后的回<E79A84><E59B9E>?
* @note 此方法主要用于测<E4BA8E><E6B58B>?
*/
- (void)executeDetection:(NSString *)cacheKey
ip:(NSString *)ip
port:(nullable NSNumber *)port
callback:(HttpdnsIPQualityCallback)callback;
/**
* 建立TCP连接并测量连接时<E68EA5><E697B6>?
* @param ip 要连接的IP地址
* @param port 连接端口
* @return 连接耗时毫秒<E7A792><EFBC89>?1表示连接失败
* @note 此方法主要用于测<E4BA8E><E6B58B>?
*/
- (NSInteger)tcpConnectToIP:(NSString *)ip port:(int)port;
/**
* 添加待处理任<E79086><E4BBBB>?
* @param cacheKey 缓存键通常是域<E698AF><E59F9F>?
* @param ip 要检测的IP地址
* @param port 连接端口
* @param callback 检测完成后的回<E79A84><E59B9E>?
* @note 此方法主要用于测<E4BA8E><E6B58B>?
*/
- (void)addPendingTask:(NSString *)cacheKey
ip:(NSString *)ip
port:(nullable NSNumber *)port
callback:(HttpdnsIPQualityCallback)callback;
/**
* 处理待处理任务队<E58AA1><E9989F>?
* @note 此方法主要用于测<E4BA8E><E6B58B>?
*/
- (void)processPendingTasksIfNeeded;
/**
* 处理所有待处理任务
* @note 此方法主要用于测<E4BA8E><E6B58B>?
*/
- (void)processPendingTasks;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,291 @@
//
// HttpdnsIPQualityDetector.m
// NewHttpDNS
//
// Created by xuyecan on 2025/3/13.
// Copyright © 2025 new-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.new.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
// 2IP
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

View 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

View 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;
}
}
// 使广<EFBFBD><EFBFBD>?G
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

View 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

View 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<EFBFBD><EFBFBD>?
sessionId<EFBFBD><EFBFBD>?2base62
*/
+ (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 {
// <EFBFBD><EFBFBD>?
if (plaintext == nil || [plaintext length] == 0 || key == nil || [key length] != kCCKeySizeAES128) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: @"Invalid input parameters"}];
}
return nil;
}
// CBC128bit(16)IV
NSMutableData *iv = [NSMutableData dataWithLength:kCCBlockSizeAES128];
int result = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, iv.mutableBytes);
if (result != 0) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_ENCRYPT_RANDOM_IV_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: @"Failed to generate random IV"}];
}
return nil;
}
// (<EFBFBD><EFBFBD>?
size_t bufferSize = [plaintext length] + kCCBlockSizeAES128;
size_t encryptedSize = 0;
// <EFBFBD><EFBFBD>?
NSMutableData *cipherData = [NSMutableData dataWithLength:bufferSize];
//
// AESPKCS5PaddingPKCS7Padding
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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Encryption failed with status: %d", cryptStatus]}];
}
return nil;
}
// <EFBFBD><EFBFBD>?
[cipherData setLength:encryptedSize];
// IV<EFBFBD><EFBFBD>?
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;
}
// <EFBFBD><EFBFBD>?
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 {
// <EFBFBD><EFBFBD>?
if (ciphertext == nil || [ciphertext length] <= kCCBlockSizeAES128 || key == nil || [key length] != kCCKeySizeAES128) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_ENCRYPT_INVALID_PARAMS_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: @"Invalid input parameters for decryption"}];
}
return nil;
}
// IV16<EFBFBD><EFBFBD>?
NSData *iv = [ciphertext subdataWithRange:NSMakeRange(0, kCCBlockSizeAES128)];
NSData *actualCiphertext = [ciphertext subdataWithRange:NSMakeRange(kCCBlockSizeAES128, ciphertext.length - kCCBlockSizeAES128)];
// <EFBFBD><EFBFBD>?
size_t bufferSize = actualCiphertext.length + kCCBlockSizeAES128;
size_t decryptedSize = 0;
// <EFBFBD><EFBFBD>?
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:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_ENCRYPT_FAILED_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Decryption failed with status: %d", cryptStatus]}];
}
return nil;
}
// <EFBFBD><EFBFBD>?
[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:TrustHttpDNS_IPTypeV4 ttl:hostObject.v4ttl];
if (customV4TTL > 0) {
hostObject.v4ttl = customV4TTL;
}
}
if ([self isNotEmptyArray:[hostObject getV6Ips]]) {
int64_t customV6TTL = [dnsService.ttlDelegate httpdnsHost:host ipType:TrustHttpDNS_IPTypeV6 ttl:hostObject.v6ttl];
if (customV6TTL > 0) {
hostObject.v6ttl = customV6TTL;
}
}
}
}
@end

View File

@@ -0,0 +1,17 @@
//
// AppDelegate.h
// TrustHttpDNSTestDemo
//
// Created by junmo on 2018/8/3.
// Copyright © 2018<31><38>?trustapp.com. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

View File

@@ -0,0 +1,56 @@
//
// AppDelegate.m
// TrustHttpDNSTestDemo
//
// Created by junmo on 2018/8/3.
// Copyright © 2018<EFBFBD><EFBFBD>?trustapp.com. All rights reserved.
//
#import "AppDelegate.h"
#import "DemoViewController.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
DemoViewController *vc = [DemoViewController new];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
self.window.rootViewController = nav;
[self.window makeKeyAndVisible];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@end

View File

@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="vfq-jO-Ao8"/>
<viewControllerLayoutGuide type="bottom" id="cLZ-g3-3sL"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,12 @@
<?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>accountID</key>
<integer>139450</integer>
<key>secretKey</key>
<string>807a19762f8eaefa8563489baf198535</string>
<key>aesSecretKey</key>
<string>82c0af0d0cb2d69c4f87bb25c2e23929</string>
</dict>
</plist>

View File

@@ -0,0 +1,21 @@
//
// DemoConfigLoader.h
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import <Foundation/Foundation.h>
@interface DemoConfigLoader : NSObject
@property (nonatomic, assign, readonly) NSInteger accountID;
@property (nonatomic, copy, readonly) NSString *secretKey;
@property (nonatomic, copy, readonly) NSString *aesSecretKey;
@property (nonatomic, assign, readonly) BOOL hasValidAccount;
+ (instancetype)shared;
@end

View File

@@ -0,0 +1,85 @@
//
// DemoConfigLoader.m
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import "DemoConfigLoader.h"
@implementation DemoConfigLoader {
NSInteger _accountID;
NSString *_secretKey;
NSString *_aesSecretKey;
BOOL _hasValidAccount;
}
+ (instancetype)shared {
static DemoConfigLoader *loader;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
loader = [[DemoConfigLoader alloc] init];
});
return loader;
}
- (instancetype)init {
if (self = [super init]) {
[self loadConfig];
}
return self;
}
// Bundle > <EFBFBD><EFBFBD>?accountID <EFBFBD><EFBFBD>?
- (void)loadConfig {
_accountID = 0;
_secretKey = @"";
_aesSecretKey = @"";
_hasValidAccount = NO;
NSDictionary *bundleDict = nil;
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"DemoConfig" ofType:@"plist"];
if (plistPath.length > 0) {
bundleDict = [NSDictionary dictionaryWithContentsOfFile:plistPath];
}
NSDictionary *env = [[NSProcessInfo processInfo] environment];
NSNumber *acc = bundleDict[@"accountID"]; // NSNumber preferred in plist
NSString *secret = bundleDict[@"secretKey"] ?: @"";
NSString *aes = bundleDict[@"aesSecretKey"] ?: @"";
NSString *envAcc = env[@"HTTPDNS_ACCOUNT_ID"];
NSString *envSecret = env[@"HTTPDNS_SECRET_KEY"];
NSString *envAes = env[@"HTTPDNS_AES_SECRET_KEY"];
if (envAcc.length > 0) {
acc = @([envAcc integerValue]);
}
if (envSecret.length > 0) {
secret = envSecret;
}
if (envAes.length > 0) {
aes = envAes;
}
if (acc != nil && [acc integerValue] > 0 && secret.length > 0) {
_accountID = [acc integerValue];
_secretKey = secret;
_aesSecretKey = aes ?: @"";
_hasValidAccount = YES;
} else {
_accountID = 0;
_secretKey = @"";
_aesSecretKey = @"";
_hasValidAccount = NO;
}
}
- (NSInteger)accountID { return _accountID; }
- (NSString *)secretKey { return _secretKey; }
- (NSString *)aesSecretKey { return _aesSecretKey; }
- (BOOL)hasValidAccount { return _hasValidAccount; }
@end

View File

@@ -0,0 +1,49 @@
//
// DemoHttpdnsScenario.h
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-23
//
#import <Foundation/Foundation.h>
#import "DemoResolveModel.h"
#import "HttpdnsService.h"
NS_ASSUME_NONNULL_BEGIN
@class DemoHttpdnsScenario;
@interface DemoHttpdnsScenarioConfig : NSObject <NSCopying>
@property (nonatomic, copy) NSString *host;
@property (nonatomic, assign) HttpdnsQueryIPType ipType;
@property (nonatomic, assign) BOOL httpsEnabled;
@property (nonatomic, assign) BOOL persistentCacheEnabled;
@property (nonatomic, assign) BOOL reuseExpiredIPEnabled;
- (instancetype)init;
@end
@protocol DemoHttpdnsScenarioDelegate <NSObject>
- (void)scenario:(DemoHttpdnsScenario *)scenario didUpdateModel:(DemoResolveModel *)model;
- (void)scenario:(DemoHttpdnsScenario *)scenario didAppendLogLine:(NSString *)line;
@end
@interface DemoHttpdnsScenario : NSObject
@property (nonatomic, weak, nullable) id<DemoHttpdnsScenarioDelegate> delegate;
@property (nonatomic, strong, readonly) DemoResolveModel *model;
- (instancetype)initWithDelegate:(id<DemoHttpdnsScenarioDelegate>)delegate;
- (void)applyConfig:(DemoHttpdnsScenarioConfig *)config;
- (void)resolveSyncNonBlocking;
- (void)resolveSync;
- (NSString *)logSnapshot;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,153 @@
//
// DemoHttpdnsScenario.m
// NewHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-23
//
#import "DemoHttpdnsScenario.h"
#import "DemoConfigLoader.h"
@interface DemoHttpdnsScenarioConfig ()
@end
@implementation DemoHttpdnsScenarioConfig
- (instancetype)init {
if (self = [super init]) {
_host = @"www.new.com";
_ipType = HttpdnsQueryIPTypeBoth;
_httpsEnabled = YES;
_persistentCacheEnabled = YES;
_reuseExpiredIPEnabled = YES;
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
DemoHttpdnsScenarioConfig *cfg = [[[self class] allocWithZone:zone] init];
cfg.host = self.host;
cfg.ipType = self.ipType;
cfg.httpsEnabled = self.httpsEnabled;
cfg.persistentCacheEnabled = self.persistentCacheEnabled;
cfg.reuseExpiredIPEnabled = self.reuseExpiredIPEnabled;
return cfg;
}
@end
@interface DemoHttpdnsScenario () <HttpdnsLoggerProtocol, HttpdnsTTLDelegate>
@property (nonatomic, strong) HttpDnsService *service;
@property (nonatomic, strong) DemoHttpdnsScenarioConfig *config;
@property (nonatomic, strong) NSMutableString *logBuffer;
@property (nonatomic, strong) dispatch_queue_t logQueue;
@end
@implementation DemoHttpdnsScenario
- (instancetype)initWithDelegate:(id<DemoHttpdnsScenarioDelegate>)delegate {
if (self = [super init]) {
_delegate = delegate;
_model = [[DemoResolveModel alloc] init];
_config = [[DemoHttpdnsScenarioConfig alloc] init];
_logBuffer = [NSMutableString string];
_logQueue = dispatch_queue_create("com.new.httpdns.demo.log", DISPATCH_QUEUE_SERIAL);
[self buildService];
[self applyConfig:_config];
}
return self;
}
- (void)buildService {
DemoConfigLoader *cfg = [DemoConfigLoader shared];
if (cfg.hasValidAccount) {
if (cfg.aesSecretKey.length > 0) {
self.service = [[HttpDnsService alloc] initWithAccountID:cfg.accountID secretKey:cfg.secretKey aesSecretKey:cfg.aesSecretKey];
} else {
self.service = [[HttpDnsService alloc] initWithAccountID:cfg.accountID secretKey:cfg.secretKey];
}
} else {
self.service = [HttpDnsService sharedInstance];
}
[self.service setLogEnabled:YES];
[self.service setNetworkingTimeoutInterval:8];
[self.service setDegradeToLocalDNSEnabled:YES];
self.service.ttlDelegate = self;
[self.service setLogHandler:self];
}
- (void)applyConfig:(DemoHttpdnsScenarioConfig *)config {
self.config = [config copy];
self.model.host = self.config.host;
self.model.ipType = self.config.ipType;
[self.service setHTTPSRequestEnabled:self.config.httpsEnabled];
[self.service setPersistentCacheIPEnabled:self.config.persistentCacheEnabled];
[self.service setReuseExpiredIPEnabled:self.config.reuseExpiredIPEnabled];
}
- (void)resolveSyncNonBlocking {
NSString *queryHost = [self currentHost];
HttpdnsQueryIPType ipType = self.config.ipType;
NSTimeInterval startMs = [[NSDate date] timeIntervalSince1970] * 1000.0;
HttpdnsResult *result = [self.service resolveHostSyncNonBlocking:queryHost byIpType:ipType];
[self handleResult:result host:queryHost ipType:ipType start:startMs];
}
- (void)resolveSync {
NSString *queryHost = [self currentHost];
HttpdnsQueryIPType ipType = self.config.ipType;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSTimeInterval startMs = [[NSDate date] timeIntervalSince1970] * 1000.0;
HttpdnsResult *result = [self.service resolveHostSync:queryHost byIpType:ipType];
[self handleResult:result host:queryHost ipType:ipType start:startMs];
});
}
- (NSString *)logSnapshot {
__block NSString *snapshot = @"";
dispatch_sync(self.logQueue, ^{
snapshot = [self.logBuffer copy];
});
return snapshot;
}
- (NSString *)currentHost {
return self.config.host.length > 0 ? self.config.host : @"www.new.com";
}
- (void)handleResult:(HttpdnsResult *)result host:(NSString *)host ipType:(HttpdnsQueryIPType)ipType start:(NSTimeInterval)startMs {
dispatch_async(dispatch_get_main_queue(), ^{
self.model.host = host;
self.model.ipType = ipType;
[self.model updateWithResult:result startTimeMs:startMs];
id<DemoHttpdnsScenarioDelegate> delegate = self.delegate;
if (delegate != nil) {
[delegate scenario:self didUpdateModel:self.model];
}
});
}
- (void)log:(NSString *)logStr {
if (logStr.length == 0) {
return;
}
NSString *line = [NSString stringWithFormat:@"%@ %@\n", [NSDate date], logStr];
// 使
dispatch_async(self.logQueue, ^{
[self.logBuffer appendString:line];
id<DemoHttpdnsScenarioDelegate> delegate = self.delegate;
if (delegate != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[delegate scenario:self didAppendLogLine:line];
});
}
});
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(NewHttpDNS_IPType)ipType ttl:(int64_t)ttl {
return ttl;
}
@end

View File

@@ -0,0 +1,15 @@
//
// DemoLogViewController.h
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import <UIKit/UIKit.h>
@interface DemoLogViewController : UIViewController
- (void)setInitialText:(NSString *)text;
- (void)appendLine:(NSString *)line;
@end

View File

@@ -0,0 +1,68 @@
//
// DemoLogViewController.m
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import "DemoLogViewController.h"
@interface DemoLogViewController ()
@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, copy) NSString *pendingInitialText;
@end
@implementation DemoLogViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"日志";
self.view.backgroundColor = [UIColor systemBackgroundColor];
self.textView = [[UITextView alloc] initWithFrame:CGRectZero];
self.textView.translatesAutoresizingMaskIntoConstraints = NO;
self.textView.editable = NO;
if (@available(iOS 13.0, *)) {
self.textView.font = [UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular];
} else {
self.textView.font = [UIFont systemFontOfSize:12];
}
[self.view addSubview:self.textView];
[NSLayoutConstraint activateConstraints:@[[self.textView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:8], [self.textView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:12], [self.textView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-12], [self.textView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-8]]];
if (self.pendingInitialText.length > 0) {
self.textView.text = self.pendingInitialText;
self.pendingInitialText = nil;
}
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"关闭" style:UIBarButtonItemStylePlain target:self action:@selector(onClose)];
self.navigationItem.leftBarButtonItem = close;
}
- (void)onClose {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)setInitialText:(NSString *)text {
if (self.isViewLoaded) {
self.textView.text = text ?: @"";
[self.textView scrollRangeToVisible:NSMakeRange(self.textView.text.length, 0)];
} else {
self.pendingInitialText = [text copy];
}
}
- (void)appendLine:(NSString *)line {
//
if (self.isViewLoaded) {
NSString *append = line ?: @"";
if (append.length > 0) {
self.textView.text = [self.textView.text stringByAppendingString:append];
[self.textView scrollRangeToVisible:NSMakeRange(self.textView.text.length, 0)];
}
}
}
@end

View File

@@ -0,0 +1,27 @@
//
// DemoResolveModel.h
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import <Foundation/Foundation.h>
#import "HttpdnsRequest.h"
#import "HttpdnsResult.h"
@interface DemoResolveModel : NSObject
@property (nonatomic, copy) NSString *host;
@property (nonatomic, assign) HttpdnsQueryIPType ipType;
@property (nonatomic, copy) NSArray<NSString *> *ipv4s;
@property (nonatomic, copy) NSArray<NSString *> *ipv6s;
@property (nonatomic, assign) NSTimeInterval elapsedMs;
@property (nonatomic, assign) NSTimeInterval ttlV4;
@property (nonatomic, assign) NSTimeInterval ttlV6;
- (void)updateWithResult:(HttpdnsResult *)result startTimeMs:(NSTimeInterval)startMs;
@end

View File

@@ -0,0 +1,42 @@
//
// DemoResolveModel.m
// NewHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import "DemoResolveModel.h"
@implementation DemoResolveModel
- (instancetype)init {
if (self = [super init]) {
_host = @"www.new.com";
_ipType = HttpdnsQueryIPTypeBoth;
_ipv4s = @[];
_ipv6s = @[];
_elapsedMs = 0;
_ttlV4 = 0;
_ttlV6 = 0;
}
return self;
}
- (void)updateWithResult:(HttpdnsResult *)result startTimeMs:(NSTimeInterval)startMs {
NSTimeInterval now = [[NSDate date] timeIntervalSince1970] * 1000.0;
_elapsedMs = MAX(0, now - startMs);
if (result != nil) {
_ipv4s = result.ips ?: @[];
_ipv6s = result.ipv6s ?: @[];
_ttlV4 = result.ttl;
_ttlV6 = result.v6ttl;
} else {
_ipv4s = @[];
_ipv6s = @[];
_ttlV4 = 0;
_ttlV6 = 0;
}
}
@end

View File

@@ -0,0 +1,13 @@
//
// DNSDemoViewController.h
// TrustHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import <UIKit/UIKit.h>
#import "DemoHttpdnsScenario.h"
@interface DemoViewController : UIViewController <DemoHttpdnsScenarioDelegate>
@end

View File

@@ -0,0 +1,321 @@
//
// DNSDemoViewController.m
// NewHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import "DemoViewController.h"
#import "DemoResolveModel.h"
#import "DemoLogViewController.h"
#import "DemoHttpdnsScenario.h"
@interface DemoViewController () <DemoHttpdnsScenarioDelegate>
@property (nonatomic, strong) DemoHttpdnsScenario *scenario;
@property (nonatomic, strong) DemoHttpdnsScenarioConfig *scenarioConfig;
@property (nonatomic, strong) DemoResolveModel *model;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIStackView *stack;
@property (nonatomic, strong) UITextField *hostField;
@property (nonatomic, strong) UISegmentedControl *ipTypeSeg;
@property (nonatomic, strong) UISwitch *swHTTPS;
@property (nonatomic, strong) UISwitch *swPersist;
@property (nonatomic, strong) UISwitch *swReuse;
@property (nonatomic, strong) UILabel *elapsedLabel;
@property (nonatomic, strong) UILabel *ttlLabel;
@property (nonatomic, strong) UITextView *resultTextView;
@property (nonatomic, weak) DemoLogViewController *presentedLogVC;
@end
@implementation DemoViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"HTTPDNS Demo";
self.view.backgroundColor = [UIColor systemBackgroundColor];
self.scenario = [[DemoHttpdnsScenario alloc] initWithDelegate:self];
self.model = self.scenario.model;
self.scenarioConfig = [[DemoHttpdnsScenarioConfig alloc] init];
[self buildUI];
[self reloadUIFromModel:self.model];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"日志" style:UIBarButtonItemStylePlain target:self action:@selector(onShowLog)];
}
- (void)buildUI {
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.scrollView];
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
self.stack = [[UIStackView alloc] init];
self.stack.axis = UILayoutConstraintAxisVertical;
self.stack.spacing = 12.0;
self.stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.stack];
[NSLayoutConstraint activateConstraints:@[
[self.stack.topAnchor constraintEqualToAnchor:self.scrollView.topAnchor constant:16],
[self.stack.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.stack.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.stack.bottomAnchor constraintEqualToAnchor:self.scrollView.bottomAnchor constant:-16],
[self.stack.widthAnchor constraintEqualToAnchor:self.view.widthAnchor constant:-32]
]];
UIStackView *row1 = [self labeledRow:@"Host"];
self.hostField = [[UITextField alloc] init];
self.hostField.placeholder = @"www.new.com";
self.hostField.text = self.scenarioConfig.host;
self.hostField.borderStyle = UITextBorderStyleRoundedRect;
[row1 addArrangedSubview:self.hostField];
[self.stack addArrangedSubview:row1];
UIStackView *row2 = [self labeledRow:@"IP Type"];
self.ipTypeSeg = [[UISegmentedControl alloc] initWithItems:@[@"IPv4", @"IPv6", @"Both"]];
self.ipTypeSeg.selectedSegmentIndex = [self segmentIndexForIpType:self.scenarioConfig.ipType];
[self.ipTypeSeg addTarget:self action:@selector(onIPTypeChanged:) forControlEvents:UIControlEventValueChanged];
[row2 addArrangedSubview:self.ipTypeSeg];
[self.stack addArrangedSubview:row2];
UIStackView *opts = [[UIStackView alloc] init];
opts.axis = UILayoutConstraintAxisHorizontal;
opts.alignment = UIStackViewAlignmentCenter;
opts.distribution = UIStackViewDistributionFillEqually;
opts.spacing = 8;
[self.stack addArrangedSubview:opts];
[opts addArrangedSubview:[self switchItem:@"HTTPS" action:@selector(onToggleHTTPS:) out:&_swHTTPS]];
[opts addArrangedSubview:[self switchItem:@"持久化" action:@selector(onTogglePersist:) out:&_swPersist]];
[opts addArrangedSubview:[self switchItem:@"复用过期" action:@selector(onToggleReuse:) out:&_swReuse]];
self.swHTTPS.on = self.scenarioConfig.httpsEnabled;
self.swPersist.on = self.scenarioConfig.persistentCacheEnabled;
self.swReuse.on = self.scenarioConfig.reuseExpiredIPEnabled;
[self applyOptionSwitches];
UIStackView *actions = [[UIStackView alloc] init];
actions.axis = UILayoutConstraintAxisHorizontal;
actions.spacing = 12;
actions.distribution = UIStackViewDistributionFillEqually;
[self.stack addArrangedSubview:actions];
UIButton *btnAsync = [self filledButton:@"Resolve (SyncNonBlocing)" action:@selector(onResolveAsync)];
UIButton *btnSync = [self borderButton:@"Resolve (Sync)" action:@selector(onResolveSync)];
[actions addArrangedSubview:btnAsync];
[actions addArrangedSubview:btnSync];
UIStackView *info = [self labeledRow:@"Info"];
self.elapsedLabel = [self monoLabel:@"elapsed: - ms"];
self.ttlLabel = [self monoLabel:@"ttl v4/v6: -/- s"];
[info addArrangedSubview:self.elapsedLabel];
[info addArrangedSubview:self.ttlLabel];
[self.stack addArrangedSubview:info];
if (@available(iOS 11.0, *)) { [self.stack setCustomSpacing:24 afterView:info]; }
UILabel *resultTitle = [[UILabel alloc] init];
resultTitle.text = @"结果";
resultTitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
[self.stack addArrangedSubview:resultTitle];
self.resultTextView = [[UITextView alloc] init];
self.resultTextView.translatesAutoresizingMaskIntoConstraints = NO;
self.resultTextView.editable = NO;
if (@available(iOS 13.0, *)) {
self.resultTextView.font = [UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular];
self.resultTextView.textColor = [UIColor labelColor];
} else {
self.resultTextView.font = [UIFont systemFontOfSize:12];
self.resultTextView.textColor = [UIColor blackColor];
}
self.resultTextView.backgroundColor = [UIColor clearColor];
self.resultTextView.textContainerInset = UIEdgeInsetsMake(8, 12, 8, 12);
[self.stack addArrangedSubview:self.resultTextView];
[self.resultTextView.heightAnchor constraintEqualToConstant:320].active = YES;
}
- (UIStackView *)labeledRow:(NSString *)title {
UIStackView *row = [[UIStackView alloc] init];
row.axis = UILayoutConstraintAxisHorizontal;
row.alignment = UIStackViewAlignmentCenter;
row.spacing = 8.0;
UILabel *label = [[UILabel alloc] init];
label.text = title;
[label.widthAnchor constraintGreaterThanOrEqualToConstant:64].active = YES;
[row addArrangedSubview:label];
return row;
}
- (UILabel *)monoLabel:(NSString *)text {
UILabel *l = [[UILabel alloc] init];
l.text = text;
if (@available(iOS 13.0, *)) {
l.font = [UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular];
} else {
l.font = [UIFont systemFontOfSize:12];
}
return l;
}
- (UIButton *)filledButton:(NSString *)title action:(SEL)sel {
UIButton *b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:title forState:UIControlStateNormal];
b.backgroundColor = [UIColor systemBlueColor];
[b setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
b.layer.cornerRadius = 8;
[b.heightAnchor constraintEqualToConstant:44].active = YES;
[b addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
return b;
}
- (UIButton *)borderButton:(NSString *)title action:(SEL)sel {
UIButton *b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:title forState:UIControlStateNormal];
b.layer.borderWidth = 1;
b.layer.borderColor = [UIColor systemBlueColor].CGColor;
b.layer.cornerRadius = 8;
[b.heightAnchor constraintEqualToConstant:44].active = YES;
[b addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
return b;
}
- (UIView *)switchItem:(NSString *)title action:(SEL)sel out:(UISwitch * __strong *)outSwitch {
UIStackView *box = [[UIStackView alloc] init];
box.axis = UILayoutConstraintAxisVertical;
box.alignment = UIStackViewAlignmentCenter;
UILabel *l = [[UILabel alloc] init];
l.text = title;
UISwitch *s = [[UISwitch alloc] init];
[s addTarget:self action:sel forControlEvents:UIControlEventValueChanged];
[box addArrangedSubview:s];
[box addArrangedSubview:l];
if (outSwitch != NULL) {
*outSwitch = s;
}
return box;
}
- (NSInteger)segmentIndexForIpType:(HttpdnsQueryIPType)ipType {
switch (ipType) {
case HttpdnsQueryIPTypeIpv4: { return 0; }
case HttpdnsQueryIPTypeIpv6: { return 1; }
default: { return 2; }
}
}
#pragma mark - Actions
- (void)onIPTypeChanged:(UISegmentedControl *)seg {
HttpdnsQueryIPType type = HttpdnsQueryIPTypeBoth;
switch (seg.selectedSegmentIndex) {
case 0: type = HttpdnsQueryIPTypeIpv4; break;
case 1: type = HttpdnsQueryIPTypeIpv6; break;
default: type = HttpdnsQueryIPTypeBoth; break;
}
self.model.ipType = type;
self.scenarioConfig.ipType = type;
[self.scenario applyConfig:self.scenarioConfig];
}
- (void)applyOptionSwitches {
self.scenarioConfig.httpsEnabled = self.swHTTPS.isOn;
self.scenarioConfig.persistentCacheEnabled = self.swPersist.isOn;
self.scenarioConfig.reuseExpiredIPEnabled = self.swReuse.isOn;
[self.scenario applyConfig:self.scenarioConfig];
}
- (void)onToggleHTTPS:(UISwitch *)s { [self applyOptionSwitches]; }
- (void)onTogglePersist:(UISwitch *)s { [self applyOptionSwitches]; }
- (void)onToggleReuse:(UISwitch *)s { [self applyOptionSwitches]; }
- (void)onResolveAsync {
[self.view endEditing:YES];
NSString *host = self.hostField.text.length > 0 ? self.hostField.text : @"www.new.com";
self.model.host = host;
self.scenarioConfig.host = host;
[self.scenario applyConfig:self.scenarioConfig];
[self.scenario resolveSyncNonBlocking];
}
- (void)onResolveSync {
[self.view endEditing:YES];
NSString *host = self.hostField.text.length > 0 ? self.hostField.text : @"www.new.com";
self.model.host = host;
self.scenarioConfig.host = host;
[self.scenario applyConfig:self.scenarioConfig];
[self.scenario resolveSync];
}
- (void)onShowLog {
DemoLogViewController *logVC = [DemoLogViewController new];
[logVC setInitialText:[self.scenario logSnapshot]];
self.presentedLogVC = logVC;
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:logVC];
nav.modalPresentationStyle = UIModalPresentationAutomatic;
[self presentViewController:nav animated:YES completion:nil];
}
- (void)reloadUIFromModel:(DemoResolveModel *)model {
self.model = model;
if (![self.hostField isFirstResponder]) {
self.hostField.text = model.host;
}
NSInteger segIndex = [self segmentIndexForIpType:model.ipType];
if (self.ipTypeSeg.selectedSegmentIndex != segIndex) {
self.ipTypeSeg.selectedSegmentIndex = segIndex;
}
self.elapsedLabel.text = [NSString stringWithFormat:@"elapsed: %.0f ms", model.elapsedMs];
self.ttlLabel.text = [NSString stringWithFormat:@"ttl v4/v6: %.0f/%.0f s", model.ttlV4, model.ttlV6];
self.resultTextView.text = [self buildJSONText:model];
}
- (NSString *)buildJSONText:(DemoResolveModel *)model {
NSString *ipTypeStr = @"both";
switch (model.ipType) {
case HttpdnsQueryIPTypeIpv4: { ipTypeStr = @"ipv4"; break; }
case HttpdnsQueryIPTypeIpv6: { ipTypeStr = @"ipv6"; break; }
default: { ipTypeStr = @"both"; break; }
}
NSDictionary *dict = @{
@"host": model.host ?: @"",
@"ipType": ipTypeStr,
@"elapsedMs": @(model.elapsedMs),
@"ttl": @{ @"v4": @(model.ttlV4), @"v6": @(model.ttlV6) },
@"ipv4": model.ipv4s ?: @[],
@"ipv6": model.ipv6s ?: @[]
};
NSError *err = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&err];
if (data == nil || err != nil) {
return [NSString stringWithFormat:@"{\n \"error\": \"%@\"\n}", err.localizedDescription ?: @"json serialize failed"];
}
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
#pragma mark - DemoHttpdnsScenarioDelegate
- (void)scenario:(DemoHttpdnsScenario *)scenario didUpdateModel:(DemoResolveModel *)model {
[self reloadUIFromModel:model];
}
- (void)scenario:(DemoHttpdnsScenario *)scenario didAppendLogLine:(NSString *)line {
if (self.presentedLogVC != nil) {
[self.presentedLogVC appendLine:line];
}
}
@end

View File

@@ -0,0 +1,48 @@
<?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>$(DEVELOPMENT_LANGUAGE)</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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2</string>
<key>CFBundleVersion</key>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
//
// main.m
// TrustHttpDNSTestDemo
//
// Created by junmo on 2018/8/3.
// Copyright © 2018<EFBFBD><EFBFBD>?trustapp.com. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@@ -0,0 +1,477 @@
//
// DBTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2025/3/15.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsDB.h"
#import "HttpdnsHostRecord.h"
@interface DBTest : TestBase
@property (nonatomic, strong) HttpdnsDB *db;
@property (nonatomic, assign) NSInteger testAccountId;
@end
@implementation DBTest
- (void)setUp {
[super setUp];
// Use a test-specific account ID to avoid conflicts with real data
self.testAccountId = 999999;
self.db = [[HttpdnsDB alloc] initWithAccountId:self.testAccountId];
// Clean up any existing data
[self.db deleteAll];
}
- (void)tearDown {
// Clean up after tests
[self.db deleteAll];
self.db = nil;
[super tearDown];
}
#pragma mark - Initialization Tests
- (void)testInitialization {
XCTAssertNotNil(self.db, @"Database should be initialized successfully");
// Test with invalid account ID (negative value)
HttpdnsDB *invalidDB = [[HttpdnsDB alloc] initWithAccountId:-1];
XCTAssertNotNil(invalidDB, @"Database should still initialize with negative account ID");
}
#pragma mark - Create Tests
- (void)testCreateRecord {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"test.example.com" cacheKey:@"test_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Verify the record was created by querying it
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"test_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"test_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"test.example.com", @"Hostname should match");
XCTAssertEqualObjects(fetchedRecord.clientIp, @"192.168.1.1", @"Client IP should match");
XCTAssertEqual(fetchedRecord.v4ips.count, 2, @"Should have 2 IPv4 addresses");
XCTAssertEqual(fetchedRecord.v6ips.count, 1, @"Should have 1 IPv6 address");
}
- (void)testCreateMultipleRecords {
// Create multiple test records
NSArray<NSString *> *hostnames = @[@"host1.example.com", @"host2.example.com", @"host3.example.com"];
NSArray<NSString *> *cacheKeys = @[@"cache_key_1", @"cache_key_2", @"cache_key_3"];
for (NSInteger i = 0; i < hostnames.count; i++) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostnames[i] cacheKey:cacheKeys[i]];
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed for %@", hostnames[i]);
}
// Verify all records were created
for (NSInteger i = 0; i < cacheKeys.count; i++) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKeys[i]];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record for %@", cacheKeys[i]);
XCTAssertEqualObjects(fetchedRecord.cacheKey, cacheKeys[i], @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, hostnames[i], @"Hostname should match");
}
}
- (void)testCreateRecordWithNilCacheKey {
// Create a record with nil cacheKey
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"test.example.com" cacheKey:nil];
// Attempt to insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertFalse(result, @"Record creation should fail with nil cacheKey");
}
- (void)testCreateRecordWithEmptyValues {
// Create a record with empty arrays and nil values
HttpdnsHostRecord *record = [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:@"empty_cache_key"
hostName:@"empty.example.com"
createAt:nil
modifyAt:nil
clientIp:nil
v4ips:@[]
v4ttl:0
v4LookupTime:0
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed with empty values");
// Verify the record was created
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"empty_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"empty_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"empty.example.com", @"Hostname should match");
XCTAssertNil(fetchedRecord.clientIp, @"Client IP should be nil");
XCTAssertEqual(fetchedRecord.v4ips.count, 0, @"Should have 0 IPv4 addresses");
XCTAssertEqual(fetchedRecord.v6ips.count, 0, @"Should have 0 IPv6 addresses");
// Verify createAt and modifyAt were automatically set
XCTAssertNotNil(fetchedRecord.createAt, @"createAt should be automatically set");
XCTAssertNotNil(fetchedRecord.modifyAt, @"modifyAt should be automatically set");
}
- (void)testCreateMultipleRecordsWithSameHostname {
// Create multiple records with the same hostname but different cache keys
NSString *hostname = @"same.example.com";
NSArray<NSString *> *cacheKeys = @[@"same_cache_key_1", @"same_cache_key_2", @"same_cache_key_3"];
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed for %@", cacheKey);
}
// Verify all records were created
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record for %@", cacheKey);
XCTAssertEqualObjects(fetchedRecord.cacheKey, cacheKey, @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, hostname, @"Hostname should match");
}
}
#pragma mark - Update Tests
- (void)testUpdateRecord {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"update.example.com" cacheKey:@"update_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Fetch the record to get its createAt timestamp
HttpdnsHostRecord *originalRecord = [self.db selectByCacheKey:@"update_cache_key"];
NSDate *originalCreateAt = originalRecord.createAt;
NSDate *originalModifyAt = originalRecord.modifyAt;
// Wait a moment to ensure timestamps will be different
[NSThread sleepForTimeInterval:0.1];
// Update the record with new values
HttpdnsHostRecord *updatedRecord = [[HttpdnsHostRecord alloc] initWithId:originalRecord.id
cacheKey:@"update_cache_key"
hostName:@"updated.example.com" // Changed hostname
createAt:[NSDate date] // Try to change createAt
modifyAt:[NSDate date]
clientIp:@"10.0.0.1" // Changed
v4ips:@[@"10.0.0.2", @"10.0.0.3"] // Changed
v4ttl:600 // Changed
v4LookupTime:originalRecord.v4LookupTime + 1000
v6ips:@[@"2001:db8::1", @"2001:db8::2"] // Changed
v6ttl:1200 // Changed
v6LookupTime:originalRecord.v6LookupTime + 1000
extra:@"updated"]; // Changed
// Update the record
result = [self.db createOrUpdate:updatedRecord];
XCTAssertTrue(result, @"Record update should succeed");
// Fetch the updated record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"update_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the updated record");
// Verify the updated values
XCTAssertEqualObjects(fetchedRecord.hostName, @"updated.example.com", @"Hostname should be updated");
XCTAssertEqualObjects(fetchedRecord.clientIp, @"10.0.0.1", @"Client IP should be updated");
XCTAssertEqual(fetchedRecord.v4ips.count, 2, @"Should have 2 IPv4 addresses");
XCTAssertEqualObjects(fetchedRecord.v4ips[0], @"10.0.0.2", @"IPv4 address should be updated");
XCTAssertEqual(fetchedRecord.v4ttl, 600, @"v4ttl should be updated");
XCTAssertEqual(fetchedRecord.v6ips.count, 2, @"Should have 2 IPv6 addresses");
XCTAssertEqual(fetchedRecord.v6ttl, 1200, @"v6ttl should be updated");
XCTAssertEqualObjects(fetchedRecord.extra, @"updated", @"Extra data should be updated");
// Verify createAt was preserved and modifyAt was updated
XCTAssertEqualWithAccuracy([fetchedRecord.createAt timeIntervalSince1970],
[originalCreateAt timeIntervalSince1970],
0.001,
@"createAt should not change on update");
XCTAssertTrue([fetchedRecord.modifyAt timeIntervalSinceDate:originalModifyAt] > 0,
@"modifyAt should be updated to a later time");
}
- (void)testUpdateNonExistentRecord {
// Create a record that doesn't exist in the database
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"nonexistent.example.com" cacheKey:@"nonexistent_cache_key"];
// Update (which should actually create) the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"createOrUpdate should succeed for non-existent record");
// Verify the record was created
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"nonexistent_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
}
#pragma mark - Query Tests
- (void)testSelectByCacheKey {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"query.example.com" cacheKey:@"query_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Query the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"query_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"query_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"query.example.com", @"Hostname should match");
}
- (void)testSelectNonExistentRecord {
// Query a record that doesn't exist
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"nonexistent_cache_key"];
XCTAssertNil(fetchedRecord, @"Should return nil for non-existent record");
}
- (void)testSelectWithNilCacheKey {
// Query with nil cacheKey
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:nil];
XCTAssertNil(fetchedRecord, @"Should return nil for nil cacheKey");
}
#pragma mark - Delete Tests
- (void)testDeleteByCacheKey {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"delete.example.com" cacheKey:@"delete_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Verify the record exists
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"delete_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
// Delete the record
result = [self.db deleteByCacheKey:@"delete_cache_key"];
XCTAssertTrue(result, @"Record deletion should succeed");
// Verify the record was deleted
fetchedRecord = [self.db selectByCacheKey:@"delete_cache_key"];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
- (void)testDeleteByHostname {
// Create multiple records with the same hostname but different cache keys
NSString *hostname = @"delete_multiple.example.com";
NSArray<NSString *> *cacheKeys = @[@"delete_cache_key_1", @"delete_cache_key_2", @"delete_cache_key_3"];
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
[self.db createOrUpdate:record];
}
// Verify records exist
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
}
// Delete all records with the same hostname
BOOL result = [self.db deleteByHostNameArr:@[hostname]];
XCTAssertTrue(result, @"Deleting records by hostname should succeed");
// Verify all records were deleted
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
}
- (void)testDeleteNonExistentRecord {
// Delete a record that doesn't exist
BOOL result = [self.db deleteByCacheKey:@"nonexistent_cache_key"];
XCTAssertTrue(result, @"Deleting non-existent record should still return success");
}
- (void)testDeleteWithNilCacheKey {
// Delete with nil cacheKey
BOOL result = [self.db deleteByCacheKey:nil];
XCTAssertFalse(result, @"Deleting with nil cacheKey should fail");
}
- (void)testDeleteAll {
// Create multiple test records
NSArray<NSString *> *hostnames = @[@"host1.example.com", @"host2.example.com", @"host3.example.com"];
NSArray<NSString *> *cacheKeys = @[@"cache_key_1", @"cache_key_2", @"cache_key_3"];
for (NSInteger i = 0; i < hostnames.count; i++) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostnames[i] cacheKey:cacheKeys[i]];
[self.db createOrUpdate:record];
}
// Verify records exist
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
}
// Delete all records
BOOL result = [self.db deleteAll];
XCTAssertTrue(result, @"Deleting all records should succeed");
// Verify all records were deleted
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
}
#pragma mark - Timestamp Tests
- (void)testCreateAtPreservation {
// Create a test record with a specific createAt time
NSDate *pastDate = [NSDate dateWithTimeIntervalSinceNow:-3600]; // 1 hour ago
HttpdnsHostRecord *record = [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:@"timestamp_cache_key"
hostName:@"timestamp.example.com"
createAt:pastDate
modifyAt:[NSDate date]
clientIp:@"192.168.1.1"
v4ips:@[@"192.168.1.2"]
v4ttl:300
v4LookupTime:1000
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Fetch the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"timestamp_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record");
// Verify createAt was preserved
// 使
XCTAssertEqualWithAccuracy([fetchedRecord.createAt timeIntervalSince1970],
[[NSDate date] timeIntervalSince1970],
0.001,
@"createAt should be preserved");
// Update the record
HttpdnsHostRecord *updatedRecord = [[HttpdnsHostRecord alloc] initWithId:fetchedRecord.id
cacheKey:@"timestamp_cache_key"
hostName:@"timestamp.example.com"
createAt:[NSDate date] // Try to change createAt
modifyAt:[NSDate date]
clientIp:@"10.0.0.1"
v4ips:@[@"10.0.0.2"]
v4ttl:600
v4LookupTime:2000
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Update the record
result = [self.db createOrUpdate:updatedRecord];
XCTAssertTrue(result, @"Record update should succeed");
// Fetch the updated record
HttpdnsHostRecord *updatedFetchedRecord = [self.db selectByCacheKey:@"timestamp_cache_key"];
XCTAssertNotNil(updatedFetchedRecord, @"Should be able to fetch the updated record");
// Verify createAt was still preserved after update
XCTAssertEqualWithAccuracy([updatedFetchedRecord.createAt timeIntervalSince1970],
[fetchedRecord.createAt timeIntervalSince1970],
0.001,
@"createAt should still be preserved after update");
// Verify modifyAt was updated
XCTAssertTrue([updatedFetchedRecord.modifyAt timeIntervalSinceDate:fetchedRecord.modifyAt] > 0,
@"modifyAt should be updated to a later time");
}
#pragma mark - Concurrency Tests
- (void)testConcurrentAccess {
// Create a dispatch group for synchronization
dispatch_group_t group = dispatch_group_create();
// Create a concurrent queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
// Number of concurrent operations
NSInteger operationCount = 10;
// Perform concurrent operations
for (NSInteger i = 0; i < operationCount; i++) {
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
NSString *hostname = [NSString stringWithFormat:@"concurrent%ld.example.com", (long)i];
NSString *cacheKey = [NSString stringWithFormat:@"concurrent_cache_key_%ld", (long)i];
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed in concurrent operation");
// Query the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record in concurrent operation");
dispatch_group_leave(group);
});
}
// Wait for all operations to complete with a timeout
XCTAssertEqual(dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), 0,
@"All concurrent operations should complete within timeout");
// Verify all records were created
for (NSInteger i = 0; i < operationCount; i++) {
NSString *cacheKey = [NSString stringWithFormat:@"concurrent_cache_key_%ld", (long)i];
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch all records after concurrent operations");
}
}
#pragma mark - Helper Methods
- (HttpdnsHostRecord *)createTestRecordWithHostname:(NSString *)hostname cacheKey:(NSString *)cacheKey {
return [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:cacheKey
hostName:hostname
createAt:[NSDate date]
modifyAt:[NSDate date]
clientIp:@"192.168.1.1"
v4ips:@[@"192.168.1.2", @"192.168.1.3"]
v4ttl:300
v4LookupTime:1000
v6ips:@[@"2001:db8::1"]
v6ttl:600
v6LookupTime:2000
extra:@"value"];
}
@end

View File

@@ -0,0 +1,87 @@
//
// CacheKeyFunctionTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/6/12.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface CacheKeyFunctionTest : TestBase
@end
static NSString *sdnsHost = @"sdns1.onlyforhttpdnstest.run.place";
@implementation CacheKeyFunctionTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setPersistentCacheIPEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)testSimpleSpecifyingCacheKeySituation {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
NSString *cacheKey = [NSString stringWithFormat:@"cacheKey-%@", testHost];
__block NSString *ipPrefix = hostNameIpPrefixMap[testHost];
// 使ttl
[self.httpdns setTtlDelegate:nil];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:nil sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:testHost]);
XCTAssertGreaterThan(result.ttl, 0);
// ipip
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [testHost UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[NSThread sleepForTimeInterval:3];
//
[self.httpdns.requestManager cleanAllHostMemoryCache];
// db<EFBFBD><EFBFBD>?
[self.httpdns.requestManager syncLoadCacheFromDbToMemory];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:testHost byIpType:HttpdnsQueryIPTypeIpv4];
// 使cacheKeynil
XCTAssertNil(result);
result = [self.httpdns resolveHostSyncNonBlocking:testHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:nil sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:testHost]);
NSString *firstIp = [result firstIpv4Address];
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
@end

View File

@@ -0,0 +1,108 @@
//
// CustomTTLAndCleanCacheTest.m
// TrustHttpDNS
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
static int TEST_CUSTOM_TTL_SECOND = 3;
@interface CustomTTLAndCleanCacheTest : TestBase<HttpdnsTTLDelegate>
@end
@implementation CustomTTLAndCleanCacheTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// ttl<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
if ([host isEqual:testHost]) {
return TEST_CUSTOM_TTL_SECOND;
}
return ttl;
}
- (void)testCustomTTL {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
NSString *expectedIpPrefix = hostNameIpPrefixMap[testHost];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andForwardToRealObject();
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[NSThread sleepForTimeInterval:TEST_CUSTOM_TTL_SECOND + 1];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,101 @@
//
// EnableReuseExpiredIpTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/5/28.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface EnableReuseExpiredIpTest : TestBase <HttpdnsTTLDelegate>
@end
static int ttlForTest = 3;
@implementation EnableReuseExpiredIpTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:YES];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// <EFBFBD><EFBFBD>?
return ttlForTest;
}
- (void)testReuseExpiredIp {
NSString *host = hostNameIpPrefixMap.allKeys.firstObject;
NSString *ipPrefix = hostNameIpPrefixMap[host];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
[self.httpdns.requestManager cleanAllHostMemoryCache];
//
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertGreaterThan(result.ttl, 0);
XCTAssertLessThanOrEqual(result.ttl, ttlForTest);
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
//
[NSThread sleepForTimeInterval:ttlForTest + 1];
//
HttpdnsResult *result2 = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result2);
XCTAssertTrue([result2.host isEqualToString:host]);
XCTAssertGreaterThan(result2.ttl, 0);
XCTAssertLessThanOrEqual(result2.ttl, ttlForTest);
// <EFBFBD><EFBFBD>?
XCTAssertGreaterThan([[NSDate date] timeIntervalSince1970], result2.lastUpdatedTimeInterval + result2.ttl);
NSString *firstIp2 = [result2 firstIpv4Address];
XCTAssertTrue([firstIp2 hasPrefix:ipPrefix]);
//
[NSThread sleepForTimeInterval:1];
// 使nonblocking<EFBFBD><EFBFBD>?
HttpdnsResult *result3 = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result3);
XCTAssertTrue([result3.host isEqualToString:host]);
XCTAssertGreaterThan(result3.ttl, 0);
XCTAssertLessThanOrEqual(result3.ttl, ttlForTest);
// <EFBFBD><EFBFBD>?
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result3.lastUpdatedTimeInterval + result3.ttl);
NSString *firstIp3 = [result3 firstIpv4Address];
XCTAssertTrue([firstIp3 hasPrefix:ipPrefix]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,165 @@
//
// HttpdnsHostObjectTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2025/3/14.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsIpObject.h"
#import <OCMock/OCMock.h>
@interface HttpdnsHostObjectTest : TestBase
@end
@implementation HttpdnsHostObjectTest
- (void)setUp {
[super setUp];
}
- (void)tearDown {
[super tearDown];
}
#pragma mark - <EFBFBD><EFBFBD>?
- (void)testHostObjectProperties {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
// <EFBFBD><EFBFBD>?
hostObject.host = @"example.com";
hostObject.ttl = 60;
hostObject.queryTimes = 1;
hostObject.clientIP = @"192.168.1.1";
// <EFBFBD><EFBFBD>?
XCTAssertEqualObjects(hostObject.host, @"example.com", @"host属性应该被正确设置");
XCTAssertEqual(hostObject.ttl, 60, @"ttl属性应该被正确设置");
XCTAssertEqual(hostObject.queryTimes, 1, @"queryTimes属性应该被正确设置");
XCTAssertEqualObjects(hostObject.clientIP, @"192.168.1.1", @"clientIP属性应该被正确设置");
}
#pragma mark - IP
- (void)testIpObjectProperties {
// HttpdnsIpObject
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
// <EFBFBD><EFBFBD>?
ipObject.ip = @"1.2.3.4";
ipObject.ttl = 300;
ipObject.priority = 10;
ipObject.detectRT = 50; // detectRT<EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?
XCTAssertEqualObjects(ipObject.ip, @"1.2.3.4", @"ip属性应该被正确设置");
XCTAssertEqual(ipObject.ttl, 300, @"ttl属性应该被正确设置");
XCTAssertEqual(ipObject.priority, 10, @"priority属性应该被正确设置");
XCTAssertEqual(ipObject.detectRT, 50, @"detectRT属性应该被正确设置");
}
- (void)testIpObjectDetectRTMethods {
// HttpdnsIpObject
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
// <EFBFBD><EFBFBD>?
XCTAssertEqual(ipObject.detectRT, -1, @"detectRT的默认值应该是-1");
// <EFBFBD><EFBFBD>?
[ipObject setDetectRT:100];
XCTAssertEqual(ipObject.detectRT, 100, @"detectRT应该被正确设置为100");
// <EFBFBD><EFBFBD>?
[ipObject setDetectRT:-5];
XCTAssertEqual(ipObject.detectRT, -1, @"设置负值时detectRT应该被设置为-1");
// <EFBFBD><EFBFBD>?
[ipObject setDetectRT:0];
XCTAssertEqual(ipObject.detectRT, 0, @"detectRT应该被正确设置为0");
}
#pragma mark - IP
- (void)testHostObjectIpManagement {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.host = @"example.com";
// IP
HttpdnsIpObject *ipv4Object = [[HttpdnsIpObject alloc] init];
ipv4Object.ip = @"1.2.3.4";
ipv4Object.ttl = 300;
ipv4Object.detectRT = 50;
HttpdnsIpObject *ipv6Object = [[HttpdnsIpObject alloc] init];
ipv6Object.ip = @"2001:db8::1";
ipv6Object.ttl = 600;
ipv6Object.detectRT = 80;
// IP<EFBFBD><EFBFBD>?
[hostObject addIpv4:ipv4Object];
[hostObject addIpv6:ipv6Object];
// IP<EFBFBD><EFBFBD>?
XCTAssertEqual(hostObject.ipv4List.count, 1, @"应该<E5BA94><E8AFA5>?个IPv4对象");
XCTAssertEqual(hostObject.ipv6List.count, 1, @"应该<E5BA94><E8AFA5>?个IPv6对象");
// IP<EFBFBD><EFBFBD>?
HttpdnsIpObject *retrievedIpv4 = hostObject.ipv4List.firstObject;
XCTAssertEqualObjects(retrievedIpv4.ip, @"1.2.3.4", @"IPv4地址应该正确");
XCTAssertEqual(retrievedIpv4.detectRT, 50, @"IPv4的detectRT应该正确");
HttpdnsIpObject *retrievedIpv6 = hostObject.ipv6List.firstObject;
XCTAssertEqualObjects(retrievedIpv6.ip, @"2001:db8::1", @"IPv6地址应该正确");
XCTAssertEqual(retrievedIpv6.detectRT, 80, @"IPv6的detectRT应该正确");
}
#pragma mark - IP
- (void)testIpSortingByDetectRT {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.host = @"example.com";
// IP<EFBFBD><EFBFBD>?
HttpdnsIpObject *ip1 = [[HttpdnsIpObject alloc] init];
ip1.ip = @"1.1.1.1";
ip1.detectRT = 100;
HttpdnsIpObject *ip2 = [[HttpdnsIpObject alloc] init];
ip2.ip = @"2.2.2.2";
ip2.detectRT = 50;
HttpdnsIpObject *ip3 = [[HttpdnsIpObject alloc] init];
ip3.ip = @"3.3.3.3";
ip3.detectRT = 200;
HttpdnsIpObject *ip4 = [[HttpdnsIpObject alloc] init];
ip4.ip = @"4.4.4.4";
ip4.detectRT = -1; // <EFBFBD><EFBFBD>?
// IP
[hostObject addIpv4:ip1];
[hostObject addIpv4:ip2];
[hostObject addIpv4:ip3];
[hostObject addIpv4:ip4];
// IP
NSArray<HttpdnsIpObject *> *sortedIps = [hostObject sortedIpv4List];
//
// ip2(50ms) -> ip1(100ms) -> ip3(200ms) -> ip4(-1ms)
XCTAssertEqual(sortedIps.count, 4, @"应该<E5BA94><E8AFA5>?个IP对象");
XCTAssertEqualObjects(sortedIps[0].ip, @"2.2.2.2", @"IP<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(sortedIps[1].ip, @"1.1.1.1", @"IP<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(sortedIps[2].ip, @"3.3.3.3", @"IP<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(sortedIps[3].ip, @"4.4.4.4", @"IP<EFBFBD><EFBFBD>?);
}
@end

View File

@@ -0,0 +1,156 @@
//
// ManuallyCleanCacheTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
static int TEST_CUSTOM_TTL_SECOND = 3;
@interface ManuallyCleanCacheTest : TestBase
@end
@implementation ManuallyCleanCacheTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setIPv6Enabled:YES];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testCleanSingleHost {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
NSString *testHost = ipv4OnlyHost;
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[hostObject setV4TTL:60];
__block NSArray *mockResolverHostObjects = @[hostObject];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andReturn(mockResolverHostObjects);
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[@"invalidhostofcourse"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[testHost]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testCleanAllHost {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
NSString *testHost = ipv4OnlyHost;
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[hostObject setV4TTL:60];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
__block NSArray *mockResolverHostObjects = @[hostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andReturn(mockResolverHostObjects);
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[@"invalidhostofcourse"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanAllHostCache];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,363 @@
//
// MultithreadCorrectnessTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/5/26.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "HttpdnsRequest_Internal.h"
#import "TestBase.h"
@interface MultithreadCorrectnessTest : TestBase
@end
@implementation MultithreadCorrectnessTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setLogHandler:self];
[self.httpdns setTimeoutInterval:2];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
// 线<EFBFBD><EFBFBD>?
- (void)testNoneBlockingMethodShouldNotBlock {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
[self.httpdns resolveHostSyncNonBlocking:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 1, @"elapsedTime should be less than 1s, but is %f", elapsedTime);
}
// 线线
- (void)testBlockingMethodShouldNotBlockIfInMainThread {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 1, @"elapsedTime should be less than 1s, but is %f", elapsedTime);
}
// 线
- (void)testBlockingMethodShouldBlockIfInBackgroundThread {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:2];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 2, @"elapsedTime should be more than 2s, but is %f", elapsedTime);
}
// 线
- (void)testBlockingMethodShouldBlockIfInBackgroundThreadWithSpecifiedMaxWaitTime {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 3;
[self.httpdns resolveHostSync:request];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 3, @"elapsedTime should be more than 3s, but is %f", elapsedTime);
}
- (void)testResolveSameHostShouldWaitForTheFirstOne {
__block HttpdnsHostObject *ipv4HostObject = [self constructSimpleIpv4HostObject];
HttpdnsRemoteResolver *realResolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(realResolver);
__block NSArray *mockResolverHostObjects = @[ipv4HostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
// 1.5<EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.5];
[invocation setReturnValue:&mockResolverHostObjects];
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
[self.httpdns.requestManager cleanAllHostMemoryCache];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
});
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.5];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
//
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 1, @"elapsedTime should be more than 1s, but is %f", elapsedTime);
XCTAssert(elapsedTime <= 1.5, @"elapsedTime should not be more than 1.5s, but is %f", elapsedTime);
// TODO <EFBFBD><EFBFBD>?
// XCTAssert(elapsedTime < 4.1, @"elapsedTime should be less than 4.1s, but is %f", elapsedTime);
}
- (void)testResolveSameHostShouldRequestAgainAfterFirstFailed {
__block HttpdnsHostObject *ipv4HostObject = [self constructSimpleIpv4HostObject];
HttpdnsRemoteResolver *realResolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(realResolver);
__block atomic_int count = 0;
__block NSArray *mockResolverHostObjects = @[ipv4HostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
int localCount = atomic_fetch_add(&count, 1) + 1;
if (localCount == 1) {
[NSThread sleepForTimeInterval:0.4];
//
@throw [NSException exceptionWithName:@"TestException" reason:@"TestException" userInfo:nil];
} else {
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.4];
[invocation setReturnValue:&mockResolverHostObjects];
}
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
[self.httpdns.requestManager cleanAllHostMemoryCache];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
});
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.2];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
// <EFBFBD><EFBFBD>?
// 5<EFBFBD><EFBFBD>?
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 0.6, @"elapsedTime should be more than 0.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime < 0.8, @"elapsedTime should be less than 0.8s, but is %f", elapsedTime);
}
// <EFBFBD><EFBFBD>?
- (void)testSyncMethodSetBlockTimeout {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 2.5;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:request];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 2.6, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 2.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
// <EFBFBD><EFBFBD>?.5 - 5<EFBFBD><EFBFBD>?
- (void)testLimitResolveTimeoutRange {
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeAuto;
request.resolveTimeoutInSecond = 0.2;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertGreaterThanOrEqual(request.resolveTimeoutInSecond, 0.5);
request.resolveTimeoutInSecond = 5.1;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertLessThanOrEqual(request.resolveTimeoutInSecond, 5);
request.resolveTimeoutInSecond = 3.5;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertEqual(request.resolveTimeoutInSecond, 3.5);
}
// <EFBFBD><EFBFBD>?
- (void)testAsyncMethodSetBlockTimeout {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 2.5;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.httpdns resolveHostAsync:request completionHandler:^(HttpdnsResult *result) {
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 2.6, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 2.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
// 线线<EFBFBD><EFBFBD>?
- (void)testMultiThreadSyncMethodMaxBlockingTime {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 4.5;
const int threadCount = 10;
NSMutableArray *semaArray = [NSMutableArray new];
for (int i = 0; i < threadCount; i++) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[semaArray addObject:semaphore];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:request];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 4.5 + 0.01, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 4.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
});
}
for (int i = 0; i < threadCount; i++) {
dispatch_semaphore_wait(semaArray[i], DISPATCH_TIME_FOREVER);
}
}
@end

View File

@@ -0,0 +1,301 @@
//
// PresetCacheAndRetrieveTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/5/26.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
/**
* 使OCMockMock(使stopMocking)
* case<EFBFBD><EFBFBD>?
*/
@interface PresetCacheAndRetrieveTest : TestBase
@end
@implementation PresetCacheAndRetrieveTest
+ (void)setUp {
[super setUp];
HttpDnsService *httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[httpdns setLogEnabled:YES];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
self.httpdns = [HttpDnsService sharedInstance];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
// ipv4
- (void)testSimplyRetrieveCachedResultUnderIpv4Only {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// both
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// autoipv4
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 0);
}
// ipv6
- (void)testSimplyRetrieveCachedResultUnderIpv6Only {
[self presetNetworkEnvAsIpv6];
[self.httpdns cleanAllHostCache];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// both
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// autoipv6only
// ipv4autoipv6ipv6<EFBFBD><EFBFBD>?
// ipv4+ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
}
// ipv4ipv6
- (void)testSimplyRetrieveCachedResultUnderDualStack {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
// ipv4ipv6
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// ipv4ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
//
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
}
// ttllastLookupTimeipv4ipv6<EFBFBD><EFBFBD>?
- (void)testTTLAndLastLookUpTime {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
// ipv4ipv6
HttpdnsHostObject *hostObject1 = [self constructSimpleIpv4AndIpv6HostObject];
hostObject1.v4ttl = 200;
hostObject1.v6ttl = 300;
int64_t currentTimestamp = [[NSDate new] timeIntervalSince1970];
hostObject1.lastIPv4LookupTime = currentTimestamp - 1;
hostObject1.lastIPv6LookupTime = currentTimestamp - 2;
// <EFBFBD><EFBFBD>?
[self.httpdns.requestManager mergeLookupResultToManager:hostObject1 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// autoipv4ipv6
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertEqual(result.ttl, hostObject1.v4ttl);
XCTAssertEqual(result.lastUpdatedTimeInterval, hostObject1.lastIPv4LookupTime);
XCTAssertEqual(result.v6ttl, hostObject1.v6ttl);
XCTAssertEqual(result.v6LastUpdatedTimeInterval, hostObject1.lastIPv6LookupTime);
HttpdnsHostObject *hostObject2 = [self constructSimpleIpv4HostObject];
hostObject2.hostName = ipv4AndIpv6Host;
hostObject2.v4ttl = 600;
hostObject2.lastIPv4LookupTime = currentTimestamp - 10;
// ipv4<EFBFBD><EFBFBD>?
[self.httpdns.requestManager mergeLookupResultToManager:hostObject2 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeIpv4];
// v4v6<EFBFBD><EFBFBD>?
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertEqual(result.ttl, hostObject2.v4ttl);
XCTAssertEqual(result.lastUpdatedTimeInterval, hostObject2.lastIPv4LookupTime);
XCTAssertEqual(result.v6ttl, hostObject1.v6ttl);
XCTAssertEqual(result.v6LastUpdatedTimeInterval, hostObject1.lastIPv6LookupTime);
}
// ipv4ipv6
// ipv6ipv6<EFBFBD><EFBFBD>?
- (void)testMergeNoIpv6ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
// ipv4hostipv6
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4OnlyHost cacheKey:ipv4OnlyHost underQueryIpType:HttpdnsQueryIPTypeBoth];
[self shouldNotHaveCallNetworkRequestWhenResolving:^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
}
// ipv4ipv4ipv6
// ipv6
- (void)testMergeOnlyIpv4ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4OnlyHost cacheKey:ipv4OnlyHost underQueryIpType:HttpdnsQueryIPTypeIpv4];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 使线
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self shouldHaveCalledRequestWhenResolving:^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNil(result);
dispatch_semaphore_signal(sema);
}];
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
// ipv6ipv6ipv4
// ipv4
- (void)testMergeOnlyIpv6ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv6OnlyHost cacheKey:ipv6OnlyHost underQueryIpType:HttpdnsQueryIPTypeIpv6];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 使线
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self shouldHaveCalledRequestWhenResolving:^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv6OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNil(result);
dispatch_semaphore_signal(sema);
}];
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,265 @@
//
// ResolvingEffectiveHostTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/5/28.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
@interface ResolvingEffectiveHostTest : TestBase<HttpdnsTTLDelegate>
@end
@implementation ResolvingEffectiveHostTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// ttl<EFBFBD><EFBFBD>?-4<EFBFBD><EFBFBD>?
return arc4random_uniform(4) + 1;
}
- (void)testNormalMultipleHostsResolve {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 使ttl
[self.httpdns setTtlDelegate:nil];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[hostNameIpPrefixMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull host, NSString * _Nonnull ipPrefix, BOOL * _Nonnull stop) {
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertGreaterThan(result.ttl, 0);
// ipip
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testNonblockingMethodShouldNotBlockDuringMultithreadLongRun {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 5;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
long long executeStartTimeInMs = [[NSDate date] timeIntervalSince1970] * 1000;
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
long long executeEndTimeInMs = [[NSDate date] timeIntervalSince1970] * 1000;
// <EFBFBD><EFBFBD>?0ms
if (executeEndTimeInMs - executeStartTimeInMs >= 30) {
printf("XCTAssertWillFailed, host: %s, executeTime: %lldms\n", [host UTF8String], executeEndTimeInMs - executeStartTimeInMs);
}
XCTAssertLessThan(executeEndTimeInMs - executeStartTimeInMs, 30);
if (result) {
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
[NSThread sleepForTimeInterval:0.1];
}
});
}
[NSThread sleepForTimeInterval:testDuration + 1];
}
- (void)testMultithreadAndMultiHostResolvingForALongRun {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 4;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
[self.httpdns resolveHostAsync:host byIpType:HttpdnsQueryIPTypeIpv4 completionHandler:^(HttpdnsResult *result) {
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}];
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
if (result) {
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
[NSThread sleepForTimeInterval:0.1];
}
});
}
sleep(testDuration + 1);
}
// bothipv4
// ipv6ipv4
- (void)testMultithreadAndMultiHostResolvingForALongRunBySpecifyBothIpv4AndIpv6 {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 4;
//
__block int syncCount = 0, asyncCount = 0, syncNonBlockingCount = 0;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue(!result.hasIpv6Address);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
syncCount++;
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
[self.httpdns resolveHostAsync:host byIpType:HttpdnsQueryIPTypeBoth completionHandler:^(HttpdnsResult *result) {
XCTAssertNotNil(result);
XCTAssertTrue(!result.hasIpv6Address);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
asyncCount++;
}];
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeBoth];
if (result) {
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertTrue(!result.hasIpv6Address);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
syncNonBlockingCount++;
[NSThread sleepForTimeInterval:0.1];
}
});
}
sleep(testDuration + 1);
int theoreticalCount = threadCountForEachType * (testDuration / 0.1);
// printf all the counts
printf("syncCount: %d, asyncCount: %d, syncNonBlockingCount: %d, theoreticalCount: %d\n", syncCount, asyncCount, syncNonBlockingCount, theoreticalCount);
}
@end

View File

@@ -0,0 +1,133 @@
//
// ScheduleCenterV4Test.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/6/16.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsScheduleCenter.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsRequest_Internal.h"
#import "HttpdnsScheduleExecutor.h"
#import "HttpdnsRemoteResolver.h"
/**
* 使OCMockMock(使stopMocking)
* case<EFBFBD><EFBFBD>?
*/
@interface ScheduleCenterV4Test : TestBase
@end
@implementation ScheduleCenterV4Test
+ (void)setUp {
[super setUp];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testUpdateFailureWillMoveToNextUpdateServer {
[self presetNetworkEnvAsIpv4];
HttpdnsScheduleExecutor *realRequest = [HttpdnsScheduleExecutor new];
id mockRequest = OCMPartialMock(realRequest);
OCMStub([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andReturn(nil);
id mockRequestClass = OCMClassMock([HttpdnsScheduleExecutor class]);
OCMStub([mockRequestClass new]).andReturn(mockRequest);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
NSArray<NSString *> *updateServerHostList = [scheduleCenter currentUpdateServerV4HostList];
int updateServerCount = (int)[updateServerHostList count];
XCTAssertGreaterThan(updateServerCount, 0);
int startIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
// 2
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
OCMVerify([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]]);
int currentIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
XCTAssertEqual((startIndex + 1) % updateServerCount, currentIndex);
for (int i = 0; i < updateServerCount; i++) {
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
}
int finalIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
XCTAssertEqual(currentIndex, finalIndex % updateServerCount);
[NSThread sleepForTimeInterval:3];
}
- (void)testResolveFailureWillMoveToNextServiceServer {
[self presetNetworkEnvAsIpv4];
id mockResolver = OCMPartialMock([HttpdnsRemoteResolver new]);
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
NSError *mockError = [NSError errorWithDomain:@"com.example.error" code:123 userInfo:@{NSLocalizedDescriptionKey: @"Mock error"}];
NSError *__autoreleasing *errorPtr = nil;
[invocation getArgument:&errorPtr atIndex:3];
if (errorPtr) {
*errorPtr = mockError;
}
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
int startIndex = [scheduleCenter currentActiveServiceServerHostIndex];
int serviceServerCount = (int)[scheduleCenter currentServiceServerV4HostList].count;
HttpdnsRequest *request = [[HttpdnsRequest alloc] initWithHost:@"mock" queryIpType:HttpdnsQueryIPTypeAuto];
[request becomeBlockingRequest];
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[requestManager executeRequest:request retryCount:1];
int secondIndex = [scheduleCenter currentActiveServiceServerHostIndex];
XCTAssertEqual((startIndex + 1) % serviceServerCount, secondIndex);
}
@end

View File

@@ -0,0 +1,90 @@
//
// ScheduleCenterV6Test.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsScheduleExecutor.h"
#import "HttpdnsScheduleCenter.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsUtil.h"
/**
* 使OCMockMock(使stopMocking)
* case<EFBFBD><EFBFBD>?
*/
@interface ScheduleCenterV6Test : TestBase
@end
@implementation ScheduleCenterV6Test
+ (void)setUp {
[super setUp];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testUpdateFailureWillMoveToNextUpdateServer {
[self presetNetworkEnvAsIpv6];
HttpdnsScheduleExecutor *realRequest = [HttpdnsScheduleExecutor new];
id mockRequest = OCMPartialMock(realRequest);
OCMStub([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andReturn(nil);
id mockRequestClass = OCMClassMock([HttpdnsScheduleExecutor class]);
OCMStub([mockRequestClass new]).andReturn(mockRequest);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
NSArray<NSString *> *updateServerHostList = [scheduleCenter currentUpdateServerV4HostList];
int updateServerCount = (int)[updateServerHostList count];
XCTAssertGreaterThan(updateServerCount, 0);
// 2
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
NSString *activeUpdateHost = [scheduleCenter getActiveUpdateServerHost];
// ipv4
XCTAssertFalse([HttpdnsUtil isIPv4Address:activeUpdateHost]);
OCMVerify([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]]);
[NSThread sleepForTimeInterval:3];
}
@end

View File

@@ -0,0 +1,108 @@
//
// SdnsScenarioTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2024/5/29.
// Copyright © 2024 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface SdnsScenarioTest : TestBase <HttpdnsTTLDelegate>
@end
static int ttlForTest = 3;
static NSString *sdnsHost = @"sdns1.onlyforhttpdnstest.run.place";
@implementation SdnsScenarioTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// <EFBFBD><EFBFBD>?
return ttlForTest;
}
- (void)testSimpleSdnsScenario {
NSDictionary *extras = @{
@"testKey": @"testValue",
@"key2": @"value2",
@"key3": @"value3"
};
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:extras sdnsCacheKey:nil];
XCTAssertNotNil(result);
XCTAssertNotNil(result.ips);
// 0.0.0.0 FC
XCTAssertTrue([result.ips containsObject:@"0.0.0.0"]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testSdnsScenarioUsingCustomCacheKey {
[self.httpdns.requestManager cleanAllHostMemoryCache];
NSDictionary *extras = @{
@"testKey": @"testValue",
@"key2": @"value2",
@"key3": @"value3"
};
NSString *cacheKey = [NSString stringWithFormat:@"abcd_%@", sdnsHost];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:extras sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertNotNil(result.ips);
// 0.0.0.0 FC
XCTAssertTrue([result.ips containsObject:@"0.0.0.0"]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// cacheKeycacheKeynil
HttpdnsResult *result1 = [self.httpdns resolveHostSyncNonBlocking:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNil(result1);
// 使cachekey
HttpdnsResult *result2 = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:@{} sdnsCacheKey:cacheKey];
XCTAssertNotNil(result2);
XCTAssertNotNil(result2.ips);
// 0.0.0.0 FC
XCTAssertTrue([result2.ips containsObject:@"0.0.0.0"]);
}
@end

View File

@@ -0,0 +1,402 @@
//
// IpDetectorTest.m
// TrustHttpDNSTests
//
// Created by xuyecan on 2025/3/14.
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsIPQualityDetector.h"
@interface IpDetectorTest : TestBase
@end
@implementation IpDetectorTest
- (void)setUp {
[super setUp];
// 使maxConcurrentDetections
}
- (void)tearDown {
[super tearDown];
}
#pragma mark - <EFBFBD><EFBFBD>?
- (void)testSharedInstance {
//
HttpdnsIPQualityDetector *detector1 = [HttpdnsIPQualityDetector sharedInstance];
HttpdnsIPQualityDetector *detector2 = [HttpdnsIPQualityDetector sharedInstance];
XCTAssertEqual(detector1, detector2, @"<EFBFBD><EFBFBD>?);
XCTAssertNotNil(detector1, @"单例实例不应为nil");
}
#pragma mark - TCP
- (void)testTcpConnectToValidIP {
// IP
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// 使DNS<EFBFBD><EFBFBD>?
NSInteger costTime = [detector tcpConnectToIP:@"8.8.8.8" port:53];
//
XCTAssertGreaterThan(costTime, 0, @"连接到有效IP应返回正数耗时");
}
- (void)testTcpConnectToInvalidIP {
// IP
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// 使IP
NSInteger costTime = [detector tcpConnectToIP:@"192.168.255.255" port:12345];
// <EFBFBD><EFBFBD>?1
XCTAssertEqual(costTime, -1, @"连接到无效IP应返<E5BA94><E8BF94>?1");
}
- (void)testTcpConnectWithInvalidParameters {
// 使
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// IP
NSInteger costTime = [detector tcpConnectToIP:nil port:80];
XCTAssertEqual(costTime, -1, @"使用nil IP应返<E5BA94><E8BF94>?1");
// IP
costTime = [detector tcpConnectToIP:@"not-an-ip" port:80];
XCTAssertEqual(costTime, -1, @"使用无效格式IP应返<E5BA94><E8BF94>?1");
//
costTime = [detector tcpConnectToIP:@"8.8.8.8" port:-1];
XCTAssertEqual(costTime, -1, @"使用无效端口应返<E5BA94><E8BF94>?1");
}
#pragma mark -
- (void)testScheduleIPQualityDetection {
// IP<EFBFBD><EFBFBD>?
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection
OCMExpect([detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:[OCMArg any]]);
//
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
// executeDetection
}];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testScheduleWithInvalidParameters {
// 使
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection<EFBFBD><EFBFBD>?
OCMReject([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
// nil cacheKey
[detectorMock scheduleIPQualityDetection:nil
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// nil IP
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:nil
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// nil callback
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:nil];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testConcurrencyLimitReached {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// dispatch_semaphore_wait<EFBFBD><EFBFBD>?
// scheduleIPQualityDetectionaddPendingTask
OCMStub([detectorMock scheduleIPQualityDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
//
NSString *cacheKey;
NSString *ip;
NSNumber *port;
HttpdnsIPQualityCallback callback;
[invocation getArgument:&cacheKey atIndex:2];
[invocation getArgument:&ip atIndex:3];
[invocation getArgument:&port atIndex:4];
[invocation getArgument:&callback atIndex:5];
// addPendingTask<EFBFBD><EFBFBD>?
[detector addPendingTask:cacheKey ip:ip port:port callback:callback];
});
// addPendingTaskexecuteDetection
// 使mockmock
OCMExpect([detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:[OCMArg any]]);
OCMReject([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
//
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testAddPendingTask {
// <EFBFBD><EFBFBD>?
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// processPendingTasksIfNeeded<EFBFBD><EFBFBD>?
OCMStub([detectorMock processPendingTasksIfNeeded]);
// <EFBFBD><EFBFBD>?
NSUInteger initialCount = [detector pendingTasksCount];
//
[detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// <EFBFBD><EFBFBD>?
XCTAssertEqual([detector pendingTasksCount], initialCount + 1, @"添加任务后待处理任务数量应增<E5BA94><E5A29E>?");
//
[detectorMock stopMocking];
}
- (void)testPendingTasksProcessing {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection<EFBFBD><EFBFBD>?
OCMStub([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
//
[detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// <EFBFBD><EFBFBD>?
[detectorMock processPendingTasksIfNeeded];
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.1];
// <EFBFBD><EFBFBD>?
XCTAssertEqual([detector pendingTasksCount], 0, @"处理后待处理任务数量应为0");
//
[detectorMock stopMocking];
}
#pragma mark -
- (void)testExecuteDetection {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP<EFBFBD><EFBFBD>?
OCMStub([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(100);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"cacheKey<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(ip, @"1.2.3.4", @"IP<EFBFBD><EFBFBD>?);
XCTAssertEqual(costTime, 100, @"<EFBFBD><EFBFBD>?);
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
- (void)testExecuteDetectionWithFailure {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP
OCMStub([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(-1);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"失败回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"cacheKey<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(ip, @"1.2.3.4", @"IP<EFBFBD><EFBFBD>?);
XCTAssertEqual(costTime, -1, @"连接失败时回调中的耗时应为-1");
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
- (void)testExecuteDetectionWithNilPort {
// nil<EFBFBD><EFBFBD>?
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP使<EFBFBD><EFBFBD>?0
OCMExpect([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(100);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"默认端口回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:nil
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
#pragma mark -
- (void)testMemoryManagementInAsyncOperations {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
//
__block NSString *tempCacheKey = [@"example.com" copy];
__block NSString *tempIP = [@"1.2.3.4" copy];
//
__weak NSString *weakCacheKey = tempCacheKey;
__weak NSString *weakIP = tempIP;
// tcpConnectToIP
OCMStub([detectorMock tcpConnectToIP:[OCMArg any] port:80]).andDo(^(NSInvocation *invocation) {
// GC<EFBFBD><EFBFBD>?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// <EFBFBD><EFBFBD>?
NSInteger result = 100;
[invocation setReturnValue:&result];
});
});
//
XCTestExpectation *expectation = [self expectationWithDescription:@"内存管理回调应被执行"];
//
[detectorMock executeDetection:tempCacheKey
ip:tempIP
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"cacheKey<EFBFBD><EFBFBD>?);
XCTAssertEqualObjects(ip, @"1.2.3.4", @"IP<EFBFBD><EFBFBD>?);
[expectation fulfill];
}];
// <EFBFBD><EFBFBD>?
tempCacheKey = nil;
tempIP = nil;
// GCARC<EFBFBD><EFBFBD>?
@autoreleasepool {
// <EFBFBD><EFBFBD>?
}
// executeDetection
XCTAssertNotNil(weakCacheKey, @"cacheKey<EFBFBD><EFBFBD>?);
XCTAssertNotNil(weakIP, @"IP<EFBFBD><EFBFBD>?);
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
@end

View File

@@ -0,0 +1,24 @@
<?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>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
//
// HttpdnsNWHTTPClientTestBase.h
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// 测试基类 - 为所<E4B8BA><E68980>?HttpdnsNWHTTPClient 测试提供共享<E585B1><E4BAAB>?setup/teardown
//
#import <XCTest/XCTest.h>
#import "HttpdnsNWHTTPClient.h"
#import "HttpdnsNWHTTPClient_Internal.h"
#import "HttpdnsNWReusableConnection.h"
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsNWHTTPClientTestBase : XCTestCase
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,42 @@
//
// HttpdnsNWHTTPClientTestBase.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// -
//
// mock server
// cd TrustHttpDNSTests/Network && python3 mock_server.py
// <EFBFBD><EFBFBD>?
// - HTTP: 11080
// - HTTPS: 11443, 11444, 11445, 11446
//
#import "HttpdnsNWHTTPClientTestBase.h"
@implementation HttpdnsNWHTTPClientTestBase
- (void)setUp {
[super setUp];
// <EFBFBD><EFBFBD>?TLS <EFBFBD><EFBFBD>?mock server <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?
// 1.
// 2. <EFBFBD><EFBFBD>?loopback (127.0.0.1)
// 3. <EFBFBD><EFBFBD>?
setenv("HTTPDNS_SKIP_TLS_VERIFY", "1", 1);
self.client = [[HttpdnsNWHTTPClient alloc] init];
}
- (void)tearDown {
// <EFBFBD><EFBFBD>?
unsetenv("HTTPDNS_SKIP_TLS_VERIFY");
self.client = nil;
[super tearDown];
}
@end

View File

@@ -0,0 +1,58 @@
//
// HttpdnsNWHTTPClientTestHelper.h
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsNWHTTPClientTestHelper : NSObject
#pragma mark - HTTP 响应数据构<E68DAE><E69E84>?
// 构造标<E980A0><E6A087>?HTTP 响应数据
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body;
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks;
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应(带 trailers<72><73>?
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers;
#pragma mark - Chunked 编码工具
// 编码单个 chunk
+ (NSData *)encodeChunk:(NSData *)data;
// 编码单个 chunk带 extension<6F><6E>?
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension;
// 编码终止 chunksize=0<><30>?
+ (NSData *)encodeLastChunk;
// 编码终止 chunk带 trailers<72><73>?
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers;
#pragma mark - 测试数据生成
// 生成指定大小的随机数<E69CBA><E695B0>?
+ (NSData *)randomDataWithSize:(NSUInteger)size;
// 生成 JSON 格式的响应体
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,168 @@
//
// HttpdnsNWHTTPClientTestHelper.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import "HttpdnsNWHTTPClientTestHelper.h"
@implementation HttpdnsNWHTTPClientTestHelper
#pragma mark - HTTP <EFBFBD><EFBFBD>?
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body {
NSMutableString *response = [NSMutableString string];
//
[response appendFormat:@"HTTP/1.1 %ld %@\r\n", (long)statusCode, statusText ?: @"OK"];
//
if (headers) {
for (NSString *key in headers) {
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
}
}
// <EFBFBD><EFBFBD>?body <EFBFBD><EFBFBD>?Content-Length<EFBFBD><EFBFBD>?
if (body && body.length > 0 && !headers[@"Content-Length"]) {
[response appendFormat:@"Content-Length: %lu\r\n", (unsigned long)body.length];
}
// <EFBFBD><EFBFBD>?body
[response appendString:@"\r\n"];
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// body
if (body) {
[responseData appendData:body];
}
return [responseData copy];
}
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks {
return [self createChunkedHTTPResponseWithStatus:statusCode
headers:headers
chunks:chunks
trailers:nil];
}
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers {
NSMutableString *response = [NSMutableString string];
//
[response appendFormat:@"HTTP/1.1 %ld OK\r\n", (long)statusCode];
//
if (headers) {
for (NSString *key in headers) {
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
}
}
// Transfer-Encoding
[response appendString:@"Transfer-Encoding: chunked\r\n"];
//
[response appendString:@"\r\n"];
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// chunk
for (NSData *chunk in chunks) {
[responseData appendData:[self encodeChunk:chunk]];
}
// chunk
if (trailers) {
[responseData appendData:[self encodeLastChunkWithTrailers:trailers]];
} else {
[responseData appendData:[self encodeLastChunk]];
}
return [responseData copy];
}
#pragma mark - Chunked
+ (NSData *)encodeChunk:(NSData *)data {
return [self encodeChunk:data extension:nil];
}
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension {
NSMutableString *chunkString = [NSMutableString string];
// Chunk size
if (extension) {
[chunkString appendFormat:@"%lx;%@\r\n", (unsigned long)data.length, extension];
} else {
[chunkString appendFormat:@"%lx\r\n", (unsigned long)data.length];
}
NSMutableData *chunkData = [[chunkString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// Chunk data
[chunkData appendData:data];
// CRLF
[chunkData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
return [chunkData copy];
}
+ (NSData *)encodeLastChunk {
return [@"0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
}
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers {
NSMutableString *lastChunkString = [NSMutableString stringWithString:@"0\r\n"];
// trailer
for (NSString *key in trailers) {
[lastChunkString appendFormat:@"%@: %@\r\n", key, trailers[key]];
}
//
[lastChunkString appendString:@"\r\n"];
return [lastChunkString dataUsingEncoding:NSUTF8StringEncoding];
}
#pragma mark -
+ (NSData *)randomDataWithSize:(NSUInteger)size {
NSMutableData *data = [NSMutableData dataWithLength:size];
if (SecRandomCopyBytes(kSecRandomDefault, size, data.mutableBytes) != 0) {
// SecRandom 使<EFBFBD><EFBFBD>?
uint8_t *bytes = data.mutableBytes;
for (NSUInteger i = 0; i < size; i++) {
bytes[i] = arc4random_uniform(256);
}
}
return [data copy];
}
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
options:0
error:&error];
if (error) {
NSLog(@"JSON serialization error: %@", error);
return nil;
}
return jsonData;
}
@end

View File

@@ -0,0 +1,742 @@
//
// HttpdnsNWHTTPClientTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "HttpdnsNWHTTPClient.h"
#import "HttpdnsNWHTTPClient_Internal.h"
#import "HttpdnsNWReusableConnection.h"
#import "HttpdnsNWHTTPClientTestHelper.h"
@interface HttpdnsNWHTTPClientTests : XCTestCase
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
@end
@implementation HttpdnsNWHTTPClientTests
- (void)setUp {
[super setUp];
self.client = [[HttpdnsNWHTTPClient alloc] init];
}
- (void)tearDown {
self.client = nil;
[super tearDown];
}
#pragma mark - A. HTTP
#pragma mark - A1. Header (9<EFBFBD><EFBFBD>?
// A1.1
- (void)testParseHTTPHeaders_ValidResponse_Success {
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"application/json"}
body:nil];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // key
XCTAssertNil(error);
}
// A1.2
- (void)testParseHTTPHeaders_MultipleHeaders_AllParsed {
NSDictionary *testHeaders = @{
@"Content-Type": @"application/json",
@"Content-Length": @"123",
@"Connection": @"keep-alive",
@"X-Custom-Header": @"custom-value",
@"Cache-Control": @"no-cache"
};
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:testHeaders
body:nil];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqual(headers.count, testHeaders.count);
// <EFBFBD><EFBFBD>?key
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
XCTAssertEqualObjects(headers[@"content-length"], @"123");
XCTAssertEqualObjects(headers[@"connection"], @"keep-alive");
XCTAssertEqualObjects(headers[@"x-custom-header"], @"custom-value");
XCTAssertEqualObjects(headers[@"cache-control"], @"no-cache");
}
// A1.3 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_IncompleteData_ReturnsIncomplete {
NSString *incompleteResponse = @"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n";
NSData *data = [incompleteResponse dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultIncomplete);
}
// A1.4
- (void)testParseHTTPHeaders_InvalidStatusLine_ReturnsError {
NSString *invalidResponse = @"INVALID\r\n\r\n";
NSData *data = [invalidResponse dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
XCTAssertNotNil(error);
}
// A1.5
- (void)testParseHTTPHeaders_HeadersWithWhitespace_Trimmed {
NSString *responseWithSpaces = @"HTTP/1.1 200 OK\r\nContent-Type: application/json \r\n\r\n";
NSData *data = [responseWithSpaces dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // <EFBFBD><EFBFBD>?trim
}
// A1.6 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_EmptyHeaderValue_HandledGracefully {
NSString *responseWithEmptyValue = @"HTTP/1.1 200 OK\r\nX-Empty-Header:\r\n\r\n";
NSData *data = [responseWithEmptyValue dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"x-empty-header"], @"");
}
// A1.7 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_NonNumericStatusCode_ReturnsError {
NSString *invalidStatusCode = @"HTTP/1.1 ABC OK\r\n\r\n";
NSData *data = [invalidStatusCode dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
}
// A1.8
- (void)testParseHTTPHeaders_StatusCodeZero_ReturnsError {
NSString *zeroStatusCode = @"HTTP/1.1 0 OK\r\n\r\n";
NSData *data = [zeroStatusCode dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
}
// A1.9 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_HeaderWithoutColon_Skipped {
NSString *responseWithInvalidHeader = @"HTTP/1.1 200 OK\r\nInvalidHeader\r\nContent-Type: application/json\r\n\r\n";
NSData *data = [responseWithInvalidHeader dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); //
}
#pragma mark - A2. Chunked <EFBFBD><EFBFBD>?(8<EFBFBD><EFBFBD>?
// A2.1 chunk
- (void)testCheckChunkedBody_SingleChunk_DetectsComplete {
NSString *singleChunkBody = @"5\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", singleChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.2 chunks
- (void)testCheckChunkedBody_MultipleChunks_DetectsComplete {
NSString *multiChunkBody = @"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", multiChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.3 <EFBFBD><EFBFBD>?chunk
- (void)testCheckChunkedBody_IncompleteChunk_ReturnsIncomplete {
NSString *incompleteChunkBody = @"5\r\nhel"; // <EFBFBD><EFBFBD>?
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", incompleteChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultIncomplete);
}
// A2.4 <EFBFBD><EFBFBD>?chunk extension
- (void)testCheckChunkedBody_WithChunkExtension_Ignored {
NSString *chunkWithExtension = @"5;name=value\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithExtension];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
// headerEndIndex <EFBFBD><EFBFBD>?\r\n\r\n <EFBFBD><EFBFBD>?\r
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.5 size
- (void)testCheckChunkedBody_InvalidHexSize_ReturnsError {
NSString *invalidChunkSize = @"ZZZ\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", invalidChunkSize];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.6 Chunk size
- (void)testCheckChunkedBody_ChunkSizeOverflow_ReturnsError {
NSString *overflowChunkSize = @"FFFFFFFFFFFFFFFF\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", overflowChunkSize];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.7 CRLF <EFBFBD><EFBFBD>?
- (void)testCheckChunkedBody_MissingCRLFTerminator_ReturnsError {
NSString *missingTerminator = @"5\r\nhelloXX0\r\n\r\n"; // <EFBFBD><EFBFBD>?hello\r\n
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", missingTerminator];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.8 <EFBFBD><EFBFBD>?trailers
- (void)testCheckChunkedBody_WithTrailers_DetectsComplete {
NSString *chunkWithTrailers = @"5\r\nhello\r\n0\r\nX-Trailer: value\r\nX-Custom: test\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithTrailers];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
#pragma mark - A3. Chunked (2<EFBFBD><EFBFBD>?
// A3.1 chunks
- (void)testDecodeChunkedBody_MultipleChunks_DecodesCorrectly {
NSArray *chunks = @[
[@"hello" dataUsingEncoding:NSUTF8StringEncoding],
[@" world" dataUsingEncoding:NSUTF8StringEncoding]
];
NSData *chunkedData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
headers:nil
chunks:chunks];
// chunked body <EFBFBD><EFBFBD>?headers<EFBFBD><EFBFBD>?
NSData *headerData = [@"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
NSData *bodyData = [chunkedData subdataWithRange:NSMakeRange(headerData.length, chunkedData.length - headerData.length)];
NSError *error;
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
XCTAssertNotNil(decoded);
XCTAssertNil(error);
NSString *decodedString = [[NSString alloc] initWithData:decoded encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(decodedString, @"hello world");
}
// A3.2 nil
- (void)testDecodeChunkedBody_InvalidFormat_ReturnsNil {
NSString *invalidChunked = @"ZZZ\r\nbad data\r\n";
NSData *bodyData = [invalidChunked dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
XCTAssertNil(decoded);
XCTAssertNotNil(error);
}
#pragma mark - A4. (6<EFBFBD><EFBFBD>?
// A4.1 Content-Length
- (void)testParseResponse_WithContentLength_ParsesCorrectly {
NSString *bodyString = @"{\"ips\":[]}";
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"application/json"}
body:bodyData];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
XCTAssertEqualObjects(body, bodyData);
XCTAssertNil(error);
}
// A4.2 Chunked
- (void)testParseResponse_WithChunkedEncoding_DecodesBody {
NSArray *chunks = @[
[@"{\"ips\"" dataUsingEncoding:NSUTF8StringEncoding],
[@":[]}" dataUsingEncoding:NSUTF8StringEncoding]
];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
headers:@{@"Content-Type": @"application/json"}
chunks:chunks];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(bodyString, @"{\"ips\":[]}");
}
// A4.3 <EFBFBD><EFBFBD>?body
- (void)testParseResponse_EmptyBody_Success {
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:204
statusText:@"No Content"
headers:nil
body:nil];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 204);
XCTAssertEqual(body.length, 0);
}
// A4.4 Content-Length <EFBFBD><EFBFBD>?
- (void)testParseResponse_ContentLengthMismatch_LogsButSucceeds {
NSData *bodyData = [@"short" dataUsingEncoding:NSUTF8StringEncoding];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Length": @"100"} // <EFBFBD><EFBFBD>?
body:bodyData];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success); // <EFBFBD><EFBFBD>?
XCTAssertEqualObjects(body, bodyData);
}
// A4.5 <EFBFBD><EFBFBD>?
- (void)testParseResponse_EmptyData_ReturnsError {
NSData *emptyData = [NSData data];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:emptyData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertFalse(success);
XCTAssertNotNil(error);
}
// A4.6 headers <EFBFBD><EFBFBD>?body
- (void)testParseResponse_OnlyHeaders_EmptyBody {
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"text/plain"}
body:nil];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqual(body.length, 0);
}
#pragma mark - C. (7<EFBFBD><EFBFBD>?
// C.1 GET
- (void)testBuildHTTPRequest_BasicGET_CorrectFormat {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"TestAgent"];
XCTAssertTrue([request containsString:@"GET / HTTP/1.1\r\n"]);
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertTrue([request containsString:@"User-Agent: TestAgent\r\n"]);
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
XCTAssertTrue([request hasSuffix:@"\r\n\r\n"]);
}
// C.2 <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_WithQueryString_Included {
NSURL *url = [NSURL URLWithString:@"http://example.com/path?foo=bar&baz=qux"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"GET /path?foo=bar&baz=qux HTTP/1.1\r\n"]);
}
// C.3 User-Agent
- (void)testBuildHTTPRequest_WithUserAgent_Included {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"CustomAgent/1.0"];
XCTAssertTrue([request containsString:@"User-Agent: CustomAgent/1.0\r\n"]);
}
// C.4 HTTP <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_HTTPDefaultPort_NotInHost {
NSURL *url = [NSURL URLWithString:@"http://example.com:80/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertFalse([request containsString:@"Host: example.com:80\r\n"]);
}
// C.5 HTTPS <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_HTTPSDefaultPort_NotInHost {
NSURL *url = [NSURL URLWithString:@"https://example.com:443/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertFalse([request containsString:@"Host: example.com:443\r\n"]);
}
// C.6 <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_NonDefaultPort_InHost {
NSURL *url = [NSURL URLWithString:@"http://example.com:8080/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com:8080\r\n"]);
}
// C.7
- (void)testBuildHTTPRequest_FixedHeaders_Present {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
}
#pragma mark - E. TLS (4)
// TLS SecTrustRef mock
// <EFBFBD><EFBFBD>?
// E.1 YES
- (void)testEvaluateServerTrust_ValidCertificate_ReturnsYES {
// SecTrustRef
//
}
// E.2 Proceed YES
- (void)testEvaluateServerTrust_ProceedResult_ReturnsYES {
// Mock SecTrustEvaluate kSecTrustResultProceed
}
// E.3 NO
- (void)testEvaluateServerTrust_InvalidCertificate_ReturnsNO {
// Mock SecTrustEvaluate kSecTrustResultDeny
}
// E.4 使 SSL Policy
- (void)testEvaluateServerTrust_WithDomain_UsesSSLPolicy {
// 使<EFBFBD><EFBFBD>?SecPolicyCreateSSL(true, domain)
}
#pragma mark - F. (5<EFBFBD><EFBFBD>?
// F.1 URL
- (void)testPerformRequest_VeryLongURL_HandlesCorrectly {
NSMutableString *longPath = [NSMutableString stringWithString:@"http://example.com/"];
for (int i = 0; i < 1000; i++) {
[longPath appendString:@"long/"];
}
NSURL *url = [NSURL URLWithString:longPath];
XCTAssertNotNil(url);
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue(request.length > 5000);
}
// F.2 <EFBFBD><EFBFBD>?User-Agent
- (void)testBuildRequest_EmptyUserAgent_NoUserAgentHeader {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *requestWithNil = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertFalse([requestWithNil containsString:@"User-Agent:"]);
}
// F.3 <EFBFBD><EFBFBD>?
- (void)testParseResponse_VeryLargeBody_HandlesCorrectly {
NSData *largeBody = [HttpdnsNWHTTPClientTestHelper randomDataWithSize:5 * 1024 * 1024];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:nil
body:largeBody];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(body.length, largeBody.length);
}
// F.4 Chunked 退<EFBFBD><EFBFBD>?
- (void)testParseResponse_ChunkedDecodeFails_FallsBackToRaw {
NSString *badChunked = @"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nBAD_CHUNK_DATA";
NSData *responseData = [badChunked dataUsingEncoding:NSUTF8StringEncoding];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertNotNil(body);
}
// F.5 <EFBFBD><EFBFBD>?key
- (void)testConnectionPoolKey_DifferentHosts_SeparateKeys {
NSString *key1 = [self.client connectionPoolKeyForHost:@"host1.com" port:@"80" useTLS:NO];
NSString *key2 = [self.client connectionPoolKeyForHost:@"host2.com" port:@"80" useTLS:NO];
XCTAssertNotEqualObjects(key1, key2);
}
- (void)testConnectionPoolKey_DifferentPorts_SeparateKeys {
NSString *key1 = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
NSString *key2 = [self.client connectionPoolKeyForHost:@"example.com" port:@"8080" useTLS:NO];
XCTAssertNotEqualObjects(key1, key2);
}
- (void)testConnectionPoolKey_HTTPvsHTTPS_SeparateKeys {
NSString *keyHTTP = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
NSString *keyHTTPS = [self.client connectionPoolKeyForHost:@"example.com" port:@"443" useTLS:YES];
XCTAssertNotEqualObjects(keyHTTP, keyHTTPS);
}
@end

View File

@@ -0,0 +1,406 @@
//
// HttpdnsNWHTTPClient_BasicIntegrationTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// - (G) <EFBFBD><EFBFBD>?(J) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?2 G:7 + J:5<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_BasicIntegrationTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_BasicIntegrationTests
#pragma mark - G.
// G.1 HTTP GET
- (void)testIntegration_HTTPGetRequest_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP GET request"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
XCTAssertNotNil(response.body, @"Body should not be nil");
XCTAssertGreaterThan(response.body.length, 0, @"Body should not be empty");
// JSON
NSError *jsonError = nil;
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
options:0
error:&jsonError];
XCTAssertNotNil(jsonDict, @"Response should be valid JSON");
XCTAssertNil(jsonError, @"JSON parsing should succeed");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.2 HTTPS GET
- (void)testIntegration_HTTPSGetRequest_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTPS GET request"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
XCTAssertNotNil(response.body, @"Body should not be nil");
// TLS
XCTAssertGreaterThan(response.body.length, 0, @"HTTPS body should not be empty");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.3 HTTP 404
- (void)testIntegration_NotFound_Returns404 {
XCTestExpectation *expectation = [self expectationWithDescription:@"404 response"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/status/404"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil even for 404");
XCTAssertNil(error, @"Error should be nil for valid HTTP response");
XCTAssertEqual(response.statusCode, 404, @"Status code should be 404");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.4
- (void)testIntegration_ConnectionReuse_MultipleRequests {
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection reuse"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"First response should not be nil");
XCTAssertNil(error1, @"First request should succeed");
XCTAssertEqual(response1.statusCode, 200);
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Second response should not be nil");
XCTAssertNil(error2, @"Second request should succeed");
XCTAssertEqual(response2.statusCode, 200);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:30.0];
}
// G.5 Chunked
- (void)testIntegration_ChunkedResponse_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"Chunked response"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
// httpbin.org/stream-bytes chunked <EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/stream-bytes/1024"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200);
XCTAssertEqual(response.body.length, 1024, @"Should receive exactly 1024 bytes");
// Transfer-Encoding <EFBFBD><EFBFBD>?
NSString *transferEncoding = response.headers[@"transfer-encoding"];
if (transferEncoding) {
XCTAssertTrue([transferEncoding containsString:@"chunked"], @"Should use chunked encoding");
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
#pragma mark - <EFBFBD><EFBFBD>?
// G.6
- (void)testIntegration_RequestTimeout_ReturnsError {
XCTestExpectation *expectation = [self expectationWithDescription:@"Request timeout"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
// httpbin.org/delay/10 <EFBFBD><EFBFBD>?10 2 <EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:2.0
error:&error];
XCTAssertNil(response, @"Response should be nil on timeout");
XCTAssertNotNil(error, @"Error should be set on timeout");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:5.0];
}
// G.7 <EFBFBD><EFBFBD>?
- (void)testIntegration_CustomHeaders_Reflected {
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom headers"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/headers"
userAgent:@"TestUserAgent/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
XCTAssertEqual(response.statusCode, 200);
// JSON User-Agent <EFBFBD><EFBFBD>?
NSError *jsonError = nil;
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
options:0
error:&jsonError];
XCTAssertNotNil(jsonDict);
NSDictionary *headers = jsonDict[@"headers"];
XCTAssertTrue([headers[@"User-Agent"] containsString:@"TestUserAgent"], @"User-Agent should be sent");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
#pragma mark - J.
// J.1 <EFBFBD><EFBFBD>?1
- (void)testConnectionReuse_Expiry31Seconds_NewConnectionCreated {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection expiry"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CFAbsoluteTime time1 = CFAbsoluteTimeGetCurrent();
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"First"
timeout:15.0
error:&error1];
CFAbsoluteTime elapsed1 = CFAbsoluteTimeGetCurrent() - time1;
XCTAssertTrue(response1 != nil || error1 != nil);
// 31
[NSThread sleepForTimeInterval:31.0];
//
CFAbsoluteTime time2 = CFAbsoluteTimeGetCurrent();
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Second"
timeout:15.0
error:&error2];
CFAbsoluteTime elapsed2 = CFAbsoluteTimeGetCurrent() - time2;
XCTAssertTrue(response2 != nil || error2 != nil);
//
//
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:70.0];
}
// J.2 <EFBFBD><EFBFBD>?
- (void)testConnectionReuse_TenRequests_OnlyFourConnectionsKept {
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool size limit"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 10<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"PoolSizeTest"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
//
//
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Verification"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
// J.3
- (void)testConnectionReuse_DifferentPaths_SameConnection {
XCTestExpectation *expectation = [self expectationWithDescription:@"Different paths"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSString *> *paths = @[@"/get", @"/headers", @"/user-agent", @"/uuid"];
NSMutableArray<NSNumber *> *times = [NSMutableArray array];
for (NSString *path in paths) {
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSString *urlString = [NSString stringWithFormat:@"http://127.0.0.1:11080%@", path];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"PathTest"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - start;
XCTAssertTrue(response != nil || error != nil);
[times addObject:@(elapsed)];
}
//
//
XCTAssertEqual(times.count, paths.count);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:60.0];
}
// J.4 HTTP vs HTTPS 使
- (void)testConnectionReuse_HTTPvsHTTPS_DifferentPoolKeys {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP vs HTTPS"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// HTTP
NSError *httpError = nil;
HttpdnsNWHTTPClientResponse *httpResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HTTP"
timeout:15.0
error:&httpError];
XCTAssertTrue(httpResponse != nil || httpError != nil);
// HTTPS 使<EFBFBD><EFBFBD>?key<EFBFBD><EFBFBD>?
NSError *httpsError = nil;
HttpdnsNWHTTPClientResponse *httpsResponse = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HTTPS"
timeout:15.0
error:&httpsError];
XCTAssertTrue(httpsResponse != nil || httpsError != nil);
//
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:35.0];
}
// J.5 <EFBFBD><EFBFBD>?
- (void)testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Keep-alive"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger successCount = 0;
NSMutableArray<NSNumber *> *requestTimes = [NSMutableArray array];
// 201
for (NSInteger i = 0; i < 20; i++) {
// 1<EFBFBD><EFBFBD>?
if (i > 0) {
[NSThread sleepForTimeInterval:1.0];
}
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"KeepAlive"
timeout:10.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
[requestTimes addObject:@(elapsed)];
if (response && (response.statusCode == 200 || response.statusCode == 503)) {
successCount++;
} else {
// 退
break;
}
}
// <EFBFBD><EFBFBD>?
XCTAssertGreaterThan(successCount, 15, @"Most requests should succeed with connection reuse");
// 使keep-alive<EFBFBD><EFBFBD>?
if (requestTimes.count >= 10) {
double firstRequestTime = [requestTimes[0] doubleValue];
double laterAvgTime = 0;
for (NSInteger i = 5; i < MIN(10, requestTimes.count); i++) {
laterAvgTime += [requestTimes[i] doubleValue];
}
laterAvgTime /= MIN(5, requestTimes.count - 5);
//
XCTAssertLessThanOrEqual(laterAvgTime, firstRequestTime * 2.0, @"Connection reuse should keep latency reasonable");
}
[expectation fulfill];
});
// : 19sleep + 20×~2<EFBFBD><EFBFBD>?= 5950退
[self waitForExpectations:@[expectation] timeout:50.0];
}
@end

Some files were not shown because too many files have changed in this diff Show More