diff --git a/dns/index.go b/dns/index.go
index fc05c6f05..68bf92e0c 100644
--- a/dns/index.go
+++ b/dns/index.go
@@ -59,6 +59,8 @@ func RunOnce() {
dnsSelected = &Alidns{}
case "tencentcloud":
dnsSelected = &TencentCloud{}
+ case "trafficroute":
+ dnsSelected = &TrafficRoute{}
case "dnspod":
dnsSelected = &Dnspod{}
case "cloudflare":
diff --git a/dns/traffic_route.go b/dns/traffic_route.go
new file mode 100644
index 000000000..45e8ac796
--- /dev/null
+++ b/dns/traffic_route.go
@@ -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
+}
diff --git a/static/constant.js b/static/constant.js
index 5ba4b3483..1129ab002 100644
--- a/static/constant.js
+++ b/static/constant.js
@@ -146,6 +146,18 @@ const DNS_PROVIDERS = {
"zh-cn": "开启Dynadot动态域名解析",
}
},
+ trafficroute: {
+ name: {
+ "en": "TrafficRoute",
+ "zh-cn": "火山引擎",
+ },
+ idLabel: "AccessKey",
+ secretLabel: "SecretAccessKey",
+ helpHtml: {
+ "en": "Create AccessKey",
+ "zh-cn": "创建火山引擎 API 密钥",
+ }
+ },
};
const SVG_CODE = {
diff --git a/util/traffic_route_signer.go b/util/traffic_route_signer.go
new file mode 100644
index 000000000..f800be579
--- /dev/null
+++ b/util/traffic_route_signer.go
@@ -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
+}