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,710 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#import "HttpdnsRequest.h"
#import "HttpdnsRemoteResolver.h"
#import "HttpdnsUtil.h"
#import "HttpdnsLog_Internal.h"
#import "HttpdnsPublicConstant.h"
#import "HttpdnsInternalConstant.h"
#import "HttpdnsPersistenceUtils.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsScheduleCenter.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsReachability.h"
#import "HttpdnsRequestManager.h"
#import "HttpdnsIpStackDetector.h"
#import "HttpdnsNWHTTPClient.h"
#import <stdint.h>
static dispatch_queue_t _streamOperateSyncQueue = 0;
@interface HttpdnsRemoteResolver () <NSStreamDelegate>
@property (nonatomic, strong) NSRunLoop *runloop;
@property (nonatomic, strong) NSError *networkError;
@property (nonatomic, weak) HttpDnsService *service;
@property (nonatomic, strong) HttpdnsNWHTTPClient *httpClient;
@end
@implementation HttpdnsRemoteResolver {
NSMutableData *_resultData;
NSInputStream *_inputStream;
BOOL _responseResolved;
BOOL _compeleted;
NSTimer *_timeoutTimer;
NSDictionary *_httpJSONDict;
}
#pragma mark init
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_streamOperateSyncQueue = dispatch_queue_create("com.Trust.sdk.httpdns.runloopOperateQueue.HttpdnsRequest", DISPATCH_QUEUE_SERIAL);
});
}
- (instancetype)init {
if (self = [super init]) {
_resultData = [NSMutableData data];
_httpJSONDict = nil;
self.networkError = nil;
_responseResolved = NO;
_compeleted = NO;
_httpClient = [HttpdnsNWHTTPClient sharedInstance];
}
return self;
}
#pragma mark LookupIpAction
- (NSArray<HttpdnsHostObject *> *)parseHttpdnsResponse:(NSDictionary *)json withQueryIpType:(HttpdnsQueryIPType)queryIpType {
if (!json) {
return nil;
}
// <EFBFBD><EFBFBD>?
if (![self validateResponseCode:json]) {
return nil;
}
//
id data = [self extractDataContent:json];
if (!data) {
return nil;
}
// <EFBFBD><EFBFBD>?
NSArray *answers = [self getAnswersFromData:data];
if (!answers) {
return nil;
}
//
NSMutableArray<HttpdnsHostObject *> *hostObjects = [NSMutableArray array];
for (NSDictionary *answer in answers) {
HttpdnsHostObject *hostObject = [self createHostObjectFromAnswer:answer];
if (hostObject) {
[hostObjects addObject:hostObject];
}
}
return hostObjects;
}
// <EFBFBD><EFBFBD>?
- (BOOL)validateResponseCode:(NSDictionary *)json {
NSString *code = [json objectForKey:@"code"];
if (![code isEqualToString:@"success"]) {
HttpdnsLogDebug("Response code is not success: %@", code);
return NO;
}
return YES;
}
// <EFBFBD><EFBFBD>?
- (id)extractDataContent:(NSDictionary *)json {
// mode<EFBFBD><EFBFBD>?
NSInteger mode = [[json objectForKey:@"mode"] integerValue];
id data = [json objectForKey:@"data"];
if (mode == 1) { // AES-CBC
// <EFBFBD><EFBFBD>?
data = [self decryptData:data withMode:mode];
} else if (mode != 0) {
// AES-GCM<EFBFBD><EFBFBD>?
HttpdnsLogDebug("Unsupported encryption mode: %ld", (long)mode);
return nil;
}
if (![data isKindOfClass:[NSDictionary class]]) {
HttpdnsLogDebug("Data is not a dictionary");
return nil;
}
return data;
}
//
- (id)decryptData:(id)data withMode:(NSInteger)mode {
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
NSString *aesSecretKey = service.aesSecretKey;
if (![HttpdnsUtil isNotEmptyString:aesSecretKey]) {
HttpdnsLogDebug("Response is encrypted but no AES key is provided");
return nil;
}
if (![data isKindOfClass:[NSString class]]) {
HttpdnsLogDebug("Encrypted data is not a string");
return nil;
}
// Base64NSData
NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:data options:0];
if (!encryptedData || encryptedData.length <= 16) {
HttpdnsLogDebug("Invalid encrypted data");
return nil;
}
// secretKey
NSData *keyData = [HttpdnsUtil dataFromHexString:aesSecretKey];
if (!keyData) {
HttpdnsLogDebug("Invalid AES key format");
return nil;
}
// 使<EFBFBD><EFBFBD>?
NSError *decryptError = nil;
NSData *decryptedData = [HttpdnsUtil decryptDataAESCBC:encryptedData withKey:keyData error:&decryptError];
if (decryptError || !decryptedData) {
HttpdnsLogDebug("Failed to decrypt data: %@", decryptError);
return nil;
}
// JSON<EFBFBD><EFBFBD>?
NSError *jsonError;
id decodedData = [NSJSONSerialization JSONObjectWithData:decryptedData options:0 error:&jsonError];
if (jsonError) {
HttpdnsLogDebug("Failed to parse decrypted JSON: %@", jsonError);
return nil;
}
return decodedData;
}
//
- (NSArray *)getAnswersFromData:(NSDictionary *)data {
NSArray *answers = [data objectForKey:@"answers"];
if (![answers isKindOfClass:[NSArray class]] || answers.count == 0) {
HttpdnsLogDebug("No answers in response");
return nil;
}
return answers;
}
// <EFBFBD><EFBFBD>?
- (HttpdnsHostObject *)createHostObjectFromAnswer:(NSDictionary *)answer {
//
NSString *host = [answer objectForKey:@"dn"];
if (![HttpdnsUtil isNotEmptyString:host]) {
HttpdnsLogDebug("Missing domain name in answer");
return nil;
}
// HostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
[hostObject setHostName:host];
// IPv4
[self processIPv4Info:answer forHostObject:hostObject];
// IPv6
[self processIPv6Info:answer forHostObject:hostObject];
// ttl
[HttpdnsUtil processCustomTTL:hostObject forHost:host service:self.service];
// IP
NSString *clientIp = [[answer objectForKey:@"data"] objectForKey:@"cip"];
if ([HttpdnsUtil isNotEmptyString:clientIp]) {
[hostObject setClientIp:clientIp];
}
return hostObject;
}
// IPv4
- (void)processIPv4Info:(NSDictionary *)answer forHostObject:(HttpdnsHostObject *)hostObject {
NSDictionary *v4Data = [answer objectForKey:@"v4"];
if (![v4Data isKindOfClass:[NSDictionary class]]) {
return;
}
NSArray *ip4s = [v4Data objectForKey:@"ips"];
if ([ip4s isKindOfClass:[NSArray class]] && ip4s.count > 0) {
// IPv4
[self setIpArrayToHostObject:hostObject fromIpsArray:ip4s forIPv6:NO];
// IPv4TTL
[self setTTLForHostObject:hostObject fromData:v4Data forIPv6:NO];
// v4extra使<EFBFBD><EFBFBD>?
[self processExtraInfo:v4Data forHostObject:hostObject];
// no_ip_codeIPv4
if ([[v4Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
hostObject.hasNoIpv4Record = YES;
}
} else {
// IPv4v4<EFBFBD><EFBFBD>?
hostObject.hasNoIpv4Record = YES;
}
}
// IPv6
- (void)processIPv6Info:(NSDictionary *)answer forHostObject:(HttpdnsHostObject *)hostObject {
NSDictionary *v6Data = [answer objectForKey:@"v6"];
if (![v6Data isKindOfClass:[NSDictionary class]]) {
return;
}
NSArray *ip6s = [v6Data objectForKey:@"ips"];
if ([ip6s isKindOfClass:[NSArray class]] && ip6s.count > 0) {
// IPv6
[self setIpArrayToHostObject:hostObject fromIpsArray:ip6s forIPv6:YES];
// IPv6TTL
[self setTTLForHostObject:hostObject fromData:v6Data forIPv6:YES];
// v4 extra使v6extra
if (![hostObject getExtra]) {
[self processExtraInfo:v6Data forHostObject:hostObject];
}
// no_ip_codeIPv6
if ([[v6Data objectForKey:@"no_ip_code"] isKindOfClass:[NSString class]]) {
hostObject.hasNoIpv6Record = YES;
}
} else {
// IPv6v6<EFBFBD><EFBFBD>?
hostObject.hasNoIpv6Record = YES;
}
}
// IP<EFBFBD><EFBFBD>?
- (void)setIpArrayToHostObject:(HttpdnsHostObject *)hostObject fromIpsArray:(NSArray *)ips forIPv6:(BOOL)isIPv6 {
NSMutableArray *ipArray = [NSMutableArray array];
for (NSString *ip in ips) {
if ([HttpdnsUtil isEmptyString:ip]) {
continue;
}
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
[ipObject setIp:ip];
[ipArray addObject:ipObject];
}
if (isIPv6) {
[hostObject setV6Ips:ipArray];
} else {
[hostObject setV4Ips:ipArray];
}
}
// TTL
- (void)setTTLForHostObject:(HttpdnsHostObject *)hostObject fromData:(NSDictionary *)data forIPv6:(BOOL)isIPv6 {
NSNumber *ttl = [data objectForKey:@"ttl"];
if (ttl) {
if (isIPv6) {
hostObject.v6ttl = [ttl longLongValue];
hostObject.lastIPv6LookupTime = [NSDate date].timeIntervalSince1970;
} else {
hostObject.v4ttl = [ttl longLongValue];
hostObject.lastIPv4LookupTime = [NSDate date].timeIntervalSince1970;
}
} else {
if (isIPv6) {
hostObject.v6ttl = 0;
} else {
hostObject.v4ttl = 0;
}
}
}
//
- (void)processExtraInfo:(NSDictionary *)data forHostObject:(HttpdnsHostObject *)hostObject {
id extra = [data objectForKey:@"extra"];
if (extra) {
NSString *convertedExtra = [self convertExtraToString:extra];
if (convertedExtra) {
[hostObject setExtra:convertedExtra];
}
}
}
- (NSString *)constructHttpdnsResolvingUrl:(HttpdnsRequest *)request forV4Net:(BOOL)isV4 {
//
NSString *serverIp = [self getServerIpForNetwork:isV4];
HttpDnsService *service = self.service;
if (![HttpdnsUtil isNotEmptyString:serverIp]) {
return nil;
}
// <EFBFBD><EFBFBD>?
NSDictionary *paramsToSign = [self prepareSigningParams:request forEncryption:[self shouldUseEncryption]];
//
NSString *signature = [self calculateSignatureForParams:paramsToSign withSecretKey:service.secretKey];
// URL
NSString *url = [NSString stringWithFormat:@"%@/v2/d", serverIp];
// URL
NSString *finalUrl = [self buildFinalUrlWithBase:url
params:paramsToSign
isEncrypted:[self shouldUseEncryption]
signature:signature
request:request];
return finalUrl;
}
// 使IP
- (NSString *)getServerIpForNetwork:(BOOL)isV4 {
HttpdnsScheduleCenter *scheduleCenter = self.service.scheduleCenter;
if (!scheduleCenter) {
return nil;
}
return isV4 ? [scheduleCenter currentActiveServiceServerV4Host] : [scheduleCenter currentActiveServiceServerV6Host];
}
// 使<EFBFBD><EFBFBD>?
- (BOOL)shouldUseEncryption {
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
return [HttpdnsUtil isNotEmptyString:service.aesSecretKey];
}
//
- (NSDictionary *)prepareSigningParams:(HttpdnsRequest *)request forEncryption:(BOOL)useEncryption {
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
NSInteger accountId = service.accountID;
// <EFBFBD><EFBFBD>?
NSMutableDictionary *paramsToSign = [NSMutableDictionary dictionary];
//
NSMutableDictionary *paramsToEncrypt = [NSMutableDictionary dictionary];
// ID<EFBFBD><EFBFBD>?
[paramsToSign setObject:[NSString stringWithFormat:@"%ld", accountId] forKey:@"id"];
//
NSString *mode = useEncryption ? @"1" : @"0"; // 0: , 1: AES-CBC
[paramsToSign setObject:mode forKey:@"m"];
//
[paramsToSign setObject:@"1.0" forKey:@"v"];
//
[paramsToEncrypt setObject:request.host forKey:@"dn"];
//
[paramsToEncrypt setObject:[self getQueryTypeString:request.queryIpType] forKey:@"q"];
// SDNS
[self addSdnsParams:request.sdnsParams toParams:paramsToEncrypt];
// <EFBFBD><EFBFBD>?
long expiredTimestamp = [self calculateExpiredTimestamp];
NSString *expiredTimestampString = [NSString stringWithFormat:@"%ld", expiredTimestamp];
[paramsToSign setObject:expiredTimestampString forKey:@"exp"];
//
if (useEncryption) {
NSString *encryptedHexString = [self encryptParams:paramsToEncrypt];
if (encryptedHexString) {
[paramsToSign setObject:encryptedHexString forKey:@"enc"];
}
} else {
//
[paramsToSign addEntriesFromDictionary:paramsToEncrypt];
}
return paramsToSign;
}
// <EFBFBD><EFBFBD>?
- (NSString *)getQueryTypeString:(HttpdnsQueryIPType)queryIpType {
if ((queryIpType & HttpdnsQueryIPTypeIpv4) && (queryIpType & HttpdnsQueryIPTypeIpv6)) {
return @"4,6";
} else if (queryIpType & HttpdnsQueryIPTypeIpv6) {
return @"6";
}
return @"4";
}
// SDNS
- (void)addSdnsParams:(NSDictionary *)sdnsParams toParams:(NSMutableDictionary *)paramsToEncrypt {
if ([HttpdnsUtil isNotEmptyDictionary:sdnsParams]) {
[sdnsParams enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
NSString *sdnsKey = [NSString stringWithFormat:@"sdns-%@", key];
[paramsToEncrypt setObject:obj forKey:sdnsKey];
}];
}
}
// <EFBFBD><EFBFBD>?
- (long)calculateExpiredTimestamp {
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
long localTimestampOffset = (long)service.authTimeOffset;
long localTimestamp = (long)[[NSDate date] timeIntervalSince1970];
if (localTimestampOffset != 0) {
localTimestamp = localTimestamp + localTimestampOffset;
}
return localTimestamp + HTTPDNS_DEFAULT_AUTH_TIMEOUT_INTERVAL;
}
//
- (NSString *)encryptParams:(NSDictionary *)paramsToEncrypt {
NSError *error = nil;
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
// JSON
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:paramsToEncrypt options:0 error:&error];
if (error) {
HttpdnsLogDebug("Failed to serialize params to JSON: %@", error);
return nil;
}
// secretKey
NSData *keyData = [HttpdnsUtil dataFromHexString:service.aesSecretKey];
if (!keyData) {
HttpdnsLogDebug("Invalid AES key format");
return nil;
}
// OCAES-GCMAES-CBC
NSData *encryptedData = [HttpdnsUtil encryptDataAESCBC:jsonData withKey:keyData error:&error];
if (error) {
HttpdnsLogDebug("Failed to encrypt data: %@", error);
return nil;
}
//
return [HttpdnsUtil hexStringFromData:encryptedData];
}
//
- (NSString *)calculateSignatureForParams:(NSDictionary *)params withSecretKey:(NSString *)secretKey {
if (![HttpdnsUtil isNotEmptyString:secretKey]) {
return nil;
}
//
NSArray *sortedKeys = [[params allKeys] sortedArrayUsingSelector:@selector(compare:)];
NSMutableArray *signParts = [NSMutableArray array];
for (NSString *key in sortedKeys) {
[signParts addObject:[NSString stringWithFormat:@"%@=%@", key, [params objectForKey:key]]];
}
// <EFBFBD><EFBFBD>?
NSString *signContent = [signParts componentsJoinedByString:@"&"];
// HMAC-SHA256
return [HttpdnsUtil hmacSha256:signContent key:secretKey];
}
// URL
- (NSString *)buildFinalUrlWithBase:(NSString *)baseUrl
params:(NSDictionary *)params
isEncrypted:(BOOL)useEncryption
signature:(NSString *)signature
request:(HttpdnsRequest *)request {
HttpDnsService *service = self.service ?: [HttpDnsService sharedInstance];
NSMutableString *finalUrl = [NSMutableString stringWithString:baseUrl];
[finalUrl appendString:@"?"];
//
[finalUrl appendFormat:@"id=%ld", service.accountID];
[finalUrl appendFormat:@"&m=%@", [params objectForKey:@"m"]];
[finalUrl appendFormat:@"&exp=%@", [params objectForKey:@"exp"]];
[finalUrl appendFormat:@"&v=%@", [params objectForKey:@"v"]];
if (useEncryption) {
// enc
[finalUrl appendFormat:@"&enc=%@", [params objectForKey:@"enc"]];
} else {
// <EFBFBD><EFBFBD>?
NSMutableDictionary *paramsForPlainText = [NSMutableDictionary dictionaryWithDictionary:params];
[paramsForPlainText removeObjectForKey:@"id"];
[paramsForPlainText removeObjectForKey:@"m"];
[paramsForPlainText removeObjectForKey:@"exp"];
[paramsForPlainText removeObjectForKey:@"v"];
for (NSString *key in paramsForPlainText) {
//
if ([key isEqualToString:@"id"] || [key isEqualToString:@"m"] ||
[key isEqualToString:@"exp"] || [key isEqualToString:@"v"]) {
continue;
}
NSString *value = [paramsForPlainText objectForKey:key];
[finalUrl appendFormat:@"&%@=%@", [HttpdnsUtil URLEncodedString:key], [HttpdnsUtil URLEncodedString:value]];
}
}
// <EFBFBD><EFBFBD>?
if ([HttpdnsUtil isNotEmptyString:signature]) {
[finalUrl appendFormat:@"&s=%@", signature];
}
//
[self appendAdditionalParams:finalUrl];
return finalUrl;
}
// <EFBFBD><EFBFBD>?
- (void)appendAdditionalParams:(NSMutableString *)url {
// sessionId
NSString *sessionId = [HttpdnsUtil generateSessionID];
if ([HttpdnsUtil isNotEmptyString:sessionId]) {
[url appendFormat:@"&sid=%@", sessionId];
}
//
NSString *netType = [[HttpdnsReachability sharedInstance] currentReachabilityString];
if ([HttpdnsUtil isNotEmptyString:netType]) {
[url appendFormat:@"&net=%@", netType];
}
// SDK
NSString *versionInfo = [NSString stringWithFormat:@"ios_%@", HTTPDNS_IOS_SDK_VERSION];
[url appendFormat:@"&sdk=%@", versionInfo];
}
- (NSArray<HttpdnsHostObject *> *)resolve:(HttpdnsRequest *)request error:(NSError **)error {
HttpdnsLogDebug("lookupHostFromServer, request: %@", request);
HttpDnsService *service = [HttpDnsService getInstanceByAccountId:request.accountId];
if (!service) {
HttpdnsLogDebug("Missing service for accountId: %ld; ensure request.accountId is set and service initialized", (long)request.accountId);
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN code:Trust_HTTPDNS_HTTPS_COMMON_ERROR_CODE userInfo:@{NSLocalizedDescriptionKey: @"HttpDnsService not found for accountId"}];
}
return nil;
}
self.service = service;
NSString *url = [self constructHttpdnsResolvingUrl:request forV4Net:YES];
HttpdnsQueryIPType queryIPType = request.queryIpType;
NSArray<HttpdnsHostObject *> *hostObjects = [self sendRequest:url queryIpType:queryIPType error:error];
if (!(*error)) {
return hostObjects;
}
@try {
HttpdnsIPStackType stackType = [[HttpdnsIpStackDetector sharedInstance] currentIpStack];
// 由于上面默认只用ipv4请求这里判断如果是ipv6-only环境那就用v6的ip再试一<E8AF95><E4B880>?
if (stackType == kHttpdnsIpv6Only) {
url = [self constructHttpdnsResolvingUrl:request forV4Net:NO];
HttpdnsLogDebug("lookupHostFromServer by ipv4 server failed, construct ipv6 backup url: %@", url);
hostObjects = [self sendRequest:url queryIpType:queryIPType error:error];
if (!(*error)) {
return hostObjects;
}
}
} @catch (NSException *exception) {
HttpdnsLogDebug("lookupHostFromServer failed again by ipv6 server, exception: %@", exception.reason);
}
return nil;
}
- (NSArray<HttpdnsHostObject *> *)sendRequest:(NSString *)urlStr queryIpType:(HttpdnsQueryIPType)queryIpType error:(NSError **)error {
if (![HttpdnsUtil isNotEmptyString:urlStr]) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTPDNS_HTTPS_COMMON_ERROR_CODE
userInfo:@{NSLocalizedDescriptionKey: @"Empty resolve URL due to missing scheduler"}];
}
return nil;
}
HttpDnsService *httpdnsService = self.service;
NSString *scheme = httpdnsService.enableHttpsRequest ? @"https" : @"http";
NSString *fullUrlStr = [NSString stringWithFormat:@"%@://%@", scheme, urlStr];
NSTimeInterval timeout = httpdnsService.timeoutInterval > 0 ? httpdnsService.timeoutInterval : 10.0;
NSString *userAgent = [HttpdnsUtil generateUserAgent];
HttpdnsNWHTTPClientResponse *httpResponse = [self.httpClient performRequestWithURLString:fullUrlStr
userAgent:userAgent
timeout:timeout
error:error];
if (!httpResponse) {
return nil;
}
if (httpResponse.statusCode != 200) {
if (error) {
NSString *errorMessage = [NSString stringWithFormat:@"Unsupported http status code: %ld", (long)httpResponse.statusCode];
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTP_UNSUPPORTED_STATUS_CODE
userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
}
return nil;
}
NSError *jsonError = nil;
id jsonValue = [NSJSONSerialization JSONObjectWithData:httpResponse.body options:kNilOptions error:&jsonError];
if (jsonError) {
if (error) {
*error = jsonError;
}
return nil;
}
NSDictionary *json = [HttpdnsUtil getValidDictionaryFromJson:jsonValue];
if (!json) {
if (error) {
*error = [NSError errorWithDomain:Trust_HTTPDNS_ERROR_DOMAIN
code:Trust_HTTP_PARSE_JSON_FAILED
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse JSON response"}];
}
return nil;
}
return [self parseHttpdnsResponse:json withQueryIpType:queryIpType];
}
#pragma mark - Helper Functions
// extraNSString
- (NSString *)convertExtraToString:(id)extra {
if (!extra) {
return nil;
}
if ([extra isKindOfClass:[NSString class]]) {
// <EFBFBD><EFBFBD>?
return extra;
} else {
// JSON<EFBFBD><EFBFBD>?
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:extra options:0 error:&error];
if (!error && jsonData) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
return jsonString;
} else {
HttpdnsLogDebug("Failed to convert extra to JSON string: %@", error);
return nil;
}
}
}
@end