mirror of
https://github.com/jeessy2/ddns-go.git
synced 2026-10-16 21:13:17 +08:00
7aad574de4
Co-authored-by: dsuzejian <dsuzejian@now.cn>
295 lines
7.9 KiB
Go
295 lines
7.9 KiB
Go
package dns
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jeessy2/ddns-go/v6/config"
|
|
"github.com/jeessy2/ddns-go/v6/util"
|
|
)
|
|
|
|
// Eranet DNS实现
|
|
type Eranet struct {
|
|
DNS config.DNS
|
|
Domains config.Domains
|
|
TTL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type EranetRecord struct {
|
|
ID nowcnID `json:"id"`
|
|
Domain string
|
|
Host string
|
|
Type string
|
|
Value string
|
|
State int
|
|
// Name string
|
|
// Enabled string
|
|
}
|
|
|
|
type EranetRecordListResp struct {
|
|
EranetBaseResult
|
|
Data []EranetRecord
|
|
}
|
|
|
|
type EranetBaseResult struct {
|
|
RequestId string `json:"RequestId"`
|
|
Id int `json:"Id"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// Init 初始化
|
|
func (eranet *Eranet) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
|
|
eranet.Domains.Ipv4Cache = ipv4cache
|
|
eranet.Domains.Ipv6Cache = ipv6cache
|
|
eranet.DNS = dnsConf.DNS
|
|
eranet.Domains.GetNewIp(dnsConf)
|
|
if dnsConf.TTL == "" {
|
|
// 默认600s
|
|
eranet.TTL = "600"
|
|
} else {
|
|
eranet.TTL = dnsConf.TTL
|
|
}
|
|
eranet.httpClient = dnsConf.GetHTTPClient()
|
|
}
|
|
|
|
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
|
|
func (eranet *Eranet) AddUpdateDomainRecords() config.Domains {
|
|
eranet.addUpdateDomainRecords("A")
|
|
eranet.addUpdateDomainRecords("AAAA")
|
|
return eranet.Domains
|
|
}
|
|
|
|
func (eranet *Eranet) addUpdateDomainRecords(recordType string) {
|
|
ipAddr, domains := eranet.Domains.GetNewIpResult(recordType)
|
|
|
|
if ipAddr == "" {
|
|
return
|
|
}
|
|
|
|
for _, domain := range domains {
|
|
result, err := eranet.getRecordList(domain, recordType)
|
|
if err != nil {
|
|
util.Log("查询域名信息发生异常! %s", err)
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
return
|
|
}
|
|
|
|
if len(result.Data) > 0 {
|
|
// 默认第一个
|
|
recordSelected := result.Data[0]
|
|
params := domain.GetCustomParams()
|
|
if params.Has("Id") {
|
|
for i := 0; i < len(result.Data); i++ {
|
|
if result.Data[i].ID.String() == params.Get("Id") {
|
|
recordSelected = result.Data[i]
|
|
}
|
|
}
|
|
}
|
|
// 更新
|
|
eranet.modify(recordSelected, domain, recordType, ipAddr)
|
|
} else {
|
|
// 新增
|
|
eranet.create(domain, recordType, ipAddr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// create 创建DNS记录
|
|
func (eranet *Eranet) create(domain *config.Domain, recordType string, ipAddr string) {
|
|
param := map[string]string{
|
|
"Domain": domain.DomainName,
|
|
"Host": domain.GetSubDomain(),
|
|
"Type": recordType,
|
|
"Value": ipAddr,
|
|
"Ttl": eranet.TTL,
|
|
}
|
|
res, err := eranet.request("/api/Dns/AddDomainRecord", param, "GET")
|
|
if err != nil {
|
|
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
return
|
|
}
|
|
var result NowcnBaseResult
|
|
err = json.Unmarshal(res, &result)
|
|
if err != nil {
|
|
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
return
|
|
}
|
|
if result.Error != "" {
|
|
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, result.Error)
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
} else {
|
|
domain.UpdateStatus = config.UpdatedSuccess
|
|
}
|
|
}
|
|
|
|
// modify 修改DNS记录
|
|
func (eranet *Eranet) modify(record EranetRecord, domain *config.Domain, recordType string, ipAddr string) {
|
|
// 相同不修改
|
|
if record.Value == ipAddr {
|
|
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
|
|
return
|
|
}
|
|
param := map[string]string{
|
|
"Id": record.ID.String(),
|
|
"Domain": domain.DomainName,
|
|
"Host": domain.GetSubDomain(),
|
|
"Type": recordType,
|
|
"Value": ipAddr,
|
|
"Ttl": eranet.TTL,
|
|
}
|
|
res, err := eranet.request("/api/Dns/UpdateDomainRecord", param, "GET")
|
|
if err != nil {
|
|
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
return
|
|
}
|
|
var result NowcnBaseResult
|
|
err = json.Unmarshal(res, &result)
|
|
if err != nil {
|
|
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
return
|
|
}
|
|
if result.Error != "" {
|
|
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Error)
|
|
domain.UpdateStatus = config.UpdatedFailed
|
|
} else {
|
|
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
|
|
domain.UpdateStatus = config.UpdatedSuccess
|
|
}
|
|
}
|
|
|
|
// getRecordList 获取域名记录列表
|
|
func (eranet *Eranet) getRecordList(domain *config.Domain, typ string) (result EranetRecordListResp, err error) {
|
|
param := map[string]string{
|
|
"Domain": domain.DomainName,
|
|
"Type": typ,
|
|
"Host": domain.GetSubDomain(),
|
|
}
|
|
res, err := eranet.request("/api/Dns/DescribeRecordIndex", param, "GET")
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
err = json.Unmarshal(res, &result)
|
|
return
|
|
}
|
|
|
|
func (eranet *Eranet) queryParams(param map[string]any) string {
|
|
var queryParams []string
|
|
for key, value := range param {
|
|
// 只对键进行URL编码,值保持原样(特别是@符号)
|
|
encodedKey := url.QueryEscape(key)
|
|
valueStr := fmt.Sprintf("%v", value)
|
|
// 对值进行选择性编码,保留@符号
|
|
encodedValue := strings.ReplaceAll(url.QueryEscape(valueStr), "%40", "@")
|
|
encodedValue = strings.ReplaceAll(encodedValue, "%3A", ":")
|
|
queryParams = append(queryParams, encodedKey+"="+encodedValue)
|
|
}
|
|
return strings.Join(queryParams, "&")
|
|
}
|
|
|
|
func (t *Eranet) sign(params map[string]string, method string) (string, error) {
|
|
// 添加公共参数
|
|
params["AccessInstanceID"] = t.DNS.ID
|
|
params["SignatureMethod"] = "HMAC-SHA1"
|
|
params["SignatureNonce"] = fmt.Sprintf("%d", time.Now().UnixNano())
|
|
params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
|
|
|
|
// 1. 排序参数(按首字母顺序)
|
|
var keys []string
|
|
for k := range params {
|
|
if k != "Signature" { // 排除Signature参数
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// 2. 构造规范化请求字符串
|
|
var canonicalizedQuery []string
|
|
for _, k := range keys {
|
|
// URL编码参数名和参数值
|
|
encodedKey := util.PercentEncode(k)
|
|
encodedValue := util.PercentEncode(params[k])
|
|
canonicalizedQuery = append(canonicalizedQuery, encodedKey+"="+encodedValue)
|
|
}
|
|
canonicalizedQueryString := strings.Join(canonicalizedQuery, "&")
|
|
|
|
// 3. 构造待签名字符串
|
|
stringToSign := method + "&" + util.PercentEncode("/") + "&" + util.PercentEncode(canonicalizedQueryString)
|
|
|
|
// 4. 计算HMAC-SHA1签名
|
|
key := t.DNS.Secret + "&"
|
|
h := hmac.New(sha1.New, []byte(key))
|
|
h.Write([]byte(stringToSign))
|
|
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
|
|
// 5. 添加签名到参数中
|
|
params["Signature"] = signature
|
|
|
|
// 6. 重新构造最终的查询字符串(包含签名)
|
|
keys = append(keys, "Signature")
|
|
sort.Strings(keys)
|
|
var finalQuery []string
|
|
for _, k := range keys {
|
|
encodedKey := util.PercentEncode(k)
|
|
encodedValue := util.PercentEncode(params[k])
|
|
finalQuery = append(finalQuery, encodedKey+"="+encodedValue)
|
|
}
|
|
|
|
return strings.Join(finalQuery, "&"), nil
|
|
}
|
|
|
|
func (t *Eranet) request(apiPath string, params map[string]string, method string) ([]byte, error) {
|
|
// 生成签名
|
|
queryString, err := t.sign(params, method)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("生成签名失败: %v", err)
|
|
}
|
|
|
|
// 构造完整URL
|
|
baseURL := "https://www.eranet.com"
|
|
fullURL := baseURL + apiPath + "?" + queryString
|
|
|
|
// 创建HTTP请求
|
|
req, err := http.NewRequest(method, fullURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建请求失败: %v", err)
|
|
}
|
|
|
|
// 设置请求头
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
// 发送请求
|
|
client := t.httpClient
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("请求失败: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// 读取响应
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取响应失败: %v", err)
|
|
}
|
|
|
|
// 检查HTTP状态码
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return body, nil
|
|
}
|