feat: sync httpdns sdk/platform updates without large binaries
This commit is contained in:
183
HttpDNSSDK/sdk/ios/CLAUDE.md
Normal file
183
HttpDNSSDK/sdk/ios/CLAUDE.md
Normal 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
|
||||
46
HttpDNSSDK/sdk/ios/NewHTTPDNS.podspec
Normal file
46
HttpDNSSDK/sdk/ios/NewHTTPDNS.podspec
Normal 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"]
|
||||
|
||||
# 链接器参数:保留 Objective‑C 分类
|
||||
s.pod_target_xcconfig = {
|
||||
'OTHER_LDFLAGS' => '$(inherited) -ObjC'
|
||||
}
|
||||
s.user_target_xcconfig = {
|
||||
'OTHER_LDFLAGS' => '$(inherited) -ObjC'
|
||||
}
|
||||
end
|
||||
1382
HttpDNSSDK/sdk/ios/NewHttpDNS.xcodeproj/project.pbxproj
Normal file
1382
HttpDNSSDK/sdk/ios/NewHttpDNS.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
21
HttpDNSSDK/sdk/ios/NewHttpDNS/Config/HttpdnsPublicConstant.h
Normal file
21
HttpDNSSDK/sdk/ios/NewHttpDNS/Config/HttpdnsPublicConstant.h
Normal 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 */
|
||||
@@ -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
|
||||
112
HttpDNSSDK/sdk/ios/NewHttpDNS/Config/HttpdnsRegionConfigLoader.m
Normal file
112
HttpDNSSDK/sdk/ios/NewHttpDNS/Config/HttpdnsRegionConfigLoader.m
Normal 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
|
||||
30
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsDegradationDelegate.h
Normal file
30
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsDegradationDelegate.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#ifndef HttpdnsDegradationDelegate_h
|
||||
#define HttpdnsDegradationDelegate_h
|
||||
|
||||
__attribute__((deprecated("不再建议通过设置此回调实现降级逻辑,而是自行在调用HTTPDNS解析域名前做判断")))
|
||||
@protocol HttpDNSDegradationDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldDegradeHTTPDNS:(NSString *)hostName;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* HttpdnsDegradationDelegate_h */
|
||||
36
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsEdgeService.h
Normal file
36
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsEdgeService.h
Normal 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
|
||||
277
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsEdgeService.m
Normal file
277
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsEdgeService.m
Normal 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
|
||||
21
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsLocalResolver.h
Normal file
21
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsLocalResolver.h
Normal 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
|
||||
150
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsLocalResolver.m
Normal file
150
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsLocalResolver.m
Normal 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; // 同时支持IPv4和IPv6
|
||||
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. 收集所有IPv4和IPv6地址
|
||||
NSMutableArray<NSString *> *ipv4Array = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *ipv6Array = [NSMutableArray array];
|
||||
|
||||
for (struct addrinfo *p = res; p != NULL; p = p->ai_next) {
|
||||
if (p->ai_family == AF_INET || p->ai_family == AF_INET6) {
|
||||
char hostBuffer[NI_MAXHOST];
|
||||
memset(hostBuffer, 0, sizeof(hostBuffer));
|
||||
|
||||
if (getnameinfo(p->ai_addr, (socklen_t)p->ai_addrlen,
|
||||
hostBuffer, sizeof(hostBuffer),
|
||||
NULL, 0, NI_NUMERICHOST) == 0) {
|
||||
NSString *ipString = [NSString stringWithUTF8String:hostBuffer];
|
||||
if (p->ai_family == AF_INET) {
|
||||
[ipv4Array addObject:ipString];
|
||||
} else {
|
||||
[ipv6Array addObject:ipString];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
|
||||
// 5. 根据queryIpType确定保留哪些IP类型
|
||||
BOOL wantIPv4 = NO;
|
||||
BOOL wantIPv6 = NO;
|
||||
|
||||
switch (request.queryIpType) {
|
||||
case HttpdnsQueryIPTypeAuto:
|
||||
// Auto模式:如果有IPv4则始终返回,如果有IPv6则也包含
|
||||
// 无条件设置wantIPv4为YES
|
||||
wantIPv4 = YES;
|
||||
// 如果DNS返回了IPv6地址,则也包含IPv6
|
||||
wantIPv6 = (ipv6Array.count > 0);
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeIpv4:
|
||||
wantIPv4 = YES;
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeIpv6:
|
||||
wantIPv6 = YES;
|
||||
break;
|
||||
|
||||
case HttpdnsQueryIPTypeBoth:
|
||||
wantIPv4 = YES;
|
||||
wantIPv6 = YES;
|
||||
break;
|
||||
}
|
||||
|
||||
// 6. 构建最终的HttpdnsIpObject数组
|
||||
NSMutableArray<HttpdnsIpObject *> *v4IpObjects = [NSMutableArray array];
|
||||
NSMutableArray<HttpdnsIpObject *> *v6IpObjects = [NSMutableArray array];
|
||||
|
||||
if (wantIPv4) {
|
||||
for (NSString *ipStr in ipv4Array) {
|
||||
HttpdnsIpObject *ipObj = [[HttpdnsIpObject alloc] init];
|
||||
[ipObj setIp:ipStr]; // ipObj.ip = ipStr
|
||||
// connectedRT默认<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];
|
||||
|
||||
// IPv4和IPv6的默认TTL<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];
|
||||
|
||||
// 标记是否没有IPv4或IPv6记录
|
||||
[hostObject setHasNoIpv4Record:(v4IpObjects.count == 0)];
|
||||
[hostObject setHasNoIpv6Record:(v6IpObjects.count == 0)];
|
||||
|
||||
// 如果需要,可以在这里设置clientIp或额外字<EFBFBD><EFBFBD>?
|
||||
// 现在保留为默认<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
|
||||
|
||||
return hostObject;
|
||||
}
|
||||
|
||||
@end
|
||||
28
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRemoteResolver.h
Normal file
28
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRemoteResolver.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@interface HttpdnsRemoteResolver : NSObject
|
||||
|
||||
- (NSArray<HttpdnsHostObject *> *)resolve:(HttpdnsRequest *)request error:(NSError **)error;
|
||||
|
||||
@end
|
||||
710
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRemoteResolver.m
Normal file
710
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRemoteResolver.m
Normal file
@@ -0,0 +1,710 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
#import "HttpdnsRequest.h"
|
||||
#import "HttpdnsRemoteResolver.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsReachability.h"
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
#import <stdint.h>
|
||||
|
||||
|
||||
static dispatch_queue_t _streamOperateSyncQueue = 0;
|
||||
|
||||
@interface HttpdnsRemoteResolver () <NSStreamDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSRunLoop *runloop;
|
||||
@property (nonatomic, strong) NSError *networkError;
|
||||
@property (nonatomic, weak) HttpDnsService *service;
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPClient *httpClient;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HttpdnsRemoteResolver {
|
||||
NSMutableData *_resultData;
|
||||
NSInputStream *_inputStream;
|
||||
BOOL _responseResolved;
|
||||
BOOL _compeleted;
|
||||
NSTimer *_timeoutTimer;
|
||||
NSDictionary *_httpJSONDict;
|
||||
}
|
||||
|
||||
#pragma mark init
|
||||
|
||||
+ (void)initialize {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_streamOperateSyncQueue = dispatch_queue_create("com.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;
|
||||
}
|
||||
|
||||
// 将Base64字符串转为NSData
|
||||
NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:data options:0];
|
||||
if (!encryptedData || encryptedData.length <= 16) {
|
||||
HttpdnsLogDebug("Invalid encrypted data");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 从secretKey转换为二进制密钥
|
||||
NSData *keyData = [HttpdnsUtil dataFromHexString:aesSecretKey];
|
||||
if (!keyData) {
|
||||
HttpdnsLogDebug("Invalid AES key format");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 使用工具类解<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];
|
||||
|
||||
// 设置IPv4的TTL
|
||||
[self setTTLForHostObject:hostObject fromData:v4Data forIPv6:NO];
|
||||
|
||||
// 处理v4的extra字段,优先使<EFBFBD><EFBFBD>?
|
||||
[self processExtraInfo:v4Data forHostObject:hostObject];
|
||||
|
||||
// 检查是否有no_ip_code字段,表示无IPv4记录
|
||||
if ([[v4Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
|
||||
hostObject.hasNoIpv4Record = YES;
|
||||
}
|
||||
} else {
|
||||
// 没有IPv4地址但有v4节点,可能是无记<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];
|
||||
|
||||
// 设置IPv6的TTL
|
||||
[self setTTLForHostObject:hostObject fromData:v6Data forIPv6:YES];
|
||||
|
||||
// 只有在没有v4 extra的情况下才使用v6的extra
|
||||
if (![hostObject getExtra]) {
|
||||
[self processExtraInfo:v6Data forHostObject:hostObject];
|
||||
}
|
||||
|
||||
// 检查是否有no_ip_code字段,表示无IPv6记录
|
||||
if ([[v6Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
|
||||
hostObject.hasNoIpv6Record = YES;
|
||||
}
|
||||
} else {
|
||||
// 没有IPv6地址但有v6节点,可能是无记<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;
|
||||
}
|
||||
|
||||
// 目前在OC中没有比较好的实现AES-GCM的方式,因此这里选择AES-CBC加密
|
||||
NSData *encryptedData = [HttpdnsUtil encryptDataAESCBC:jsonData withKey:keyData error:&error];
|
||||
if (error) {
|
||||
HttpdnsLogDebug("Failed to encrypt data: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 将加密结果转为十六进制字符串
|
||||
return [HttpdnsUtil hexStringFromData:encryptedData];
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
- (NSString *)calculateSignatureForParams:(NSDictionary *)params withSecretKey:(NSString *)secretKey {
|
||||
if (![HttpdnsUtil isNotEmptyString:secretKey]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 按照签名要求对参数进行排序并生成签名内容
|
||||
NSArray *sortedKeys = [[params allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSMutableArray *signParts = [NSMutableArray array];
|
||||
|
||||
for (NSString *key in sortedKeys) {
|
||||
[signParts addObject:[NSString stringWithFormat:@"%@=%@", key, [params objectForKey:key]]];
|
||||
}
|
||||
|
||||
// 组合签名字符<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
|
||||
// 将extra字段转换为NSString类型
|
||||
- (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
|
||||
66
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRequestManager.h
Normal file
66
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRequestManager.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpdnsRequest.h"
|
||||
|
||||
@class HttpDnsService;
|
||||
|
||||
|
||||
@class HttpdnsHostObject;
|
||||
|
||||
@interface HttpdnsRequestManager : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) NSInteger accountId;
|
||||
|
||||
- (instancetype)initWithAccountId:(NSInteger)accountId ownerService:(HttpDnsService *)service;
|
||||
|
||||
- (void)setExpiredIPEnabled:(BOOL)enable;
|
||||
|
||||
- (void)setCachedIPEnabled:(BOOL)enable discardRecordsHasExpiredFor:(NSTimeInterval)duration;
|
||||
|
||||
- (void)setDegradeToLocalDNSEnabled:(BOOL)enable;
|
||||
|
||||
- (void)setPreResolveAfterNetworkChanged:(BOOL)enable;
|
||||
|
||||
- (void)preResolveHosts:(NSArray *)hosts queryType:(HttpdnsQueryIPType)queryType;
|
||||
|
||||
- (HttpdnsHostObject *)resolveHost:(HttpdnsRequest *)request;
|
||||
|
||||
// 内部缓存开关,不触发加载DB到内存的操作
|
||||
- (void)setPersistentCacheIpEnabled:(BOOL)enable;
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfHostArray:(NSArray<NSString *> *)hostArray;
|
||||
|
||||
- (void)cleanMemoryAndPersistentCacheOfAllHosts;
|
||||
|
||||
|
||||
#pragma mark - Expose to Testcases
|
||||
|
||||
- (HttpdnsHostObject *)mergeLookupResultToManager:(HttpdnsHostObject *)result host:host cacheKey:(NSString *)cacheKey underQueryIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
- (HttpdnsHostObject *)executeRequest:(HttpdnsRequest *)request retryCount:(int)hasRetryedCount;
|
||||
|
||||
- (NSString *)showMemoryCache;
|
||||
|
||||
- (void)cleanAllHostMemoryCache;
|
||||
|
||||
- (void)syncLoadCacheFromDbToMemory;
|
||||
|
||||
@end
|
||||
645
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRequestManager.m
Normal file
645
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsRequestManager.m
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsRequestManager.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsRemoteResolver.h"
|
||||
#import "HttpdnsLocalResolver.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsScheduleCenter.h"
|
||||
#import "HttpdnsService_Internal.h"
|
||||
#import "HttpdnsReachability.h"
|
||||
#import "HttpdnsHostRecord.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpDnsLocker.h"
|
||||
#import "HttpdnsRequest_Internal.h"
|
||||
#import "HttpdnsHostObjectInMemoryCache.h"
|
||||
#import "HttpdnsIPQualityDetector.h"
|
||||
#import "HttpdnsIpStackDetector.h"
|
||||
#import "HttpdnsDB.h"
|
||||
|
||||
|
||||
static dispatch_queue_t _persistentCacheConcurrentQueue = NULL;
|
||||
static dispatch_queue_t _asyncResolveHostQueue = NULL;
|
||||
|
||||
typedef struct {
|
||||
BOOL isResultUsable;
|
||||
BOOL isResolvingRequired;
|
||||
} HostObjectExamingResult;
|
||||
|
||||
@interface HttpdnsRequestManager()
|
||||
|
||||
@property (nonatomic, strong) dispatch_queue_t cacheQueue;
|
||||
@property (nonatomic, assign, readwrite) NSInteger accountId;
|
||||
@property (nonatomic, weak) HttpDnsService *ownerService;
|
||||
|
||||
@property (atomic, setter=setPersistentCacheIpEnabled:, assign) BOOL persistentCacheIpEnabled;
|
||||
@property (atomic, setter=setDegradeToLocalDNSEnabled:, assign) BOOL degradeToLocalDNSEnabled;
|
||||
@property (atomic, assign) BOOL atomicExpiredIPEnabled;
|
||||
@property (atomic, assign) BOOL atomicPreResolveAfterNetworkChanged;
|
||||
|
||||
@property (atomic, assign) NSTimeInterval lastUpdateTimestamp;
|
||||
@property (atomic, assign) HttpdnsNetworkStatus lastNetworkStatus;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsRequestManager {
|
||||
HttpdnsHostObjectInMemoryCache *_hostObjectInMemoryCache;
|
||||
HttpdnsDB *_httpdnsDB;
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_persistentCacheConcurrentQueue = dispatch_queue_create("com.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];
|
||||
}
|
||||
// 缓存是以cacheKey为准,这里返回前,要把host替换成用户请求的这个
|
||||
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之后,返回的应当是存储在缓存中的实际对象,而非请求过程中构造出来的对象
|
||||
// 预解析不支持SDNS,所以cacheKey只能是单独的每一个hostName
|
||||
[self mergeLookupResultToManager:result host:result.hostName cacheKey:result.hostName underQueryIpType:queryIPType];
|
||||
}
|
||||
}
|
||||
|
||||
- (HttpdnsHostObject *)mergeLookupResultToManager:(HttpdnsHostObject *)result host:host cacheKey:(NSString *)cacheKey underQueryIpType:(HttpdnsQueryIPType)queryIpType {
|
||||
if (!result) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray<HttpdnsIpObject *> *v4IpObjects = [result getV4Ips];
|
||||
NSArray<HttpdnsIpObject *> *v6IpObjects = [result getV6Ips];
|
||||
NSString* extra = [result getExtra];
|
||||
|
||||
BOOL hasNoIpv4Record = NO;
|
||||
BOOL hasNoIpv6Record = NO;
|
||||
if (queryIpType & HttpdnsQueryIPTypeIpv4 && [HttpdnsUtil isEmptyArray:v4IpObjects]) {
|
||||
hasNoIpv4Record = YES;
|
||||
}
|
||||
if (queryIpType & HttpdnsQueryIPTypeIpv6 && [HttpdnsUtil isEmptyArray:v6IpObjects]) {
|
||||
hasNoIpv6Record = YES;
|
||||
}
|
||||
|
||||
HttpdnsHostObject *cachedHostObject = [_hostObjectInMemoryCache getHostObjectByCacheKey:cacheKey];
|
||||
if (!cachedHostObject) {
|
||||
HttpdnsLogDebug("Create new hostObject for cache, cacheKey: %@, host: %@", cacheKey, host);
|
||||
cachedHostObject = [[HttpdnsHostObject alloc] init];
|
||||
}
|
||||
|
||||
[cachedHostObject setCacheKey:cacheKey];
|
||||
[cachedHostObject setClientIp:result.clientIp];
|
||||
|
||||
[cachedHostObject setHostName:host];
|
||||
[cachedHostObject setIsLoadFromDB:NO];
|
||||
[cachedHostObject setHasNoIpv4Record:hasNoIpv4Record];
|
||||
[cachedHostObject setHasNoIpv6Record:hasNoIpv6Record];
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v4IpObjects]) {
|
||||
[cachedHostObject setV4Ips:v4IpObjects];
|
||||
[cachedHostObject setV4TTL:result.getV4TTL];
|
||||
[cachedHostObject setLastIPv4LookupTime:result.lastIPv4LookupTime];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6IpObjects]) {
|
||||
[cachedHostObject setV6Ips:v6IpObjects];
|
||||
[cachedHostObject setV6TTL:result.getV6TTL];
|
||||
[cachedHostObject setLastIPv6LookupTime:result.lastIPv6LookupTime];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyString:extra]) {
|
||||
[cachedHostObject setExtra:extra];
|
||||
}
|
||||
|
||||
HttpdnsLogDebug("Updated hostObject to cached, cacheKey: %@, host: %@", cacheKey, host);
|
||||
|
||||
// 由于从缓存中读取到的是拷贝出来的新对象,字段赋值不会影响缓存中的值对象,因此这里无论如何都要放回缓存
|
||||
[_hostObjectInMemoryCache setHostObject:cachedHostObject forCacheKey:cacheKey];
|
||||
|
||||
[self persistToDB:cacheKey hostObject:cachedHostObject];
|
||||
|
||||
NSArray *ipv4StrArray = [cachedHostObject getV4IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:ipv4StrArray]) {
|
||||
[self initiateQualityDetectionForIP:ipv4StrArray forHost:host cacheKey:cacheKey];
|
||||
}
|
||||
|
||||
NSArray *ipv6StrArray = [cachedHostObject getV6IpStrings];
|
||||
if ([HttpdnsUtil isNotEmptyArray:ipv6StrArray]) {
|
||||
[self initiateQualityDetectionForIP:ipv6StrArray forHost:host cacheKey:cacheKey];
|
||||
}
|
||||
return cachedHostObject;
|
||||
}
|
||||
|
||||
- (void)initiateQualityDetectionForIP:(NSArray *)ipArray forHost:(NSString *)host cacheKey:(NSString *)cacheKey {
|
||||
HttpDnsService *service = self.ownerService ?: [HttpDnsService sharedInstance];
|
||||
NSDictionary<NSString *, NSNumber *> *dataSource = [service getIPRankingDatasource];
|
||||
if (!dataSource || ![dataSource objectForKey:host]) {
|
||||
return;
|
||||
}
|
||||
NSNumber *port = [dataSource objectForKey:host];
|
||||
for (NSString *ip in ipArray) {
|
||||
[[HttpdnsIPQualityDetector sharedInstance] scheduleIPQualityDetection:cacheKey
|
||||
ip:ip
|
||||
port:port
|
||||
callback:^(NSString * _Nonnull cacheKey, NSString * _Nonnull ip, NSInteger costTime) {
|
||||
[self->_hostObjectInMemoryCache updateQualityForCacheKey:cacheKey forIp:ip withConnectedRT:costTime];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isHostsNumberLimitReached {
|
||||
if ([_hostObjectInMemoryCache count] >= HTTPDNS_MAX_MANAGE_HOST_NUM) {
|
||||
HttpdnsLogDebug("Can't handle more than %d hosts due to the software configuration.", HTTPDNS_MAX_MANAGE_HOST_NUM);
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)handleReachabilityNotification:(NSNotification *)notification {
|
||||
[self networkChanged];
|
||||
}
|
||||
|
||||
- (void)networkChanged {
|
||||
HttpdnsNetworkStatus currentStatus = [[HttpdnsReachability sharedInstance] currentReachabilityStatus];
|
||||
NSString *currentStatusString = [[HttpdnsReachability sharedInstance] currentReachabilityString];
|
||||
|
||||
// 重新检测协议栈代价小,所以只要网络切换就发起检<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
|
||||
481
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService.h
Normal file
481
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService.h
Normal file
@@ -0,0 +1,481 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
// 头文件包含需使用相对目录,确保通过 CocoaPods 安装后能被模块化编译找到
|
||||
// #import "HttpdnsRequest.h"
|
||||
// #import "HttpdnsResult.h"
|
||||
// #import "HttpdnsDegradationDelegate.h"
|
||||
// #import "HttpdnsLoggerProtocol.h"
|
||||
|
||||
#import <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 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
/// 同步解析域名,会阻塞当前线程,直到从缓存中获取到有效解析结果,或者从服务器拿到最新解析结<E69E90><E7BB93>?
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先返回这个结果,然后启动后台线程去更新解析结<E69E90><E7BB93>?
|
||||
/// 为了防止在主线程中误用本接口导致APP卡顿,本接口会做检测,若发现调用线程是主线程,则自动降级到resolveHostSyncNonBlocking接口的实现逻辑<E980BB><E8BE91>?
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(NSString *)cacheKey;
|
||||
|
||||
/// 同步解析域名,会阻塞当前线程,直到从缓存中获取到有效解析结果,或者从服务器拿到最新解析结<E69E90><E7BB93>?
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先返回这个结果,然后启动后台线程去更新解析结<E69E90><E7BB93>?
|
||||
/// 为了防止在主线程中误用本接口导致APP卡顿,本接口会做检测,若发现调用线程是主线程,则自动降级到resolveHostSyncNonBlocking接口的实现逻辑<E980BB><E8BE91>?
|
||||
/// @param request 请求参数对象
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSync:(HttpdnsRequest *)request;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结<E69E90><E7BB93>?
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结<E69E90><E7BB93>?
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(nullable NSString *)cacheKey completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 异步解析域名,不会阻塞当前线程,会在从缓存中获取到有效结果,或从服务器拿到最新解析结果后,通过回调返回结果
|
||||
/// 如果允许复用过期的解析结果且存在过期结果的情况下,会先在回调中返回这个结果,然后启动后台线程去更新解析结<E69E90><E7BB93>?
|
||||
/// @param request 请求参数对象
|
||||
/// @handler 解析结果回调
|
||||
- (void)resolveHostAsync:(HttpdnsRequest *)request completionHandler:(void (^)(HttpdnsResult * nullable))handler;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为<E883BD><E4B8BA>?
|
||||
/// 先查询缓存,缓存中存在有效结<E69588><E7BB93>?未过期,或者过期但配置了可以复用过期解析结<E69E90><E7BB93>?,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为<E883BD><E4B8BA>?
|
||||
/// 先查询缓存,缓存中存在有效结<E69588><E7BB93>?未过期,或者过期但配置了可以复用过期解析结<E69E90><E7BB93>?,则直接返回结果,如果缓存未命中,则发起异步解析请求
|
||||
/// @param host 需要解析的域名
|
||||
/// @param queryIpType 可设置为自动选择,ipv4,ipv6. 设置为自动选择时,会自动根据当前所处网络环境选择解析ipv4或ipv6
|
||||
/// @param sdnsParams 如果域名配置了sdns自定义解析,通过此参数携带自定义参数
|
||||
/// @param cacheKey sdns自定义解析缓存key
|
||||
/// @return 解析结果
|
||||
- (nullable HttpdnsResult *)resolveHostSyncNonBlocking:(NSString *)host byIpType:(HttpdnsQueryIPType)queryIpType withSdnsParams:(nullable NSDictionary<NSString *, NSString *> *)sdnsParams sdnsCacheKey:(nullable NSString *)cacheKey;
|
||||
|
||||
/// 伪异步解析域名,不会阻塞当前线程,首次解析结果可能为<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
|
||||
1250
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService.m
Normal file
1250
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService.m
Normal file
File diff suppressed because it is too large
Load Diff
51
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService_Internal.h
Normal file
51
HttpDNSSDK/sdk/ios/NewHttpDNS/HttpdnsService_Internal.h
Normal 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
|
||||
26
HttpDNSSDK/sdk/ios/NewHttpDNS/Info.plist
Normal file
26
HttpDNSSDK/sdk/ios/NewHttpDNS/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// HttpdnsIpStackDetector.h
|
||||
// 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
|
||||
185
HttpDNSSDK/sdk/ios/NewHttpDNS/IpStack/HttpdnsIpStackDetector.m
Normal file
185
HttpDNSSDK/sdk/ios/NewHttpDNS/IpStack/HttpdnsIpStackDetector.m
Normal 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套接字到指定的单播地址。这不会产生网络流量,
|
||||
* 但如果系统对目标没有或有限的可达性(例如,没有IPv4地址,没有IPv6默认路由等),
|
||||
* 将快速失败。
|
||||
*/
|
||||
static const unsigned int kMaxLoopCount = 10;
|
||||
|
||||
static int httpdns_test_connect(int pf, struct sockaddr * addr, size_t addrlen) {
|
||||
int s = socket(pf, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s < 0) {
|
||||
return 0;
|
||||
}
|
||||
int ret;
|
||||
unsigned int loop_count = 0;
|
||||
do {
|
||||
ret = connect(s, addr, (socklen_t)addrlen);
|
||||
} while (ret < 0 && errno == EINTR && loop_count++ < kMaxLoopCount);
|
||||
if (loop_count >= kMaxLoopCount) {
|
||||
HttpdnsLogDebug("connect error. loop_count = %d", loop_count);
|
||||
}
|
||||
int success = (ret == 0);
|
||||
loop_count = 0;
|
||||
do {
|
||||
ret = close(s);
|
||||
} while (ret < 0 && errno == EINTR && loop_count++ < kMaxLoopCount);
|
||||
if (loop_count >= kMaxLoopCount) {
|
||||
HttpdnsLogDebug("close error. loop_count = %d", loop_count);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/*
|
||||
* 以下函数用于确定IPv4或IPv6连接是否可用,以实现AI_ADDRCONFIG。
|
||||
*
|
||||
* 严格来说,AI_ADDRCONFIG不应该检查连接是否可用,
|
||||
* 而是检查指定协议族的地址是否"在本地系统上配置"。
|
||||
* 然而,bionic目前不支持getifaddrs,
|
||||
* 所以检查连接是下一个最佳选择。
|
||||
*/
|
||||
static int httpdns_have_ipv6(void) {
|
||||
static struct sockaddr_in6 sin6_test = {0};
|
||||
sin6_test.sin6_family = AF_INET6;
|
||||
sin6_test.sin6_port = 80;
|
||||
sin6_test.sin6_flowinfo = 0;
|
||||
sin6_test.sin6_scope_id = 0;
|
||||
bzero(sin6_test.sin6_addr.s6_addr, sizeof(sin6_test.sin6_addr.s6_addr));
|
||||
sin6_test.sin6_addr.s6_addr[0] = 0x20;
|
||||
// union
|
||||
httpdns_sockaddr_union addr = {.httpdns_in6 = sin6_test};
|
||||
return httpdns_test_connect(PF_INET6, &addr.httpdns_generic, sizeof(addr.httpdns_in6));
|
||||
}
|
||||
|
||||
static int httpdns_have_ipv4(void) {
|
||||
static struct sockaddr_in sin_test = {0};
|
||||
sin_test.sin_family = AF_INET;
|
||||
sin_test.sin_port = 80;
|
||||
sin_test.sin_addr.s_addr = htonl(0x08080808L); // 8.8.8.8
|
||||
// union
|
||||
httpdns_sockaddr_union addr = {.httpdns_in = sin_test};
|
||||
return httpdns_test_connect(PF_INET, &addr.httpdns_generic, sizeof(addr.httpdns_in));
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于IPv4和IPv6连接检测当前IP协议栈类型
|
||||
*/
|
||||
static HttpdnsIPStackType detectIpStack(void) {
|
||||
int hasIPv4 = httpdns_have_ipv4();
|
||||
int hasIPv6 = httpdns_have_ipv6();
|
||||
|
||||
HttpdnsLogDebug("IP stack detection: IPv4=%d, IPv6=%d", hasIPv4, hasIPv6);
|
||||
|
||||
if (hasIPv4 && hasIPv6) {
|
||||
return kHttpdnsIpDual;
|
||||
} else if (hasIPv4) {
|
||||
return kHttpdnsIpv4Only;
|
||||
} else if (hasIPv6) {
|
||||
return kHttpdnsIpv6Only;
|
||||
} else {
|
||||
return kHttpdnsIpUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
@implementation HttpdnsIpStackDetector {
|
||||
HttpdnsIPStackType _lastDetectedIpStack;
|
||||
dispatch_queue_t _detectSerialQueue; // 用于控制检测操作的串行队列
|
||||
dispatch_queue_t _updateSerialQueue;
|
||||
BOOL _isDetecting; // 标记是否正在进行检测
|
||||
NSTimeInterval _lastDetectionTime; // 上次检测的时间戳
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static HttpdnsIpStackDetector *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lastDetectedIpStack = kHttpdnsIpUnknown;
|
||||
_isDetecting = NO;
|
||||
_lastDetectionTime = 0;
|
||||
// 创建串行队列用于控制IP栈检测的并发
|
||||
_detectSerialQueue = dispatch_queue_create("com.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
|
||||
30
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog.h
Normal file
30
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsLog : NSObject
|
||||
|
||||
+ (void)enableLog;
|
||||
|
||||
+ (void)disableLog;
|
||||
|
||||
+ (BOOL)isEnabled;
|
||||
|
||||
@end
|
||||
73
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog.m
Normal file
73
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog.m
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
|
||||
static BOOL HttpdnsLogIsEnabled = NO;
|
||||
|
||||
static id<HttpdnsLoggerProtocol> sLogHandler;
|
||||
|
||||
@implementation HttpdnsLog
|
||||
|
||||
+ (void)enableLog {
|
||||
HttpdnsLogIsEnabled = YES;
|
||||
}
|
||||
|
||||
+ (void)disableLog {
|
||||
HttpdnsLogIsEnabled = NO;
|
||||
}
|
||||
|
||||
+ (BOOL)isEnabled {
|
||||
return HttpdnsLogIsEnabled;
|
||||
}
|
||||
|
||||
+ (void)setLogHandler:(id<HttpdnsLoggerProtocol>)handler {
|
||||
SEL sel = NSSelectorFromString(@"log:");
|
||||
if (handler && [handler respondsToSelector:sel]) {
|
||||
sLogHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)unsetLogHandler {
|
||||
sLogHandler = nil;
|
||||
}
|
||||
|
||||
+ (BOOL)validLogHandler {
|
||||
return (sLogHandler != nil);
|
||||
}
|
||||
|
||||
+ (void)outputToLogHandler:(NSString *)logStr {
|
||||
if (sLogHandler) {
|
||||
[sLogHandler log:logStr];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)getFormattedDateTimeStr {
|
||||
static NSDateFormatter *dateFormatter;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [[NSDateFormatter alloc] init];
|
||||
});
|
||||
|
||||
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];
|
||||
return [dateFormatter stringFromDate:[NSDate date]];
|
||||
}
|
||||
|
||||
@end
|
||||
45
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog_Internal.h
Normal file
45
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLog_Internal.h
Normal 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
|
||||
20
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLoggerProtocol.h
Normal file
20
HttpDNSSDK/sdk/ios/NewHttpDNS/Log/HttpdnsLoggerProtocol.h
Normal 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 */
|
||||
90
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostObject.h
Normal file
90
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostObject.h
Normal 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
|
||||
306
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostObject.m
Normal file
306
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostObject.m
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "HttpdnsHostRecord.h"
|
||||
#import "HttpdnsIPQualityDetector.h"
|
||||
|
||||
|
||||
@implementation HttpdnsIpObject
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
// 初始化connectedRT为最大整数<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) {
|
||||
// 注意,_hasNoIpv4Record为true时,说明域名没有配置ipv4ip,不是需要去请求的情<EFBFBD><EFBFBD>?
|
||||
if ([HttpdnsUtil isEmptyArray:[self getV4Ips]] && !_hasNoIpv4Record) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
} else if (queryType & HttpdnsQueryIPTypeIpv6 && !_hasNoIpv6Record) {
|
||||
// 注意,_hasNoIpv6Record为true时,说明域名没有配置ipv6ip,不是需要去请求的情<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 {
|
||||
// 将IP对象数组转换为IP字符串数<EFBFBD><EFBFBD>?
|
||||
NSArray<NSString *> *v4IpStrings = [self getV4IpStrings];
|
||||
NSArray<NSString *> *v6IpStrings = [self getV6IpStrings];
|
||||
|
||||
// 创建当前时间作为modifyAt
|
||||
NSDate *currentDate = [NSDate date];
|
||||
|
||||
// 使用hostName作为cacheKey,保持与fromDBRecord方法的一致<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;
|
||||
}
|
||||
|
||||
// 查找匹配的IP对象并更新connectedRT
|
||||
BOOL found = NO;
|
||||
NSMutableArray<HttpdnsIpObject *> *mutableIpObjects = [ipObjects mutableCopy];
|
||||
|
||||
for (HttpdnsIpObject *ipObject in ipObjects) {
|
||||
if ([ipObject.ip isEqualToString:ip]) {
|
||||
ipObject.connectedRT = connectedRT;
|
||||
found = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据connectedRT值对IP列表进行排序<EFBFBD><EFBFBD>?1值放在最<EFBFBD><EFBFBD>?
|
||||
[mutableIpObjects sortUsingComparator:^NSComparisonResult(HttpdnsIpObject *obj1, HttpdnsIpObject *obj2) {
|
||||
// 如果obj1的connectedRT<EFBFBD><EFBFBD>?1,将其排在后<EFBFBD><EFBFBD>?
|
||||
if (obj1.connectedRT == -1) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
// 如果obj2的connectedRT<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
|
||||
53
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostRecord.h
Normal file
53
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostRecord.h
Normal 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
|
||||
91
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostRecord.m
Normal file
91
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsHostRecord.m
Normal 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
|
||||
64
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsRequest.h
Normal file
64
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsRequest.h
Normal 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
|
||||
76
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsRequest.m
Normal file
76
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsRequest.m
Normal 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
|
||||
@@ -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 */
|
||||
42
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsResult.h
Normal file
42
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsResult.h
Normal 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
|
||||
53
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsResult.m
Normal file
53
HttpDNSSDK/sdk/ios/NewHttpDNS/Model/HttpdnsResult.m
Normal 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
|
||||
42
HttpDNSSDK/sdk/ios/NewHttpDNS/Network/HttpdnsNWHTTPClient.h
Normal file
42
HttpDNSSDK/sdk/ios/NewHttpDNS/Network/HttpdnsNWHTTPClient.h
Normal 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
|
||||
834
HttpDNSSDK/sdk/ios/NewHttpDNS/Network/HttpdnsNWHTTPClient.m
Normal file
834
HttpDNSSDK/sdk/ios/NewHttpDNS/Network/HttpdnsNWHTTPClient.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
30
HttpDNSSDK/sdk/ios/NewHttpDNS/NewHttpDNS.h
Normal file
30
HttpDNSSDK/sdk/ios/NewHttpDNS/NewHttpDNS.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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>
|
||||
75
HttpDNSSDK/sdk/ios/NewHttpDNS/Persistent/HttpdnsDB.h
Normal file
75
HttpDNSSDK/sdk/ios/NewHttpDNS/Persistent/HttpdnsDB.h
Normal 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
|
||||
608
HttpDNSSDK/sdk/ios/NewHttpDNS/Persistent/HttpdnsDB.m
Normal file
608
HttpDNSSDK/sdk/ios/NewHttpDNS/Persistent/HttpdnsDB.m
Normal 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) {
|
||||
// 更新记录,保留原始的createAt,更新modifyAt为当前时间
|
||||
recordToSave = [[HttpdnsHostRecord alloc] initWithId:record.id
|
||||
cacheKey:record.cacheKey
|
||||
hostName:record.hostName
|
||||
createAt:existingRecord.createAt // 保留原始createAt
|
||||
modifyAt:now // 更新modifyAt
|
||||
clientIp:record.clientIp
|
||||
v4ips:record.v4ips
|
||||
v4ttl:record.v4ttl
|
||||
v4LookupTime:record.v4LookupTime
|
||||
v6ips:record.v6ips
|
||||
v6ttl:record.v6ttl
|
||||
v6LookupTime:record.v6LookupTime
|
||||
extra:record.extra];
|
||||
} else {
|
||||
// 新记录,设置createAt和modifyAt为当前时间
|
||||
recordToSave = [[HttpdnsHostRecord alloc] initWithId:record.id
|
||||
cacheKey:record.cacheKey
|
||||
hostName:record.hostName
|
||||
createAt:now // 新记录的createAt
|
||||
modifyAt:now // 新记录的modifyAt
|
||||
clientIp:record.clientIp
|
||||
v4ips:record.v4ips
|
||||
v4ttl:record.v4ttl
|
||||
v4LookupTime:record.v4LookupTime
|
||||
v6ips:record.v6ips
|
||||
v6ttl:record.v6ttl
|
||||
v6LookupTime:record.v6LookupTime
|
||||
extra:record.extra];
|
||||
}
|
||||
|
||||
// 使用INSERT OR REPLACE语法保存记录
|
||||
result = [self saveRecord:recordToSave];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (nullable HttpdnsHostRecord *)selectByCacheKey:(NSString *)cacheKey {
|
||||
if (!cacheKey) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block HttpdnsHostRecord *record = nil;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
record = [self selectByCacheKeyInternal:cacheKey];
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
- (BOOL)deleteByCacheKey:(NSString *)cacheKey {
|
||||
if (!cacheKey) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__block BOOL result = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, [cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
result = (sqlite3_step(stmt) == SQLITE_DONE);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSInteger)deleteByHostNameArr:(NSArray<NSString *> *)hostNameArr {
|
||||
if (!hostNameArr || hostNameArr.count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 过滤掉空值
|
||||
NSMutableArray *validHostNames = [NSMutableArray array];
|
||||
for (NSString *hostname in hostNameArr) {
|
||||
if (hostname && hostname.length > 0) {
|
||||
[validHostNames addObject:hostname];
|
||||
}
|
||||
}
|
||||
|
||||
if (validHostNames.count == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
__block NSInteger deletedCount = 0;
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
// 构建IN子句的占位符
|
||||
NSMutableString *placeholders = [NSMutableString string];
|
||||
for (NSUInteger i = 0; i < validHostNames.count; i++) {
|
||||
[placeholders appendString:@"?"];
|
||||
if (i < validHostNames.count - 1) {
|
||||
[placeholders appendString:@","];
|
||||
}
|
||||
}
|
||||
|
||||
// 构建SQL语句
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (%@)",
|
||||
kTableName, kColumnHostName, placeholders];
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
// 绑定所有参数
|
||||
for (NSUInteger i = 0; i < validHostNames.count; i++) {
|
||||
sqlite3_bind_text(stmt, (int)(i + 1), [validHostNames[i] UTF8String], -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
deletedCount = sqlite3_changes(_db);
|
||||
} else {
|
||||
NSLog(@"Failed to delete records: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
} else {
|
||||
NSLog(@"Failed to prepare delete statement: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
});
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
- (BOOL)deleteAll {
|
||||
__block BOOL result = NO;
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@", kTableName];
|
||||
char *errMsg;
|
||||
|
||||
result = (sqlite3_exec(_db, [sql UTF8String], NULL, NULL, &errMsg) == SQLITE_OK);
|
||||
|
||||
if (errMsg) {
|
||||
NSLog(@"Failed to delete all records: %s", errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<HttpdnsHostRecord *> *)getAllRecords {
|
||||
__block NSMutableArray<HttpdnsHostRecord *> *records = [NSMutableArray array];
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@", kTableName];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
HttpdnsHostRecord *record = [self recordFromStatement:stmt];
|
||||
if (record) {
|
||||
[records addObject:record];
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
} else {
|
||||
NSLog(@"Failed to prepare getAllRecords statement: %s", sqlite3_errmsg(_db));
|
||||
}
|
||||
});
|
||||
|
||||
return [records copy];
|
||||
}
|
||||
|
||||
- (NSInteger)cleanRecordAlreadExpiredAt:(NSTimeInterval)specifiedTime {
|
||||
__block NSInteger cleanedCount = 0;
|
||||
|
||||
// 获取所有记录
|
||||
NSArray<HttpdnsHostRecord *> *allRecords = [self getAllRecords];
|
||||
|
||||
dispatch_sync(_dbQueue, ^{
|
||||
for (HttpdnsHostRecord *record in allRecords) {
|
||||
BOOL v4Expired = NO;
|
||||
BOOL v6Expired = NO;
|
||||
|
||||
// 检查IPv4记录是否过期
|
||||
if (record.v4LookupTime + record.v4ttl <= specifiedTime) {
|
||||
v4Expired = YES;
|
||||
}
|
||||
|
||||
// 检查IPv6记录是否过期
|
||||
if (record.v6LookupTime + record.v6ttl <= specifiedTime) {
|
||||
v6Expired = YES;
|
||||
}
|
||||
|
||||
// 如果两种IP类型都过期,删除整条记录
|
||||
if (v4Expired && v6Expired) {
|
||||
NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
// 如果只有一种IP类型过期,更新记录
|
||||
else if (v4Expired || v6Expired) {
|
||||
NSMutableArray<NSString *> *v4ips = [NSMutableArray arrayWithArray:record.v4ips];
|
||||
NSMutableArray<NSString *> *v6ips = [NSMutableArray arrayWithArray:record.v6ips];
|
||||
|
||||
// 如果IPv4过期,清空IPv4记录
|
||||
if (v4Expired) {
|
||||
[v4ips removeAllObjects];
|
||||
}
|
||||
|
||||
// 如果IPv6过期,清空IPv6记录
|
||||
if (v6Expired) {
|
||||
[v6ips removeAllObjects];
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"UPDATE %@ SET %@ = ?, %@ = ? WHERE %@ = ?",
|
||||
kTableName,
|
||||
kColumnV4Ips,
|
||||
kColumnV6Ips,
|
||||
kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) == SQLITE_OK) {
|
||||
// 绑定v4ips
|
||||
if (v4ips.count > 0) {
|
||||
NSString *v4ipsStr = [v4ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, 1, [v4ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, 1);
|
||||
}
|
||||
|
||||
// 绑定v6ips
|
||||
if (v6ips.count > 0) {
|
||||
NSString *v6ipsStr = [v6ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, 2, [v6ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, 2);
|
||||
}
|
||||
|
||||
// 绑定cacheKey
|
||||
sqlite3_bind_text(stmt, 3, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (BOOL)openDB {
|
||||
if (sqlite3_open([_dbPath UTF8String], &_db) != SQLITE_OK) {
|
||||
NSLog(@"Failed to open database: %s", sqlite3_errmsg(_db));
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 创建表
|
||||
return [self createTableIfNeeded];
|
||||
}
|
||||
|
||||
- (BOOL)createTableIfNeeded {
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"CREATE TABLE IF NOT EXISTS %@ ("
|
||||
@"%@ INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
@"%@ TEXT UNIQUE NOT NULL, "
|
||||
@"%@ TEXT NOT NULL, "
|
||||
@"%@ REAL, "
|
||||
@"%@ REAL, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ TEXT, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ INTEGER, "
|
||||
@"%@ TEXT"
|
||||
@")",
|
||||
kTableName,
|
||||
kColumnId,
|
||||
kColumnCacheKey,
|
||||
kColumnHostName,
|
||||
kColumnCreateAt,
|
||||
kColumnModifyAt,
|
||||
kColumnClientIp,
|
||||
kColumnV4Ips,
|
||||
kColumnV4Ttl,
|
||||
kColumnV4LookupTime,
|
||||
kColumnV6Ips,
|
||||
kColumnV6Ttl,
|
||||
kColumnV6LookupTime,
|
||||
kColumnExtra];
|
||||
|
||||
char *errMsg;
|
||||
if (sqlite3_exec(_db, [sql UTF8String], NULL, NULL, &errMsg) != SQLITE_OK) {
|
||||
NSLog(@"Failed to create table: %s", errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)saveRecord:(HttpdnsHostRecord *)record {
|
||||
// 使用INSERT OR REPLACE语法,如果记录存在则更新,不存在则插入
|
||||
NSString *sql = [NSString stringWithFormat:
|
||||
@"INSERT OR REPLACE INTO %@ ("
|
||||
@"%@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@) "
|
||||
@"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
kTableName,
|
||||
kColumnCacheKey,
|
||||
kColumnHostName,
|
||||
kColumnCreateAt,
|
||||
kColumnModifyAt,
|
||||
kColumnClientIp,
|
||||
kColumnV4Ips,
|
||||
kColumnV4Ttl,
|
||||
kColumnV4LookupTime,
|
||||
kColumnV6Ips,
|
||||
kColumnV6Ttl,
|
||||
kColumnV6LookupTime,
|
||||
kColumnExtra];
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
|
||||
NSLog(@"Failed to prepare save statement: %s", sqlite3_errmsg(_db));
|
||||
return NO;
|
||||
}
|
||||
|
||||
// 绑定参数
|
||||
int index = 1;
|
||||
|
||||
// 绑定cacheKey (唯一键)
|
||||
sqlite3_bind_text(stmt, index++, [record.cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
// 绑定hostName
|
||||
sqlite3_bind_text(stmt, index++, [record.hostName UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
// 绑定createAt
|
||||
if (record.createAt) {
|
||||
sqlite3_bind_double(stmt, index++, [record.createAt timeIntervalSince1970]);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定modifyAt
|
||||
if (record.modifyAt) {
|
||||
sqlite3_bind_double(stmt, index++, [record.modifyAt timeIntervalSince1970]);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定clientIp
|
||||
if (record.clientIp) {
|
||||
sqlite3_bind_text(stmt, index++, [record.clientIp UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v4ips
|
||||
if (record.v4ips.count > 0) {
|
||||
NSString *v4ipsStr = [record.v4ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, index++, [v4ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v4ttl
|
||||
sqlite3_bind_int64(stmt, index++, record.v4ttl);
|
||||
|
||||
// 绑定v4LookupTime
|
||||
sqlite3_bind_int64(stmt, index++, record.v4LookupTime);
|
||||
|
||||
// 绑定v6ips
|
||||
if (record.v6ips.count > 0) {
|
||||
NSString *v6ipsStr = [record.v6ips componentsJoinedByString:@","];
|
||||
sqlite3_bind_text(stmt, index++, [v6ipsStr UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
// 绑定v6ttl
|
||||
sqlite3_bind_int64(stmt, index++, record.v6ttl);
|
||||
|
||||
// 绑定v6LookupTime
|
||||
sqlite3_bind_int64(stmt, index++, record.v6LookupTime);
|
||||
|
||||
// 绑定extra
|
||||
if (record.extra) {
|
||||
sqlite3_bind_text(stmt, index++, [record.extra UTF8String], -1, SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, index++);
|
||||
}
|
||||
|
||||
BOOL result = (sqlite3_step(stmt) == SQLITE_DONE);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (HttpdnsHostRecord *)selectByCacheKeyInternal:(NSString *)cacheKey {
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@ = ?", kTableName, kColumnCacheKey];
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
|
||||
NSLog(@"Failed to prepare query statement: %s", sqlite3_errmsg(_db));
|
||||
return nil;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, [cacheKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
|
||||
HttpdnsHostRecord *record = nil;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
record = [self recordFromStatement:stmt];
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return record;
|
||||
}
|
||||
|
||||
- (HttpdnsHostRecord *)recordFromStatement:(sqlite3_stmt *)stmt {
|
||||
// 获取id
|
||||
NSUInteger recordId = (NSUInteger)sqlite3_column_int64(stmt, 0);
|
||||
|
||||
// 获取cacheKey
|
||||
const char *cacheKeyChars = (const char *)sqlite3_column_text(stmt, 1);
|
||||
NSString *cacheKey = cacheKeyChars ? [NSString stringWithUTF8String:cacheKeyChars] : nil;
|
||||
|
||||
// 获取hostName
|
||||
const char *hostNameChars = (const char *)sqlite3_column_text(stmt, 2);
|
||||
NSString *hostName = hostNameChars ? [NSString stringWithUTF8String:hostNameChars] : nil;
|
||||
|
||||
// 获取createAt
|
||||
NSDate *createAt = nil;
|
||||
if (sqlite3_column_type(stmt, 3) != SQLITE_NULL) {
|
||||
double createAtTimestamp = sqlite3_column_double(stmt, 3);
|
||||
createAt = [NSDate dateWithTimeIntervalSince1970:createAtTimestamp];
|
||||
}
|
||||
|
||||
// 获取modifyAt
|
||||
NSDate *modifyAt = nil;
|
||||
if (sqlite3_column_type(stmt, 4) != SQLITE_NULL) {
|
||||
double modifyAtTimestamp = sqlite3_column_double(stmt, 4);
|
||||
modifyAt = [NSDate dateWithTimeIntervalSince1970:modifyAtTimestamp];
|
||||
}
|
||||
|
||||
// 获取clientIp
|
||||
const char *clientIpChars = (const char *)sqlite3_column_text(stmt, 5);
|
||||
NSString *clientIp = clientIpChars ? [NSString stringWithUTF8String:clientIpChars] : nil;
|
||||
|
||||
// 获取v4ips
|
||||
NSArray<NSString *> *v4ips = nil;
|
||||
const char *v4ipsChars = (const char *)sqlite3_column_text(stmt, 6);
|
||||
if (v4ipsChars) {
|
||||
NSString *v4ipsStr = [NSString stringWithUTF8String:v4ipsChars];
|
||||
v4ips = [v4ipsStr componentsSeparatedByString:@","];
|
||||
} else {
|
||||
v4ips = @[];
|
||||
}
|
||||
|
||||
// 获取v4ttl
|
||||
int64_t v4ttl = sqlite3_column_int64(stmt, 7);
|
||||
|
||||
// 获取v4LookupTime
|
||||
int64_t v4LookupTime = sqlite3_column_int64(stmt, 8);
|
||||
|
||||
// 获取v6ips
|
||||
NSArray<NSString *> *v6ips = nil;
|
||||
const char *v6ipsChars = (const char *)sqlite3_column_text(stmt, 9);
|
||||
if (v6ipsChars) {
|
||||
NSString *v6ipsStr = [NSString stringWithUTF8String:v6ipsChars];
|
||||
v6ips = [v6ipsStr componentsSeparatedByString:@","];
|
||||
} else {
|
||||
v6ips = @[];
|
||||
}
|
||||
|
||||
// 获取v6ttl
|
||||
int64_t v6ttl = sqlite3_column_int64(stmt, 10);
|
||||
|
||||
// 获取v6LookupTime
|
||||
int64_t v6LookupTime = sqlite3_column_int64(stmt, 11);
|
||||
|
||||
// 获取extra
|
||||
NSString *extra = nil;
|
||||
const char *extraChars = (const char *)sqlite3_column_text(stmt, 12);
|
||||
if (extraChars) {
|
||||
extra = [NSString stringWithUTF8String:extraChars];
|
||||
}
|
||||
|
||||
// 创建记录对象
|
||||
return [[HttpdnsHostRecord alloc] initWithId:recordId
|
||||
cacheKey:cacheKey
|
||||
hostName:hostName
|
||||
createAt:createAt
|
||||
modifyAt:modifyAt
|
||||
clientIp:clientIp
|
||||
v4ips:v4ips
|
||||
v4ttl:v4ttl
|
||||
v4LookupTime:v4LookupTime
|
||||
v6ips:v6ips
|
||||
v6ttl:v6ttl
|
||||
v6LookupTime:v6LookupTime
|
||||
extra:extra];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpdnsPersistenceUtils : NSObject
|
||||
|
||||
+ (NSString *)httpdnsDataDirectory;
|
||||
+ (NSString *)scheduleCenterResultDirectory;
|
||||
/// 多账号隔离:返回指定账号的调度结果目<E69E9C><E79BAE>?
|
||||
+ (NSString *)scheduleCenterResultDirectoryForAccount:(NSInteger)accountId;
|
||||
|
||||
+ (BOOL)saveJSON:(id)JSON toPath:(NSString *)path;
|
||||
+ (id)getJSONFromPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import "HttpdnsPersistenceUtils.h"
|
||||
#import "HttpdnsService.h"
|
||||
#import "HttpdnsUtil.h"
|
||||
|
||||
static NSString *const 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
|
||||
@@ -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
|
||||
405
HttpDNSSDK/sdk/ios/NewHttpDNS/Scheduler/HttpdnsScheduleCenter.m
Normal file
405
HttpDNSSDK/sdk/ios/NewHttpDNS/Scheduler/HttpdnsScheduleCenter.m
Normal 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 ()
|
||||
|
||||
// 为了简单,无论v4还是v6,都只共同维<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/NSString,屏蔽NSNull等异常输<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];
|
||||
|
||||
// 调度server列表总是服务server列表加上兜底域名
|
||||
self->_ipv4UpdateServerHostList = [HttpdnsUtil joinArrays:v4Result
|
||||
withArray:[regionConfigLoader getUpdateV4FallbackHostList:self->_currentRegion]];
|
||||
}
|
||||
|
||||
if ([HttpdnsUtil isNotEmptyArray:v6Result]) {
|
||||
self->_ipv6ServiceServerHostList = [v6Result copy];
|
||||
|
||||
// 调度server列表总是服务server列表加上兜底域名
|
||||
self->_ipv6UpdateServerHostList = [HttpdnsUtil joinArrays:v6Result
|
||||
withArray:[regionConfigLoader getUpdateV6FallbackHostList:self->_currentRegion]];
|
||||
}
|
||||
|
||||
self->_currentActiveUpdateHostIndex = 0;
|
||||
self->_currentActiveServiceHostIndex = 0;
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)getActiveUpdateServerHost {
|
||||
HttpdnsIPStackType currentStack = [[HttpdnsIpStackDetector sharedInstance] currentIpStack];
|
||||
if (currentStack == kHttpdnsIpv6Only) {
|
||||
NSString *v6Host = [self currentActiveUpdateServerV6Host];
|
||||
if ([HttpdnsUtil isIPv6Address:v6Host]) {
|
||||
return [NSString stringWithFormat:@"[%@]", v6Host];
|
||||
}
|
||||
return v6Host;
|
||||
}
|
||||
|
||||
return [self currentActiveUpdateServerV4Host];
|
||||
}
|
||||
|
||||
- (void)initServerListByRegion:(NSString *)region {
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self->_currentRegion = region;
|
||||
|
||||
HttpdnsRegionConfigLoader *regionConfigLoader = [HttpdnsRegionConfigLoader sharedInstance];
|
||||
|
||||
self.ipv4ServiceServerHostList = [regionConfigLoader getSeriveV4HostList:region];
|
||||
self.ipv4UpdateServerHostList = [HttpdnsUtil joinArrays:[regionConfigLoader getSeriveV4HostList:region]
|
||||
withArray:[regionConfigLoader getUpdateV4FallbackHostList:region]];
|
||||
|
||||
self.ipv6ServiceServerHostList = [regionConfigLoader getSeriveV6HostList:region];
|
||||
self.ipv6UpdateServerHostList = [HttpdnsUtil joinArrays:[regionConfigLoader getSeriveV6HostList:region]
|
||||
withArray:[regionConfigLoader getUpdateV6FallbackHostList:region]];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)rotateServiceServerHost {
|
||||
__block int timeToUpdate = NO;
|
||||
dispatch_sync(_scheduleConfigLocalOperationQueue, ^{
|
||||
self.currentActiveServiceHostIndex++;
|
||||
|
||||
int total = (int)self.ipv4ServiceServerHostList.count + (int)self.ipv6ServiceServerHostList.count;
|
||||
if (self.currentActiveServiceHostIndex % total == 0) {
|
||||
timeToUpdate = YES;
|
||||
}
|
||||
});
|
||||
|
||||
if (timeToUpdate) {
|
||||
// 每次服务server列表轮转之后,尝<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
|
||||
@@ -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
|
||||
@@ -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.12今天起,调度服务由后端就近调度,不再需要传入region参数,但为了兼容不传region默认就是国内region的逻辑,默认都传入region=global
|
||||
* https://203.107.1.1/100000/ss?region=global&platform=ios&sdk_version=3.1.7&sid=LpmJIA2CUoi4&net=wifi
|
||||
*/
|
||||
- (NSString *)constructRequestURLWithUpdateHost:(NSString *)updateHost {
|
||||
NSString *urlPath = [NSString stringWithFormat:@"%ld/ss?region=global&platform=ios&sdk_version=%@", (long)_accountId, HTTPDNS_IOS_SDK_VERSION];
|
||||
urlPath = [self urlFormatSidNetBssid:urlPath];
|
||||
urlPath = [urlPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];
|
||||
return [NSString stringWithFormat:@"https://%@/%@", updateHost, urlPath];
|
||||
}
|
||||
|
||||
// url 添加 sid net
|
||||
- (NSString *)urlFormatSidNetBssid:(NSString *)url {
|
||||
NSString *sessionId = [HttpdnsUtil generateSessionID];
|
||||
if ([HttpdnsUtil isNotEmptyString:sessionId]) {
|
||||
url = [NSString stringWithFormat:@"%@&sid=%@", url, sessionId];
|
||||
}
|
||||
|
||||
NSString *netType = [[HttpdnsReachability sharedInstance] currentReachabilityString];
|
||||
if ([HttpdnsUtil isNotEmptyString:netType]) {
|
||||
url = [NSString stringWithFormat:@"%@&net=%@", url, netType];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fetchRegionConfigFromServer:(NSString *)updateHost error:(NSError **)pError {
|
||||
NSString *fullUrlStr = [self constructRequestURLWithUpdateHost:updateHost];
|
||||
HttpdnsLogDebug("ScRequest URL: %@", fullUrlStr);
|
||||
NSTimeInterval timeout = _timeoutInterval > 0 ? _timeoutInterval : HTTPDNS_DEFAULT_REQUEST_TIMEOUT_INTERVAL;
|
||||
NSString *userAgent = [HttpdnsUtil generateUserAgent];
|
||||
|
||||
NSError *requestError = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.httpClient performRequestWithURLString:fullUrlStr
|
||||
userAgent:userAgent
|
||||
timeout:timeout
|
||||
error:&requestError];
|
||||
if (!response) {
|
||||
if (pError) {
|
||||
*pError = requestError;
|
||||
HttpdnsLogDebug("ScRequest failed with url: %@, error: %@", fullUrlStr, requestError);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
NSDictionary *dict = @{@"ResponseCode": [NSString stringWithFormat:@"%ld", (long)response.statusCode]};
|
||||
if (pError) {
|
||||
*pError = [NSError errorWithDomain: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
|
||||
28
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpDnsLocker.h
Normal file
28
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpDnsLocker.h
Normal 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 */
|
||||
91
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpDnsLocker.m
Normal file
91
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpDnsLocker.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
291
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsIPQualityDetector.m
Normal file
291
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsIPQualityDetector.m
Normal 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秒
|
||||
// 更长的超时时间不是很有必要,因为建连超过2秒的IP,已经没有优选必要了
|
||||
timeout.tv_sec = 2;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
int selectResult = select(socketFd + 1, NULL, &fdSet, NULL, &timeout);
|
||||
|
||||
if (selectResult <= 0) {
|
||||
// 超时或错误
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ timed out or error: %s", ip, strerror(errno));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
} else {
|
||||
// 检查连接是否成功
|
||||
int error;
|
||||
socklen_t errorLen = sizeof(error);
|
||||
if (getsockopt(socketFd, SOL_SOCKET, SO_ERROR, &error, &errorLen) < 0 || error != 0) {
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ failed after select: %s", ip, strerror(error));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 其他错误
|
||||
HttpdnsLogDebug("IPQualityDetector connection to %@ failed: %s", ip, strerror(errno));
|
||||
close(socketFd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 结束计时
|
||||
gettimeofday(&endTime, NULL);
|
||||
|
||||
// 关闭socket
|
||||
close(socketFd);
|
||||
|
||||
// 计算耗时(毫秒)
|
||||
long seconds = endTime.tv_sec - startTime.tv_sec;
|
||||
long microseconds = endTime.tv_usec - startTime.tv_usec;
|
||||
NSInteger costTime = (seconds * 1000) + (microseconds / 1000);
|
||||
|
||||
return costTime;
|
||||
}
|
||||
|
||||
@end
|
||||
106
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsReachability.h
Normal file
106
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsReachability.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
//! Project version number for MacOSReachability.
|
||||
FOUNDATION_EXPORT double ReachabilityVersionNumber;
|
||||
|
||||
//! Project version string for MacOSReachability.
|
||||
FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
|
||||
|
||||
/**
|
||||
* Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X.
|
||||
*
|
||||
* @see http://nshipster.com/ns_enum-ns_options/
|
||||
**/
|
||||
#ifndef NS_ENUM
|
||||
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
|
||||
#endif
|
||||
|
||||
extern NSString *const kHttpdnsReachabilityChangedNotification;
|
||||
|
||||
typedef NS_ENUM(NSInteger, HttpdnsNetworkStatus) {
|
||||
// Apple NetworkStatus Compatible Names.
|
||||
HttpdnsNotReachable = 0,
|
||||
HttpdnsReachableViaWiFi = 1,
|
||||
HttpdnsReachableVia2G = 2,
|
||||
HttpdnsReachableVia3G = 3,
|
||||
HttpdnsReachableVia4G = 4,
|
||||
HttpdnsReachableVia5G = 5
|
||||
};
|
||||
|
||||
@class HttpdnsReachability;
|
||||
|
||||
typedef void (^NetworkReachable)(HttpdnsReachability * reachability);
|
||||
typedef void (^NetworkUnreachable)(HttpdnsReachability * reachability);
|
||||
typedef void (^NetworkReachability)(HttpdnsReachability * reachability, SCNetworkConnectionFlags flags);
|
||||
|
||||
|
||||
@interface HttpdnsReachability : NSObject
|
||||
|
||||
@property (nonatomic, copy) NetworkReachable reachableBlock;
|
||||
@property (nonatomic, copy) NetworkUnreachable unreachableBlock;
|
||||
@property (nonatomic, copy) NetworkReachability reachabilityBlock;
|
||||
|
||||
@property (nonatomic, assign) BOOL reachableOnWWAN;
|
||||
|
||||
+(instancetype)sharedInstance;
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname;
|
||||
// This is identical to the function above, but is here to maintain
|
||||
//compatibility with Apples original code. (see .m)
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname;
|
||||
+(instancetype)reachabilityForInternetConnection;
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress;
|
||||
+(instancetype)reachabilityForLocalWiFi;
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url;
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref;
|
||||
|
||||
-(BOOL)startNotifier;
|
||||
-(void)stopNotifier;
|
||||
|
||||
-(BOOL)isReachable;
|
||||
-(BOOL)isReachableViaWWAN;
|
||||
-(BOOL)isReachableViaWiFi;
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired; // Identical DDG variant.
|
||||
-(BOOL)connectionRequired; // Apple's routine.
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand;
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired;
|
||||
|
||||
-(HttpdnsNetworkStatus)currentReachabilityStatus;
|
||||
-(SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
-(NSString*)currentReachabilityString;
|
||||
-(NSString*)currentReachabilityFlags;
|
||||
|
||||
@end
|
||||
559
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsReachability.m
Normal file
559
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsReachability.m
Normal file
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "HttpdnsReachability.h"
|
||||
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
#import <netinet6/in6.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#import <netdb.h>
|
||||
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
|
||||
|
||||
NSString *const kHttpdnsReachabilityChangedNotification = @"kHttpdnsReachabilityChangedNotification";
|
||||
|
||||
static CTTelephonyNetworkInfo *networkInfo;
|
||||
|
||||
@interface HttpdnsReachability ()
|
||||
|
||||
@property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef;
|
||||
@property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue;
|
||||
@property (nonatomic, strong) id reachabilityObject;
|
||||
|
||||
-(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags;
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags)
|
||||
{
|
||||
return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c",
|
||||
#if TARGET_OS_IPHONE
|
||||
(flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-',
|
||||
#else
|
||||
'X',
|
||||
#endif
|
||||
(flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-'];
|
||||
}
|
||||
|
||||
// Start listening for reachability notifications on the current run loop
|
||||
static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
|
||||
{
|
||||
#pragma unused (target)
|
||||
|
||||
HttpdnsReachability *reachability = ((__bridge HttpdnsReachability*)info);
|
||||
|
||||
// We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool,
|
||||
// but what the heck eh?
|
||||
@autoreleasepool
|
||||
{
|
||||
[reachability reachabilityChanged:flags];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@implementation HttpdnsReachability
|
||||
|
||||
#pragma mark - Class Constructor Methods
|
||||
+(instancetype)sharedInstance
|
||||
{
|
||||
static HttpdnsReachability *instance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
networkInfo = [[CTTelephonyNetworkInfo alloc] init];
|
||||
instance = [HttpdnsReachability reachabilityWithHostname:@"www.taobao.com"];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithHostName:(NSString*)hostname
|
||||
{
|
||||
return [HttpdnsReachability reachabilityWithHostname:hostname];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithHostname:(NSString*)hostname
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithAddress:(void *)hostAddress
|
||||
{
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress);
|
||||
if (ref)
|
||||
{
|
||||
id reachability = [[self alloc] initWithReachabilityRef:ref];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForInternetConnection
|
||||
{
|
||||
struct sockaddr_in zeroAddress;
|
||||
bzero(&zeroAddress, sizeof(zeroAddress));
|
||||
zeroAddress.sin_len = sizeof(zeroAddress);
|
||||
zeroAddress.sin_family = AF_INET;
|
||||
|
||||
return [self reachabilityWithAddress:&zeroAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityForLocalWiFi
|
||||
{
|
||||
struct sockaddr_in localWifiAddress;
|
||||
bzero(&localWifiAddress, sizeof(localWifiAddress));
|
||||
localWifiAddress.sin_len = sizeof(localWifiAddress);
|
||||
localWifiAddress.sin_family = AF_INET;
|
||||
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
|
||||
localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM);
|
||||
|
||||
return [self reachabilityWithAddress:&localWifiAddress];
|
||||
}
|
||||
|
||||
+(instancetype)reachabilityWithURL:(NSURL*)url
|
||||
{
|
||||
id reachability;
|
||||
|
||||
NSString *host = url.host;
|
||||
BOOL isIpAddress = [self isIpAddress:host];
|
||||
|
||||
if (isIpAddress)
|
||||
{
|
||||
NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80);
|
||||
|
||||
struct sockaddr_in address;
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons([port intValue]);
|
||||
address.sin_addr.s_addr = inet_addr([host UTF8String]);
|
||||
|
||||
reachability = [self reachabilityWithAddress:&address];
|
||||
}
|
||||
else
|
||||
{
|
||||
reachability = [self reachabilityWithHostname:host];
|
||||
}
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
+(BOOL)isIpAddress:(NSString*)host
|
||||
{
|
||||
struct in_addr pin;
|
||||
return 1 == inet_aton([host UTF8String], &pin);
|
||||
}
|
||||
|
||||
|
||||
// Initialization methods
|
||||
|
||||
-(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
self.reachableOnWWAN = YES;
|
||||
self.reachabilityRef = ref;
|
||||
|
||||
// We need to create a serial queue.
|
||||
// We allocate this once for the lifetime of the notifier.
|
||||
|
||||
self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)dealloc
|
||||
{
|
||||
[self stopNotifier];
|
||||
|
||||
if(self.reachabilityRef)
|
||||
{
|
||||
CFRelease(self.reachabilityRef);
|
||||
self.reachabilityRef = nil;
|
||||
}
|
||||
|
||||
self.reachableBlock = nil;
|
||||
self.unreachableBlock = nil;
|
||||
self.reachabilityBlock = nil;
|
||||
self.reachabilitySerialQueue = nil;
|
||||
}
|
||||
|
||||
#pragma mark - Notifier Methods
|
||||
|
||||
// Notifier
|
||||
// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD
|
||||
// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS.
|
||||
// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want)
|
||||
|
||||
-(BOOL)startNotifier
|
||||
{
|
||||
// allow start notifier to be called multiple times
|
||||
if(self.reachabilityObject && (self.reachabilityObject == self))
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL };
|
||||
context.info = (__bridge void *)self;
|
||||
|
||||
if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context))
|
||||
{
|
||||
// Set it as our reachability queue, which will retain the queue
|
||||
if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue))
|
||||
{
|
||||
// this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves
|
||||
// woah
|
||||
self.reachabilityObject = self;
|
||||
return YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
|
||||
// UH OH - FAILURE - stop any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
}
|
||||
|
||||
// if we get here we fail at the internet
|
||||
self.reachabilityObject = nil;
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(void)stopNotifier
|
||||
{
|
||||
// First stop, any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
|
||||
// Unregister target from the GCD serial dispatch queue.
|
||||
SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL);
|
||||
|
||||
self.reachabilityObject = nil;
|
||||
}
|
||||
|
||||
#pragma mark - reachability tests
|
||||
|
||||
// This is for the case where you flick the airplane mode;
|
||||
// you end up getting something like this:
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
//Reachability: WR ct-----
|
||||
//Reachability: -- -------
|
||||
// We treat this as 4 UNREACHABLE triggers - really apple should do better than this
|
||||
|
||||
#define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
|
||||
|
||||
-(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags
|
||||
{
|
||||
BOOL connectionUP = YES;
|
||||
|
||||
if(!(flags & kSCNetworkReachabilityFlagsReachable))
|
||||
connectionUP = NO;
|
||||
|
||||
if( (flags & testcase) == testcase )
|
||||
connectionUP = NO;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
// We're on 3G.
|
||||
if(!self.reachableOnWWAN)
|
||||
{
|
||||
// We don't want to connect when on 3G.
|
||||
connectionUP = NO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return connectionUP;
|
||||
}
|
||||
|
||||
-(BOOL)isReachable
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
return NO;
|
||||
|
||||
return [self isReachableWithFlags:flags];
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWWAN
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're REACHABLE
|
||||
if(flags & kSCNetworkReachabilityFlagsReachable)
|
||||
{
|
||||
// Now, check we're on WWAN
|
||||
if(flags & kSCNetworkReachabilityFlagsIsWWAN)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(BOOL)isReachableViaWiFi
|
||||
{
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
// Check we're reachable
|
||||
if((flags & kSCNetworkReachabilityFlagsReachable))
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
// Check we're NOT on WWAN
|
||||
if((flags & kSCNetworkReachabilityFlagsIsWWAN))
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
-(BOOL)isConnectionRequired
|
||||
{
|
||||
return [self connectionRequired];
|
||||
}
|
||||
|
||||
-(BOOL)connectionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Dynamic, on demand connection?
|
||||
-(BOOL)isConnectionOnDemand
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand)));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Is user intervention required?
|
||||
-(BOOL)isInterventionRequired
|
||||
{
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags))
|
||||
{
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - reachability status stuff
|
||||
|
||||
-(HttpdnsNetworkStatus)currentReachabilityStatus
|
||||
{
|
||||
if([self isReachable])
|
||||
{
|
||||
if([self isReachableViaWiFi])
|
||||
{
|
||||
return HttpdnsReachableViaWiFi;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
NSString *nettype = networkInfo.currentRadioAccessTechnology;
|
||||
if (nettype)
|
||||
{
|
||||
if ([CTRadioAccessTechnologyGPRS isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyEdge isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMA1x isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia2G;
|
||||
}
|
||||
else if ([CTRadioAccessTechnologyLTE isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia4G;
|
||||
}
|
||||
else if ([CTRadioAccessTechnologyWCDMA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyHSDPA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyHSUPA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORev0 isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORevA isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyCDMAEVDORevB isEqualToString:nettype] ||
|
||||
[CTRadioAccessTechnologyeHRPD isEqualToString:nettype])
|
||||
{
|
||||
return HttpdnsReachableVia3G;
|
||||
}
|
||||
else
|
||||
{
|
||||
return HttpdnsReachableVia5G;
|
||||
}
|
||||
}
|
||||
// 默认以使用最广泛<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
|
||||
81
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsUtil.h
Normal file
81
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsUtil.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class HttpDnsService;
|
||||
|
||||
@class HttpdnsHostObject;
|
||||
|
||||
@interface HttpdnsUtil : NSObject
|
||||
|
||||
+ (BOOL)isIPv4Address:(NSString *)addr;
|
||||
|
||||
+ (BOOL)isIPv6Address:(NSString *)addr;
|
||||
|
||||
+ (BOOL)isAnIP:(NSString *)candidate;
|
||||
|
||||
+ (BOOL)isAHost:(NSString *)host;
|
||||
|
||||
+ (void)warnMainThreadIfNecessary;
|
||||
|
||||
+ (NSDictionary *)getValidDictionaryFromJson:(id)jsonValue;
|
||||
|
||||
+ (BOOL)isEmptyArray:(NSArray *)inputArr;
|
||||
|
||||
+ (BOOL)isNotEmptyArray:(NSArray *)inputArr;
|
||||
|
||||
+ (BOOL)isEmptyString:(NSString *)inputStr;
|
||||
|
||||
+ (BOOL)isNotEmptyString:(NSString *)inputStr;
|
||||
|
||||
+ (BOOL)isValidJSON:(id)JSON;
|
||||
|
||||
+ (BOOL)isEmptyDictionary:(NSDictionary *)inputDict;
|
||||
|
||||
+ (BOOL)isNotEmptyDictionary:(NSDictionary *)inputDict;
|
||||
|
||||
+ (NSArray *)joinArrays:(NSArray *)array1 withArray:(NSArray *)array2;
|
||||
|
||||
+ (NSString *)getMD5StringFrom:(NSString *)originString;
|
||||
|
||||
+ (NSString *)URLEncodedString:(NSString *)str;
|
||||
|
||||
+ (NSString *)generateSessionID;
|
||||
|
||||
+ (NSString *)generateUserAgent;
|
||||
|
||||
+ (NSData *)encryptDataAESCBC:(NSData *)plaintext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error;
|
||||
|
||||
+ (NSData *)decryptDataAESCBC:(NSData *)ciphertext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error;
|
||||
|
||||
+ (NSString *)hexStringFromData:(NSData *)data;
|
||||
|
||||
+ (NSData *)dataFromHexString:(NSString *)hexString;
|
||||
|
||||
+ (NSString *)hmacSha256:(NSString *)data key:(NSString *)key;
|
||||
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host;
|
||||
+ (void)processCustomTTL:(HttpdnsHostObject *)hostObject forHost:(NSString *)host service:(HttpDnsService *)service;
|
||||
|
||||
@end
|
||||
471
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsUtil.m
Normal file
471
HttpDNSSDK/sdk/ios/NewHttpDNS/Utils/HttpdnsUtil.m
Normal file
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "HttpdnsUtil.h"
|
||||
#import "HttpdnsLog_Internal.h"
|
||||
#import "CommonCrypto/CommonCrypto.h"
|
||||
#import "arpa/inet.h"
|
||||
#import "HttpdnsPublicConstant.h"
|
||||
#import "httpdnsReachability.h"
|
||||
#import "HttpdnsInternalConstant.h"
|
||||
#import "HttpdnsHostObject.h"
|
||||
#import "HttpdnsService.h"
|
||||
|
||||
@implementation HttpdnsUtil
|
||||
|
||||
+ (BOOL)isIPv4Address:(NSString *)addr {
|
||||
if (!addr) {
|
||||
return NO;
|
||||
}
|
||||
struct in_addr dst;
|
||||
return inet_pton(AF_INET, [addr UTF8String], &(dst.s_addr)) == 1;
|
||||
}
|
||||
|
||||
+ (BOOL)isIPv6Address:(NSString *)addr {
|
||||
if (!addr) {
|
||||
return NO;
|
||||
}
|
||||
struct in6_addr dst6;
|
||||
return inet_pton(AF_INET6, [addr UTF8String], &dst6) == 1;
|
||||
}
|
||||
|
||||
+ (BOOL)isAnIP:(NSString *)candidate {
|
||||
if ([self isIPv4Address:candidate]) {
|
||||
return YES;
|
||||
}
|
||||
if ([self isIPv6Address:candidate]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)isAHost:(NSString *)host {
|
||||
static dispatch_once_t once_token;
|
||||
static NSRegularExpression *hostExpression = nil;
|
||||
dispatch_once(&once_token, ^{
|
||||
hostExpression = [[NSRegularExpression alloc] initWithPattern:@"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
|
||||
if (!host.length) {
|
||||
return NO;
|
||||
}
|
||||
NSTextCheckingResult *checkResult = [hostExpression firstMatchInString:host options:0 range:NSMakeRange(0, [host length])];
|
||||
if (checkResult.range.length == [host length]) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)warnMainThreadIfNecessary {
|
||||
if ([NSThread isMainThread]) {
|
||||
HttpdnsLogDebug("Warning: A long-running Paas operation is being executed on the main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSDictionary *)getValidDictionaryFromJson:(id)jsonValue {
|
||||
NSDictionary *dictionaryValueFromJson = nil;
|
||||
if ([jsonValue isKindOfClass:[NSDictionary class]]) {
|
||||
if ([(NSDictionary *)jsonValue allKeys].count > 0) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:jsonValue];
|
||||
@try {
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
} @catch (NSException *exception) {}
|
||||
dictionaryValueFromJson = [jsonValue copy];
|
||||
}
|
||||
}
|
||||
return dictionaryValueFromJson;
|
||||
}
|
||||
|
||||
+ (void)removeNSNullValueFromArray:(NSMutableArray *)array {
|
||||
NSMutableArray *objToRemove = nil;
|
||||
NSMutableIndexSet *indexToReplace = [[NSMutableIndexSet alloc] init];
|
||||
NSMutableArray *objForReplace = [[NSMutableArray alloc] init];
|
||||
for (int i = 0; i < array.count; ++i) {
|
||||
id value = [array objectAtIndex:i];
|
||||
if ([value isKindOfClass:[NSNull class]]) {
|
||||
if (!objToRemove) {
|
||||
objToRemove = [[NSMutableArray alloc] init];
|
||||
}
|
||||
[objToRemove addObject:value];
|
||||
} else if ([value isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:value];
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
[indexToReplace addIndex:i];
|
||||
[objForReplace addObject:mutableDict];
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *v = [value mutableCopy];
|
||||
[self removeNSNullValueFromArray:v];
|
||||
[indexToReplace addIndex:i];
|
||||
[objForReplace addObject:v];
|
||||
}
|
||||
}
|
||||
[array replaceObjectsAtIndexes:indexToReplace withObjects:objForReplace];
|
||||
if (objToRemove) {
|
||||
[array removeObjectsInArray:objToRemove];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)removeNSNullValueFromDictionary:(NSMutableDictionary *)dict {
|
||||
for (id key in [dict allKeys]) {
|
||||
id value = [dict objectForKey:key];
|
||||
if ([value isKindOfClass:[NSNull class]]) {
|
||||
[dict removeObjectForKey:key];
|
||||
} else if ([value isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:value];
|
||||
[self removeNSNullValueFromDictionary:mutableDict];
|
||||
[dict setObject:mutableDict forKey:key];
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *v = [value mutableCopy];
|
||||
[self removeNSNullValueFromArray:v];
|
||||
[dict setObject:v forKey:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyArray:(NSArray *)inputArr {
|
||||
if (!inputArr) {
|
||||
return YES;
|
||||
}
|
||||
return [inputArr count] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyArray:(NSArray *)inputArr {
|
||||
return ![self isEmptyArray:inputArr];
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyString:(NSString *)inputStr {
|
||||
if (!inputStr) {
|
||||
return YES;
|
||||
}
|
||||
return [inputStr length] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyString:(NSString *)inputStr {
|
||||
return ![self isEmptyString:inputStr];
|
||||
}
|
||||
|
||||
+ (BOOL)isValidJSON:(id)JSON {
|
||||
BOOL isValid;
|
||||
@try {
|
||||
isValid = ([JSON isKindOfClass:[NSDictionary class]] || [JSON isKindOfClass:[NSArray class]]);
|
||||
} @catch (NSException *ignore) {
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
+ (BOOL)isEmptyDictionary:(NSDictionary *)inputDict {
|
||||
if (!inputDict) {
|
||||
return YES;
|
||||
}
|
||||
return [inputDict count] == 0;
|
||||
}
|
||||
|
||||
+ (BOOL)isNotEmptyDictionary:(NSDictionary *)inputDict {
|
||||
return ![self isEmptyDictionary:inputDict];
|
||||
}
|
||||
|
||||
+ (NSArray *)joinArrays:(NSArray *)array1 withArray:(NSArray *)array2 {
|
||||
NSMutableArray *resultArray = [array1 mutableCopy];
|
||||
[resultArray addObjectsFromArray:array2];
|
||||
return [resultArray copy];
|
||||
}
|
||||
|
||||
+ (id)convertJsonDataToObject:(NSData *)jsonData {
|
||||
if (jsonData) {
|
||||
NSError *error;
|
||||
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];
|
||||
if (!error) {
|
||||
return jsonObj;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (NSString *)getMD5StringFrom:(NSString *)originString {
|
||||
const char * pointer = [originString UTF8String];
|
||||
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
|
||||
|
||||
CC_MD5(pointer, (CC_LONG)strlen(pointer), md5Buffer);
|
||||
|
||||
NSMutableString *string = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
|
||||
[string appendFormat:@"%02x",md5Buffer[i]];
|
||||
|
||||
return [string copy];
|
||||
}
|
||||
|
||||
+ (NSString *)URLEncodedString:(NSString *)str {
|
||||
if (str) {
|
||||
return [str stringByAddingPercentEncodingWithAllowedCharacters:
|
||||
[NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]\""].invertedSet];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/**
|
||||
生成sessionId
|
||||
App打开生命周期只生成一次,不做持久<EFBFBD><EFBFBD>?
|
||||
sessionId<EFBFBD><EFBFBD>?2位,采用base62编码
|
||||
*/
|
||||
+ (NSString *)generateSessionID {
|
||||
static NSString *sessionId = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
NSUInteger length = alphabet.length;
|
||||
if (![HttpdnsUtil isNotEmptyString:sessionId]) {
|
||||
NSMutableString *mSessionId = [NSMutableString string];
|
||||
for (int i = 0; i < 12; i++) {
|
||||
[mSessionId appendFormat:@"%@", [alphabet substringWithRange:NSMakeRange(arc4random() % length, 1)]];
|
||||
}
|
||||
sessionId = [mSessionId copy];
|
||||
}
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
+ (NSString *)generateUserAgent {
|
||||
UIDevice *device = [UIDevice currentDevice];
|
||||
NSString *systemName = [device systemName];
|
||||
NSString *systemVersion = [device systemVersion];
|
||||
NSString *model = [device model];
|
||||
|
||||
NSString *userAgent = [NSString stringWithFormat:@"HttpdnsSDK/%@ (%@; iOS %@; %@)", HTTPDNS_IOS_SDK_VERSION, model, systemVersion, systemName];
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
+ (NSData *)encryptDataAESCBC:(NSData *)plaintext
|
||||
withKey:(NSData *)key
|
||||
error:(NSError **)error {
|
||||
// 检查输入参<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;
|
||||
}
|
||||
|
||||
// 为CBC模式生成128bit(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];
|
||||
|
||||
// 执行加密
|
||||
// AES中PKCS5Padding与PKCS7Padding相同
|
||||
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
|
||||
kCCAlgorithmAES,
|
||||
kCCOptionPKCS7Padding,
|
||||
[key bytes],
|
||||
[key length],
|
||||
[iv bytes],
|
||||
[plaintext bytes],
|
||||
[plaintext length],
|
||||
[cipherData mutableBytes],
|
||||
[cipherData length],
|
||||
&encryptedSize);
|
||||
|
||||
if (cryptStatus != kCCSuccess) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain: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;
|
||||
}
|
||||
|
||||
// 提取IV(前16字节)和实际的密<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
|
||||
17
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/AppDelegate.h
Normal file
17
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/AppDelegate.h
Normal 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
|
||||
|
||||
56
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/AppDelegate.m
Normal file
56
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/AppDelegate.m
Normal 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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
12
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfig.plist
Normal file
12
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfig.plist
Normal 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>
|
||||
21
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfigLoader.h
Normal file
21
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfigLoader.h
Normal 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
|
||||
|
||||
85
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfigLoader.m
Normal file
85
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoConfigLoader.m
Normal 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
|
||||
|
||||
49
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoHttpdnsScenario.h
Normal file
49
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoHttpdnsScenario.h
Normal 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
|
||||
|
||||
153
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoHttpdnsScenario.m
Normal file
153
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoHttpdnsScenario.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
27
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoResolveModel.h
Normal file
27
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoResolveModel.h
Normal 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
|
||||
|
||||
42
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoResolveModel.m
Normal file
42
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoResolveModel.m
Normal 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
|
||||
|
||||
13
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoViewController.h
Normal file
13
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoViewController.h
Normal 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
|
||||
321
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoViewController.m
Normal file
321
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/DemoViewController.m
Normal 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
|
||||
48
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/Info.plist
Normal file
48
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/Info.plist
Normal 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>
|
||||
16
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/main.m
Normal file
16
HttpDNSSDK/sdk/ios/NewHttpDNSTestDemo/main.m
Normal 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]));
|
||||
}
|
||||
}
|
||||
477
HttpDNSSDK/sdk/ios/NewHttpDNSTests/DB/DBTest.m
Normal file
477
HttpDNSSDK/sdk/ios/NewHttpDNSTests/DB/DBTest.m
Normal 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
|
||||
@@ -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);
|
||||
// 同步接口,不复用过期ip的情况下,解析出的ip一定是未过期的
|
||||
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];
|
||||
// 没有使用cacheKey,所以这里应该是nil
|
||||
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
|
||||
108
HttpDNSSDK/sdk/ios/NewHttpDNSTests/HighLevelTest/CustomTTLTest.m
Normal file
108
HttpDNSSDK/sdk/ios/NewHttpDNSTests/HighLevelTest/CustomTTLTest.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经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];
|
||||
|
||||
// 请求类型为ipv4,拿到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,拿到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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 请求类型为auto,只拿到ipv4结果
|
||||
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];
|
||||
|
||||
// 请求类型为ipv4,拿到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,拿到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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 请求类型为auto,注意,我们认为ipv6only只存在理论上,比如实验室环境
|
||||
// 因此,ipv4的地址是一定会去解析的,auto的作用在于,如果发现网络还支持ipv6,那就多获取ipv6的结<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]);
|
||||
}
|
||||
|
||||
// 网络情况为ipv4和ipv6下的缓存测试
|
||||
- (void)testSimplyRetrieveCachedResultUnderDualStack {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
[self.httpdns cleanAllHostCache];
|
||||
|
||||
// 存入ipv4和ipv6的地址
|
||||
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]);
|
||||
|
||||
// 请求ipv4和ipv6
|
||||
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]);
|
||||
}
|
||||
|
||||
// ttl、lastLookupTime,ipv4和ipv6是分开处理<EFBFBD><EFBFBD>?
|
||||
- (void)testTTLAndLastLookUpTime {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
[self.httpdns cleanAllHostCache];
|
||||
|
||||
// 存入ipv4和ipv6的地址
|
||||
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];
|
||||
|
||||
// auto在当前环境下即请求ipv4和ipv6
|
||||
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];
|
||||
|
||||
// v4的信息发生变化,v6的信息保持不<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);
|
||||
}
|
||||
|
||||
// 只缓存ipv4单栈的地址,按请求双栈类型存入,此时会标记该域名没有ipv6地址
|
||||
// 按预期,会判断该域名没有ipv6地址,因此不会返回ipv6地址,也不会发请<EFBFBD><EFBFBD>?
|
||||
- (void)testMergeNoIpv6ResultAndGetBoth {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
|
||||
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
|
||||
|
||||
// 双栈下解析结果仅有ipv4,合并时会标记该host无ipv6
|
||||
[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);
|
||||
}];
|
||||
}
|
||||
|
||||
// 缓存ipv4单栈的地址,但是请求ipv4类型存入,此时不会打标记没有ipv6
|
||||
// 于是,读取时,会尝试发请求获取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);
|
||||
}
|
||||
|
||||
// 缓存ipv6单栈的地址,但是请求ipv6类型存入,此时不会打标记没有ipv4
|
||||
// 于是,读取时,会尝试发请求获取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
|
||||
@@ -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);
|
||||
// 同步接口,不复用过期ip的情况下,解析出的ip一定是未过期的
|
||||
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);
|
||||
}
|
||||
|
||||
// 指定查询both,但域名都只配置了ipv4
|
||||
// 这种情况下,会自动打标该域名无ipv6,后续的结果只会包含ipv4地址
|
||||
- (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
|
||||
@@ -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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经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
|
||||
@@ -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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经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
|
||||
@@ -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);
|
||||
|
||||
// 按预期,上面的结果是按cacheKey来缓存的,这里不指定cacheKey,应当拿到nil
|
||||
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
|
||||
402
HttpDNSSDK/sdk/ios/NewHttpDNSTests/IPDetector/IpDetectorTest.m
Normal file
402
HttpDNSSDK/sdk/ios/NewHttpDNSTests/IPDetector/IpDetectorTest.m
Normal 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>?
|
||||
// 模拟scheduleIPQualityDetection内部实现,当调用时直接执行addPendingTask
|
||||
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];
|
||||
});
|
||||
|
||||
// 设置期望:验证addPendingTask被调用,而executeDetection不被调用
|
||||
// 使用同一个mock对象,避免创建多个mock
|
||||
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;
|
||||
|
||||
// 强制GC(注意:在ARC下这不一定会立即触发<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
|
||||
24
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Info.plist
Normal file
24
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Info.plist
Normal 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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
// 编码终止 chunk(size=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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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];
|
||||
|
||||
// 20个请求,间隔1秒(第一个请求立即执行)
|
||||
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];
|
||||
});
|
||||
|
||||
// 超时计算: 19秒sleep + 20个请求×~2<EFBFBD><EFBFBD>?= 59秒,设置50秒(提前退出机制保证效率)
|
||||
[self waitForExpectations:@[expectation] timeout:50.0];
|
||||
}
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user