feat: sync httpdns sdk/platform updates without large binaries

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>accountID</key>
<integer>139450</integer>
<key>secretKey</key>
<string>807a19762f8eaefa8563489baf198535</string>
<key>aesSecretKey</key>
<string>82c0af0d0cb2d69c4f87bb25c2e23929</string>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2</string>
<key>CFBundleVersion</key>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

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