diff --git a/dns/eranet.go b/dns/eranet.go index 359473e..0798c79 100644 --- a/dns/eranet.go +++ b/dns/eranet.go @@ -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 +} diff --git a/dns/nowcn.go b/dns/nowcn.go index b0c07cd..f58f12e 100644 --- a/dns/nowcn.go +++ b/dns/nowcn.go @@ -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 } diff --git a/util/string.go b/util/string.go index 5636bc6..cf45fcd 100644 --- a/util/string.go +++ b/util/string.go @@ -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 +}