2024-04-27 01:36:36 -04:00
|
|
|
|
package ddns
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/hmac"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
2024-08-23 23:11:06 -04:00
|
|
|
|
"fmt"
|
2024-04-27 01:36:36 -04:00
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
"github.com/naiba/nezha/pkg/utils"
|
2024-04-27 01:36:36 -04:00
|
|
|
|
)
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
const te = "https://dnspod.tencentcloudapi.com"
|
|
|
|
|
|
2024-04-27 01:36:36 -04:00
|
|
|
|
type ProviderTencentCloud struct {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
secretID string
|
|
|
|
|
secretKey string
|
|
|
|
|
domainConfig *DomainConfig
|
|
|
|
|
resp *tcResp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type tcReq struct {
|
|
|
|
|
RecordType string `json:"RecordType"`
|
|
|
|
|
Domain string `json:"Domain"`
|
|
|
|
|
RecordLine string `json:"RecordLine"`
|
|
|
|
|
Subdomain string `json:"Subdomain,omitempty"`
|
|
|
|
|
SubDomain string `json:"SubDomain,omitempty"` // As is
|
|
|
|
|
Value string `json:"Value,omitempty"`
|
|
|
|
|
TTL uint32 `json:"TTL,omitempty"`
|
|
|
|
|
RecordId uint64 `json:"RecordId,omitempty"`
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
type tcResp struct {
|
|
|
|
|
Response struct {
|
|
|
|
|
RecordList []struct {
|
|
|
|
|
RecordId uint64
|
|
|
|
|
Value string
|
|
|
|
|
}
|
|
|
|
|
Error struct {
|
|
|
|
|
Code string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
|
|
|
|
|
return &ProviderTencentCloud{
|
|
|
|
|
secretID: id,
|
|
|
|
|
secretKey: key,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
if domainConfig == nil {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return fmt.Errorf("获取 DDNS 配置失败")
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
2024-08-23 23:11:06 -04:00
|
|
|
|
provider.domainConfig = domainConfig
|
2024-04-27 01:36:36 -04:00
|
|
|
|
|
|
|
|
|
// 当IPv4和IPv6同时成功才算作成功
|
2024-08-23 23:11:06 -04:00
|
|
|
|
var err error
|
|
|
|
|
if provider.domainConfig.EnableIPv4 {
|
|
|
|
|
if err = provider.addDomainRecord(true); err != nil {
|
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
if provider.domainConfig.EnableIpv6 {
|
|
|
|
|
if err = provider.addDomainRecord(false); err != nil {
|
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
|
|
|
|
|
err := provider.findDNSRecord(isIpv4)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
if err != nil {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
|
|
|
|
|
return provider.createDNSRecord(isIpv4)
|
|
|
|
|
} else if provider.resp.Response.Error.Code != "" {
|
|
|
|
|
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 默认情况下更新 DNS 记录
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return provider.updateDNSRecord(isIpv4)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
|
|
|
|
|
var ipType string
|
|
|
|
|
if isIPv4 {
|
|
|
|
|
ipType = "A"
|
|
|
|
|
} else {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
ipType = "AAAA"
|
|
|
|
|
}
|
2024-08-23 23:11:06 -04:00
|
|
|
|
|
|
|
|
|
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
|
|
|
data := &tcReq{
|
|
|
|
|
RecordType: ipType,
|
|
|
|
|
Domain: realDomain,
|
|
|
|
|
RecordLine: "默认",
|
|
|
|
|
Subdomain: prefix,
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
2024-08-23 23:11:06 -04:00
|
|
|
|
|
|
|
|
|
jsonData, _ := utils.Json.Marshal(data)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
body, err := provider.sendRequest("DescribeRecordList", jsonData)
|
|
|
|
|
if err != nil {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
provider.resp = &tcResp{}
|
|
|
|
|
err = utils.Json.Unmarshal(body, provider.resp)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
if err != nil {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return nil
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
|
|
|
|
|
var ipType, ipAddr string
|
|
|
|
|
if isIPv4 {
|
|
|
|
|
ipType = "A"
|
|
|
|
|
ipAddr = provider.domainConfig.Ipv4Addr
|
|
|
|
|
} else {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
ipType = "AAAA"
|
2024-08-23 23:11:06 -04:00
|
|
|
|
ipAddr = provider.domainConfig.Ipv6Addr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
|
|
|
data := &tcReq{
|
|
|
|
|
RecordType: ipType,
|
|
|
|
|
RecordLine: "默认",
|
|
|
|
|
Domain: realDomain,
|
|
|
|
|
SubDomain: prefix,
|
|
|
|
|
Value: ipAddr,
|
|
|
|
|
TTL: 600,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, _ := utils.Json.Marshal(data)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
_, err := provider.sendRequest("CreateRecord", jsonData)
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
|
|
|
|
|
var ipType, ipAddr string
|
|
|
|
|
if isIPv4 {
|
|
|
|
|
ipType = "A"
|
|
|
|
|
ipAddr = provider.domainConfig.Ipv4Addr
|
|
|
|
|
} else {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
ipType = "AAAA"
|
2024-08-23 23:11:06 -04:00
|
|
|
|
ipAddr = provider.domainConfig.Ipv6Addr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
|
|
|
data := &tcReq{
|
|
|
|
|
RecordType: ipType,
|
|
|
|
|
RecordLine: "默认",
|
|
|
|
|
Domain: realDomain,
|
|
|
|
|
SubDomain: prefix,
|
|
|
|
|
Value: ipAddr,
|
|
|
|
|
TTL: 600,
|
|
|
|
|
RecordId: provider.resp.Response.RecordList[0].RecordId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, _ := utils.Json.Marshal(data)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
_, err := provider.sendRequest("ModifyRecord", jsonData)
|
2024-08-23 23:11:06 -04:00
|
|
|
|
return err
|
2024-04-27 01:36:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 以下为辅助方法,如发送 HTTP 请求等
|
2024-07-06 22:19:58 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
2024-08-23 23:11:06 -04:00
|
|
|
|
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
|
2024-04-27 01:36:36 -04:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
req.Header.Set("X-TC-Version", "2021-03-23")
|
|
|
|
|
|
2024-08-23 23:11:06 -04:00
|
|
|
|
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
|
|
|
|
|
resp, err := utils.HttpClient.Do(req)
|
2024-04-27 01:36:36 -04:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer func(Body io.ReadCloser) {
|
|
|
|
|
err := Body.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
|
|
|
|
}
|
|
|
|
|
}(resp.Body)
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return body, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
|
|
|
|
|
|
2024-07-06 22:19:58 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) sha256hex(s string) string {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
b := sha256.Sum256([]byte(s))
|
|
|
|
|
return hex.EncodeToString(b[:])
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 22:19:58 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
hashed := hmac.New(sha256.New, []byte(key))
|
|
|
|
|
hashed.Write([]byte(s))
|
|
|
|
|
return string(hashed.Sum(nil))
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 22:19:58 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
var b strings.Builder
|
|
|
|
|
for _, str := range strs {
|
|
|
|
|
b.WriteString(str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 22:19:58 -04:00
|
|
|
|
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
2024-04-27 01:36:36 -04:00
|
|
|
|
algorithm := "TC3-HMAC-SHA256"
|
|
|
|
|
service := "dnspod"
|
|
|
|
|
host := provider.WriteString(service, ".tencentcloudapi.com")
|
|
|
|
|
timestamp := time.Now().Unix()
|
|
|
|
|
timestampStr := strconv.FormatInt(timestamp, 10)
|
|
|
|
|
|
|
|
|
|
// 步骤 1:拼接规范请求串
|
|
|
|
|
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
|
|
|
|
|
signedHeaders := "content-type;host;x-tc-action"
|
|
|
|
|
hashedRequestPayload := provider.sha256hex(payload)
|
|
|
|
|
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
|
|
|
|
|
|
|
|
|
|
// 步骤 2:拼接待签名字符串
|
|
|
|
|
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
|
|
|
|
|
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
|
|
|
|
|
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
|
|
|
|
|
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
|
|
|
|
|
|
|
|
|
|
// 步骤 3:计算签名
|
|
|
|
|
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
|
|
|
|
|
secretService := provider.hmacsha256(service, secretDate)
|
|
|
|
|
secretSigning := provider.hmacsha256("tc3_request", secretService)
|
|
|
|
|
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
|
|
|
|
|
|
|
|
|
|
// 步骤 4:拼接 Authorization
|
|
|
|
|
authorization := provider.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)
|
|
|
|
|
}
|