Skip to content

Commit

Permalink
feat: support DNSPod API 3.0 (#713)
Browse files Browse the repository at this point in the history
* feat: support Tencent Cloud DNSPod API 3.0

https://cloud.tencent.com/document/api/1427/56193

* chore: no longer need `tencentCloudHost`

* chore(TencentCloudSigner): no longer used `fmt`

* perf(TencentCloudSigner): use `strings.Builder` to build strings

https://pkg.go.dev/strings#Builder
  • Loading branch information
WaterLemons2k committed May 26, 2023
1 parent 89166e6 commit 6645d78
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 3 deletions.
2 changes: 1 addition & 1 deletion config/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (d Domain) GetFullDomain() string {
}

// GetSubDomain 获得子域名,为空返回@
// 阿里云/dnspod/namecheap 需要
// 阿里云/腾讯云/dnspod/namecheap 需要
func (d Domain) GetSubDomain() string {
if d.SubDomain != "" {
return d.SubDomain
Expand Down
2 changes: 2 additions & 0 deletions dns/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func RunOnce() {
switch dc.DNS.Name {
case "alidns":
dnsSelected = &Alidns{}
case "tencentcloud":
dnsSelected = &TencentCloud{}
case "dnspod":
dnsSelected = &Dnspod{}
case "cloudflare":
Expand Down
235 changes: 235 additions & 0 deletions dns/tencent_cloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package dns

import (
"bytes"
"encoding/json"
"log"
"net/http"
"strconv"

"github.com/jeessy2/ddns-go/v5/config"
"github.com/jeessy2/ddns-go/v5/util"
)

const (
tencentCloudEndPoint = "https://dnspod.tencentcloudapi.com"
tencentCloudVersion = "2021-03-23"
)

// TencentCloud 腾讯云 DNSPod API 3.0 实现
// https://cloud.tencent.com/document/api/1427/56193
type TencentCloud struct {
DNS config.DNS
Domains config.Domains
TTL int
}

// TencentCloudRecord 腾讯云记录
type TencentCloudRecord struct {
Domain string `json:"Domain"`
// DescribeRecordList 不需要 SubDomain
SubDomain string `json:"SubDomain,omitempty"`
// CreateRecord/ModifyRecord 不需要 Subdomain
Subdomain string `json:"Subdomain,omitempty"`
RecordType string `json:"RecordType"`
RecordLine string `json:"RecordLine"`
// DescribeRecordList 不需要 Value
Value string `json:"Value,omitempty"`
// CreateRecord/DescribeRecordList 不需要 RecordId
RecordId int `json:"RecordId,omitempty"`
// DescribeRecordList 不需要 TTL
TTL int `json:"TTL,omitempty"`
}

// TencentCloudRecordListsResp 获取域名的解析记录列表返回结果
type TencentCloudRecordListsResp struct {
TencentCloudStatus
Response struct {
RecordCountInfo struct {
TotalCount int `json:"TotalCount"`
} `json:"RecordCountInfo"`

RecordList []TencentCloudRecord `json:"RecordList"`
}
}

// TencentCloudStatus 腾讯云返回状态
// https://cloud.tencent.com/document/product/1427/56192
type TencentCloudStatus struct {
Response struct {
Error struct {
Code string
Message string
}
}
}

func (tc *TencentCloud) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
tc.Domains.Ipv4Cache = ipv4cache
tc.Domains.Ipv6Cache = ipv6cache
tc.DNS = dnsConf.DNS
tc.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认 600s
tc.TTL = 600
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
tc.TTL = 600
} else {
tc.TTL = ttl
}
}
}

// AddUpdateDomainRecords 添加或更新 IPv4/IPv6 记录
func (tc *TencentCloud) AddUpdateDomainRecords() config.Domains {
tc.addUpdateDomainRecords("A")
tc.addUpdateDomainRecords("AAAA")
return tc.Domains
}

func (tc *TencentCloud) addUpdateDomainRecords(recordType string) {
ipAddr, domains := tc.Domains.GetNewIpResult(recordType)

if ipAddr == "" {
return
}

for _, domain := range domains {
result, err := tc.getRecordList(domain, recordType)
if err != nil {
domain.UpdateStatus = config.UpdatedFailed
return
}

if result.Response.RecordCountInfo.TotalCount > 0 {
// 默认第一个
recordSelected := result.Response.RecordList[0]
params := domain.GetCustomParams()
if params.Has("RecordId") {
for i := 0; i < result.Response.RecordCountInfo.TotalCount; i++ {
if strconv.Itoa(result.Response.RecordList[i].RecordId) == params.Get("RecordId") {
recordSelected = result.Response.RecordList[i]
}
}
}

// 修改记录
tc.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 添加记录
tc.create(domain, recordType, ipAddr)
}
}
}

// create 添加记录
// CreateRecord https://cloud.tencent.com/document/api/1427/56180
func (tc *TencentCloud) create(domain *config.Domain, recordType string, ipAddr string) {
record := &TencentCloudRecord{
Domain: domain.DomainName,
SubDomain: domain.GetSubDomain(),
RecordType: recordType,
RecordLine: tc.getRecordLine(domain),
Value: ipAddr,
TTL: tc.TTL,
}

var status TencentCloudStatus
err := tc.request(
"CreateRecord",
record,
&status,
)
if err == nil && status.Response.Error.Code == "" {
log.Printf("新增域名解析 %s 成功!IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
log.Printf("新增域名解析 %s 失败!Code: %s, Message: %s", domain, status.Response.Error.Code, status.Response.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}

// modify 修改记录
// ModifyRecord https://cloud.tencent.com/document/api/1427/56157
func (tc *TencentCloud) modify(record TencentCloudRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if record.Value == ipAddr {
log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var status TencentCloudStatus
record.Domain = domain.DomainName
record.SubDomain = domain.GetSubDomain()
record.RecordType = recordType
record.RecordLine = tc.getRecordLine(domain)
record.Value = ipAddr
record.TTL = tc.TTL
err := tc.request(
"ModifyRecord",
record,
&status,
)
if err == nil && status.Response.Error.Code == "" {
log.Printf("更新域名解析 %s 成功!IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
log.Printf("更新域名解析 %s 失败!Code: %s, Message: %s", domain, status.Response.Error.Code, status.Response.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}

// getRecordList 获取域名的解析记录列表
// DescribeRecordList https://cloud.tencent.com/document/api/1427/56166
func (tc *TencentCloud) getRecordList(domain *config.Domain, recordType string) (result TencentCloudRecordListsResp, err error) {
record := TencentCloudRecord{
Domain: domain.DomainName,
Subdomain: domain.GetSubDomain(),
RecordType: recordType,
RecordLine: tc.getRecordLine(domain),
}
err = tc.request(
"DescribeRecordList",
record,
&result,
)

return
}

// getRecordLine 获取记录线路,为空返回默认
func (tc *TencentCloud) getRecordLine(domain *config.Domain) string {
if domain.GetCustomParams().Has("RecordLine") {
return domain.GetCustomParams().Get("RecordLine")
}
return "默认"
}

// request 统一请求接口
func (tc *TencentCloud) request(action string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
"POST",
tencentCloudEndPoint,
bytes.NewBuffer(jsonStr),
)
if err != nil {
log.Println("http.NewRequest 失败. Error: ", err)
return
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", tencentCloudVersion)

util.TencentCloudSigner(tc.DNS.ID, tc.DNS.Secret, req, action, string(jsonStr))

client := util.CreateHTTPClient()
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, tencentCloudEndPoint, err, result)

return
}
14 changes: 14 additions & 0 deletions util/string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package util

import "strings"

// WriteString 使用 strings.Builder 生成字符串并返回 string
// https://pkg.go.dev/strings#Builder
func WriteString(strs ...string) string {
var b strings.Builder
for _, str := range strs {
b.WriteString(str)
}

return b.String()
}
57 changes: 57 additions & 0 deletions util/tencent_cloud_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package util

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"strings"
"time"
)

func sha256hex(s string) string {
b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:])
}

func tencentCloudHmacsha256(s, key string) string {
hashed := hmac.New(sha256.New, []byte(key))
hashed.Write([]byte(s))
return string(hashed.Sum(nil))
}

// TencentCloudSigner 腾讯云签名方法 v3 https://cloud.tencent.com/document/api/1427/56189#Golang
func TencentCloudSigner(secretId string, secretKey string, r *http.Request, action string, payload string) {
algorithm := "TC3-HMAC-SHA256"
service := "dnspod"
host := WriteString(service, ".tencentcloudapi.com")
timestamp := time.Now().Unix()
timestampStr := strconv.FormatInt(timestamp, 10)

// step 1: build canonical request string
canonicalHeaders := WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
signedHeaders := "content-type;host;x-tc-action"
hashedRequestPayload := sha256hex(payload)
canonicalRequest := WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)

// step 2: build string to sign
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
credentialScope := WriteString(date, "/", service, "/tc3_request")
hashedCanonicalRequest := sha256hex(canonicalRequest)
string2sign := WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)

// step 3: sign string
secretDate := tencentCloudHmacsha256(date, WriteString("TC3", secretKey))
secretService := tencentCloudHmacsha256(service, secretDate)
secretSigning := tencentCloudHmacsha256("tc3_request", secretService)
signature := hex.EncodeToString([]byte(tencentCloudHmacsha256(string2sign, secretSigning)))

// step 4: build authorization
authorization := WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)

r.Header.Add("Authorization", authorization)
r.Header.Set("Host", host)
r.Header.Set("X-TC-Action", action)
r.Header.Add("X-TC-Timestamp", timestampStr)
}
12 changes: 10 additions & 2 deletions web/writing.html
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,18 @@ <h5 class="portlet__head">Webhook</h5>
helpHtml: "<a target='_blank' href='https://ram.console.aliyun.com/manage/ak?spm=5176.12818093.nav-right.dak.488716d0mHaMgg'>创建 AccessKey</a>"
},
{
name: 'DnsPod(腾讯云)',
name: '腾讯云',
id: 'tencentcloud',
idLabel: 'SecretId',
secretLabel: 'SecretKey',
helpHtml: "<a target='_blank' href='https://console.dnspod.cn/account/token/apikey'>创建腾讯云 API 密钥</a>"
},
{
name: 'DnsPod',
id: 'dnspod',
idLabel: 'ID',
secretLabel: 'Token',
helpHtml: "<a target='_blank' href='https://console.dnspod.cn/account/token/token'>创建密钥</a>"
helpHtml: "<a target='_blank' href='https://console.dnspod.cn/account/token/token'>创建 DNSPod Token</a>"
},
{
name: 'Cloudflare',
Expand Down Expand Up @@ -456,6 +463,7 @@ <h5 class="portlet__head">Webhook</h5>
elemRadio = {
'DnsName': {
'alidns': document.getElementById('alidns'),
'tencentcloud': document.getElementById('tencentcloud'),
'dnspod': document.getElementById('dnspod'),
'cloudflare': document.getElementById('cloudflare'),
'huaweicloud': document.getElementById('huaweicloud'),
Expand Down

0 comments on commit 6645d78

Please sign in to comment.