Add volcengine TrafficRoute support (#1234)

* feat: add volcengine TrafficRoute DNS in web UI

* feat: add volcengine TrafficRoute DNS service logic

* feat: add volcengine TrafficRoute DNS service logic

---------

Co-authored-by: jeessy2 <6205259+jeessy2@users.noreply.github.com>
This commit is contained in:
Wang, Yijie
2024-08-29 20:42:56 +08:00
committed by GitHub
parent 771e180aa3
commit 67372c0964
4 changed files with 450 additions and 0 deletions

View File

@ -59,6 +59,8 @@ func RunOnce() {
dnsSelected = &Alidns{}
case "tencentcloud":
dnsSelected = &TencentCloud{}
case "trafficroute":
dnsSelected = &TrafficRoute{}
case "dnspod":
dnsSelected = &Dnspod{}
case "cloudflare":

295
dns/traffic_route.go Normal file
View File

@ -0,0 +1,295 @@
package dns
import (
"encoding/json"
"net/http"
"reflect"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
trafficRouteEndpoint = "https://open.volcengineapi.com"
trafficRouteVersion = "2018-08-01"
)
// TrafficRoute trafficRoute
type TrafficRoute struct {
DNS config.DNS
Domains config.Domains
TTL int
}
// TrafficRouteRecord record
type TrafficRouteMeta struct {
ZID int `json:"ZID"`
RecordID string `json:"RecordID"` // 需要更新的解析记录的 ID
PQDN string `json:"PQDN"` // 解析记录所包含的主机名
Host string `json:"Host"` // 主机记录,即子域名的域名前缀
TTL int `json:"TTL"` // 解析记录的过期时间
Type string `json:"Type"` // 解析记录的类型
Line string `json:"Line"` // 解析记录对应的线路代号, 一般为default
Value string `json:"Value"` // 解析记录的记录值
}
// TrafficRouteZonesResp TrafficRoute zones返回结果
type TrafficRouteZonesResp struct {
Resp TrafficRouteRespMeta
Total int
Result struct {
Zones []struct {
ZID int
ZoneName string
RecordCount int
}
Total int
}
}
// TrafficRouteResp 修改/添加返回结果
type TrafficRouteRecordsResp struct {
Resp TrafficRouteRespMeta
Result struct {
TotalCount int
Records []TrafficRouteMeta
}
}
// TrafficRouteStatus TrafficRoute 返回状态
// https://www.volcengine.com/docs/6758/155089
type TrafficRouteStatus struct {
Resp TrafficRouteRespMeta
Result struct {
ZoneName string
Status bool
RecordCount int
}
}
// TrafficRoute 公共状态
type TrafficRouteRespMeta struct {
RequestId string
Action string
Version string
Service string
Region string
Error struct {
CodeN int
Code string
Message string
MessageCN string
}
}
func (tr *TrafficRoute) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
tr.Domains.Ipv4Cache = ipv4cache
tr.Domains.Ipv6Cache = ipv6cache
tr.DNS = dnsConf.DNS
tr.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认 600s
tr.TTL = 600
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
tr.TTL = 600
} else {
tr.TTL = ttl
}
}
}
// AddUpdateDomainRecords 添加或更新 IPv4/IPv6 记录
func (tr *TrafficRoute) AddUpdateDomainRecords() config.Domains {
tr.addUpdateDomainRecords("A")
tr.addUpdateDomainRecords("AAAA")
return tr.Domains
}
func (tr *TrafficRoute) addUpdateDomainRecords(recordType string) {
ipAddr, domains := tr.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
// 获取域名列表
ZoneResp, err := tr.listZones()
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if ZoneResp.Result.Total == 0 {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
return
}
zoneID := ZoneResp.Result.Zones[0].ZID
var recordResp TrafficRouteRecordsResp
record := &TrafficRouteMeta{
ZID: zoneID,
}
err = tr.request(
"GET",
"ListRecords",
record,
&recordResp,
)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if recordResp.Result.Records == nil {
util.Log("查询域名信息发生异常! %s", recordResp.Resp.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
return
}
find := false
for _, record := range recordResp.Result.Records {
if record.Type == recordType {
// 更新
tr.modify(record, zoneID, domain, recordType, ipAddr)
find = true
break
}
}
if !find {
// 新增
tr.create(zoneID, domain, recordType, ipAddr)
}
}
}
// create 添加记录
// CreateRecord https://www.volcengine.com/docs/6758/155104
func (tr *TrafficRoute) create(zoneID int, domain *config.Domain, recordType string, ipAddr string) {
record := &TrafficRouteMeta{
ZID: zoneID,
Host: domain.GetSubDomain(),
Type: recordType,
Value: ipAddr,
TTL: tr.TTL,
}
var status TrafficRouteStatus
err := tr.request(
"POST",
"CreateRecord",
record,
&status,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if reflect.ValueOf(status.Result.Status).IsZero() {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s, ", domain, status.Resp.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
// update 修改记录
// UpdateRecord https://www.volcengine.com/docs/6758/155106
func (tr *TrafficRoute) modify(record TrafficRouteMeta, zoneID int, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if (record.Value == ipAddr) && (record.Host == domain.GetSubDomain()) {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var status TrafficRouteStatus
record.Host = domain.GetSubDomain()
record.Type = recordType
// record.Line = "default"
record.Value = ipAddr
record.TTL = tr.TTL
err := tr.request(
"POST",
"UpdateRecord",
record,
&status,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if reflect.ValueOf(status.Result.Status).IsZero() {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s, ", domain, status.Resp.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
// List 获得域名记录列表
// ListZones https://www.volcengine.com/docs/6758/155100
func (tr *TrafficRoute) listZones() (result TrafficRouteZonesResp, err error) {
record := TrafficRouteMeta{}
err = tr.request(
"GET",
"ListZones",
record,
&result,
)
return result, err
}
// request 统一请求接口
func (tr *TrafficRoute) request(method string, action string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
var req *http.Request
// updateZoneResult, err := requestDNS("POST", map[string][]string{}, map[string]string{}, secretId, secretKey, action, body)
if action != "ListRecords" {
req, err = util.TrafficRouteSigner(method, map[string][]string{}, map[string]string{}, tr.DNS.ID, tr.DNS.Secret, action, jsonStr)
} else {
var QueryParamConv TrafficRouteMeta
jsonRes := json.Unmarshal(jsonStr, &QueryParamConv)
if jsonRes != nil {
util.Log("%v", jsonRes)
return
}
zoneID := strconv.Itoa(QueryParamConv.ZID)
QueryParam := map[string][]string{"ZID": []string{zoneID}}
req, err = util.TrafficRouteSigner(method, QueryParam, map[string]string{}, tr.DNS.ID, tr.DNS.Secret, action, []byte{})
}
if err != nil {
return err
}
client := util.CreateHTTPClient()
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return err
}

View File

@ -146,6 +146,18 @@ const DNS_PROVIDERS = {
"zh-cn": "<a target='_blank' href='https://www.dynadot.com/community/help/question/enable-DDNS'>开启Dynadot动态域名解析</a>",
}
},
trafficroute: {
name: {
"en": "TrafficRoute",
"zh-cn": "火山引擎",
},
idLabel: "AccessKey",
secretLabel: "SecretAccessKey",
helpHtml: {
"en": "<a target='_blank' href='https://console.volcengine.com/iam/keymanage/'>Create AccessKey</a>",
"zh-cn": "<a target='_blank' href='https://console.volcengine.com/iam/keymanage/'>创建火山引擎 API 密钥</a>",
}
},
};
const SVG_CODE = {

View File

@ -0,0 +1,141 @@
package util
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
const Version = "2018-08-01"
const Service = "DNS"
const Region = "cn-north-1"
const Host = "open.volcengineapi.com"
// 第一步:准备辅助函数。
// sha256非对称加密
func hmacSHA256(key []byte, content string) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(content))
return mac.Sum(nil)
}
// sha256 hash算法
func hashSHA256(content []byte) string {
h := sha256.New()
h.Write(content)
return hex.EncodeToString(h.Sum(nil))
}
// 第二步:准备需要用到的结构体定义。
// 签算请求结构体
type RequestParam struct {
Body []byte
Method string
Date time.Time
Path string
Host string
QueryList url.Values
}
// 身份证明结构体
type Credentials struct {
AccessKeyID string
SecretAccessKey string
Service string
Region string
}
// 签算结果结构体
type SignRequest struct {
XDate string
Host string
ContentType string
XContentSha256 string
Authorization string
}
// 第三步:创建一个 DNS 的 API 请求函数。签名计算的过程包含在该函数中。
func TrafficRouteSigner(method string, query map[string][]string, header map[string]string, ak string, sk string, action string, body []byte) (*http.Request, error) {
// 第四步在requestDNS中创建一个 HTTP 请求实例。
// 创建 HTTP 请求实例。该实例会在后续用到。
request, _ := http.NewRequest(method, "https://"+Host+"/", bytes.NewReader(body))
urlVales := url.Values{}
for k, v := range query {
urlVales[k] = v
}
urlVales["Action"] = []string{action}
urlVales["Version"] = []string{Version}
request.URL.RawQuery = urlVales.Encode()
for k, v := range header {
request.Header.Set(k, v)
}
// 第五步:创建身份证明。其中的 Service 和 Region 字段是固定的。ak 和 sk 分别代表 AccessKeyID 和 SecretAccessKey。同时需要初始化签名结构体。一些签名计算时需要的属性也在这里处理。
// 初始化身份证明
credential := Credentials{
AccessKeyID: ak,
SecretAccessKey: sk,
Service: Service,
Region: Region,
}
// 初始化签名结构体
requestParam := RequestParam{
Body: body,
Host: request.Host,
Path: "/",
Method: request.Method,
Date: time.Now().UTC(),
QueryList: request.URL.Query(),
}
// 第六步:接下来开始计算签名。在计算签名前,先准备好用于接收签算结果的 signResult 变量,并设置一些参数。
// 初始化签名结果的结构体
xDate := requestParam.Date.Format("20060102T150405Z")
shortXDate := xDate[:8]
XContentSha256 := hashSHA256(requestParam.Body)
contentType := "application/json"
signResult := SignRequest{
Host: requestParam.Host, // 设置Host
XContentSha256: XContentSha256, // 加密body
XDate: xDate, // 设置标准化时间
ContentType: contentType, // 设置Content-Type 为 application/json
}
// 第七步:计算 Signature 签名。
signedHeadersStr := strings.Join([]string{"content-type", "host", "x-content-sha256", "x-date"}, ";")
canonicalRequestStr := strings.Join([]string{
requestParam.Method,
requestParam.Path,
request.URL.RawQuery,
strings.Join([]string{"content-type:" + contentType, "host:" + requestParam.Host, "x-content-sha256:" + XContentSha256, "x-date:" + xDate}, "\n"),
"",
signedHeadersStr,
XContentSha256,
}, "\n")
hashedCanonicalRequest := hashSHA256([]byte(canonicalRequestStr))
credentialScope := strings.Join([]string{shortXDate, credential.Region, credential.Service, "request"}, "/")
stringToSign := strings.Join([]string{
"HMAC-SHA256",
xDate,
credentialScope,
hashedCanonicalRequest,
}, "\n")
kDate := hmacSHA256([]byte(credential.SecretAccessKey), shortXDate)
kRegion := hmacSHA256(kDate, credential.Region)
kService := hmacSHA256(kRegion, credential.Service)
kSigning := hmacSHA256(kService, "request")
signature := hex.EncodeToString(hmacSHA256(kSigning, stringToSign))
signResult.Authorization = fmt.Sprintf("HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s", credential.AccessKeyID+"/"+credentialScope, signedHeadersStr, signature)
// 第八步:将 Signature 签名写入HTTP Header 中,并发送 HTTP 请求。
// 设置经过签名的5个HTTP Header
request.Header.Set("Host", signResult.Host)
request.Header.Set("Content-Type", signResult.ContentType)
request.Header.Set("X-Date", signResult.XDate)
request.Header.Set("X-Content-Sha256", signResult.XContentSha256)
request.Header.Set("Authorization", signResult.Authorization)
return request, nil
}