fix: nowcn,eranet,更换接口路径 (#1520)

* fix: v1接口会收到ip白名单限制,改用v2接口

* remove: 去掉打印测试

---------

Co-authored-by: dsuzejian <dsuzejian@now.cn>
This commit is contained in:
suguer
2025-08-01 09:20:44 +08:00
committed by GitHub
parent 9ad1065a72
commit 6836982e3b
3 changed files with 283 additions and 119 deletions

View File

@ -1,22 +1,23 @@
package dns
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
eranetRecordListAPI string = "http://api.eranet.com:2080/api/dns/describe-record-index.json"
eranetRecordModifyURL string = "http://api.eranet.com:2080/api/dns/update-domain-record.json"
eranetRecordCreateAPI string = "http://api.eranet.com:2080/api/dns/add-domain-record.json"
)
// https://partner.tnet.hk/adminCN/mode_Http_Api_detail.php
// Eranet DNS实现
type Eranet struct {
DNS config.DNS
@ -36,11 +37,11 @@ type EranetRecord struct {
}
type EranetRecordListResp struct {
EranetStatus
EranetBaseResult
Data []EranetRecord
}
type EranetStatus struct {
type EranetBaseResult struct {
RequestId string `json:"RequestId"`
Id int `json:"Id"`
Error string `json:"error"`
@ -104,22 +105,28 @@ func (eranet *Eranet) addUpdateDomainRecords(recordType string) {
// create 创建DNS记录
func (eranet *Eranet) create(domain *config.Domain, recordType string, ipAddr string) {
param := map[string]any{
param := map[string]string{
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": eranet.TTL,
}
res, err := eranet.request(eranetRecordCreateAPI, param)
res, err := eranet.request("/api/Dns/AddDomainRecord", param, "GET")
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
} else if res.Error != "" {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, res.Error)
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
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
}
}
@ -131,20 +138,27 @@ func (eranet *Eranet) modify(record EranetRecord, domain *config.Domain, recordT
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
param := map[string]any{
"Id": record.ID,
param := map[string]string{
"Id": strconv.Itoa(record.ID),
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": eranet.TTL,
}
res, err := eranet.request(eranetRecordModifyURL, param)
res, err := eranet.request("/api/Dns/UpdateDomainRecord", param, "GET")
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
} else if res.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, res.Error)
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
@ -152,43 +166,15 @@ func (eranet *Eranet) modify(record EranetRecord, domain *config.Domain, recordT
}
}
// request 发送HTTP请求
func (eranet *Eranet) request(apiAddr string, param map[string]any) (status EranetStatus, err error) {
param["auth-userid"] = eranet.DNS.ID
param["api-key"] = eranet.DNS.Secret
fullURL := apiAddr + "?" + eranet.queryParams(param)
client := util.CreateHTTPClient()
resp, err := client.Get(fullURL)
// 处理响应
err = util.GetHTTPResponse(resp, err, &status)
return
}
// getRecordList 获取域名记录列表
func (eranet *Eranet) getRecordList(domain *config.Domain, typ string) (result EranetRecordListResp, err error) {
param := map[string]any{
"Domain": domain.DomainName,
"auth-userid": eranet.DNS.ID,
"api-key": eranet.DNS.Secret,
}
fullURL := eranetRecordListAPI + "?" + eranet.queryParams(param)
client := util.CreateHTTPClient()
resp, err := client.Get(fullURL)
var response EranetRecordListResp
result = EranetRecordListResp{
Data: make([]EranetRecord, 0),
}
err = util.GetHTTPResponse(resp, err, &response)
for _, v := range response.Data {
if v.Host == domain.GetSubDomain() {
result.Data = append(result.Data, v)
break
}
param := map[string]string{
"Domain": domain.DomainName,
"Type": typ,
"Host": domain.GetSubDomain(),
}
res, err := eranet.request("/api/Dns/DescribeRecordIndex", param, "GET")
err = json.Unmarshal(res, &result)
return
}
@ -205,3 +191,98 @@ func (eranet *Eranet) queryParams(param map[string]any) string {
}
return strings.Join(queryParams, "&")
}
func (t *Eranet) sign(params map[string]string, method string) (string, error) {
// 添加公共参数
params["AccessKeyID"] = 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 := &http.Client{
Timeout: 30 * time.Second,
}
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
}

View File

@ -1,22 +1,23 @@
package dns
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
nowcnRecordListAPI string = "https://todapi.now.cn:2443/api/dns/describe-record-index.json"
nowcnRecordModifyURL string = "https://todapi.now.cn:2443/api/dns/update-domain-record.json"
nowcnRecordCreateAPI string = "https://todapi.now.cn:2443/api/dns/add-domain-record.json"
)
// https://www.todaynic.com/partner/mode_Http_Api_detail.php?target_id=d15d8028-7c4f-4a5c-9d15-3a4481c4178e
// https://www.todaynic.com/docApi/
// Nowcn nowcn DNS实现
type Nowcn struct {
DNS config.DNS
@ -38,12 +39,12 @@ type NowcnRecord struct {
// NowcnRecordListResp 记录列表响应
type NowcnRecordListResp struct {
NowcnStatus
NowcnBaseResult
Data []NowcnRecord
}
// NowcnStatus API响应状态
type NowcnStatus struct {
type NowcnBaseResult struct {
RequestId string `json:"RequestId"`
Id int `json:"Id"`
Error string `json:"error"`
@ -107,22 +108,28 @@ func (nowcn *Nowcn) addUpdateDomainRecords(recordType string) {
// create 创建DNS记录
func (nowcn *Nowcn) create(domain *config.Domain, recordType string, ipAddr string) {
param := map[string]any{
param := map[string]string{
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": nowcn.TTL,
}
res, err := nowcn.request(nowcnRecordCreateAPI, param)
res, err := nowcn.request("/api/Dns/AddDomainRecord", param, "GET")
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
} else if res.Error != "" {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, res.Error)
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
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
}
}
@ -134,20 +141,27 @@ func (nowcn *Nowcn) modify(record NowcnRecord, domain *config.Domain, recordType
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
param := map[string]any{
"Id": record.ID,
param := map[string]string{
"Id": strconv.Itoa(record.ID),
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": nowcn.TTL,
}
res, err := nowcn.request(nowcnRecordModifyURL, param)
res, err := nowcn.request("/api/Dns/UpdateDomainRecord", param, "GET")
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
} else if res.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, res.Error)
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
@ -155,56 +169,109 @@ func (nowcn *Nowcn) modify(record NowcnRecord, domain *config.Domain, recordType
}
}
// request 发送HTTP请求
func (nowcn *Nowcn) request(apiAddr string, param map[string]any) (status NowcnStatus, err error) {
param["auth-userid"] = nowcn.DNS.ID
param["api-key"] = nowcn.DNS.Secret
fullURL := apiAddr + "?" + nowcn.queryParams(param)
client := util.CreateHTTPClient()
resp, err := client.Get(fullURL)
// 处理响应
err = util.GetHTTPResponse(resp, err, &status)
return
}
// getRecordList 获取域名记录列表
func (nowcn *Nowcn) getRecordList(domain *config.Domain, typ string) (result NowcnRecordListResp, err error) {
param := map[string]any{
"Domain": domain.DomainName,
"auth-userid": nowcn.DNS.ID,
"api-key": nowcn.DNS.Secret,
}
fullURL := nowcnRecordListAPI + "?" + nowcn.queryParams(param)
client := util.CreateHTTPClient()
resp, err := client.Get(fullURL)
var response NowcnRecordListResp
result = NowcnRecordListResp{
Data: make([]NowcnRecord, 0),
}
err = util.GetHTTPResponse(resp, err, &response)
for _, v := range response.Data {
if v.Host == domain.GetSubDomain() {
result.Data = append(result.Data, v)
break
}
param := map[string]string{
"Domain": domain.DomainName,
"Type": typ,
"Host": domain.GetSubDomain(),
}
res, err := nowcn.request("/api/Dns/DescribeRecordIndex", param, "GET")
err = json.Unmarshal(res, &result)
return
}
func (nowcn *Nowcn) 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)
func (t *Nowcn) sign(params map[string]string, method string) (string, error) {
// 添加公共参数
params["AccessKeyID"] = 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)
}
}
return strings.Join(queryParams, "&")
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 *Nowcn) 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://api.now.cn"
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 := &http.Client{
Timeout: 30 * time.Second,
}
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
}

View File

@ -1,6 +1,9 @@
package util
import "strings"
import (
"net/url"
"strings"
)
// WriteString creates a new string using [strings.Builder].
func WriteString(strs ...string) string {
@ -32,3 +35,16 @@ func SplitLines(s string) []string {
return strings.Split(s, "\n")
}
func PercentEncode(value string) string {
if value == "" {
return ""
}
// 使用Go标准库进行URL编码
encoded := url.QueryEscape(value)
// 按照RFC3986规则调整编码
encoded = strings.ReplaceAll(encoded, "+", "%20")
encoded = strings.ReplaceAll(encoded, "*", "%2A")
encoded = strings.ReplaceAll(encoded, "%7E", "~")
return encoded
}