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 +}