feat: support huaweicloud

This commit is contained in:
jeessy2
2020-10-09 23:29:28 +08:00
parent 750b37f643
commit 9fbff7d156
9 changed files with 502 additions and 10 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__debug_bin
.DS_Store

217
dns/huawei.go Normal file
View File

@ -0,0 +1,217 @@
package dns
import (
"bytes"
"ddns-go/config"
"ddns-go/util"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
const (
huaweicloudEndpoint string = "https://dns.myhuaweicloud.com"
)
// Huaweicloud Huaweicloud
type Huaweicloud struct {
DNSConfig config.DNSConfig
Domains
}
// HuaweicloudZonesResp zones response
type HuaweicloudZonesResp struct {
Zones []struct {
ID string
Name string
Recordsets []HuaweicloudRecordsets
}
}
// HuaweicloudRecordsResp 记录返回结果
type HuaweicloudRecordsResp struct {
Recordsets []HuaweicloudRecordsets
}
// HuaweicloudRecordsets 记录
type HuaweicloudRecordsets struct {
ID string
Name string `json:"name"`
ZoneID string `json:"zone_id"`
Status string
Type string `json:"type"`
Records []string `json:"records"`
}
// Init 初始化
func (hw *Huaweicloud) Init(conf *config.Config) {
hw.DNSConfig = conf.DNS
hw.Domains.ParseDomain(conf)
}
// AddUpdateIpv4DomainRecords 添加或更新IPV4记录
func (hw *Huaweicloud) AddUpdateIpv4DomainRecords() {
hw.addUpdateDomainRecords("A")
}
// AddUpdateIpv6DomainRecords 添加或更新IPV6记录
func (hw *Huaweicloud) AddUpdateIpv6DomainRecords() {
hw.addUpdateDomainRecords("AAAA")
}
func (hw *Huaweicloud) addUpdateDomainRecords(recordType string) {
ipAddr := hw.Ipv4Addr
domains := hw.Ipv4Domains
if recordType == "AAAA" {
ipAddr = hw.Ipv6Addr
domains = hw.Ipv6Domains
}
if ipAddr == "" {
return
}
for _, domain := range domains {
var records HuaweicloudRecordsResp
err := hw.request(
"GET",
fmt.Sprintf(huaweicloudEndpoint+"/v2/recordsets?type=%s&name=%s", recordType, domain),
nil,
&records,
)
if err != nil {
return
}
find := false
for _, record := range records.Recordsets {
// 名称相同才更新。华为云默认是模糊搜索
if record.Name == domain.String()+"." {
// 更新
hw.modify(record, domain, recordType, ipAddr)
find = true
break
}
}
if !find {
// 新增
hw.create(domain, recordType, ipAddr)
}
}
}
// 创建
func (hw *Huaweicloud) create(domain *Domain, recordType string, ipAddr string) {
zone, err := hw.getZones(domain)
if err != nil || len(zone.Zones) == 0 {
log.Println("未能找到公网域名, 请检查域名是否添加")
return
}
zoneID := zone.Zones[0].ID
for _, z := range zone.Zones {
if z.Name == domain.DomainName+"." {
zoneID = z.ID
break
}
}
record := &HuaweicloudRecordsets{
Type: recordType,
Name: domain.String() + ".",
Records: []string{ipAddr},
}
var result HuaweicloudRecordsets
err = hw.request(
"POST",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones/%s/recordsets", zoneID),
record,
&result,
)
if err == nil && (len(result.Records) > 0 && result.Records[0] == ipAddr) {
log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
} else {
log.Printf("新增域名解析 %s 失败Status: %s", domain, result.Status)
}
}
// 修改
func (hw *Huaweicloud) modify(record HuaweicloudRecordsets, domain *Domain, recordType string, ipAddr string) {
// 相同不修改
if len(record.Records) > 0 && record.Records[0] == ipAddr {
log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var request map[string]interface{} = make(map[string]interface{})
request["records"] = []string{ipAddr}
var result HuaweicloudRecordsets
err := hw.request(
"PUT",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones/%s/recordsets/%s", record.ZoneID, record.ID),
&request,
&result,
)
if err == nil && (len(result.Records) > 0 && result.Records[0] == ipAddr) {
log.Printf("更新域名解析 %s 成功IP: %s, 状态: %s", domain, ipAddr, result.Status)
} else {
log.Printf("更新域名解析 %s 失败Status: %s", domain, result.Status)
}
}
// 获得域名记录列表
func (hw *Huaweicloud) getZones(domain *Domain) (result HuaweicloudZonesResp, err error) {
err = hw.request(
"GET",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones?name=%s", domain.DomainName),
nil,
&result,
)
return
}
// request 统一请求接口
func (hw *Huaweicloud) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
log.Println("http.NewRequest失败. Error: ", err)
return
}
s := util.Signer{
Key: hw.DNSConfig.ID,
Secret: hw.DNSConfig.Secret,
}
s.Sign(req)
req.Header.Add("content-type", "application/json")
clt := http.Client{}
clt.Timeout = 1 * time.Minute
resp, err := clt.Do(req)
err = util.GetHTTPResponse(resp, url, err, result)
return
}

