管理端全部功能跑通
This commit is contained in:
5
EdgeHttpDNS/.gitignore
vendored
Normal file
5
EdgeHttpDNS/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*.zip
|
||||
edge-dns
|
||||
configs/
|
||||
logs/
|
||||
data/
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 0. 文档信息
|
||||
- 目标文件:`EdgeHttpDNS/HTTPDNS后端开发计划.md`
|
||||
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeNode + SDK对接接口`
|
||||
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeHttpDNS + SDK对接接口`
|
||||
- 交付策略:一次性全量交付(非分阶段)
|
||||
- 核心约束:
|
||||
- 仅新协议
|
||||
@@ -12,6 +12,7 @@
|
||||
- 独立 `edgeHTTPDNS*` 数据表
|
||||
- 新增独立节点角色 `NodeRoleHTTPDNS`
|
||||
- 访问日志 MySQL + ClickHouse 双写(查询优先 ClickHouse)
|
||||
- 不复用智能DNS(EdgeDNS/NS)模块的 DAO/Service/Store,HTTPDNS 独立实现(可参考并复制所需能力)
|
||||
|
||||
## 1. 目标与成功标准
|
||||
1. 将 HTTPDNS 从当前 Admin 侧 mock/store 方案落地为真实后端能力。
|
||||
@@ -26,13 +27,13 @@
|
||||
- 访问日志与运行日志可查询、可筛选、可分页
|
||||
- 节点配置与状态可下发和回传
|
||||
- 主备集群服务域名可在应用设置中配置并生效
|
||||
- EdgeNode 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
|
||||
- EdgeHttpDNS 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
|
||||
|
||||
## 2. 架构与边界
|
||||
### 2.1 服务边界
|
||||
1. EdgeAdmin:仅负责页面动作与 RPC 编排,不存业务状态。
|
||||
2. EdgeAPI:负责数据存储、策略匹配、接口服务、日志汇聚。
|
||||
3. EdgeNode(HTTPDNS节点):负责执行解析、接收策略任务、上报运行日志/访问日志,并执行 SNI/Host 解耦路由、WAF 动态验签、隐匿 SNI 转发。
|
||||
3. EdgeHttpDNS(HTTPDNS节点):负责执行解析、上报运行日志/访问日志、接收任务。
|
||||
4. SDK:手动配置应用关联主备服务域名,调用 `/resolve` 获取结果。
|
||||
|
||||
### 2.2 不做项
|
||||
@@ -85,13 +86,13 @@
|
||||
- 日志:访问日志分页查询、运行日志分页查询
|
||||
- 测试:在线解析测试调用(入参包含 appId、clusterId、domain、qtype、clientIp)
|
||||
|
||||
### 3.3 节点日志上报 RPC(EdgeNode -> EdgeAPI)
|
||||
### 3.3 节点日志上报 RPC(EdgeHttpDNS -> EdgeAPI)
|
||||
1. `CreateHTTPDNSAccessLogs`(批量)
|
||||
2. `CreateHTTPDNSRuntimeLogs`(批量)
|
||||
3. 幂等键:`requestId + nodeId`
|
||||
4. 支持高吞吐批量提交和失败重试
|
||||
|
||||
### 3.4 节点路由与 WAF 策略下发契约(EdgeAPI -> EdgeNode)
|
||||
### 3.4 节点路由与 WAF 策略下发契约(EdgeAPI -> EdgeHttpDNS)
|
||||
1. 下发内容最小集合:
|
||||
- `appId/domain/serviceDomain`
|
||||
- `sniMode`(固定为隐匿 SNI)
|
||||
@@ -168,7 +169,7 @@
|
||||
- 规则变更
|
||||
- 证书变更
|
||||
- 路由与 WAF 策略变更
|
||||
3. EdgeNode 增加 HTTPDNS 子服务:
|
||||
3. EdgeHttpDNS 增加 HTTPDNS 子服务:
|
||||
- 接收配置快照
|
||||
- 执行解析
|
||||
- 上报运行/访问日志
|
||||
@@ -215,7 +216,7 @@
|
||||
2. `/resolve` 各错误码分支
|
||||
3. 节点日志上报双写(MySQL+CH)
|
||||
4. CH 不可用时 MySQL 回退查询
|
||||
5. EdgeAPI 策略下发 -> EdgeNode 路由/WAF 执行 -> 日志落库全链路
|
||||
5. EdgeAPI 策略下发 -> EdgeHttpDNS 路由/WAF 执行 -> 日志落库全链路
|
||||
|
||||
### 9.3 回归测试
|
||||
1. 智能DNS功能不受影响
|
||||
@@ -228,14 +229,14 @@
|
||||
3. 自定义解析按线路返回预期 IP
|
||||
4. 访问日志筛选与概要展示正确
|
||||
5. 运行日志级别/字段与智能DNS一致
|
||||
6. EdgeNode 可在 SNI 与 Host 解耦场景下正确路由到目标应用
|
||||
6. EdgeHttpDNS 可在 SNI 与 Host 解耦场景下正确路由到目标应用
|
||||
7. 开启 WAF 动态验签后,合法请求通过、非法签名请求被拒绝且有审计日志
|
||||
|
||||
## 10. 发布与回滚
|
||||
1. 发布顺序:
|
||||
- DB migration
|
||||
- EdgeAPI(DAO+RPC+resolve)
|
||||
- EdgeNode(角色+上报)
|
||||
- EdgeHttpDNS(角色+上报)
|
||||
- EdgeAdmin(RPC切换)
|
||||
2. 开关控制:
|
||||
- `httpdns.resolve.enabled`
|
||||
|
||||
92
EdgeHttpDNS/SNI隐匿开发计划.md
Normal file
92
EdgeHttpDNS/SNI隐匿开发计划.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# SNI隐匿开发计划(HTTPDNS专项)
|
||||
|
||||
## 0. 目标
|
||||
在明文网络层(DNS 与 TLS ClientHello)中不出现真实业务域名;真实业务域名仅出现在加密后的 HTTP 层(Host / :authority)中传输。
|
||||
|
||||
## 1. 固定原则
|
||||
1. 只做 SNI 隐匿能力,不引入其他无关能力描述。
|
||||
2. 客户端不走系统 DNS 解析业务域名。
|
||||
3. TLS 握手阶段不发送真实业务域名 SNI。
|
||||
4. CDN/WAF 节点必须支持“空SNI接入 + Host路由”。
|
||||
5. 真实域名只在 HTTPS 加密通道内携带。
|
||||
|
||||
## 2. 端到端链路(目标形态)
|
||||
1. App 调用 SDK,请求解析业务域名。
|
||||
2. SDK 从 HTTPDNS 获取业务域名对应的“接入 IP 列表”(不是业务域名 DNS)。
|
||||
3. SDK 直连接入 IP 发起 TLS:
|
||||
- SNI 置空(或不发送)
|
||||
- 不出现真实业务域名
|
||||
4. TLS 建立后发起 HTTPS 请求:
|
||||
- Host / :authority = 真实业务域名
|
||||
5. CDN/WAF 节点在解密后读取 Host,将流量路由到对应业务源站。
|
||||
|
||||
## 3. SDK 改造要求
|
||||
### 3.1 连接行为
|
||||
1. 提供“按 IP 直连”的请求通道。
|
||||
2. TLS 握手时固定空 SNI,不允许带真实域名。
|
||||
3. HTTP 层强制写入真实 Host / :authority。
|
||||
|
||||
### 3.2 证书校验
|
||||
1. 仍必须做证书链校验(不允许关闭 TLS 安全校验)。
|
||||
2. 证书校验目标为接入层证书(CDN/WAF 对外证书),而非业务源站证书。
|
||||
|
||||
### 3.3 多IP与容错
|
||||
1. HTTPDNS 返回多个接入 IP 时,SDK 按顺序/策略重试。
|
||||
2. 连接失败可切换下一 IP。
|
||||
3. 缓存与过期策略保持稳定,避免频繁抖动。
|
||||
|
||||
### 3.4 多端一致性
|
||||
1. Android/iOS/Flutter 需保证一致行为:
|
||||
- 空 SNI
|
||||
- Host 注入
|
||||
- 失败重试策略
|
||||
2. 文档与示例代码同步更新。
|
||||
|
||||
## 4. CDN/WAF 节点改造要求
|
||||
### 4.1 TLS 接入
|
||||
1. 支持无 SNI ClientHello 的 TLS 握手。
|
||||
2. 为接入域名部署有效证书(覆盖客户端连接目标)。
|
||||
|
||||
### 4.2 路由逻辑
|
||||
1. 以 Host / :authority 作为业务路由主键。
|
||||
2. 路由匹配前做标准化:
|
||||
- 小写化
|
||||
- 去端口
|
||||
3. Host 未命中时返回明确错误(4xx),禁止兜底到默认站点。
|
||||
|
||||
### 4.3 回源行为
|
||||
1. 节点到源站可继续使用 HTTPS 回源。
|
||||
2. 回源主机名与证书校验按现有网关策略执行。
|
||||
|
||||
### 4.4 可观测性
|
||||
1. 增加日志字段:
|
||||
- `tlsSniPresent`(是否携带 SNI)
|
||||
- `host`
|
||||
- `routeResult`
|
||||
2. 可按“空SNI请求占比、Host路由命中率”监控。
|
||||
|
||||
## 5. 控制面(管理端)要求
|
||||
1. 页面仅展示“已启用 SNI 隐匿(空SNI)”,不提供策略切换。
|
||||
2. 集群侧需可检查“节点是否支持空SNI接入”。
|
||||
3. 发布配置时支持灰度与回滚。
|
||||
|
||||
## 6. 验收标准
|
||||
1. 抓包验证:
|
||||
- DNS 明文流量中不出现真实业务域名
|
||||
- TLS ClientHello 中不出现真实业务域名 SNI
|
||||
2. 请求验证:
|
||||
- HTTPS 请求 Host 为真实业务域名
|
||||
- CDN/WAF 按 Host 正确路由
|
||||
3. 稳定性验证:
|
||||
- 多 IP 切换成功
|
||||
- 节点故障时请求可恢复
|
||||
|
||||
## 7. 上线顺序
|
||||
1. 先升级 CDN/WAF 节点能力(空SNI接入 + Host路由)。
|
||||
2. 再升级 SDK(空SNI + Host注入)。
|
||||
3. 最后按应用灰度开启,观察指标后全量。
|
||||
|
||||
## 8. 风险与约束
|
||||
1. 若 CDN/WAF 不支持空 SNI,链路会在握手阶段失败。
|
||||
2. 若 Host 路由不严格,可能出现串站风险。
|
||||
3. 若客户端错误关闭证书校验,会引入严重安全风险。
|
||||
BIN
EdgeHttpDNS/bin/edge-httpdns
Normal file
BIN
EdgeHttpDNS/bin/edge-httpdns
Normal file
Binary file not shown.
6
EdgeHttpDNS/build/build-all.sh
Normal file
6
EdgeHttpDNS/build/build-all.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
./build.sh linux amd64
|
||||
# ./build.sh linux arm64
|
||||
# ./build.sh darwin amd64
|
||||
# ./build.sh darwin arm64
|
||||
150
EdgeHttpDNS/build/build.sh
Normal file
150
EdgeHttpDNS/build/build.sh
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
function build() {
|
||||
ROOT=$(dirname "$0")
|
||||
NAME="edge-httpdns"
|
||||
VERSION=$(lookup-version "$ROOT"/../internal/const/const.go)
|
||||
DIST=$ROOT/"../dist/${NAME}"
|
||||
OS=${1}
|
||||
ARCH=${2}
|
||||
|
||||
if [ -z "$OS" ]; then
|
||||
echo "usage: build.sh OS ARCH"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$ARCH" ]; then
|
||||
echo "usage: build.sh OS ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ZIP_PATH=$(which zip)
|
||||
if [ -z "$ZIP_PATH" ]; then
|
||||
echo "we need 'zip' command to compress files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "building v${VERSION}/${OS}/${ARCH} ..."
|
||||
ZIP="${NAME}-${OS}-${ARCH}-v${VERSION}.zip"
|
||||
|
||||
rm -rf "$DIST"
|
||||
mkdir -p "$DIST"/bin
|
||||
mkdir -p "$DIST"/configs
|
||||
mkdir -p "$DIST"/logs
|
||||
mkdir -p "$DIST"/data
|
||||
|
||||
cp "$ROOT"/configs/api_httpdns.template.yaml "$DIST"/configs
|
||||
copy_fluent_bit_assets "$ROOT" "$DIST" "$OS" "$ARCH" || exit 1
|
||||
|
||||
env GOOS="${OS}" GOARCH="${ARCH}" CGO_ENABLED=1 \
|
||||
go build -trimpath -o "$DIST"/bin/${NAME} -ldflags="-s -w" "$ROOT"/../cmd/edge-httpdns/main.go
|
||||
|
||||
if [ ! -f "$DIST"/bin/${NAME} ]; then
|
||||
echo "build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# delete hidden files
|
||||
find "$DIST" -name ".DS_Store" -delete
|
||||
find "$DIST" -name ".gitignore" -delete
|
||||
|
||||
echo "zip files ..."
|
||||
cd "${DIST}/../" || exit 1
|
||||
if [ -f "${ZIP}" ]; then
|
||||
rm -f "${ZIP}"
|
||||
fi
|
||||
zip -r -X -q "${ZIP}" ${NAME}/
|
||||
rm -rf "${NAME}"
|
||||
cd - || exit 1
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
function copy_fluent_bit_assets() {
|
||||
ROOT=$1
|
||||
DIST=$2
|
||||
OS=$3
|
||||
ARCH=$4
|
||||
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
|
||||
FLUENT_DIST="$DIST/deploy/fluent-bit"
|
||||
|
||||
if [ ! -d "$FLUENT_ROOT" ]; then
|
||||
echo "[error] fluent-bit source directory not found: $FLUENT_ROOT"
|
||||
return 1
|
||||
fi
|
||||
verify_fluent_bit_package_matrix "$FLUENT_ROOT" "$ARCH" || return 1
|
||||
|
||||
rm -rf "$FLUENT_DIST"
|
||||
mkdir -p "$FLUENT_DIST"
|
||||
|
||||
for file in fluent-bit.conf fluent-bit-dns.conf fluent-bit-https.conf fluent-bit-dns-https.conf fluent-bit-windows.conf fluent-bit-windows-https.conf parsers.conf clickhouse-upstream.conf clickhouse-upstream-windows.conf README.md; do
|
||||
if [ -f "$FLUENT_ROOT/$file" ]; then
|
||||
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$OS" = "linux" ]; then
|
||||
PACKAGE_SRC="$FLUENT_ROOT/packages/linux-$ARCH"
|
||||
PACKAGE_DST="$FLUENT_DIST/packages/linux-$ARCH"
|
||||
if [ -d "$PACKAGE_SRC" ]; then
|
||||
mkdir -p "$PACKAGE_DST"
|
||||
cp -R "$PACKAGE_SRC/." "$PACKAGE_DST/"
|
||||
else
|
||||
echo "[error] fluent-bit package directory not found: $PACKAGE_SRC"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$FLUENT_DIST/.gitignore"
|
||||
rm -f "$FLUENT_DIST"/logs.db*
|
||||
rm -rf "$FLUENT_DIST/storage"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function verify_fluent_bit_package_matrix() {
|
||||
FLUENT_ROOT=$1
|
||||
ARCH=$2
|
||||
REQUIRED_FILES=()
|
||||
if [ "$ARCH" = "amd64" ]; then
|
||||
REQUIRED_FILES=(
|
||||
"packages/linux-amd64/fluent-bit_4.2.2_amd64.deb"
|
||||
"packages/linux-amd64/fluent-bit-4.2.2-1.x86_64.rpm"
|
||||
)
|
||||
elif [ "$ARCH" = "arm64" ]; then
|
||||
REQUIRED_FILES=(
|
||||
"packages/linux-arm64/fluent-bit_4.2.2_arm64.deb"
|
||||
"packages/linux-arm64/fluent-bit-4.2.2-1.aarch64.rpm"
|
||||
)
|
||||
else
|
||||
echo "[error] unsupported arch for fluent-bit package validation: $ARCH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
MISSING=0
|
||||
for FILE in "${REQUIRED_FILES[@]}"; do
|
||||
if [ ! -f "$FLUENT_ROOT/$FILE" ]; then
|
||||
echo "[error] fluent-bit matrix package missing: $FLUENT_ROOT/$FILE"
|
||||
MISSING=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$MISSING" -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
function lookup-version() {
|
||||
FILE=$1
|
||||
VERSION_DATA=$(cat "$FILE")
|
||||
re="Version[ ]+=[ ]+\"([0-9.]+)\""
|
||||
if [[ $VERSION_DATA =~ $re ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "could not match version"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build "$1" "$2"
|
||||
43
EdgeHttpDNS/cmd/edge-httpdns/main.go
Normal file
43
EdgeHttpDNS/cmd/edge-httpdns/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/apps"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/configs"
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/nodes"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := apps.NewAppCmd().
|
||||
Version(teaconst.Version).
|
||||
Product(teaconst.ProductName).
|
||||
Usage(teaconst.ProcessName + " [-v|start|stop|restart|status|service|daemon]")
|
||||
|
||||
app.On("start:before", func() {
|
||||
_, err := configs.LoadAPIConfig()
|
||||
if err != nil {
|
||||
fmt.Println("[ERROR]start failed: load config from '" + configs.ConfigFileName + "' failed: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
app.On("daemon", func() {
|
||||
nodes.NewHTTPDNSNode().Daemon()
|
||||
})
|
||||
|
||||
app.On("service", func() {
|
||||
err := nodes.NewHTTPDNSNode().InstallSystemService()
|
||||
if err != nil {
|
||||
fmt.Println("[ERROR]install failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("done")
|
||||
})
|
||||
|
||||
app.Run(func() {
|
||||
nodes.NewHTTPDNSNode().Run()
|
||||
})
|
||||
}
|
||||
BIN
EdgeHttpDNS/edge-httpdns.exe
Normal file
BIN
EdgeHttpDNS/edge-httpdns.exe
Normal file
Binary file not shown.
27
EdgeHttpDNS/go.mod
Normal file
27
EdgeHttpDNS/go.mod
Normal file
@@ -0,0 +1,27 @@
|
||||
module github.com/TeaOSLab/EdgeHttpDNS
|
||||
|
||||
go 1.25
|
||||
|
||||
replace github.com/TeaOSLab/EdgeCommon => ../EdgeCommon
|
||||
|
||||
require (
|
||||
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
|
||||
github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6
|
||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
|
||||
google.golang.org/grpc v1.78.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/oschwald/geoip2-golang v1.13.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
67
EdgeHttpDNS/go.sum
Normal file
67
EdgeHttpDNS/go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6 h1:dS3pTxrLlDQxdoxSUcHkHnr3LHpsBIXv8v2/xw65RN8=
|
||||
github.com/iwind/TeaGo v0.0.0-20240411075713-6c1fc9aca7b6/go.mod h1:SfqVbWyIPdVflyA6lMgicZzsoGS8pyeLiTRe8/CIpGI=
|
||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
|
||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
149
EdgeHttpDNS/internal/apps/app_cmd.go
Normal file
149
EdgeHttpDNS/internal/apps/app_cmd.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/gosock/pkg/gosock"
|
||||
)
|
||||
|
||||
type AppCmd struct {
|
||||
product string
|
||||
version string
|
||||
usage string
|
||||
directives map[string]func()
|
||||
sock *gosock.Sock
|
||||
}
|
||||
|
||||
func NewAppCmd() *AppCmd {
|
||||
return &AppCmd{
|
||||
directives: map[string]func(){},
|
||||
sock: gosock.NewTmpSock(teaconst.ProcessName),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppCmd) Product(product string) *AppCmd {
|
||||
a.product = product
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AppCmd) Version(version string) *AppCmd {
|
||||
a.version = version
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AppCmd) Usage(usage string) *AppCmd {
|
||||
a.usage = usage
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AppCmd) On(arg string, callback func()) {
|
||||
a.directives[arg] = callback
|
||||
}
|
||||
|
||||
func (a *AppCmd) Run(main func()) {
|
||||
args := os.Args[1:]
|
||||
if len(args) == 0 {
|
||||
main()
|
||||
return
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "-v", "version", "-version", "--version":
|
||||
fmt.Println(a.product+" v"+a.version, "(build:", runtimeString()+")")
|
||||
return
|
||||
case "help", "-h", "--help":
|
||||
fmt.Println(a.product + " v" + a.version)
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" " + a.usage)
|
||||
return
|
||||
case "start":
|
||||
a.runDirective("start:before")
|
||||
a.runStart()
|
||||
return
|
||||
case "stop":
|
||||
a.runStop()
|
||||
return
|
||||
case "restart":
|
||||
a.runStop()
|
||||
time.Sleep(1 * time.Second)
|
||||
a.runDirective("start:before")
|
||||
a.runStart()
|
||||
return
|
||||
case "status":
|
||||
a.runStatus()
|
||||
return
|
||||
default:
|
||||
if callback, ok := a.directives[args[0]]; ok {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
fmt.Println("unknown command '" + args[0] + "'")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppCmd) runStart() {
|
||||
pid := a.getPID()
|
||||
if pid > 0 {
|
||||
fmt.Println(a.product+" already started, pid:", pid)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0])
|
||||
cmd.Env = append(os.Environ(), "EdgeBackground=on")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Println(a.product+" start failed:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(a.product+" started, pid:", cmd.Process.Pid)
|
||||
}
|
||||
|
||||
func (a *AppCmd) runStop() {
|
||||
pid := a.getPID()
|
||||
if pid == 0 {
|
||||
fmt.Println(a.product + " not started")
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = a.sock.Send(&gosock.Command{Code: "stop"})
|
||||
fmt.Println(a.product+" stopped, pid:", pid)
|
||||
}
|
||||
|
||||
func (a *AppCmd) runStatus() {
|
||||
pid := a.getPID()
|
||||
if pid == 0 {
|
||||
fmt.Println(a.product + " not started")
|
||||
return
|
||||
}
|
||||
fmt.Println(a.product+" is running, pid:", pid)
|
||||
}
|
||||
|
||||
func (a *AppCmd) runDirective(name string) {
|
||||
if callback, ok := a.directives[name]; ok && callback != nil {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppCmd) getPID() int {
|
||||
if !a.sock.IsListening() {
|
||||
return 0
|
||||
}
|
||||
|
||||
reply, err := a.sock.Send(&gosock.Command{Code: "pid"})
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return maps.NewMap(reply.Params).GetInt("pid")
|
||||
}
|
||||
|
||||
func runtimeString() string {
|
||||
return runtime.GOOS + "/" + runtime.GOARCH
|
||||
}
|
||||
23
EdgeHttpDNS/internal/const/const.go
Normal file
23
EdgeHttpDNS/internal/const/const.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8"
|
||||
|
||||
ProductName = "Edge HTTPDNS"
|
||||
ProcessName = "edge-httpdns"
|
||||
ProductNameZH = "Edge HTTPDNS"
|
||||
|
||||
Role = "httpdns"
|
||||
|
||||
EncryptKey = "8f983f4d69b83aaa0d74b21a212f6967"
|
||||
EncryptMethod = "aes-256-cfb"
|
||||
|
||||
SystemdServiceName = "edge-httpdns"
|
||||
|
||||
// HTTPDNS node tasks from API.
|
||||
TaskTypeHTTPDNSConfigChanged = "httpdnsConfigChanged"
|
||||
TaskTypeHTTPDNSAppChanged = "httpdnsAppChanged"
|
||||
TaskTypeHTTPDNSDomainChanged = "httpdnsDomainChanged"
|
||||
TaskTypeHTTPDNSRuleChanged = "httpdnsRuleChanged"
|
||||
TaskTypeHTTPDNSTLSChanged = "httpdnsTLSChanged"
|
||||
)
|
||||
7
EdgeHttpDNS/internal/encrypt/method.go
Normal file
7
EdgeHttpDNS/internal/encrypt/method.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package encrypt
|
||||
|
||||
type MethodInterface interface {
|
||||
Init(key []byte, iv []byte) error
|
||||
Encrypt(src []byte) (dst []byte, err error)
|
||||
Decrypt(dst []byte) (src []byte, err error)
|
||||
}
|
||||
64
EdgeHttpDNS/internal/encrypt/method_aes_256_cfb.go
Normal file
64
EdgeHttpDNS/internal/encrypt/method_aes_256_cfb.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
)
|
||||
|
||||
type AES256CFBMethod struct {
|
||||
block cipher.Block
|
||||
iv []byte
|
||||
}
|
||||
|
||||
func (m *AES256CFBMethod) Init(key, iv []byte) error {
|
||||
keyLen := len(key)
|
||||
if keyLen > 32 {
|
||||
key = key[:32]
|
||||
} else if keyLen < 32 {
|
||||
key = append(key, bytes.Repeat([]byte{' '}, 32-keyLen)...)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.block = block
|
||||
|
||||
ivLen := len(iv)
|
||||
if ivLen > aes.BlockSize {
|
||||
iv = iv[:aes.BlockSize]
|
||||
} else if ivLen < aes.BlockSize {
|
||||
iv = append(iv, bytes.Repeat([]byte{' '}, aes.BlockSize-ivLen)...)
|
||||
}
|
||||
m.iv = iv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AES256CFBMethod) Encrypt(src []byte) (dst []byte, err error) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = RecoverMethodPanic(recover())
|
||||
}()
|
||||
|
||||
dst = make([]byte, len(src))
|
||||
cipher.NewCFBEncrypter(m.block, m.iv).XORKeyStream(dst, src)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *AES256CFBMethod) Decrypt(dst []byte) (src []byte, err error) {
|
||||
if len(dst) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = RecoverMethodPanic(recover())
|
||||
}()
|
||||
|
||||
src = make([]byte, len(dst))
|
||||
cipher.NewCFBDecrypter(m.block, m.iv).XORKeyStream(src, dst)
|
||||
return
|
||||
}
|
||||
15
EdgeHttpDNS/internal/encrypt/method_raw.go
Normal file
15
EdgeHttpDNS/internal/encrypt/method_raw.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package encrypt
|
||||
|
||||
type RawMethod struct{}
|
||||
|
||||
func (m *RawMethod) Init(key []byte, iv []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RawMethod) Encrypt(src []byte) (dst []byte, err error) {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func (m *RawMethod) Decrypt(dst []byte) (src []byte, err error) {
|
||||
return dst, nil
|
||||
}
|
||||
40
EdgeHttpDNS/internal/encrypt/method_utils.go
Normal file
40
EdgeHttpDNS/internal/encrypt/method_utils.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var methods = map[string]reflect.Type{
|
||||
"raw": reflect.TypeOf(new(RawMethod)).Elem(),
|
||||
"aes-256-cfb": reflect.TypeOf(new(AES256CFBMethod)).Elem(),
|
||||
}
|
||||
|
||||
func NewMethodInstance(method string, key string, iv string) (MethodInterface, error) {
|
||||
valueType, ok := methods[method]
|
||||
if !ok {
|
||||
return nil, errors.New("method '" + method + "' not found")
|
||||
}
|
||||
|
||||
instance, ok := reflect.New(valueType).Interface().(MethodInterface)
|
||||
if !ok {
|
||||
return nil, errors.New("method '" + method + "' must implement MethodInterface")
|
||||
}
|
||||
|
||||
err := instance.Init([]byte(key), []byte(iv))
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func RecoverMethodPanic(err interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s, ok := err.(string); ok {
|
||||
return errors.New(s)
|
||||
}
|
||||
if e, ok := err.(error); ok {
|
||||
return e
|
||||
}
|
||||
return errors.New("unknown error")
|
||||
}
|
||||
157
EdgeHttpDNS/internal/nodes/httpdns_node.go
Normal file
157
EdgeHttpDNS/internal/nodes/httpdns_node.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/utils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/gosock/pkg/gosock"
|
||||
)
|
||||
|
||||
type HTTPDNSNode struct {
|
||||
sock *gosock.Sock
|
||||
|
||||
quitOnce sync.Once
|
||||
quitCh chan struct{}
|
||||
}
|
||||
|
||||
func NewHTTPDNSNode() *HTTPDNSNode {
|
||||
return &HTTPDNSNode{
|
||||
sock: gosock.NewTmpSock(teaconst.ProcessName),
|
||||
quitCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) Run() {
|
||||
err := n.listenSock()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE]" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
go n.start()
|
||||
select {}
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) Daemon() {
|
||||
path := os.TempDir() + "/" + teaconst.ProcessName + ".sock"
|
||||
for {
|
||||
conn, err := net.DialTimeout("unix", path, 1*time.Second)
|
||||
if err != nil {
|
||||
exe, exeErr := os.Executable()
|
||||
if exeErr != nil {
|
||||
log.Println("[DAEMON]", exeErr)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe)
|
||||
cmd.Env = append(os.Environ(), "EdgeBackground=on")
|
||||
if runtime.GOOS != "windows" {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
startErr := cmd.Start()
|
||||
if startErr != nil {
|
||||
log.Println("[DAEMON]", startErr)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) InstallSystemService() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager := utils.NewServiceManager(teaconst.SystemdServiceName, teaconst.ProductName)
|
||||
return manager.Install(exe, []string{})
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) listenSock() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.sock.IsListening() {
|
||||
reply, err := n.sock.Send(&gosock.Command{Code: "pid"})
|
||||
if err == nil {
|
||||
return errors.New("the process is already running, pid: " + maps.NewMap(reply.Params).GetString("pid"))
|
||||
}
|
||||
return errors.New("the process is already running")
|
||||
}
|
||||
|
||||
go func() {
|
||||
n.sock.OnCommand(func(cmd *gosock.Command) {
|
||||
switch cmd.Code {
|
||||
case "pid":
|
||||
_ = cmd.Reply(&gosock.Command{
|
||||
Code: "pid",
|
||||
Params: map[string]interface{}{
|
||||
"pid": os.Getpid(),
|
||||
},
|
||||
})
|
||||
case "info":
|
||||
exePath, _ := os.Executable()
|
||||
_ = cmd.Reply(&gosock.Command{
|
||||
Code: "info",
|
||||
Params: map[string]interface{}{
|
||||
"pid": os.Getpid(),
|
||||
"version": teaconst.Version,
|
||||
"path": exePath,
|
||||
},
|
||||
})
|
||||
case "stop":
|
||||
_ = cmd.ReplyOk()
|
||||
n.stop()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
|
||||
err := n.sock.Listen()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][sock]", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) start() {
|
||||
log.Println("[HTTPDNS_NODE]started")
|
||||
|
||||
snapshotManager := NewSnapshotManager(n.quitCh)
|
||||
statusManager := NewStatusManager(n.quitCh)
|
||||
taskManager := NewTaskManager(n.quitCh, snapshotManager)
|
||||
resolveServer := NewResolveServer(n.quitCh, snapshotManager)
|
||||
|
||||
go snapshotManager.Start()
|
||||
go statusManager.Start()
|
||||
go taskManager.Start()
|
||||
go resolveServer.Start()
|
||||
}
|
||||
|
||||
func (n *HTTPDNSNode) stop() {
|
||||
n.quitOnce.Do(func() {
|
||||
close(n.quitCh)
|
||||
_ = n.sock.Close()
|
||||
})
|
||||
}
|
||||
1119
EdgeHttpDNS/internal/nodes/resolve_server.go
Normal file
1119
EdgeHttpDNS/internal/nodes/resolve_server.go
Normal file
File diff suppressed because it is too large
Load Diff
39
EdgeHttpDNS/internal/nodes/runtime_log.go
Normal file
39
EdgeHttpDNS/internal/nodes/runtime_log.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
|
||||
)
|
||||
|
||||
func reportRuntimeLog(level string, logType string, module string, description string, requestID string) {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][runtime-log]", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
nodeID := int64(0)
|
||||
|
||||
now := time.Now()
|
||||
_, err = rpcClient.HTTPDNSRuntimeLogRPC.CreateHTTPDNSRuntimeLogs(rpcClient.Context(), &pb.CreateHTTPDNSRuntimeLogsRequest{
|
||||
Logs: []*pb.HTTPDNSRuntimeLog{
|
||||
{
|
||||
NodeId: nodeID,
|
||||
Level: level,
|
||||
Type: logType,
|
||||
Module: module,
|
||||
Description: description,
|
||||
Count: 1,
|
||||
RequestId: requestID,
|
||||
CreatedAt: now.Unix(),
|
||||
Day: now.Format("20060102"),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][runtime-log]", err.Error())
|
||||
}
|
||||
}
|
||||
160
EdgeHttpDNS/internal/nodes/snapshot_manager.go
Normal file
160
EdgeHttpDNS/internal/nodes/snapshot_manager.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
|
||||
)
|
||||
|
||||
type LoadedDomain struct {
|
||||
Domain *pb.HTTPDNSDomain
|
||||
Rules []*pb.HTTPDNSCustomRule
|
||||
}
|
||||
|
||||
type LoadedApp struct {
|
||||
App *pb.HTTPDNSApp
|
||||
Domains map[string]*LoadedDomain // key: lower(domain)
|
||||
}
|
||||
|
||||
type LoadedSnapshot struct {
|
||||
LoadedAt int64
|
||||
|
||||
NodeID int64
|
||||
ClusterID int64
|
||||
|
||||
Clusters map[int64]*pb.HTTPDNSCluster
|
||||
Apps map[string]*LoadedApp // key: lower(appId)
|
||||
}
|
||||
|
||||
type SnapshotManager struct {
|
||||
quitCh <-chan struct{}
|
||||
ticker *time.Ticker
|
||||
|
||||
locker sync.RWMutex
|
||||
snapshot *LoadedSnapshot
|
||||
}
|
||||
|
||||
func NewSnapshotManager(quitCh <-chan struct{}) *SnapshotManager {
|
||||
return &SnapshotManager{
|
||||
quitCh: quitCh,
|
||||
ticker: time.NewTicker(30 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SnapshotManager) Start() {
|
||||
defer m.ticker.Stop()
|
||||
|
||||
if err := m.RefreshNow("startup"); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][snapshot]initial refresh failed:", err.Error())
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.ticker.C:
|
||||
if err := m.RefreshNow("periodic"); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][snapshot]periodic refresh failed:", err.Error())
|
||||
}
|
||||
case <-m.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SnapshotManager) Current() *LoadedSnapshot {
|
||||
m.locker.RLock()
|
||||
defer m.locker.RUnlock()
|
||||
return m.snapshot
|
||||
}
|
||||
|
||||
func (m *SnapshotManager) RefreshNow(reason string) error {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeResp, err := rpcClient.HTTPDNSNodeRPC.FindHTTPDNSNode(rpcClient.Context(), &pb.FindHTTPDNSNodeRequest{
|
||||
NodeId: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nodeResp.GetNode() == nil {
|
||||
return fmt.Errorf("httpdns node info not found")
|
||||
}
|
||||
|
||||
clusterResp, err := rpcClient.HTTPDNSClusterRPC.FindAllHTTPDNSClusters(rpcClient.Context(), &pb.FindAllHTTPDNSClustersRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusters := map[int64]*pb.HTTPDNSCluster{}
|
||||
for _, cluster := range clusterResp.GetClusters() {
|
||||
if cluster == nil || cluster.GetId() <= 0 {
|
||||
continue
|
||||
}
|
||||
clusters[cluster.GetId()] = cluster
|
||||
}
|
||||
|
||||
appResp, err := rpcClient.HTTPDNSAppRPC.FindAllHTTPDNSApps(rpcClient.Context(), &pb.FindAllHTTPDNSAppsRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apps := map[string]*LoadedApp{}
|
||||
for _, app := range appResp.GetApps() {
|
||||
if app == nil || app.GetId() <= 0 || len(app.GetAppId()) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
domainResp, err := rpcClient.HTTPDNSDomainRPC.ListHTTPDNSDomainsWithAppId(rpcClient.Context(), &pb.ListHTTPDNSDomainsWithAppIdRequest{
|
||||
AppDbId: app.GetId(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][snapshot]list domains failed, appId:", app.GetAppId(), "err:", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
domains := map[string]*LoadedDomain{}
|
||||
for _, domain := range domainResp.GetDomains() {
|
||||
if domain == nil || domain.GetId() <= 0 || len(domain.GetDomain()) == 0 {
|
||||
continue
|
||||
}
|
||||
ruleResp, err := rpcClient.HTTPDNSRuleRPC.ListHTTPDNSCustomRulesWithDomainId(rpcClient.Context(), &pb.ListHTTPDNSCustomRulesWithDomainIdRequest{
|
||||
DomainId: domain.GetId(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][snapshot]list rules failed, domain:", domain.GetDomain(), "err:", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
domains[strings.ToLower(strings.TrimSpace(domain.GetDomain()))] = &LoadedDomain{
|
||||
Domain: domain,
|
||||
Rules: ruleResp.GetRules(),
|
||||
}
|
||||
}
|
||||
|
||||
apps[strings.ToLower(strings.TrimSpace(app.GetAppId()))] = &LoadedApp{
|
||||
App: app,
|
||||
Domains: domains,
|
||||
}
|
||||
}
|
||||
|
||||
snapshot := &LoadedSnapshot{
|
||||
LoadedAt: time.Now().Unix(),
|
||||
NodeID: nodeResp.GetNode().GetId(),
|
||||
ClusterID: nodeResp.GetNode().GetClusterId(),
|
||||
Clusters: clusters,
|
||||
Apps: apps,
|
||||
}
|
||||
|
||||
m.locker.Lock()
|
||||
m.snapshot = snapshot
|
||||
m.locker.Unlock()
|
||||
|
||||
reportRuntimeLog("info", "config", "snapshot", "snapshot refreshed: "+reason, fmt.Sprintf("snapshot-%d", time.Now().UnixNano()))
|
||||
return nil
|
||||
}
|
||||
144
EdgeHttpDNS/internal/nodes/status_manager.go
Normal file
144
EdgeHttpDNS/internal/nodes/status_manager.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/configs"
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/utils"
|
||||
)
|
||||
|
||||
|
||||
type StatusManager struct {
|
||||
quitCh <-chan struct{}
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func NewStatusManager(quitCh <-chan struct{}) *StatusManager {
|
||||
return &StatusManager{
|
||||
quitCh: quitCh,
|
||||
ticker: time.NewTicker(30 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *StatusManager) Start() {
|
||||
defer m.ticker.Stop()
|
||||
|
||||
m.update()
|
||||
for {
|
||||
select {
|
||||
case <-m.ticker.C:
|
||||
m.update()
|
||||
case <-m.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *StatusManager) update() {
|
||||
status := m.collectStatus()
|
||||
statusJSON, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][status]marshal status failed:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][status]rpc unavailable:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := configs.SharedAPIConfig()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][status]load config failed:", err.Error())
|
||||
return
|
||||
}
|
||||
nodeId, _ := strconv.ParseInt(config.NodeId, 10, 64)
|
||||
|
||||
_, err = rpcClient.HTTPDNSNodeRPC.UpdateHTTPDNSNodeStatus(rpcClient.Context(), &pb.UpdateHTTPDNSNodeStatusRequest{
|
||||
NodeId: nodeId,
|
||||
IsUp: true,
|
||||
IsInstalled: true,
|
||||
IsActive: true,
|
||||
StatusJSON: statusJSON,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][status]update status failed:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *StatusManager) collectStatus() *nodeconfigs.NodeStatus {
|
||||
now := time.Now().Unix()
|
||||
|
||||
status := &nodeconfigs.NodeStatus{
|
||||
BuildVersion: teaconst.Version,
|
||||
BuildVersionCode: utils.VersionToLong(teaconst.Version),
|
||||
ConfigVersion: 0,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
CPULogicalCount: runtime.NumCPU(),
|
||||
CPUPhysicalCount: runtime.NumCPU(),
|
||||
IsActive: true,
|
||||
ConnectionCount: 0,
|
||||
UpdatedAt: now,
|
||||
Timestamp: now,
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err == nil {
|
||||
total, failed, avgCostSeconds := rpcClient.GetAndResetMetrics()
|
||||
if total > 0 {
|
||||
status.APISuccessPercent = float64(total-failed) * 100.0 / float64(total)
|
||||
status.APIAvgCostSeconds = avgCostSeconds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
status.Hostname = hostname
|
||||
|
||||
exePath, _ := os.Executable()
|
||||
status.ExePath = exePath
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
status.MemoryTotal = memStats.Sys
|
||||
if status.MemoryTotal > 0 {
|
||||
status.MemoryUsage = float64(memStats.Alloc) / float64(status.MemoryTotal)
|
||||
}
|
||||
|
||||
load1m, load5m, load15m := readLoadAvg()
|
||||
status.Load1m = load1m
|
||||
status.Load5m = load5m
|
||||
status.Load15m = load15m
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func readLoadAvg() (float64, float64, float64) {
|
||||
data, err := os.ReadFile("/proc/loadavg")
|
||||
if err != nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
parts := strings.Fields(strings.TrimSpace(string(data)))
|
||||
if len(parts) < 3 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
load1m, _ := strconv.ParseFloat(parts[0], 64)
|
||||
load5m, _ := strconv.ParseFloat(parts[1], 64)
|
||||
load15m, _ := strconv.ParseFloat(parts[2], 64)
|
||||
return load1m, load5m, load15m
|
||||
}
|
||||
131
EdgeHttpDNS/internal/nodes/task_manager.go
Normal file
131
EdgeHttpDNS/internal/nodes/task_manager.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
|
||||
)
|
||||
|
||||
type TaskManager struct {
|
||||
quitCh <-chan struct{}
|
||||
ticker *time.Ticker
|
||||
version int64
|
||||
|
||||
snapshotManager *SnapshotManager
|
||||
}
|
||||
|
||||
func NewTaskManager(quitCh <-chan struct{}, snapshotManager *SnapshotManager) *TaskManager {
|
||||
return &TaskManager{
|
||||
quitCh: quitCh,
|
||||
ticker: time.NewTicker(20 * time.Second),
|
||||
version: 0,
|
||||
snapshotManager: snapshotManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TaskManager) Start() {
|
||||
defer m.ticker.Stop()
|
||||
|
||||
m.processTasks()
|
||||
for {
|
||||
select {
|
||||
case <-m.ticker.C:
|
||||
m.processTasks()
|
||||
case <-m.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TaskManager) processTasks() {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][task]rpc unavailable:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := rpcClient.NodeTaskRPC.FindNodeTasks(rpcClient.Context(), &pb.FindNodeTasksRequest{
|
||||
Version: m.version,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][task]fetch tasks failed:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range resp.GetNodeTasks() {
|
||||
ok, errorMessage := m.handleTask(task)
|
||||
|
||||
_, reportErr := rpcClient.NodeTaskRPC.ReportNodeTaskDone(rpcClient.Context(), &pb.ReportNodeTaskDoneRequest{
|
||||
NodeTaskId: task.GetId(),
|
||||
IsOk: ok,
|
||||
Error: errorMessage,
|
||||
})
|
||||
if reportErr != nil {
|
||||
log.Println("[HTTPDNS_NODE][task]report task result failed:", reportErr.Error())
|
||||
}
|
||||
|
||||
if task.GetVersion() > m.version {
|
||||
m.version = task.GetVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TaskManager) handleTask(task *pb.NodeTask) (bool, string) {
|
||||
taskType := task.GetType()
|
||||
requestID := fmt.Sprintf("task-%d", task.GetId())
|
||||
|
||||
switch taskType {
|
||||
case teaconst.TaskTypeHTTPDNSConfigChanged:
|
||||
if m.snapshotManager != nil {
|
||||
if err := m.snapshotManager.RefreshNow(taskType); err != nil {
|
||||
reportRuntimeLog("error", "config", "task-manager", "refresh snapshot failed: "+err.Error(), requestID)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
reportRuntimeLog("info", "config", "task-manager", "HTTPDNS configuration updated", requestID)
|
||||
return true, ""
|
||||
case teaconst.TaskTypeHTTPDNSAppChanged:
|
||||
if m.snapshotManager != nil {
|
||||
if err := m.snapshotManager.RefreshNow(taskType); err != nil {
|
||||
reportRuntimeLog("error", "app", "task-manager", "refresh snapshot failed: "+err.Error(), requestID)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
reportRuntimeLog("info", "app", "task-manager", "HTTPDNS app policy updated", requestID)
|
||||
return true, ""
|
||||
case teaconst.TaskTypeHTTPDNSDomainChanged:
|
||||
if m.snapshotManager != nil {
|
||||
if err := m.snapshotManager.RefreshNow(taskType); err != nil {
|
||||
reportRuntimeLog("error", "domain", "task-manager", "refresh snapshot failed: "+err.Error(), requestID)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
reportRuntimeLog("info", "domain", "task-manager", "HTTPDNS domain binding updated", requestID)
|
||||
return true, ""
|
||||
case teaconst.TaskTypeHTTPDNSRuleChanged:
|
||||
if m.snapshotManager != nil {
|
||||
if err := m.snapshotManager.RefreshNow(taskType); err != nil {
|
||||
reportRuntimeLog("error", "rule", "task-manager", "refresh snapshot failed: "+err.Error(), requestID)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
reportRuntimeLog("info", "rule", "task-manager", "HTTPDNS custom rule updated", requestID)
|
||||
return true, ""
|
||||
case teaconst.TaskTypeHTTPDNSTLSChanged:
|
||||
if m.snapshotManager != nil {
|
||||
if err := m.snapshotManager.RefreshNow(taskType); err != nil {
|
||||
reportRuntimeLog("error", "tls", "task-manager", "refresh snapshot failed: "+err.Error(), requestID)
|
||||
return false, err.Error()
|
||||
}
|
||||
}
|
||||
reportRuntimeLog("info", "tls", "task-manager", "HTTPDNS TLS config updated", requestID)
|
||||
return true, ""
|
||||
default:
|
||||
reportRuntimeLog("warning", "task", "task-manager", "unknown task type: "+taskType, requestID)
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
222
EdgeHttpDNS/internal/rpc/rpc_client.go
Normal file
222
EdgeHttpDNS/internal/rpc/rpc_client.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/configs"
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/encrypt"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/encoding/gzip"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
|
||||
type RPCClient struct {
|
||||
apiConfig *configs.APIConfig
|
||||
conns []*grpc.ClientConn
|
||||
locker sync.RWMutex
|
||||
|
||||
NodeTaskRPC pb.NodeTaskServiceClient
|
||||
HTTPDNSNodeRPC pb.HTTPDNSNodeServiceClient
|
||||
HTTPDNSClusterRPC pb.HTTPDNSClusterServiceClient
|
||||
HTTPDNSAppRPC pb.HTTPDNSAppServiceClient
|
||||
HTTPDNSDomainRPC pb.HTTPDNSDomainServiceClient
|
||||
HTTPDNSRuleRPC pb.HTTPDNSRuleServiceClient
|
||||
HTTPDNSRuntimeLogRPC pb.HTTPDNSRuntimeLogServiceClient
|
||||
HTTPDNSAccessLogRPC pb.HTTPDNSAccessLogServiceClient
|
||||
HTTPDNSSandboxRPC pb.HTTPDNSSandboxServiceClient
|
||||
|
||||
totalRequests int64
|
||||
failedRequests int64
|
||||
totalCostMs int64
|
||||
}
|
||||
|
||||
|
||||
func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
|
||||
if apiConfig == nil {
|
||||
return nil, errors.New("api config should not be nil")
|
||||
}
|
||||
|
||||
client := &RPCClient{apiConfig: apiConfig}
|
||||
client.NodeTaskRPC = pb.NewNodeTaskServiceClient(client)
|
||||
client.HTTPDNSNodeRPC = pb.NewHTTPDNSNodeServiceClient(client)
|
||||
client.HTTPDNSClusterRPC = pb.NewHTTPDNSClusterServiceClient(client)
|
||||
client.HTTPDNSAppRPC = pb.NewHTTPDNSAppServiceClient(client)
|
||||
client.HTTPDNSDomainRPC = pb.NewHTTPDNSDomainServiceClient(client)
|
||||
client.HTTPDNSRuleRPC = pb.NewHTTPDNSRuleServiceClient(client)
|
||||
client.HTTPDNSRuntimeLogRPC = pb.NewHTTPDNSRuntimeLogServiceClient(client)
|
||||
client.HTTPDNSAccessLogRPC = pb.NewHTTPDNSAccessLogServiceClient(client)
|
||||
client.HTTPDNSSandboxRPC = pb.NewHTTPDNSSandboxServiceClient(client)
|
||||
|
||||
err := client.init()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *RPCClient) Context() context.Context {
|
||||
ctx := context.Background()
|
||||
payload := maps.Map{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"type": "httpdns",
|
||||
"userId": 0,
|
||||
}
|
||||
|
||||
method, err := encrypt.NewMethodInstance(teaconst.EncryptMethod, c.apiConfig.Secret, c.apiConfig.NodeId)
|
||||
if err != nil {
|
||||
return context.Background()
|
||||
}
|
||||
encrypted, err := method.Encrypt(payload.AsJSON())
|
||||
if err != nil {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
token := base64.StdEncoding.EncodeToString(encrypted)
|
||||
return metadata.AppendToOutgoingContext(ctx, "nodeId", c.apiConfig.NodeId, "token", token)
|
||||
}
|
||||
|
||||
func (c *RPCClient) UpdateConfig(config *configs.APIConfig) error {
|
||||
c.apiConfig = config
|
||||
c.locker.Lock()
|
||||
defer c.locker.Unlock()
|
||||
return c.init()
|
||||
}
|
||||
|
||||
func (c *RPCClient) init() error {
|
||||
conns := []*grpc.ClientConn{}
|
||||
for _, endpoint := range c.apiConfig.RPCEndpoints {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse endpoint failed: %w", err)
|
||||
}
|
||||
|
||||
var conn *grpc.ClientConn
|
||||
callOptions := grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(128<<20),
|
||||
grpc.MaxCallSendMsgSize(128<<20),
|
||||
grpc.UseCompressor(gzip.Name),
|
||||
)
|
||||
keepaliveParams := grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 30 * time.Second,
|
||||
})
|
||||
|
||||
if u.Scheme == "http" {
|
||||
conn, err = grpc.Dial(u.Host, grpc.WithTransportCredentials(insecure.NewCredentials()), callOptions, keepaliveParams)
|
||||
} else if u.Scheme == "https" {
|
||||
conn, err = grpc.Dial(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})), callOptions, keepaliveParams)
|
||||
} else {
|
||||
return errors.New("invalid endpoint scheme '" + u.Scheme + "'")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conns = append(conns, conn)
|
||||
}
|
||||
|
||||
if len(conns) == 0 {
|
||||
return errors.New("no available rpc endpoints")
|
||||
}
|
||||
|
||||
c.conns = conns
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RPCClient) pickConn() *grpc.ClientConn {
|
||||
c.locker.RLock()
|
||||
defer c.locker.RUnlock()
|
||||
|
||||
countConns := len(c.conns)
|
||||
if countConns == 0 {
|
||||
return nil
|
||||
}
|
||||
if countConns == 1 {
|
||||
return c.conns[0]
|
||||
}
|
||||
|
||||
for _, state := range []connectivity.State{
|
||||
connectivity.Ready,
|
||||
connectivity.Idle,
|
||||
connectivity.Connecting,
|
||||
connectivity.TransientFailure,
|
||||
} {
|
||||
available := []*grpc.ClientConn{}
|
||||
for _, conn := range c.conns {
|
||||
if conn.GetState() == state {
|
||||
available = append(available, conn)
|
||||
}
|
||||
}
|
||||
if len(available) > 0 {
|
||||
return c.randConn(available)
|
||||
}
|
||||
}
|
||||
|
||||
return c.randConn(c.conns)
|
||||
}
|
||||
|
||||
func (c *RPCClient) randConn(conns []*grpc.ClientConn) *grpc.ClientConn {
|
||||
l := len(conns)
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
if l == 1 {
|
||||
return conns[0]
|
||||
}
|
||||
return conns[rands.Int(0, l-1)]
|
||||
}
|
||||
|
||||
func (c *RPCClient) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
||||
conn := c.pickConn()
|
||||
if conn == nil {
|
||||
return errors.New("can not get available grpc connection")
|
||||
}
|
||||
|
||||
atomic.AddInt64(&c.totalRequests, 1)
|
||||
start := time.Now()
|
||||
err := conn.Invoke(ctx, method, args, reply, opts...)
|
||||
costMs := time.Since(start).Milliseconds()
|
||||
atomic.AddInt64(&c.totalCostMs, costMs)
|
||||
|
||||
if err != nil {
|
||||
atomic.AddInt64(&c.failedRequests, 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetAndResetMetrics() (total int64, failed int64, avgCostSeconds float64) {
|
||||
total = atomic.SwapInt64(&c.totalRequests, 0)
|
||||
failed = atomic.SwapInt64(&c.failedRequests, 0)
|
||||
costMs := atomic.SwapInt64(&c.totalCostMs, 0)
|
||||
|
||||
if total > 0 {
|
||||
avgCostSeconds = float64(costMs) / float64(total) / 1000.0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (c *RPCClient) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
conn := c.pickConn()
|
||||
if conn == nil {
|
||||
return nil, errors.New("can not get available grpc connection")
|
||||
}
|
||||
return conn.NewStream(ctx, desc, method, opts...)
|
||||
}
|
||||
31
EdgeHttpDNS/internal/rpc/shared.go
Normal file
31
EdgeHttpDNS/internal/rpc/shared.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/configs"
|
||||
)
|
||||
|
||||
var sharedRPCClient *RPCClient
|
||||
var sharedLocker sync.Mutex
|
||||
|
||||
func SharedRPC() (*RPCClient, error) {
|
||||
sharedLocker.Lock()
|
||||
defer sharedLocker.Unlock()
|
||||
|
||||
config, err := configs.SharedAPIConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sharedRPCClient == nil {
|
||||
client, err := NewRPCClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedRPCClient = client
|
||||
return sharedRPCClient, nil
|
||||
}
|
||||
|
||||
return sharedRPCClient, nil
|
||||
}
|
||||
18
EdgeHttpDNS/internal/utils/ip.go
Normal file
18
EdgeHttpDNS/internal/utils/ip.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
)
|
||||
|
||||
func IP2Long(ip string) uint32 {
|
||||
s := net.ParseIP(ip)
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if len(s) == 16 {
|
||||
return binary.BigEndian.Uint32(s[12:16])
|
||||
}
|
||||
return binary.BigEndian.Uint32(s)
|
||||
}
|
||||
46
EdgeHttpDNS/internal/utils/service.go
Normal file
46
EdgeHttpDNS/internal/utils/service.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
type ServiceManager struct {
|
||||
Name string
|
||||
Description string
|
||||
|
||||
onceLocker sync.Once
|
||||
}
|
||||
|
||||
func NewServiceManager(name, description string) *ServiceManager {
|
||||
manager := &ServiceManager{
|
||||
Name: name,
|
||||
Description: description,
|
||||
}
|
||||
manager.resetRoot()
|
||||
return manager
|
||||
}
|
||||
|
||||
func (m *ServiceManager) setup() {
|
||||
m.onceLocker.Do(func() {})
|
||||
}
|
||||
|
||||
func (m *ServiceManager) resetRoot() {
|
||||
if !Tea.IsTesting() {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
exePath = os.Args[0]
|
||||
}
|
||||
link, err := filepath.EvalSymlinks(exePath)
|
||||
if err == nil {
|
||||
exePath = link
|
||||
}
|
||||
fullPath, err := filepath.Abs(exePath)
|
||||
if err == nil {
|
||||
Tea.UpdateRoot(filepath.Dir(filepath.Dir(fullPath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
65
EdgeHttpDNS/internal/utils/service_linux.go
Normal file
65
EdgeHttpDNS/internal/utils/service_linux.go
Normal file
@@ -0,0 +1,65 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
)
|
||||
|
||||
var systemdServiceFile = "/etc/systemd/system/" + teaconst.SystemdServiceName + ".service"
|
||||
|
||||
func (m *ServiceManager) Install(exePath string, args []string) error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can install the service")
|
||||
}
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc := `[Unit]
|
||||
Description=GoEdge HTTPDNS Node Service
|
||||
Before=shutdown.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
ExecStart=` + exePath + ` daemon
|
||||
ExecStop=` + exePath + ` stop
|
||||
ExecReload=` + exePath + ` restart
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
||||
|
||||
err = os.WriteFile(systemdServiceFile, []byte(desc), 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "stop", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return exec.Command(systemd, "enable", teaconst.SystemdServiceName+".service").Run()
|
||||
}
|
||||
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return os.Remove(systemdServiceFile)
|
||||
}
|
||||
14
EdgeHttpDNS/internal/utils/service_others.go
Normal file
14
EdgeHttpDNS/internal/utils/service_others.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package utils
|
||||
|
||||
import "errors"
|
||||
|
||||
func (m *ServiceManager) Install(exePath string, args []string) error {
|
||||
return errors.New("service install is only supported on linux in this version")
|
||||
}
|
||||
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
return errors.New("service uninstall is only supported on linux in this version")
|
||||
}
|
||||
15
EdgeHttpDNS/internal/utils/version.go
Normal file
15
EdgeHttpDNS/internal/utils/version.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func VersionToLong(version string) uint32 {
|
||||
countDots := strings.Count(version, ".")
|
||||
if countDots == 2 {
|
||||
version += ".0"
|
||||
} else if countDots == 1 {
|
||||
version += ".0.0"
|
||||
} else if countDots == 0 {
|
||||
version += ".0.0.0"
|
||||
}
|
||||
return IP2Long(version)
|
||||
}
|
||||
@@ -1,148 +1,77 @@
|
||||
# Alicloud HTTPDNS Android SDK
|
||||
# HTTPDNS Android SDK (SNI Hidden v1.0.0)
|
||||
|
||||
面向 Android 的 HTTP/HTTPS DNS 解析 SDK,提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 Android API 19(Android 4.4)。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 鉴权请求与可选 AES 传输加密
|
||||
- IPv4/IPv6 双栈解析,支持自动/同时解析
|
||||
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
|
||||
- 预解析、区域路由、网络切换自动刷新
|
||||
- 可定制日志回调与会话追踪 `sessionId`
|
||||
|
||||
## 安装(Gradle)
|
||||
|
||||
在项目的 `build.gradle` 中添加:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'com.aliyun.ams:alicloud-android-httpdns:2.6.7'
|
||||
}
|
||||
```
|
||||
|
||||
请访问 [Android SDK发布说明](https://help.aliyun.com/document_detail/435251.html) 查看最新版本号。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### Java
|
||||
## 1. Init
|
||||
|
||||
```java
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns;
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService;
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
// 初始化配置
|
||||
String appId = "app1f1ndpo9";
|
||||
|
||||
new InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setSecretKey("YOUR_SECRET_KEY")
|
||||
.setEnableExpiredIp(true) // 允许返回过期 IP
|
||||
.buildFor("YOUR_ACCOUNT_ID");
|
||||
.setPrimaryServiceHost("httpdns-a.example.com")
|
||||
.setBackupServiceHost("httpdns-b.example.com")
|
||||
.setServicePort(443)
|
||||
.setSecretKey("your-sign-secret") // optional if sign is enabled
|
||||
.setEnableHttps(true)
|
||||
.buildFor(appId);
|
||||
|
||||
// 获取实例
|
||||
HttpDnsService httpDns = HttpDns.getService("YOUR_ACCOUNT_ID");
|
||||
|
||||
// 预解析热点域名
|
||||
httpDns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com")));
|
||||
|
||||
// 解析域名
|
||||
HTTPDNSResult result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto);
|
||||
String[] ips = result.getIps();
|
||||
HttpDnsService httpDnsService = HttpDns.getService(appId);
|
||||
```
|
||||
|
||||
### Kotlin
|
||||
|
||||
```kotlin
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType
|
||||
|
||||
// 初始化配置
|
||||
InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setSecretKey("YOUR_SECRET_KEY")
|
||||
.setEnableExpiredIp(true) // 允许返回过期 IP
|
||||
.buildFor("YOUR_ACCOUNT_ID")
|
||||
|
||||
// 获取实例
|
||||
val httpDns = HttpDns.getService("YOUR_ACCOUNT_ID")
|
||||
|
||||
// 预解析热点域名
|
||||
httpDns.setPreResolveHosts(arrayListOf("www.aliyun.com"))
|
||||
|
||||
// 解析域名
|
||||
val result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto)
|
||||
val ips = result.ips
|
||||
```
|
||||
|
||||
### 提示
|
||||
|
||||
- 启动时通过 `setPreResolveHosts()` 预热热点域名
|
||||
- 如需在刷新期间容忍 TTL 过期,可开启 `setEnableExpiredIp(true)`
|
||||
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障
|
||||
|
||||
## 源码构建
|
||||
|
||||
```bash
|
||||
./gradlew clean :httpdns-sdk:assembleRelease
|
||||
```
|
||||
|
||||
构建产物位于 `httpdns-sdk/build/outputs/aar/` 目录。
|
||||
|
||||
### 版本说明
|
||||
|
||||
项目使用 `productFlavors` 区分不同版本:
|
||||
- `normal`:中国大陆版本
|
||||
- `intl`:国际版本
|
||||
- `end2end`:用于单元测试
|
||||
|
||||
## 测试
|
||||
|
||||
### 运行单元测试
|
||||
|
||||
```bash
|
||||
./gradlew clean :httpdns-sdk:testEnd2endForTestUnitTest
|
||||
```
|
||||
|
||||
### Demo 应用
|
||||
|
||||
SDK 提供了两个 Demo:
|
||||
|
||||
#### 1. app module(旧版 Demo)
|
||||
|
||||
在 `MyApp.java` 中配置测试账号:
|
||||
## 2. Resolve
|
||||
|
||||
```java
|
||||
private HttpDnsHolder holderA = new HttpDnsHolder("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret");
|
||||
private HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null);
|
||||
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
|
||||
"api.business.com",
|
||||
RequestIpType.auto,
|
||||
null,
|
||||
null
|
||||
);
|
||||
```
|
||||
|
||||
> 两个实例用于测试实例间互不影响,体验时只需配置一个
|
||||
## 3. Official HTTP Adapter (IP + Empty-SNI + Host)
|
||||
|
||||
#### 2. demo module(推荐)
|
||||
```java
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterRequest;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterResponse;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
|
||||
使用 Kotlin + MVVM 开发,功能更丰富。在 `demo/build.gradle` 中配置测试账号:
|
||||
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(
|
||||
httpDnsService,
|
||||
new HttpDnsAdapterOptions.Builder()
|
||||
.setConnectTimeoutMillis(3000)
|
||||
.setReadTimeoutMillis(5000)
|
||||
.setRequestIpType(RequestIpType.auto)
|
||||
.setAllowInsecureCertificatesForDebugOnly(false)
|
||||
.build()
|
||||
);
|
||||
|
||||
```groovy
|
||||
buildConfigField "String", "ACCOUNT_ID", "\"请替换为测试用实例的accountId\""
|
||||
buildConfigField "String", "SECRET_KEY", "\"请替换为测试用实例的secret\""
|
||||
buildConfigField "String", "AES_SECRET_KEY", "\"请替换为测试用实例的aes\""
|
||||
HttpDnsAdapterResponse response = adapter.execute(
|
||||
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
|
||||
);
|
||||
```
|
||||
|
||||
## 依赖与要求
|
||||
Behavior is fixed:
|
||||
- Resolve by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
- Android API 19+(Android 4.4+)
|
||||
- 需要权限:`INTERNET`、`ACCESS_NETWORK_STATE`
|
||||
## 4. Public Errors
|
||||
|
||||
## 安全说明
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
- 切勿提交真实的 AccountID/SecretKey,请通过本地安全配置或 CI 注入
|
||||
- 若担心设备时间偏差影响鉴权,可使用 `setAuthCurrentTime()` 校正时间
|
||||
## 5. Removed Public Params
|
||||
|
||||
## 文档
|
||||
|
||||
官方文档:[Android SDK手册](https://help.aliyun.com/document_detail/435250.html)
|
||||
|
||||
## 感谢
|
||||
|
||||
本项目中 Inet64Util 工具类由 [Shinelw](https://github.com/Shinelw) 贡献支持,感谢。
|
||||
Do not use legacy public parameters:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.net.NetworkStateManager;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
/**
|
||||
@@ -66,4 +68,18 @@ public class HttpDns {
|
||||
sHolder = new HttpDnsInstanceHolder(new InstanceCreator());
|
||||
NetworkStateManager.getInstance().reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.alibaba.sdk.android.httpdns;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -73,6 +75,20 @@ public class HttpDns {
|
||||
InitConfig.addConfig(accountId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或者禁用httpdns,理论上这个是内部接口,不给外部使用的
|
||||
* 但是已经对外暴露,所以保留
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.alibaba.sdk.android.httpdns;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class HttpDnsV1Client {
|
||||
private HttpDnsV1Client() {
|
||||
}
|
||||
|
||||
public static HttpDnsService init(Context context,
|
||||
String appId,
|
||||
String primaryServiceHost,
|
||||
String backupServiceHost,
|
||||
int servicePort,
|
||||
String signSecret) {
|
||||
InitConfig.Builder builder = new InitConfig.Builder()
|
||||
.setContext(context)
|
||||
.setEnableHttps(true)
|
||||
.setPrimaryServiceHost(primaryServiceHost)
|
||||
.setServicePort(servicePort > 0 ? servicePort : 443);
|
||||
|
||||
if (!TextUtils.isEmpty(backupServiceHost)) {
|
||||
builder.setBackupServiceHost(backupServiceHost);
|
||||
}
|
||||
if (!TextUtils.isEmpty(signSecret)) {
|
||||
builder.setSecretKey(signSecret);
|
||||
}
|
||||
|
||||
builder.buildFor(appId);
|
||||
if (!TextUtils.isEmpty(signSecret)) {
|
||||
return HttpDns.getService(context, appId, signSecret);
|
||||
}
|
||||
return HttpDns.getService(context, appId);
|
||||
}
|
||||
|
||||
public static HTTPDNSResult resolveHost(HttpDnsService service,
|
||||
String host,
|
||||
String qtype,
|
||||
String cip) {
|
||||
if (service == null) {
|
||||
return HTTPDNSResult.empty(host);
|
||||
}
|
||||
RequestIpType requestIpType = RequestIpType.auto;
|
||||
if ("AAAA".equalsIgnoreCase(qtype)) {
|
||||
requestIpType = RequestIpType.v6;
|
||||
}
|
||||
|
||||
Map<String, String> params = null;
|
||||
if (!TextUtils.isEmpty(cip)) {
|
||||
params = new HashMap<>();
|
||||
params.put("cip", cip);
|
||||
}
|
||||
|
||||
return service.getHttpDnsResultForHostSyncNonBlocking(host, requestIpType, params, host);
|
||||
}
|
||||
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return HttpDns.buildHttpClientAdapter(service);
|
||||
}
|
||||
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service,
|
||||
HttpDnsAdapterOptions options) {
|
||||
return HttpDns.buildHttpClientAdapter(service, options);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,9 @@ public class InitConfig {
|
||||
private final String mBizTags;
|
||||
private final String aesSecretKey;
|
||||
private final String secretKey;
|
||||
private final String primaryServiceHost;
|
||||
private final String backupServiceHost;
|
||||
private final int servicePort;
|
||||
private final Context context;
|
||||
|
||||
private InitConfig(Builder builder) {
|
||||
@@ -82,6 +85,9 @@ public class InitConfig {
|
||||
mEnableObservable = builder.enableObservable;
|
||||
mBizTags = builder.bizTags;
|
||||
aesSecretKey = builder.aesSecretKey;
|
||||
primaryServiceHost = builder.primaryServiceHost;
|
||||
backupServiceHost = builder.backupServiceHost;
|
||||
servicePort = builder.servicePort;
|
||||
context = builder.context;
|
||||
secretKey = builder.secretKey;
|
||||
}
|
||||
@@ -159,6 +165,18 @@ public class InitConfig {
|
||||
return aesSecretKey;
|
||||
}
|
||||
|
||||
public String getPrimaryServiceHost() {
|
||||
return primaryServiceHost;
|
||||
}
|
||||
|
||||
public String getBackupServiceHost() {
|
||||
return backupServiceHost;
|
||||
}
|
||||
|
||||
public int getServicePort() {
|
||||
return servicePort;
|
||||
}
|
||||
|
||||
public Context getContext() {return context;}
|
||||
|
||||
public String getSecretKey() {
|
||||
@@ -184,6 +202,9 @@ public class InitConfig {
|
||||
private Map<String, String> sdnsGlobalParams = null;
|
||||
private String bizTags = null;
|
||||
private String aesSecretKey = null;
|
||||
private String primaryServiceHost = null;
|
||||
private String backupServiceHost = null;
|
||||
private int servicePort = -1;
|
||||
private Context context = null;
|
||||
private String secretKey = null;
|
||||
|
||||
@@ -415,6 +436,43 @@ public class InitConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主服务域名。
|
||||
*/
|
||||
public Builder setPrimaryServiceHost(String host) {
|
||||
this.primaryServiceHost = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置备服务域名。
|
||||
*/
|
||||
public Builder setBackupServiceHost(String host) {
|
||||
this.backupServiceHost = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置服务域名,支持主备两个。
|
||||
*/
|
||||
public Builder setServiceHosts(List<String> hosts) {
|
||||
if (hosts != null && hosts.size() > 0) {
|
||||
this.primaryServiceHost = hosts.get(0);
|
||||
}
|
||||
if (hosts != null && hosts.size() > 1) {
|
||||
this.backupServiceHost = hosts.get(1);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务端口,默认 -1 表示使用协议默认端口。
|
||||
*/
|
||||
public Builder setServicePort(int port) {
|
||||
this.servicePort = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置context
|
||||
* @param context 上下文
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.alibaba.sdk.android.httpdns.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -41,6 +42,7 @@ import com.alibaba.sdk.android.httpdns.utils.Constants;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* 域名解析服务 httpdns接口的实现
|
||||
@@ -60,6 +62,7 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
private SignService mSignService;
|
||||
private AESEncryptService mAESEncryptService;
|
||||
private boolean resolveAfterNetworkChange = true;
|
||||
private boolean mUseCustomServiceHosts = false;
|
||||
/**
|
||||
* crash defend 默认关闭
|
||||
*/
|
||||
@@ -115,7 +118,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
tryUpdateRegionServer(sContext, accountId);
|
||||
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
favorInit(sContext, accountId);
|
||||
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
@@ -163,10 +168,37 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
mRequestHandler.setSdnsGlobalParams(config.getSdnsGlobalParams());
|
||||
mAESEncryptService.setAesSecretKey(config.getAesSecretKey());
|
||||
applyCustomServiceHosts(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void applyCustomServiceHosts(InitConfig config) {
|
||||
String primaryHost = config.getPrimaryServiceHost();
|
||||
String backupHost = config.getBackupServiceHost();
|
||||
ArrayList<String> hosts = new ArrayList<>();
|
||||
if (!TextUtils.isEmpty(primaryHost)) {
|
||||
hosts.add(primaryHost.trim());
|
||||
}
|
||||
if (!TextUtils.isEmpty(backupHost) && !backupHost.trim().equalsIgnoreCase(primaryHost == null ? "" : primaryHost.trim())) {
|
||||
hosts.add(backupHost.trim());
|
||||
}
|
||||
if (hosts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] serverHosts = hosts.toArray(new String[0]);
|
||||
int[] ports = new int[serverHosts.length];
|
||||
int servicePort = config.getServicePort();
|
||||
Arrays.fill(ports, servicePort > 0 ? servicePort : -1);
|
||||
|
||||
mHttpDnsConfig.setInitServers(mHttpDnsConfig.getRegion(), serverHosts, ports, serverHosts, ports);
|
||||
mHttpDnsConfig.setDefaultUpdateServer(serverHosts, ports);
|
||||
mHttpDnsConfig.setDefaultUpdateServerIpv6(serverHosts, ports);
|
||||
mHttpDnsConfig.setHTTPSRequestEnabled(true);
|
||||
mUseCustomServiceHosts = true;
|
||||
}
|
||||
|
||||
protected void beforeInit() {
|
||||
// only for test
|
||||
}
|
||||
@@ -218,7 +250,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
mRequestHandler.resetStatus();
|
||||
|
||||
//服务IP更新,触发服务IP测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -589,6 +623,12 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
|
||||
@Override
|
||||
public void setRegion(String region) {
|
||||
if (mUseCustomServiceHosts) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("ignore setRegion in custom service host mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!mHttpDnsConfig.isEnabled()) {
|
||||
HttpDnsLog.i("service is disabled");
|
||||
return;
|
||||
@@ -605,9 +645,13 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
if (changed) {
|
||||
mResultRepo.clearMemoryCache();
|
||||
//region变化,服务IP变成对应的预置IP,触发测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
}
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mScheduleService.updateRegionServerIps(region, Constants.UPDATE_REGION_SERVER_SCENES_REGION_CHANGE);
|
||||
}
|
||||
mScheduleService.updateRegionServerIps(region, Constants.UPDATE_REGION_SERVER_SCENES_REGION_CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -684,7 +728,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
});
|
||||
|
||||
//网络变化,触发服务IP测速
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
if (!mUseCustomServiceHosts) {
|
||||
mRegionServerRankingService.rankServiceIp(mHttpDnsConfig.getCurrentServer());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
@@ -817,6 +863,9 @@ public class HttpDnsServiceImpl implements HttpDnsService, OnRegionServerIpUpdat
|
||||
}
|
||||
|
||||
private void tryUpdateRegionServer(Context context, String accountId) {
|
||||
if (mUseCustomServiceHosts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mHttpDnsConfig.getCurrentServer().shouldUpdateServerIp()) {
|
||||
mScheduleService.updateRegionServerIps(Constants.UPDATE_REGION_SERVER_SCENES_INIT);
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.alibaba.sdk.android.httpdns.impl;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -67,6 +66,30 @@ public class SignService {
|
||||
return signStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新版 /resolve 请求签名:
|
||||
* appId|lower(domain)|upper(qtype)|exp|nonce
|
||||
*/
|
||||
public String signResolve(String appId, String domain, String qtype, String exp, String nonce) {
|
||||
if (TextUtils.isEmpty(mSecretKey)
|
||||
|| TextUtils.isEmpty(appId)
|
||||
|| TextUtils.isEmpty(domain)
|
||||
|| TextUtils.isEmpty(qtype)
|
||||
|| TextUtils.isEmpty(exp)
|
||||
|| TextUtils.isEmpty(nonce)) {
|
||||
return "";
|
||||
}
|
||||
String raw = appId + "|" + domain.toLowerCase() + "|" + qtype.toUpperCase() + "|" + exp + "|" + nonce;
|
||||
try {
|
||||
return hmacSha256(raw);
|
||||
} catch (Exception e) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("sign resolve fail.", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateV2SignContent(Map<String, String> map) {
|
||||
Map<String, String> sortedMap = new TreeMap<>();
|
||||
for(Map.Entry<String, String> entry : map.entrySet()) {
|
||||
@@ -91,9 +114,17 @@ public class SignService {
|
||||
private String hmacSha256(String content)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac mac = Mac.getInstance(ALGORITHM);
|
||||
mac.init(new SecretKeySpec(CommonUtil.decodeHex(mSecretKey), ALGORITHM));
|
||||
mac.init(new SecretKeySpec(mSecretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM));
|
||||
byte[] signedBytes = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
return CommonUtil.encodeHexString(signedBytes);
|
||||
StringBuilder sb = new StringBuilder(signedBytes.length * 2);
|
||||
for (byte b : signedBytes) {
|
||||
String h = Integer.toHexString(b & 0xff);
|
||||
if (h.length() == 1) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(h);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void setCurrentTimestamp(long serverTime) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpDnsAdapterException extends IOException {
|
||||
private final String errorCode;
|
||||
|
||||
public HttpDnsAdapterException(String errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterException(String errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
public class HttpDnsAdapterOptions {
|
||||
private final int connectTimeoutMillis;
|
||||
private final int readTimeoutMillis;
|
||||
private final boolean allowInsecureCertificatesForDebugOnly;
|
||||
private final RequestIpType requestIpType;
|
||||
|
||||
private HttpDnsAdapterOptions(Builder builder) {
|
||||
connectTimeoutMillis = builder.connectTimeoutMillis;
|
||||
readTimeoutMillis = builder.readTimeoutMillis;
|
||||
allowInsecureCertificatesForDebugOnly = builder.allowInsecureCertificatesForDebugOnly;
|
||||
requestIpType = builder.requestIpType;
|
||||
}
|
||||
|
||||
public int getConnectTimeoutMillis() {
|
||||
return connectTimeoutMillis;
|
||||
}
|
||||
|
||||
public int getReadTimeoutMillis() {
|
||||
return readTimeoutMillis;
|
||||
}
|
||||
|
||||
public boolean isAllowInsecureCertificatesForDebugOnly() {
|
||||
return allowInsecureCertificatesForDebugOnly;
|
||||
}
|
||||
|
||||
public RequestIpType getRequestIpType() {
|
||||
return requestIpType;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private int connectTimeoutMillis = 3000;
|
||||
private int readTimeoutMillis = 5000;
|
||||
private boolean allowInsecureCertificatesForDebugOnly = false;
|
||||
private RequestIpType requestIpType = RequestIpType.auto;
|
||||
|
||||
public Builder setConnectTimeoutMillis(int value) {
|
||||
if (value > 0) {
|
||||
connectTimeoutMillis = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReadTimeoutMillis(int value) {
|
||||
if (value > 0) {
|
||||
readTimeoutMillis = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowInsecureCertificatesForDebugOnly(boolean value) {
|
||||
allowInsecureCertificatesForDebugOnly = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRequestIpType(RequestIpType type) {
|
||||
if (type != null) {
|
||||
requestIpType = type;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterOptions build() {
|
||||
return new HttpDnsAdapterOptions(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpDnsAdapterRequest {
|
||||
private final String method;
|
||||
private final String url;
|
||||
private final Map<String, String> headers;
|
||||
private final byte[] body;
|
||||
|
||||
public HttpDnsAdapterRequest(String method, String url) {
|
||||
this(method, url, null, null);
|
||||
}
|
||||
|
||||
public HttpDnsAdapterRequest(String method, String url, Map<String, String> headers, byte[] body) {
|
||||
this.method = method == null || method.trim().isEmpty() ? "GET" : method.trim().toUpperCase();
|
||||
this.url = url;
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
this.headers = Collections.emptyMap();
|
||||
} else {
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
}
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpDnsAdapterResponse {
|
||||
private final int statusCode;
|
||||
private final Map<String, List<String>> headers;
|
||||
private final byte[] body;
|
||||
private final String usedIp;
|
||||
|
||||
public HttpDnsAdapterResponse(int statusCode, Map<String, List<String>> headers, byte[] body, String usedIp) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.usedIp = usedIp;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public String getUsedIp() {
|
||||
return usedIp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
public final class HttpDnsErrorCode {
|
||||
private HttpDnsErrorCode() {
|
||||
}
|
||||
|
||||
public static final String NO_IP_AVAILABLE = "NO_IP_AVAILABLE";
|
||||
public static final String TLS_EMPTY_SNI_FAILED = "TLS_EMPTY_SNI_FAILED";
|
||||
public static final String HOST_ROUTE_REJECTED = "HOST_ROUTE_REJECTED";
|
||||
public static final String RESOLVE_SIGN_INVALID = "RESOLVE_SIGN_INVALID";
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package com.alibaba.sdk.android.httpdns.network;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class HttpDnsHttpAdapter {
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final HttpDnsService httpDnsService;
|
||||
private final HttpDnsAdapterOptions options;
|
||||
|
||||
public HttpDnsHttpAdapter(HttpDnsService httpDnsService) {
|
||||
this(httpDnsService, new HttpDnsAdapterOptions.Builder().build());
|
||||
}
|
||||
|
||||
public HttpDnsHttpAdapter(HttpDnsService httpDnsService, HttpDnsAdapterOptions options) {
|
||||
if (httpDnsService == null) {
|
||||
throw new IllegalArgumentException("httpDnsService should not be null");
|
||||
}
|
||||
this.httpDnsService = httpDnsService;
|
||||
this.options = options == null ? new HttpDnsAdapterOptions.Builder().build() : options;
|
||||
}
|
||||
|
||||
public HttpDnsAdapterResponse execute(HttpDnsAdapterRequest request) throws IOException {
|
||||
if (request == null || request.getUrl() == null || request.getUrl().trim().isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.HOST_ROUTE_REJECTED,
|
||||
"request or url is empty");
|
||||
}
|
||||
|
||||
URL originalURL = new URL(request.getUrl());
|
||||
String originalHost = originalURL.getHost();
|
||||
if (originalHost == null || originalHost.trim().isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.HOST_ROUTE_REJECTED,
|
||||
"invalid original host");
|
||||
}
|
||||
if (!"https".equalsIgnoreCase(originalURL.getProtocol())) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"only https scheme is supported in empty sni mode");
|
||||
}
|
||||
|
||||
List<String> candidateIps = resolveIps(originalHost);
|
||||
if (candidateIps.isEmpty()) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.NO_IP_AVAILABLE,
|
||||
"HTTPDNS returned no ip for host: " + originalHost);
|
||||
}
|
||||
|
||||
IOException lastException = null;
|
||||
for (String ip : candidateIps) {
|
||||
if (ip == null || ip.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String usedIp = ip.trim();
|
||||
HttpsURLConnection connection = null;
|
||||
try {
|
||||
int port = originalURL.getPort() > 0 ? originalURL.getPort() : 443;
|
||||
URL targetURL = new URL("https", usedIp, port, originalURL.getFile());
|
||||
connection = (HttpsURLConnection) targetURL.openConnection();
|
||||
connection.setConnectTimeout(options.getConnectTimeoutMillis());
|
||||
connection.setReadTimeout(options.getReadTimeoutMillis());
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod(request.getMethod());
|
||||
connection.setSSLSocketFactory(createSocketFactory());
|
||||
connection.setHostnameVerifier(createHostnameVerifier(originalHost));
|
||||
connection.setRequestProperty("Host", originalHost);
|
||||
for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
|
||||
if (entry.getKey() == null || entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
if ("host".equalsIgnoreCase(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
byte[] body = request.getBody();
|
||||
if (body != null && body.length > 0 && allowsRequestBody(request.getMethod())) {
|
||||
connection.setDoOutput(true);
|
||||
connection.getOutputStream().write(body);
|
||||
}
|
||||
|
||||
int code = connection.getResponseCode();
|
||||
byte[] payload = readResponseBytes(connection, code);
|
||||
Map<String, List<String>> headers = sanitizeHeaders(connection.getHeaderFields());
|
||||
return new HttpDnsAdapterResponse(code, headers, payload, usedIp);
|
||||
} catch (IOException e) {
|
||||
lastException = e;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastException == null) {
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"all ip attempts failed with no explicit exception");
|
||||
}
|
||||
|
||||
throw new HttpDnsAdapterException(HttpDnsErrorCode.TLS_EMPTY_SNI_FAILED,
|
||||
"all ip attempts failed", lastException);
|
||||
}
|
||||
|
||||
private List<String> resolveIps(String host) {
|
||||
RequestIpType type = options.getRequestIpType();
|
||||
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(host, type, null, null);
|
||||
if (result == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> ips = new ArrayList<>();
|
||||
String[] v4 = result.getIps();
|
||||
if (v4 != null) {
|
||||
for (String item : v4) {
|
||||
if (item != null && item.length() > 0) {
|
||||
ips.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
String[] v6 = result.getIpv6s();
|
||||
if (v6 != null) {
|
||||
for (String item : v6) {
|
||||
if (item != null && item.length() > 0) {
|
||||
ips.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
private byte[] readResponseBytes(HttpsURLConnection connection, int code) throws IOException {
|
||||
InputStream stream = code >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
||||
if (stream == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int count;
|
||||
while ((count = stream.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
stream.close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private Map<String, List<String>> sanitizeHeaders(Map<String, List<String>> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, List<String>> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : source.entrySet()) {
|
||||
if (entry.getKey() == null) {
|
||||
continue;
|
||||
}
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean allowsRequestBody(String method) {
|
||||
if (method == null) {
|
||||
return false;
|
||||
}
|
||||
String upper = method.toUpperCase();
|
||||
return "POST".equals(upper) || "PUT".equals(upper) || "PATCH".equals(upper) || "DELETE".equals(upper);
|
||||
}
|
||||
|
||||
private HostnameVerifier createHostnameVerifier(final String originalHost) {
|
||||
if (options.isAllowInsecureCertificatesForDebugOnly()) {
|
||||
return new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
|
||||
return new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return defaultVerifier.verify(originalHost, session);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private SSLSocketFactory createSocketFactory() throws IOException {
|
||||
try {
|
||||
SSLSocketFactory baseFactory;
|
||||
if (options.isAllowInsecureCertificatesForDebugOnly()) {
|
||||
baseFactory = trustAllSocketFactory();
|
||||
} else {
|
||||
baseFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
}
|
||||
return new NoSniSocketFactory(baseFactory);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("create ssl socket factory failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SSLSocketFactory trustAllSocketFactory() throws GeneralSecurityException {
|
||||
TrustManager[] managers = new TrustManager[]{new X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}};
|
||||
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, managers, new SecureRandom());
|
||||
return context.getSocketFactory();
|
||||
}
|
||||
|
||||
private static class NoSniSocketFactory extends SSLSocketFactory {
|
||||
private final SSLSocketFactory delegate;
|
||||
|
||||
private NoSniSocketFactory(SSLSocketFactory delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return delegate.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.Socket s, String host, int port, boolean autoClose)
|
||||
throws IOException {
|
||||
return sanitize(delegate.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(String host, int port) throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(String host, int port, java.net.InetAddress localHost, int localPort)
|
||||
throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.InetAddress host, int port) throws IOException {
|
||||
return sanitize(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.Socket createSocket(java.net.InetAddress address, int port, java.net.InetAddress localAddress,
|
||||
int localPort) throws IOException {
|
||||
return sanitize(delegate.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private java.net.Socket sanitize(java.net.Socket socket) {
|
||||
if (!(socket instanceof SSLSocket)) {
|
||||
return socket;
|
||||
}
|
||||
SSLSocket sslSocket = (SSLSocket) socket;
|
||||
try {
|
||||
SSLParameters params = sslSocket.getSSLParameters();
|
||||
try {
|
||||
java.lang.reflect.Method setServerNames = SSLParameters.class.getMethod("setServerNames", List.class);
|
||||
setServerNames.invoke(params, Collections.emptyList());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
sslSocket.setSSLParameters(params);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return sslSocket;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,7 @@ import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
|
||||
@@ -61,13 +59,7 @@ public class HttpRequest<T> {
|
||||
//设置通用UA
|
||||
conn.setRequestProperty("User-Agent", requestConfig.getUA());
|
||||
if (conn instanceof HttpsURLConnection) {
|
||||
((HttpsURLConnection)conn).setHostnameVerifier(new HostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return HttpsURLConnection.getDefaultHostnameVerifier().verify(
|
||||
HttpRequestConfig.HTTPS_CERTIFICATE_HOSTNAME, session);
|
||||
}
|
||||
});
|
||||
((HttpsURLConnection) conn).setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
|
||||
}
|
||||
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
in = conn.getErrorStream();
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.BuildConfig;
|
||||
import com.alibaba.sdk.android.httpdns.NetType;
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsConfig;
|
||||
import com.alibaba.sdk.android.httpdns.impl.SignService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestConfig;
|
||||
import com.alibaba.sdk.android.httpdns.track.SessionTrackMgr;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ResolveHostHelper {
|
||||
public static HttpRequestConfig getConfig(HttpDnsConfig config, String host,
|
||||
@@ -28,18 +25,18 @@ public class ResolveHostHelper {
|
||||
Map<String, String> globalParams,
|
||||
SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
HashMap<String, String> extraArgs = null;
|
||||
if (cacheKey != null) {
|
||||
extraArgs = new HashMap<>();
|
||||
HashMap<String, String> mergedExtras = null;
|
||||
if ((globalParams != null && !globalParams.isEmpty()) || (extras != null && !extras.isEmpty())) {
|
||||
mergedExtras = new HashMap<>();
|
||||
if (globalParams != null) {
|
||||
extraArgs.putAll(globalParams);
|
||||
mergedExtras.putAll(globalParams);
|
||||
}
|
||||
if (extras != null) {
|
||||
extraArgs.putAll(extras);
|
||||
mergedExtras.putAll(extras);
|
||||
}
|
||||
}
|
||||
|
||||
String path = getPath(config, host, type, extraArgs, signService, encryptService);
|
||||
String path = getPath(config, host, type, mergedExtras, signService, encryptService);
|
||||
HttpRequestConfig requestConfig = getHttpRequestConfig(config, path, signService.isSignMode());
|
||||
requestConfig.setUA(config.getUA());
|
||||
requestConfig.setAESEncryptService(encryptService);
|
||||
@@ -50,202 +47,62 @@ public class ResolveHostHelper {
|
||||
Map<String, String> extras,
|
||||
SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
//参数加密
|
||||
String enc = "";
|
||||
String query = getQuery(type);
|
||||
String version = "1.0";
|
||||
String tags = config.getBizTags();
|
||||
AESEncryptService.EncryptionMode mode = AESEncryptService.EncryptionMode.PLAIN;
|
||||
if (encryptService.isEncryptionMode()) {
|
||||
String encryptJson = buildEncryptionStr(host, query, extras, tags);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("encryptJson:" + encryptJson);
|
||||
}
|
||||
mode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? AESEncryptService.EncryptionMode.AES_GCM : AESEncryptService.EncryptionMode.AES_CBC;
|
||||
enc = encryptService.encrypt(encryptJson, mode);
|
||||
String qtype = getQType(type);
|
||||
StringBuilder query = new StringBuilder();
|
||||
appendQuery(query, "appId", config.getAccountId());
|
||||
appendQuery(query, "dn", host);
|
||||
appendQuery(query, "qtype", qtype);
|
||||
appendQuery(query, "sdk_version", BuildConfig.VERSION_NAME);
|
||||
appendQuery(query, "os", "android");
|
||||
String sid = SessionTrackMgr.getInstance().getSessionId();
|
||||
if (!TextUtils.isEmpty(sid)) {
|
||||
appendQuery(query, "sid", sid);
|
||||
}
|
||||
|
||||
String expireTime = signService.getExpireTime();
|
||||
|
||||
String queryStr = buildQueryStr(config.getAccountId(), mode.getMode(), host,
|
||||
query, extras, enc, expireTime, version, tags);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("query parameter:" + queryStr);
|
||||
}
|
||||
|
||||
//加签
|
||||
if (signService.isSignMode()) {
|
||||
Map<String, String> signParamMap = new HashMap<>();
|
||||
if (encryptService.isEncryptionMode()) {
|
||||
signParamMap.put("enc", enc);
|
||||
signParamMap.put("exp", expireTime);
|
||||
signParamMap.put("id", config.getAccountId());
|
||||
signParamMap.put("m", mode.getMode());
|
||||
signParamMap.put("v", version);
|
||||
}else {
|
||||
signParamMap.put("dn", host);
|
||||
signParamMap.put("exp", expireTime);
|
||||
signParamMap.put("id", config.getAccountId());
|
||||
signParamMap.put("m", mode.getMode());
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
signParamMap.put("q", query);
|
||||
}
|
||||
if (extras != null) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
signParamMap.put("sdns-" + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
signParamMap.put("tags", tags);
|
||||
}
|
||||
signParamMap.put("v", version);
|
||||
}
|
||||
|
||||
String sign = signService.sign(signParamMap);
|
||||
if (TextUtils.isEmpty(sign)) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("param sign fail");
|
||||
}
|
||||
}else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("sign:" + sign);
|
||||
}
|
||||
queryStr += "&s=" + sign;
|
||||
}
|
||||
}
|
||||
String path = "/v2/d?" + queryStr + "&sdk=android_" + BuildConfig.VERSION_NAME + getSid();
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("path:" + path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String buildQueryStr(String accountId, String mode, String host,
|
||||
String query, Map<String, String> extras, String enc,
|
||||
String expireTime, String version, String tags) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.append("id=").append(accountId);
|
||||
stringBuilder.append("&m=").append(mode);
|
||||
if (TextUtils.isEmpty(enc)) {
|
||||
stringBuilder.append("&dn=").append(host);
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
stringBuilder.append("&q=").append(query);
|
||||
}
|
||||
|
||||
String extra = getExtra(extras);
|
||||
if (!TextUtils.isEmpty(extra)) {
|
||||
stringBuilder.append(extra);
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
stringBuilder.append("&tags=").append(tags);
|
||||
}
|
||||
}else {
|
||||
stringBuilder.append("&enc=").append(enc);
|
||||
}
|
||||
stringBuilder.append("&v=").append(version);
|
||||
stringBuilder.append("&exp=").append(expireTime);
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private static String buildEncryptionStr(String host, String query, Map<String, String> extras, String tags) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("dn", host);
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
json.put("q", query);
|
||||
}
|
||||
if (!TextUtils.isEmpty(getExtra(extras))) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
if (!checkKey(entry.getKey()) || !checkValue(entry.getValue())) {
|
||||
continue;
|
||||
}
|
||||
json.put("sdns-" + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
json.put("tags", tags);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("encrypt param transfer to json fail.", e);
|
||||
}
|
||||
|
||||
}
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
private static String getQuery(RequestIpType type) {
|
||||
String query = "";
|
||||
switch (type) {
|
||||
case v6:
|
||||
query = "6";
|
||||
break;
|
||||
case both:
|
||||
query = "4,6";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private static String getExtra(Map<String, String> extras) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean isKey = true;
|
||||
boolean isValue = true;
|
||||
if (extras != null) {
|
||||
for (Map.Entry<String, String> entry : extras.entrySet()) {
|
||||
sb.append("&sdns-");
|
||||
sb.append(entry.getKey());
|
||||
sb.append("=");
|
||||
sb.append(entry.getValue());
|
||||
if (!checkKey(entry.getKey())) {
|
||||
isKey = false;
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义key不合法:" + entry.getKey());
|
||||
break;
|
||||
}
|
||||
if (!checkValue(entry.getValue())) {
|
||||
isValue = false;
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义value不合法:" + entry.getValue());
|
||||
break;
|
||||
}
|
||||
String cip = extras.get("cip");
|
||||
if (!TextUtils.isEmpty(cip)) {
|
||||
appendQuery(query, "cip", cip);
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
if (isKey && isValue) {
|
||||
String extra = sb.toString();
|
||||
if (extra.getBytes(StandardCharsets.UTF_8).length <= 1000) {
|
||||
return extra;
|
||||
} else {
|
||||
HttpDnsLog.e("设置自定义参数失败,自定义参数过长");
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
if (signService.isSignMode()) {
|
||||
String exp = signService.getExpireTime();
|
||||
String nonce = randomNonce();
|
||||
String sign = signService.signResolve(config.getAccountId(), host, qtype, exp, nonce);
|
||||
appendQuery(query, "exp", exp);
|
||||
appendQuery(query, "nonce", nonce);
|
||||
appendQuery(query, "sign", sign);
|
||||
}
|
||||
return "/resolve?" + query;
|
||||
}
|
||||
|
||||
private static boolean checkKey(String s) {
|
||||
return s.matches("[a-zA-Z0-9\\-_]+");
|
||||
private static void appendQuery(StringBuilder query, String key, String value) {
|
||||
if (TextUtils.isEmpty(key) || value == null) {
|
||||
return;
|
||||
}
|
||||
if (query.length() > 0) {
|
||||
query.append("&");
|
||||
}
|
||||
query.append(key).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static boolean checkValue(String s) {
|
||||
return s.matches("[a-zA-Z0-9\\-_=]+");
|
||||
private static String getQType(RequestIpType type) {
|
||||
if (type == RequestIpType.v6) {
|
||||
return "AAAA";
|
||||
}
|
||||
return "A";
|
||||
}
|
||||
|
||||
private static String randomNonce() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
public static HttpRequestConfig getConfig(HttpDnsConfig config, ArrayList<String> hostList,
|
||||
RequestIpType type, SignService signService,
|
||||
AESEncryptService encryptService) {
|
||||
//拼接host
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < hostList.size(); i++) {
|
||||
if (i != 0) {
|
||||
stringBuilder.append(",");
|
||||
}
|
||||
stringBuilder.append(hostList.get(i));
|
||||
String host = "";
|
||||
if (hostList != null && hostList.size() > 0) {
|
||||
host = hostList.get(0);
|
||||
}
|
||||
String host = stringBuilder.toString();
|
||||
String path = getPath(config, host, type, null, signService, encryptService);
|
||||
HttpRequestConfig requestConfig = getHttpRequestConfig(config, path, signService.isSignMode());
|
||||
requestConfig.setUA(config.getUA());
|
||||
@@ -268,14 +125,6 @@ public class ResolveHostHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTags(HttpDnsConfig config) {
|
||||
if (TextUtils.isEmpty(config.getBizTags())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "&tags=" + config.getBizTags();
|
||||
}
|
||||
|
||||
public static String getSid() {
|
||||
String sessionId = SessionTrackMgr.getInstance().getSessionId();
|
||||
if (sessionId == null) {
|
||||
@@ -284,4 +133,14 @@ public class ResolveHostHelper {
|
||||
return "&sid=" + sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容可观测模块透传 tags。
|
||||
*/
|
||||
public static String getTags(HttpDnsConfig config) {
|
||||
if (config == null || TextUtils.isEmpty(config.getBizTags())) {
|
||||
return "";
|
||||
}
|
||||
return "&tags=" + config.getBizTags();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.RequestIpType;
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsConfig;
|
||||
import com.alibaba.sdk.android.httpdns.impl.SignService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.BatchResolveHttpRequestStatusWatcher;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequest;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestConfig;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestTask;
|
||||
import com.alibaba.sdk.android.httpdns.request.HttpRequestWatcher;
|
||||
import com.alibaba.sdk.android.httpdns.request.RequestCallback;
|
||||
import com.alibaba.sdk.android.httpdns.request.RetryHttpRequest;
|
||||
import com.alibaba.sdk.android.httpdns.serverip.RegionServerScheduleService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 发起域名解析请求
|
||||
*/
|
||||
@@ -43,9 +38,18 @@ public class ResolveHostRequestHandler {
|
||||
public void requestResolveHost(final String host, final RequestIpType type,
|
||||
Map<String, String> extras, final String cacheKey,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
if (type == RequestIpType.both) {
|
||||
requestResolveHostBoth(host, extras, cacheKey, callback);
|
||||
return;
|
||||
}
|
||||
requestResolveHostSingle(host, type, extras, cacheKey, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostSingle(final String host, final RequestIpType type,
|
||||
Map<String, String> extras, final String cacheKey,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
HttpRequestConfig requestConfig = ResolveHostHelper.getConfig(mHttpDnsConfig, host, type,
|
||||
extras, cacheKey, mGlobalParams, mSignService, mAESEncryptService);
|
||||
//补充可观测数据
|
||||
requestConfig.setResolvingHost(host);
|
||||
requestConfig.setResolvingIpType(type);
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
@@ -54,30 +58,140 @@ public class ResolveHostRequestHandler {
|
||||
mCategoryController.getCategory().resolve(mHttpDnsConfig, requestConfig, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostBoth(final String host,
|
||||
final Map<String, String> extras,
|
||||
final String cacheKey,
|
||||
final RequestCallback<ResolveHostResponse> callback) {
|
||||
final ArrayList<ResolveHostResponse.HostItem> mergedItems = new ArrayList<>();
|
||||
final String[] serverIpHolder = new String[]{""};
|
||||
final Throwable[] errorHolder = new Throwable[]{null};
|
||||
|
||||
requestResolveHostSingle(host, RequestIpType.v4, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
requestResolveHostSingle(host, RequestIpType.v6, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (errorHolder[0] == null) {
|
||||
errorHolder[0] = throwable;
|
||||
}
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
errorHolder[0] = throwable;
|
||||
requestResolveHostSingle(host, RequestIpType.v6, extras, cacheKey, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
mergeResponse(resolveHostResponse, mergedItems, serverIpHolder);
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (errorHolder[0] == null) {
|
||||
errorHolder[0] = throwable;
|
||||
}
|
||||
finishBothResolve(callback, mergedItems, serverIpHolder[0], errorHolder[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void mergeResponse(ResolveHostResponse response,
|
||||
ArrayList<ResolveHostResponse.HostItem> mergedItems,
|
||||
String[] serverIpHolder) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
if (response.getItems() != null && !response.getItems().isEmpty()) {
|
||||
mergedItems.addAll(response.getItems());
|
||||
}
|
||||
if (serverIpHolder[0].isEmpty() && response.getServerIp() != null) {
|
||||
serverIpHolder[0] = response.getServerIp();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishBothResolve(RequestCallback<ResolveHostResponse> callback,
|
||||
ArrayList<ResolveHostResponse.HostItem> mergedItems,
|
||||
String serverIp,
|
||||
Throwable error) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
if (!mergedItems.isEmpty()) {
|
||||
callback.onSuccess(new ResolveHostResponse(mergedItems, serverIp));
|
||||
return;
|
||||
}
|
||||
if (error != null) {
|
||||
callback.onFail(error);
|
||||
return;
|
||||
}
|
||||
callback.onSuccess(new ResolveHostResponse(new ArrayList<ResolveHostResponse.HostItem>(), serverIp));
|
||||
}
|
||||
|
||||
public void requestResolveHost(final ArrayList<String> hostList, final RequestIpType type,
|
||||
RequestCallback<ResolveHostResponse> callback) {
|
||||
HttpRequestConfig requestConfig = ResolveHostHelper.getConfig(mHttpDnsConfig, hostList,
|
||||
type, mSignService, mAESEncryptService);
|
||||
requestConfig.setResolvingIpType(type);
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
if (hostList == null || hostList.isEmpty()) {
|
||||
callback.onSuccess(new ResolveHostResponse(new ArrayList<ResolveHostResponse.HostItem>(), ""));
|
||||
return;
|
||||
}
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("start resolve hosts async for " + hostList.toString() + " " + type);
|
||||
}
|
||||
|
||||
HttpRequest<ResolveHostResponse> request = new HttpRequest<>(
|
||||
requestConfig, new ResolveHostResponseParser(mAESEncryptService));
|
||||
request = new HttpRequestWatcher<>(request,
|
||||
new BatchResolveHttpRequestStatusWatcher(mHttpDnsConfig.getObservableManager()));
|
||||
// 切换服务IP,更新服务IP
|
||||
request = new HttpRequestWatcher<>(request, new ShiftServerWatcher(mHttpDnsConfig,
|
||||
mScheduleService, mCategoryController));
|
||||
// 重试一次
|
||||
request = new RetryHttpRequest<>(request, 1);
|
||||
try {
|
||||
mHttpDnsConfig.getResolveWorker().execute(
|
||||
new HttpRequestTask<>(request, callback));
|
||||
} catch (Throwable e) {
|
||||
callback.onFail(e);
|
||||
final ArrayList<ResolveHostResponse.HostItem> allItems = new ArrayList<>();
|
||||
final String[] serverIpHolder = new String[]{""};
|
||||
requestResolveHostsSequentially(hostList, 0, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
|
||||
private void requestResolveHostsSequentially(final ArrayList<String> hostList,
|
||||
final int index,
|
||||
final RequestIpType type,
|
||||
final ArrayList<ResolveHostResponse.HostItem> allItems,
|
||||
final String[] serverIpHolder,
|
||||
final RequestCallback<ResolveHostResponse> callback) {
|
||||
if (index >= hostList.size()) {
|
||||
callback.onSuccess(new ResolveHostResponse(allItems, serverIpHolder[0]));
|
||||
return;
|
||||
}
|
||||
final String host = hostList.get(index);
|
||||
requestResolveHost(host, type, null, null, new RequestCallback<ResolveHostResponse>() {
|
||||
@Override
|
||||
public void onSuccess(ResolveHostResponse resolveHostResponse) {
|
||||
if (resolveHostResponse != null) {
|
||||
if (resolveHostResponse.getItems() != null) {
|
||||
allItems.addAll(resolveHostResponse.getItems());
|
||||
}
|
||||
if (serverIpHolder[0].isEmpty()) {
|
||||
serverIpHolder[0] = resolveHostResponse.getServerIp();
|
||||
}
|
||||
}
|
||||
requestResolveHostsSequentially(hostList, index + 1, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(Throwable throwable) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.w("batch resolve host fail: " + host, throwable);
|
||||
}
|
||||
requestResolveHostsSequentially(hostList, index + 1, type, allItems, serverIpHolder, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,14 +202,14 @@ public class ResolveHostRequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置嗅探模式的请求时间间隔
|
||||
* 设置嗅探模式请求时间间隔
|
||||
*/
|
||||
public void setSniffTimeInterval(int timeInterval) {
|
||||
mCategoryController.setSniffTimeInterval(timeInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sdns的全局参数
|
||||
* 设置 SDNS 全局参数(当前服务端不使用,保留兼容)
|
||||
*/
|
||||
public void setSdnsGlobalParams(Map<String, String> params) {
|
||||
this.mGlobalParams.clear();
|
||||
@@ -105,10 +219,9 @@ public class ResolveHostRequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除sdns的全局参数
|
||||
* 清除 SDNS 全局参数
|
||||
*/
|
||||
public void clearSdnsGlobalParams() {
|
||||
this.mGlobalParams.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -122,78 +123,47 @@ public class ResolveHostResponse {
|
||||
}
|
||||
|
||||
public static ResolveHostResponse fromResponse(String serverIp, String body) throws JSONException {
|
||||
|
||||
ArrayList<HostItem> items = new ArrayList<>();
|
||||
JSONObject jsonObject = new JSONObject(body);
|
||||
|
||||
if (jsonObject.has("answers")) {
|
||||
JSONArray answers = jsonObject.getJSONArray("answers");
|
||||
for (int i = 0; i < answers.length(); i++) {
|
||||
JSONObject answer = answers.getJSONObject(i);
|
||||
String hostName = null;
|
||||
int ttl = 0;
|
||||
String extra = null;
|
||||
String[] ips = null;
|
||||
String[] ipsv6 = null;
|
||||
String noIpCode = null;
|
||||
if (answer.has("dn")) {
|
||||
hostName = answer.getString("dn");
|
||||
JSONObject responseObject = new JSONObject(body);
|
||||
JSONObject data = responseObject.optJSONObject("data");
|
||||
if (data == null) {
|
||||
return new ResolveHostResponse(items, serverIp);
|
||||
}
|
||||
String hostName = data.optString("domain");
|
||||
int ttl = data.optInt("ttl", 60);
|
||||
String summary = data.optString("summary", null);
|
||||
JSONArray records = data.optJSONArray("records");
|
||||
HashMap<RequestIpType, ArrayList<String>> typedIPs = new HashMap<>();
|
||||
if (records != null) {
|
||||
for (int i = 0; i < records.length(); i++) {
|
||||
JSONObject record = records.optJSONObject(i);
|
||||
if (record == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (answer.has("v4")) {
|
||||
JSONObject ipv4 = answer.getJSONObject("v4");
|
||||
if (ipv4.has("ips")) {
|
||||
JSONArray ipArray = ipv4.getJSONArray("ips");
|
||||
if (ipArray.length() != 0) {
|
||||
ips = new String[ipArray.length()];
|
||||
for (int j = 0; j < ipArray.length(); j++) {
|
||||
ips[j] = ipArray.getString(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipv4.has("ttl")) {
|
||||
ttl = ipv4.getInt("ttl");
|
||||
}
|
||||
if (ipv4.has("extra")) {
|
||||
extra = ipv4.getString("extra");
|
||||
}
|
||||
if (ipv4.has("no_ip_code")) {
|
||||
noIpCode = ipv4.getString("no_ip_code");
|
||||
}
|
||||
items.add(new HostItem(hostName, RequestIpType.v4, ips, ttl, extra, noIpCode));
|
||||
if (!TextUtils.isEmpty(noIpCode)) {
|
||||
HttpDnsLog.w("RESOLVE FAIL, HOST:" + hostName + ", QUERY:4, "
|
||||
+ "Msg:" + noIpCode);
|
||||
}
|
||||
String ip = record.optString("ip");
|
||||
if (TextUtils.isEmpty(ip)) {
|
||||
continue;
|
||||
}
|
||||
if (answer.has("v6")) {
|
||||
JSONObject ipv6 = answer.getJSONObject("v6");
|
||||
if (ipv6.has("ips")) {
|
||||
JSONArray ipArray = ipv6.getJSONArray("ips");
|
||||
if (ipArray.length() != 0) {
|
||||
ipsv6 = new String[ipArray.length()];
|
||||
for (int j = 0; j < ipArray.length(); j++) {
|
||||
ipsv6[j] = ipArray.getString(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipv6.has("ttl")) {
|
||||
ttl = ipv6.getInt("ttl");
|
||||
}
|
||||
if (ipv6.has("extra")) {
|
||||
extra = ipv6.getString("extra");
|
||||
}
|
||||
if (ipv6.has("no_ip_code")) {
|
||||
noIpCode = ipv6.getString("no_ip_code");
|
||||
}
|
||||
items.add(new HostItem(hostName, RequestIpType.v6, ipsv6, ttl, extra, noIpCode));
|
||||
if (!TextUtils.isEmpty(noIpCode)) {
|
||||
HttpDnsLog.w("RESOLVE FAIL, HOST:" + hostName + ", QUERY:6, "
|
||||
+ "Msg:" + noIpCode);
|
||||
}
|
||||
String recordType = record.optString("type", data.optString("qtype", "A")).toUpperCase();
|
||||
RequestIpType ipType = "AAAA".equals(recordType) ? RequestIpType.v6 : RequestIpType.v4;
|
||||
ArrayList<String> list = typedIPs.get(ipType);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
typedIPs.put(ipType, list);
|
||||
}
|
||||
list.add(ip);
|
||||
}
|
||||
}
|
||||
if (typedIPs.isEmpty()) {
|
||||
String qtype = data.optString("qtype", "A");
|
||||
RequestIpType ipType = "AAAA".equalsIgnoreCase(qtype) ? RequestIpType.v6 : RequestIpType.v4;
|
||||
items.add(new HostItem(hostName, ipType, null, ttl, summary, null));
|
||||
} else {
|
||||
for (RequestIpType type : typedIPs.keySet()) {
|
||||
ArrayList<String> ipList = typedIPs.get(type);
|
||||
String[] ips = ipList == null ? null : ipList.toArray(new String[0]);
|
||||
items.add(new HostItem(hostName, type, ips, ttl, summary, null));
|
||||
}
|
||||
|
||||
}
|
||||
return new ResolveHostResponse(items, serverIp);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,27 @@
|
||||
package com.alibaba.sdk.android.httpdns.resolve;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.AESEncryptService;
|
||||
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
|
||||
import com.alibaba.sdk.android.httpdns.request.ResponseParser;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class ResolveHostResponseParser implements
|
||||
ResponseParser<ResolveHostResponse> {
|
||||
|
||||
private final AESEncryptService mAESEncryptService;
|
||||
public class ResolveHostResponseParser implements ResponseParser<ResolveHostResponse> {
|
||||
|
||||
public ResolveHostResponseParser(AESEncryptService aesEncryptService) {
|
||||
mAESEncryptService = aesEncryptService;
|
||||
// 新版协议固定 HTTPS,不使用 AES 响应体解密,保留构造参数仅为兼容调用方。
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolveHostResponse parse(String serverIp, String response) throws Throwable {
|
||||
String data = "";
|
||||
JSONObject jsonResponse = new JSONObject(response);
|
||||
if (jsonResponse.has("code")) {
|
||||
String code = jsonResponse.getString("code");
|
||||
if (TextUtils.equals(code, "success")) {
|
||||
if (jsonResponse.has("data")) {
|
||||
data = jsonResponse.getString("data");
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
//解密
|
||||
AESEncryptService.EncryptionMode mode = AESEncryptService.EncryptionMode.PLAIN;
|
||||
if (jsonResponse.has("mode")) {
|
||||
String serverEncryptMode = jsonResponse.getString("mode");
|
||||
if (TextUtils.equals(serverEncryptMode, AESEncryptService.EncryptionMode.AES_GCM.getMode())) {
|
||||
mode = AESEncryptService.EncryptionMode.AES_GCM;
|
||||
} else if (TextUtils.equals(serverEncryptMode, AESEncryptService.EncryptionMode.AES_CBC.getMode())) {
|
||||
mode = AESEncryptService.EncryptionMode.AES_CBC;
|
||||
}
|
||||
}
|
||||
data = mAESEncryptService.decrypt(data, mode);
|
||||
if (TextUtils.isEmpty(data)) {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response data decrypt fail");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response data is empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("解析失败,原因为" + code);
|
||||
}
|
||||
throw new Exception(code);
|
||||
}
|
||||
}else {
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.e("response don't have code");
|
||||
JSONObject responseObject = new JSONObject(response);
|
||||
String code = responseObject.optString("code");
|
||||
if (!"SUCCESS".equalsIgnoreCase(code)) {
|
||||
String message = responseObject.optString("message");
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
throw new Exception(code + ":" + message);
|
||||
}
|
||||
if (HttpDnsLog.isPrint()) {
|
||||
HttpDnsLog.d("request success " + data);
|
||||
}
|
||||
return ResolveHostResponse.fromResponse(serverIp, data);
|
||||
return ResolveHostResponse.fromResponse(serverIp, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.impl.HttpDnsInstanceHolder;
|
||||
import com.alibaba.sdk.android.httpdns.impl.InstanceCreator;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsAdapterOptions;
|
||||
import com.alibaba.sdk.android.httpdns.network.HttpDnsHttpAdapter;
|
||||
import com.alibaba.sdk.android.httpdns.utils.CommonUtil;
|
||||
|
||||
/**
|
||||
@@ -74,6 +76,20 @@ public class HttpDns {
|
||||
InitConfig.addConfig(accountId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service) {
|
||||
return new HttpDnsHttpAdapter(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build official IP-direct + empty-SNI adapter with options.
|
||||
*/
|
||||
public static HttpDnsHttpAdapter buildHttpClientAdapter(HttpDnsService service, HttpDnsAdapterOptions options) {
|
||||
return new HttpDnsHttpAdapter(service, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用或者禁用httpdns,理论上这个是内部接口,不给外部使用的
|
||||
* 但是已经对外暴露,所以保留
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ext {
|
||||
httpdnsDebugVersion = '2.6.7'
|
||||
httpdnsDebugVersion = '1.0.0'
|
||||
|
||||
loggerVersion = '1.2.0'
|
||||
crashDefendVersion = '0.0.6'
|
||||
utdidVersion = '2.6.0'
|
||||
ipdetectorVersion = '1.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
149
EdgeHttpDNS/sdk/build_packages.sh
Normal file
149
EdgeHttpDNS/sdk/build_packages.sh
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
DIST_DIR="$ROOT/dist"
|
||||
TMP_DIR="$ROOT/.tmp_sdk_pkg"
|
||||
|
||||
function lookup_version() {
|
||||
VERSION_FILE="$ROOT/../internal/const/const.go"
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "0.0.0"
|
||||
return
|
||||
fi
|
||||
VERSION=$(grep -E 'Version[[:space:]]*=' "$VERSION_FILE" | head -n 1 | sed -E 's/.*"([0-9.]+)".*/\1/')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "0.0.0"
|
||||
else
|
||||
echo "$VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_cmd() {
|
||||
CMD=$1
|
||||
if ! command -v "$CMD" >/dev/null 2>&1; then
|
||||
echo "missing command: $CMD"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function zip_dir() {
|
||||
SRC_DIR=$1
|
||||
ZIP_FILE=$2
|
||||
(
|
||||
cd "$SRC_DIR" || exit 1
|
||||
zip -r -X -q "$ZIP_FILE" .
|
||||
)
|
||||
}
|
||||
|
||||
function build_android_sdk_package() {
|
||||
echo "[sdk] building android aar ..."
|
||||
ensure_cmd zip
|
||||
if [ ! -x "$ROOT/android/gradlew" ]; then
|
||||
echo "android gradlew not found: $ROOT/android/gradlew"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$ROOT/android" || exit 1
|
||||
./gradlew :httpdns-sdk:clean :httpdns-sdk:assembleNormalRelease
|
||||
)
|
||||
|
||||
AAR_FILE=$(find "$ROOT/android/httpdns-sdk/build/outputs/aar" -type f -name "*normal-release*.aar" | head -n 1)
|
||||
if [ -z "$AAR_FILE" ]; then
|
||||
AAR_FILE=$(find "$ROOT/android/httpdns-sdk/build/outputs/aar" -type f -name "*release*.aar" | head -n 1)
|
||||
fi
|
||||
if [ -z "$AAR_FILE" ] || [ ! -f "$AAR_FILE" ]; then
|
||||
echo "android aar is not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/android"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp "$AAR_FILE" "$PKG_DIR/alicloud-android-httpdns.aar"
|
||||
if [ -f "$ROOT/android/README.md" ]; then
|
||||
cp "$ROOT/android/README.md" "$PKG_DIR/README.md"
|
||||
fi
|
||||
if [ -f "$ROOT/android/httpdns-sdk/proguard-rules.pro" ]; then
|
||||
cp "$ROOT/android/httpdns-sdk/proguard-rules.pro" "$PKG_DIR/proguard-rules.pro"
|
||||
fi
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-android.zip"
|
||||
}
|
||||
|
||||
function build_ios_sdk_package() {
|
||||
echo "[sdk] packaging ios xcframework ..."
|
||||
ensure_cmd zip
|
||||
|
||||
CANDIDATES=(
|
||||
"$ROOT/ios/dist/AlicloudHttpDNS.xcframework"
|
||||
"$ROOT/ios/AlicloudHttpDNS.xcframework"
|
||||
"$ROOT/ios/Build/AlicloudHttpDNS.xcframework"
|
||||
)
|
||||
|
||||
XCFRAMEWORK_DIR=""
|
||||
for path in "${CANDIDATES[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
XCFRAMEWORK_DIR="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$XCFRAMEWORK_DIR" ]; then
|
||||
echo "ios xcframework not found."
|
||||
echo "please build it on macOS first, then place AlicloudHttpDNS.xcframework under EdgeHttpDNS/sdk/ios/dist/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/ios"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp -R "$XCFRAMEWORK_DIR" "$PKG_DIR/"
|
||||
if [ -f "$ROOT/ios/AlicloudHTTPDNS.podspec" ]; then
|
||||
cp "$ROOT/ios/AlicloudHTTPDNS.podspec" "$PKG_DIR/"
|
||||
fi
|
||||
if [ -f "$ROOT/ios/README.md" ]; then
|
||||
cp "$ROOT/ios/README.md" "$PKG_DIR/README.md"
|
||||
fi
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-ios.zip"
|
||||
}
|
||||
|
||||
function build_flutter_sdk_package() {
|
||||
echo "[sdk] packaging flutter plugin ..."
|
||||
ensure_cmd zip
|
||||
PLUGIN_DIR="$ROOT/flutter/aliyun_httpdns"
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
echo "flutter plugin directory not found: $PLUGIN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_DIR="$TMP_DIR/flutter"
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR"
|
||||
cp -R "$PLUGIN_DIR" "$PKG_DIR/"
|
||||
rm -rf "$PKG_DIR/aliyun_httpdns/example/.dart_tool" "$PKG_DIR/aliyun_httpdns/example/build" "$PKG_DIR/aliyun_httpdns/.dart_tool" "$PKG_DIR/aliyun_httpdns/build"
|
||||
|
||||
zip_dir "$PKG_DIR" "$DIST_DIR/httpdns-sdk-flutter.zip"
|
||||
}
|
||||
|
||||
function main() {
|
||||
VERSION=$(lookup_version)
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR" "$DIST_DIR"
|
||||
|
||||
build_android_sdk_package
|
||||
build_ios_sdk_package
|
||||
build_flutter_sdk_package
|
||||
|
||||
cp "$DIST_DIR/httpdns-sdk-android.zip" "$DIST_DIR/httpdns-sdk-android-v${VERSION}.zip"
|
||||
cp "$DIST_DIR/httpdns-sdk-ios.zip" "$DIST_DIR/httpdns-sdk-ios-v${VERSION}.zip"
|
||||
cp "$DIST_DIR/httpdns-sdk-flutter.zip" "$DIST_DIR/httpdns-sdk-flutter-v${VERSION}.zip"
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
echo "[sdk] done. output: $DIST_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -1,532 +1,73 @@
|
||||
# Aliyun HTTPDNS Flutter Plugin
|
||||
# HTTPDNS Flutter SDK (SNI Hidden v1.0.0)
|
||||
|
||||
阿里云EMAS HTTPDNS Flutter插件,提供基于原生SDK的域名解析能力。
|
||||
|
||||
一、快速入门
|
||||
-----------------------
|
||||
|
||||
### 1.1 开通服务
|
||||
|
||||
请参考[快速入门文档](https://help.aliyun.com/document_detail/2867674.html)开通HTTPDNS。
|
||||
|
||||
### 1.2 获取配置
|
||||
|
||||
请参考开发配置文档在EMAS控制台开发配置中获取AccountId/SecretKey/AESSecretKey等信息,用于初始化SDK。
|
||||
|
||||
## 二、安装
|
||||
|
||||
在`pubspec.yaml`中加入dependencies:
|
||||
```yaml
|
||||
dependencies:
|
||||
aliyun_httpdns: ^1.0.1
|
||||
```
|
||||
添加依赖之后需要执行一次 `flutter pub get`。
|
||||
|
||||
### 原生SDK版本说明
|
||||
|
||||
插件已集成了对应平台的HTTPDNS原生SDK,当前版本:
|
||||
|
||||
- **Android**: `com.aliyun.ams:alicloud-android-httpdns:2.6.7`
|
||||
- **iOS**: `AlicloudHTTPDNS:3.3.0`
|
||||
|
||||
|
||||
|
||||
三、配置和使用
|
||||
------------------------
|
||||
|
||||
### 3.1 初始化配置
|
||||
|
||||
应用启动后,需要先初始化插件,才能调用HTTPDNS能力。
|
||||
初始化主要是配置AccountId/SecretKey等信息及功能开关。
|
||||
示例代码如下:
|
||||
## 1. Initialization
|
||||
|
||||
```dart
|
||||
// 初始化 HTTPDNS
|
||||
await AliyunHttpdns.init(
|
||||
accountId: '您的AccountId',
|
||||
secretKey: '您的SecretKey',
|
||||
);
|
||||
|
||||
// 设置功能选项
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
|
||||
// 构建服务
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
// 设置预解析域名
|
||||
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
|
||||
print("init success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.1.1 日志配置
|
||||
|
||||
应用开发过程中,如果要输出HTTPDNS的日志,可以调用日志输出控制方法,开启日志,示例代码如下:
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
print("enableLog success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.1.2 sessionId记录
|
||||
|
||||
应用在运行过程中,可以调用获取SessionId方法获取sessionId,记录到应用的数据采集系统中。
|
||||
sessionId用于表示标识一次应用运行,线上排查时,可以用于查询应用一次运行过程中的解析日志,示例代码如下:
|
||||
|
||||
```dart
|
||||
final sessionId = await AliyunHttpdns.getSessionId();
|
||||
print("SessionId = $sessionId");
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3.2 域名解析
|
||||
|
||||
#### 3.2.1 预解析
|
||||
|
||||
当需要提前解析域名时,可以调用预解析域名方法,示例代码如下:
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveHosts(["www.aliyun.com", "www.example.com"], ipType: 'both');
|
||||
print("preResolveHosts success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
调用之后,插件会发起域名解析,并把结果缓存到内存,用于后续请求时直接使用。
|
||||
|
||||
### 3.2.2 域名解析
|
||||
|
||||
当需要解析域名时,可以通过调用域名解析方法解析域名获取IP,示例代码如下:
|
||||
|
||||
```dart
|
||||
Future<void> _resolve() async {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking('www.aliyun.com', ipType: 'both');
|
||||
final ipv4List = res['ipv4'] ?? [];
|
||||
final ipv6List = res['ipv6'] ?? [];
|
||||
print('IPv4: $ipv4List');
|
||||
print('IPv6: $ipv6List');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
四、Flutter最佳实践
|
||||
------------------------------
|
||||
|
||||
### 4.1 原理说明
|
||||
|
||||
本示例展示了一种更直接的集成方式,通过自定义HTTP客户端适配器来实现HTTPDNS集成:
|
||||
|
||||
1. 创建自定义的HTTP客户端适配器,拦截网络请求
|
||||
2. 在适配器中调用HTTPDNS插件解析域名为IP地址
|
||||
3. 使用解析得到的IP地址创建直接的Socket连接
|
||||
4. 对于HTTPS连接,确保正确设置SNI(Server Name Indication)为原始域名
|
||||
|
||||
这种方式避免了创建本地代理服务的复杂性,直接在HTTP客户端层面集成HTTPDNS功能。
|
||||
|
||||
### 4.2 示例说明
|
||||
|
||||
完整应用示例请参考插件包中example应用。
|
||||
|
||||
#### 4.2.1 自定义HTTP客户端适配器实现
|
||||
|
||||
自定义适配器的实现请参考插件包中example/lib/net/httpdns_http_client_adapter.dart文件。本方案由EMAS团队设计实现,参考请注明出处。
|
||||
适配器内部会拦截HTTP请求,调用HTTPDNS进行域名解析,并使用解析后的IP创建socket连接。
|
||||
|
||||
本示例支持三种网络库:Dio、HttpClient、http包。代码如下:
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:aliyun_httpdns/aliyun_httpdns.dart';
|
||||
|
||||
// Dio 适配器
|
||||
IOHttpClientAdapter buildHttpdnsHttpClientAdapter() {
|
||||
final HttpClient client = HttpClient();
|
||||
_configureHttpClient(client);
|
||||
_configureConnectionFactory(client);
|
||||
|
||||
final IOHttpClientAdapter adapter = IOHttpClientAdapter(createHttpClient: () => client)
|
||||
..validateCertificate = (cert, host, port) => true;
|
||||
return adapter;
|
||||
}
|
||||
|
||||
// 原生 HttpClient
|
||||
HttpClient buildHttpdnsNativeHttpClient() {
|
||||
final HttpClient client = HttpClient();
|
||||
_configureHttpClient(client);
|
||||
_configureConnectionFactory(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
// http 包适配器
|
||||
http.Client buildHttpdnsHttpPackageClient() {
|
||||
final HttpClient httpClient = buildHttpdnsNativeHttpClient();
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
// HttpClient 基础配置
|
||||
void _configureHttpClient(HttpClient client) {
|
||||
client.findProxy = (Uri _) => 'DIRECT';
|
||||
client.idleTimeout = const Duration(seconds: 90);
|
||||
client.maxConnectionsPerHost = 8;
|
||||
}
|
||||
|
||||
// 配置基于 HTTPDNS 的连接工厂
|
||||
// 本方案由EMAS团队设计实现,参考请注明出处。
|
||||
void _configureConnectionFactory(HttpClient client) {
|
||||
client.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final String domain = uri.host;
|
||||
final bool https = uri.scheme.toLowerCase() == 'https';
|
||||
final int port = uri.port == 0 ? (https ? 443 : 80) : uri.port;
|
||||
|
||||
final List<InternetAddress> targets = await _resolveTargets(domain);
|
||||
final Object target = targets.isNotEmpty ? targets.first : domain;
|
||||
|
||||
if (!https) {
|
||||
return Socket.startConnect(target, port);
|
||||
}
|
||||
|
||||
// HTTPS:先 TCP,再 TLS(SNI=域名),并保持可取消
|
||||
bool cancelled = false;
|
||||
final Future<ConnectionTask<Socket>> rawStart = Socket.startConnect(target, port);
|
||||
final Future<Socket> upgraded = rawStart.then((task) async {
|
||||
final Socket raw = await task.socket;
|
||||
if (cancelled) {
|
||||
raw.destroy();
|
||||
throw const SocketException('Connection cancelled');
|
||||
}
|
||||
final SecureSocket secure = await SecureSocket.secure(
|
||||
raw,
|
||||
host: domain, // 重要:使用原始域名作为SNI
|
||||
);
|
||||
if (cancelled) {
|
||||
secure.destroy();
|
||||
throw const SocketException('Connection cancelled');
|
||||
}
|
||||
return secure;
|
||||
});
|
||||
return ConnectionTask.fromSocket(
|
||||
upgraded,
|
||||
() {
|
||||
cancelled = true;
|
||||
try {
|
||||
rawStart.then((t) => t.cancel());
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 通过 HTTPDNS 解析目标 IP 列表
|
||||
Future<List<InternetAddress>> _resolveTargets(String domain) async {
|
||||
try {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(domain, ipType: 'both');
|
||||
final List<String> ipv4 = res['ipv4'] ?? [];
|
||||
final List<String> ipv6 = res['ipv6'] ?? [];
|
||||
final List<InternetAddress> targets = [
|
||||
...ipv4.map(InternetAddress.tryParse).whereType<InternetAddress>(),
|
||||
...ipv6.map(InternetAddress.tryParse).whereType<InternetAddress>(),
|
||||
];
|
||||
if (targets.isEmpty) {
|
||||
debugPrint('[dio] HTTPDNS no result for $domain, fallback to system DNS');
|
||||
} else {
|
||||
debugPrint('[dio] HTTPDNS resolved $domain -> ${targets.first.address}');
|
||||
}
|
||||
return targets;
|
||||
} catch (e) {
|
||||
debugPrint('[dio] HTTPDNS resolve failed: $e, fallback to system DNS');
|
||||
return const <InternetAddress>[];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 4.2.2 适配器集成和使用
|
||||
|
||||
适配器的集成请参考插件包中example/lib/main.dart文件。
|
||||
首先需要初始化HTTPDNS,然后配置网络库使用自定义适配器,示例代码如下:
|
||||
|
||||
```dart
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
late final Dio _dio;
|
||||
late final HttpClient _httpClient;
|
||||
late final http.Client _httpPackageClient;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 初始化 HTTPDNS
|
||||
_initHttpDnsOnce();
|
||||
|
||||
// 配置网络库使用 HTTPDNS 适配器
|
||||
_dio = Dio();
|
||||
_dio.httpClientAdapter = buildHttpdnsHttpClientAdapter();
|
||||
_dio.options.headers['Connection'] = 'keep-alive';
|
||||
|
||||
_httpClient = buildHttpdnsNativeHttpClient();
|
||||
_httpPackageClient = buildHttpdnsHttpPackageClient();
|
||||
}
|
||||
|
||||
Future<void> _initHttpDnsOnce() async {
|
||||
try {
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000,
|
||||
secretKey: '您的SecretKey',
|
||||
);
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
// 设置预解析域名
|
||||
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
|
||||
} catch (e) {
|
||||
debugPrint('[httpdns] init failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
使用配置好的网络库发起请求时,会自动使用HTTPDNS进行域名解析:
|
||||
|
||||
```dart
|
||||
// 使用 Dio
|
||||
final response = await _dio.get('https://www.aliyun.com');
|
||||
|
||||
// 使用 HttpClient
|
||||
final request = await _httpClient.getUrl(Uri.parse('https://www.aliyun.com'));
|
||||
final response = await request.close();
|
||||
|
||||
// 使用 http 包
|
||||
final response = await _httpPackageClient.get(Uri.parse('https://www.aliyun.com'));
|
||||
```
|
||||
|
||||
#### 4.2.3 资源清理
|
||||
|
||||
在组件销毁时,记得清理相关资源:
|
||||
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_urlController.dispose();
|
||||
_httpClient.close();
|
||||
_httpPackageClient.close();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
五、API
|
||||
----------------------
|
||||
|
||||
### 5.1 日志输出控制
|
||||
|
||||
控制是否打印Log。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
print("enableLog success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 5.2 初始化
|
||||
|
||||
初始化配置, 在应用启动时调用。
|
||||
|
||||
```dart
|
||||
// 基础初始化
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000,
|
||||
secretKey: 'your_secret_key',
|
||||
aesSecretKey: 'your_aes_secret_key', // 可选
|
||||
appId: 'app1f1ndpo9',
|
||||
primaryServiceHost: 'httpdns-a.example.com',
|
||||
backupServiceHost: 'httpdns-b.example.com', // optional
|
||||
servicePort: 443,
|
||||
secretKey: 'your-sign-secret', // optional if sign is enabled
|
||||
);
|
||||
|
||||
// 配置功能选项
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
|
||||
|
||||
// 构建服务实例
|
||||
await AliyunHttpdns.build();
|
||||
|
||||
print("init success");
|
||||
```
|
||||
|
||||
|
||||
|
||||
初始化参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 | 支持平台 |
|
||||
|-------------|--------|------|------------|-------------|
|
||||
| accountId | int | 必选参数 | Account ID | Android/iOS |
|
||||
| secretKey | String | 可选参数 | 加签密钥 | Android/iOS |
|
||||
| aesSecretKey| String | 可选参数 | 加密密钥 | Android/iOS |
|
||||
|
||||
功能配置方法:
|
||||
|
||||
- `setHttpsRequestEnabled(bool)` - 设置是否使用HTTPS解析链路
|
||||
- `setLogEnabled(bool)` - 设置是否开启日志
|
||||
- `setPersistentCacheIPEnabled(bool)` - 设置是否开启持久化缓存
|
||||
- `setReuseExpiredIPEnabled(bool)` - 设置是否允许复用过期IP
|
||||
- `setPreResolveAfterNetworkChanged(bool)` - 设置网络切换时是否自动刷新解析
|
||||
|
||||
|
||||
|
||||
### 5.3 域名解析
|
||||
|
||||
解析指定域名。
|
||||
## 2. Resolve
|
||||
|
||||
```dart
|
||||
Future<void> _resolve() async {
|
||||
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
'www.aliyun.com',
|
||||
ipType: 'both', // 'auto', 'ipv4', 'ipv6', 'both'
|
||||
);
|
||||
|
||||
final ipv4List = res['ipv4'] ?? [];
|
||||
final ipv6List = res['ipv6'] ?? [];
|
||||
print('IPv4: $ipv4List');
|
||||
print('IPv6: $ipv6List');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|------------|---------------------|------|----------------------------------------|
|
||||
| hostname | String | 必选参数 | 要解析的域名 |
|
||||
| ipType | String | 可选参数 | 请求IP类型: 'auto', 'ipv4', 'ipv6', 'both' |
|
||||
|
||||
|
||||
|
||||
返回数据结构:
|
||||
|
||||
| 字段名 | 类型 | 功能 |
|
||||
|------|--------------|----------------------------------|
|
||||
| ipv4 | List<String> | IPv4地址列表,如: ["1.1.1.1", "2.2.2.2"] |
|
||||
| ipv6 | List<String> | IPv6地址列表,如: ["::1", "::2"] |
|
||||
|
||||
|
||||
|
||||
### 5.4 预解析域名
|
||||
|
||||
预解析域名, 解析后缓存在SDK中,下次解析时直接从缓存中获取,提高解析速度。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveHosts(
|
||||
["www.aliyun.com", "www.example.com"],
|
||||
ipType: 'both'
|
||||
final result = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
'api.business.com',
|
||||
ipType: 'both',
|
||||
);
|
||||
print("preResolveHosts success");
|
||||
|
||||
final ipv4 = result['ipv4'] ?? <String>[];
|
||||
final ipv6 = result['ipv6'] ?? <String>[];
|
||||
```
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|--------|--------------|------|----------------------------------------|
|
||||
| hosts | List<String> | 必选参数 | 预解析域名列表 |
|
||||
| ipType | String | 可选参数 | 请求IP类型: 'auto', 'ipv4', 'ipv6', 'both' |
|
||||
|
||||
|
||||
|
||||
### 5.5 获取SessionId
|
||||
|
||||
获取SessionId, 用于排查追踪问题。
|
||||
## 3. Official HTTP Adapter (IP + Host)
|
||||
|
||||
```dart
|
||||
final sessionId = await AliyunHttpdns.getSessionId();
|
||||
print("SessionId = $sessionId");
|
||||
```
|
||||
|
||||
|
||||
|
||||
无需参数,直接返回当前会话ID。
|
||||
|
||||
### 5.6 清除缓存
|
||||
|
||||
清除所有DNS解析缓存。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.cleanAllHostCache();
|
||||
print("缓存清除成功");
|
||||
```
|
||||
|
||||
### 5.7 持久化缓存配置
|
||||
|
||||
设置是否开启持久化缓存功能。开启后,SDK 会将解析结果保存到本地,App 重启后可以从本地加载缓存,提升首屏加载速度。
|
||||
|
||||
```dart
|
||||
// 基础用法:开启持久化缓存
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
|
||||
|
||||
// 高级用法:开启持久化缓存并设置过期时间阈值
|
||||
await AliyunHttpdns.setPersistentCacheIPEnabled(
|
||||
true,
|
||||
discardExpiredAfterSeconds: 86400 // 1天,单位:秒
|
||||
final adapter = AliyunHttpdns.createHttpAdapter(
|
||||
options: const AliyunHttpdnsAdapterOptions(
|
||||
ipType: 'auto',
|
||||
connectTimeoutMs: 3000,
|
||||
readTimeoutMs: 5000,
|
||||
allowInsecureCertificatesForDebugOnly: false,
|
||||
),
|
||||
);
|
||||
print("持久化缓存已开启");
|
||||
|
||||
final resp = await adapter.request(
|
||||
Uri.parse('https://api.business.com/v1/ping'),
|
||||
method: 'GET',
|
||||
headers: {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
print(resp.statusCode);
|
||||
print(resp.usedIp);
|
||||
```
|
||||
|
||||
参数:
|
||||
Behavior is fixed:
|
||||
- Resolve host by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as the real business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|----------------------------|------|------|------------------------------------------|
|
||||
| enabled | bool | 必选参数 | 是否开启持久化缓存 |
|
||||
| discardExpiredAfterSeconds | int | 可选参数 | 过期时间阈值(秒),App 启动时会丢弃过期超过此时长的缓存记录,建议设置为 1 天(86400 秒) |
|
||||
|
||||
注意事项:
|
||||
- 持久化缓存仅影响第一次域名解析结果,后续解析仍会请求 HTTPDNS 服务器
|
||||
- 如果业务服务器 IP 变化频繁,建议谨慎开启此功能
|
||||
- 建议在 `build()` 之前调用此接口
|
||||
|
||||
### 5.8 网络变化时自动刷新预解析
|
||||
|
||||
设置在网络环境变化时是否自动刷新预解析域名的缓存。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true);
|
||||
print("网络变化自动刷新已启用");
|
||||
```
|
||||
|
||||
### 5.9 IP 优选
|
||||
|
||||
设置需要进行 IP 优选的域名列表。开启后,SDK 会对解析返回的 IP 列表进行 TCP 测速并排序,优先返回连接速度最快的 IP。
|
||||
|
||||
```dart
|
||||
await AliyunHttpdns.setIPRankingList({
|
||||
'www.aliyun.com': 443,
|
||||
});
|
||||
print("IP 优选配置成功");
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数名 | 类型 | 是否必须 | 功能 |
|
||||
|-------------|-----------------|------|------------------------------|
|
||||
| hostPortMap | Map<String, int> | 必选参数 | 域名和端口的映射,例如:{'www.aliyun.com': 443} |
|
||||
## 4. Public Errors
|
||||
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
## 5. Deprecated Params Removed
|
||||
|
||||
The public init API no longer accepts:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.aliyun.ams.httpdns
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
import com.alibaba.sdk.android.httpdns.HttpDns
|
||||
import com.alibaba.sdk.android.httpdns.HttpDnsService
|
||||
import com.alibaba.sdk.android.httpdns.InitConfig
|
||||
@@ -15,18 +14,18 @@ import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
private var appContext: Context? = null
|
||||
|
||||
// Cached service keyed by accountId to avoid re-creating
|
||||
private var service: HttpDnsService? = null
|
||||
private var accountId: String? = null
|
||||
private var secretKey: String? = null
|
||||
private var aesSecretKey: String? = null
|
||||
|
||||
// Desired states collected before build()
|
||||
private var appId: String? = null
|
||||
private var secretKey: String? = null
|
||||
private var primaryServiceHost: String? = null
|
||||
private var backupServiceHost: String? = null
|
||||
private var servicePort: Int? = null
|
||||
|
||||
private var desiredPersistentCacheEnabled: Boolean? = null
|
||||
private var desiredDiscardExpiredAfterSeconds: Int? = null
|
||||
private var desiredReuseExpiredIPEnabled: Boolean? = null
|
||||
@@ -35,10 +34,6 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
private var desiredPreResolveAfterNetworkChanged: Boolean? = null
|
||||
private var desiredIPRankingMap: Map<String, Int>? = null
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
appContext = flutterPluginBinding.applicationContext
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "aliyun_httpdns")
|
||||
@@ -46,14 +41,7 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
}
|
||||
|
||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
// Log every incoming call with method name and raw arguments
|
||||
try {
|
||||
Log.i("AliyunHttpDns", "invoke method=${call.method}, args=${call.arguments}")
|
||||
} catch (_: Throwable) {
|
||||
Log.i("AliyunHttpDns", "invoke method=${call.method}, args=<unprintable>")
|
||||
}
|
||||
when (call.method) {
|
||||
// Dart: init(accountId, secretKey?, aesSecretKey?) — only save states here
|
||||
"initialize" -> {
|
||||
val args = call.arguments as? Map<*, *> ?: emptyMap<String, Any>()
|
||||
val ctx = appContext
|
||||
@@ -61,261 +49,315 @@ class AliyunHttpDnsPlugin : FlutterPlugin, MethodCallHandler {
|
||||
result.error("no_context", "Android context not attached", null)
|
||||
return
|
||||
}
|
||||
val accountAny = args["accountId"]
|
||||
val account = when (accountAny) {
|
||||
is Int -> accountAny.toString()
|
||||
is Long -> accountAny.toString()
|
||||
is String -> accountAny
|
||||
else -> null
|
||||
}
|
||||
val secret = (args["secretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
val aes = (args["aesSecretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
|
||||
if (account.isNullOrBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing accountId")
|
||||
val appIdAny = args["appId"]
|
||||
val parsedAppId = when (appIdAny) {
|
||||
is Int -> appIdAny.toString()
|
||||
is Long -> appIdAny.toString()
|
||||
is String -> appIdAny.trim()
|
||||
else -> ""
|
||||
}
|
||||
if (parsedAppId.isBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing appId")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
// Save desired states only; actual build happens on 'build'
|
||||
accountId = account
|
||||
|
||||
val primaryHostArg = (args["primaryServiceHost"] as? String)?.trim()
|
||||
if (primaryHostArg.isNullOrBlank()) {
|
||||
Log.i("AliyunHttpDns", "initialize missing primaryServiceHost")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
|
||||
val secret = (args["secretKey"] as? String)?.takeIf { it.isNotBlank() }
|
||||
val backup = (args["backupServiceHost"] as? String)?.trim()
|
||||
val port = when (val portAny = args["servicePort"]) {
|
||||
is Int -> portAny
|
||||
is Long -> portAny.toInt()
|
||||
is String -> portAny.toIntOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
appId = parsedAppId
|
||||
secretKey = secret
|
||||
aesSecretKey = aes
|
||||
Log.i("AliyunHttpDns", "initialize saved state, account=$account")
|
||||
primaryServiceHost = primaryHostArg
|
||||
backupServiceHost = backup?.trim()?.takeIf { it.isNotEmpty() }
|
||||
servicePort = if (port != null && port > 0) port else null
|
||||
|
||||
Log.i(
|
||||
"AliyunHttpDns",
|
||||
"initialize appId=$appId, primaryServiceHost=$primaryServiceHost, backupServiceHost=$backupServiceHost, servicePort=$servicePort"
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Dart: setLogEnabled(enabled) — save desired
|
||||
"setLogEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredLogEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "setLogEnabled desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setHttpsRequestEnabled(enabled)
|
||||
"setHttpsRequestEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredHttpsEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "https request desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPersistentCacheIPEnabled(enabled, discardExpiredAfterSeconds?) — save desired
|
||||
"setPersistentCacheIPEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
val discard = call.argument<Int>("discardExpiredAfterSeconds")
|
||||
desiredPersistentCacheEnabled = enabled
|
||||
desiredDiscardExpiredAfterSeconds = discard
|
||||
Log.i("AliyunHttpDns", "persistent cache desired=$enabled discard=$discard")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setReuseExpiredIPEnabled(enabled) — save desired
|
||||
"setReuseExpiredIPEnabled" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredReuseExpiredIPEnabled = enabled
|
||||
Log.i("AliyunHttpDns", "reuse expired ip desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPreResolveAfterNetworkChanged(enabled) — save desired (applied at build via InitConfig)
|
||||
"setPreResolveAfterNetworkChanged" -> {
|
||||
val enabled = call.argument<Boolean>("enabled") == true
|
||||
desiredPreResolveAfterNetworkChanged = enabled
|
||||
Log.i("AliyunHttpDns", "preResolveAfterNetworkChanged desired=$enabled")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setIPRankingList(hostPortMap) — save desired
|
||||
"setIPRankingList" -> {
|
||||
val hostPortMap = call.argument<Map<String, Int>>("hostPortMap")
|
||||
desiredIPRankingMap = hostPortMap
|
||||
Log.i("AliyunHttpDns", "IP ranking list desired, hosts=${hostPortMap?.keys?.joinToString()}")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: setPreResolveHosts(hosts, ipType)
|
||||
"setPreResolveHosts" -> {
|
||||
val hosts = call.argument<List<String>>("hosts") ?: emptyList()
|
||||
val ipTypeStr = call.argument<String>("ipType") ?: "auto"
|
||||
val type = when (ipTypeStr.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
val type = mapIpType(call.argument<String>("ipType") ?: "auto")
|
||||
try {
|
||||
service?.setPreResolveHosts(hosts, type)
|
||||
Log.i("AliyunHttpDns", "preResolve set for ${hosts.size} hosts, type=$type")
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "setPreResolveHosts failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: getSessionId
|
||||
"getSessionId" -> {
|
||||
val sid = try { service?.getSessionId() } catch (_: Throwable) { null }
|
||||
val sid = try {
|
||||
service?.getSessionId()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
result.success(sid)
|
||||
}
|
||||
|
||||
// Dart: cleanAllHostCache
|
||||
"cleanAllHostCache" -> {
|
||||
try {
|
||||
// Best-effort: empty list to clear all
|
||||
service?.cleanHostCache(ArrayList())
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "cleanAllHostCache failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// Dart: build() — construct InitConfig and acquire service using desired states
|
||||
"build" -> {
|
||||
val ctx = appContext
|
||||
val account = accountId
|
||||
if (ctx == null || account.isNullOrBlank()) {
|
||||
val currentAppId = appId
|
||||
if (ctx == null || currentAppId.isNullOrBlank()) {
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
desiredLogEnabled?.let { enabled ->
|
||||
try {
|
||||
HttpDnsLog.enable(enabled)
|
||||
Log.i("AliyunHttpDns", "HttpDnsLog.enable($enabled)")
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "HttpDnsLog.enable failed: ${t.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val builder = InitConfig.Builder()
|
||||
|
||||
// Optional builder params
|
||||
try { builder.javaClass.getMethod("setContext", Context::class.java).invoke(builder, ctx) } catch (_: Throwable) {}
|
||||
try {
|
||||
desiredLogEnabled?.let { HttpDnsLog.enable(it) }
|
||||
|
||||
val builder = InitConfig.Builder()
|
||||
var hostConfigApplied = false
|
||||
try {
|
||||
builder.javaClass.getMethod("setContext", Context::class.java)
|
||||
.invoke(builder, ctx)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
if (!secretKey.isNullOrBlank()) {
|
||||
builder.javaClass.getMethod("setSecretKey", String::class.java).invoke(builder, secretKey)
|
||||
builder.javaClass.getMethod("setSecretKey", String::class.java)
|
||||
.invoke(builder, secretKey)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
if (!aesSecretKey.isNullOrBlank()) {
|
||||
builder.javaClass.getMethod("setAesSecretKey", String::class.java).invoke(builder, aesSecretKey)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
// Prefer HTTPS if requested
|
||||
val enableHttps = desiredHttpsEnabled ?: true
|
||||
builder.javaClass.getMethod("setEnableHttps", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enableHttps)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredHttpsEnabled?.let { en ->
|
||||
builder.javaClass.getMethod("setEnableHttps", Boolean::class.javaPrimitiveType).invoke(builder, en)
|
||||
builder.javaClass.getMethod("setPrimaryServiceHost", String::class.java)
|
||||
.invoke(builder, primaryServiceHost)
|
||||
hostConfigApplied = true
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "setPrimaryServiceHost failed: ${t.message}")
|
||||
}
|
||||
try {
|
||||
backupServiceHost?.let {
|
||||
builder.javaClass.getMethod("setBackupServiceHost", String::class.java)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
servicePort?.let {
|
||||
builder.javaClass.getMethod("setServicePort", Int::class.javaPrimitiveType)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredPersistentCacheEnabled?.let { enabled ->
|
||||
val discardSeconds = desiredDiscardExpiredAfterSeconds
|
||||
if (discardSeconds != null && discardSeconds >= 0) {
|
||||
val expiredThresholdMillis = discardSeconds.toLong() * 1000L
|
||||
builder.javaClass.getMethod("setEnableCacheIp", Boolean::class.javaPrimitiveType, Long::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled, expiredThresholdMillis)
|
||||
val thresholdMillis = discardSeconds.toLong() * 1000L
|
||||
builder.javaClass.getMethod(
|
||||
"setEnableCacheIp",
|
||||
Boolean::class.javaPrimitiveType,
|
||||
Long::class.javaPrimitiveType
|
||||
).invoke(builder, enabled, thresholdMillis)
|
||||
} else {
|
||||
builder.javaClass.getMethod("setEnableCacheIp", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled)
|
||||
builder.javaClass.getMethod(
|
||||
"setEnableCacheIp",
|
||||
Boolean::class.javaPrimitiveType
|
||||
).invoke(builder, enabled)
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) { }
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredReuseExpiredIPEnabled?.let { enabled ->
|
||||
desiredReuseExpiredIPEnabled?.let {
|
||||
builder.javaClass.getMethod("setEnableExpiredIp", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, enabled)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) { }
|
||||
// Apply preResolve-after-network-changed
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredPreResolveAfterNetworkChanged?.let { en ->
|
||||
builder.javaClass.getMethod("setPreResolveAfterNetworkChanged", Boolean::class.javaPrimitiveType).invoke(builder, en)
|
||||
desiredPreResolveAfterNetworkChanged?.let {
|
||||
builder.javaClass.getMethod("setPreResolveAfterNetworkChanged", Boolean::class.javaPrimitiveType)
|
||||
.invoke(builder, it)
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
|
||||
// Apply IP ranking list
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
desiredIPRankingMap?.let { map ->
|
||||
if (map.isNotEmpty()) {
|
||||
// Create List<IPRankingBean>
|
||||
val ipRankingBeanClass = Class.forName("com.alibaba.sdk.android.httpdns.ranking.IPRankingBean")
|
||||
val constructor = ipRankingBeanClass.getConstructor(String::class.java, Int::class.javaPrimitiveType)
|
||||
val beanClass = Class.forName("com.alibaba.sdk.android.httpdns.ranking.IPRankingBean")
|
||||
val ctor = beanClass.getConstructor(String::class.java, Int::class.javaPrimitiveType)
|
||||
val list = ArrayList<Any>()
|
||||
map.forEach { (host, port) ->
|
||||
val bean = constructor.newInstance(host, port)
|
||||
list.add(bean)
|
||||
list.add(ctor.newInstance(host, port))
|
||||
}
|
||||
val m = builder.javaClass.getMethod("setIPRankingList", List::class.java)
|
||||
m.invoke(builder, list)
|
||||
Log.i("AliyunHttpDns", "setIPRankingList applied with ${list.size} hosts")
|
||||
builder.javaClass.getMethod("setIPRankingList", List::class.java)
|
||||
.invoke(builder, list)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.w("AliyunHttpDns", "setIPRankingList failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
|
||||
builder.buildFor(account)
|
||||
if (!hostConfigApplied) {
|
||||
Log.w("AliyunHttpDns", "build failed: sdk core does not support primaryServiceHost")
|
||||
result.success(false)
|
||||
return
|
||||
}
|
||||
|
||||
builder.buildFor(currentAppId)
|
||||
|
||||
service = if (!secretKey.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, account, secretKey)
|
||||
HttpDns.getService(ctx, currentAppId, secretKey)
|
||||
} else {
|
||||
HttpDns.getService(ctx, account)
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
}
|
||||
|
||||
Log.i("AliyunHttpDns", "build completed for account=$account")
|
||||
|
||||
result.success(true)
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "build failed: ${t.message}")
|
||||
Log.w("AliyunHttpDns", "build failed: ${t.message}")
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Dart: resolveHostSyncNonBlocking(hostname, ipType, sdnsParams?, cacheKey?)
|
||||
"resolveHostSyncNonBlocking" -> {
|
||||
val hostname = call.argument<String>("hostname")
|
||||
if (hostname.isNullOrBlank()) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>()))
|
||||
return
|
||||
}
|
||||
val ipTypeStr = call.argument<String>("ipType") ?: "auto"
|
||||
val type = when (ipTypeStr.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
|
||||
val type = mapIpType(call.argument<String>("ipType") ?: "auto")
|
||||
try {
|
||||
val svc = service ?: run {
|
||||
val ctx = appContext
|
||||
val acc = accountId
|
||||
if (ctx != null && !acc.isNullOrBlank()) HttpDns.getService(ctx, acc) else null
|
||||
val currentAppId = appId
|
||||
if (ctx != null && !currentAppId.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val r = svc?.getHttpDnsResultForHostSyncNonBlocking(hostname, type)
|
||||
val v4 = r?.ips?.toList() ?: emptyList()
|
||||
val v6 = r?.ipv6s?.toList() ?: emptyList()
|
||||
// 记录解析结果,便于排查:包含 host、请求类型以及返回的 IPv4/IPv6 列表
|
||||
Log.d(
|
||||
"HttpdnsPlugin",
|
||||
"resolve result host=" + hostname + ", type=" + type +
|
||||
", ipv4=" + v4.joinToString(prefix = "[", postfix = "]") +
|
||||
", ipv6=" + v6.joinToString(prefix = "[", postfix = "]")
|
||||
)
|
||||
result.success(mapOf("ipv4" to v4, "ipv6" to v6))
|
||||
} catch (t: Throwable) {
|
||||
Log.i("AliyunHttpDns", "resolveHostSyncNonBlocking failed: ${t.message}")
|
||||
} catch (_: Throwable) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>()))
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy methods removed: preResolve / clearCache handled at app layer if needed
|
||||
"resolveHostV1" -> {
|
||||
val hostname = call.argument<String>("hostname")
|
||||
if (hostname.isNullOrBlank()) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>(), "ttl" to 0))
|
||||
return
|
||||
}
|
||||
val qtype = (call.argument<String>("qtype") ?: "A").uppercase()
|
||||
val cip = call.argument<String>("cip")?.trim()
|
||||
val requestType = if (qtype == "AAAA") RequestIpType.v6 else RequestIpType.v4
|
||||
try {
|
||||
val svc = service ?: run {
|
||||
val ctx = appContext
|
||||
val currentAppId = appId
|
||||
if (ctx != null && !currentAppId.isNullOrBlank()) {
|
||||
HttpDns.getService(ctx, currentAppId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val params = if (!cip.isNullOrEmpty()) mapOf("cip" to cip) else null
|
||||
val r = svc?.getHttpDnsResultForHostSyncNonBlocking(hostname, requestType, params, hostname)
|
||||
val v4 = r?.ips?.toList() ?: emptyList()
|
||||
val v6 = r?.ipv6s?.toList() ?: emptyList()
|
||||
result.success(
|
||||
mapOf(
|
||||
"ipv4" to v4,
|
||||
"ipv6" to v6,
|
||||
"ttl" to (r?.ttl ?: 0),
|
||||
)
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
result.success(mapOf("ipv4" to emptyList<String>(), "ipv6" to emptyList<String>(), "ttl" to 0))
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapIpType(ipType: String): RequestIpType {
|
||||
return when (ipType.lowercase()) {
|
||||
"ipv4", "v4" -> RequestIpType.v4
|
||||
"ipv6", "v6" -> RequestIpType.v6
|
||||
"both", "64" -> RequestIpType.both
|
||||
else -> RequestIpType.auto
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
service = null
|
||||
|
||||
@@ -16,11 +16,14 @@ This example demonstrates:
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Replace the `accountId` and `secretKey` in `lib/main.dart` with your own credentials:
|
||||
1. Replace the SDK init parameters in `lib/main.dart` with your own values:
|
||||
```dart
|
||||
await AliyunHttpdns.init(
|
||||
accountId: YOUR_ACCOUNT_ID, // Replace with your account ID
|
||||
secretKey: 'YOUR_SECRET_KEY', // Replace with your secret key
|
||||
appId: 'YOUR_APP_ID',
|
||||
primaryServiceHost: 'httpdns-a.example.com',
|
||||
backupServiceHost: 'httpdns-b.example.com',
|
||||
servicePort: 443,
|
||||
secretKey: 'YOUR_SIGN_SECRET', // optional if sign is enabled
|
||||
);
|
||||
```
|
||||
|
||||
@@ -48,4 +51,4 @@ The example uses a modern approach with `HttpClient.connectionFactory` to integr
|
||||
|
||||
## Note
|
||||
|
||||
The credentials in this example are placeholders. Please obtain your own credentials from the [Aliyun HTTPDNS Console](https://help.aliyun.com/document_detail/2867674.html).
|
||||
The values in this example are placeholders. Use your own platform app configuration.
|
||||
|
||||
@@ -64,8 +64,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
_httpdnsIniting = true;
|
||||
try {
|
||||
await AliyunHttpdns.init(
|
||||
accountId: 000000, // 请替换为您的 Account ID
|
||||
secretKey: 'your_secret_key_here', // 请替换为您的 Secret Key
|
||||
appId: 'app1f1ndpo9', // 请替换为您的应用 AppId
|
||||
primaryServiceHost: 'httpdns-a.example.com', // 请替换为主服务域名
|
||||
backupServiceHost: 'httpdns-b.example.com', // 可选:备服务域名
|
||||
servicePort: 443,
|
||||
secretKey: 'your_sign_secret_here', // 可选:仅验签开启时需要
|
||||
);
|
||||
await AliyunHttpdns.setHttpsRequestEnabled(true);
|
||||
await AliyunHttpdns.setLogEnabled(true);
|
||||
|
||||
@@ -1,203 +1,324 @@
|
||||
import Flutter
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AlicloudHTTPDNS
|
||||
import CommonCrypto
|
||||
|
||||
public class AliyunHttpDnsPlugin: NSObject, FlutterPlugin {
|
||||
private var channel: FlutterMethodChannel!
|
||||
private var appId: String?
|
||||
private var secretKey: String?
|
||||
private var primaryServiceHost: String?
|
||||
private var backupServiceHost: String?
|
||||
private var servicePort: Int = 443
|
||||
|
||||
// Desired states saved until build()
|
||||
private var desiredAccountId: Int?
|
||||
private var desiredSecretKey: String?
|
||||
private var desiredAesSecretKey: String?
|
||||
|
||||
private var desiredPersistentCacheEnabled: Bool?
|
||||
private var desiredDiscardExpiredAfterSeconds: Int?
|
||||
private var desiredReuseExpiredIPEnabled: Bool?
|
||||
private var desiredLogEnabled: Bool?
|
||||
private var desiredHttpsEnabled: Bool?
|
||||
private var desiredPreResolveAfterNetworkChanged: Bool?
|
||||
private var desiredIPRankingMap: [String: NSNumber]?
|
||||
|
||||
private var desiredConnectTimeoutSeconds: TimeInterval = 3
|
||||
private var desiredReadTimeoutSeconds: TimeInterval = 5
|
||||
|
||||
private lazy var sessionId: String = {
|
||||
UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
||||
}()
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "aliyun_httpdns", binaryMessenger: registrar.messenger())
|
||||
let instance = AliyunHttpDnsPlugin()
|
||||
instance.channel = channel
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
// Dart: init(accountId, secretKey?, aesSecretKey?) — only save desired state
|
||||
case "initialize":
|
||||
let options = call.arguments as? [String: Any] ?? [:]
|
||||
let accountIdAny = options["accountId"]
|
||||
let secretKey = options["secretKey"] as? String
|
||||
let aesSecretKey = options["aesSecretKey"] as? String
|
||||
|
||||
guard let accountId = (accountIdAny as? Int) ?? Int((accountIdAny as? String) ?? "") else {
|
||||
NSLog("AliyunHttpDns: initialize missing accountId")
|
||||
guard let rawAppId = options["appId"] as? String,
|
||||
let rawPrimaryHost = options["primaryServiceHost"] as? String else {
|
||||
result(false)
|
||||
return
|
||||
}
|
||||
desiredAccountId = accountId
|
||||
desiredSecretKey = secretKey
|
||||
desiredAesSecretKey = aesSecretKey
|
||||
NSLog("AliyunHttpDns: initialize saved accountId=\(accountId)")
|
||||
|
||||
let normalizedAppId = rawAppId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedPrimaryHost = rawPrimaryHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if normalizedAppId.isEmpty || normalizedPrimaryHost.isEmpty {
|
||||
result(false)
|
||||
return
|
||||
}
|
||||
|
||||
appId = normalizedAppId
|
||||
primaryServiceHost = normalizedPrimaryHost
|
||||
backupServiceHost = (options["backupServiceHost"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
secretKey = (options["secretKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let p = options["servicePort"] as? Int, p > 0 {
|
||||
servicePort = p
|
||||
}
|
||||
result(true)
|
||||
|
||||
|
||||
|
||||
// Dart: setLogEnabled(enabled) — save desired
|
||||
case "setLogEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredLogEnabled = enabled
|
||||
NSLog("AliyunHttpDns: log desired=\(enabled)")
|
||||
desiredLogEnabled = (args?["enabled"] as? Bool) ?? false
|
||||
result(nil)
|
||||
|
||||
case "setHttpsRequestEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredHttpsEnabled = enabled
|
||||
NSLog("AliyunHttpDns: https request desired=\(enabled)")
|
||||
desiredHttpsEnabled = (args?["enabled"] as? Bool) ?? false
|
||||
result(nil)
|
||||
|
||||
// Dart: setPersistentCacheIPEnabled(enabled, discardExpiredAfterSeconds?) — save desired
|
||||
case "setPersistentCacheIPEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
let discard = args?["discardExpiredAfterSeconds"] as? Int
|
||||
desiredPersistentCacheEnabled = enabled
|
||||
desiredDiscardExpiredAfterSeconds = discard
|
||||
NSLog("AliyunHttpDns: persistent cache desired=\(enabled) discard=\(discard ?? -1)")
|
||||
result(nil)
|
||||
|
||||
// Dart: setReuseExpiredIPEnabled(enabled) — save desired
|
||||
case "setReuseExpiredIPEnabled":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredReuseExpiredIPEnabled = enabled
|
||||
NSLog("AliyunHttpDns: reuse expired ip desired=\(enabled)")
|
||||
result(nil)
|
||||
|
||||
case "setPreResolveAfterNetworkChanged":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let enabled = (args?["enabled"] as? Bool) ?? false
|
||||
desiredPreResolveAfterNetworkChanged = enabled
|
||||
NSLog("AliyunHttpDns: preResolveAfterNetworkChanged desired=\(enabled)")
|
||||
result(nil)
|
||||
|
||||
case "setIPRankingList":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let hostPortMap = args?["hostPortMap"] as? [String: NSNumber]
|
||||
desiredIPRankingMap = hostPortMap
|
||||
NSLog("AliyunHttpDns: IP ranking list desired, hosts=\(hostPortMap?.keys.joined(separator: ", ") ?? "")")
|
||||
result(nil)
|
||||
|
||||
case "setPreResolveHosts":
|
||||
let args = call.arguments as? [String: Any]
|
||||
let hosts = (args?["hosts"] as? [String]) ?? []
|
||||
let ipTypeStr = (args?["ipType"] as? String) ?? "auto"
|
||||
switch ipTypeStr.lowercased() {
|
||||
case "ipv4", "v4":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(0))
|
||||
case "ipv6", "v6":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(1))
|
||||
case "both", "64":
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(2))
|
||||
default:
|
||||
HttpDnsService.sharedInstance().setPreResolveHosts(hosts)
|
||||
}
|
||||
result(nil)
|
||||
|
||||
case "getSessionId":
|
||||
let sid = HttpDnsService.sharedInstance().getSessionId()
|
||||
result(sid)
|
||||
result(sessionId)
|
||||
|
||||
case "cleanAllHostCache":
|
||||
HttpDnsService.sharedInstance().cleanAllHostCache()
|
||||
result(nil)
|
||||
|
||||
// Dart: build() — construct service and apply desired states
|
||||
case "build":
|
||||
guard let accountId = desiredAccountId else {
|
||||
result(false)
|
||||
return
|
||||
if desiredHttpsEnabled == false {
|
||||
NSLog("AliyunHttpDns(iOS): HTTPS is required by current protocol, force enabled")
|
||||
}
|
||||
// Initialize singleton
|
||||
if let secret = desiredSecretKey, !secret.isEmpty {
|
||||
if let aes = desiredAesSecretKey, !aes.isEmpty {
|
||||
_ = HttpDnsService(accountID: accountId, secretKey: secret, aesSecretKey: aes)
|
||||
} else {
|
||||
_ = HttpDnsService(accountID: accountId, secretKey: secret)
|
||||
}
|
||||
} else {
|
||||
_ = HttpDnsService(accountID: accountId) // deprecated but acceptable fallback
|
||||
}
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
// Apply desired runtime flags
|
||||
if let enable = desiredPersistentCacheEnabled {
|
||||
if let discard = desiredDiscardExpiredAfterSeconds, discard >= 0 {
|
||||
svc.setPersistentCacheIPEnabled(enable, discardRecordsHasExpiredFor: TimeInterval(discard))
|
||||
} else {
|
||||
svc.setPersistentCacheIPEnabled(enable)
|
||||
}
|
||||
}
|
||||
if let enable = desiredReuseExpiredIPEnabled {
|
||||
svc.setReuseExpiredIPEnabled(enable)
|
||||
}
|
||||
if let enable = desiredLogEnabled {
|
||||
svc.setLogEnabled(enable)
|
||||
}
|
||||
if let enable = desiredHttpsEnabled {
|
||||
svc.setHTTPSRequestEnabled(enable)
|
||||
if desiredLogEnabled == true {
|
||||
NSLog("AliyunHttpDns(iOS): build success, appId=\(appId ?? "")")
|
||||
}
|
||||
result(appId != nil && primaryServiceHost != nil)
|
||||
|
||||
if let en = desiredPreResolveAfterNetworkChanged {
|
||||
svc.setPreResolveAfterNetworkChanged(en)
|
||||
}
|
||||
if let ipRankingMap = desiredIPRankingMap, !ipRankingMap.isEmpty {
|
||||
svc.setIPRankingDatasource(ipRankingMap)
|
||||
}
|
||||
NSLog("AliyunHttpDns: build completed accountId=\(accountId)")
|
||||
result(true)
|
||||
|
||||
// Dart: resolveHostSyncNonBlocking(hostname, ipType, sdnsParams?, cacheKey?)
|
||||
case "resolveHostSyncNonBlocking":
|
||||
guard let args = call.arguments as? [String: Any], let host = args["hostname"] as? String else {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let hostname = args["hostname"] as? String else {
|
||||
result(["ipv4": [], "ipv6": []])
|
||||
return
|
||||
}
|
||||
let ipTypeStr = (args["ipType"] as? String) ?? "auto"
|
||||
let sdnsParams = args["sdnsParams"] as? [String: String]
|
||||
let cacheKey = args["cacheKey"] as? String
|
||||
let type: HttpdnsQueryIPType
|
||||
switch ipTypeStr.lowercased() {
|
||||
case "ipv4", "v4": type = .ipv4
|
||||
case "ipv6", "v6": type = .ipv6
|
||||
case "both", "64": type = .both
|
||||
default: type = .auto
|
||||
let ipType = ((args["ipType"] as? String) ?? "auto").lowercased()
|
||||
resolveHost(hostname: hostname, ipType: ipType) { payload in
|
||||
result(payload)
|
||||
}
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
var v4: [String] = []
|
||||
var v6: [String] = []
|
||||
if let params = sdnsParams, let key = cacheKey, let r = svc.resolveHostSyncNonBlocking(host, by: type, withSdnsParams: params, sdnsCacheKey: key) {
|
||||
if r.hasIpv4Address() { v4 = r.ips }
|
||||
if r.hasIpv6Address() { v6 = r.ipv6s }
|
||||
} else if let r = svc.resolveHostSyncNonBlocking(host, by: type) {
|
||||
if r.hasIpv4Address() { v4 = r.ips }
|
||||
if r.hasIpv6Address() { v6 = r.ipv6s }
|
||||
case "resolveHostV1":
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let hostname = args["hostname"] as? String else {
|
||||
result(["ipv4": [], "ipv6": [], "ttl": 0])
|
||||
return
|
||||
}
|
||||
let qtype = ((args["qtype"] as? String) ?? "A").uppercased()
|
||||
let cip = (args["cip"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
resolveSingle(hostname: hostname, qtype: qtype, cip: cip) { records, ttl in
|
||||
if qtype == "AAAA" {
|
||||
result(["ipv4": [], "ipv6": records, "ttl": ttl])
|
||||
} else {
|
||||
result(["ipv4": records, "ipv6": [], "ttl": ttl])
|
||||
}
|
||||
}
|
||||
result(["ipv4": v4, "ipv6": v6])
|
||||
|
||||
// Legacy methods removed: preResolve / clearCache
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveHost(hostname: String, ipType: String, completion: @escaping ([String: [String]]) -> Void) {
|
||||
let qtypes: [String]
|
||||
switch ipType {
|
||||
case "ipv6", "v6":
|
||||
qtypes = ["AAAA"]
|
||||
case "both", "64":
|
||||
qtypes = ["A", "AAAA"]
|
||||
default:
|
||||
qtypes = ["A"]
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
let lock = NSLock()
|
||||
var ipv4: [String] = []
|
||||
var ipv6: [String] = []
|
||||
|
||||
for qtype in qtypes {
|
||||
group.enter()
|
||||
resolveSingle(hostname: hostname, qtype: qtype, cip: nil) { records, _ in
|
||||
lock.lock()
|
||||
if qtype == "AAAA" {
|
||||
ipv6.append(contentsOf: records)
|
||||
} else {
|
||||
ipv4.append(contentsOf: records)
|
||||
}
|
||||
lock.unlock()
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
completion([
|
||||
"ipv4": Array(Set(ipv4)),
|
||||
"ipv6": Array(Set(ipv6))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveSingle(hostname: String, qtype: String, cip: String?, completion: @escaping ([String], Int) -> Void) {
|
||||
guard let appId = appId,
|
||||
let primary = primaryServiceHost else {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
var hosts: [String] = [primary]
|
||||
if let backup = backupServiceHost, !backup.isEmpty, backup != primary {
|
||||
hosts.append(backup)
|
||||
}
|
||||
|
||||
func attempt(_ index: Int) {
|
||||
if index >= hosts.count {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
let serviceHost = hosts[index]
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
components.host = serviceHost
|
||||
components.port = servicePort
|
||||
components.path = "/resolve"
|
||||
|
||||
var queryItems: [URLQueryItem] = [
|
||||
URLQueryItem(name: "appId", value: appId),
|
||||
URLQueryItem(name: "dn", value: hostname),
|
||||
URLQueryItem(name: "qtype", value: qtype),
|
||||
URLQueryItem(name: "sdk_version", value: "flutter-ios-1.0.0"),
|
||||
URLQueryItem(name: "os", value: "ios"),
|
||||
URLQueryItem(name: "sid", value: sessionId)
|
||||
]
|
||||
|
||||
if let secret = secretKey, !secret.isEmpty {
|
||||
let exp = String(Int(Date().timeIntervalSince1970) + 600)
|
||||
let nonce = UUID().uuidString.replacingOccurrences(of: "-", with: "")
|
||||
let signRaw = "\(appId)|\(hostname.lowercased())|\(qtype.uppercased())|\(exp)|\(nonce)"
|
||||
let sign = hmacSHA256Hex(message: signRaw, secret: secret)
|
||||
queryItems.append(URLQueryItem(name: "exp", value: exp))
|
||||
queryItems.append(URLQueryItem(name: "nonce", value: nonce))
|
||||
queryItems.append(URLQueryItem(name: "sign", value: sign))
|
||||
}
|
||||
if let cip = cip, !cip.isEmpty {
|
||||
queryItems.append(URLQueryItem(name: "cip", value: cip))
|
||||
}
|
||||
|
||||
components.queryItems = queryItems
|
||||
guard let url = components.url else {
|
||||
completion([], 0)
|
||||
return
|
||||
}
|
||||
|
||||
var req = URLRequest(url: url)
|
||||
req.httpMethod = "GET"
|
||||
req.timeoutInterval = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
req.setValue(serviceHost, forHTTPHeaderField: "Host")
|
||||
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.timeoutIntervalForRequest = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
config.timeoutIntervalForResource = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
|
||||
let session = URLSession(configuration: config)
|
||||
session.dataTask(with: req) { [weak self] data, _, error in
|
||||
defer { session.finishTasksAndInvalidate() }
|
||||
|
||||
if let error = error {
|
||||
if self?.desiredLogEnabled == true {
|
||||
NSLog("AliyunHttpDns(iOS): resolve request failed host=\(serviceHost), err=\(error.localizedDescription)")
|
||||
}
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
|
||||
let parsedIPs = Self.extractIPsFromResolveResponse(data: data, qtype: qtype)
|
||||
if parsedIPs.isEmpty {
|
||||
attempt(index + 1)
|
||||
return
|
||||
}
|
||||
let ttl = Self.extractTTLFromResolveResponse(data: data)
|
||||
completion(parsedIPs, ttl)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
attempt(0)
|
||||
}
|
||||
|
||||
private static func extractIPsFromResolveResponse(data: Data, qtype: String) -> [String] {
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
isResolveSuccessCode(obj["code"]),
|
||||
let dataObj = obj["data"] as? [String: Any],
|
||||
let records = dataObj["records"] as? [[String: Any]] else {
|
||||
return []
|
||||
}
|
||||
|
||||
var ips: [String] = []
|
||||
for row in records {
|
||||
let type = ((row["type"] as? String) ?? "").uppercased()
|
||||
if type != qtype.uppercased() {
|
||||
continue
|
||||
}
|
||||
if let ip = row["ip"] as? String, !ip.isEmpty {
|
||||
ips.append(ip)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
private static func extractTTLFromResolveResponse(data: Data) -> Int {
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataObj = obj["data"] as? [String: Any],
|
||||
let ttlValue = dataObj["ttl"] else {
|
||||
return 0
|
||||
}
|
||||
if let ttl = ttlValue as? Int {
|
||||
return ttl
|
||||
}
|
||||
if let ttlString = ttlValue as? String, let ttl = Int(ttlString) {
|
||||
return ttl
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private static func isResolveSuccessCode(_ codeValue: Any?) -> Bool {
|
||||
if let code = codeValue as? String {
|
||||
let normalized = code.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
return normalized == "SUCCESS" || normalized == "0"
|
||||
}
|
||||
if let code = codeValue as? Int {
|
||||
return code == 0
|
||||
}
|
||||
if let code = codeValue as? NSNumber {
|
||||
return code.intValue == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func hmacSHA256Hex(message: String, secret: String) -> String {
|
||||
guard let keyData = secret.data(using: .utf8),
|
||||
let messageData = message.data(using: .utf8) else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
keyData.withUnsafeBytes { keyBytes in
|
||||
messageData.withUnsafeBytes { msgBytes in
|
||||
CCHmac(
|
||||
CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
keyBytes.baseAddress,
|
||||
keyData.count,
|
||||
msgBytes.baseAddress,
|
||||
messageData.count,
|
||||
&digest
|
||||
)
|
||||
}
|
||||
}
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'aliyun_httpdns'
|
||||
s.version = '1.0.2'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'aliyun httpdns flutter plugin'
|
||||
s.description = <<-DESC
|
||||
aliyun httpdns flutter plugin.
|
||||
@@ -16,7 +16,6 @@ DESC
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.static_framework = true
|
||||
s.dependency 'Flutter'
|
||||
s.dependency 'AlicloudHTTPDNS', '3.4.0'
|
||||
s.platform = :ios, '10.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
|
||||
@@ -1,87 +1,89 @@
|
||||
import 'dart:async';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AliyunHttpdns {
|
||||
static const MethodChannel _channel = MethodChannel('aliyun_httpdns');
|
||||
|
||||
/// 1) 初始化:使用 accountId/secretKey/aesSecretKey
|
||||
/// New API only:
|
||||
/// appId + primary/backup service host + optional sign secret.
|
||||
static Future<bool> init({
|
||||
required int accountId,
|
||||
required String appId,
|
||||
required String primaryServiceHost,
|
||||
String? backupServiceHost,
|
||||
int servicePort = 443,
|
||||
String? secretKey,
|
||||
String? aesSecretKey,
|
||||
}) async {
|
||||
final ok =
|
||||
await _channel.invokeMethod<bool>('initialize', <String, dynamic>{
|
||||
'accountId': accountId,
|
||||
if (secretKey != null) 'secretKey': secretKey,
|
||||
if (aesSecretKey != null) 'aesSecretKey': aesSecretKey,
|
||||
});
|
||||
final String normalizedAppId = appId.trim();
|
||||
final String normalizedPrimary = primaryServiceHost.trim();
|
||||
if (normalizedAppId.isEmpty || normalizedPrimary.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> args = <String, dynamic>{
|
||||
'appId': normalizedAppId,
|
||||
'primaryServiceHost': normalizedPrimary,
|
||||
if (backupServiceHost != null && backupServiceHost.trim().isNotEmpty)
|
||||
'backupServiceHost': backupServiceHost.trim(),
|
||||
if (servicePort > 0) 'servicePort': servicePort,
|
||||
if (secretKey != null && secretKey.isNotEmpty) 'secretKey': secretKey,
|
||||
};
|
||||
|
||||
final bool? ok = await _channel.invokeMethod<bool>('initialize', args);
|
||||
return ok ?? false;
|
||||
}
|
||||
|
||||
/// 构建底层 service,只有在调用了 initialize / 一系列 setXxx 后,
|
||||
/// 调用本方法才会真正创建底层实例并应用配置
|
||||
static Future<bool> build() async {
|
||||
final ok = await _channel.invokeMethod<bool>('build');
|
||||
final bool? ok = await _channel.invokeMethod<bool>('build');
|
||||
return ok ?? false;
|
||||
}
|
||||
|
||||
/// 2) 设置日志开关
|
||||
static Future<void> setLogEnabled(bool enabled) async {
|
||||
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 3) 设置持久化缓存
|
||||
static Future<void> setPersistentCacheIPEnabled(bool enabled,
|
||||
{int? discardExpiredAfterSeconds}) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
|
||||
await _channel.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
if (discardExpiredAfterSeconds != null)
|
||||
'discardExpiredAfterSeconds': discardExpiredAfterSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
/// 4) 是否允许复用过期 IP
|
||||
static Future<void> setReuseExpiredIPEnabled(bool enabled) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 设置是否使用 HTTPS 解析链路,避免明文流量被系统拦截
|
||||
static Future<void> setHttpsRequestEnabled(bool enabled) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{'enabled': enabled});
|
||||
}
|
||||
|
||||
/// 5) 伪异步解析:返回 IPv4/IPv6 数组
|
||||
/// 返回格式:{"ipv4": `List<String>`, "ipv6": `List<String>`}
|
||||
static Future<Map<String, List<String>>> resolveHostSyncNonBlocking(
|
||||
String hostname, {
|
||||
String ipType = 'auto', // auto/ipv4/ipv6/both
|
||||
Map<String, String>? sdnsParams,
|
||||
String? cacheKey,
|
||||
}) async {
|
||||
final Map<dynamic, dynamic>? res = await _channel
|
||||
.invokeMethod('resolveHostSyncNonBlocking', <String, dynamic>{
|
||||
final Map<dynamic, dynamic>? res =
|
||||
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostSyncNonBlocking',
|
||||
<String, dynamic>{
|
||||
'hostname': hostname,
|
||||
'ipType': ipType,
|
||||
if (sdnsParams != null) 'sdnsParams': sdnsParams,
|
||||
if (cacheKey != null) 'cacheKey': cacheKey,
|
||||
});
|
||||
final Map<String, List<String>> out = {
|
||||
|
||||
final Map<String, List<String>> out = <String, List<String>>{
|
||||
'ipv4': <String>[],
|
||||
'ipv6': <String>[],
|
||||
};
|
||||
if (res == null) return out;
|
||||
final v4 = res['ipv4'];
|
||||
final v6 = res['ipv6'];
|
||||
if (res == null) {
|
||||
return out;
|
||||
}
|
||||
|
||||
final dynamic v4 = res['ipv4'];
|
||||
final dynamic v6 = res['ipv6'];
|
||||
if (v4 is List) {
|
||||
out['ipv4'] = v4.map((e) => e.toString()).toList();
|
||||
}
|
||||
@@ -91,51 +93,190 @@ class AliyunHttpdns {
|
||||
return out;
|
||||
}
|
||||
|
||||
// 解析域名,返回 A/AAAA 记录等(保留旧接口以兼容,未在本任务使用)
|
||||
static Future<Map<String, dynamic>?> resolve(String hostname,
|
||||
{Map<String, dynamic>? options}) async {
|
||||
final res = await _channel.invokeMethod<Map<dynamic, dynamic>>('resolve', {
|
||||
/// V1 resolve API:
|
||||
/// qtype supports A / AAAA, optional cip for route simulation.
|
||||
static Future<Map<String, dynamic>> resolveHost(
|
||||
String hostname, {
|
||||
String qtype = 'A',
|
||||
String? cip,
|
||||
}) async {
|
||||
final Map<dynamic, dynamic>? res =
|
||||
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostV1', <String, dynamic>{
|
||||
'hostname': hostname,
|
||||
if (options != null) 'options': options,
|
||||
'qtype': qtype,
|
||||
if (cip != null && cip.trim().isNotEmpty) 'cip': cip.trim(),
|
||||
});
|
||||
return res?.map((key, value) => MapEntry(key.toString(), value));
|
||||
|
||||
final Map<String, dynamic> out = <String, dynamic>{
|
||||
'ipv4': <String>[],
|
||||
'ipv6': <String>[],
|
||||
'ttl': 0,
|
||||
};
|
||||
if (res == null) {
|
||||
return out;
|
||||
}
|
||||
final dynamic v4 = res['ipv4'];
|
||||
final dynamic v6 = res['ipv6'];
|
||||
if (v4 is List) {
|
||||
out['ipv4'] = v4.map((e) => e.toString()).toList();
|
||||
}
|
||||
if (v6 is List) {
|
||||
out['ipv6'] = v6.map((e) => e.toString()).toList();
|
||||
}
|
||||
final dynamic ttl = res['ttl'];
|
||||
if (ttl is int) {
|
||||
out['ttl'] = ttl;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 1) setPreResolveHosts: 传入 host 列表,native 侧调用 SDK 预解析
|
||||
static Future<void> setPreResolveHosts(List<String> hosts,
|
||||
{String ipType = 'auto'}) async {
|
||||
static Future<void> setPreResolveHosts(List<String> hosts, {String ipType = 'auto'}) async {
|
||||
await _channel.invokeMethod<void>('setPreResolveHosts', <String, dynamic>{
|
||||
'hosts': hosts,
|
||||
'ipType': ipType,
|
||||
});
|
||||
}
|
||||
|
||||
// 2) setLogEnabled: 已有,同步保留(在此文件顶部已有 setLogEnabled 实现)
|
||||
|
||||
// 3) setPreResolveAfterNetworkChanged: 是否在网络切换时自动刷新解析
|
||||
static Future<void> setPreResolveAfterNetworkChanged(bool enabled) async {
|
||||
await _channel.invokeMethod<void>(
|
||||
'setPreResolveAfterNetworkChanged', <String, dynamic>{
|
||||
await _channel.invokeMethod<void>('setPreResolveAfterNetworkChanged', <String, dynamic>{
|
||||
'enabled': enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// 4) getSessionId: 获取会话 id
|
||||
static Future<String?> getSessionId() async {
|
||||
final sid = await _channel.invokeMethod<String>('getSessionId');
|
||||
return sid;
|
||||
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
|
||||
await _channel
|
||||
.invokeMethod<void>('setIPRankingList', <String, dynamic>{'hostPortMap': hostPortMap});
|
||||
}
|
||||
|
||||
static Future<String?> getSessionId() async {
|
||||
return _channel.invokeMethod<String>('getSessionId');
|
||||
}
|
||||
|
||||
// 5) cleanAllHostCache: 清除所有缓存
|
||||
static Future<void> cleanAllHostCache() async {
|
||||
await _channel.invokeMethod<void>('cleanAllHostCache');
|
||||
}
|
||||
|
||||
/// 设置 IP 优选列表
|
||||
/// [hostPortMap] 域名和端口的映射,例如:{'www.aliyun.com': 443}
|
||||
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
|
||||
await _channel.invokeMethod<void>('setIPRankingList', <String, dynamic>{
|
||||
'hostPortMap': hostPortMap,
|
||||
});
|
||||
|
||||
static AliyunHttpdnsHttpAdapter createHttpAdapter({
|
||||
AliyunHttpdnsAdapterOptions options = const AliyunHttpdnsAdapterOptions(),
|
||||
}) {
|
||||
return AliyunHttpdnsHttpAdapter._(options);
|
||||
}
|
||||
}
|
||||
|
||||
class AliyunHttpdnsAdapterOptions {
|
||||
final String ipType;
|
||||
final int connectTimeoutMs;
|
||||
final int readTimeoutMs;
|
||||
final bool allowInsecureCertificatesForDebugOnly;
|
||||
|
||||
const AliyunHttpdnsAdapterOptions({
|
||||
this.ipType = 'auto',
|
||||
this.connectTimeoutMs = 3000,
|
||||
this.readTimeoutMs = 5000,
|
||||
this.allowInsecureCertificatesForDebugOnly = false,
|
||||
});
|
||||
}
|
||||
|
||||
class AliyunHttpdnsRequestResult {
|
||||
final int statusCode;
|
||||
final Map<String, List<String>> headers;
|
||||
final Uint8List body;
|
||||
final String usedIp;
|
||||
|
||||
const AliyunHttpdnsRequestResult({
|
||||
required this.statusCode,
|
||||
required this.headers,
|
||||
required this.body,
|
||||
required this.usedIp,
|
||||
});
|
||||
}
|
||||
|
||||
class AliyunHttpdnsHttpAdapter {
|
||||
final AliyunHttpdnsAdapterOptions _options;
|
||||
|
||||
AliyunHttpdnsHttpAdapter._(this._options);
|
||||
|
||||
/// Fixed behavior:
|
||||
/// 1) resolve host by HTTPDNS
|
||||
/// 2) connect to IP:443
|
||||
/// 3) keep HTTP Host as original domain
|
||||
/// 4) no fallback to domain connect
|
||||
Future<AliyunHttpdnsRequestResult> request(
|
||||
Uri uri, {
|
||||
String method = 'GET',
|
||||
Map<String, String>? headers,
|
||||
List<int>? body,
|
||||
}) async {
|
||||
if (uri.host.isEmpty) {
|
||||
throw const HttpException('HOST_ROUTE_REJECTED: host is empty');
|
||||
}
|
||||
if (uri.scheme.toLowerCase() != 'https') {
|
||||
throw const HttpException('TLS_EMPTY_SNI_FAILED: only https is supported');
|
||||
}
|
||||
|
||||
final Map<String, List<String>> resolved = await AliyunHttpdns.resolveHostSyncNonBlocking(
|
||||
uri.host,
|
||||
ipType: _options.ipType,
|
||||
);
|
||||
final List<String> ips = <String>[
|
||||
...?resolved['ipv4'],
|
||||
...?resolved['ipv6'],
|
||||
];
|
||||
if (ips.isEmpty) {
|
||||
throw const HttpException('NO_IP_AVAILABLE: HTTPDNS returned empty ip list');
|
||||
}
|
||||
|
||||
Object? lastError;
|
||||
for (final String ip in ips) {
|
||||
final HttpClient client = HttpClient();
|
||||
client.connectionTimeout = Duration(milliseconds: _options.connectTimeoutMs);
|
||||
if (_options.allowInsecureCertificatesForDebugOnly) {
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
}
|
||||
|
||||
try {
|
||||
final Uri target = uri.replace(
|
||||
host: ip,
|
||||
port: uri.hasPort ? uri.port : 443,
|
||||
);
|
||||
final HttpClientRequest req = await client
|
||||
.openUrl(method, target)
|
||||
.timeout(Duration(milliseconds: _options.connectTimeoutMs));
|
||||
req.headers.host = uri.host;
|
||||
headers?.forEach((String key, String value) {
|
||||
if (key.toLowerCase() == 'host') {
|
||||
return;
|
||||
}
|
||||
req.headers.set(key, value);
|
||||
});
|
||||
if (body != null && body.isNotEmpty) {
|
||||
req.add(body);
|
||||
}
|
||||
|
||||
final HttpClientResponse resp =
|
||||
await req.close().timeout(Duration(milliseconds: _options.readTimeoutMs));
|
||||
final List<int> payload =
|
||||
await resp.fold<List<int>>(<int>[], (List<int> previous, List<int> element) {
|
||||
previous.addAll(element);
|
||||
return previous;
|
||||
});
|
||||
final Map<String, List<String>> responseHeaders = <String, List<String>>{};
|
||||
resp.headers.forEach((String name, List<String> values) {
|
||||
responseHeaders[name] = values;
|
||||
});
|
||||
return AliyunHttpdnsRequestResult(
|
||||
statusCode: resp.statusCode,
|
||||
headers: responseHeaders,
|
||||
body: Uint8List.fromList(payload),
|
||||
usedIp: ip,
|
||||
);
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
throw HttpException('TLS_EMPTY_SNI_FAILED: all ip connect attempts failed, error=$lastError');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: aliyun_httpdns
|
||||
description: "Aliyun HTTPDNS Flutter plugin."
|
||||
version: 1.0.2
|
||||
version: 1.0.0
|
||||
homepage: https://help.aliyun.com/document_detail/2584339.html
|
||||
|
||||
environment:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "AlicloudHTTPDNS"
|
||||
s.version = "3.4.1"
|
||||
s.version = "1.0.0"
|
||||
s.summary = "Aliyun Mobile Service HTTPDNS iOS SDK (source distribution)."
|
||||
s.description = <<-DESC
|
||||
HTTPDNS iOS SDK 源码分发版本,提供通过 HTTP(S) 进行域名解析、
|
||||
@@ -27,6 +27,7 @@ Pod::Spec.new do |s|
|
||||
s.public_header_files = [
|
||||
"AlicloudHttpDNS/AlicloudHttpDNS.h",
|
||||
"AlicloudHttpDNS/HttpdnsService.h",
|
||||
"AlicloudHttpDNS/HttpdnsEdgeService.h",
|
||||
"AlicloudHttpDNS/Model/HttpdnsResult.h",
|
||||
"AlicloudHttpDNS/Model/HttpdnsRequest.h",
|
||||
"AlicloudHttpDNS/Log/HttpdnsLog.h",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import <AlicloudHTTPDNS/HttpdnsService.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsRequest.h>
|
||||
#import <AlicloudHTTPDNS/HttpDnsResult.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsEdgeService.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsLoggerProtocol.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsDegradationDelegate.h>
|
||||
#import <AlicloudHTTPDNS/HttpdnsIpStackDetector.h>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#ifndef PublicConstant_h
|
||||
#define PublicConstant_h
|
||||
|
||||
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"3.4.1";
|
||||
static NSString *const HTTPDNS_IOS_SDK_VERSION = @"1.0.0";
|
||||
|
||||
#define ALICLOUD_HTTPDNS_DEFAULT_REGION_KEY @"cn"
|
||||
#define ALICLOUD_HTTPDNS_HONGKONG_REGION_KEY @"hk"
|
||||
|
||||
36
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.h
Normal file
36
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsEdgeResolveResult : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *requestId;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv4s;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *ipv6s;
|
||||
@property (nonatomic, assign) NSInteger ttl;
|
||||
|
||||
@end
|
||||
|
||||
@interface HttpdnsEdgeService : NSObject
|
||||
|
||||
- (instancetype)initWithAppId:(NSString *)appId
|
||||
primaryServiceHost:(NSString *)primaryServiceHost
|
||||
backupServiceHost:(nullable NSString *)backupServiceHost
|
||||
servicePort:(NSInteger)servicePort
|
||||
signSecret:(nullable NSString *)signSecret;
|
||||
|
||||
- (void)resolveHost:(NSString *)host
|
||||
queryType:(NSString *)queryType
|
||||
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable result, NSError *_Nullable error))completion;
|
||||
|
||||
/// Connect by IP + HTTPS and keep Host header as business domain.
|
||||
/// This path will not fallback to domain connect.
|
||||
- (void)requestURL:(NSURL *)url
|
||||
method:(NSString *)method
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
body:(nullable NSData *)body
|
||||
completion:(void (^)(NSData *_Nullable data, NSHTTPURLResponse *_Nullable response, NSError *_Nullable error))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
277
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.m
Normal file
277
EdgeHttpDNS/sdk/ios/AlicloudHttpDNS/HttpdnsEdgeService.m
Normal file
@@ -0,0 +1,277 @@
|
||||
#import "HttpdnsEdgeService.h"
|
||||
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
|
||||
static NSString * const kHttpdnsEdgeErrorDomain = @"com.goeedge.httpdns.edge";
|
||||
|
||||
@implementation HttpdnsEdgeResolveResult
|
||||
@end
|
||||
|
||||
@interface HttpdnsEdgeService ()
|
||||
|
||||
@property (nonatomic, copy) NSString *appId;
|
||||
@property (nonatomic, copy) NSString *primaryServiceHost;
|
||||
@property (nonatomic, copy) NSString *backupServiceHost;
|
||||
@property (nonatomic, assign) NSInteger servicePort;
|
||||
@property (nonatomic, copy) NSString *signSecret;
|
||||
@property (nonatomic, copy) NSString *sessionId;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsEdgeService
|
||||
|
||||
- (instancetype)initWithAppId:(NSString *)appId
|
||||
primaryServiceHost:(NSString *)primaryServiceHost
|
||||
backupServiceHost:(NSString *)backupServiceHost
|
||||
servicePort:(NSInteger)servicePort
|
||||
signSecret:(NSString *)signSecret {
|
||||
if (self = [super init]) {
|
||||
_appId = [appId copy];
|
||||
_primaryServiceHost = [primaryServiceHost copy];
|
||||
_backupServiceHost = backupServiceHost.length > 0 ? [backupServiceHost copy] : @"";
|
||||
_servicePort = servicePort > 0 ? servicePort : 443;
|
||||
_signSecret = signSecret.length > 0 ? [signSecret copy] : @"";
|
||||
_sessionId = [[[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""] copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)resolveHost:(NSString *)host
|
||||
queryType:(NSString *)queryType
|
||||
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable, NSError *_Nullable))completion {
|
||||
if (host.length == 0 || self.appId.length == 0 || self.primaryServiceHost.length == 0) {
|
||||
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
|
||||
code:1001
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"invalid init config or host"}];
|
||||
completion(nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *qtype = queryType.length > 0 ? queryType.uppercaseString : @"A";
|
||||
NSArray<NSString *> *hosts = self.backupServiceHost.length > 0
|
||||
? @[self.primaryServiceHost, self.backupServiceHost]
|
||||
: @[self.primaryServiceHost];
|
||||
|
||||
[self requestResolveHosts:hosts index:0 host:host qtype:qtype completion:completion];
|
||||
}
|
||||
|
||||
- (void)requestURL:(NSURL *)url
|
||||
method:(NSString *)method
|
||||
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||
body:(NSData *)body
|
||||
completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion {
|
||||
NSString *originHost = url.host ?: @"";
|
||||
if (originHost.length == 0) {
|
||||
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
|
||||
code:2001
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"invalid request host"}];
|
||||
completion(nil, nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
[self resolveHost:originHost queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
|
||||
if (error != nil || result.ipv4s.count == 0) {
|
||||
completion(nil, nil, error ?: [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
|
||||
code:2002
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"NO_IP_AVAILABLE"}]);
|
||||
return;
|
||||
}
|
||||
|
||||
[self requestByIPList:result.ipv4s
|
||||
index:0
|
||||
url:url
|
||||
method:method
|
||||
headers:headers
|
||||
body:body
|
||||
completion:completion];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)requestByIPList:(NSArray<NSString *> *)ips
|
||||
index:(NSInteger)index
|
||||
url:(NSURL *)url
|
||||
method:(NSString *)method
|
||||
headers:(NSDictionary<NSString *, NSString *> *)headers
|
||||
body:(NSData *)body
|
||||
completion:(void (^)(NSData *_Nullable, NSHTTPURLResponse *_Nullable, NSError *_Nullable))completion {
|
||||
if (index >= ips.count) {
|
||||
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
|
||||
code:2003
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"TLS_EMPTY_SNI_FAILED"}];
|
||||
completion(nil, nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *ip = ips[index];
|
||||
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
|
||||
components.host = ip;
|
||||
if (components.port == nil) {
|
||||
components.port = @(443);
|
||||
}
|
||||
|
||||
NSURL *targetURL = components.URL;
|
||||
if (targetURL == nil) {
|
||||
[self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:targetURL
|
||||
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
|
||||
timeoutInterval:8];
|
||||
request.HTTPMethod = method.length > 0 ? method : @"GET";
|
||||
if (body.length > 0) {
|
||||
request.HTTPBody = body;
|
||||
}
|
||||
|
||||
[request setValue:url.host forHTTPHeaderField:@"Host"];
|
||||
[headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
if ([key.lowercaseString isEqualToString:@"host"]) {
|
||||
return;
|
||||
}
|
||||
[request setValue:obj forHTTPHeaderField:key];
|
||||
}];
|
||||
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
config.timeoutIntervalForRequest = 8;
|
||||
config.timeoutIntervalForResource = 8;
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
||||
|
||||
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
[session finishTasksAndInvalidate];
|
||||
|
||||
if (error != nil) {
|
||||
[self requestByIPList:ips index:index + 1 url:url method:method headers:headers body:body completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
completion(data, (NSHTTPURLResponse *)response, nil);
|
||||
}] resume];
|
||||
}
|
||||
|
||||
- (void)requestResolveHosts:(NSArray<NSString *> *)hosts
|
||||
index:(NSInteger)index
|
||||
host:(NSString *)host
|
||||
qtype:(NSString *)qtype
|
||||
completion:(void (^)(HttpdnsEdgeResolveResult *_Nullable result, NSError *_Nullable error))completion {
|
||||
if (index >= hosts.count) {
|
||||
NSError *error = [NSError errorWithDomain:kHttpdnsEdgeErrorDomain
|
||||
code:1002
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"resolve failed on all service hosts"}];
|
||||
completion(nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *serviceHost = hosts[index];
|
||||
NSURLComponents *components = [NSURLComponents new];
|
||||
components.scheme = @"https";
|
||||
components.host = serviceHost;
|
||||
components.port = @(self.servicePort);
|
||||
components.path = @"/resolve";
|
||||
|
||||
NSMutableArray<NSURLQueryItem *> *items = [NSMutableArray arrayWithArray:@[
|
||||
[NSURLQueryItem queryItemWithName:@"appId" value:self.appId],
|
||||
[NSURLQueryItem queryItemWithName:@"dn" value:host],
|
||||
[NSURLQueryItem queryItemWithName:@"qtype" value:qtype],
|
||||
[NSURLQueryItem queryItemWithName:@"sid" value:self.sessionId],
|
||||
[NSURLQueryItem queryItemWithName:@"sdk_version" value:@"ios-native-1.0.0"],
|
||||
[NSURLQueryItem queryItemWithName:@"os" value:@"ios"],
|
||||
]];
|
||||
|
||||
if (self.signSecret.length > 0) {
|
||||
NSString *exp = [NSString stringWithFormat:@"%ld", (long)([[NSDate date] timeIntervalSince1970] + 600)];
|
||||
NSString *nonce = [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""];
|
||||
NSString *raw = [NSString stringWithFormat:@"%@|%@|%@|%@|%@",
|
||||
self.appId,
|
||||
host.lowercaseString,
|
||||
qtype.uppercaseString,
|
||||
exp,
|
||||
nonce];
|
||||
NSString *sign = [self hmacSha256Hex:raw secret:self.signSecret];
|
||||
[items addObject:[NSURLQueryItem queryItemWithName:@"exp" value:exp]];
|
||||
[items addObject:[NSURLQueryItem queryItemWithName:@"nonce" value:nonce]];
|
||||
[items addObject:[NSURLQueryItem queryItemWithName:@"sign" value:sign]];
|
||||
}
|
||||
|
||||
components.queryItems = items;
|
||||
NSURL *url = components.URL;
|
||||
if (url == nil) {
|
||||
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
|
||||
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
|
||||
timeoutInterval:6];
|
||||
request.HTTPMethod = @"GET";
|
||||
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
config.timeoutIntervalForRequest = 6;
|
||||
config.timeoutIntervalForResource = 6;
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
||||
|
||||
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
[session finishTasksAndInvalidate];
|
||||
|
||||
if (error != nil || data.length == 0) {
|
||||
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (![json isKindOfClass:NSDictionary.class]) {
|
||||
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *code = [NSString stringWithFormat:@"%@", json[@"code"] ?: @""];
|
||||
if (![code.uppercaseString isEqualToString:@"SUCCESS"]) {
|
||||
[self requestResolveHosts:hosts index:index + 1 host:host qtype:qtype completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *dataJSON = [json[@"data"] isKindOfClass:NSDictionary.class] ? json[@"data"] : @{};
|
||||
NSArray *records = [dataJSON[@"records"] isKindOfClass:NSArray.class] ? dataJSON[@"records"] : @[];
|
||||
|
||||
NSMutableArray<NSString *> *ipv4 = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *ipv6 = [NSMutableArray array];
|
||||
for (NSDictionary *row in records) {
|
||||
NSString *type = [NSString stringWithFormat:@"%@", row[@"type"] ?: @""];
|
||||
NSString *ip = [NSString stringWithFormat:@"%@", row[@"ip"] ?: @""];
|
||||
if (ip.length == 0) {
|
||||
continue;
|
||||
}
|
||||
if ([type.uppercaseString isEqualToString:@"AAAA"]) {
|
||||
[ipv6 addObject:ip];
|
||||
} else {
|
||||
[ipv4 addObject:ip];
|
||||
}
|
||||
}
|
||||
|
||||
HttpdnsEdgeResolveResult *result = [HttpdnsEdgeResolveResult new];
|
||||
result.requestId = [NSString stringWithFormat:@"%@", json[@"requestId"] ?: @""];
|
||||
result.ipv4s = ipv4;
|
||||
result.ipv6s = ipv6;
|
||||
result.ttl = [dataJSON[@"ttl"] integerValue];
|
||||
completion(result, nil);
|
||||
}] resume];
|
||||
}
|
||||
|
||||
- (NSString *)hmacSha256Hex:(NSString *)data secret:(NSString *)secret {
|
||||
NSData *keyData = [secret dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *messageData = [data dataUsingEncoding:NSUTF8StringEncoding];
|
||||
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
|
||||
CCHmac(kCCHmacAlgSHA256,
|
||||
keyData.bytes,
|
||||
keyData.length,
|
||||
messageData.bytes,
|
||||
messageData.length,
|
||||
cHMAC);
|
||||
|
||||
NSMutableString *result = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
||||
[result appendFormat:@"%02x", cHMAC[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,73 +1,58 @@
|
||||
# Alicloud HTTPDNS iOS SDK
|
||||
# HTTPDNS iOS SDK (SNI Hidden v1.0.0)
|
||||
|
||||
面向 iOS 的 HTTP/HTTPS DNS 解析 SDK,提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 iOS 12.0。
|
||||
## 1. Init
|
||||
|
||||
## 功能特性
|
||||
- 鉴权请求与可选 AES 传输加密
|
||||
- IPv4/IPv6 双栈解析,支持自动/同时解析
|
||||
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
|
||||
- 预解析、区域路由、网络切换自动刷新
|
||||
- 可定制日志回调与会话追踪 `sessionId`
|
||||
|
||||
## 安装(CocoaPods)
|
||||
在 `Podfile` 中添加:
|
||||
|
||||
```ruby
|
||||
platform :ios, '12.0'
|
||||
target 'YourApp' do
|
||||
pod 'AlicloudHTTPDNS', '~> 3.4.1'
|
||||
end
|
||||
```
|
||||
|
||||
执行 `pod install` 安装依赖。
|
||||
|
||||
## 快速开始
|
||||
Objective‑C
|
||||
```objc
|
||||
#import <AlicloudHttpDNS/AlicloudHttpDNS.h>
|
||||
#import <AlicloudHTTPDNS/AlicloudHTTPDNS.h>
|
||||
|
||||
// 使用鉴权初始化(单例);密钥请勿硬编码到仓库
|
||||
HttpDnsService *service = [[HttpDnsService alloc] initWithAccountID:1000000
|
||||
secretKey:@"<YOUR_SECRET>"];
|
||||
[service setPersistentCacheIPEnabled:YES];
|
||||
[service setPreResolveHosts:@[@"www.aliyun.com"] byIPType:HttpdnsQueryIPTypeAuto];
|
||||
|
||||
// 同步解析(会根据网络自动选择 v4/v6)
|
||||
HttpdnsResult *r = [service resolveHostSync:@"www.aliyun.com" byIpType:HttpdnsQueryIPTypeAuto];
|
||||
NSLog(@"IPv4: %@", r.ips);
|
||||
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
|
||||
initWithAppId:@"app1f1ndpo9"
|
||||
primaryServiceHost:@"httpdns-a.example.com"
|
||||
backupServiceHost:@"httpdns-b.example.com"
|
||||
servicePort:443
|
||||
signSecret:@"your-sign-secret"]; // optional if sign is enabled
|
||||
```
|
||||
|
||||
Swift
|
||||
```swift
|
||||
import AlicloudHttpDNS
|
||||
## 2. Resolve
|
||||
|
||||
_ = HttpDnsService(accountID: 1000000, secretKey: "<YOUR_SECRET>")
|
||||
let svc = HttpDnsService.sharedInstance()
|
||||
let res = svc?.resolveHostSync("www.aliyun.com", byIpType: .auto)
|
||||
print(res?.ips ?? [])
|
||||
```objc
|
||||
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"requestId=%@ ipv4=%@ ipv6=%@ ttl=%ld", result.requestId, result.ipv4s, result.ipv6s, (long)result.ttl);
|
||||
}];
|
||||
```
|
||||
|
||||
提示
|
||||
- 启动时通过 `setPreResolveHosts(_:byIPType:)` 预热热点域名。
|
||||
- 如需在刷新期间容忍 TTL 过期,可开启 `setReuseExpiredIPEnabled:YES`。
|
||||
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障。
|
||||
## 3. Official Request Adapter (IP + Host)
|
||||
|
||||
## 源码构建
|
||||
执行 `./build_xc_framework.sh` 生成 XCFramework。脚本会从 `gitlab.alibaba-inc.com` 克隆内部构建工具;外部环境建议优先使用 CocoaPods 引入。
|
||||
```objc
|
||||
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
|
||||
[service requestURL:url method:@"GET" headers:@{@"Accept": @"application/json"} body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
return;
|
||||
}
|
||||
NSLog(@"status=%ld", (long)response.statusCode);
|
||||
}];
|
||||
```
|
||||
|
||||
## 测试
|
||||
- 在 Xcode 使用 Scheme `AlicloudHttpDNSTests` 运行。
|
||||
- 含 OCMock 的用例批量执行可能出现内存问题,请单个运行。
|
||||
- 非 Mock 用例使用预置参数:AccountID `1000000`,测试域名 `*.onlyforhttpdnstest.run.place`(每年需续期)。
|
||||
Behavior is fixed:
|
||||
- Resolve by `/resolve`.
|
||||
- Connect to resolved IP over HTTPS.
|
||||
- Keep `Host` header as business domain.
|
||||
- No fallback to domain direct request.
|
||||
|
||||
## 依赖与链接
|
||||
- iOS 12.0+;需链接 `CoreTelephony`、`SystemConfiguration`
|
||||
- 额外库:`sqlite3.0`、`resolv`;`OTHER_LDFLAGS` 包含 `-ObjC -lz`
|
||||
## 4. Public Errors
|
||||
|
||||
## 安全说明
|
||||
- 切勿提交真实的 AccountID/SecretKey,請通过本地安全配置或 CI 注入。
|
||||
- 若担心设备时间偏差影响鉴权,可用 `setInternalAuthTimeBaseBySpecifyingCurrentTime:` 校正。
|
||||
- `NO_IP_AVAILABLE`
|
||||
- `TLS_EMPTY_SNI_FAILED`
|
||||
- `HOST_ROUTE_REJECTED`
|
||||
- `RESOLVE_SIGN_INVALID`
|
||||
|
||||
## Demo 与贡献
|
||||
- 示例应用:`AlicloudHttpDNSTestDemo/`
|
||||
- 贡献与提交流程请参见 `AGENTS.md`(提交信息与 PR 规范)。
|
||||
## 5. Removed Public Params
|
||||
|
||||
Do not expose legacy public parameters:
|
||||
- `accountId`
|
||||
- `serviceDomain`
|
||||
- `endpoint`
|
||||
- `aesSecretKey`
|
||||
|
||||
Reference in New Issue
Block a user