Skip to content

Commit f607362

Browse files
committed
Add Tencent Cloud DNS Provider
1 parent b392ee2 commit f607362

File tree

4 files changed

+243
-4
lines changed

4 files changed

+243
-4
lines changed

pkg/ddns/cloudflare.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *
5959
}
6060

6161
func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
62-
realDomain := SplitDomain(domain)
62+
_, realDomain := SplitDomain(domain)
6363
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
6464
body, err := provider.sendRequest("GET", url, nil)
6565
if err != nil {

pkg/ddns/helper.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ func SetStringHeadersToRequest(req *http.Request, headers []string) {
3232
}
3333
}
3434

35-
// SplitDomain 获取顶级域
36-
func SplitDomain(domain string) (realDomain string) {
35+
// SplitDomain 分割域名为前缀和一级域名
36+
func SplitDomain(domain string) (prefix string, realDomain string) {
3737
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
38-
return realDomain
38+
prefix = domain[:len(domain)-len(realDomain)-1]
39+
return prefix, realDomain
3940
}

pkg/ddns/tencentcloud.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package ddns
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"log"
8+
"net/http"
9+
"crypto/hmac"
10+
"crypto/sha256"
11+
"encoding/hex"
12+
"strconv"
13+
"strings"
14+
"time"
15+
)
16+
17+
const (
18+
url = "https://dnspod.tencentcloudapi.com"
19+
)
20+
21+
type ProviderTencentCloud struct {
22+
SecretID string
23+
SecretKey string
24+
}
25+
26+
func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
27+
if domainConfig == nil {
28+
return false
29+
}
30+
31+
// 当IPv4和IPv6同时成功才算作成功
32+
var resultV4 = true
33+
var resultV6 = true
34+
if domainConfig.EnableIPv4 {
35+
if !provider.addDomainRecord(domainConfig, true) {
36+
resultV4 = false
37+
}
38+
}
39+
40+
if domainConfig.EnableIpv6 {
41+
if !provider.addDomainRecord(domainConfig, false) {
42+
resultV6 = false
43+
}
44+
}
45+
46+
return resultV4 && resultV6
47+
}
48+
49+
func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
50+
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
51+
if err != nil {
52+
log.Printf("查找 DNS 记录时出错: %s\n", err)
53+
return false
54+
}
55+
56+
if errResponse, ok := record["Error"].(map[string]interface{}); ok {
57+
if errCode, ok := errResponse["Code"].(string); ok && errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
58+
// 添加 DNS 记录
59+
return provider.createDNSRecord(domainConfig.FullDomain, domainConfig, isIpv4)
60+
} else {
61+
log.Printf("查询 DNS 记录时出错,错误代码为: %s\n", errCode)
62+
}
63+
}
64+
65+
// 默认情况下更新 DNS 记录
66+
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
67+
}
68+
69+
func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
70+
var ipType = "A"
71+
if !isIPv4 {
72+
ipType = "AAAA"
73+
}
74+
_, realDomain := SplitDomain(domain)
75+
prefix, _ := SplitDomain(domain)
76+
data := map[string]interface{}{
77+
"RecordType": ipType,
78+
"Domain": realDomain,
79+
"RecordLine": "默认",
80+
"Subdomain": prefix,
81+
}
82+
jsonData, _ := json.Marshal(data)
83+
body, err := provider.sendRequest("DescribeRecordList", jsonData)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
var res map[string]interface{}
89+
err = json.Unmarshal(body, &res)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
result := res["Response"].(map[string]interface{})
95+
return result, nil
96+
}
97+
98+
func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
99+
var ipType = "A"
100+
var ipAddr = domainConfig.Ipv4Addr
101+
if !isIPv4 {
102+
ipType = "AAAA"
103+
ipAddr = domainConfig.Ipv6Addr
104+
}
105+
_, realDomain := SplitDomain(domain)
106+
prefix, _ := SplitDomain(domain)
107+
data := map[string]interface{}{
108+
"RecordType": ipType,
109+
"RecordLine": "默认",
110+
"Domain": realDomain,
111+
"SubDomain": prefix,
112+
"Value": ipAddr,
113+
"TTL": 600,
114+
}
115+
jsonData, _ := json.Marshal(data)
116+
_, err := provider.sendRequest("CreateRecord", jsonData)
117+
return err == nil
118+
}
119+
120+
func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
121+
var ipType = "A"
122+
var ipAddr = domainConfig.Ipv4Addr
123+
if !isIPv4 {
124+
ipType = "AAAA"
125+
ipAddr = domainConfig.Ipv6Addr
126+
}
127+
_, realDomain := SplitDomain(domain)
128+
prefix, _ := SplitDomain(domain)
129+
data := map[string]interface{}{
130+
"RecordType": ipType,
131+
"RecordLine": "默认",
132+
"Domain": realDomain,
133+
"SubDomain": prefix,
134+
"Value": ipAddr,
135+
"TTL": 600,
136+
"RecordId": recordID,
137+
}
138+
jsonData, _ := json.Marshal(data)
139+
_, err := provider.sendRequest("ModifyRecord", jsonData)
140+
return err == nil
141+
}
142+
143+
// 以下为辅助方法,如发送 HTTP 请求等
144+
func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
145+
client := &http.Client{}
146+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
req.Header.Set("Content-Type", "application/json")
152+
req.Header.Set("X-TC-Version", "2021-03-23")
153+
154+
provider.signRequest(provider.SecretID, provider.SecretKey, req, action, string(data))
155+
resp, err := client.Do(req)
156+
if err != nil {
157+
return nil, err
158+
}
159+
defer func(Body io.ReadCloser) {
160+
err := Body.Close()
161+
if err != nil {
162+
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
163+
}
164+
}(resp.Body)
165+
166+
body, err := io.ReadAll(resp.Body)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
return body, nil
172+
}
173+
174+
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
175+
176+
func (provider ProviderTencentCloud) sha256hex(s string) string {
177+
b := sha256.Sum256([]byte(s))
178+
return hex.EncodeToString(b[:])
179+
}
180+
181+
func (provider ProviderTencentCloud) hmacsha256(s, key string) string {
182+
hashed := hmac.New(sha256.New, []byte(key))
183+
hashed.Write([]byte(s))
184+
return string(hashed.Sum(nil))
185+
}
186+
187+
func (provider ProviderTencentCloud) WriteString(strs ...string) string {
188+
var b strings.Builder
189+
for _, str := range strs {
190+
b.WriteString(str)
191+
}
192+
193+
return b.String()
194+
}
195+
196+
func (provider ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
197+
algorithm := "TC3-HMAC-SHA256"
198+
service := "dnspod"
199+
host := provider.WriteString(service, ".tencentcloudapi.com")
200+
timestamp := time.Now().Unix()
201+
timestampStr := strconv.FormatInt(timestamp, 10)
202+
203+
// 步骤 1:拼接规范请求串
204+
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
205+
signedHeaders := "content-type;host;x-tc-action"
206+
hashedRequestPayload := provider.sha256hex(payload)
207+
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
208+
209+
// 步骤 2:拼接待签名字符串
210+
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
211+
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
212+
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
213+
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
214+
215+
// 步骤 3:计算签名
216+
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
217+
secretService := provider.hmacsha256(service, secretDate)
218+
secretSigning := provider.hmacsha256("tc3_request", secretService)
219+
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
220+
221+
// 步骤 4:拼接 Authorization
222+
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
223+
224+
r.Header.Add("Authorization", authorization)
225+
r.Header.Set("Host", host)
226+
r.Header.Set("X-TC-Action", action)
227+
r.Header.Add("X-TC-Timestamp", timestampStr)
228+
}

service/singleton/ddns.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
3737
return ddns2.ProviderCloudflare{
3838
Secret: Conf.DDNS.AccessSecret,
3939
}, nil
40+
case "tencentcloud":
41+
return ddns2.ProviderTencentCloud{
42+
SecretID: Conf.DDNS.AccessID,
43+
SecretKey: Conf.DDNS.AccessSecret,
44+
}, nil
4045
}
4146
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
4247
}
@@ -61,6 +66,11 @@ func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
6166
return ddns2.ProviderCloudflare{
6267
Secret: profile.AccessSecret,
6368
}, nil
69+
case "tencentcloud":
70+
return ddns2.ProviderTencentCloud{
71+
SecretID: profile.AccessID,
72+
SecretKey: profile.AccessSecret,
73+
}, nil
6474
}
6575
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider))
6676
}

0 commit comments

Comments
 (0)