View File

@ -78,6 +78,8 @@ func RunOnce() {
dnsSelected = &Dnspod{}
case "cloudflare":
dnsSelected = &Cloudflare{}
case "huaweicloud":
dnsSelected = &Huaweicloud{}
default:
dnsSelected = &Alidns{}
}

View File

@ -183,7 +183,7 @@ func bootstrapMinCss() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "bootstrap.min.css", size: 160403, mode: os.FileMode(420), modTime: time.Unix(1598592638, 0)}
info := bindataFileInfo{name: "bootstrap.min.css", size: 160403, mode: os.FileMode(420), modTime: time.Unix(1599874470, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -203,7 +203,7 @@ func commonCss() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "common.css", size: 1053, mode: os.FileMode(420), modTime: time.Unix(1599713357, 0)}
info := bindataFileInfo{name: "common.css", size: 1053, mode: os.FileMode(420), modTime: time.Unix(1599874470, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -223,7 +223,7 @@ func faviconIco() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "favicon.ico", size: 4286, mode: os.FileMode(420), modTime: time.Unix(1598592638, 0)}
info := bindataFileInfo{name: "favicon.ico", size: 4286, mode: os.FileMode(420), modTime: time.Unix(1599874470, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -243,7 +243,7 @@ func jquery351MinJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "jquery-3.5.1.min.js", size: 89476, mode: os.FileMode(420), modTime: time.Unix(1599812064, 0)}
info := bindataFileInfo{name: "jquery-3.5.1.min.js", size: 89476, mode: os.FileMode(420), modTime: time.Unix(1599874470, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@ -64,6 +64,12 @@
Cloudflare
</label>
</div>
<div class="form-check form-check-inline col-form-label">
<input class="form-check-input" type="radio" name="DnsName" id="huaweicloud" value="huaweicloud" onclick="huaweicloudCheckedFun()" {{if eq $.DNS.Name "huaweicloud"}}checked{{end}}>
<label class="form-check-label" for="huaweicloud">
华为云
</label>
</div>
<small id="dns_help" class="form-text text-muted"></small>
</div>
</div>
@ -224,7 +230,7 @@
document.getElementById("dnsIdLabel").innerHTML = "AccessKey ID"
document.getElementById("dnsSecretLabel").innerHTML = "AccessKey Secret"
document.getElementById("dns_help").innerHTML = "https://ram.console.aliyun.com/manage/ak"
document.getElementById("dns_help").innerHTML = "<a target='_blank' href='https://ram.console.aliyun.com/manage/ak?spm=5176.12818093.nav-right.dak.488716d0mHaMgg'>创建 AccessKey</a>"
}
function dnspodCheckedFun() {
@ -234,7 +240,7 @@
document.getElementById("dnsIdLabel").innerHTML = "ID"
document.getElementById("dnsSecretLabel").innerHTML = "Token"
document.getElementById("dns_help").innerHTML = "https://console.dnspod.cn/account/token"
document.getElementById("dns_help").innerHTML = "<a target='_blank' href='https://console.dnspod.cn/account/token'>创建密钥</a>"
}
function cloudflareCheckedFun() {
@ -243,7 +249,17 @@
document.getElementById("DnsID").disabled= true
document.getElementById("DnsID").value= ""
document.getElementById("dnsSecretLabel").innerHTML = "Token"
document.getElementById("dns_help").innerHTML = "https://dash.cloudflare.com/profile/api-tokens"
document.getElementById("dns_help").innerHTML = "<a target='_blank' href='https://dash.cloudflare.com/profile/api-tokens'>创建令牌->编辑区域 DNS(使用模板)</a>"
}
function huaweicloudCheckedFun() {
document.getElementById("DnsID").disabled= false
if (beforeDnsID)
document.getElementById("DnsID").value= beforeDnsID
document.getElementById("dnsIdLabel").innerHTML = "Access Key Id"
document.getElementById("dnsSecretLabel").innerHTML = "Secret Access Key"
document.getElementById("dns_help").innerHTML = "<a target='_blank' href='https://console.huaweicloud.com/iam/?locale=zh-cn#/mine/accessKey'>新增访问密钥</a>"
}
var dnsName = '{{$.DNS.Name}}'
@ -261,6 +277,10 @@
cloudflareCheckedFun()
break;
}
case "huaweicloud": {
huaweicloudCheckedFun()
break;
}
default: {
alidnsCheckedFun()
break;

42
util/escape.go Normal file
View File

@ -0,0 +1,42 @@
// based on https://github.com/golang/go/blob/master/src/net/url/url.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package util
func shouldEscape(c byte) bool {
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c == '-' || c == '~' || c == '.' {
return false
}
return true
}
func escape(s string) string {
hexCount := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
hexCount++
}
}
if hexCount == 0 {
return s
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case shouldEscape(c):
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
default:
t[j] = s[i]
j++
}
}
return string(t)
}

View File

@ -20,11 +20,12 @@ func GetHTTPResponse(resp *http.Response, url string, err error, result interfac
log.Printf("请求接口%s失败! ERROR: %s\n", url, err)
}
if resp.StatusCode != 200 {
if resp.StatusCode != 200 && resp.StatusCode != 202 {
log.Printf("请求接口%s失败! %s\n", url, string(body))
err = fmt.Errorf("请求接口%s失败! %s", url, string(body))
}
// log.Println(string(body))
err = json.Unmarshal(body, &result)
if err != nil {

208
util/signer.go Normal file
View File

@ -0,0 +1,208 @@
// HWS API Gateway Signature
// based on https://github.com/datastream/aws/blob/master/signv4.go
// Copyright (c) 2014, Xianjie
package util
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
)
const (
BasicDateFormat = "20060102T150405Z"
Algorithm = "SDK-HMAC-SHA256"
HeaderXDate = "X-Sdk-Date"
HeaderHost = "host"
HeaderAuthorization = "Authorization"
HeaderContentSha256 = "X-Sdk-Content-Sha256"
)
func hmacsha256(key []byte, data string) ([]byte, error) {
h := hmac.New(sha256.New, []byte(key))
if _, err := h.Write([]byte(data)); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// Build a CanonicalRequest from a regular request string
//
// CanonicalRequest =
// HTTPRequestMethod + '\n' +
// CanonicalURI + '\n' +
// CanonicalQueryString + '\n' +
// CanonicalHeaders + '\n' +
// SignedHeaders + '\n' +
// HexEncode(Hash(RequestPayload))
func CanonicalRequest(r *http.Request, signedHeaders []string) (string, error) {
var hexencode string
var err error
if hex := r.Header.Get(HeaderContentSha256); hex != "" {
hexencode = hex
} else {
data, err := RequestPayload(r)
if err != nil {
return "", err
}
hexencode, err = HexEncodeSHA256Hash(data)
if err != nil {
return "", err
}
}
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", r.Method, CanonicalURI(r), CanonicalQueryString(r), CanonicalHeaders(r, signedHeaders), strings.Join(signedHeaders, ";"), hexencode), err
}
// CanonicalURI returns request uri
func CanonicalURI(r *http.Request) string {
pattens := strings.Split(r.URL.Path, "/")
var uri []string
for _, v := range pattens {
uri = append(uri, escape(v))
}
urlpath := strings.Join(uri, "/")
if len(urlpath) == 0 || urlpath[len(urlpath)-1] != '/' {
urlpath = urlpath + "/"
}
return urlpath
}
// CanonicalQueryString
func CanonicalQueryString(r *http.Request) string {
var keys []string
query := r.URL.Query()
for key := range query {
keys = append(keys, key)
}
sort.Strings(keys)
var a []string
for _, key := range keys {
k := escape(key)
sort.Strings(query[key])
for _, v := range query[key] {
kv := fmt.Sprintf("%s=%s", k, escape(v))
a = append(a, kv)
}
}
queryStr := strings.Join(a, "&")
r.URL.RawQuery = queryStr
return queryStr
}
// CanonicalHeaders
func CanonicalHeaders(r *http.Request, signerHeaders []string) string {
var a []string
header := make(map[string][]string)
for k, v := range r.Header {
header[strings.ToLower(k)] = v
}
for _, key := range signerHeaders {
value := header[key]
if strings.EqualFold(key, HeaderHost) {
value = []string{r.Host}
}
sort.Strings(value)
for _, v := range value {
a = append(a, key+":"+strings.TrimSpace(v))
}
}
return fmt.Sprintf("%s\n", strings.Join(a, "\n"))
}
// SignedHeaders
func SignedHeaders(r *http.Request) []string {
var a []string
for key := range r.Header {
a = append(a, strings.ToLower(key))
}
sort.Strings(a)
return a
}
// RequestPayload
func RequestPayload(r *http.Request) ([]byte, error) {
if r.Body == nil {
return []byte(""), nil
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return []byte(""), err
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
return b, err
}
// Create a "String to Sign".
func StringToSign(canonicalRequest string, t time.Time) (string, error) {
hash := sha256.New()
_, err := hash.Write([]byte(canonicalRequest))
if err != nil {
return "", err
}
return fmt.Sprintf("%s\n%s\n%x",
Algorithm, t.UTC().Format(BasicDateFormat), hash.Sum(nil)), nil
}
// Create the HWS Signature.
func SignStringToSign(stringToSign string, signingKey []byte) (string, error) {
hm, err := hmacsha256(signingKey, stringToSign)
return fmt.Sprintf("%x", hm), err
}
// HexEncodeSHA256Hash returns hexcode of sha256
func HexEncodeSHA256Hash(body []byte) (string, error) {
hash := sha256.New()
if body == nil {
body = []byte("")
}
_, err := hash.Write(body)
return fmt.Sprintf("%x", hash.Sum(nil)), err
}
// Get the finalized value for the "Authorization" header. The signature parameter is the output from SignStringToSign
func AuthHeaderValue(signature, accessKey string, signedHeaders []string) string {
return fmt.Sprintf("%s Access=%s, SignedHeaders=%s, Signature=%s", Algorithm, accessKey, strings.Join(signedHeaders, ";"), signature)
}
// Signature HWS meta
type Signer struct {
Key string
Secret string
}
// SignRequest set Authorization header
func (s *Signer) Sign(r *http.Request) error {
var t time.Time
var err error
var dt string
if dt = r.Header.Get(HeaderXDate); dt != "" {
t, err = time.Parse(BasicDateFormat, dt)
}
if err != nil || dt == "" {
t = time.Now()
r.Header.Set(HeaderXDate, t.UTC().Format(BasicDateFormat))
}
signedHeaders := SignedHeaders(r)
canonicalRequest, err := CanonicalRequest(r, signedHeaders)
if err != nil {
return err
}
stringToSign, err := StringToSign(canonicalRequest, t)
if err != nil {
return err
}
signature, err := SignStringToSign(stringToSign, []byte(s.Secret))
if err != nil {
return err
}
authValue := AuthHeaderValue(signature, s.Key, signedHeaders)
r.Header.Set(HeaderAuthorization, authValue)
return nil
}

File diff suppressed because one or more lines are too long