管理端全部功能跑通
This commit is contained in:
@@ -1,148 +1,77 @@
|
||||
# Alicloud HTTPDNS Android SDK
|
||||
# HTTPDNS Android SDK (SNI Hidden v1.0.0)
|
||||
|
||||
面向 Android 的 HTTP/HTTPS DNS 解析 SDK,提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 Android API 19(Android 4.4)。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 鉴权请求与可选 AES 传输加密
|
||||
- IPv4/IPv6 双栈解析,支持自动/同时解析
|
||||
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
|
||||
- 预解析、区域路由、网络切换自动刷新
|
||||
- 可定制日志回调与会话追踪 `sessionId`
|
||||
|
||||
## 安装(Gradle)
|
||||
|
||||
在项目的 `build.gradle` 中添加:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'com.aliyun.ams:alicloud-android-httpdns:2.6.7'
|
||||
}
|
||||
```
|
||||
|
||||
请访问 [Android SDK发布说明](https://help.aliyun.com/document_detail/435251.html) 查看最新版本号。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### Java
|
||||
## 1. Init
|
||||
|
||||
```java
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns;
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService;
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
// 初始化配置
|
||||
String appId = "app1f1ndpo9";
|
||||
|
||||
new InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setSecretKey("YOUR_SECRET_KEY")
|
||||
.setEnableExpiredIp(true) // 允许返回过期 IP
|
||||
.buildFor("YOUR_ACCOUNT_ID");
|
||||
.setPrimaryServiceHost("httpdns-a.example.com")
|
||||
.setBackupServiceHost("httpdns-b.example.com")
|
||||
.setServicePort(443)
|
||||
.setSecretKey("your-sign-secret") // optional if sign is enabled
|
||||
.setEnableHttps(true)
|
||||
.buildFor(appId);
|
||||
|
||||
// 获取实例
|
||||
HttpDnsService httpDns = HttpDns.getService("YOUR_ACCOUNT_ID");
|
||||
|
||||
// 预解析热点域名
|
||||
httpDns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com")));
|
||||
|
||||
// 解析域名
|
||||
HTTPDNSResult result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto);
|
||||
String[] ips = result.getIps();
|
||||
HttpDnsService httpDnsService = HttpDns.getService(appId);
|
||||
```
|
||||
|
||||
### Kotlin
|
||||
|
||||
```kotlin
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType
|
||||
|
||||
// 初始化配置
|
||||
InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setSecretKey("YOUR_SECRET_KEY")
|
||||
.setEnableExpiredIp(true) // 允许返回过期 IP
|
||||
.buildFor("YOUR_ACCOUNT_ID")
|
||||
|
||||
// 获取实例
|
||||
val httpDns = HttpDns.getService("YOUR_ACCOUNT_ID")
|
||||
|
||||
// 预解析热点域名
|
||||
httpDns.setPreResolveHosts(arrayListOf("www.aliyun.com"))
|
||||
|
||||
// 解析域名
|
||||
val result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto)
|
||||
val ips = result.ips
|
||||
```
|
||||
|
||||
### 提示
|
||||
|
||||
- 启动时通过 `setPreResolveHosts()` 预热热点域名
|
||||
- 如需在刷新期间容忍 TTL 过期,可开启 `setEnableExpiredIp(true)`
|
||||
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障
|
||||
|
||||
## 源码构建
|
||||
|
||||
```bash
|
||||
./gradlew clean :httpdns-sdk:assembleRelease
|
||||
```
|
||||
|
||||
构建产物位于 `httpdns-sdk/build/outputs/aar/` 目录。
|
||||
|
||||
### 版本说明
|
||||
|
||||
项目使用 `productFlavors` 区分不同版本:
|
||||
- `normal`:中国大陆版本
|
||||
- `intl`:国际版本
|
||||
- `end2end`:用于单元测试
|
||||
|
||||
## 测试
|
||||
|
||||
### 运行单元测试
|
||||
|
||||
```bash
|
||||
./gradlew clean :httpdns-sdk:testEnd2endForTestUnitTest
|
||||
```
|
||||
|
||||
### Demo 应用
|
||||
|
||||
SDK 提供了两个 Demo:
|
||||
|
||||
#### 1. app module(旧版 Demo)
|
||||
|
||||
在 `MyApp.java` 中配置测试账号:
|
||||
## 2. Resolve
|
||||
|
||||
```java
|
||||
private HttpDnsHolder holderA = new HttpDnsHolder("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret");
|
||||
private HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null);
|
||||
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
|
||||
"api.business.com",
|
||||
RequestIpType.auto,
|
||||
null,
|
||||
null
|
||||
);
|
||||
```
|
||||
|
||||
> 两个实例用于测试实例间互不影响,体验时只需配置一个
|
||||
## 3. Official HTTP Adapter (IP + Empty-SNI + Host)
|
||||
|
||||
#### 2. demo module(推荐)
|
||||
```java
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterRequest;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterResponse;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
|
||||
使用 Kotlin + MVVM 开发,功能更丰富。在 `demo/build.gradle` 中配置测试账号:
|
||||
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(
|
||||
httpDnsService,
|
||||
new HttpDnsAdapterOptions.Builder()
|
||||
.setConnectTimeoutMillis(3000)
|
||||
.setReadTimeoutMillis(5000)
|
||||
.setRequestIpType(RequestIpType.auto)
|
||||
.setAllowInsecureCertificatesForDebugOnly(false)
|
||||
.build()
|
||||
);
|
||||
|
||||
```groovy
|
||||
buildConfigField "String", "ACCOUNT_ID", "\"请替换为测试用实例的accountId\""
|
||||
buildConfigField "String", "SECRET_KEY", "\"请替换为测试用实例的secret\""
|
||||
buildConfigField "String", "AES_SECRET_KEY", "\"请替换为测试用实例的aes\""
|
||||
HttpDnsAdapterResponse response = adapter.execute(
|
||||
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
|
||||
);
|
||||
```
|
||||
|
||||
## 依赖与要求
|
||||
Behavior is fixed:
|
||||
- Resolve by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
- Android API 19+(Android 4.4+)
|
||||
- 需要权限:`INTERNET`、`ACCESS_NETWORK_STATE`
|
||||
## 4. Public Errors
|
||||
|
||||
## 安全说明
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
- 切勿提交真实的 AccountID/SecretKey,请通过本地安全配置或 CI 注入
|
||||
- 若担心设备时间偏差影响鉴权,可使用 `setAuthCurrentTime()` 校正时间
|
||||
## 5. Removed Public Params
|
||||
|
||||
## 文档
|
||||
|
||||
官方文档:[Android SDK手册](https://help.aliyun.com/document_detail/435250.html)
|
||||
|
||||
## 感谢
|
||||
|
||||
本项目中 Inet64Util 工具类由 [Shinelw](https://github.com/Shinelw) 贡献支持,感谢。
|
||||
Do not use legacy public parameters:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.net.NetworkStateManager;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
/**
|
||||
@@ -66,4 +68,18 @@ public class HttpDns {
|
||||
sHolder = new HttpDnsInstanceHolder(new InstanceCreator());
|
||||
NetworkStateManager.getInstance().reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.alibaba.sdk.android.httpdns;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -73,6 +75,20 @@ public class HttpDns {
|
||||
InitConfig.addConfig(accountId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或者禁用httpdns,理论上这个是内部接口,不给外部使用的
|
||||
* 但是已经对外暴露,所以保留
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.alibaba.sdk.android.httpdns;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class HttpDnsV1Client {
|
||||
private HttpDnsV1Client() {
|
||||
}
|
||||
|
||||
public static HttpDnsService init(Context context,
|
||||
String appId,
|
||||
String primaryServiceHost,
|
||||
String backupServiceHost,
|
||||
int servicePort,
|
||||
String signSecret) {
|
||||
InitConfig.Builder builder = new InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setEnableHttps(true)
|
||||
.setPrimaryServiceHost(primaryServiceHost)
|
||||
.setServicePort(servicePort > 0 ? servicePort : 443);
|
||||
|
||||
if (!TextUtils.isEmpty(backupServiceHost)) {
|
||||
builder.setBackupServiceHost(backupServiceHost);
|
||||
}
|
||||
if (!TextUtils.isEmpty(signSecret)) {
|
||||
builder.setSecretKey(signSecret);
|
||||
}
|
||||
|
||||
builder.buildFor(appId);
|
||||
if (!TextUtils.isEmpty(signSecret)) {
|
||||
return HttpDns.getService(context, appId, signSecret);
|
||||
}
|
||||
return HttpDns.getService(context, appId);
|
||||
}
|
||||
|
||||
public static HTTPDNSResult resolveHost(HttpDnsService service,
|
||||
String host,
|
||||
String qtype,
|
||||
String cip) {
|
||||
if (service == null) {
|
||||
return HTTPDNSResult.empty(host);
|
||||
}
|
||||
RequestIpType requestIpType = RequestIpType.auto;
|
||||
if ("AAAA".equalsIgnoreCase(qtype)) {
|
||||
requestIpType = RequestIpType.v6;
|
||||
}
|
||||
|
||||
Map<String, String> params = null;
|
||||
if (!TextUtils.isEmpty(cip)) {
|
||||
params = new HashMap<>();
|
||||
params.put("cip", cip);
|
||||
}
|
||||
|
||||
return service.getHttpDnsResultForHostSyncNonBlocking(host, requestIpType, params, host);
|
||||
}
|
||||
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return HttpDns.buildHttpClientAdapter(service);
|
||||
}
|
||||
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service,
|
||||
HttpDnsAdapterOptions options) {
|
||||
return HttpDns.buildHttpClientAdapter(service, options);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,9 @@ public class InitConfig {
|
||||
private final String mBizTags;
|
||||
private final String aesSecretKey;
|
||||
private final String secretKey;
|
||||
private final String primaryServiceHost;
|
||||
private final String backupServiceHost;
|
||||
private final int servicePort;
|
||||
private final Context context;
|
||||
|
||||
private InitConfig(Builder builder) {
|
||||
@@ -82,6 +85,9 @@ public class InitConfig {
|
||||
mEnableObservable = builder.enableObservable;
|
||||
mBizTags = builder.bizTags;
|
||||
aesSecretKey = builder.aesSecretKey;
|
||||
primaryServiceHost = builder.primaryServiceHost;
|
||||
backupServiceHost = builder.backupServiceHost;
|
||||
servicePort = builder.servicePort;
|
||||
context = builder.context;
|
||||
secretKey = builder.secretKey;
|
||||
}
|
||||
@@ -159,6 +165,18 @@ public class InitConfig {
|
||||
return aesSecretKey;
|
||||
}
|
||||
|
||||
public String getPrimaryServiceHost() {
|
||||
return primaryServiceHost;
|
||||
}
|
||||
|
||||
public String getBackupServiceHost() {
|
||||
return backupServiceHost;
|
||||
}
|
||||
|
||||
public int getServicePort() {
|
||||
return servicePort;
|
||||
}
|
||||
|
||||
public Context getContext() {return context;}
|
||||
|
||||
public String getSecretKey() {
|
||||
@@ -184,6 +202,9 @@ public class InitConfig {
|
||||
private Map<String, String> sdnsGlobalParams = null;
|
||||
private String bizTags = null;
|
||||
private String aesSecretKey = null;
|
||||
private String primaryServiceHost = null;
|
||||
private String backupServiceHost = null;
|
||||
private int servicePort = -1;
|
||||
private Context context = null;
|
||||
private String secretKey = null;
|
||||
|
||||
@@ -415,6 +436,43 @@ public class InitConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主服务域名。
|
||||
*/
|
||||
public Builder setPrimaryServiceHost(String host) {
|
||||
this.primaryServiceHost = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置备服务域名。
|
||||
*/
|
||||
public Builder setBackupServiceHost(String host) {
|
||||
this.backupServiceHost = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置服务域名,支持主备两个。
|
||||
*/
|
||||
public Builder setServiceHosts(List<String> hosts) {
|
||||
if (hosts != null && hosts.size() > 0) {
|
||||
this.primaryServiceHost = hosts.get(0);
|
||||
}
|
||||
if (hosts != null && hosts.size() > 1) {
|
||||
this.backupServiceHost = hosts.get(1);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务端口,默认 -1 表示使用协议默认端口。
|
||||
*/
|
||||
public Builder setServicePort(int port) {
|
||||
this.servicePort = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置context
|
||||
* @param context 上下文
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.alibaba.sdk.android.httpdns.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -41,6 +42,7 @@ import com.alibaba.sdk.android.httpdns.utils.Constants;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* 域名解析服务 httpdns接口的实现
|
||||
@@ -60,6 +62,7 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
private SignService mSignService;
|
||||
private AESEncryptService mAESEncryptService;
|
||||
private boolean resolveAfterNetworkChange = true;
|
||||
private boolean mUseCustomServiceHosts = false;
|
||||
/**
|
||||
* crash defend 默认关闭
|
||||
*/
|
||||
@@ -115,7 +118,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
tryUpdateRegionServer(sContext, accountId);
|
||||
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
favorInit(sContext, accountId);
|
||||
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
@@ -163,10 +168,37 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
mRequestHandler.setSdnsGlobalParams(config.getSdnsGlobalParams());
|
||||
mAESEncryptService.setAesSecretKey(config.getAesSecretKey());
|
||||
applyCustomServiceHosts(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void applyCustomServiceHosts(InitConfig config) {
|
||||
String primaryHost = config.getPrimaryServiceHost();
|
||||
String backupHost = config.getBackupServiceHost();
|
||||
ArrayList<String> hosts = new ArrayList<>();
|
||||
if (!TextUtils.isEmpty(primaryHost)) {
|
||||
hosts.add(primaryHost.trim());
|
||||
}
|
||||
if (!TextUtils.isEmpty(backupHost) && !backupHost.trim().equalsIgnoreCase(primaryHost == null ? "" : primaryHost.trim())) {
|
||||
hosts.add(backupHost.trim());
|
||||
}
|
||||
if (hosts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] serverHosts = hosts.toArray(new String[0]);
|
||||
int[] ports = new int[serverHosts.length];
|
||||
int servicePort = config.getServicePort();
|
||||
Arrays.fill(ports, servicePort > 0 ? servicePort : -1);
|
||||
|
||||
mHttpDnsConfig.setInitServers(mHttpDnsConfig.getRegion(), serverHosts, ports, serverHosts, ports);
|
||||
mHttpDnsConfig.setDefaultUpdateServer(serverHosts, ports);
|
||||
mHttpDnsConfig.setDefaultUpdateServerIpv6(serverHosts, ports);
|
||||
mHttpDnsConfig.setHTTPSRequestEnabled(true);
|
||||
mUseCustomServiceHosts = true;
|
||||
}
|
||||
|
||||
protected void beforeInit() {
|
||||
// only for test
|
||||
}
|
||||
@@ -218,7 +250,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
mRequestHandler.resetStatus();
|
||||
|
||||
//服务IP更新,触发服务IP测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -589,6 +623,12 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
@Override
|
||||
public void setRegion(String region) {
|
||||
if (mUseCustomServiceHosts) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("ignore setRegion in custom service host mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!mHttpDnsConfig.isEnabled()) {
|
||||
HttpDnsLog.i("service is disabled");
|
||||
return;
|
||||
@@ -605,9 +645,13 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
if (changed) {
|
||||
mResultRepo.clearMemoryCache();
|
||||
//region变化,服务IP变成对应的预置IP,触发测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
}
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mScheduleService.updateRegionServerIps(region, Constants.UPDATE_REGION_SERVER_SCENES_REGION_CHANGE);
|
||||
}
|
||||
mScheduleService.updateRegionServerIps(region, Constants.UPDATE_REGION_SERVER_SCENES_REGION_CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -684,7 +728,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
});
|
||||
|
||||
//网络变化,触发服务IP测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
@@ -817,6 +863,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
}
|
||||
|
||||
private void tryUpdateRegionServer(Context context, String accountId) {
|
||||
if (mUseCustomServiceHosts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mHttpDnsConfig.getCurrentServer().shouldUpdateServerIp()) {
|
||||
mScheduleService.updateRegionServerIps(Constants.UPDATE_REGION_SERVER_SCENES_INIT);
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.alibaba.sdk.android.httpdns.impl;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -67,6 +66,30 @@ public class SignService {
|
||||
return signStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新版 /resolve 请求签名:
|
||||
* appId|lower(domain)|upper(qtype)|exp|nonce
|
||||
*/
|
||||
public String signResolve(String appId, String domain, String qtype, String exp, String nonce) {
|
||||
if (TextUtils.isEmpty(mSecretKey)
|
||||
|| TextUtils.isEmpty(appId)
|
||||
|| TextUtils.isEmpty(domain)
|
||||
|| TextUtils.isEmpty(qtype)
|
||||
|| TextUtils.isEmpty(exp)
|
||||
|| TextUtils.isEmpty(nonce)) {
|
||||
return "";
|
||||
}
|
||||
String raw = appId + "|" + domain.toLowerCase() + "|" + qtype.toUpperCase() + "|" + exp + "|" + nonce;
|
||||
try {
|
||||
return hmacSha256(raw);
|
||||
} catch (Exception e) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("sign resolve fail.", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateV2SignContent(Map<String, String> map) {
|
||||
Map<String, String> sortedMap = new TreeMap<>();
|
||||
for(Map.Entry<String, String> entry : map.entrySet()) {
|
||||
@@ -91,9 +114,17 @@ public class SignService {
|
||||
private String hmacSha256(String content)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac mac = Mac.getInstance(ALGORITHM);
|
||||
mac.init(new SecretKeySpec(CommonUtil.decodeHex(mSecretKey), ALGORITHM));
|
||||
mac.init(new SecretKeySpec(mSecretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM));
|
||||
byte[] signedBytes = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
return CommonUtil.encodeHexString(signedBytes);
|
||||
StringBuilder sb = new StringBuilder(signedBytes.length * 2);
|
||||
for (byte b : signedBytes) {
|
||||
String h = Integer.toHexString(b & 0xff);
|
||||
if (h.length() == 1) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(h);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void setCurrentTimestamp(long serverTime) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpDnsAdapterException extends IOException {
|
||||
private final String errorCode;
|
||||
|
||||
public HttpDnsAdapterException(String errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterException(String errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
public class HttpDnsAdapterOptions {
|
||||
private final int connectTimeoutMillis;
|
||||
private final int readTimeoutMillis;
|
||||
private final boolean allowInsecureCertificatesForDebugOnly;
|
||||
private final RequestIpType requestIpType;
|
||||
|
||||
private HttpDnsAdapterOptions(Builder builder) {
|
||||
connectTimeoutMillis = builder.connectTimeoutMillis;
|
||||
readTimeoutMillis = builder.readTimeoutMillis;
|
||||
allowInsecureCertificatesForDebugOnly = builder.allowInsecureCertificatesForDebugOnly;
|
||||
requestIpType = builder.requestIpType;
|
||||
}
|
||||
|
||||
public int getConnectTimeoutMillis() {
|
||||
return connectTimeoutMillis;
|
||||
}
|
||||
|
||||
public int getReadTimeoutMillis() {
|
||||
return readTimeoutMillis;
|
||||
}
|
||||
|
||||
public boolean isAllowInsecureCertificatesForDebugOnly() {
|
||||
return allowInsecureCertificatesForDebugOnly;
|
||||
}
|
||||
|
||||
public RequestIpType getRequestIpType() {
|
||||
return requestIpType;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private int connectTimeoutMillis = 3000;
|
||||
private int readTimeoutMillis = 5000;
|
||||
private boolean allowInsecureCertificatesForDebugOnly = false;
|
||||
private RequestIpType requestIpType = RequestIpType.auto;
|
||||
|
||||
public Builder setConnectTimeoutMillis(int value) {
|
||||
if (value > 0) {
|
||||
connectTimeoutMillis = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReadTimeoutMillis(int value) {
|
||||
if (value > 0) {
|
||||
readTimeoutMillis = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowInsecureCertificatesForDebugOnly(boolean value) {
|
||||
allowInsecureCertificatesForDebugOnly = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRequestIpType(RequestIpType type) {
|
||||
if (type != null) {
|
||||
requestIpType = type;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterOptions build() {
|
||||
return new HttpDnsAdapterOptions(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpDnsAdapterRequest {
|
||||
private final String method;
|
||||
private final String url;
|
||||
private final Map<String, String> headers;
|
||||
private final byte[] body;
|
||||
|
||||
public HttpDnsAdapterRequest(String method, String url) {
|
||||
this(method, url, null, null);
|
||||
}
|
||||
|
||||
public HttpDnsAdapterRequest(String method, String url, Map<String, String> headers, byte[] body) {
|
||||
this.method = method == null || method.trim().isEmpty() ? "GET" : method.trim().toUpperCase();
|
||||
this.url = url;
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
this.headers = Collections.emptyMap();
|
||||
} else {
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
}
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpDnsAdapterResponse {
|
||||
private final int statusCode;
|
||||
private final Map<String, List<String>> headers;
|
||||
private final byte[] body;
|
||||
private final String usedIp;
|
||||
|
||||
public HttpDnsAdapterResponse(int statusCode, Map<String, List<String>> headers, byte[] body, String usedIp) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.usedIp = usedIp;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public String getUsedIp() {
|
||||
return usedIp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
public final class HttpDnsErrorCode {
|
||||
private HttpDnsErrorCode() {
|
||||
}
|
||||
|
||||
public static final String NO_IP_AVAILABLE = "NO_IP_AVAILABLE";
|
||||
public static final String TLS_EMPTY_SNI_FAILED = "TLS_EMPTY_SNI_FAILED";
|
||||
public static final String HOST_ROUTE_REJECTED = "HOST_ROUTE_REJECTED";
|
||||
public static final String RESOLVE_SIGN_INVALID = "RESOLVE_SIGN_INVALID";
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class HttpDnsHttpAdapter {
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final HttpDnsService httpDnsService;
|
||||
private final HttpDnsAdapterOptions options;
|
||||
|
||||
public HttpDnsHttpAdapter(HttpDnsService httpDnsService) {
|
||||
this(httpDnsService, new HttpDnsAdapterOptions.Builder().build());
|
||||
}
|
||||
|
||||
public HttpDnsHttpAdapter(HttpDnsService httpDnsService, HttpDnsAdapterOptions options) {
|
||||
if (httpDnsService == null) {
|
||||
throw new IllegalArgumentException("httpDnsService should not be null");
|
||||
}
|
||||
this.httpDnsService = httpDnsService;
|
||||
this.options = options == null ? new HttpDnsAdapterOptions.Builder().build() : options;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterResponse execute(HttpDnsAdapterRequest request) throws IOException {
|
||||
if (request == null || request.getUrl() == null || request.getUrl().trim().isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.HOST_ROUTE_REJECTED,
|
||||
"request or url is empty");
|
||||
}
|
||||
|
||||
URL originalURL = new URL(request.getUrl());
|
||||
String originalHost = originalURL.getHost();
|
||||
if (originalHost == null || originalHost.trim().isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.HOST_ROUTE_REJECTED,
|
||||
"invalid original host");
|
||||
}
|
||||
if (!"https".equalsIgnoreCase(originalURL.getProtocol())) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"only https scheme is supported in empty sni mode");
|
||||
}
|
||||
|
||||
List<String> candidateIps = resolveIps(originalHost);
|
||||
if (candidateIps.isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.NO_IP_AVAILABLE,
|
||||
"HTTPDNS returned no ip for host: " + originalHost);
|
||||
}
|
||||
|
||||
IOException lastException = null;
|
||||
for (String ip : candidateIps) {
|
||||
if (ip == null || ip.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String usedIp = ip.trim();
|
||||
HttpsURLConnection connection = null;
|
||||
try {
|
||||
int port = originalURL.getPort() > 0 ? originalURL.getPort() : 443;
|
||||
URL targetURL = new URL("https", usedIp, port, originalURL.getFile());
|
||||
connection = (HttpsURLConnection) targetURL.openConnection();
|
||||
connection.setConnectTimeout(options.getConnectTimeoutMillis());
|
||||
connection.setReadTimeout(options.getReadTimeoutMillis());
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod(request.getMethod());
|
||||
connection.setSSLSocketFactory(createSocketFactory());
|
||||
connection.setHostnameVerifier(createHostnameVerifier(originalHost));
|
||||
connection.setRequestProperty("Host", originalHost);
|
||||
for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
|
||||
if (entry.getKey() == null || entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
if ("host".equalsIgnoreCase(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
byte[] body = request.getBody();
|
||||
if (body != null && body.length > 0 && allowsRequestBody(request.getMethod())) {
|
||||
connection.setDoOutput(true);
|
||||
connection.getOutputStream().write(body);
|
||||
}
|
||||
|
||||
int code = connection.getResponseCode();
|
||||
byte[] payload = readResponseBytes(connection, code);
|
||||
Map<String, List<String>> headers = sanitizeHeaders(connection.getHeaderFields());
|
||||
return new HttpDnsAdapterResponse(code, headers, payload, usedIp);
|
||||
} catch (IOException e) {
|
||||
lastException = e;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastException == null) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"all ip attempts failed with no explicit exception");
|
||||
}
|
||||
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"all ip attempts failed", lastException);
|
||||
}
|
||||
|
||||
private List<String> resolveIps(String host) {
|
||||
RequestIpType type = options.getRequestIpType();
|
||||
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(host, type, null, null);
|
||||
if (result == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> ips = new ArrayList<>();
|
||||
String[] v4 = result.getIps();
|
||||
if (v4 != null) {
|
||||
for (String item : v4) {
|
||||
if (item != null && item.length() > 0) {
|
||||
ips.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
String[] v6 = result.getIpv6s();
|
||||
if (v6 != null) {
|
||||
for (String item : v6) {
|
||||
if (item != null && item.length() > 0) {
|
||||
ips.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
private byte[] readResponseBytes(HttpsURLConnection connection, int code) throws IOException {
|
||||
InputStream stream = code >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
||||
if (stream == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int count;
|
||||
while ((count = stream.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
stream.close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private Map<String, List<String>> sanitizeHeaders(Map<String, List<String>> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, List<String>> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : source.entrySet()) {
|
||||
if (entry.getKey() == null) {
|
||||
continue;
|
||||
}
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean allowsRequestBody(String method) {
|
||||
if (method == null) {
|
||||
return false;
|
||||
}
|
||||
String upper = method.toUpperCase();
|
||||
return "POST".equals(upper) || "PUT".equals(upper) || "PATCH".equals(upper) || "DELETE".equals(upper);
|
||||
}
|
||||
|
||||
private HostnameVerifier createHostnameVerifier(final String originalHost) {
|
||||
if (options.isAllowInsecureCertificatesForDebugOnly()) {
|
||||
return new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
|
||||
return new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return defaultVerifier.verify(originalHost, session);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private SSLSocketFactory createSocketFactory() throws IOException {
|
||||
try {
|
||||
SSLSocketFactory baseFactory;
|
||||
if (options.isAllowInsecureCertificatesForDebugOnly()) {
|
||||
baseFactory = trustAllSocketFactory();
|
||||
} else {
|
||||
baseFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
}
|
||||
return new NoSniSocketFactory(baseFactory);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("create ssl socket factory failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SSLSocketFactory trustAllSocketFactory() throws GeneralSecurityException {
|
||||
TrustManager[] managers = new TrustManager[]{new X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}};
|
||||
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, managers, new SecureRandom());
|
||||
return context.getSocketFactory();
|
||||
}
|
||||
|
||||
private static class NoSniSocketFactory extends SSLSocketFactory {
|
||||
private final SSLSocketFactory delegate;
|
||||
|
||||
private NoSniSocketFactory(SSLSocketFactory delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return delegate.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.Socket s, String host, int port, boolean autoClose)
|
||||
throws IOException {
|
||||
return sanitize(delegate.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(String host, int port) throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(String host, int port, java.net.InetAddress localHost, int localPort)
|
||||
throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.InetAddress host, int port) throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.InetAddress address, int port, java.net.InetAddress localAddress,
|
||||
int localPort) throws IOException {
|
||||
return sanitize(delegate.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private java.net.Socket sanitize(java.net.Socket socket) {
|
||||
if (!(socket instanceof SSLSocket)) {
|
||||
return socket;
|
||||
}
|
||||
SSLSocket sslSocket = (SSLSocket) socket;
|
||||
try {
|
||||
SSLParameters params = sslSocket.getSSLParameters();
|
||||
try {
|
||||
java.lang.reflect.Method setServerNames = SSLParameters.class.getMethod("setServerNames", List.class);
|
||||
setServerNames.invoke(params, Collections.emptyList());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
sslSocket.setSSLParameters(params);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return sslSocket;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,7 @@ import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
|
||||
@@ -61,13 +59,7 @@ public class HttpRequest<T> {
|
||||
//设置通用UA
|
||||
conn.setRequestProperty("User-Agent", requestConfig.getUA());
|
||||
if (conn instanceof HttpsURLConnection) {
|
||||
((HttpsURLConnection)conn).setHostnameVerifier(new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return HttpsURLConnection.getDefaultHostnameVerifier().verify(
|
||||
HttpRequestConfig.HTTPS_CERTIFICATE_HOSTNAME, session);
|
||||
}
|
||||
});
|
||||
((HttpsURLConnection) conn).setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
|
||||
}
|
||||
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
in = conn.getErrorStream();
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.BuildConfig;
|
||||
import com.alibaba.sdk.android.httpdns.NetType;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsConfig;
|
||||
import com.alibaba.sdk.android.httpdns.impl.SignService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestConfig;
|
||||
import com.alibaba.sdk.android.httpdns.track.SessionTrackMgr;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ResolveHostHelper {
|
||||
public static HttpRequestConfig getConfig(HttpDnsConfig config, String host,
|
||||
@@ -28,18 +25,18 @@ public class ResolveHostHelper {
|
||||
Map<String, String> globalParams,
|
||||
SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
HashMap<String, String> extraArgs = null;
|
||||
if (cacheKey != null) {
|
||||
extraArgs = new HashMap<>();
|
||||
HashMap<String, String> mergedExtras = null;
|
||||
if ((globalParams != null && !globalParams.isEmpty()) || (extras != null && !extras.isEmpty())) {
|
||||
mergedExtras = new HashMap<>();
|
||||
if (globalParams != null) {
|
||||
extraArgs.putAll(globalParams);
|
||||
mergedExtras.putAll(globalParams);
|
||||
}
|
||||
if (extras != null) {
|
||||
extraArgs.putAll(extras);
|
||||
mergedExtras.putAll(extras);
|
||||
}
|
||||
}
|
||||
|
||||
String path = getPath(config, host, type, extraArgs, signService, encryptService);
|
||||
String path = getPath(config, host, type, mergedExtras, signService, encryptService);
|
||||
HttpRequestConfig requestConfig = getHttpRequestConfig(config, path, signService.isSignMode());
|
||||
requestConfig.setUA(config.getUA());
|
||||
requestConfig.setAESEncryptService(encryptService);
|
||||
@@ -50,202 +47,62 @@ public class ResolveHostHelper {
|
||||
Map<String, String> extras,
|
||||
SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
//参数加密
|
||||
String enc = "";
|
||||
String query = getQuery(type);
|
||||
String version = "1.0";
|
||||
String tags = config.getBizTags();
|
||||
AESEncryptService.EncryptionMode mode = AESEncryptService.EncryptionMode.PLAIN;
|
||||
if (encryptService.isEncryptionMode()) {
|
||||
String encryptJson = buildEncryptionStr(host, query, extras, tags);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("encryptJson:" + encryptJson);
|
||||
}
|
||||
mode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? AESEncryptService.EncryptionMode.AES_GCM : AESEncryptService.EncryptionMode.AES_CBC;
|
||||
enc = encryptService.encrypt(encryptJson, mode);
|
||||
String qtype = getQType(type);
|
||||
StringBuilder query = new StringBuilder();
|
||||
appendQuery(query, "appId", config.getAccountId());
|
||||
appendQuery(query, "dn", host);
|
||||
appendQuery(query, "qtype", qtype);
|
||||
appendQuery(query, "sdk_version", BuildConfig.VERSION_NAME);
|
||||
appendQuery(query, "os", "android");
|
||||
String sid = SessionTrackMgr.getInstance().getSessionId();
|
||||
if (!TextUtils.isEmpty(sid)) {
|
||||
appendQuery(query, "sid", sid);
|
||||
}
|
||||
|
||||
String expireTime = signService.getExpireTime();
|
||||
|
||||
String queryStr = buildQueryStr(config.getAccountId(), mode.getMode(), host,
|
||||
query, extras, enc, expireTime, version, tags);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("query parameter:" + queryStr);
|
||||
}
|
||||
|
||||
//加签
|
||||
if (signService.isSignMode()) {
|
||||
Map<String, String> signParamMap = new HashMap<>();
|
||||
if (encryptService.isEncryptionMode()) {
|
||||
signParamMap.put("enc", enc);
|
||||
signParamMap.put("exp", expireTime);
|
||||
signParamMap.put("id", config.getAccountId());
|
||||
signParamMap.put("m", mode.getMode());
|
||||
signParamMap.put("v", version);
|
||||
}else {
|
||||
signParamMap.put("dn", host);
|
||||
signParamMap.put("exp", expireTime);
|
||||
signParamMap.put("id", config.getAccountId());
|
||||
signParamMap.put("m", mode.getMode());
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
signParamMap.put("q", query);
|
||||
}
|
||||
if (extras != null) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
signParamMap.put("sdns-" + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
signParamMap.put("tags", tags);
|
||||
}
|
||||
signParamMap.put("v", version);
|
||||
}
|
||||
|
||||
String sign = signService.sign(signParamMap);
|
||||
if (TextUtils.isEmpty(sign)) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("param sign fail");
|
||||
}
|
||||
}else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("sign:" + sign);
|
||||
}
|
||||
queryStr += "&s=" + sign;
|
||||
}
|
||||
}
|
||||
String path = "/v2/d?" + queryStr + "&sdk=android_" + BuildConfig.VERSION_NAME + getSid();
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("path:" + path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String buildQueryStr(String accountId, String mode, String host,
|
||||
String query, Map<String, String> extras, String enc,
|
||||
String expireTime, String version, String tags) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.append("id=").append(accountId);
|
||||
stringBuilder.append("&m=").append(mode);
|
||||
if (TextUtils.isEmpty(enc)) {
|
||||
stringBuilder.append("&dn=").append(host);
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
stringBuilder.append("&q=").append(query);
|
||||
}
|
||||
|
||||
String extra = getExtra(extras);
|
||||
if (!TextUtils.isEmpty(extra)) {
|
||||
stringBuilder.append(extra);
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
stringBuilder.append("&tags=").append(tags);
|
||||
}
|
||||
}else {
|
||||
stringBuilder.append("&enc=").append(enc);
|
||||
}
|
||||
stringBuilder.append("&v=").append(version);
|
||||
stringBuilder.append("&exp=").append(expireTime);
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private static String buildEncryptionStr(String host, String query, Map<String, String> extras, String tags) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("dn", host);
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
json.put("q", query);
|
||||
}
|
||||
if (!TextUtils.isEmpty(getExtra(extras))) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
if (!checkKey(entry.getKey()) || !checkValue(entry.getValue())) {
|
||||
continue;
|
||||
}
|
||||
json.put("sdns-" + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
json.put("tags", tags);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("encrypt param transfer to json fail.", e);
|
||||
}
|
||||
|
||||
}
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
private static String getQuery(RequestIpType type) {
|
||||
String query = "";
|
||||
switch (type) {
|
||||
case v6:
|
||||
query = "6";
|
||||
break;
|
||||
case both:
|
||||
query = "4,6";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private static String getExtra(Map<String, String> extras) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean isKey = true;
|
||||
boolean isValue = true;
|
||||
if (extras != null) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
sb.append("&sdns-");
|
||||
sb.append(entry.getKey());
|
||||
sb.append("=");
|
||||
sb.append(entry.getValue());
|
||||
if (!checkKey(entry.getKey())) {
|
||||
isKey = false;
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义key不合法:" + entry.getKey());
|
||||
break;
|
||||
}
|
||||
if (!checkValue(entry.getValue())) {
|
||||
isValue = false;
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义value不合法:" + entry.getValue());
|
||||
break;
|
||||
}
|
||||
String cip = extras.get("cip");
|
||||
if (!TextUtils.isEmpty(cip)) {
|
||||
appendQuery(query, "cip", cip);
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
if (isKey && isValue) {
|
||||
String extra = sb.toString();
|
||||
if (extra.getBytes(StandardCharsets.UTF_8).length <= 1000) {
|
||||
return extra;
|
||||
} else {
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义参数过长");
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
if (signService.isSignMode()) {
|
||||
String exp = signService.getExpireTime();
|
||||
String nonce = randomNonce();
|
||||
String sign = signService.signResolve(config.getAccountId(), host, qtype, exp, nonce);
|
||||
appendQuery(query, "exp", exp);
|
||||
appendQuery(query, "nonce", nonce);
|
||||
appendQuery(query, "sign", sign);
|
||||
}
|
||||
return "/resolve?" + query;
|
||||
}
|
||||
|
||||
private static boolean checkKey(String s) {
|
||||
return s.matches("[a-zA-Z0-9\\-_]+");
|
||||
private static void appendQuery(StringBuilder query, String key, String value) {
|
||||
if (TextUtils.isEmpty(key) || value == null) {
|
||||
return;
|
||||
}
|
||||
if (query.length() > 0) {
|
||||
query.append("&");
|
||||
}
|
||||
query.append(key).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static boolean checkValue(String s) {
|
||||
return s.matches("[a-zA-Z0-9\\-_=]+");
|
||||
private static String getQType(RequestIpType type) {
|
||||
if (type == RequestIpType.v6) {
|
||||
return "AAAA";
|
||||
}
|
||||
return "A";
|
||||
}
|
||||
|
||||
private static String randomNonce() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
public static HttpRequestConfig getConfig(HttpDnsConfig config, ArrayList<String> hostList,
|
||||
RequestIpType type, SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
//拼接host
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < hostList.size(); i++) {
|
||||
if (i != 0) {
|
||||
stringBuilder.append(",");
|
||||
}
|
||||
stringBuilder.append(hostList.get(i));
|
||||
String host = "";
|
||||
if (hostList != null && hostList.size() > 0) {
|
||||
host = hostList.get(0);
|
||||
}
|
||||
String host = stringBuilder.toString();
|
||||
String path = getPath(config, host, type, null, signService, encryptService);
|
||||
HttpRequestConfig requestConfig = getHttpRequestConfig(config, path, signService.isSignMode());
|
||||
requestConfig.setUA(config.getUA());
|
||||
@@ -268,14 +125,6 @@ public class ResolveHostHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTags(HttpDnsConfig config) {
|
||||
if (TextUtils.isEmpty(config.getBizTags())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "&tags=" + config.getBizTags();
|
||||
}
|
||||
|
||||
public static String getSid() {
|
||||
String sessionId = SessionTrackMgr.getInstance().getSessionId();
|
||||
if (sessionId == null) {
|
||||
@@ -284,4 +133,14 @@ public class ResolveHostHelper {
|
||||
return "&sid=" + sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容可观测模块透传 tags。
|
||||
*/
|
||||
public static String getTags(HttpDnsConfig config) {
|
||||
if (config == null || TextUtils.isEmpty(config.getBizTags())) {
|
||||
return "";
|
||||
}
|
||||
return "&tags=" + config.getBizTags();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsConfig;
|
||||
import com.alibaba.sdk.android.httpdns.impl.SignService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.BatchResolveHttpRequestStatusWatcher;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequest;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestConfig;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestTask;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestWatcher;
|
||||
import com.alibaba.sdk.android.httpdns.request.RequestCallback;
|
||||
import com.alibaba.sdk.android.httpdns.request.RetryHttpRequest;
|
||||
import com.alibaba.sdk.android.httpdns.serverip.RegionServerScheduleService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 发起域名解析请求
|
||||
*/
|
||||
@@ -43,9 +38,18 @@ public class ResolveHostRequestHandler {
|
||||
public void requestResolveHost(final String host, final RequestIpType type,
|
||||
Map<String, String> extras, final String cacheKey,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
if (type == RequestIpType.both) {
|
||||
requestResolveHostBoth(host, extras, cacheKey, callback);
|
||||
return;
|
||||
}
|
||||
requestResolveHostSingle(host, type, extras, cacheKey, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostSingle(final String host, final RequestIpType type,
|
||||
Map<String, String> extras, final String cacheKey,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
HttpRequestConfig requestConfig = ResolveHostHelper.getConfig(mHttpDnsConfig, host, type,
|
||||
extras, cacheKey, mGlobalParams, mSignService, mAESEncryptService);
|
||||
//补充可观测数据
|
||||
requestConfig.setResolvingHost(host);
|
||||
requestConfig.setResolvingIpType(type);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
@@ -54,30 +58,140 @@ public class ResolveHostRequestHandler {
|
||||
mCategoryController.getCategory().resolve(mHttpDnsConfig, requestConfig, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostBoth(final String host,
|
||||
final Map<String, String> extras,
|
||||
final String cacheKey,
|
||||
final RequestCallback<ResolveHostResponse> callback) {
|
||||
final ArrayList<ResolveHostResponse.HostItem> mergedItems = new ArrayList<>();
|
||||
final String[] serverIpHolder = new String[]{""};
|
||||
final Throwable[] errorHolder = new Throwable[]{null};
|
||||
|
||||
requestResolveHostSingle(host, RequestIpType.v4, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
requestResolveHostSingle(host, RequestIpType.v6, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (errorHolder[0] == null) {
|
||||
errorHolder[0] = throwable;
|
||||
}
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
errorHolder[0] = throwable;
|
||||
requestResolveHostSingle(host, RequestIpType.v6, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (errorHolder[0] == null) {
|
||||
errorHolder[0] = throwable;
|
||||
}
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void mergeResponse(ResolveHostResponse response,
|
||||
ArrayList<ResolveHostResponse.HostItem> mergedItems,
|
||||
String[] serverIpHolder) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
if (response.getItems() != null && !response.getItems().isEmpty()) {
|
||||
mergedItems.addAll(response.getItems());
|
||||
}
|
||||
if (serverIpHolder[0].isEmpty() && response.getServerIp() != null) {
|
||||
serverIpHolder[0] = response.getServerIp();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishBothResolve(RequestCallback<ResolveHostResponse> callback,
|
||||
ArrayList<ResolveHostResponse.HostItem> mergedItems,
|
||||
String serverIp,
|
||||
Throwable error) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
if (!mergedItems.isEmpty()) {
|
||||
callback.onSuccess(new ResolveHostResponse(mergedItems, serverIp));
|
||||
return;
|
||||
}
|
||||
if (error != null) {
|
||||
callback.onFail(error);
|
||||
return;
|
||||
}
|
||||
callback.onSuccess(new ResolveHostResponse(new ArrayList<ResolveHostResponse.HostItem>(), serverIp));
|
||||
}
|
||||
|
||||
public void requestResolveHost(final ArrayList<String> hostList, final RequestIpType type,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
HttpRequestConfig requestConfig = ResolveHostHelper.getConfig(mHttpDnsConfig, hostList,
|
||||
type, mSignService, mAESEncryptService);
|
||||
requestConfig.setResolvingIpType(type);
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
if (hostList == null || hostList.isEmpty()) {
|
||||
callback.onSuccess(new ResolveHostResponse(new ArrayList<ResolveHostResponse.HostItem>(), ""));
|
||||
return;
|
||||
}
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("start resolve hosts async for " + hostList.toString() + " " + type);
|
||||
}
|
||||
|
||||
HttpRequest<ResolveHostResponse> request = new HttpRequest<>(
|
||||
requestConfig, new ResolveHostResponseParser(mAESEncryptService));
|
||||
request = new HttpRequestWatcher<>(request,
|
||||
new BatchResolveHttpRequestStatusWatcher(mHttpDnsConfig.getObservableManager()));
|
||||
// 切换服务IP,更新服务IP
|
||||
request = new HttpRequestWatcher<>(request, new ShiftServerWatcher(mHttpDnsConfig,
|
||||
mScheduleService, mCategoryController));
|
||||
// 重试一次
|
||||
request = new RetryHttpRequest<>(request, 1);
|
||||
try {
|
||||
mHttpDnsConfig.getResolveWorker().execute(
|
||||
new HttpRequestTask<>(request, callback));
|
||||
} catch (Throwable e) {
|
||||
callback.onFail(e);
|
||||
final ArrayList<ResolveHostResponse.HostItem> allItems = new ArrayList<>();
|
||||
final String[] serverIpHolder = new String[]{""};
|
||||
requestResolveHostsSequentially(hostList, 0, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostsSequentially(final ArrayList<String> hostList,
|
||||
final int index,
|
||||
final RequestIpType type,
|
||||
final ArrayList<ResolveHostResponse.HostItem> allItems,
|
||||
final String[] serverIpHolder,
|
||||
final RequestCallback<ResolveHostResponse> callback) {
|
||||
if (index >= hostList.size()) {
|
||||
callback.onSuccess(new ResolveHostResponse(allItems, serverIpHolder[0]));
|
||||
return;
|
||||
}
|
||||
final String host = hostList.get(index);
|
||||
requestResolveHost(host, type, null, null, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
if (resolveHostResponse != null) {
|
||||
if (resolveHostResponse.getItems() != null) {
|
||||
allItems.addAll(resolveHostResponse.getItems());
|
||||
}
|
||||
if (serverIpHolder[0].isEmpty()) {
|
||||
serverIpHolder[0] = resolveHostResponse.getServerIp();
|
||||
}
|
||||
}
|
||||
requestResolveHostsSequentially(hostList, index + 1, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.w("batch resolve host fail: " + host, throwable);
|
||||
}
|
||||
requestResolveHostsSequentially(hostList, index + 1, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,14 +202,14 @@ public class ResolveHostRequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置嗅探模式的请求时间间隔
|
||||
* 设置嗅探模式请求时间间隔
|
||||
*/
|
||||
public void setSniffTimeInterval(int timeInterval) {
|
||||
mCategoryController.setSniffTimeInterval(timeInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sdns的全局参数
|
||||
* 设置 SDNS 全局参数(当前服务端不使用,保留兼容)
|
||||
*/
|
||||
public void setSdnsGlobalParams(Map<String, String> params) {
|
||||
this.mGlobalParams.clear();
|
||||
@@ -105,10 +219,9 @@ public class ResolveHostRequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除sdns的全局参数
|
||||
* 清除 SDNS 全局参数
|
||||
*/
|
||||
public void clearSdnsGlobalParams() {
|
||||
this.mGlobalParams.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -122,78 +123,47 @@ public class ResolveHostResponse {
|
||||
}
|
||||
|
||||
public static ResolveHostResponse fromResponse(String serverIp, String body) throws JSONException {
|
||||
|
||||
ArrayList<HostItem> items = new ArrayList<>();
|
||||
JSONObject jsonObject = new JSONObject(body);
|
||||
|
||||
if (jsonObject.has("answers")) {
|
||||
JSONArray answers = jsonObject.getJSONArray("answers");
|
||||
for (int i = 0; i < answers.length(); i++) {
|
||||
JSONObject answer = answers.getJSONObject(i);
|
||||
String hostName = null;
|
||||
int ttl = 0;
|
||||
String extra = null;
|
||||
String[] ips = null;
|
||||
String[] ipsv6 = null;
|
||||
String noIpCode = null;
|
||||
if (answer.has("dn")) {
|
||||
hostName = answer.getString("dn");
|
||||
JSONObject responseObject = new JSONObject(body);
|
||||
JSONObject data = responseObject.optJSONObject("data");
|
||||
if (data == null) {
|
||||
return new ResolveHostResponse(items, serverIp);
|
||||
}
|
||||
String hostName = data.optString("domain");
|
||||
int ttl = data.optInt("ttl", 60);
|
||||
String summary = data.optString("summary", null);
|
||||
JSONArray records = data.optJSONArray("records");
|
||||
HashMap<RequestIpType, ArrayList<String>> typedIPs = new HashMap<>();
|
||||
if (records != null) {
|
||||
for (int i = 0; i < records.length(); i++) {
|
||||
JSONObject record = records.optJSONObject(i);
|
||||
if (record == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (answer.has("v4")) {
|
||||
JSONObject ipv4 = answer.getJSONObject("v4");
|
||||
if (ipv4.has("ips")) {
|
||||
JSONArray ipArray = ipv4.getJSONArray("ips");
|
||||
if (ipArray.length() != 0) {
|
||||
ips = new String[ipArray.length()];
|
||||
for (int j = 0; j < ipArray.length(); j++) {
|
||||
ips[j] = ipArray.getString(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipv4.has("ttl")) {
|
||||
ttl = ipv4.getInt("ttl");
|
||||
}
|
||||
if (ipv4.has("extra")) {
|
||||
extra = ipv4.getString("extra");
|
||||
}
|
||||
if (ipv4.has("no_ip_code")) {
|
||||
noIpCode = ipv4.getString("no_ip_code");
|
||||
}
|
||||
items.add(new HostItem(hostName, RequestIpType.v4, ips, ttl, extra, noIpCode));
|
||||
if (!TextUtils.isEmpty(noIpCode)) {
|
||||
HttpDnsLog.w("RESOLVE FAIL, HOST:" + hostName + ", QUERY:4, "
|
||||
+ "Msg:" + noIpCode);
|
||||
}
|
||||
String ip = record.optString("ip");
|
||||
if (TextUtils.isEmpty(ip)) {
|
||||
continue;
|
||||
}
|
||||
if (answer.has("v6")) {
|
||||
JSONObject ipv6 = answer.getJSONObject("v6");
|
||||
if (ipv6.has("ips")) {
|
||||
JSONArray ipArray = ipv6.getJSONArray("ips");
|
||||
if (ipArray.length() != 0) {
|
||||
ipsv6 = new String[ipArray.length()];
|
||||
for (int j = 0; j < ipArray.length(); j++) {
|
||||
ipsv6[j] = ipArray.getString(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipv6.has("ttl")) {
|
||||
ttl = ipv6.getInt("ttl");
|
||||
}
|
||||
if (ipv6.has("extra")) {
|
||||
extra = ipv6.getString("extra");
|
||||
}
|
||||
if (ipv6.has("no_ip_code")) {
|
||||
noIpCode = ipv6.getString("no_ip_code");
|
||||
}
|
||||
items.add(new HostItem(hostName, RequestIpType.v6, ipsv6, ttl, extra, noIpCode));
|
||||
if (!TextUtils.isEmpty(noIpCode)) {
|
||||
HttpDnsLog.w("RESOLVE FAIL, HOST:" + hostName + ", QUERY:6, "
|
||||
+ "Msg:" + noIpCode);
|
||||
}
|
||||
String recordType = record.optString("type", data.optString("qtype", "A")).toUpperCase();
|
||||
RequestIpType ipType = "AAAA".equals(recordType) ? RequestIpType.v6 : RequestIpType.v4;
|
||||
ArrayList<String> list = typedIPs.get(ipType);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
typedIPs.put(ipType, list);
|
||||
}
|
||||
list.add(ip);
|
||||
}
|
||||
}
|
||||
if (typedIPs.isEmpty()) {
|
||||
String qtype = data.optString("qtype", "A");
|
||||
RequestIpType ipType = "AAAA".equalsIgnoreCase(qtype) ? RequestIpType.v6 : RequestIpType.v4;
|
||||
items.add(new HostItem(hostName, ipType, null, ttl, summary, null));
|
||||
} else {
|
||||
for (RequestIpType type : typedIPs.keySet()) {
|
||||
ArrayList<String> ipList = typedIPs.get(type);
|
||||
String[] ips = ipList == null ? null : ipList.toArray(new String[0]);
|
||||
items.add(new HostItem(hostName, type, ips, ttl, summary, null));
|
||||
}
|
||||
|
||||
}
|
||||
return new ResolveHostResponse(items, serverIp);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,27 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.ResponseParser;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class ResolveHostResponseParser implements
|
||||
ResponseParser<ResolveHostResponse> {
|
||||
|
||||
private final AESEncryptService mAESEncryptService;
|
||||
public class ResolveHostResponseParser implements ResponseParser<ResolveHostResponse> {
|
||||
|
||||
public ResolveHostResponseParser(AESEncryptService aesEncryptService) {
|
||||
mAESEncryptService = aesEncryptService;
|
||||
// 新版协议固定 HTTPS,不使用 AES 响应体解密,保留构造参数仅为兼容调用方。
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolveHostResponse parse(String serverIp, String response) throws Throwable {
|
||||
String data = "";
|
||||
JSONObject jsonResponse = new JSONObject(response);
|
||||
if (jsonResponse.has("code")) {
|
||||
String code = jsonResponse.getString("code");
|
||||
if (TextUtils.equals(code, "success")) {
|
||||
if (jsonResponse.has("data")) {
|
||||
data = jsonResponse.getString("data");
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
//解密
|
||||
AESEncryptService.EncryptionMode mode = AESEncryptService.EncryptionMode.PLAIN;
|
||||
if (jsonResponse.has("mode")) {
|
||||
String serverEncryptMode = jsonResponse.getString("mode");
|
||||
if (TextUtils.equals(serverEncryptMode, AESEncryptService.EncryptionMode.AES_GCM.getMode())) {
|
||||
mode = AESEncryptService.EncryptionMode.AES_GCM;
|
||||
} else if (TextUtils.equals(serverEncryptMode, AESEncryptService.EncryptionMode.AES_CBC.getMode())) {
|
||||
mode = AESEncryptService.EncryptionMode.AES_CBC;
|
||||
}
|
||||
}
|
||||
data = mAESEncryptService.decrypt(data, mode);
|
||||
if (TextUtils.isEmpty(data)) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response data decrypt fail");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response data is empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("解析失败,原因为" + code);
|
||||
}
|
||||
throw new Exception(code);
|
||||
}
|
||||
}else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response don't have code");
|
||||
JSONObject responseObject = new JSONObject(response);
|
||||
String code = responseObject.optString("code");
|
||||
if (!"SUCCESS".equalsIgnoreCase(code)) {
|
||||
String message = responseObject.optString("message");
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
throw new Exception(code + ":" + message);
|
||||
}
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("request success " + data);
|
||||
}
|
||||
return ResolveHostResponse.fromResponse(serverIp, data);
|
||||
return ResolveHostResponse.fromResponse(serverIp, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
/**
|
||||
@@ -74,6 +76,20 @@ public class HttpDns {
|
||||
InitConfig.addConfig(accountId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或者禁用httpdns,理论上这个是内部接口,不给外部使用的
|
||||
* 但是已经对外暴露,所以保留
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ext {
|
||||
httpdnsDebugVersion = '2.6.7'
|
||||
httpdnsDebugVersion = '1.0.0'
|
||||
|
||||
loggerVersion = '1.2.0'
|
||||
crashDefendVersion = '0.0.6'
|
||||
utdidVersion = '2.6.0'
|
||||
ipdetectorVersion = '1.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
149
EdgeHttpDNS/sdk/build_packages.sh
Normal file
149
EdgeHttpDNS/sdk/build_packages.sh
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
DIST_DIR="$ROOT/dist"
|
||||
TMP_DIR="$ROOT/.tmp_sdk_pkg"
|
||||
|
||||
function lookup_version() {
|
||||
VERSION_FILE="$ROOT/../internal/const/const.go"
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "0.0.0"
|
||||
return
|
||||
fi
|
||||
VERSION=$(grep -E 'Version[[:space:]]*=' "$VERSION_FILE" | head -n 1 | sed -E 's/.*"([0-9.]+)".*/\1/')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "0.0.0"
|
||||
else
|
||||
echo "$VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_cmd() {
|
||||
CMD=$1
|
||||
if ! command -v "$CMD" >/dev/null 2>&1; then
|
||||
echo "missing command: $CMD"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function zip_dir() {
|
||||
SRC_DIR=$1
|
||||
ZIP_FILE=$2
|
||||
(
|
||||
cd "$SRC_DIR" || exit 1
|
||||
zip -r -X -q "$ZIP_FILE" .
|
||||
)
|
||||
}
|
||||
|
||||
function build_android_sdk_package() {
|
||||
echo "[sdk] building android aar ..."
|
||||
ensure_cmd zip
|
||||
if [ ! -x "$ROOT/android/gradlew" ]; then
|
||||
echo "android gradlew not found: $ROOT/android/gradlew"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$ROOT/android" || exit 1
|
||||
./gradlew :httpdns-sdk:clean :httpdns-sdk:assembleNormalRelease
|
||||
)
|
||||
|
||||
AAR_FILE=$(find "$ROOT/android/httpdns-sdk/build/outputs/aar" -type f -name "*normal-release*.aar" | head -n 1)
|
||||
if [ -z "$AAR_FILE" ]; then
|
||||
AAR_FILE=$(find "$ROOT/android/httpdns-sdk/build/outputs/aar" -type f -name "*release*.aar" | head -n 1)
|
||||
fi
|
||||
if [ -z "$AAR_FILE" ] || [ ! -f "$AAR_FILE" ]; then
|
||||
echo "android aar is not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/android"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp "$AAR_FILE" "$PKG_DIR/alicloud-android-httpdns.aar"
|
||||
if [ -f "$ROOT/android/README.md" ]; then
|
||||
cp "$ROOT/android/README.md" "$PKG_DIR/README.md"
|
||||
fi
|
||||
if [ -f "$ROOT/android/httpdns-sdk/proguard-rules.pro" ]; then
|
||||
cp "$ROOT/android/httpdns-sdk/proguard-rules.pro" "$PKG_DIR/proguard-rules.pro"
|
||||
fi
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-android.zip"
|
||||
}
|
||||
|
||||
function build_ios_sdk_package() {
|
||||
echo "[sdk] packaging ios xcframework ..."
|
||||
ensure_cmd zip
|
||||
|
||||
CANDIDATES=(
|
||||
"$ROOT/ios/dist/AlicloudHttpDNS.xcframework"
|
||||
"$ROOT/ios/AlicloudHttpDNS.xcframework"
|
||||
"$ROOT/ios/Build/AlicloudHttpDNS.xcframework"
|
||||
)
|
||||
|
||||
XCFRAMEWORK_DIR=""
|
||||
for path in "${CANDIDATES[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
XCFRAMEWORK_DIR="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$XCFRAMEWORK_DIR" ]; then
|
||||
echo "ios xcframework not found."
|
||||
echo "please build it on macOS first, then place AlicloudHttpDNS.xcframework under EdgeHttpDNS/sdk/ios/dist/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/ios"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp -R "$XCFRAMEWORK_DIR" "$PKG_DIR/"
|
||||
if [ -f "$ROOT/ios/AlicloudHTTPDNS.podspec" ]; then
|
||||
cp "$ROOT/ios/AlicloudHTTPDNS.podspec" "$PKG_DIR/"
|
||||
fi
|
||||
if [ -f "$ROOT/ios/README.md" ]; then
|
||||
cp "$ROOT/ios/README.md" "$PKG_DIR/README.md"
|
||||
fi
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-ios.zip"
|
||||
}
|
||||
|
||||
function build_flutter_sdk_package() {
|
||||
echo "[sdk] packaging flutter plugin ..."
|
||||
ensure_cmd zip
|
||||
PLUGIN_DIR="$ROOT/flutter/aliyun_httpdns"
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
echo "flutter plugin directory not found: $PLUGIN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/flutter"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp -R "$PLUGIN_DIR" "$PKG_DIR/"
|
||||
rm -rf "$PKG_DIR/aliyun_httpdns/example/.dart_tool" "$PKG_DIR/aliyun_httpdns/example/build" "$PKG_DIR/aliyun_httpdns/.dart_tool" "$PKG_DIR/aliyun_httpdns/build"
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-flutter.zip"
|
||||
}
|
||||
|
||||
function main() {
|
||||
VERSION=$(lookup_version)
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR" "$DIST_DIR"
|
||||
|
||||
build_android_sdk_package
|
||||
build_ios_sdk_package
|
||||
build_flutter_sdk_package
|
||||
|
||||
cp "$DIST_DIR/httpdns-sdk-android.zip" "$DIST_DIR/httpdns-sdk-android-v${VERSION}.zip"
|
||||
cp "$DIST_DIR/httpdns-sdk-ios.zip" "$DIST_DIR/httpdns-sdk-ios-v${VERSION}.zip"
|
||||
cp "$DIST_DIR/httpdns-sdk-flutter.zip" "$DIST_DIR/httpdns-sdk-flutter-v${VERSION}.zip"
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
echo "[sdk] done. output: $DIST_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -1,532 +1,73 @@
|
||||
# Aliyun HTTPDNS Flutter Plugin
|
||||
# HTTPDNS Flutter SDK (SNI Hidden v1.0.0)
|
||||
|
||||
阿里云EMAS HTTPDNS Flutter插件,提供基于原生SDK的域名解析能力。
|
||||
|
||||
一、快速入门
|
||||
-----------------------
|
||||
|
||||
### 1.1 开通服务
|
||||
|
||||
请参考[快速入门文档](https://help.aliyun.com/document_detail/2867674.html)开通HTTPDNS。
|
||||
|
||||
### 1.2 获取配置
|
||||
|
||||
请参考开发配置文档在EMAS控制台开发配置中获取AccountId/SecretKey/AESSecretKey等信息,用于初始化SDK。
|
||||
|
||||
## 二、安装
|
||||
|
||||
在`pubspec.yaml`中加入dependencies:
|
||||
```yaml
|
||||
dependencies:
|
||||
aliyun_httpdns: ^1.0.1
|
||||
```
|
||||
添加依赖之后需要执行一次 `flutter pub get`。
|
||||
|
||||
### 原生SDK版本说明
|
||||
|
||||
插件已集成了对应平台的HTTPDNS原生SDK,当前版本:
|
||||
|
||||
- **Android**: `com.aliyun.ams:alicloud-android-httpdns:2.6.7`
|
||||
- **iOS**: `AlicloudHTTPDNS:3.3.0`
|
||||
|
||||
|
||||
|
||||
三、配置和使用
|
||||
------------------------
|
||||
|
||||
### 3.1 初始化配置
|
||||
|
||||
应用启动后,需要先初始化插件,才能调用HTTPDNS能力。
|
||||
初始化主要是配置AccountId/SecretKey等信息及功能开关。
|
||||
示例代码如下:
|
||||
## 1. Initialization
|
||||
|
||||
```dart
|
||||
// 初始化 HTTPDNS
|
||||
await AliyunHttpdns.init(
|
||||
accountId: '您的AccountId',
|
||||
secretKey: '您的SecretKey',
|
||||
);
|
||||
|
||||
// 设置功能选项
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
|
||||
// 构建服务
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
// 设置预解析域名
|
||||
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
|
||||
print("init success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.1.1 日志配置
|
||||
|
||||
应用开发过程中,如果要输出HTTPDNS的日志,可以调用日志输出控制方法,开启日志,示例代码如下:
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
print("enableLog success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.1.2 sessionId记录
|
||||
|
||||
应用在运行过程中,可以调用获取SessionId方法获取sessionId,记录到应用的数据采集系统中。
|
||||
sessionId用于表示标识一次应用运行,线上排查时,可以用于查询应用一次运行过程中的解析日志,示例代码如下:
|
||||
|
||||
```dart
|
||||
final sessionId = await AliyunHttpdns.getSessionId();
|
||||
print("SessionId = $sessionId");
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3.2 域名解析
|
||||
|
||||
#### 3.2.1 预解析
|
||||
|
||||
当需要提前解析域名时,可以调用预解析域名方法,示例代码如下:
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveHosts(["www.aliyun.com", "www.example.com"], ipType: 'both');
|
||||
print("preResolveHosts success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
调用之后,插件会发起域名解析,并把结果缓存到内存,用于后续请求时直接使用。
|
||||
|
||||
### 3.2.2 域名解析
|
||||
|
||||
当需要解析域名时,可以通过调用域名解析方法解析域名获取IP,示例代码如下:
|
||||
|
||||
```dart
|
||||
Future<void> _resolve() async {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking('www.aliyun.com', ipType: 'both');
|
||||
final ipv4List = res['ipv4'] ?? [];
|
||||
final ipv6List = res['ipv6'] ?? [];
|
||||
print('IPv4: $ipv4List');
|
||||
print('IPv6: $ipv6List');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
四、Flutter最佳实践
|
||||
------------------------------
|
||||
|
||||
### 4.1 原理说明
|
||||
|
||||
本示例展示了一种更直接的集成方式,通过自定义HTTP客户端适配器来实现HTTPDNS集成:
|
||||
|
||||
1. 创建自定义的HTTP客户端适配器,拦截网络请求
|
||||
2. 在适配器中调用HTTPDNS插件解析域名为IP地址
|
||||
3. 使用解析得到的IP地址创建直接的Socket连接
|
||||
4. 对于HTTPS连接,确保正确设置SNI(Server Name Indication)为原始域名
|
||||
|
||||
这种方式避免了创建本地代理服务的复杂性,直接在HTTP客户端层面集成HTTPDNS功能。
|
||||
|
||||
### 4.2 示例说明
|
||||
|
||||
完整应用示例请参考插件包中example应用。
|
||||
|
||||
#### 4.2.1 自定义HTTP客户端适配器实现
|
||||
|
||||
自定义适配器的实现请参考插件包中example/lib/net/httpdns_http_client_adapter.dart文件。本方案由EMAS团队设计实现,参考请注明出处。
|
||||
适配器内部会拦截HTTP请求,调用HTTPDNS进行域名解析,并使用解析后的IP创建socket连接。
|
||||
|
||||
本示例支持三种网络库:Dio、HttpClient、http包。代码如下:
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:aliyun_httpdns/aliyun_httpdns.dart';
|
||||
|
||||
// Dio 适配器
|
||||
IOHttpClientAdapter buildHttpdnsHttpClientAdapter() {
|
||||
final HttpClient client = HttpClient();
|
||||
_configureHttpClient(client);
|
||||
_configureConnectionFactory(client);
|
||||
|
||||
final IOHttpClientAdapter adapter = IOHttpClientAdapter(createHttpClient: () => client)
|
||||
..validateCertificate = (cert, host, port) => true;
|
||||
return adapter;
|
||||
}
|
||||
|
||||
// 原生 HttpClient
|
||||
HttpClient buildHttpdnsNativeHttpClient() {
|
||||
final HttpClient client = HttpClient();
|
||||
_configureHttpClient(client);
|
||||
_configureConnectionFactory(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
// http 包适配器
|
||||
http.Client buildHttpdnsHttpPackageClient() {
|
||||
final HttpClient httpClient = buildHttpdnsNativeHttpClient();
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
// HttpClient 基础配置
|
||||
void _configureHttpClient(HttpClient client) {
|
||||
client.findProxy = (Uri _) => 'DIRECT';
|
||||
client.idleTimeout = const Duration(seconds: 90);
|
||||
client.maxConnectionsPerHost = 8;
|
||||
}
|
||||
|
||||
// 配置基于 HTTPDNS 的连接工厂
|
||||
// 本方案由EMAS团队设计实现,参考请注明出处。
|
||||
void _configureConnectionFactory(HttpClient client) {
|
||||
client.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final String domain = uri.host;
|
||||
final bool https = uri.scheme.toLowerCase() == 'https';
|
||||
final int port = uri.port == 0 ? (https ? 443 : 80) : uri.port;
|
||||
|
||||
final List<InternetAddress> targets = await _resolveTargets(domain);
|
||||
final Object target = targets.isNotEmpty ? targets.first : domain;
|
||||
|
||||
if (!https) {
|
||||
return Socket.startConnect(target, port);
|
||||
}
|
||||
|
||||
// HTTPS:先 TCP,再 TLS(SNI=域名),并保持可取消
|
||||
bool cancelled = false;
|
||||
final Future<ConnectionTask<Socket>> rawStart = Socket.startConnect(target, port);
|
||||
final Future<Socket> upgraded = rawStart.then((task) async {
|
||||
final Socket raw = await task.socket;
|
||||
if (cancelled) {
|
||||
raw.destroy();
|
||||
throw const SocketException('Connection cancelled');
|
||||
}
|
||||
final SecureSocket secure = await SecureSocket.secure(
|
||||
raw,
|
||||
host: domain, // 重要:使用原始域名作为SNI
|
||||
);
|
||||
if (cancelled) {
|
||||
secure.destroy();
|
||||
throw const SocketException('Connection cancelled');
|
||||
}
|
||||
return secure;
|
||||
});
|
||||
return ConnectionTask.fromSocket(
|
||||
upgraded,
|
||||
() {
|
||||
cancelled = true;
|
||||
try {
|
||||
rawStart.then((t) => t.cancel());
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 通过 HTTPDNS 解析目标 IP 列表
|
||||
Future<List<InternetAddress>> _resolveTargets(String domain) async {
|
||||
try {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(domain, ipType: 'both');
|
||||
final List<String> ipv4 = res['ipv4'] ?? [];
|
||||
final List<String> ipv6 = res['ipv6'] ?? [];
|
||||
final List<InternetAddress> targets = [
|
||||
...ipv4.map(InternetAddress.tryParse).whereType<InternetAddress>(),
|
||||
...ipv6.map(InternetAddress.tryParse).whereType<InternetAddress>(),
|
||||
];
|
||||
if (targets.isEmpty) {
|
||||
debugPrint('[dio] HTTPDNS no result for $domain, fallback to system DNS');
|
||||
} else {
|
||||
debugPrint('[dio] HTTPDNS resolved $domain -> ${targets.first.address}');
|
||||
}
|
||||
return targets;
|
||||
} catch (e) {
|
||||
debugPrint('[dio] HTTPDNS resolve failed: $e, fallback to system DNS');
|
||||
return const <InternetAddress>[];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 4.2.2 适配器集成和使用
|
||||
|
||||
适配器的集成请参考插件包中example/lib/main.dart文件。
|
||||
首先需要初始化HTTPDNS,然后配置网络库使用自定义适配器,示例代码如下:
|
||||
|
||||
```dart
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
late final Dio _dio;
|
||||
late final HttpClient _httpClient;
|
||||
late final http.Client _httpPackageClient;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 初始化 HTTPDNS
|
||||
_initHttpDnsOnce();
|
||||
|
||||
// 配置网络库使用 HTTPDNS 适配器
|
||||
_dio = Dio();
|
||||
_dio.httpClientAdapter = buildHttpdnsHttpClientAdapter();
|
||||
_dio.options.headers['Connection'] = 'keep-alive';
|
||||
|
||||
_httpClient = buildHttpdnsNativeHttpClient();
|
||||
_httpPackageClient = buildHttpdnsHttpPackageClient();
|
||||
}
|
||||
|
||||
Future<void> _initHttpDnsOnce() async {
|
||||
try {
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000,
|
||||
secretKey: '您的SecretKey',
|
||||
);
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
// 设置预解析域名
|
||||
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
|
||||
} catch (e) {
|
||||
debugPrint('[httpdns] init failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
使用配置好的网络库发起请求时,会自动使用HTTPDNS进行域名解析:
|
||||
|
||||
```dart
|
||||
// 使用 Dio
|
||||
final response = await _dio.get('https://www.aliyun.com');
|
||||
|
||||
// 使用 HttpClient
|
||||
final request = await _httpClient.getUrl(Uri.parse('https://www.aliyun.com'));
|
||||
final response = await request.close();
|
||||
|
||||
// 使用 http 包
|
||||
final response = await _httpPackageClient.get(Uri.parse('https://www.aliyun.com'));
|
||||
```
|
||||
|
||||
#### 4.2.3 资源清理
|
||||
|
||||
在组件销毁时,记得清理相关资源:
|
||||
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_urlController.dispose();
|
||||
_httpClient.close();
|
||||
_httpPackageClient.close();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
五、API
|
||||
----------------------
|
||||
|
||||
### 5.1 日志输出控制
|
||||
|
||||
控制是否打印Log。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
print("enableLog success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 5.2 初始化
|
||||
|
||||
初始化配置, 在应用启动时调用。
|
||||
|
||||
```dart
|
||||
// 基础初始化
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000,
|
||||
secretKey: 'your_secret_key',
|
||||
aesSecretKey: 'your_aes_secret_key', // 可选
|
||||
appId: 'app1f1ndpo9',
|
||||
primaryServiceHost: 'httpdns-a.example.com',
|
||||
backupServiceHost: 'httpdns-b.example.com', // optional
|
||||
servicePort: 443,
|
||||
secretKey: 'your-sign-secret', // optional if sign is enabled
|
||||
);
|
||||
|
||||
// 配置功能选项
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
|
||||
// 构建服务实例
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
print("init success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
初始化参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 | 支持平台 |
|
||||
|-------------|--------|------|------------|-------------|
|
||||
| accountId | int | 必选参数 | Account ID | Android/iOS |
|
||||
| secretKey | String | 可选参数 | 加签密钥 | Android/iOS |
|
||||
| aesSecretKey| String | 可选参数 | 加密密钥 | Android/iOS |
|
||||
|
||||
功能配置方法:
|
||||
|
||||
- `setHttpsRequestEnabled(bool)` - 设置是否使用HTTPS解析链路
|
||||
- `setLogEnabled(bool)` - 设置是否开启日志
|
||||
- `setPersistentCacheIPEnabled(bool)` - 设置是否开启持久化缓存
|
||||
- `setReuseExpiredIPEnabled(bool)` - 设置是否允许复用过期IP
|
||||
- `setPreResolveAfterNetworkChanged(bool)` - 设置网络切换时是否自动刷新解析
|
||||
|
||||
|
||||
|
||||
### 5.3 域名解析
|
||||
|
||||
解析指定域名。
|
||||
## 2. Resolve
|
||||
|
||||
```dart
|
||||
Future<void> _resolve() async {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
'www.aliyun.com',
|
||||
ipType: 'both', // 'auto', 'ipv4', 'ipv6', 'both'
|
||||
);
|
||||
|
||||
final ipv4List = res['ipv4'] ?? [];
|
||||
final ipv6List = res['ipv6'] ?? [];
|
||||
print('IPv4: $ipv4List');
|
||||
print('IPv6: $ipv6List');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|------------|---------------------|------|----------------------------------------|
|
||||
| hostname | String | 必选参数 | 要解析的域名 |
|
||||
| ipType | String | 可选参数 | 请求IP类型: 'auto', 'ipv4', 'ipv6', 'both' |
|
||||
|
||||
|
||||
|
||||
返回数据结构:
|
||||
|
||||
| 字段名 | 类型 | 功能 |
|
||||
|------|--------------|----------------------------------|
|
||||
| ipv4 | List<String> | IPv4地址列表,如: ["1.1.1.1", "2.2.2.2"] |
|
||||
| ipv6 | List<String> | IPv6地址列表,如: ["::1", "::2"] |
|
||||
|
||||
|
||||
|
||||
### 5.4 预解析域名
|
||||
|
||||
预解析域名, 解析后缓存在SDK中,下次解析时直接从缓存中获取,提高解析速度。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveHosts(
|
||||
["www.aliyun.com", "www.example.com"],
|
||||
ipType: 'both'
|
||||
final result = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
'api.business.com',
|
||||
ipType: 'both',
|
||||
);
|
||||
print("preResolveHosts success");
|
||||
|
||||
final ipv4 = result['ipv4'] ?? <String>[];
|
||||
final ipv6 = result['ipv6'] ?? <String>[];
|
||||
```
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|--------|--------------|------|----------------------------------------|
|
||||
| hosts | List<String> | 必选参数 | 预解析域名列表 |
|
||||
| ipType | String | 可选参数 | 请求IP类型: 'auto', 'ipv4', 'ipv6', 'both' |
|
||||
|
||||
|
||||
|
||||
### 5.5 获取SessionId
|
||||
|
||||
获取SessionId, 用于排查追踪问题。
|
||||
## 3. Official HTTP Adapter (IP + Host)
|
||||
|
||||
```dart
|
||||
final sessionId = await AliyunHttpdns.getSessionId();
|
||||
print("SessionId = $sessionId");
|
||||
```
|
||||
|
||||
|
||||
|
||||
无需参数,直接返回当前会话ID。
|
||||
|
||||
### 5.6 清除缓存
|
||||
|
||||
清除所有DNS解析缓存。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.cleanAllHostCache();
|
||||
print("缓存清除成功");
|
||||
```
|
||||
|
||||
### 5.7 持久化缓存配置
|
||||
|
||||
设置是否开启持久化缓存功能。开启后,SDK 会将解析结果保存到本地,App 重启后可以从本地加载缓存,提升首屏加载速度。
|
||||
|
||||
```dart
|
||||
// 基础用法:开启持久化缓存
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
|
||||
// 高级用法:开启持久化缓存并设置过期时间阈值
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(
|
||||
true,
|
||||
discardExpiredAfterSeconds: 86400 // 1天,单位:秒
|
||||
final adapter = AliyunHttpdns.createHttpAdapter(
|
||||
options: const AliyunHttpdnsAdapterOptions(
|
||||
ipType: 'auto',
|
||||
connectTimeoutMs: 3000,
|
||||
readTimeoutMs: 5000,
|
||||
allowInsecureCertificatesForDebugOnly: false,
|
||||
),
|
||||
);
|
||||
print("持久化缓存已开启");
|
||||
|
||||
final resp = await adapter.request(
|
||||
Uri.parse('https://api.business.com/v1/ping'),
|
||||
method: 'GET',
|
||||
headers: {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
print(resp.statusCode);
|
||||
print(resp.usedIp);
|
||||
```
|
||||
|
||||
参数:
|
||||
Behavior is fixed:
|
||||
- Resolve host by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as the real business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|----------------------------|------|------|------------------------------------------|
|
||||
| enabled | bool | 必选参数 | 是否开启持久化缓存 |
|
||||
| discardExpiredAfterSeconds | int | 可选参数 | 过期时间阈值(秒),App 启动时会丢弃过期超过此时长的缓存记录,建议设置为 1 天(86400 秒) |
|
||||
|
||||
注意事项:
|
||||
- 持久化缓存仅影响第一次域名解析结果,后续解析仍会请求 HTTPDNS 服务器
|
||||
- 如果业务服务器 IP 变化频繁,建议谨慎开启此功能
|
||||
- 建议在 `build()` 之前调用此接口
|
||||
|
||||
### 5.8 网络变化时自动刷新预解析
|
||||
|
||||
设置在网络环境变化时是否自动刷新预解析域名的缓存。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true);
|
||||
print("网络变化自动刷新已启用");
|
||||
```
|
||||
|
||||
### 5.9 IP 优选
|
||||
|
||||
设置需要进行 IP 优选的域名列表。开启后,SDK 会对解析返回的 IP 列表进行 TCP 测速并排序,优先返回连接速度最快的 IP。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setIPRankingList({
|
||||
'www.aliyun.com': 443,
|
||||
});
|
||||
print("IP 优选配置成功");
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|-------------|-----------------|------|------------------------------|
|
||||
| hostPortMap | Map<String, int> | 必选参数 | 域名和端口的映射,例如:{'www.aliyun.com': 443} |
|
||||
## 4. Public Errors
|
||||
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
## 5. Deprecated Params Removed
|
||||
|
||||
The public init API no longer accepts:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.aliyun.ams.httpdns
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig
|
||||
@@ -15,18 +14,18 @@ import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
private var appContext: Context? = null
|
||||
|
||||
// Cached service keyed by accountId to avoid re-creating
|
||||
private var service: HttpDnsService? = null
|
||||
private var accountId: String? = null
|
||||
private var secretKey: String? = null
|
||||
private var aesSecretKey: String? = null
|
||||
|
||||
// Desired states collected before build()
|
||||
private var appId: String? = null
|
||||
private var secretKey: String? = null
|
||||
private var primaryServiceHost: String? = null
|
||||
private var backupServiceHost: String? = null
|
||||
private var servicePort: Int? = null
|
||||
|
||||
private var desiredPersistentCacheEnabled: Boolean? = null
|
||||
private var desiredDiscardExpiredAfterSeconds: Int? = null
|
||||
private var desiredReuseExpiredIPEnabled: Boolean? = null
|
||||
@@ -35,10 +34,6 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
private var desiredPreResolveAfterNetworkChanged: Boolean? = null
|
||||
private var desiredIPRankingMap: Map<String, Int>? = null
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
appContext = flutterPluginBinding.applicationContext
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "aliyun_httpdns")
|
||||
@@ -46,14 +41,7 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
// Log every incoming call with method name and raw arguments
|
||||
try {
|
||||
Log.i("AliyunHttpDns", "invoke method=${call.method}, args=${call.arguments}")
|
||||
} catch (_: Throwable) {
|
||||
Log.i("AliyunHttpDns", "invoke method=${call.method}, args=<unprintable>")
|
||||
}
|
||||
when (call.method) {
|
||||
// Dart: init(accountId, secretKey?, aesSecretKey?) — only save states here
|
||||
"initialize" -> {
|
||||
val args = call.arguments as? Map<*, *> ?: emptyMap<String, Any>()
|
||||
val ctx = appContext
|
||||
@@ -61,261 +49,315 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
result.error("no_context", "Android context not attached", null)
|
||||
return
|
||||
}
|
||||
val accountAny = args["accountId"]
|
||||
val account = when (accountAny) {
|
||||
is Int -> accountAny.toString()
|
||||
is Long -> accountAny.toString()
|
||||
is String -> accountAny
|
||||
else -> null
|
||||
}
|
||||
val secret = (args["secretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
val aes = (args["aesSecretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (account.isNullOrBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing accountId")
|
||||
val appIdAny = args["appId"]
|
||||
val parsedAppId = when (appIdAny) {
|
||||
is Int -> appIdAny.toString()
|
||||
is Long -> appIdAny.toString()
|
||||
is String -> appIdAny.trim()
|
||||
else -> ""
|
||||
}
|
||||
if (parsedAppId.isBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing appId")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
// Save desired states only; actual build happens on 'build'
|
||||
accountId = account
|
||||
|
||||
val primaryHostArg = (args["primaryServiceHost"] as? String)?.trim()
|
||||
if (primaryHostArg.isNullOrBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing primaryServiceHost")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
|
||||
val secret = (args["secretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
val backup = (args["backupServiceHost"] as? String)?.trim()
|
||||
val port = when (val portAny = args["servicePort"]) {
|
||||
is Int -> portAny
|
||||
is Long -> portAny.toInt()
|
||||
is String -> portAny.toIntOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
appId = parsedAppId
|
||||
secretKey = secret
|
||||
aesSecretKey = aes
|
||||
Log.i("AliyunHttpDns", "initialize saved state, account=$account")
|
||||
primaryServiceHost = primaryHostArg
|
||||
backupServiceHost = backup?.trim()?.takeIf { it.isNotEmpty() }
|
||||
servicePort = if (port != null && port > 0) port else null
|
||||
|
||||
Log.i(
|
||||
"AliyunHttpDns",
|
||||
"initialize appId=$appId, primaryServiceHost=$primaryServiceHost, backupServiceHost=$backupServiceHost, servicePort=$servicePort"
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Dart: setLogEnabled(enabled) — save desired
|
||||
"setLogEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredLogEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "setLogEnabled desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setHttpsRequestEnabled(enabled)
|
||||
"setHttpsRequestEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredHttpsEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "https request desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPersistentCacheIPEnabled(enabled, discardExpiredAfterSeconds?) — save desired
|
||||
"setPersistentCacheIPEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
val discard = call.argument<Int>("discardExpiredAfterSeconds")
|
||||
desiredPersistentCacheEnabled = enabled
|
||||
desiredDiscardExpiredAfterSeconds = discard
|
||||
Log.i("AliyunHttpDns", "persistent cache desired=$enabled discard=$discard")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setReuseExpiredIPEnabled(enabled) — save desired
|
||||
"setReuseExpiredIPEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredReuseExpiredIPEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "reuse expired ip desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPreResolveAfterNetworkChanged(enabled) — save desired (applied at build via InitConfig)
|
||||
"setPreResolveAfterNetworkChanged" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredPreResolveAfterNetworkChanged = enabled
|
||||
Log.i("AliyunHttpDns", "preResolveAfterNetworkChanged desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setIPRankingList(hostPortMap) — save desired
|
||||
"setIPRankingList" -> {
|
||||
val hostPortMap = call.argument<Map<String, Int>>("hostPortMap")
|
||||
desiredIPRankingMap = hostPortMap
|
||||
Log.i("AliyunHttpDns", "IP ranking list desired, hosts=${hostPortMap?.keys?.joinToString()}")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPreResolveHosts(hosts, ipType)
|
||||
"setPreResolveHosts" -> {
|
||||
val hosts = call.argument<List<String>>("hosts") ?: emptyList()
|
||||
val ipTypeStr = call.argument<String>("ipType") ?: "auto"
|
||||
val type = when (ipTypeStr.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
val type = mapIpType(call.argument<String>("ipType") ?: "auto")
|
||||
try {
|
||||
service?.setPreResolveHosts(hosts, type)
|
||||
Log.i("AliyunHttpDns", "preResolve set for ${hosts.size} hosts, type=$type")
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "setPreResolveHosts failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: getSessionId
|
||||
"getSessionId" -> {
|
||||
val sid = try { service?.getSessionId() } catch (_: Throwable) { null }
|
||||
val sid = try {
|
||||
service?.getSessionId()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
result.success(sid)
|
||||
}
|
||||
|
||||
// Dart: cleanAllHostCache
|
||||
"cleanAllHostCache" -> {
|
||||
try {
|
||||
// Best-effort: empty list to clear all
|
||||
service?.cleanHostCache(ArrayList())
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "cleanAllHostCache failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: build() — construct InitConfig and acquire service using desired states
|
||||
"build" -> {
|
||||
val ctx = appContext
|
||||
val account = accountId
|
||||
if (ctx == null || account.isNullOrBlank()) {
|
||||
val currentAppId = appId
|
||||
if (ctx == null || currentAppId.isNullOrBlank()) {
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
desiredLogEnabled?.let { enabled ->
|
||||
try {
|
||||
HttpDnsLog.enable(enabled)
|
||||
Log.i("AliyunHttpDns", "HttpDnsLog.enable($enabled)")
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "HttpDnsLog.enable failed: ${t.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val builder = InitConfig.Builder()
|
||||
|
||||
// Optional builder params
|
||||
try { builder.javaClass.getMethod("setContext", Context::class.java).invoke(builder, ctx) } catch (_: Throwable) {}
|
||||
try {
|
||||
desiredLogEnabled?.let { HttpDnsLog.enable(it) }
|
||||
|
||||
val builder = InitConfig.Builder()
|
||||
var hostConfigApplied = false
|
||||
try {
|
||||
builder.javaClass.getMethod("setContext", Context::class.java)
|
||||
.invoke(builder, ctx)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
if (!secretKey.isNullOrBlank()) {
|
||||
builder.javaClass.getMethod("setSecretKey", String::class.java).invoke(builder, secretKey)
|
||||
builder.javaClass.getMethod("setSecretKey", String::class.java)
|
||||
.invoke(builder, secretKey)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
if (!aesSecretKey.isNullOrBlank()) {
|
||||
builder.javaClass.getMethod("setAesSecretKey", String::class.java).invoke(builder, aesSecretKey)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
// Prefer HTTPS if requested
|
||||
val enableHttps = desiredHttpsEnabled ?: true
|
||||
builder.javaClass.getMethod("setEnableHttps", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enableHttps)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredHttpsEnabled?.let { en ->
|
||||
builder.javaClass.getMethod("setEnableHttps", Boolean::class.javaPrimitiveType).invoke(builder, en)
|
||||
builder.javaClass.getMethod("setPrimaryServiceHost", String::class.java)
|
||||
.invoke(builder, primaryServiceHost)
|
||||
hostConfigApplied = true
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "setPrimaryServiceHost failed: ${t.message}")
|
||||
}
|
||||
try {
|
||||
backupServiceHost?.let {
|
||||
builder.javaClass.getMethod("setBackupServiceHost", String::class.java)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
servicePort?.let {
|
||||
builder.javaClass.getMethod("setServicePort", Int::class.javaPrimitiveType)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredPersistentCacheEnabled?.let { enabled ->
|
||||
val discardSeconds = desiredDiscardExpiredAfterSeconds
|
||||
if (discardSeconds != null && discardSeconds >= 0) {
|
||||
val expiredThresholdMillis = discardSeconds.toLong() * 1000L
|
||||
builder.javaClass.getMethod("setEnableCacheIp", Boolean::class.javaPrimitiveType, Long::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled, expiredThresholdMillis)
|
||||
val thresholdMillis = discardSeconds.toLong() * 1000L
|
||||
builder.javaClass.getMethod(
|
||||
"setEnableCacheIp",
|
||||
Boolean::class.javaPrimitiveType,
|
||||
Long::class.javaPrimitiveType
|
||||
).invoke(builder, enabled, thresholdMillis)
|
||||
} else {
|
||||
builder.javaClass.getMethod("setEnableCacheIp", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled)
|
||||
builder.javaClass.getMethod(
|
||||
"setEnableCacheIp",
|
||||
Boolean::class.javaPrimitiveType
|
||||
).invoke(builder, enabled)
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) { }
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredReuseExpiredIPEnabled?.let { enabled ->
|
||||
desiredReuseExpiredIPEnabled?.let {
|
||||
builder.javaClass.getMethod("setEnableExpiredIp", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) { }
|
||||
// Apply preResolve-after-network-changed
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredPreResolveAfterNetworkChanged?.let { en ->
|
||||
builder.javaClass.getMethod("setPreResolveAfterNetworkChanged", Boolean::class.javaPrimitiveType).invoke(builder, en)
|
||||
desiredPreResolveAfterNetworkChanged?.let {
|
||||
builder.javaClass.getMethod("setPreResolveAfterNetworkChanged", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
|
||||
// Apply IP ranking list
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredIPRankingMap?.let { map ->
|
||||
if (map.isNotEmpty()) {
|
||||
// Create List<IPRankingBean>
|
||||
val ipRankingBeanClass = Class.forName("com.alibaba.sdk.android.httpdns.ranking.IPRankingBean")
|
||||
val constructor = ipRankingBeanClass.getConstructor(String::class.java, Int::class.javaPrimitiveType)
|
||||
val beanClass = Class.forName("com.alibaba.sdk.android.httpdns.ranking.IPRankingBean")
|
||||
val ctor = beanClass.getConstructor(String::class.java, Int::class.javaPrimitiveType)
|
||||
val list = ArrayList<Any>()
|
||||
map.forEach { (host, port) ->
|
||||
val bean = constructor.newInstance(host, port)
|
||||
list.add(bean)
|
||||
list.add(ctor.newInstance(host, port))
|
||||
}
|
||||
val m = builder.javaClass.getMethod("setIPRankingList", List::class.java)
|
||||
m.invoke(builder, list)
|
||||
Log.i("AliyunHttpDns", "setIPRankingList applied with ${list.size} hosts")
|
||||
builder.javaClass.getMethod("setIPRankingList", List::class.java)
|
||||
.invoke(builder, list)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "setIPRankingList failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
builder.buildFor(account)
|
||||
if (!hostConfigApplied) {
|
||||
Log.w("AliyunHttpDns", "build failed: sdk core does not support primaryServiceHost")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
|
||||
builder.buildFor(currentAppId)
|
||||
|
||||
service = if (!secretKey.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, account, secretKey)
|
||||
HttpDns.getService(ctx, currentAppId, secretKey)
|
||||
} else {
|
||||
HttpDns.getService(ctx, account)
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
}
|
||||
|
||||
Log.i("AliyunHttpDns", "build completed for account=$account")
|
||||
|
||||
result.success(true)
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "build failed: ${t.message}")
|
||||
Log.w("AliyunHttpDns", "build failed: ${t.message}")
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Dart: resolveHostSyncNonBlocking(hostname, ipType, sdnsParams?, cacheKey?)
|
||||
"resolveHostSyncNonBlocking" -> {
|
||||
val hostname = call.argument<String>("hostname")
|
||||
if (hostname.isNullOrBlank()) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>()))
|
||||
return
|
||||
}
|
||||
val ipTypeStr = call.argument<String>("ipType") ?: "auto"
|
||||
val type = when (ipTypeStr.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
|
||||
val type = mapIpType(call.argument<String>("ipType") ?: "auto")
|
||||
try {
|
||||
val svc = service ?: run {
|
||||
val ctx = appContext
|
||||
val acc = accountId
|
||||
if (ctx != null && !acc.isNullOrBlank()) HttpDns.getService(ctx, acc) else null
|
||||
val currentAppId = appId
|
||||
if (ctx != null && !currentAppId.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val r = svc?.getHttpDnsResultForHostSyncNonBlocking(hostname, type)
|
||||
val v4 = r?.ips?.toList() ?: emptyList()
|
||||
val v6 = r?.ipv6s?.toList() ?: emptyList()
|
||||
// 记录解析结果,便于排查:包含 host、请求类型以及返回的 IPv4/IPv6 列表
|
||||
Log.d(
|
||||
"HttpdnsPlugin",
|
||||
"resolve result host=" + hostname + ", type=" + type +
|
||||
", ipv4=" + v4.joinToString(prefix = "[", postfix = "]") +
|
||||
", ipv6=" + v6.joinToString(prefix = "[", postfix = "]")
|
||||
)
|
||||
result.success(mapOf("ipv4" to v4, "ipv6" to v6))
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "resolveHostSyncNonBlocking failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>()))
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy methods removed: preResolve / clearCache handled at app layer if needed
|
||||
"resolveHostV1" -> {
|
||||
val hostname = call.argument<String>("hostname")
|
||||
if (hostname.isNullOrBlank()) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>(), "ttl" to 0))
|
||||
return
|
||||
}
|
||||
val qtype = (call.argument<String>("qtype") ?: "A").uppercase()
|
||||
val cip = call.argument<String>("cip")?.trim()
|
||||
val requestType = if (qtype == "AAAA") RequestIpType.v6 else RequestIpType.v4
|
||||
try {
|
||||
val svc = service ?: run {
|
||||
val ctx = appContext
|
||||
val currentAppId = appId
|
||||
if (ctx != null && !currentAppId.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val params = if (!cip.isNullOrEmpty()) mapOf("cip" to cip) else null
|
||||
val r = svc?.getHttpDnsResultForHostSyncNonBlocking(hostname, requestType, params, hostname)
|
||||
val v4 = r?.ips?.toList() ?: emptyList()
|
||||
val v6 = r?.ipv6s?.toList() ?: emptyList()
|
||||
result.success(
|
||||
mapOf(
|
||||
"ipv4" to v4,
|
||||
"ipv6" to v6,
|
||||
"ttl" to (r?.ttl ?: 0),
|
||||
)
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>(), "ttl" to 0))
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapIpType(ipType: String): RequestIpType {
|
||||
return when (ipType.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
service = null
|
||||
|
||||
@@ -16,11 +16,14 @@ This example demonstrates:
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Replace the `accountId` and `secretKey` in `lib/main.dart` with your own credentials:
|
||||
1. Replace the SDK init parameters in `lib/main.dart` with your own values:
|
||||
```dart
|
||||
await AliyunHttpdns.init(
|
||||
accountId: YOUR_ACCOUNT_ID, // Replace with your account ID
|
||||
secretKey: 'YOUR_SECRET_KEY', // Replace with your secret key
|
||||
appId: 'YOUR_APP_ID',
|
||||
primaryServiceHost: 'httpdns-a.example.com',
|
||||
backupServiceHost: 'httpdns-b.example.com',
|
||||
servicePort: 443,
|
||||
secretKey: 'YOUR_SIGN_SECRET', // optional if sign is enabled
|
||||
);
|
||||
```
|
||||
|
||||
@@ -48,4 +51,4 @@ The example uses a modern approach with `HttpClient.connectionFactory` to integr
|
||||
|
||||
## Note
|
||||
|
||||
The credentials in this example are placeholders. Please obtain your own credentials from the [Aliyun HTTPDNS Console](https://help.aliyun.com/document_detail/2867674.html).
|
||||
The values in this example are placeholders. Use your own platform app configuration.
|
||||
|
||||
@@ -64,8 +64,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
_httpdnsIniting = true;
|
||||
try {
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000, // 请替换为您的 Account ID
|
||||
secretKey: 'your_secret_key_here', // 请替换为您的 Secret Key
|
||||
appId: 'app1f1ndpo9', // 请替换为您的应用 AppId
|
||||
primaryServiceHost: 'httpdns-a.example.com', // 请替换为主服务域名
|
||||
backupServiceHost: 'httpdns-b.example.com', // 可选:备服务域名
|
||||
servicePort: 443,
|
||||
secretKey: 'your_sign_secret_here', // 可选:仅验签开启时需要
|
||||
);
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
|
||||
@@ -1,203 +1,324 @@
|
||||
import Flutter
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AlicloudHTTPDNS
|
||||
import CommonCrypto
|
||||
|
||||
public class AliyunHttpDnsPlugin: NSObject, FlutterPlugin {
|
||||
private var channel: FlutterMethodChannel!
|
||||
private var appId: String?
|
||||
private var secretKey: String?
|
||||
private var primaryServiceHost: String?
|
||||
private var backupServiceHost: String?
|
||||
private var servicePort: Int = 443
|
||||
|
||||
// Desired states saved until build()
|
||||
private var desiredAccountId: Int?
|
||||
private var desiredSecretKey: String?
|
||||
private var desiredAesSecretKey: String?
|
||||
|
||||
private var desiredPersistentCacheEnabled: Bool?
|
||||
private var desiredDiscardExpiredAfterSeconds: Int?
|
||||
private var desiredReuseExpiredIPEnabled: Bool?
|
||||
private var desiredLogEnabled: Bool?
|
||||
private var desiredHttpsEnabled: Bool?
|
||||
private var desiredPreResolveAfterNetworkChanged: Bool?
|
||||
private var desiredIPRankingMap: [String: NSNumber]?
|
||||
|
||||
private var desiredConnectTimeoutSeconds: TimeInterval = 3
|
||||
private var desiredReadTimeoutSeconds: TimeInterval = 5
|
||||
|
||||
private lazy var sessionId: String = {
|
||||
UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
||||
}()
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "aliyun_httpdns", binaryMessenger: registrar.messenger())
|
||||
let instance = AliyunHttpDnsPlugin()
|
||||
instance.channel = channel
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
// Dart: init(accountId, secretKey?, aesSecretKey?) — only save desired state
|
||||
case "initialize":
|
||||
let options = call.arguments as? [String: Any] ?? [:]
|
||||
let accountIdAny = options["accountId"]
|
||||
let secretKey = options["secretKey"] as? String
|
||||
let aesSecretKey = options["aesSecretKey"] as? String
|
||||
|
||||
guard let accountId = (accountIdAny as? Int) ?? Int((accountIdAny as? String) ?? "") else {
|
||||
NSLog("AliyunHttpDns: initialize missing accountId")
|
||||
guard let rawAppId = options["appId"] as? String,
|
||||
let rawPrimaryHost = options["primaryServiceHost"] as? String else {
|
||||
result(false)
|
||||
return
|
||||
}
|
||||
desiredAccountId = accountId
|
||||
desiredSecretKey = secretKey
|
||||
desiredAesSecretKey = aesSecretKey
|
||||
NSLog("AliyunHttpDns: initialize saved accountId=\(accountId)")
|
||||
|
||||
let normalizedAppId = rawAppId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedPrimaryHost = rawPrimaryHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if normalizedAppId.isEmpty || normalizedPrimaryHost.isEmpty {
|
||||
result(false)
|
||||
return
|
||||
}
|
||||
|
||||
appId = normalizedAppId
|
||||
primaryServiceHost = normalizedPrimaryHost
|
||||
backupServiceHost = (options["backupServiceHost"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
secretKey = (options["secretKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let p = options["servicePort"] as? Int, p > 0 {
|
||||
servicePort = p
|
||||
}
|
||||
result(true)
|
||||
|
||||
|
||||
|
||||
// Dart: setLogEnabled(enabled) — save desired
|
||||
case "setLogEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredLogEnabled = enabled
|
||||
NSLog("AliyunHttpDns: log desired=\(enabled)")
|
||||
desiredLogEnabled = (args?["enabled"] as? Bool) ?? false
|
||||
result(nil)
|
||||
|
||||
case "setHttpsRequestEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredHttpsEnabled = enabled
|
||||
NSLog("AliyunHttpDns: https request desired=\(enabled)")
|
||||
desiredHttpsEnabled = (args?["enabled"] as? Bool) ?? false
|
||||
result(nil)
|
||||
|
||||
// Dart: setPersistentCacheIPEnabled(enabled, discardExpiredAfterSeconds?) — save desired
|
||||
case "setPersistentCacheIPEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
let discard = args?["discardExpiredAfterSeconds"] as? Int
|
||||
desiredPersistentCacheEnabled = enabled
|
||||
desiredDiscardExpiredAfterSeconds = discard
|
||||
NSLog("AliyunHttpDns: persistent cache desired=\(enabled) discard=\(discard ?? -1)")
|
||||
result(nil)
|
||||
|
||||
// Dart: setReuseExpiredIPEnabled(enabled) — save desired
|
||||
case "setReuseExpiredIPEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredReuseExpiredIPEnabled = enabled
|
||||
NSLog("AliyunHttpDns: reuse expired ip desired=\(enabled)")
|
||||
result(nil)
|
||||
|
||||
case "setPreResolveAfterNetworkChanged":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredPreResolveAfterNetworkChanged = enabled
|
||||
NSLog("AliyunHttpDns: preResolveAfterNetworkChanged desired=\(enabled)")
|
||||
result(nil)
|
||||
|
||||
case "setIPRankingList":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let hostPortMap = args?["hostPortMap"] as? [String: NSNumber]
|
||||
desiredIPRankingMap = hostPortMap
|
||||
NSLog("AliyunHttpDns: IP ranking list desired, hosts=\(hostPortMap?.keys.joined(separator: ", ") ?? "")")
|
||||
result(nil)
|
||||
|
||||
case "setPreResolveHosts":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let hosts = (args?["hosts"] as? [String]) ?? []
|
||||
let ipTypeStr = (args?["ipType"] as? String) ?? "auto"
|
||||
switch ipTypeStr.lowercased() {
|
||||
case "ipv4", "v4":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(0))
|
||||
case "ipv6", "v6":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(1))
|
||||
case "both", "64":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(2))
|
||||
default:
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts)
|
||||
}
|
||||
result(nil)
|
||||
|
||||
case "getSessionId":
|
||||
let sid = HttpDnsService.sharedInstance().getSessionId()
|
||||
result(sid)
|
||||
result(sessionId)
|
||||
|
||||
case "cleanAllHostCache":
|
||||
HttpDnsService.sharedInstance().cleanAllHostCache()
|
||||
result(nil)
|
||||
|
||||
// Dart: build() — construct service and apply desired states
|
||||
case "build":
|
||||
guard let accountId = desiredAccountId else {
|
||||
result(false)
|
||||
return
|
||||
if desiredHttpsEnabled == false {
|
||||
NSLog("AliyunHttpDns(iOS): HTTPS is required by current protocol, force enabled")
|
||||
}
|
||||
// Initialize singleton
|
||||
if let secret = desiredSecretKey, !secret.isEmpty {
|
||||
if let aes = desiredAesSecretKey, !aes.isEmpty {
|
||||
_ = HttpDnsService(accountID: accountId, secretKey: secret, aesSecretKey: aes)
|
||||
} else {
|
||||
_ = HttpDnsService(accountID: accountId, secretKey: secret)
|
||||
}
|
||||
} else {
|
||||
_ = HttpDnsService(accountID: accountId) // deprecated but acceptable fallback
|
||||
}
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
// Apply desired runtime flags
|
||||
if let enable = desiredPersistentCacheEnabled {
|
||||
if let discard = desiredDiscardExpiredAfterSeconds, discard >= 0 {
|
||||
svc.setPersistentCacheIPEnabled(enable, discardRecordsHasExpiredFor: TimeInterval(discard))
|
||||
} else {
|
||||
svc.setPersistentCacheIPEnabled(enable)
|
||||
}
|
||||
}
|
||||
if let enable = desiredReuseExpiredIPEnabled {
|
||||
svc.setReuseExpiredIPEnabled(enable)
|
||||
}
|
||||
if let enable = desiredLogEnabled {
|
||||
svc.setLogEnabled(enable)
|
||||
}
|
||||
if let enable = desiredHttpsEnabled {
|
||||
svc.setHTTPSRequestEnabled(enable)
|
||||
if desiredLogEnabled == true {
|
||||
NSLog("AliyunHttpDns(iOS): build success, appId=\(appId ?? "")")
|
||||
}
|
||||
result(appId != nil && primaryServiceHost != nil)
|
||||
|
||||
if let en = desiredPreResolveAfterNetworkChanged {
|
||||
svc.setPreResolveAfterNetworkChanged(en)
|
||||
}
|
||||
if let ipRankingMap = desiredIPRankingMap, !ipRankingMap.isEmpty {
|
||||
svc.setIPRankingDatasource(ipRankingMap)
|
||||
}
|
||||
NSLog("AliyunHttpDns: build completed accountId=\(accountId)")
|
||||
result(true)
|
||||
|
||||
// Dart: resolveHostSyncNonBlocking(hostname, ipType, sdnsParams?, cacheKey?)
|
||||
case "resolveHostSyncNonBlocking":
|
||||
guard let args = call.arguments as? [String: Any], let host = args["hostname"] as? String else {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let hostname = args["hostname"] as? String else {
|
||||
result(["ipv4": [], "ipv6": []])
|
||||
return
|
||||
}
|
||||
let ipTypeStr = (args["ipType"] as? String) ?? "auto"
|
||||
let sdnsParams = args["sdnsParams"] as? [String: String]
|
||||
let cacheKey = args["cacheKey"] as? String
|
||||
let type: HttpdnsQueryIPType
|
||||
switch ipTypeStr.lowercased() {
|
||||
case "ipv4", "v4": type = .ipv4
|
||||
case "ipv6", "v6": type = .ipv6
|
||||
case "both", "64": type = .both
|
||||
default: type = .auto
|
||||
let ipType = ((args["ipType"] as? String) ?? "auto").lowercased()
|
||||
resolveHost(hostname: hostname, ipType: ipType) { payload in
|
||||
result(payload)
|
||||
}
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
var v4: [String] = []
|
||||
var v6: [String] = []
|
||||
if let params = sdnsParams, let key = cacheKey, let r = svc.resolveHostSyncNonBlocking(host, by: type, withSdnsParams: params, sdnsCacheKey: key) {
|
||||
if r.hasIpv4Address() { v4 = r.ips }
|
||||
if r.hasIpv6Address() { v6 = r.ipv6s }
|
||||
} else if let r = svc.resolveHostSyncNonBlocking(host, by: type) {
|
||||
if r.hasIpv4Address() { v4 = r.ips }
|
||||
if r.hasIpv6Address() { v6 = r.ipv6s }
|
||||
case "resolveHostV1":
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let hostname = args["hostname"] as? String else {
|
||||
result(["ipv4": [], "ipv6": [], "ttl": 0])
|
||||
return
|
||||
}
|
||||
let qtype = ((args["qtype"] as? String) ?? "A").uppercased()
|
||||
let cip = (args["cip"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
resolveSingle(hostname: hostname, qtype: qtype, cip: cip) { records, ttl in
|
||||
if qtype == "AAAA" {
|
||||
result(["ipv4": [], "ipv6": records, "ttl": ttl])
|
||||
} else {
|
||||
result(["ipv4": records, "ipv6": [], "ttl": ttl])
|
||||
}
|
||||
}
|
||||
result(["ipv4": v4, "ipv6": v6])
|
||||
|
||||
// Legacy methods removed: preResolve / clearCache
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveHost(hostname: String, ipType: String, completion: @escaping ([String: [String]]) -> Void) {
|
||||
let qtypes: [String]
|
||||
switch ipType {
|
||||
case "ipv6", "v6":
|
||||
qtypes = ["AAAA"]
|
||||
case "both", "64":
|
||||
qtypes = ["A", "AAAA"]
|
||||
default:
|
||||
qtypes = ["A"]
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
let lock = NSLock()
|
||||
var ipv4: [String] = []
|
||||
var ipv6: [String] = []
|
||||
|
||||
for qtype in qtypes {
|
||||
group.enter()
|
||||
resolveSingle(hostname: hostname, qtype: qtype, cip: nil) { records, _ in
|
||||
lock.lock()
|
||||
if qtype == "AAAA" {
|
||||
ipv6.append(contentsOf: records)
|
||||
} else {
|
||||
ipv4.append(contentsOf: records)
|
||||
}
|
||||
lock.unlock()
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
completion([
|
||||
"ipv4": Array(Set(ipv4)),
|
||||
"ipv6": Array(Set(ipv6))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveSingle(hostname: String, qtype: String, cip: String?, completion: @escaping ([String], Int) -> Void) {
|
||||
guard let appId = appId,
|
||||
let primary = primaryServiceHost else {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
var hosts: [String] = [primary]
|
||||
if let backup = backupServiceHost, !backup.isEmpty, backup != primary {
|
||||
hosts.append(backup)
|
||||
}
|
||||
|
||||
func attempt(_ index: Int) {
|
||||
if index >= hosts.count {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
let serviceHost = hosts[index]
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
components.host = serviceHost
|
||||
components.port = servicePort
|
||||
components.path = "/resolve"
|
||||
|
||||
var queryItems: [URLQueryItem] = [
|
||||
URLQueryItem(name: "appId", value: appId),
|
||||
URLQueryItem(name: "dn", value: hostname),
|
||||
URLQueryItem(name: "qtype", value: qtype),
|
||||
URLQueryItem(name: "sdk_version", value: "flutter-ios-1.0.0"),
|
||||
URLQueryItem(name: "os", value: "ios"),
|
||||
URLQueryItem(name: "sid", value: sessionId)
|
||||
]
|
||||
|
||||
if let secret = secretKey, !secret.isEmpty {
|
||||
let exp = String(Int(Date().timeIntervalSince1970) + 600)
|
||||
let nonce = UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
||||
let signRaw = "\(appId)|\(hostname.lowercased())|\(qtype.uppercased())|\(exp)|\(nonce)"
|
||||
let sign = hmacSHA256Hex(message: signRaw, secret: secret)
|
||||
queryItems.append(URLQueryItem(name: "exp", value: exp))
|
||||
queryItems.append(URLQueryItem(name: "nonce", value: nonce))
|
||||
queryItems.append(URLQueryItem(name: "sign", value: sign))
|
||||
}
|
||||
if let cip = cip, !cip.isEmpty {
|
||||
queryItems.append(URLQueryItem(name: "cip", value: cip))
|
||||
}
|
||||
|
||||
components.queryItems = queryItems
|
||||
guard let url = components.url else {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
var req = URLRequest(url: url)
|
||||
req.httpMethod = "GET"
|
||||
req.timeoutInterval = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
req.setValue(serviceHost, forHTTPHeaderField: "Host")
|
||||
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.timeoutIntervalForRequest = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
config.timeoutIntervalForResource = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
let session = URLSession(configuration: config)
|
||||
session.dataTask(with: req) { [weak self] data, _, error in
|
||||
defer { session.finishTasksAndInvalidate() }
|
||||
|
||||
if let error = error {
|
||||
if self?.desiredLogEnabled == true {
|
||||
NSLog("AliyunHttpDns(iOS): resolve request failed host=\(serviceHost), err=\(error.localizedDescription)")
|
||||
}
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
|
||||
let parsedIPs = Self.extractIPsFromResolveResponse(data: data, qtype: qtype)
|
||||
if parsedIPs.isEmpty {
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
let ttl = Self.extractTTLFromResolveResponse(data: data)
|
||||
completion(parsedIPs, ttl)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
attempt(0)
|
||||
}
|
||||
|
||||
private static func extractIPsFromResolveResponse(data: Data, qtype: String) -> [String] {
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
isResolveSuccessCode(obj["code"]),
|
||||
let dataObj = obj["data"] as? [String: Any],
|
||||
let records = dataObj["records"] as? [[String: Any]] else {
|
||||
return []
|
||||
}
|
||||
|
||||
var ips: [String] = []
|
||||
for row in records {
|
||||
let type = ((row["type"] as? String) ?? "").uppercased()
|
||||
if type != qtype.uppercased() {
|
||||
continue
|
||||
}
|
||||
if let ip = row["ip"] as? String, !ip.isEmpty {
|
||||
ips.append(ip)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
private static func extractTTLFromResolveResponse(data: Data) -> Int {
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataObj = obj["data"] as? [String: Any],
|
||||
let ttlValue = dataObj["ttl"] else {
|
||||
return 0
|
||||
}
|
||||
if let ttl = ttlValue as? Int {
|
||||
return ttl
|
||||
}
|
||||
if let ttlString = ttlValue as? String, let ttl = Int(ttlString) {
|
||||
return ttl
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private static func isResolveSuccessCode(_ codeValue: Any?) -> Bool {
|
||||
if let code = codeValue as? String {
|
||||
let normalized = code.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
return normalized == "SUCCESS" || normalized == "0"
|
||||
}
|
||||
if let code = codeValue as? Int {
|
||||
return code == 0
|
||||
}
|
||||
if let code = codeValue as? NSNumber {
|
||||
return code.intValue == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func hmacSHA256Hex(message: String, secret: String) -> String {
|
||||
guard let keyData = secret.data(using: .utf8),
|
||||
let messageData = message.data(using: .utf8) else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
keyData.withUnsafeBytes { keyBytes in
|
||||
messageData.withUnsafeBytes { msgBytes in
|
||||
CCHmac(
|
||||
CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
keyBytes.baseAddress,
|
||||
keyData.count,
|
||||
msgBytes.baseAddress,
|
||||
messageData.count,
|
||||
&digest
|
||||
)
|
||||
}
|
||||
}
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'aliyun_httpdns'
|
||||
s.version = '1.0.2'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'aliyun httpdns flutter plugin'
|
||||
s.description = <<-DESC
|
||||
aliyun httpdns flutter plugin.
|
||||
@@ -16,7 +16,6 @@ DESC
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.static_framework = true
|
||||
s.dependency 'Flutter'
|
||||
s.dependency 'AlicloudHTTPDNS', '3.4.0'
|
||||
s.platform = :ios, '10.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
|
||||
@@ -1,87 +1,89 @@
|
||||
import 'dart:async';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AliyunHttpdns {
|
||||
static const MethodChannel _channel = MethodChannel('aliyun_httpdns');
|
||||
|
||||
/// 1) 初始化:使用 accountId/secretKey/aesSecretKey
|
||||
/// New API only:
|
||||
/// appId + primary/backup service host + optional sign secret.
|
||||
static Future<bool> init({
|
||||
required int accountId,
|
||||
required String appId,
|
||||
required String primaryServiceHost,
|
||||
String? backupServiceHost,
|
||||
int servicePort = 443,
|
||||
String? secretKey,
|
||||
String? aesSecretKey,
|
||||
}) async {
|
||||
final ok =
|
||||
await _channel.invokeMethod<bool>('initialize', <String, dynamic>{
|
||||
'accountId': accountId,
|
||||
if (secretKey != null) 'secretKey': secretKey,
|
||||
if (aesSecretKey != null) 'aesSecretKey': aesSecretKey,
|
||||
});
|
||||
final String normalizedAppId = appId.trim();
|
||||
final String normalizedPrimary = primaryServiceHost.trim();
|
||||
if (normalizedAppId.isEmpty || normalizedPrimary.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> args = <String, dynamic>{
|
||||
'appId': normalizedAppId,
|
||||
'primaryServiceHost': normalizedPrimary,
|
||||
if (backupServiceHost != null && backupServiceHost.trim().isNotEmpty)
|
||||
'backupServiceHost': backupServiceHost.trim(),
|
||||
if (servicePort > 0) 'servicePort': servicePort,
|
||||
if (secretKey != null && secretKey.isNotEmpty) 'secretKey': secretKey,
|
||||
};
|
||||
|
||||
final bool? ok = await _channel.invokeMethod<bool>('initialize', args);
|
||||
return ok ?? false;
|
||||
}
|
||||
|
||||
/// 构建底层 service,只有在调用了 initialize / 一系列 setXxx 后,
|
||||
/// 调用本方法才会真正创建底层实例并应用配置
|
||||
static Future<bool> build() async {
|
||||
final ok = await _channel.invokeMethod<bool>('build');
|
||||
final bool? ok = await _channel.invokeMethod<bool>('build');
|
||||
return ok ?? false;
|
||||
}
|
||||
|
||||
/// 2) 设置日志开关
|
||||
static Future<void> setLogEnabled(bool enabled) async {
|
||||
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 3) 设置持久化缓存
|
||||
static Future<void> setPersistentCacheIPEnabled(bool enabled,
|
||||
{int? discardExpiredAfterSeconds}) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
|
||||
await _channel.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
if (discardExpiredAfterSeconds != null)
|
||||
'discardExpiredAfterSeconds': discardExpiredAfterSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
/// 4) 是否允许复用过期 IP
|
||||
static Future<void> setReuseExpiredIPEnabled(bool enabled) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 设置是否使用 HTTPS 解析链路,避免明文流量被系统拦截
|
||||
static Future<void> setHttpsRequestEnabled(bool enabled) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 5) 伪异步解析:返回 IPv4/IPv6 数组
|
||||
/// 返回格式:{"ipv4": `List<String>`, "ipv6": `List<String>`}
|
||||
static Future<Map<String, List<String>>> resolveHostSyncNonBlocking(
|
||||
String hostname, {
|
||||
String ipType = 'auto', // auto/ipv4/ipv6/both
|
||||
Map<String, String>? sdnsParams,
|
||||
String? cacheKey,
|
||||
}) async {
|
||||
final Map<dynamic, dynamic>? res = await _channel
|
||||
.invokeMethod('resolveHostSyncNonBlocking', <String, dynamic>{
|
||||
final Map<dynamic, dynamic>? res =
|
||||
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostSyncNonBlocking',
|
||||
<String, dynamic>{
|
||||
'hostname': hostname,
|
||||
'ipType': ipType,
|
||||
if (sdnsParams != null) 'sdnsParams': sdnsParams,
|
||||
if (cacheKey != null) 'cacheKey': cacheKey,
|
||||
});
|
||||
final Map<String, List<String>> out = {
|
||||
|
||||
final Map<String, List<String>> out = <String, List<String>>{
|
||||
'ipv4': <String>[],
|
||||
'ipv6': <String>[],
|
||||
};
|
||||
if (res == null) return out;
|
||||
final v4 = res['ipv4'];
|
||||
final v6 = res['ipv6'];
|
||||
if (res == null) {
|
||||
return out;
|
||||
}
|
||||
|
||||
final dynamic v4 = res['ipv4'];
|
||||
final dynamic v6 = res['ipv6'];
|
||||
if (v4 is List) {
|
||||
out['ipv4'] = v4.map((e) => e.toString()).toList();
|
||||
}
|
||||
@@ -91,51 +93,190 @@ class AliyunHttpdns {
|
||||
return out;
|
||||
}
|
||||
|
||||
// 解析域名,返回 A/AAAA 记录等(保留旧接口以兼容,未在本任务使用)
|
||||
static Future<Map<String, dynamic>?> resolve(String hostname,
|
||||
{Map<String, dynamic>? options}) async {
|
||||
final res = await _channel.invokeMethod<Map<dynamic, dynamic>>('resolve', {
|
||||
/// V1 resolve API:
|
||||
/// qtype supports A / AAAA, optional cip for route simulation.
|
||||
static Future<Map<String, dynamic>> resolveHost(
|
||||
String hostname, {
|
||||
String qtype = 'A',
|
||||
String? cip,
|
||||
}) async {
|
||||
final Map<dynamic, dynamic>? res =
|
||||
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostV1', <String, dynamic>{
|
||||
'hostname': hostname,
|
||||
if (options != null) 'options': options,
|
||||
'qtype': qtype,
|
||||
if (cip != null && cip.trim().isNotEmpty) 'cip': cip.trim(),
|
||||
});
|
||||
return res?.map((key, value) => MapEntry(key.toString(), value));
|
||||
|
||||
final Map<String, dynamic> out = <String, dynamic>{
|
||||
'ipv4': <String>[],
|
||||
'ipv6': <String>[],
|
||||
'ttl': 0,
|
||||
};
|
||||
if (res == null) {
|
||||
return out;
|
||||
}
|
||||
final dynamic v4 = res['ipv4'];
|
||||
final dynamic v6 = res['ipv6'];
|
||||
if (v4 is List) {
|
||||
out['ipv4'] = v4.map((e) => e.toString()).toList();
|
||||
}
|
||||
if (v6 is List) {
|
||||
out['ipv6'] = v6.map((e) => e.toString()).toList();
|
||||
}
|
||||
final dynamic ttl = res['ttl'];
|
||||
if (ttl is int) {
|
||||
out['ttl'] = ttl;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 1) setPreResolveHosts: 传入 host 列表,native 侧调用 SDK 预解析
|
||||
static Future<void> setPreResolveHosts(List<String> hosts,
|
||||
{String ipType = 'auto'}) async {
|
||||
static Future<void> setPreResolveHosts(List<String> hosts, {String ipType = 'auto'}) async {
|
||||
await _channel.invokeMethod<void>('setPreResolveHosts', <String, dynamic>{
|
||||
'hosts': hosts,
|
||||
'ipType': ipType,
|
||||
});
|
||||
}
|
||||
|
||||
// 2) setLogEnabled: 已有,同步保留(在此文件顶部已有 setLogEnabled 实现)
|
||||
|
||||
// 3) setPreResolveAfterNetworkChanged: 是否在网络切换时自动刷新解析
|
||||
static Future<void> setPreResolveAfterNetworkChanged(bool enabled) async {
|
||||
await _channel.invokeMethod<void>(
|
||||
'setPreResolveAfterNetworkChanged', <String, dynamic>{
|
||||
await _channel.invokeMethod<void>('setPreResolveAfterNetworkChanged', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// 4) getSessionId: 获取会话 id
|
||||
static Future<String?> getSessionId() async {
|
||||
final sid = await _channel.invokeMethod<String>('getSessionId');
|
||||
return sid;
|
||||
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setIPRankingList', <String, dynamic>{'hostPortMap': hostPortMap});
|
||||
}
|
||||
|
||||
static Future<String?> getSessionId() async {
|
||||
return _channel.invokeMethod<String>('getSessionId');
|
||||
}
|
||||
|
||||
// 5) cleanAllHostCache: 清除所有缓存
|
||||
static Future<void> cleanAllHostCache() async {
|
||||
await _channel.invokeMethod<void>('cleanAllHostCache');
|
||||
}
|
||||
|
||||
/// 设置 IP 优选列表
|
||||
/// [hostPortMap] 域名和端口的映射,例如:{'www.aliyun.com': 443}
|
||||
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
|
||||
await _channel.invokeMethod<void>('setIPRankingList', <String, dynamic>{
|
||||
'hostPortMap': hostPortMap,
|
||||
});
|
||||
|
||||
static AliyunHttpdnsHttpAdapter createHttpAdapter({
|
||||
AliyunHttpdnsAdapterOptions options = const AliyunHttpdnsAdapterOptions(),
|
||||
}) {
|
||||
return AliyunHttpdnsHttpAdapter._(options);
|
||||
}
|
||||
}
|
||||
|
||||
class AliyunHttpdnsAdapterOptions {
|
||||
final String ipType;
|
||||
final int connectTimeoutMs;
|
||||
final int readTimeoutMs;
|
||||
final bool allowInsecureCertificatesForDebugOnly;
|
||||
|
||||
const AliyunHttpdnsAdapterOptions({
|
||||
this.ipType = 'auto',
|
||||
this.connectTimeoutMs = 3000,
|
||||
this.readTimeoutMs = 5000,
|
||||
this.allowInsecureCertificatesForDebugOnly = false,
|
||||
});
|
||||
}
|
||||
|
||||
class AliyunHttpdnsRequestResult {
|
||||
final int statusCode;
|
||||
final Map<String, List<String>> headers;
|
||||
final Uint8List body;
|
||||
final String usedIp;
|
||||
|
||||
const AliyunHttpdnsRequestResult({
|
||||
required this.statusCode,
|
||||
required this.headers,
|
||||
required this.body,
|
||||
required this.usedIp,
|
||||
});
|
||||
}
|
||||
|
||||
class AliyunHttpdnsHttpAdapter {
|
||||
final AliyunHttpdnsAdapterOptions _options;
|
||||
|
||||
AliyunHttpdnsHttpAdapter._(this._options);
|
||||
|
||||
/// Fixed behavior:
|
||||
/// 1) resolve host by HTTPDNS
|
||||
/// 2) connect to IP:443
|
||||
/// 3) keep HTTP Host as original domain
|
||||
/// 4) no fallback to domain connect
|
||||
Future<AliyunHttpdnsRequestResult> request(
|
||||
Uri uri, {
|
||||
String method = 'GET',
|
||||
Map<String, String>? headers,
|
||||
List<int>? body,
|
||||
}) async {
|
||||
if (uri.host.isEmpty) {
|
||||
throw const HttpException('HOST_ROUTE_REJECTED: host is empty');
|
||||
}
|
||||
if (uri.scheme.toLowerCase() != 'https') {
|
||||
throw const HttpException('TLS_EMPTY_SNI_FAILED: only https is supported');
|
||||
}
|
||||
|
||||
final Map<String, List<String>> resolved = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
uri.host,
|
||||
ipType: _options.ipType,
|
||||
);
|
||||
final List<String> ips = <String>[
|
||||
...?resolved['ipv4'],
|
||||
...?resolved['ipv6'],
|
||||
];
|
||||
if (ips.isEmpty) {
|
||||
throw const HttpException('NO_IP_AVAILABLE: HTTPDNS returned empty ip list');
|
||||
}
|
||||
|
||||
Object? lastError;
|
||||
for (final String ip in ips) {
|
||||
final HttpClient client = HttpClient();
|
||||
client.connectionTimeout = Duration(milliseconds: _options.connectTimeoutMs);
|
||||
if (_options.allowInsecureCertificatesForDebugOnly) {
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
}
|
||||
|
||||
try {
|
||||
final Uri target = uri.replace(
|
||||
host: ip,
|
||||
port: uri.hasPort ? uri.port : 443,
|
||||
);
|
||||
final HttpClientRequest req = await client
|
||||
.openUrl(method, target)
|
||||
.timeout(Duration(milliseconds: _options.connectTimeoutMs));
|
||||
req.headers.host = uri.host;
|
||||
headers?.forEach((String key, String value) {
|
||||
if (key.toLowerCase() == 'host') {
|
||||
return;
|
||||
}
|
||||
req.headers.set(key, value);
|
||||
});
|
||||
if (body != null && body.isNotEmpty) {
|
||||
req.add(body);
|
||||
}
|
||||
|
||||
final HttpClientResponse resp =
|
||||
await req.close().timeout(Duration(milliseconds: _options.readTimeoutMs));
|
||||
final List<int> payload =
|
||||
await resp.fold<List<int>>(<int>[], (List<int> previous, List<int> element) {
|
||||
previous.addAll(element);
|
||||
return previous;
|
||||
});
|
||||
final Map<String, List<String>> responseHeaders = <String, List<String>>{};
|
||||
resp.headers.forEach((String name, List<String> values) {
|
||||
responseHeaders[name] = values;
|
||||
});
|
||||
return AliyunHttpdnsRequestResult(
|
||||
statusCode: resp.statusCode,
|
||||
headers: responseHeaders,
|
||||
body: Uint8List.fromList(payload),
|
||||
usedIp: ip,
|
||||
);
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
throw HttpException('TLS_EMPTY_SNI_FAILED: all ip connect attempts failed, error=$lastError');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: aliyun_httpdns
|
||||
description: "Aliyun HTTPDNS Flutter plugin."
|
||||
version: 1.0.2
|
||||
version: 1.0.0
|
||||
homepage: https://help.aliyun.com/document_detail/2584339.html
|
||||
|
||||
environment:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "AlicloudHTTPDNS"
|
||||
s.version = "3.4.1"
|
||||
s.version = "1.0.0"
|
||||
s.summary = "Aliyun Mobile Service HTTPDNS iOS SDK (source distribution)."
|
||||
s.description = <<-DESC
|
||||
HTTPDNS iOS SDK 源码分发版本,提供通过 HTTP(S) 进行域名解析、
|
||||
@@ -27,6 +27,7 @@ Pod::Spec.new do |s|
|
||||
s.public_header_files = [
|
||||
"AlicloudHttpDNS/AlicloudHttpDNS.h",
|
||||
"AlicloudHttpDNS/HttpdnsService.h",
|
||||
"AlicloudHttpDNS/HttpdnsEdgeService.h",
|
||||
"AlicloudHttpDNS/Model/HttpdnsResult.h",
|
||||
"AlicloudHttpDNS/Model/HttpdnsRequest.h",
|
||||
"AlicloudHttpDNS/Log/HttpdnsLog.h",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import <AlicloudHTTPDNS/HttpdnsService.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsRequest.h>
|
||||
#import <AlicloudHTTPDNS/HttpDnsResult.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsEdgeService.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsLoggerProtocol.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsDegradationDelegate.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsIpStackDetector.h>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#ifndef PublicConstant_h
|
||||
#define PublicConstant_h
|
||||
|
||||
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"3.4.1";
|
||||
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"1.0.0";
|
||||
|
||||
#define ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY @"cn"
|
||||
#define ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY @"hk"
|
||||
|
||||
36
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.h
Normal file
36
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/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
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.m
Normal file
277
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/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
|
||||
@@ -1,73 +1,58 @@
|
||||
# Alicloud HTTPDNS iOS SDK
|
||||
# HTTPDNS iOS SDK (SNI Hidden v1.0.0)
|
||||
|
||||
面向 iOS 的 HTTP/HTTPS DNS 解析 SDK,提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 iOS 12.0。
|
||||
## 1. Init
|
||||
|
||||
## 功能特性
|
||||
- 鉴权请求与可选 AES 传输加密
|
||||
- IPv4/IPv6 双栈解析,支持自动/同时解析
|
||||
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
|
||||
- 预解析、区域路由、网络切换自动刷新
|
||||
- 可定制日志回调与会话追踪 `sessionId`
|
||||
|
||||
## 安装(CocoaPods)
|
||||
在 `Podfile` 中添加:
|
||||
|
||||
```ruby
|
||||
platform :ios, '12.0'
|
||||
target 'YourApp' do
|
||||
pod 'AlicloudHTTPDNS', '~> 3.4.1'
|
||||
end
|
||||
```
|
||||
|
||||
执行 `pod install` 安装依赖。
|
||||
|
||||
## 快速开始
|
||||
Objective‑C
|
||||
```objc
|
||||
#import <AlicloudHttpDNS/AlicloudHttpDNS.h>
|
||||
#import <AlicloudHTTPDNS/AlicloudHTTPDNS.h>
|
||||
|
||||
// 使用鉴权初始化(单例);密钥请勿硬编码到仓库
|
||||
HttpDnsService *service = [[HttpDnsService alloc] initWithAccountID:1000000
|
||||
secretKey:@"<YOUR_SECRET>"];
|
||||
[service setPersistentCacheIPEnabled:YES];
|
||||
[service setPreResolveHosts:@[@"www.aliyun.com"] byIPType:HttpdnsQueryIPTypeAuto];
|
||||
|
||||
// 同步解析(会根据网络自动选择 v4/v6)
|
||||
HttpdnsResult *r = [service resolveHostSync:@"www.aliyun.com" byIpType:HttpdnsQueryIPTypeAuto];
|
||||
NSLog(@"IPv4: %@", r.ips);
|
||||
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
|
||||
initWithAppId:@"app1f1ndpo9"
|
||||
primaryServiceHost:@"httpdns-a.example.com"
|
||||
backupServiceHost:@"httpdns-b.example.com"
|
||||
servicePort:443
|
||||
signSecret:@"your-sign-secret"]; // optional if sign is enabled
|
||||
```
|
||||
|
||||
Swift
|
||||
```swift
|
||||
import AlicloudHttpDNS
|
||||
## 2. Resolve
|
||||
|
||||
_ = HttpDnsService(accountID: 1000000, secretKey: "<YOUR_SECRET>")
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
let res = svc?.resolveHostSync("www.aliyun.com", byIpType: .auto)
|
||||
print(res?.ips ?? [])
|
||||
```objc
|
||||
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"requestId=%@ ipv4=%@ ipv6=%@ ttl=%ld", result.requestId, result.ipv4s, result.ipv6s, (long)result.ttl);
|
||||
}];
|
||||
```
|
||||
|
||||
提示
|
||||
- 启动时通过 `setPreResolveHosts(_:byIPType:)` 预热热点域名。
|
||||
- 如需在刷新期间容忍 TTL 过期,可开启 `setReuseExpiredIPEnabled:YES`。
|
||||
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障。
|
||||
## 3. Official Request Adapter (IP + Host)
|
||||
|
||||
## 源码构建
|
||||
执行 `./build_xc_framework.sh` 生成 XCFramework。脚本会从 `gitlab.alibaba-inc.com` 克隆内部构建工具;外部环境建议优先使用 CocoaPods 引入。
|
||||
```objc
|
||||
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
|
||||
[service requestURL:url method:@"GET" headers:@{@"Accept": @"application/json"} body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"status=%ld", (long)response.statusCode);
|
||||
}];
|
||||
```
|
||||
|
||||
## 测试
|
||||
- 在 Xcode 使用 Scheme `AlicloudHttpDNSTests` 运行。
|
||||
- 含 OCMock 的用例批量执行可能出现内存问题,请单个运行。
|
||||
- 非 Mock 用例使用预置参数:AccountID `1000000`,测试域名 `*.onlyforhttpdnstest.run.place`(每年需续期)。
|
||||
Behavior is fixed:
|
||||
- Resolve by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
## 依赖与链接
|
||||
- iOS 12.0+;需链接 `CoreTelephony`、`SystemConfiguration`
|
||||
- 额外库:`sqlite3.0`、`resolv`;`OTHER_LDFLAGS` 包含 `-ObjC -lz`
|
||||
## 4. Public Errors
|
||||
|
||||
## 安全说明
|
||||
- 切勿提交真实的 AccountID/SecretKey,請通过本地安全配置或 CI 注入。
|
||||
- 若担心设备时间偏差影响鉴权,可用 `setInternalAuthTimeBaseBySpecifyingCurrentTime:` 校正。
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
## Demo 与贡献
|
||||
- 示例应用:`AlicloudHttpDNSTestDemo/`
|
||||
- 贡献与提交流程请参见 `AGENTS.md`(提交信息与 PR 规范)。
|
||||
## 5. Removed Public Params
|
||||
|
||||
Do not expose legacy public parameters:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
Reference in New Issue
Block a user