diff --git a/EdgeHttpDNS/HTTPDNS主计划.md b/EdgeHttpDNS/HTTPDNS主计划.md new file mode 100644 index 0000000..58fc47d --- /dev/null +++ b/EdgeHttpDNS/HTTPDNS主计划.md @@ -0,0 +1,98 @@ +# HTTPDNS 主计划(V1.1) + +## 1. 目标与范围 +1. 新增独立模块 `EdgeHttpDNS`,独立构建、独立部署。 +2. 不改造 `EdgeDNS` 现有职责,`EdgeDNS` 继续处理传统 Port 53 DNS 请求。 +3. 构建端到端链路:`SDK -> EdgeHttpDNS -> EdgeNode 动态验签 -> WAF 放行/403`。 +4. P1 包含 ECH 全量能力(记录发布、密钥与节点解密链路)。 +5. 当前阶段仅实施管理平台能力,用户平台页面暂缓。 + +## 2. 关键决策(已锁定) +1. 对外接口仅保留 `POST /resolve`。 +2. 客户端 IP 策略:源 IP 优先,支持签名覆盖 `client_ip`。 +3. 场景判定规则:按“域名接入状态”自动判定。 +4. 场景1(仅 HTTPDNS 域名):直连权威 DNS。 +5. 场景2(已接入 CDN 域名):从边缘节点池计算并返回 CDN 节点 IP。 +6. `sni_policy` 枚举:`none|mask|empty|ech`(Level2 支持 `mask` 与 `empty`)。 +7. 证书字段:`cert_fingerprints[] + fingerprint_algo`。 +8. `EdgeNode` 动态验签采用请求头签名:`X-Edge-Resolve-*`。 +9. ECH 记录由第三方 DNS API 发布(通过凭证中心编排)。 + +## 3. 系统职责拆分 +### 3.1 管控端(仅管理平台)`edge-admin` +1. 配置中心:`AppID/Secret`、解析策略、SNI 隐匿等级、证书校验策略。 +2. 凭证中心:第三方 DNS 运营商 API 凭证管理与复用。 +3. ECH 发布编排:记录创建、更新、状态追踪、失败重试。 +4. 发布灰度、回退控制与审计日志。 + +### 3.2 解析网关 `edge-httpdns` +1. 鉴权与签名校验、防重放、限流。 +2. 双场景解析编排(权威 DNS / 边缘节点池调度)。 +3. 下发 `sni_policy`、`public_sni`、证书指纹策略。 +4. 生成并返回 `verify_headers` 供客户端请求透传。 + +### 3.3 边缘节点 `edge-node` +1. SNI 与 Host 解耦路由。 +2. 动态验签(验签失败返回 403)。 +3. ECH 解密与策略联动。 + +## 4. 接口规范(摘要) +### 4.1 Endpoint +- `POST /resolve` + +### 4.2 返回关键字段 +1. `ips` +2. `ttl` +3. `sni_policy` +4. `public_sni` +5. `cert_fingerprints` +6. `fingerprint_algo` +7. `verify_headers` + +### 4.3 动态验签请求头 +1. `X-Edge-Resolve-AppId` +2. `X-Edge-Resolve-Expire` +3. `X-Edge-Resolve-Nonce` +4. `X-Edge-Resolve-Sign` + +## 5. 页面规划(当前仅管理平台) +### 5.1 管理平台(Admin) +1. HTTPDNS 集群默认配置页。 +2. HTTPDNS 应用管理页(含 AppID/Secret、启停、限流、策略覆写)。 +3. 域名绑定与策略页(含测试 IP 指向 CDN 节点能力)。 +4. 边缘节点与调度策略页。 +5. 第三方 DNS 凭证中心页。 +6. ECH 发布状态与审计页。 +7. 解析调试页。 +8. WAF 动态验证规则页。 +9. 一键回退与发布控制页。 + +### 5.2 用户平台(User) +- 暂缓,不在当前阶段实施。 +- 所有配置与操作先收敛到管理平台。 + +## 6. SDK 路线 +1. 三端均采用 fork 改造路线(Android / iOS / Flutter)。 +2. 已落地源码目录: + - `EdgeHttpDNS/sdk/android` + - `EdgeHttpDNS/sdk/ios` + - `EdgeHttpDNS/sdk/flutter/aliyun_httpdns` +3. 溯源文档: + - `EdgeHttpDNS/sdk/SOURCE_LOCK.md` + - `EdgeHttpDNS/sdk/THIRD_PARTY_NOTICES.md` + +## 7. 实施阶段(更新) +1. Phase A:管理平台配置模型与页面(不做用户平台)。 +2. Phase B:`EdgeHttpDNS` 核心解析链路与双场景实现。 +3. Phase C:`EdgeNode` 动态验签与 WAF 联动。 +4. Phase D:ECH 密钥、记录发布、节点解密联调。 +5. Phase E:三端 SDK 改造与灰度上线。 +6. Phase F:用户平台能力评估与二期补齐(可选)。 + +## 8. 验收标准 +1. 场景判定准确,返回 IP 正确。 +2. 动态验签通过可放行,失败稳定返回 403。 +3. `/resolve` 返回字段与 SDK 消费一致。 +4. ECH 记录发布成功且链路可用。 +5. 管理平台可独立完成全流程配置、发布、回退与审计。 +6. 具备观测能力(QPS、错误率、签名通过率、403 率)。 diff --git a/EdgeHttpDNS/sdk/SOURCE_LOCK.md b/EdgeHttpDNS/sdk/SOURCE_LOCK.md new file mode 100644 index 0000000..e1f0194 --- /dev/null +++ b/EdgeHttpDNS/sdk/SOURCE_LOCK.md @@ -0,0 +1,25 @@ +# EdgeHttpDNS SDK Source Lock + +This directory vendors upstream SDK source snapshots for HTTPDNS integration. + +Fetched at (UTC): `2026-02-18T09:31:28Z` + +## Android SDK + +- Upstream repository: `https://github.com/aliyun/alibabacloud-httpdns-android-sdk` +- Locked commit: `eeb17d677161ec94b5f41a9d6437501ddc24e6d2` +- Local path: `EdgeHttpDNS/sdk/android` + +## iOS SDK + +- Upstream repository: `https://github.com/aliyun/alibabacloud-httpdns-ios-sdk` +- Locked commit: `19f5bacd1d1399a00ba654bb72ababb3e91d0a3a` +- Local path: `EdgeHttpDNS/sdk/ios` + +## Flutter Plugin SDK + +- Upstream repository: `https://github.com/aliyun/alicloud-flutter-demo` +- Locked commit: `588b807e5480d8592c57d439a6b1c52e8c313569` +- Imported subtree: `httpdns_flutter_demo/packages/aliyun_httpdns` +- Local path: `EdgeHttpDNS/sdk/flutter/aliyun_httpdns` + diff --git a/EdgeHttpDNS/sdk/THIRD_PARTY_NOTICES.md b/EdgeHttpDNS/sdk/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..1ce5e94 --- /dev/null +++ b/EdgeHttpDNS/sdk/THIRD_PARTY_NOTICES.md @@ -0,0 +1,31 @@ +# Third-Party Notices for EdgeHttpDNS SDK Sources + +This directory includes third-party source snapshots imported from Alibaba Cloud open-source repositories. + +## 1) Android SDK (`EdgeHttpDNS/sdk/android`) + +- Source: `https://github.com/aliyun/alibabacloud-httpdns-android-sdk` +- Commit: `eeb17d677161ec94b5f41a9d6437501ddc24e6d2` +- License file in imported source: + - `EdgeHttpDNS/sdk/android/LICENSE` +- Observed license: MIT License + +## 2) Flutter plugin (`EdgeHttpDNS/sdk/flutter/aliyun_httpdns`) + +- Source: `https://github.com/aliyun/alicloud-flutter-demo` +- Commit: `588b807e5480d8592c57d439a6b1c52e8c313569` +- Imported subtree: `httpdns_flutter_demo/packages/aliyun_httpdns` +- License file in imported source: + - `EdgeHttpDNS/sdk/flutter/aliyun_httpdns/LICENSE` +- Observed license: MIT License + +## 3) iOS SDK (`EdgeHttpDNS/sdk/ios`) + +- Source: `https://github.com/aliyun/alibabacloud-httpdns-ios-sdk` +- Commit: `19f5bacd1d1399a00ba654bb72ababb3e91d0a3a` +- Current status: + - No standalone top-level `LICENSE` file was found in the upstream repository snapshot imported here. + - Some source headers reference Apache 2.0 notices, but repository-level license declaration is not explicit. +- Action required before merging to protected branch: + - Complete legal/license confirmation for iOS source usage. + diff --git a/EdgeHttpDNS/sdk/android/.github/ISSUE_TEMPLATE/bug_report.md b/EdgeHttpDNS/sdk/android/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..466bf4b --- /dev/null +++ b/EdgeHttpDNS/sdk/android/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add logcat logs to help explain your problem. + +**Environment (please complete the following information):** + - Device: [e.g. Pixel 3] + - OS: [e.g. Android 10] + - SDK Version [e.g. 2.0.2] + +**Additional context** +Add any other context about the problem here. diff --git a/EdgeHttpDNS/sdk/android/.gitignore b/EdgeHttpDNS/sdk/android/.gitignore new file mode 100644 index 0000000..6990041 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/.gitignore @@ -0,0 +1,26 @@ +.DS_Store +*.swp + +# Kiro IDE +.kiro/ + +# VSCode +.vscode/ + +# Gradle +build +.gradle/ +target/ + +# Intellij project files +*.iml +*.ipr +*.iws +.idea/ + +# sonar +.sonar/ + +# Android +local.properties +nohup.out diff --git a/EdgeHttpDNS/sdk/android/LICENSE b/EdgeHttpDNS/sdk/android/LICENSE new file mode 100644 index 0000000..c5a4392 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Alibaba Cloud + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/EdgeHttpDNS/sdk/android/README.md b/EdgeHttpDNS/sdk/android/README.md new file mode 100644 index 0000000..1b74dd9 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/README.md @@ -0,0 +1,148 @@ +# Alicloud HTTPDNS Android SDK + +面向 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 + +```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; + +// 初始化配置 +new InitConfig.Builder() + .setContext(context) + .setSecretKey("YOUR_SECRET_KEY") + .setEnableExpiredIp(true) // 允许返回过期 IP + .buildFor("YOUR_ACCOUNT_ID"); + +// 获取实例 +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(); +``` + +### 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` 中配置测试账号: + +```java +private HttpDnsHolder holderA = new HttpDnsHolder("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret"); +private HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null); +``` + +> 两个实例用于测试实例间互不影响,体验时只需配置一个 + +#### 2. demo module(推荐) + +使用 Kotlin + MVVM 开发,功能更丰富。在 `demo/build.gradle` 中配置测试账号: + +```groovy +buildConfigField "String", "ACCOUNT_ID", "\"请替换为测试用实例的accountId\"" +buildConfigField "String", "SECRET_KEY", "\"请替换为测试用实例的secret\"" +buildConfigField "String", "AES_SECRET_KEY", "\"请替换为测试用实例的aes\"" +``` + +## 依赖与要求 + +- Android API 19+(Android 4.4+) +- 需要权限:`INTERNET`、`ACCESS_NETWORK_STATE` + +## 安全说明 + +- 切勿提交真实的 AccountID/SecretKey,请通过本地安全配置或 CI 注入 +- 若担心设备时间偏差影响鉴权,可使用 `setAuthCurrentTime()` 校正时间 + +## 文档 + +官方文档:[Android SDK手册](https://help.aliyun.com/document_detail/435250.html) + +## 感谢 + +本项目中 Inet64Util 工具类由 [Shinelw](https://github.com/Shinelw) 贡献支持,感谢。 diff --git a/EdgeHttpDNS/sdk/android/app/.gitignore b/EdgeHttpDNS/sdk/android/app/.gitignore new file mode 100644 index 0000000..0ce017c --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/.gitignore @@ -0,0 +1,2 @@ +/build +/libs/alicloud-android-httpdns-*.aar diff --git a/EdgeHttpDNS/sdk/android/app/build.gradle b/EdgeHttpDNS/sdk/android/app/build.gradle new file mode 100644 index 0000000..269ea3b --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/build.gradle @@ -0,0 +1,84 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.aliyun.ams.httpdns.demo' + compileSdkVersion 34 + buildToolsVersion "30.0.2" + defaultConfig { + applicationId "com.aliyun.ams.httpdns.demo2" + minSdkVersion 19 + targetSdkVersion 34 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + forTest { + // 注意这里的配置,并不是需要编译forTest的app,而是避免httpdns-sdk在AndroidStudio改为end2end运行测试时 BuildVariants报错 + initWith release + debuggable true + } + } + + variantFilter { variant -> + def names = variant.flavors*.name + def type = variant.buildType.name + // To check for a certain build type, use variant.buildType.name == "" + if ((names.contains("normal") && type.contains("forTest")) + || (names.contains("intl") && type.contains("forTest")) + || (names.contains("end2end") && type.contains("release")) + || (names.contains("end2end") && type.contains("debug")) + ) { + // Gradle ignores any variants that satisfy the conditions above. + setIgnore(true) + } + } + + testOptions { + unitTests { + all { + jvmArgs '-noverify' + systemProperty 'robolectric.logging.enable', true + } + } + } + + flavorDimensions "version" + + productFlavors { + normal { + + } + + intl { + + } + + end2end { + // 注意这里的配置,并不是需要编译end2end的app,而是避免httpdns-sdk在AndroidStudio改为end2end运行测试时 BuildVariants报错 + } + } +} + +configurations.all { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + +dependencies { + + implementation project(':httpdns-sdk') + + implementation("com.squareup.okhttp3:okhttp:3.9.0") +} diff --git a/EdgeHttpDNS/sdk/android/app/proguard-rules.pro b/EdgeHttpDNS/sdk/android/app/proguard-rules.pro new file mode 100644 index 0000000..cd5518a --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/proguard-rules.pro @@ -0,0 +1,30 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/liyazhou/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +-dontwarn okhttp3.** +-dontwarn okio.** +-dontwarn com.alibaba.sdk.android.httpdns.test.** +-dontwarn com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector +-keep class com.aliyun.ams.ipdetector.Inet64Util{*;} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/AndroidManifest.xml b/EdgeHttpDNS/sdk/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d8f2a0c --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsActivity.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsActivity.java new file mode 100644 index 0000000..649f123 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsActivity.java @@ -0,0 +1,356 @@ +package com.aliyun.ams.httpdns.demo; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; + +import com.alibaba.sdk.android.httpdns.DegradationFilter; +import com.alibaba.sdk.android.httpdns.NetType; +import com.alibaba.sdk.android.httpdns.RequestIpType; +import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector; +import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean; +import com.aliyun.ams.httpdns.demo.base.BaseActivity; +import com.aliyun.ams.httpdns.demo.http.HttpUrlConnectionRequest; +import com.aliyun.ams.httpdns.demo.okhttp.OkHttpRequest; +import com.aliyun.ams.httpdns.demo.utils.ThreadUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author zonglin.nzl + * @date 8/30/22 + */ +public class HttpDnsActivity extends BaseActivity { + + public static final String SCHEMA_HTTPS = "https://"; + public static final String SCHEMA_HTTP = "http://"; + + public static final String TAOBAO_URL = "www.taobao.com"; + public static final String ALIYUN_URL = "www.aliyun.com"; + + public static final String[] hosts = new String[]{ + TAOBAO_URL, ALIYUN_URL + }; + + public static String getUrl(String schema, String host) { + return schema + host; + } + + /** + * 要请求的schema + */ + private String schema = SCHEMA_HTTPS; + /** + * 要请求的域名 + */ + private String host = ALIYUN_URL; + /** + * 要解析的ip类型 + */ + private RequestIpType requestIpType = RequestIpType.v4; + + private HttpUrlConnectionRequest httpUrlConnectionRequest; + private OkHttpRequest okHttpRequest; + private NetworkRequest networkRequest = httpUrlConnectionRequest; + + private ExecutorService worker = Executors.newSingleThreadExecutor(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + httpUrlConnectionRequest = new HttpUrlConnectionRequest(this); + okHttpRequest = new OkHttpRequest(this); + networkRequest = httpUrlConnectionRequest; + + addFourButton("切换实例", v -> { + MyApp.getInstance().changeHolder(); + sendLog("httpdns实例已切换"); + }, "获取配置", v -> { + sendLog(MyApp.getInstance().getCurrentHolder().getCurrentConfig()); + sendLog("要解析的域名是" + host); + sendLog("要解析的ip类型是" + requestIpType.name()); + sendLog("要模拟请求的url是" + getUrl(schema, host)); + sendLog("模拟请求的网络框架是" + (networkRequest == httpUrlConnectionRequest ? " HttpUrlConnection" : "OkHttp")); + }, "清除配置缓存", v -> { + MyApp.getInstance().getCurrentHolder().cleanSp(); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "配置缓存清除, 重启生效"); + }, "清除日志", v -> cleanLog()); + + addTwoButton("开启https", v -> { + MyApp.getInstance().getCurrentHolder().setEnableHttps(true); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "开启https"); + }, "关闭https", v -> { + MyApp.getInstance().getCurrentHolder().setEnableHttps(false); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "关闭https"); + }); + + addTwoButton("允许过期IP", v -> { + MyApp.getInstance().getCurrentHolder().setEnableExpiredIp(true); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "允许过期IP"); + }, "不允许过期IP", v -> { + MyApp.getInstance().getCurrentHolder().setEnableExpiredIp(false); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "不允许过期IP"); + }); + + + addTwoButton("允许持久化缓存", v -> { + MyApp.getInstance().getCurrentHolder().setEnableCacheIp(true); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "允许持久化缓存"); + }, "不允许持久化缓存", v -> { + MyApp.getInstance().getCurrentHolder().setEnableCacheIp(false); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "不允许持久化缓存"); + }); + + addThreeButton("设置中国大陆", v -> { + MyApp.getInstance().getCurrentHolder().setRegion(null); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到中国大陆"); + }, "设置中国香港", v -> { + MyApp.getInstance().getCurrentHolder().setRegion("hk"); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到中国香港"); + }, "设置新加坡", v -> { + MyApp.getInstance().getCurrentHolder().setRegion("sg"); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到新加坡"); + }); + + addEditTextButton("超时时长 ms", "设置超时ms", view -> { + EditText et = (EditText) view; + int timeout = Integer.parseInt(et.getEditableText().toString()); + MyApp.getInstance().getCurrentHolder().setTimeout(timeout); + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "设置超时 " + timeout); + }); + + addTwoButton("开启降级", v -> { + // 注意:降级过滤器现在需要通过InitConfig设置,重启应用生效 + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "降级功能需要通过InitConfig配置"); + }, "关闭降级", v -> { + // 注意:降级过滤器现在需要通过InitConfig设置,重启应用生效 + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "降级功能需要通过InitConfig配置"); + }); + + addTwoButton("开启网络变化后预解析", v -> { + // 注意:此功能现在需要通过InitConfig设置,重启应用生效 + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "网络变化后预解析需要通过InitConfig配置"); + }, "关闭网络变化后预解析", v -> { + // 注意:此功能现在需要通过InitConfig设置,重启应用生效 + sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "网络变化后预解析需要通过InitConfig配置"); + }); + + addView(R.layout.item_autocomplete_edittext_button, view -> { + final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne); + final EditText etOne = view.findViewById(R.id.etOne); + Button btnOne = view.findViewById(R.id.btnOne); + + actvOne.setHint("请输入域名"); + ArrayAdapter adapter = new ArrayAdapter(getApplicationContext(), + android.R.layout.simple_dropdown_item_1line, hosts); + actvOne.setAdapter(adapter); + + etOne.setHint("请输入自定义ttl"); + + btnOne.setText("指定域名ttl s"); + btnOne.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String host = actvOne.getEditableText().toString(); + int ttl = Integer.parseInt(etOne.getEditableText().toString()); + MyApp.getInstance().getCurrentHolder().setHostTtl(host, ttl); + sendLog("指定域名" + host + "的ttl为" + ttl + "秒"); + } + }); + }); + + addView(R.layout.item_autocomplete_edittext_button, view -> { + final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne); + final EditText etOne = view.findViewById(R.id.etOne); + Button btnOne = view.findViewById(R.id.btnOne); + + actvOne.setHint("域名"); + ArrayAdapter adapter = new ArrayAdapter<>(getApplicationContext(), + android.R.layout.simple_dropdown_item_1line, hosts); + actvOne.setAdapter(adapter); + + etOne.setHint("请输入端口"); + + btnOne.setText("添加ipRanking配置"); + btnOne.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String host = actvOne.getEditableText().toString(); + int port = Integer.parseInt(etOne.getEditableText().toString()); + MyApp.getInstance().getCurrentHolder().addIpProbeItem(new IPRankingBean(host, port)); + sendLog("添加域名" + host + " 探测"); + } + }); + }); + + addAutoCompleteTextViewButton(hosts, "主站域名", "添加主站域名", view -> { + AutoCompleteTextView actvOne = (AutoCompleteTextView) view; + String host = actvOne.getEditableText().toString(); + MyApp.getInstance().getCurrentHolder().addHostWithFixedIp(host); + sendLog("添加主站域名" + host); + }); + + addAutoCompleteTextViewButton(hosts, "域名", "删除指定域名的缓存", view -> { + AutoCompleteTextView actvOne = (AutoCompleteTextView) view; + String host = actvOne.getEditableText().toString(); + ArrayList list = new ArrayList<>(); + list.add(host); + MyApp.getInstance().getService().cleanHostCache(list); + sendLog("清除指定host的缓存" + host); + }); + + addOneButton("清除所有缓存", v -> { + MyApp.getInstance().getService().cleanHostCache(null); + sendLog("清除所有缓存"); + }); + + addFourButton("获取当前网络状态", v -> { + NetType type = HttpDnsNetworkDetector.getInstance().getNetType(getApplicationContext()); + sendLog("获取网络状态 " + type.name()); + }, "禁用网络状态缓存", v -> { + HttpDnsNetworkDetector.getInstance().disableCache(true); + sendLog("网络状态 禁用缓存 "); + }, "开启网络状态缓存", v -> { + HttpDnsNetworkDetector.getInstance().disableCache(false); + sendLog("网络状态 开启缓存 "); + }, "清除网络状态缓存", v -> { + HttpDnsNetworkDetector.getInstance().cleanCache(false); + sendLog("网络状态清除缓存 "); + }); + + addTwoButton("禁止读取IP", v -> { + HttpDnsNetworkDetector.getInstance().setCheckInterface(false); + sendLog("查询网络状态时 禁止读取IP"); + }, "允许读取IP", v -> { + HttpDnsNetworkDetector.getInstance().setCheckInterface(true); + sendLog("查询网络状态时 允许读取IP"); + }); + + addAutoCompleteTextViewButton(hosts, "域名", "设置检测网络使用的域名", view -> { + AutoCompleteTextView actvOne = (AutoCompleteTextView) view; + String host = actvOne.getEditableText().toString(); + HttpDnsNetworkDetector.getInstance().setHostToCheckNetType(host); + sendLog("设置检测网络状态使用的域名为" + host); + }); + + addTwoButton("模拟请求使用https请求", v -> { + schema = SCHEMA_HTTPS; + sendLog("测试url使用https"); + }, "模拟请求使用http请求", v -> { + schema = SCHEMA_HTTP; + sendLog("测试url使用http"); + }); + + addTwoButton("HttpUrlConnection", v -> { + networkRequest = httpUrlConnectionRequest; + sendLog("指定网络实现方式为HttpUrlConnection"); + }, "Okhttp", v -> { + networkRequest = okHttpRequest; + sendLog("指定网络实现方式为okhttp"); + }); + + + addFourButton("指定v4", v -> { + requestIpType = RequestIpType.v4; + sendLog("要解析的IP类型指定为ipv4"); + }, "指定v6", v -> { + requestIpType = RequestIpType.v6; + sendLog("要解析的IP类型指定为ipv6"); + }, "都解析", v -> { + requestIpType = RequestIpType.both; + sendLog("要解析的IP类型指定为ipv4和ipv6"); + }, "自动判断", v -> { + requestIpType = RequestIpType.auto; + sendLog("要解析的IP类型根据网络情况自动判断"); + }); + + addAutoCompleteTextViewButton(hosts, "域名", "指定要解析的域名", new OnButtonClick() { + @Override + public void onBtnClick(View view) { + AutoCompleteTextView actvOne = (AutoCompleteTextView) view; + host = actvOne.getEditableText().toString(); + sendLog("要解析的域名" + host); + } + }); + + addTwoButton("异步解析", v -> worker.execute(new Runnable() { + @Override + public void run() { + sendLog("开始发起网络请求"); + sendLog("网络实现方式为" + (networkRequest == httpUrlConnectionRequest ? "HttpUrlConnection" : "okhttp")); + String url = getUrl(schema, host); + sendLog("url is " + url); + sendLog("httpdns 使用 异步解析api"); + sendLog("指定解析ip类型为" + requestIpType.name()); + networkRequest.updateHttpDnsConfig(true, requestIpType); + try { + String response = networkRequest.httpGet(url); + if (response != null && response.length() > 30) { + response = response.trim(); + if (response.length() > 30) { + response = response.substring(0, 30) + "..."; + } + } + sendLog("请求结束 response is " + response + " 完整记录请看logcat日志"); + } catch (Exception e) { + e.printStackTrace(); + sendLog("请求结束 发生异常 " + e.getClass().getName() + e.getMessage() + " 完整记录请看logcat日志"); + } + + } + }), "同步解析", v -> worker.execute(() -> { + sendLog("开始发起网络请求"); + sendLog("网络实现方式为" + (networkRequest == httpUrlConnectionRequest ? "HttpUrlConnection" : "okhttp")); + String url = getUrl(schema, host); + sendLog("url is " + url); + sendLog("httpdns 使用 同步解析api"); + sendLog("指定解析ip类型为" + requestIpType.name()); + networkRequest.updateHttpDnsConfig(false, requestIpType); + try { + String response = networkRequest.httpGet(url); + if (response != null && response.length() > 30) { + response = response.substring(0, 30) + "..."; + } + sendLog("请求结束 response is " + response + " 完整记录请看logcat日志"); + } catch (Exception e) { + e.printStackTrace(); + sendLog("请求结束 发生异常 " + e.getClass().getName() + e.getMessage() + " 完整记录请看logcat日志"); + } + })); + + addThreeButton("发起预解析", v -> worker.execute(() -> { + ArrayList tmp = new ArrayList<>(); + Collections.addAll(tmp, hosts); + MyApp.getInstance().getService().setPreResolveHosts(tmp, requestIpType); + sendLog("已发起预解析请求"); + }), "跳转SDNS测试界面", + v -> startActivity(new Intent(HttpDnsActivity.this, SDNSActivity.class)), "跳转Webview测试界面", new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(HttpDnsActivity.this, WebViewActivity.class)); + } + }); + + + final String[] validHosts = new String[]{ + "www.aliyun.com", + "www.taobao.com" + }; + + addTwoButton("并发异步请求", v -> { + ThreadUtil.multiThreadTest(validHosts, 100, 20, 10 * 60 * 1000, true, requestIpType); + sendLog("异步api并发测试开始,大约耗时10分钟,请最后查看logcat日志,确认结果,建议关闭httpdns日志,避免日志量过大"); + }, "并发同步请求", v -> { + ThreadUtil.multiThreadTest(validHosts, 100, 20, 10 * 60 * 1000, false, requestIpType); + sendLog("同步api并发测试开始,大约耗时10分钟,请最后查看logcat日志,确认结果,建议关闭httpdns日志,避免日志量过大"); + }); + + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsHolder.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsHolder.java new file mode 100644 index 0000000..2026ff5 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/HttpDnsHolder.java @@ -0,0 +1,342 @@ +package com.aliyun.ams.httpdns.demo; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.alibaba.sdk.android.httpdns.CacheTtlChanger; +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; +import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean; +import com.aliyun.ams.httpdns.demo.utils.SpUtil; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * 保存Httpdns 及 相关配置, + * 方便修改 + */ +public class HttpDnsHolder { + + public static final String SP_PREFIX = "httpdns_config_"; + + public static String getSpName(String accountId) { + return SP_PREFIX + accountId; + } + + public static final String KEY_EXPIRED_IP = "enableExpiredIp"; + public static final String KEY_CACHE_IP = "enableCacheIp"; + public static final String KEY_TIMEOUT = "timeout"; + public static final String KEY_HTTPS = "enableHttps"; + public static final String KEY_IP_RANKING_ITEMS = "ipProbeItems"; + public static final String KEY_REGION = "region"; + public static final String KEY_TTL_CHANGER = "cacheTtlChanger"; + public static final String KEY_HOST_NOT_CHANGE = "hostListWithFixedIp"; + + private HttpDnsService service; + private String accountId; + private String secret; + private Context context; + + private boolean enableExpiredIp; + private boolean enableCacheIp; + private int timeout; + private boolean enableHttps; + private ArrayList ipRankingList = null; + private String region; + private ArrayList hostListWithFixedIp; + private HashMap ttlCache; + private final CacheTtlChanger cacheTtlChanger = new CacheTtlChanger() { + @Override + public int changeCacheTtl(String host, RequestIpType type, int ttl) { + if (ttlCache != null && ttlCache.get(host) != null) { + return ttlCache.get(host); + } + return ttl; + } + }; + + public HttpDnsHolder(String accountId) { + this.accountId = accountId; + } + + public HttpDnsHolder(String accountId, String secret) { + this.accountId = accountId; + this.secret = secret; + } + + /** + * 初始化httpdns的配置 + * + * @param context + */ + public void init(Context context) { + this.context = context.getApplicationContext(); + SpUtil.readSp(context, getSpName(accountId), new SpUtil.OnGetSp() { + @Override + public void onGetSp(SharedPreferences sp) { + enableExpiredIp = sp.getBoolean(KEY_EXPIRED_IP, true); + enableCacheIp = sp.getBoolean(KEY_CACHE_IP, false); + timeout = sp.getInt(KEY_TIMEOUT, 5 * 1000); + enableHttps = sp.getBoolean(KEY_HTTPS, false); + ipRankingList = convertToProbeList(sp.getString(KEY_IP_RANKING_ITEMS, null)); + region = sp.getString(KEY_REGION, null); + ttlCache = convertToCacheTtlData(sp.getString(KEY_TTL_CHANGER, null)); + hostListWithFixedIp = convertToStringList(sp.getString(KEY_HOST_NOT_CHANGE, null)); + } + }); + + // 初始化httpdns 的配置,此步骤需要在第一次获取HttpDnsService实例之前 + new InitConfig.Builder() + .setEnableExpiredIp(enableExpiredIp) + .setEnableCacheIp(enableCacheIp) + .setTimeout(timeout) + .setEnableHttps(enableHttps) + .setIPRankingList(ipRankingList) + .setRegion(region) + .configCacheTtlChanger(cacheTtlChanger) + .configHostWithFixedIp(hostListWithFixedIp) + .buildFor(accountId); + + getService(); + } + + public HttpDnsService getService() { + if (service == null) { + if (secret != null) { + service = HttpDns.getService(context, accountId, secret); + } else { + service = HttpDns.getService(context, accountId); + } + } + return service; + } + + public String getAccountId() { + return accountId; + } + + public void setEnableExpiredIp(final boolean enableExpiredIp) { + this.enableExpiredIp = enableExpiredIp; + // 注意:此配置需要重启应用生效,因为现在通过InitConfig设置 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putBoolean(KEY_EXPIRED_IP, enableExpiredIp); + } + }); + } + + public void setEnableCacheIp(final boolean enableCacheIp) { + this.enableCacheIp = enableCacheIp; + // 注意:此配置需要重启应用生效,因为现在通过InitConfig设置 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putBoolean(KEY_CACHE_IP, enableCacheIp); + } + }); + } + + public void setTimeout(final int timeout) { + this.timeout = timeout; + // 注意:此配置需要重启应用生效,因为现在通过InitConfig设置 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putInt(KEY_TIMEOUT, timeout); + } + }); + } + + public void setEnableHttps(final boolean enableHttps) { + this.enableHttps = enableHttps; + // 注意:此配置需要重启应用生效,因为现在通过InitConfig设置 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putBoolean(KEY_HTTPS, enableHttps); + } + }); + } + + public void setRegion(final String region) { + this.region = region; + getService().setRegion(region); + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_REGION, region); + } + }); + } + + public void addHostWithFixedIp(String host) { + if (this.hostListWithFixedIp == null) { + this.hostListWithFixedIp = new ArrayList<>(); + } + this.hostListWithFixedIp.add(host); + // 重启生效 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_HOST_NOT_CHANGE, convertHostList(hostListWithFixedIp)); + } + }); + } + + public void addIpProbeItem(IPRankingBean ipProbeItem) { + if (this.ipRankingList == null) { + this.ipRankingList = new ArrayList<>(); + } + this.ipRankingList.add(ipProbeItem); + // 注意:此配置需要重启应用生效,因为现在通过InitConfig设置 + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_IP_RANKING_ITEMS, convertProbeList(ipRankingList)); + } + }); + } + + + public void setHostTtl(String host, int ttl) { + if (ttlCache == null) { + ttlCache = new HashMap<>(); + } + ttlCache.put(host, ttl); + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_TTL_CHANGER, convertTtlCache(ttlCache)); + } + }); + } + + public void cleanSp() { + SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.clear(); + } + }); + } + + public String getCurrentConfig() { + StringBuilder sb = new StringBuilder(); + sb.append("当前配置 accountId : ").append(accountId).append("\n") + .append("是否允许过期IP : ").append(enableExpiredIp).append("\n") + .append("是否开启本地缓存 : ").append(enableCacheIp).append("\n") + .append("是否开启HTTPS : ").append(enableHttps).append("\n") + .append("当前region设置 : ").append(region).append("\n") + .append("当前超时设置 : ").append(timeout).append("\n") + .append("当前探测配置 : ").append(convertProbeList(ipRankingList)).append("\n") + .append("当前缓存配置 : ").append(convertTtlCache(ttlCache)).append("\n") + .append("当前主站域名配置 : ").append(convertHostList(hostListWithFixedIp)).append("\n") + ; + return sb.toString(); + } + + + private static String convertHostList(List hostListWithFixedIp) { + if (hostListWithFixedIp == null) { + return null; + } + JSONArray array = new JSONArray(); + for (String host : hostListWithFixedIp) { + array.put(host); + } + return array.toString(); + } + + private static String convertTtlCache(HashMap ttlCache) { + if (ttlCache == null) { + return null; + } + JSONObject jsonObject = new JSONObject(); + for (String host : ttlCache.keySet()) { + try { + jsonObject.put(host, ttlCache.get(host)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return jsonObject.toString(); + } + + private static String convertProbeList(List ipProbeItems) { + if (ipProbeItems == null) { + return null; + } + JSONObject jsonObject = new JSONObject(); + for (IPRankingBean item : ipProbeItems) { + try { + jsonObject.put(item.getHostName(), item.getPort()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return jsonObject.toString(); + } + + private static ArrayList convertToProbeList(String json) { + if (json == null) { + return null; + } + try { + JSONObject jsonObject = new JSONObject(json); + ArrayList list = new ArrayList<>(); + for (Iterator it = jsonObject.keys(); it.hasNext(); ) { + String host = it.next(); + list.add(new IPRankingBean(host, jsonObject.getInt(host))); + } + return list; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + private static HashMap convertToCacheTtlData(String json) { + if (json == null) { + return null; + } + try { + JSONObject jsonObject = new JSONObject(json); + HashMap map = new HashMap<>(); + for (Iterator it = jsonObject.keys(); it.hasNext(); ) { + String host = it.next(); + map.put(host, jsonObject.getInt(host)); + } + return map; + } catch (JSONException e) { + e.printStackTrace(); + } + + return null; + } + + private static ArrayList convertToStringList(String json) { + if (json != null) { + try { + JSONArray array = new JSONArray(json); + ArrayList list = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + list.add(array.getString(i)); + } + return list; + } catch (JSONException e) { + e.printStackTrace(); + } + } + return null; + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/MyApp.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/MyApp.java new file mode 100644 index 0000000..3ab1ea2 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/MyApp.java @@ -0,0 +1,92 @@ +package com.aliyun.ams.httpdns.demo; + +import android.app.Application; +import android.content.SharedPreferences; +import android.util.Log; + +import com.alibaba.sdk.android.httpdns.HttpDnsService; +import com.alibaba.sdk.android.httpdns.ILogger; +import com.alibaba.sdk.android.httpdns.log.HttpDnsLog; +import com.aliyun.ams.httpdns.demo.utils.SpUtil; + +public class MyApp extends Application { + + private static final String SP_NAME = "HTTPDNS_DEMO"; + private static final String KEY_INSTANCE = "KEY_INSTANCE"; + private static final String VALUE_INSTANCE_A = "A"; + private static final String VALUE_INSTANCE_B = "B"; + + public static final String TAG = "HTTPDNS DEMO"; + private static MyApp instance; + + public static MyApp getInstance() { + return instance; + } + + private final HttpDnsHolder holderA = new HttpDnsHolder("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret"); + private final HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null); + + private HttpDnsHolder current = holderA; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + + // 开启logcat 日志 默认关闭, 开发测试过程中可以开启 + HttpDnsLog.enable(true); + // 注入日志接口,接受httpdns的日志,开发测试过程中可以开启, 基础日志需要先enable才生效,一些错误日志不需要 + HttpDnsLog.setLogger(new ILogger() { + @Override + public void log(String msg) { + Log.d("HttpDnsLogger", msg); + } + }); + + // 初始化httpdns的配置 + holderA.init(this); + holderB.init(this); + + SpUtil.readSp(this, SP_NAME, new SpUtil.OnGetSp() { + @Override + public void onGetSp(SharedPreferences sp) { + String flag = sp.getString(KEY_INSTANCE, VALUE_INSTANCE_A); + if (flag.equals(VALUE_INSTANCE_A)) { + current = holderA; + } else { + current = holderB; + } + } + }); + } + + public HttpDnsHolder getCurrentHolder() { + return current; + } + + public HttpDnsHolder changeHolder() { + if (current == holderA) { + current = holderB; + SpUtil.writeSp(instance, SP_NAME, new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_INSTANCE, VALUE_INSTANCE_B); + } + }); + } else { + current = holderA; + SpUtil.writeSp(instance, SP_NAME, new SpUtil.OnGetSpEditor() { + @Override + public void onGetSpEditor(SharedPreferences.Editor editor) { + editor.putString(KEY_INSTANCE, VALUE_INSTANCE_A); + } + }); + } + return current; + } + + public HttpDnsService getService() { + return current.getService(); + } + +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/NetworkRequest.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/NetworkRequest.java new file mode 100644 index 0000000..cbade10 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/NetworkRequest.java @@ -0,0 +1,20 @@ +package com.aliyun.ams.httpdns.demo; + +import com.alibaba.sdk.android.httpdns.RequestIpType; + +public interface NetworkRequest { + + /** + * 设置httpdns的配置 + */ + void updateHttpDnsConfig(boolean async, RequestIpType requestIpType); + + /** + * get请求 + * + * @param url + * @return + */ + String httpGet(String url) throws Exception; + +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/SDNSActivity.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/SDNSActivity.java new file mode 100644 index 0000000..7c44fb6 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/SDNSActivity.java @@ -0,0 +1,132 @@ +package com.aliyun.ams.httpdns.demo; + +import android.os.Bundle; +import android.view.View; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; + +import com.alibaba.sdk.android.httpdns.HTTPDNSResult; +import com.alibaba.sdk.android.httpdns.RequestIpType; +import com.aliyun.ams.httpdns.demo.base.BaseActivity; + +import java.util.HashMap; + +import static com.aliyun.ams.httpdns.demo.HttpDnsActivity.ALIYUN_URL; + +public class SDNSActivity extends BaseActivity { + + private final HashMap globalParams = new HashMap<>(); + /** + * 要请求的域名 + */ + private String host = HttpDnsActivity.ALIYUN_URL; + /** + * 要解析的ip类型 + */ + private RequestIpType requestIpType = RequestIpType.v4; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addEditTextEditTextButton("key", "value", "添加全局配置", new OnButtonClickMoreView() { + @Override + public void onBtnClick(View[] views) { + EditText etOne = (EditText) views[0]; + EditText etTwo = (EditText) views[1]; + + String key = etOne.getEditableText().toString(); + String value = etTwo.getEditableText().toString(); + + globalParams.put(key, value); + + // 注意:SDNS全局参数现在需要通过InitConfig设置,重启应用生效 + sendLog("添加全局参数 " + key + " : " + value); + } + }); + + addOneButton("清除全局配置", new View.OnClickListener() { + @Override + public void onClick(View v) { + globalParams.clear(); + // 注意:SDNS全局参数现在需要通过InitConfig设置,重启应用生效 + sendLog("清除全局参数"); + } + }); + + addFourButton("指定v4", new View.OnClickListener() { + @Override + public void onClick(View v) { + requestIpType = RequestIpType.v4; + sendLog("要解析的IP类型指定为ipv4"); + } + }, "指定v6", new View.OnClickListener() { + @Override + public void onClick(View v) { + requestIpType = RequestIpType.v6; + sendLog("要解析的IP类型指定为ipv6"); + } + }, "都解析", new View.OnClickListener() { + @Override + public void onClick(View v) { + requestIpType = RequestIpType.both; + sendLog("要解析的IP类型指定为ipv4和ipv6"); + } + }, "自动判断", new View.OnClickListener() { + @Override + public void onClick(View v) { + requestIpType = RequestIpType.auto; + sendLog("要解析的IP类型根据网络情况自动判断"); + } + }); + + addAutoCompleteTextViewButton(HttpDnsActivity.hosts, "域名", "指定要解析的域名", new OnButtonClick() { + @Override + public void onBtnClick(View view) { + AutoCompleteTextView actvOne = (AutoCompleteTextView) view; + host = actvOne.getEditableText().toString(); + sendLog("要解析的域名" + host); + } + }); + + addEditTextEditTextButton("key", "value", "发起请求", new OnButtonClickMoreView() { + @Override + public void onBtnClick(View[] views) { + EditText etOne = (EditText) views[0]; + EditText etTwo = (EditText) views[1]; + + String key = etOne.getEditableText().toString(); + String value = etTwo.getEditableText().toString(); + + HashMap map = new HashMap<>(); + + map.put(key, value); + + sendLog("发起SDNS请求 requestIpType is " + requestIpType.name()); + HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试SDNS"); + sendLog("结果 " + result); + } + }); + + addTwoButton("scale1参数请求", new View.OnClickListener() { + @Override + public void onClick(View v) { + HashMap map = new HashMap<>(); + map.put("scale", "scale1"); + sendLog("发起SDNS请求 requestIpType is " + requestIpType.name() + " scale : scale1"); + HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试1"); + sendLog("结果 " + result); + } + }, "scale2参数请求", new View.OnClickListener() { + @Override + public void onClick(View v) { + HashMap map = new HashMap<>(); + map.put("scale", "scale2"); + sendLog("发起SDNS请求 requestIpType is " + requestIpType.name() + " scale : scale2"); + HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试2"); + sendLog("结果 " + result); + } + }); + + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/WebViewActivity.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/WebViewActivity.java new file mode 100644 index 0000000..3ed8572 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/WebViewActivity.java @@ -0,0 +1,402 @@ +package com.aliyun.ams.httpdns.demo; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.TextView; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +public class WebViewActivity extends Activity { + + private WebView webView; + private static final String targetUrl = "http://www.apple.com"; + + private static final String TAG = MyApp.TAG + "WebView"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_webview); + + initBar(); + initHttpDnsWebView(); + } + + @Override + public boolean + onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) { + webView.goBack();//返回上个页面 + return true; + } + return super.onKeyDown(keyCode, event);//退出Activity + } + + private void initBar() { + findViewById(R.id.bar_img).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + WebViewActivity.this.finish(); + } + }); + + ((TextView) findViewById(R.id.bar_text)).setText("HTTPDNS"); + } + + private void initHttpDnsWebView() { + + webView = (WebView) this.findViewById(R.id.wv_container); + + webView.setWebViewClient(new WebViewClient() { + + @SuppressLint("NewApi") + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String scheme = request.getUrl().getScheme().trim(); + String method = request.getMethod(); + Map headerFields = request.getRequestHeaders(); + String url = request.getUrl().toString(); + Log.e(TAG, "url:" + url); + // 无法拦截body,拦截方案只能正常处理不带body的请求; + if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) + && method.equalsIgnoreCase("get")) { + try { + URLConnection connection = recursiveRequest(url, headerFields, null); + + if (connection == null) { + Log.e(TAG, "connection null"); + return super.shouldInterceptRequest(view, request); + } + + // 注*:对于POST请求的Body数据,WebResourceRequest接口中并没有提供,这里无法处理 + String contentType = connection.getContentType(); + String mime = getMime(contentType); + String charset = getCharset(contentType); + HttpURLConnection httpURLConnection = (HttpURLConnection) connection; + int statusCode = httpURLConnection.getResponseCode(); + String response = httpURLConnection.getResponseMessage(); + Map> headers = httpURLConnection.getHeaderFields(); + Set headerKeySet = headers.keySet(); + Log.e(TAG, "code:" + httpURLConnection.getResponseCode()); + Log.e(TAG, "mime:" + mime + "; charset:" + charset); + + + // 无mime类型的请求不拦截 + if (TextUtils.isEmpty(mime)) { + Log.e(TAG, "no MIME"); + return super.shouldInterceptRequest(view, request); + } else { + // 二进制资源无需编码信息 + if (!TextUtils.isEmpty(charset) || (isBinaryRes(mime))) { + WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream()); + resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response); + Map responseHeader = new HashMap(); + for (String key : headerKeySet) { + // HttpUrlConnection可能包含key为null的报头,指向该http请求状态码 + responseHeader.put(key, httpURLConnection.getHeaderField(key)); + } + resourceResponse.setResponseHeaders(responseHeader); + return resourceResponse; + } else { + Log.e(TAG, "non binary resource for " + mime); + return super.shouldInterceptRequest(view, request); + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return super.shouldInterceptRequest(view, request); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + // API < 21 只能拦截URL参数 + return super.shouldInterceptRequest(view, url); + } + }); + + + webView.loadUrl(targetUrl); + } + + + /** + * 从contentType中获取MIME类型 + * + * @param contentType + * @return + */ + private String getMime(String contentType) { + if (contentType == null) { + return null; + } + return contentType.split(";")[0]; + } + + /** + * 从contentType中获取编码信息 + * + * @param contentType + * @return + */ + private String getCharset(String contentType) { + if (contentType == null) { + return null; + } + + String[] fields = contentType.split(";"); + if (fields.length <= 1) { + return null; + } + + String charset = fields[1]; + if (!charset.contains("=")) { + return null; + } + charset = charset.substring(charset.indexOf("=") + 1); + return charset; + } + + + /** + * 是否是二进制资源,二进制资源可以不需要编码信息 + */ + private boolean isBinaryRes(String mime) { + if (mime.startsWith("image") + || mime.startsWith("audio") + || mime.startsWith("video")) { + return true; + } else { + return false; + } + } + + + public URLConnection recursiveRequest(String path, Map headers, String reffer) { + HttpURLConnection conn; + URL url = null; + try { + url = new URL(path); + // 异步接口获取IP + String ip = MyApp.getInstance().getService().getIpByHostAsync(url.getHost()); + if (ip != null) { + // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置 + Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!"); + String newUrl = path.replaceFirst(url.getHost(), ip); + conn = (HttpURLConnection) new URL(newUrl).openConnection(); + + if (headers != null) { + for (Map.Entry field : headers.entrySet()) { + conn.setRequestProperty(field.getKey(), field.getValue()); + } + } + // 设置HTTP请求头Host域 + conn.setRequestProperty("Host", url.getHost()); + } else { + return null; + } + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + conn.setInstanceFollowRedirects(false); + if (conn instanceof HttpsURLConnection) { + final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn; + WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory( + (HttpsURLConnection)conn); + + // sni场景,创建SSLScocket + httpsURLConnection.setSSLSocketFactory(sslSocketFactory); + // https场景,证书校验 + httpsURLConnection.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + String host = httpsURLConnection.getRequestProperty("Host"); + if (null == host) { + host = httpsURLConnection.getURL().getHost(); + } + return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session); + } + }); + } + int code = conn.getResponseCode();// Network block + if (needRedirect(code)) { + // 原有报头中含有cookie,放弃拦截 + if (containCookie(headers)) { + return null; + } + + String location = conn.getHeaderField("Location"); + if (location == null) { + location = conn.getHeaderField("location"); + } + + if (location != null) { + if (!(location.startsWith("http://") || location + .startsWith("https://"))) { + //某些时候会省略host,只返回后面的path,所以需要补全url + URL originalUrl = new URL(path); + location = originalUrl.getProtocol() + "://" + + originalUrl.getHost() + location; + } + Log.e(TAG, "code: " + code + "; location: " + location + "; path " + path); + return recursiveRequest(location, headers, path); + } else { + // 无法获取location信息,让浏览器获取 + return null; + } + } else { + // redirect finish. + Log.e(TAG, "redirect finish"); + return conn; + } + } catch (MalformedURLException e) { + Log.w(TAG, "recursiveRequest MalformedURLException"); + } catch (IOException e) { + Log.w(TAG, "recursiveRequest IOException"); + } catch (Exception e) { + Log.w(TAG, "unknow exception"); + } + return null; + } + + + private boolean needRedirect(int code) { + return code >= 300 && code < 400; + } + + + /** + * header中是否含有cookie + */ + private boolean containCookie(Map headers) { + for (Map.Entry headerField : headers.entrySet()) { + if (headerField.getKey().contains("Cookie")) { + return true; + } + } + return false; + } + + + static class WebviewTlsSniSocketFactory extends SSLSocketFactory { + private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName(); + HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + private final HttpsURLConnection conn; + + public WebviewTlsSniSocketFactory(HttpsURLConnection conn) { + this.conn = conn; + } + + @Override + public Socket createSocket() throws IOException { + return null; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return null; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return null; + } + + // TLS layer + + @Override + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException { + String peerHost = this.conn.getRequestProperty("Host"); + if (peerHost == null) + peerHost = host; + Log.i(TAG, "customized createSocket. host: " + peerHost); + InetAddress address = plainSocket.getInetAddress(); + if (autoClose) { + // we don't need the plainSocket + plainSocket.close(); + } + // create and connect SSL socket, but don't do hostname/certificate verification yet + SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0); + SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port); + + // enable TLSv1.1/1.2 if available + ssl.setEnabledProtocols(ssl.getSupportedProtocols()); + + // set up SNI before the handshake + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.i(TAG, "Setting SNI hostname"); + sslSocketFactory.setHostname(ssl, peerHost); + } else { + Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection"); + try { + java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class); + setHostnameMethod.invoke(ssl, peerHost); + } catch (Exception e) { + Log.w(TAG, "SNI not useable", e); + } + } + + // verify hostname and certificate + SSLSession session = ssl.getSession(); + + if (!hostnameVerifier.verify(peerHost, session)) + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost); + + Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); + + return ssl; + } + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/base/BaseActivity.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/base/BaseActivity.java new file mode 100644 index 0000000..a821464 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/base/BaseActivity.java @@ -0,0 +1,263 @@ +package com.aliyun.ams.httpdns.demo.base; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.aliyun.ams.httpdns.demo.MyApp; +import com.aliyun.ams.httpdns.demo.R; + +public class BaseActivity extends Activity { + + public static final int MSG_WHAT_LOG = 10000; + + private ScrollView logScrollView; + private TextView logView; + private LinearLayout llContainer; + + private Handler handler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_base); + + handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case MSG_WHAT_LOG: + logView.setText(logView.getText() + "\n" + (String) msg.obj); + handler.post(new Runnable() { + @Override + public void run() { + logScrollView.fullScroll(View.FOCUS_DOWN); + } + }); + break; + } + } + }; + + logScrollView = findViewById(R.id.logScrollView); + logView = findViewById(R.id.tvConsoleText); + llContainer = findViewById(R.id.llContainer); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + handler.removeCallbacksAndMessages(null); + handler = null; + } + + /** + * 发送日志到界面 + * + * @param log + */ + protected void sendLog(String log) { + Log.d(MyApp.TAG, log); + if (handler != null) { + Message msg = handler.obtainMessage(MSG_WHAT_LOG, log); + handler.sendMessage(msg); + } + } + + protected void cleanLog() { + logView.setText(""); + } + + protected void addView(int layoutId, OnViewCreated created) { + FrameLayout container = new FrameLayout(this); + View.inflate(this, layoutId, container); + llContainer.addView(container, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + created.onViewCreated(container); + } + + protected void addOneButton( + final String labelOne, final View.OnClickListener clickListenerOne + ) { + addView(R.layout.item_one_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + btnOne.setOnClickListener(clickListenerOne); + } + }); + } + + protected void addTwoButton( + final String labelOne, final View.OnClickListener clickListenerOne, + final String labelTwo, final View.OnClickListener clickListenerTwo + ) { + addView(R.layout.item_two_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + btnOne.setOnClickListener(clickListenerOne); + + Button btnTwo = view.findViewById(R.id.btnTwo); + btnTwo.setText(labelTwo); + btnTwo.setOnClickListener(clickListenerTwo); + } + }); + } + + protected void addThreeButton( + final String labelOne, final View.OnClickListener clickListenerOne, + final String labelTwo, final View.OnClickListener clickListenerTwo, + final String labelThree, final View.OnClickListener clickListenerThree + ) { + addView(R.layout.item_three_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + btnOne.setOnClickListener(clickListenerOne); + + Button btnTwo = view.findViewById(R.id.btnTwo); + btnTwo.setText(labelTwo); + btnTwo.setOnClickListener(clickListenerTwo); + + Button btnThree = view.findViewById(R.id.btnThree); + btnThree.setText(labelThree); + btnThree.setOnClickListener(clickListenerThree); + } + }); + } + + + protected void addFourButton( + final String labelOne, final View.OnClickListener clickListenerOne, + final String labelTwo, final View.OnClickListener clickListenerTwo, + final String labelThree, final View.OnClickListener clickListenerThree, + final String labelFour, final View.OnClickListener clickListenerFour + ) { + addView(R.layout.item_four_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + btnOne.setOnClickListener(clickListenerOne); + + Button btnTwo = view.findViewById(R.id.btnTwo); + btnTwo.setText(labelTwo); + btnTwo.setOnClickListener(clickListenerTwo); + + Button btnThree = view.findViewById(R.id.btnThree); + btnThree.setText(labelThree); + btnThree.setOnClickListener(clickListenerThree); + + Button btnFour = view.findViewById(R.id.btnFour); + btnFour.setText(labelFour); + btnFour.setOnClickListener(clickListenerFour); + } + }); + } + + protected void addEditTextButton( + final String hint, + final String labelOne, final OnButtonClick clickListenerOne + ) { + addView(R.layout.item_edit_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + final EditText editText = view.findViewById(R.id.etOne); + editText.setHint(hint); + btnOne.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + clickListenerOne.onBtnClick(editText); + } + }); + } + }); + } + + + protected void addEditTextEditTextButton( + final String hintOne, final String hintTwo, + final String labelOne, final OnButtonClickMoreView clickListenerOne + ) { + addView(R.layout.item_edit_edit_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + final EditText editTextOne = view.findViewById(R.id.etOne); + editTextOne.setHint(hintOne); + final EditText editTextTwo = view.findViewById(R.id.etTwo); + editTextTwo.setHint(hintTwo); + btnOne.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + clickListenerOne.onBtnClick(new View[]{editTextOne, editTextTwo}); + } + }); + } + }); + } + + protected void addAutoCompleteTextViewButton( + final String[] strings, final String hint, final String labelOne, final OnButtonClick clickListenerOne + ) { + addView(R.layout.item_autocomplete_button, new OnViewCreated() { + @Override + public void onViewCreated(View view) { + + Button btnOne = view.findViewById(R.id.btnOne); + btnOne.setText(labelOne); + + final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne); + ArrayAdapter adapter = new ArrayAdapter(getApplicationContext(), + android.R.layout.simple_dropdown_item_1line, strings); + actvOne.setAdapter(adapter); + actvOne.setHint(hint); + + btnOne.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + clickListenerOne.onBtnClick(actvOne); + } + }); + } + }); + } + + public interface OnViewCreated { + void onViewCreated(View view); + } + + public interface OnButtonClick { + void onBtnClick(View view); + } + + public interface OnButtonClickMoreView { + void onBtnClick(View[] views); + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/http/HttpUrlConnectionRequest.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/http/HttpUrlConnectionRequest.java new file mode 100644 index 0000000..24563ff --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/http/HttpUrlConnectionRequest.java @@ -0,0 +1,257 @@ +package com.aliyun.ams.httpdns.demo.http; + + +import android.content.Context; +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.util.Log; + +import com.alibaba.sdk.android.httpdns.HTTPDNSResult; +import com.alibaba.sdk.android.httpdns.NetType; +import com.alibaba.sdk.android.httpdns.RequestIpType; +import com.alibaba.sdk.android.httpdns.SyncService; +import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector; +import com.aliyun.ams.httpdns.demo.MyApp; +import com.aliyun.ams.httpdns.demo.NetworkRequest; +import com.aliyun.ams.httpdns.demo.utils.Util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * 使用HttpUrlConnection 实现请求 + */ +public class HttpUrlConnectionRequest implements NetworkRequest { + + public static final String TAG = MyApp.TAG + "HttpUrl"; + + private final Context context; + private boolean async; + private RequestIpType type; + + public HttpUrlConnectionRequest(Context context) { + this.context = context.getApplicationContext(); + } + + @Override + public void updateHttpDnsConfig(boolean async, RequestIpType requestIpType) { + this.async = async; + this.type = requestIpType; + } + + @Override + public String httpGet(String url) throws Exception { + Log.d(TAG, "使用httpUrlConnection 请求" + url + " 异步接口 " + async + " ip类型 " + type.name()); + + HttpURLConnection conn = getConnection(url); + InputStream in = null; + BufferedReader streamReader = null; + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + in = conn.getErrorStream(); + String errStr = null; + if (in != null) { + streamReader = new BufferedReader(new InputStreamReader(in, "UTF-8")); + errStr = readStringFrom(streamReader).toString(); + } + Log.d(TAG, "请求失败 " + conn.getResponseCode() + " err " + errStr); + throw new Exception("Status Code : " + conn.getResponseCode() + " Msg : " + errStr); + } else { + in = conn.getInputStream(); + streamReader = new BufferedReader(new InputStreamReader(in, "UTF-8")); + String responseStr = readStringFrom(streamReader).toString(); + Log.d(TAG, "请求成功 " + responseStr); + return responseStr; + } + } + + private HttpURLConnection getConnection(String url) throws IOException { + final String host = new URL(url).getHost(); + HttpURLConnection conn = null; + HTTPDNSResult result; + /* 切换为新版标准api */ + if (async) { + result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(host, type); + } else { + result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(host, type); + } + Log.d(TAG, "httpdns 解析 " + host + " 结果为 " + result + " ttl is " + Util.getTtl(result)); + + // 这里需要根据实际情况选择使用ipv6地址 还是 ipv4地址, 下面示例的代码优先使用了ipv6地址 + if (result.getIpv6s() != null && result.getIpv6s().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v4) { + String newUrl = url.replace(host, "[" + result.getIpv6s()[0] + "]"); + conn = (HttpURLConnection) new URL(newUrl).openConnection(); + conn.setRequestProperty("Host", host); + Log.d(TAG, "使用ipv6地址 " + newUrl); + } else if (result.getIps() != null && result.getIps().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v6) { + String newUrl = url.replace(host, result.getIps()[0]); + conn = (HttpURLConnection) new URL(newUrl).openConnection(); + conn.setRequestProperty("Host", host); + Log.d(TAG, "使用ipv4地址 " + newUrl); + } + + if (conn == null) { + Log.d(TAG, "httpdns 未返回解析结果,走localdns"); + conn = (HttpURLConnection) new URL(url).openConnection(); + } + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + conn.setInstanceFollowRedirects(false); + if (conn instanceof HttpsURLConnection) { + final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn; + WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory( + (HttpsURLConnection)conn); + + // sni场景,创建SSLSocket + httpsURLConnection.setSSLSocketFactory(sslSocketFactory); + // https场景,证书校验 + httpsURLConnection.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + String host = httpsURLConnection.getRequestProperty("Host"); + if (null == host) { + host = httpsURLConnection.getURL().getHost(); + } + return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session); + } + }); + } + int code = conn.getResponseCode();// Network block + if (needRedirect(code)) { + //临时重定向和永久重定向location的大小写有区分 + String location = conn.getHeaderField("Location"); + if (location == null) { + location = conn.getHeaderField("location"); + } + if (!(location.startsWith("http://") || location + .startsWith("https://"))) { + //某些时候会省略host,只返回后面的path,所以需要补全url + URL originalUrl = new URL(url); + location = originalUrl.getProtocol() + "://" + + originalUrl.getHost() + location; + } + return getConnection(location); + } + return conn; + } + + private boolean needRedirect(int code) { + return code >= 300 && code < 400; + } + + static class WebviewTlsSniSocketFactory extends SSLSocketFactory { + private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName(); + HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + private HttpsURLConnection conn; + + public WebviewTlsSniSocketFactory(HttpsURLConnection conn) { + this.conn = conn; + } + + @Override + public Socket createSocket() throws IOException { + return null; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return null; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return null; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return null; + } + + // TLS layer + + @Override + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException { + String peerHost = this.conn.getRequestProperty("Host"); + if (peerHost == null) + peerHost = host; + Log.i(TAG, "customized createSocket. host: " + peerHost); + InetAddress address = plainSocket.getInetAddress(); + if (autoClose) { + // we don't need the plainSocket + plainSocket.close(); + } + // create and connect SSL socket, but don't do hostname/certificate verification yet + SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0); + SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port); + + // enable TLSv1.1/1.2 if available + ssl.setEnabledProtocols(ssl.getSupportedProtocols()); + + // set up SNI before the handshake + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.i(TAG, "Setting SNI hostname"); + sslSocketFactory.setHostname(ssl, peerHost); + } else { + Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection"); + try { + java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class); + setHostnameMethod.invoke(ssl, peerHost); + } catch (Exception e) { + Log.w(TAG, "SNI not useable", e); + } + } + + // verify hostname and certificate + SSLSession session = ssl.getSession(); + + if (!hostnameVerifier.verify(peerHost, session)) + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost); + + Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); + + return ssl; + } + } + + /** + * stream to string + */ + public static StringBuilder readStringFrom(BufferedReader streamReader) throws IOException { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = streamReader.readLine()) != null) { + sb.append(line); + } + return sb; + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/okhttp/OkHttpRequest.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/okhttp/OkHttpRequest.java new file mode 100644 index 0000000..820e4b7 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/okhttp/OkHttpRequest.java @@ -0,0 +1,97 @@ +package com.aliyun.ams.httpdns.demo.okhttp; + +import android.content.Context; +import android.util.Log; + +import com.alibaba.sdk.android.httpdns.HTTPDNSResult; +import com.alibaba.sdk.android.httpdns.NetType; +import com.alibaba.sdk.android.httpdns.RequestIpType; +import com.alibaba.sdk.android.httpdns.SyncService; +import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector; +import com.aliyun.ams.httpdns.demo.MyApp; +import com.aliyun.ams.httpdns.demo.NetworkRequest; +import com.aliyun.ams.httpdns.demo.utils.Util; + +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import okhttp3.ConnectionPool; +import okhttp3.Dns; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * okhttp实现网络请求 + */ +public class OkHttpRequest implements NetworkRequest { + + public static final String TAG = MyApp.TAG + "Okhttp"; + private final OkHttpClient client; + + private boolean async; + private RequestIpType type; + + public OkHttpRequest(final Context context) { + client = new OkHttpClient.Builder() + // 这里配置连接池,是为了方便测试httpdns能力,正式代码请不要配置 + .connectionPool(new ConnectionPool(0, 10 * 1000, TimeUnit.MICROSECONDS)) + .dns(new Dns() { + @Override + public List lookup(String hostname) throws UnknownHostException { + HTTPDNSResult result; + /* 切换为新版标准api */ + if (async) { + result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(hostname, type); + } else { + result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(hostname, type); + } + Log.d(TAG, "httpdns 解析 " + hostname + " 结果为 " + result + " ttl is " + Util.getTtl(result)); + List inetAddresses = new ArrayList<>(); + // 这里需要根据实际情况选择使用ipv6地址 还是 ipv4地址, 下面示例的代码优先使用了ipv6地址 + Log.d(TAG, "netType is: " + HttpDnsNetworkDetector.getInstance().getNetType(context)); + if (result.getIpv6s() != null && result.getIpv6s().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v4) { + for (int i = 0; i < result.getIpv6s().length; i++) { + inetAddresses.addAll(Arrays.asList(InetAddress.getAllByName(result.getIpv6s()[i]))); + } + Log.d(TAG, "使用ipv6地址" + inetAddresses); + } else if (result.getIps() != null && result.getIps().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v6) { + for (int i = 0; i < result.getIps().length; i++) { + inetAddresses.addAll(Arrays.asList(InetAddress.getAllByName(result.getIps()[i]))); + } + Log.d(TAG, "使用ipv4地址" + inetAddresses); + } + if (inetAddresses.size() == 0) { + Log.d(TAG, "httpdns 未返回IP,走localdns"); + return Dns.SYSTEM.lookup(hostname); + } + return inetAddresses; + } + }) + .build(); + } + + @Override + public void updateHttpDnsConfig(boolean async, RequestIpType requestIpType) { + this.async = async; + this.type = requestIpType; + } + + @Override + public String httpGet(String url) throws Exception { + Log.d(TAG, "使用okhttp 请求" + url + " 异步接口 " + async + " ip类型 " + type.name()); + Response response = client.newCall(new Request.Builder().url(url).build()).execute(); + int code = response.code(); + String body = response.body().string(); + Log.d(TAG, "使用okhttp 请求结果 code " + code + " body " + body); + if (code != HttpURLConnection.HTTP_OK) { + throw new Exception("请求失败 code " + code + " body " + body); + } + return body; + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/SpUtil.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/SpUtil.java new file mode 100644 index 0000000..bf9c21a --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/SpUtil.java @@ -0,0 +1,27 @@ +package com.aliyun.ams.httpdns.demo.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class SpUtil { + + public static void readSp(Context context, String name, OnGetSp onGetSp) { + SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_PRIVATE); + onGetSp.onGetSp(sp); + } + + public static void writeSp(Context context, String name, OnGetSpEditor onGetSpEditor) { + SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + onGetSpEditor.onGetSpEditor(editor); + editor.commit(); + } + + public interface OnGetSp { + void onGetSp(SharedPreferences sp); + } + + public interface OnGetSpEditor { + void onGetSpEditor(SharedPreferences.Editor editor); + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/ThreadUtil.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/ThreadUtil.java new file mode 100644 index 0000000..f5efccf --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/ThreadUtil.java @@ -0,0 +1,127 @@ +package com.aliyun.ams.httpdns.demo.utils; + +import android.util.Log; + +import com.alibaba.sdk.android.httpdns.HTTPDNSResult; +import com.alibaba.sdk.android.httpdns.RequestIpType; +import com.alibaba.sdk.android.httpdns.SyncService; + +import com.aliyun.ams.httpdns.demo.MyApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ThreadUtil { + + public static void multiThreadTest(final String[] validHosts, final int hostCount, + final int threadCount, final int executeTime, + final boolean async, final RequestIpType type) { + int validCount = validHosts.length; + if (validCount > hostCount) { + validCount = hostCount; + } + final int tmpValidCount = validCount; + Log.d(MyApp.TAG, + threadCount + "线程并发,执行" + executeTime + ", 总域名" + hostCount + "个,有效" + + validCount + "个"); + new Thread(new Runnable() { + @Override + public void run() { + final ArrayList hosts = new ArrayList<>(hostCount); + for (int i = 0; i < hostCount - tmpValidCount; i++) { + hosts.add("test" + i + ".aliyun.com"); + } + hosts.addAll(Arrays.asList(validHosts).subList(0, tmpValidCount)); + + final CountDownLatch testLatch = new CountDownLatch(threadCount); + ExecutorService service = Executors.newFixedThreadPool(threadCount); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + service.execute(new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Random random = new Random(Thread.currentThread().getId()); + int allRequestCount = 0; + int slowRequestCount = 0; + int emptyResponseCount = 0; + long maxSlowRequestTime = 0; + long subSlowRequestTime = 0; + int taobaoCount = 0; + int taobaoSuccessCount = 0; + long firstTaobaoTime = 0; + long firstTaobaoSuccessTime = 0; + long begin = System.currentTimeMillis(); + while (System.currentTimeMillis() - begin < executeTime) { + String host = hosts.get(random.nextInt(hostCount)); + long startRequestTime = System.currentTimeMillis(); + HTTPDNSResult ips = null; + if (async) { + ips = MyApp.getInstance().getService().getIpsByHostAsync(host, + type); + } else { + ips = + ((SyncService)MyApp.getInstance().getService()).getByHost( + host, type); + } + long endRequestTime = System.currentTimeMillis(); + if (host.equals("www.taobao.com")) { + if (taobaoCount == 0) { + firstTaobaoTime = System.currentTimeMillis(); + } + taobaoCount++; + if (ips.getIps() != null && ips.getIps().length > 0) { + if (taobaoSuccessCount == 0) { + firstTaobaoSuccessTime = System.currentTimeMillis(); + } + taobaoSuccessCount++; + } + } + if (endRequestTime - startRequestTime > 100) { + slowRequestCount++; + subSlowRequestTime += endRequestTime - startRequestTime; + if (maxSlowRequestTime < endRequestTime - startRequestTime) { + maxSlowRequestTime = endRequestTime - startRequestTime; + } + } + if (ips == null || ips.getIps() == null + || ips.getIps().length == 0) { + emptyResponseCount++; + } + allRequestCount++; + } + + String msg = Thread.currentThread().getId() + " allRequestCount: " + + allRequestCount + ", slowRequestCount: " + slowRequestCount + + ", emptyResponseCount: " + emptyResponseCount + + ", maxSlowRequestTime : " + maxSlowRequestTime + + ", avgSlowRequestTime: " + (slowRequestCount == 0 ? 0 + : subSlowRequestTime / slowRequestCount) + + ", taoRequestCount: " + taobaoCount + ", " + + "taoSuccessRequestCount: " + + taobaoSuccessCount + ", firstTaoRequestTime: " + (firstTaobaoTime + - begin) + ", firstSuccessTaoRequestTime: " + ( + firstTaobaoSuccessTime - begin); + Log.w(MyApp.TAG, "asyncMulti " + msg); + testLatch.countDown(); + } + }); + } + + try { + testLatch.await(); + } catch (InterruptedException e) { + } + } + }).start(); + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/Util.java b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/Util.java new file mode 100644 index 0000000..04ea839 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/java/com/aliyun/ams/httpdns/demo/utils/Util.java @@ -0,0 +1,24 @@ +package com.aliyun.ams.httpdns.demo.utils; + +import com.alibaba.sdk.android.httpdns.HTTPDNSResult; + +import java.lang.reflect.Field; + +public class Util { + /** + * 获取ttl, + * 此方法是用于测试自定义ttl是否生效 + */ + public static int getTtl(HTTPDNSResult result) { + try { + Field ttlField = HTTPDNSResult.class.getDeclaredField("ttl"); + ttlField.setAccessible(true); + return ttlField.getInt(result); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return -1; + } +} diff --git a/EdgeHttpDNS/sdk/android/app/src/main/res/drawable/aliyun_bg.9.png b/EdgeHttpDNS/sdk/android/app/src/main/res/drawable/aliyun_bg.9.png new file mode 100644 index 0000000..5be6ad4 Binary files /dev/null and b/EdgeHttpDNS/sdk/android/app/src/main/res/drawable/aliyun_bg.9.png differ diff --git a/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_base.xml b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_base.xml new file mode 100644 index 0000000..a37750e --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_base.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_webview.xml b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_webview.xml new file mode 100644 index 0000000..872f292 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/activity_webview.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EdgeHttpDNS/sdk/android/app/src/main/res/layout/item_autocomplete_button.xml b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/item_autocomplete_button.xml new file mode 100644 index 0000000..2bd73c6 --- /dev/null +++ b/EdgeHttpDNS/sdk/android/app/src/main/res/layout/item_autocomplete_button.xml @@ -0,0 +1,17 @@ + + + + + +