mirror of
https://github.com/jeessy2/ddns-go.git
synced 2025-10-20 15:33:46 +08:00
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:
@ -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
295
dns/traffic_route.go
Normal 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
|
||||
}
|
@ -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 = {
|
||||
|
141
util/traffic_route_signer.go
Normal file
141
util/traffic_route_signer.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user