管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -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 19Android 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`

View File

@@ -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);
}
}

View File

@@ -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理论上这个是内部接口不给外部使用的
* 但是已经对外暴露,所以保留

View File

@@ -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);
}
}

View File

@@ -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 上下文

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

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

View File

@@ -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";
}

View File

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

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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理论上这个是内部接口不给外部使用的
* 但是已经对外暴露,所以保留

View File

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