From 9ad1065a72799cbaab51aaa811edde1c37af9264 Mon Sep 17 00:00:00 2001 From: okrc Date: Tue, 22 Jul 2025 14:35:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EEdgeOne=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20(#1514)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dns/edgeone.go | 308 +++++++++++++++++++++++++++++++++++ dns/index.go | 3 + dns/tencent_cloud.go | 2 +- static/constant.js | 12 ++ util/tencent_cloud_signer.go | 8 +- 5 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 dns/edgeone.go diff --git a/dns/edgeone.go b/dns/edgeone.go new file mode 100644 index 0000000..782a37e --- /dev/null +++ b/dns/edgeone.go @@ -0,0 +1,308 @@ +package dns + +import ( + "bytes" + "encoding/json" + "net/http" + "strconv" + + "github.com/jeessy2/ddns-go/v6/config" + "github.com/jeessy2/ddns-go/v6/util" + "golang.org/x/net/idna" +) + +// https://cloud.tencent.com/document/api/1552/80730 +const ( + edgeoneEndPoint = "https://teo.tencentcloudapi.com" + edgeoneVersion = "2022-09-01" +) + +type EdgeOne struct { + DNS config.DNS + Domains config.Domains + TTL int +} + +type EdgeOneRecord struct { + ZoneId string `json:"ZoneId"` + Name string `json:"Name"` // FullDomain + Type string `json:"Type"` // record type, e.g. A AAAA + Content string `json:"Content"` + Location string `json:"Location"` + TTL int `json:"TTL"` + Weight int `json:"Weight,omitempty"` + RecordId string `json:"RecordId,omitempty"` + Status string `json:"Status,omitempty"` +} + +type EdgeOneRecordResponse struct { + EdgeOneStatus + Response struct { + DnsRecords []EdgeOneRecord `json:"DnsRecords"` + TotalCount int `json:"TotalCount"` + } +} + +type EdgeOneZoneResponse struct { + EdgeOneStatus + Response struct { + TotalCount int `json:"TotalCount"` + Zones []struct { + ZoneId string `json:"ZoneId"` + ZoneName string `json:"ZoneName"` + } `json:"Zones"` + } +} + +type Filter struct { + Name string `json:"Name"` + Values []string `json:"Values"` +} + +type EdgeOneDescribeDns struct { + ZoneId string `json:"ZoneId,omitempty"` + Filters []Filter `json:"Filters"` +} + +// https://cloud.tencent.com/document/product/1552/80729 +type EdgeOneStatus struct { + Response struct { + Error struct { + Code string + Message string + } + } +} + +// Init 初始化 +func (eo *EdgeOne) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) { + eo.Domains.Ipv4Cache = ipv4cache + eo.Domains.Ipv6Cache = ipv6cache + eo.DNS = dnsConf.DNS + eo.Domains.GetNewIp(dnsConf) + if dnsConf.TTL == "" { + // 默认 600s + eo.TTL = 600 + } else { + ttl, err := strconv.Atoi(dnsConf.TTL) + if err != nil { + eo.TTL = 600 + } else { + eo.TTL = ttl + } + } +} + +// AddUpdateDomainRecords 添加或更新 IPv4/IPv6 记录 +func (eo *EdgeOne) AddUpdateDomainRecords() config.Domains { + eo.addUpdateDomainRecords("A") + eo.addUpdateDomainRecords("AAAA") + return eo.Domains +} + +func (eo *EdgeOne) addUpdateDomainRecords(recordType string) { + ipAddr, domains := eo.Domains.GetNewIpResult(recordType) + + if ipAddr == "" { + return + } + + for _, domain := range domains { + zoneResult, err := eo.getZone(domain.DomainName) + if err != nil || zoneResult.Response.TotalCount <= 0 || zoneResult.Response.Zones[0].ZoneName != domain.DomainName { + util.Log("查询域名信息发生异常! %s", err) + domain.UpdateStatus = config.UpdatedFailed + return + } + zoneId := zoneResult.Response.Zones[0].ZoneId + recordResult, err := eo.getRecordList(domain, recordType, zoneId) + if err != nil { + util.Log("查询域名信息发生异常! %s", err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + params := domain.GetCustomParams() + var isValid func(*EdgeOneRecord) bool + if params.Has("RecordId") { + isValid = func(r *EdgeOneRecord) bool { return r.RecordId == params.Get("RecordId") } + } else { + isValid = func(r *EdgeOneRecord) bool { + return r.Status == "enable" || r.Status == "disable" && r.Content == ipAddr + } + } + var recordSelected *EdgeOneRecord + for i := range recordResult.Response.DnsRecords { + r := &recordResult.Response.DnsRecords[i] + if isValid(r) { + recordSelected = r + break + } + } + if recordSelected != nil { + // 修改记录 + eo.modify(*recordSelected, domain, recordType, ipAddr, zoneId) + } else { + // 添加记录 + eo.create(domain, recordType, ipAddr, zoneId) + } + } +} + +// CreateDnsRecord https://cloud.tencent.com/document/product/1552/80720 +func (eo *EdgeOne) create(domain *config.Domain, recordType string, ipAddr string, ZoneId string) { + d := domain.GetFullDomain() + if domain.SubDomain == "@" { + d = domain.DomainName + } + asciiDomain, _ := idna.ToASCII(d) + record := &EdgeOneRecord{ + ZoneId: ZoneId, + Name: asciiDomain, + Type: recordType, + Content: ipAddr, + Location: eo.getLocation(domain), + TTL: eo.TTL, + } + var status EdgeOneStatus + err := eo.request( + "CreateDnsRecord", + record, + &status, + ) + + if err != nil { + util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + if status.Response.Error.Code == "" { + util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess + } else { + util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, status.Response.Error.Message) + domain.UpdateStatus = config.UpdatedFailed + } +} + +// ModifyDnsRecords https://cloud.tencent.com/document/product/1552/114252 +func (eo *EdgeOne) modify(record EdgeOneRecord, domain *config.Domain, recordType string, ipAddr string, ZoneId string) { + // 相同不修改 + if record.Content == ipAddr { + util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain) + return + } + var status EdgeOneStatus + d := domain.GetFullDomain() + if domain.SubDomain == "@" { + d = domain.DomainName + } + asciiDomain, _ := idna.ToASCII(d) + record.ZoneId = ZoneId + record.Name = asciiDomain + record.Type = recordType + record.Content = ipAddr + record.Location = eo.getLocation(domain) + record.TTL = eo.TTL + + err := eo.request( + "ModifyDnsRecords", + struct { + ZoneId string `json:"ZoneId"` + DnsRecords []EdgeOneRecord `json:"DnsRecords"` + }{ + ZoneId: ZoneId, + DnsRecords: []EdgeOneRecord{record}, + }, + &status, + ) + + if err != nil { + util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + if status.Response.Error.Code == "" { + util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess + } else { + util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, status.Response.Error.Message) + domain.UpdateStatus = config.UpdatedFailed + } +} + +func (eo *EdgeOne) getZone(domain string) (result EdgeOneZoneResponse, err error) { + asciiDomain, _ := idna.ToASCII(domain) + record := EdgeOneDescribeDns{ + Filters: []Filter{ + {Name: "zone-name", Values: []string{asciiDomain}}, + }, + } + err = eo.request( + "DescribeZones", + record, + &result, + ) + return +} + +// DescribeDnsRecords https://cloud.tencent.com/document/product/1552/80716 +func (eo *EdgeOne) getRecordList(domain *config.Domain, recordType string, ZoneId string) (result EdgeOneRecordResponse, err error) { + d := domain.GetFullDomain() + if domain.SubDomain == "@" { + d = domain.DomainName + } + asciiDomain, _ := idna.ToASCII(d) + record := EdgeOneDescribeDns{ + ZoneId: ZoneId, + Filters: []Filter{ + {Name: "name", Values: []string{asciiDomain}}, + {Name: "type", Values: []string{recordType}}, + }, + } + + err = eo.request( + "DescribeDnsRecords", + record, + &result, + ) + + return +} + +// getLocation 获取记录线路,为空返回默认 +func (eo *EdgeOne) getLocation(domain *config.Domain) string { + if domain.GetCustomParams().Has("Location") { + return domain.GetCustomParams().Get("Location") + } + return "Default" +} + +// request 统一请求接口 +func (eo *EdgeOne) 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", + edgeoneEndPoint, + bytes.NewBuffer(jsonStr), + ) + if err != nil { + return + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-TC-Version", edgeoneVersion) + + util.TencentCloudSigner(eo.DNS.ID, eo.DNS.Secret, req, action, string(jsonStr), util.EdgeOne) + + client := util.CreateHTTPClient() + resp, err := client.Do(req) + err = util.GetHTTPResponse(resp, err, result) + + return +} diff --git a/dns/index.go b/dns/index.go index f4e833d..bb50695 100644 --- a/dns/index.go +++ b/dns/index.go @@ -28,6 +28,7 @@ var ( dynadotEndpoint, dynv6Endpoint, gcoreAPIEndpoint, + edgeoneEndPoint, } Ipcache = [][2]util.IpCache{} @@ -97,6 +98,8 @@ func RunOnce() { dnsSelected = &Eranet{} case "gcore": dnsSelected = &Gcore{} + case "edgeone": + dnsSelected = &EdgeOne{} default: dnsSelected = &Alidns{} } diff --git a/dns/tencent_cloud.go b/dns/tencent_cloud.go index 98a71bf..7856a09 100644 --- a/dns/tencent_cloud.go +++ b/dns/tencent_cloud.go @@ -238,7 +238,7 @@ func (tc *TencentCloud) request(action string, data interface{}, result interfac 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)) + util.TencentCloudSigner(tc.DNS.ID, tc.DNS.Secret, req, action, string(jsonStr), util.DnsPod) client := util.CreateHTTPClient() resp, err := client.Do(req) diff --git a/static/constant.js b/static/constant.js index e60922f..1ed1fd7 100644 --- a/static/constant.js +++ b/static/constant.js @@ -227,6 +227,18 @@ const DNS_PROVIDERS = { "zh-cn": "创建 API Token", } }, + edgeone: { + name: { + "en": "Edgeone", + "zh-cn": "Edgeone", + }, + idLabel: "SecretId", + secretLabel: "SecretKey", + helpHtml: { + "en": "Create AccessKey", + "zh-cn": "创建腾讯云 API 密钥", + } + }, }; const SVG_CODE = { diff --git a/util/tencent_cloud_signer.go b/util/tencent_cloud_signer.go index 29b10ee..26a8c76 100644 --- a/util/tencent_cloud_signer.go +++ b/util/tencent_cloud_signer.go @@ -21,10 +21,14 @@ func tencentCloudHmacsha256(s, key string) string { return string(hashed.Sum(nil)) } +const ( + DnsPod = "dnspod" + EdgeOne = "teo" +) + // TencentCloudSigner 腾讯云签名方法 v3 https://cloud.tencent.com/document/api/1427/56189#Golang -func TencentCloudSigner(secretId string, secretKey string, r *http.Request, action string, payload string) { +func TencentCloudSigner(secretId string, secretKey string, r *http.Request, action string, payload string, service string) { algorithm := "TC3-HMAC-SHA256" - service := "dnspod" host := WriteString(service, ".tencentcloudapi.com") timestamp := time.Now().Unix() timestampStr := strconv.FormatInt(timestamp, 10)