├── LICENSE ├── README.md ├── _demo └── demo.go ├── aliSimple.go ├── aliSimple_test.go ├── client.go ├── client_test.go ├── go.mod ├── go.sum ├── models.go ├── models_test.go └── provider.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yu Zhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AliDNS for [`libdns`](https://github.com/libdns/libdns) 2 | ======================= 3 | [![Go Reference](https://pkg.go.dev/badge/test.svg)](https://pkg.go.dev/github.com/libdns/alidns) 4 | 5 | This package implements the [libdns interfaces](https://github.com/libdns/libdns), allowing you to manage DNS records with the [AliDNS API](https://api.aliyun.com/document/Alidns/2015-01-09/overview) ( which has a nice Go SDK implementation [here](https://github.com/aliyun/alibaba-cloud-sdk-go) ) 6 | 7 | ## Authenticating 8 | 9 | To authenticate you need to supply our AccessKeyId and AccessKeySecret to the Provider. 10 | 11 | ## Example 12 | 13 | Here's a minimal example of how to get all your DNS records using this `libdns` provider 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "github.com/libdns/alidns" 22 | ) 23 | 24 | func main() { 25 | provider := alidns.Provider{ 26 | AccKeyID: "", 27 | AccKeySecret: "", 28 | } 29 | 30 | records, err := provider.GetRecords(context.TODO(), "example.com") 31 | if err != nil { 32 | fmt.Println(err.Error()) 33 | } 34 | 35 | for _, record := range records { 36 | fmt.Printf("%s %v %s %s\n", record.Name, record.TTL.Seconds(), record.Type, record.Value) 37 | } 38 | } 39 | ``` 40 | For complete demo check [_demo/demo.go](_demo/demo.go) 41 | -------------------------------------------------------------------------------- /_demo/demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | al "github.com/libdns/alidns" 11 | l "github.com/libdns/libdns" 12 | ) 13 | 14 | func main() { 15 | accKeyID := strings.TrimSpace(os.Getenv("ACCESS_KEY_ID")) 16 | accKeySec := strings.TrimSpace(os.Getenv("ACCESS_KEY_SECRET")) 17 | if (accKeyID == "") || (accKeySec == "") { 18 | fmt.Printf("ERROR: %s\n", "ACCESS_KEY_ID or ACCESS_KEY_SECRET missing") 19 | return 20 | } 21 | 22 | zone := "" 23 | if len(os.Args) > 1 { 24 | zone = strings.TrimSpace(os.Args[1]) 25 | } 26 | if zone == "" { 27 | fmt.Printf("ERROR: %s\n", "First arg zone missing") 28 | return 29 | } 30 | 31 | fmt.Printf("Get ACCESS_KEY_ID: %s,ACCESS_KEY_SECRET: %s,ZONE: %s\n", accKeyID, accKeySec, zone) 32 | provider := al.Provider{ 33 | AccKeyID: accKeyID, 34 | AccKeySecret: accKeySec, 35 | } 36 | records, err := provider.GetRecords(context.TODO(), zone) 37 | if err != nil { 38 | fmt.Printf("ERROR: %s\n", err.Error()) 39 | return 40 | } 41 | testName := "_libdns_test" 42 | testID := "" 43 | for _, record := range records { 44 | fmt.Printf("%s (.%s): %s, %s\n", record.Name, zone, record.Value, record.Type) 45 | if testName == record.Name { 46 | testID = record.ID 47 | } 48 | } 49 | 50 | if testID == "" { 51 | fmt.Println("Creating new entry for ", testName) 52 | records, err = provider.AppendRecords(context.TODO(), zone, []l.Record{l.Record{ 53 | Type: "TXT", 54 | Name: testName, 55 | Value: fmt.Sprintf("This+is a test entry created by libdns %s", time.Now()), 56 | TTL: time.Duration(600) * time.Second, 57 | }}) 58 | if len(records) == 1 { 59 | testID = records[0].ID 60 | } 61 | if err != nil { 62 | fmt.Printf("ERROR: %s\n", err.Error()) 63 | return 64 | } 65 | } 66 | 67 | fmt.Println("Press any Key to update the test entry") 68 | fmt.Scanln() 69 | if testID != "" { 70 | fmt.Println("Replacing entry for ", testName) 71 | records, err = provider.SetRecords(context.TODO(), zone, []l.Record{l.Record{ 72 | Type: "TXT", 73 | Name: testName, 74 | Value: fmt.Sprintf("Replacement test entry created by libdns %s", time.Now()), 75 | TTL: time.Duration(605) * time.Second, 76 | ID: testID, 77 | }}) 78 | if err != nil { 79 | fmt.Printf("ERROR: %s\n", err.Error()) 80 | return 81 | } 82 | } 83 | fmt.Println("Press any Key to delete the test entry") 84 | fmt.Scanln() 85 | fmt.Println("Deleting the entry for ", testName) 86 | _, err = provider.DeleteRecords(context.TODO(), zone, records) 87 | if err != nil { 88 | fmt.Printf("ERROR: %s\n", err.Error()) 89 | return 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /aliSimple.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "math" 12 | "net/http" 13 | "net/url" 14 | "runtime" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | const defRegID string = "cn-hangzhou" 23 | const addrOfAPI string = "%s://alidns.aliyuncs.com/" 24 | 25 | // CredInfo implements param of the crediential 26 | type CredInfo struct { 27 | AccKeyID string `json:"access_key_id"` 28 | AccKeySecret string `json:"access_key_secret"` 29 | RegionID string `json:"region_id,omitempty"` 30 | } 31 | 32 | // AliClient abstructs the alidns.Client 33 | type aliClient struct { 34 | mutex sync.Mutex 35 | APIHost string 36 | reqMap []vKey 37 | sigStr string 38 | sigPwd string 39 | } 40 | 41 | // VKey implments of K-V struct 42 | type vKey struct { 43 | key string 44 | val string 45 | } 46 | 47 | func newCredInfo(pAccKeyID, pAccKeySecret, pRegionID string) *CredInfo { 48 | if pAccKeyID == "" || pAccKeySecret == "" { 49 | return nil 50 | } 51 | if len(pRegionID) == 0 { 52 | pRegionID = defRegID 53 | } 54 | return &CredInfo{ 55 | AccKeyID: pAccKeyID, 56 | AccKeySecret: pAccKeySecret, 57 | RegionID: pRegionID, 58 | } 59 | } 60 | 61 | func (c *mClient) getAliClient(cred *CredInfo, zone string) error { 62 | cl0, err := c.aClient.getAliClientSche(cred, "https") 63 | if err != nil { 64 | return err 65 | } 66 | c.aClient = cl0 67 | if zone != "" { 68 | c.getDomainInfo(context.Background(), strings.Trim(zone, ".")) 69 | } 70 | return nil 71 | } 72 | 73 | func (c *mClient) applyReq(cxt context.Context, method string, body io.Reader) (*http.Request, error) { 74 | if method == "" { 75 | method = "GET" 76 | } 77 | c0 := c.aClient 78 | c0.signReq(method) 79 | si0 := fmt.Sprintf("%s=%s", "Signature", strings.ReplaceAll(c0.sigStr, "+", "%2B")) 80 | mURL := fmt.Sprintf("%s?%s&%s", c0.APIHost, c0.reqMapToStr(), si0) 81 | req, err := http.NewRequestWithContext(cxt, method, mURL, body) 82 | req.Header.Set("Accept", "application/json") 83 | if err != nil { 84 | return &http.Request{}, err 85 | } 86 | return req, nil 87 | } 88 | 89 | func (c *aliClient) getAliClientSche(cred *CredInfo, scheme string) (*aliClient, error) { 90 | if cred == nil { 91 | return &aliClient{}, errors.New("alidns: credentials missing") 92 | } 93 | if scheme == "" { 94 | scheme = "http" 95 | } 96 | 97 | cl0 := &aliClient{ 98 | APIHost: fmt.Sprintf(addrOfAPI, scheme), 99 | reqMap: []vKey{ 100 | {key: "AccessKeyId", val: cred.AccKeyID}, 101 | {key: "Format", val: "JSON"}, 102 | {key: "SignatureMethod", val: "HMAC-SHA1"}, 103 | {key: "SignatureNonce", val: fmt.Sprintf("%d", time.Now().UnixNano())}, 104 | {key: "SignatureVersion", val: "1.0"}, 105 | {key: "Timestamp", val: time.Now().UTC().Format("2006-01-02T15:04:05Z")}, 106 | {key: "Version", val: "2015-01-09"}, 107 | }, 108 | sigStr: "", 109 | sigPwd: cred.AccKeySecret, 110 | } 111 | 112 | return cl0, nil 113 | } 114 | 115 | func (c *aliClient) signReq(method string) error { 116 | if c.sigPwd == "" || len(c.reqMap) == 0 { 117 | return errors.New("alidns: AccessKeySecret or Request(includes AccessKeyId) is Misssing") 118 | } 119 | sort.Sort(byKey(c.reqMap)) 120 | str := c.reqMapToStr() 121 | str = c.reqStrToSign(str, method) 122 | c.sigStr = signStr(str, c.sigPwd) 123 | return nil 124 | } 125 | 126 | func (c *aliClient) addReqBody(key string, value string) error { 127 | if key == "" && value == "" { 128 | return errors.New("key or value is Empty") 129 | } 130 | el := vKey{key: key, val: value} 131 | c.mutex.Lock() 132 | for _, el0 := range c.reqMap { 133 | if el.key == el0.key { 134 | c.mutex.Unlock() 135 | return errors.New("duplicate keys") 136 | } 137 | } 138 | c.reqMap = append(c.reqMap, el) 139 | c.mutex.Unlock() 140 | return nil 141 | } 142 | 143 | func (c *aliClient) setReqBody(key string, value string) error { 144 | if key == "" && value == "" { 145 | return errors.New("key or value is Empty") 146 | } 147 | el := vKey{key: key, val: value} 148 | c.mutex.Lock() 149 | for in, el0 := range c.reqMap { 150 | if el.key == el0.key { 151 | (c.reqMap)[in] = el 152 | c.mutex.Unlock() 153 | return nil 154 | } 155 | } 156 | c.mutex.Unlock() 157 | return fmt.Errorf("entry of %s not found", key) 158 | } 159 | 160 | func (c *aliClient) reqStrToSign(ins string, method string) string { 161 | if method == "" { 162 | method = "GET" 163 | } 164 | ecReq := urlEncode(ins) 165 | return fmt.Sprintf("%s&%s&%s", method, "%2F", ecReq) 166 | } 167 | 168 | func (c *aliClient) reqMapToStr() string { 169 | m0 := c.reqMap 170 | urlEn := url.Values{} 171 | c.mutex.Lock() 172 | for _, o := range m0 { 173 | urlEn.Add(o.key, o.val) 174 | } 175 | c.mutex.Unlock() 176 | return urlEn.Encode() 177 | } 178 | 179 | func signStr(ins string, sec string) string { 180 | sec = sec + "&" 181 | hm := hmac.New(sha1.New, []byte(sec)) 182 | hm.Write([]byte(ins)) 183 | sum := hm.Sum(nil) 184 | return base64.StdEncoding.EncodeToString(sum) 185 | } 186 | 187 | func goVer() float64 { 188 | verStr := runtime.Version() 189 | verStr, _ = strings.CutPrefix(verStr, "go") 190 | verStrs := strings.Split(verStr, ".") 191 | var result float64 192 | for i, v := range verStrs { 193 | tmp, _ := strconv.ParseFloat(v, 32) 194 | result = tmp * (1 / math.Pow10(i)) 195 | } 196 | return result 197 | } 198 | 199 | func urlEncode(ins string) string { 200 | str0 := ins 201 | str0 = strings.Replace(str0, "+", "%20", -1) 202 | str0 = strings.Replace(str0, "*", "%2A", -1) 203 | str0 = strings.Replace(str0, "%7E", "~", -1) 204 | 205 | str0 = url.QueryEscape(str0) 206 | if goVer() > 1.20 { 207 | str0 = strings.Replace(str0, "%26", "&", -1) 208 | } 209 | 210 | return str0 211 | } 212 | 213 | type byKey []vKey 214 | 215 | func (v byKey) Len() int { 216 | return len(v) 217 | } 218 | 219 | func (v byKey) Swap(i, j int) { 220 | v[i], v[j] = v[j], v[i] 221 | } 222 | 223 | func (v byKey) Less(i, j int) bool { 224 | return v[i].key < v[j].key 225 | } 226 | -------------------------------------------------------------------------------- /aliSimple_test.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | const AccessKeyID = "" 10 | const AccessKeySecret = "" 11 | 12 | func Test_URLEncode(t *testing.T) { 13 | s0 := urlEncode("AccessKeyId=testid&Action=DescribeDomainRecords") 14 | if s0 != "AccessKeyId%3Dtestid%26Action%3DDescribeDomainRecords" { 15 | t.Log(s0) 16 | t.Fail() 17 | } 18 | t.Log(s0) 19 | } 20 | 21 | var cl0 = &aliClient{ 22 | APIHost: fmt.Sprintf(addrOfAPI, "https"), 23 | reqMap: []vKey{ 24 | {key: "AccessKeyId", val: "testid"}, 25 | {key: "Format", val: "XML"}, 26 | {key: "Action", val: "DescribeDomainRecords"}, 27 | {key: "SignatureMethod", val: "HMAC-SHA1"}, 28 | {key: "DomainName", val: "example.com"}, 29 | {key: "SignatureVersion", val: "1.0"}, 30 | {key: "SignatureNonce", val: "f59ed6a9-83fc-473b-9cc6-99c95df3856e"}, 31 | {key: "Timestamp", val: "2016-03-24T16:41:54Z"}, 32 | {key: "Version", val: "2015-01-09"}, 33 | }, 34 | sigStr: "", 35 | sigPwd: "testsecret", 36 | } 37 | 38 | func Test_AliClintReq(t *testing.T) { 39 | str := cl0.reqMapToStr() 40 | t.Log("map to str:" + str + "\n") 41 | str = cl0.reqStrToSign(str, "GET") 42 | 43 | // validate sign string from doc: https://help.aliyun.com/document_detail/29747.html#:~:text=%E9%82%A3%E4%B9%88-,stringtosign 44 | if goVer() > 1.20 && str != "GET&%2F&AccessKeyId%3Dtestid&Action%3DDescribeDomainRecords&DomainName%3Dexample.com&Format%3DXML&SignatureMethod%3DHMAC-SHA1&SignatureNonce%3Df59ed6a9-83fc-473b-9cc6-99c95df3856e&SignatureVersion%3D1.0&Timestamp%3D2016-03-24T16%253A41%253A54Z&Version%3D2015-01-09" { 45 | t.Error("sign str error") 46 | } 47 | if goVer() < 1.20 && str != "GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeDomainRecords%26DomainName%3Dexample.com%26Format%3DXML%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3Df59ed6a9-83fc-473b-9cc6-99c95df3856e%26SignatureVersion%3D1.0%26Timestamp%3D2016-03-24T16%253A41%253A54Z%26Version%3D2015-01-09" { 48 | t.Error("sign str error") 49 | } 50 | t.Log("sign str:" + str + "\n") 51 | t.Log("signed base64:" + signStr(str, cl0.sigPwd) + "\n") 52 | 53 | } 54 | 55 | func Test_AppendDupReq(t *testing.T) { 56 | err := cl0.addReqBody("Version", "100") 57 | if err == nil { 58 | t.Fail() 59 | } 60 | } 61 | 62 | var p0 = Provider{ 63 | AccKeyID: AccessKeyID, 64 | AccKeySecret: AccessKeySecret, 65 | } 66 | 67 | func Test_RequestUrl(t *testing.T) { 68 | p0.getClient() 69 | p0.client.aClient.addReqBody("Action", "DescribeDomainRecords") 70 | p0.client.aClient.addReqBody("DomainName", "viscrop.top") 71 | p0.client.aClient.setReqBody("Timestamp", "2020-10-16T20:10:54Z") 72 | r, err := p0.client.applyReq(context.TODO(), "GET", nil) 73 | t.Log("url:", r.URL.String(), "err:", err) 74 | } 75 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | // mClient is an abstration of AliClient 13 | type mClient struct { 14 | aClient *aliClient 15 | mutex sync.Mutex 16 | } 17 | 18 | // TODO:Will complete,If we need to get Domain Info for something. 19 | func (c *mClient) getDomainInfo(ctx context.Context, zone string) error { 20 | return nil 21 | } 22 | 23 | func (c *mClient) doAPIRequest(ctx context.Context, method string, result interface{}) error { 24 | req, err := c.applyReq(ctx, method, nil) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | rsp, err := http.DefaultClient.Do(req) 30 | if err != nil { 31 | return err 32 | } 33 | defer rsp.Body.Close() 34 | 35 | var buf []byte 36 | buf, err = io.ReadAll(rsp.Body) 37 | strBody := string(buf) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | err = json.Unmarshal([]byte(strBody), result) 43 | if err != nil { 44 | return err 45 | } 46 | if rsp.StatusCode != 200 { 47 | return fmt.Errorf("get error status: HTTP %d: %+v", rsp.StatusCode, result.(*aliResult).Msg) 48 | } 49 | c.aClient = nil 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | ) 8 | 9 | func Test_ClientAPIReq(t *testing.T) { 10 | p0.getClient() 11 | p0.client.aClient.addReqBody("Action", "DescribeDomainRecords") 12 | p0.client.aClient.addReqBody("KeyWords", "vi") 13 | var rs aliDomaRecords 14 | rspData := aliResult{} 15 | err := p0.doAPIRequest(context.TODO(), &rspData) 16 | t.Log("req", p0.client.aClient, "data", rspData, "err:", err, "rs:", rs) 17 | } 18 | 19 | func Test_QueryDomainRecord(t *testing.T) { 20 | rr, name, _ := p0.queryMainDomain(context.Background(), "www.viscrop.top") 21 | r0, err := p0.queryDomainRecord(context.TODO(), rr, name, "A") 22 | t.Log("result:", r0, "err:", err) 23 | r0, err = p0.queryDomainRecord(context.TODO(), rr, name, "A", ".") 24 | t.Log("result with A rec:", r0, "err:", err) 25 | } 26 | 27 | func Test_QueryDomainRecords(t *testing.T) { 28 | _, name, _ := p0.queryMainDomain(context.Background(), "me.viscrop.top") 29 | r0, err := p0.queryDomainRecords(context.TODO(), name) 30 | t.Log("result:", r0, "err:", err) 31 | _, name, _ = p0.queryMainDomain(context.Background(), "me.viscraop.top") 32 | r0, err = p0.queryDomainRecords(context.TODO(), name) 33 | t.Log("result:", r0, "err:", err) 34 | } 35 | 36 | func Test_DomainRecordOp(t *testing.T) { 37 | dr0 := aliDomaRecord{ 38 | DName: "viscrop.top", 39 | Rr: "baidu", 40 | DTyp: "CNAME", 41 | DVal: "baidu.com", 42 | TTL: 600, 43 | } 44 | r0, err := p0.addDomainRecord(context.TODO(), dr0) 45 | t.Log("result:", r0, "err:", err) 46 | dr0, err = p0.getDomainRecord(context.TODO(), r0) 47 | t.Log("result:", dr0, "err:", err) 48 | dr0.Rr = "bai" 49 | r0, err = p0.setDomainRecord(context.TODO(), dr0) 50 | t.Log("result:", r0, "err:", err) 51 | r0, err = p0.delDomainRecord(context.TODO(), dr0) 52 | t.Log("result:", r0, "err:", err) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/libdns/alidns 2 | 3 | go 1.16 4 | 5 | require github.com/libdns/libdns v1.0.0-beta.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= 2 | github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 3 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/libdns/libdns" 9 | ) 10 | 11 | type ttl_t = uint32 12 | 13 | type aliDomaRecord struct { 14 | Rr string `json:"RR,omitempty"` 15 | Line string `json:"Line,omitempty"` 16 | Status string `json:"Status,omitempty"` 17 | Locked bool `json:"Locked,omitempty"` 18 | DTyp string `json:"Type,omitempty"` 19 | DName string `json:"DomainName,omitempty"` 20 | DVal string `json:"Value,omitempty"` 21 | RecID string `json:"RecordId,omitempty"` 22 | TTL ttl_t `json:"TTL,omitempty"` 23 | Weight int `json:"Weight,omitempty"` 24 | Priority ttl_t `json:"Priority,omitempty"` 25 | } 26 | 27 | func (r aliDomaRecord) Equals(v aliDomaRecord) bool { 28 | result := v.Rr == r.Rr 29 | result = result && v.DName == r.DName 30 | result = result && v.DVal == r.DVal 31 | result = result && v.DTyp == r.DTyp 32 | return result 33 | } 34 | 35 | type aliDomaRecords struct { 36 | Record []aliDomaRecord `json:"Record,omitempty"` 37 | } 38 | 39 | type aliResult struct { 40 | ReqID string `json:"RequestId,omitempty"` 41 | DRecords aliDomaRecords `json:"DomainRecords,omitempty"` 42 | DLvl int `json:"DomainLevel,omitempty"` 43 | DVal string `json:"Value,omitempty"` 44 | TTL ttl_t `json:"TTL,omitempty"` 45 | DName string `json:"DomainName,omitempty"` 46 | Rr string `json:"RR,omitempty"` 47 | Msg string `json:"Message,omitempty"` 48 | Rcmd string `json:"Recommend,omitempty"` 49 | HostID string `json:"HostId,omitempty"` 50 | Code string `json:"Code,omitempty"` 51 | TotalCount int `json:"TotalCount,omitempty"` 52 | PgSize int `json:"PageSize,omitempty"` 53 | PgNum int `json:"PageNumber,omitempty"` 54 | DTyp string `json:"Type,omitempty"` 55 | RecID string `json:"RecordId,omitempty"` 56 | Line string `json:"Line,omitempty"` 57 | Status string `json:"Status,omitempty"` 58 | Locked bool `json:"Locked,omitempty"` 59 | Weight int `json:"Weight,omitempty"` 60 | MinTTL int `json:"MinTtl,omitempty"` 61 | Priority ttl_t `json:"Priority,omitempty"` 62 | } 63 | 64 | func (r *aliDomaRecord) LibdnsRecord() libdns.Record { 65 | return libdns.RR{ 66 | Type: r.DTyp, 67 | Name: r.Rr, 68 | Data: r.DVal, 69 | TTL: time.Duration(r.TTL) * time.Second, 70 | } 71 | } 72 | 73 | func (r *aliResult) ToDomaRecord() aliDomaRecord { 74 | return aliDomaRecord{ 75 | RecID: r.RecID, 76 | DTyp: r.DTyp, 77 | Rr: r.Rr, 78 | DName: r.DName, 79 | DVal: r.DVal, 80 | TTL: r.TTL, 81 | Line: r.Line, 82 | Status: r.Status, 83 | Locked: r.Locked, 84 | Weight: r.Weight, 85 | Priority: r.Priority, 86 | } 87 | } 88 | 89 | // AlidnsRecord convert libdns.Record with zone to aliDomaRecord 90 | func alidnsRecord(r libdns.Record, zone ...string) aliDomaRecord { 91 | result := aliDomaRecord{} 92 | if r == nil { 93 | return result 94 | } 95 | tmpRR := r.RR() 96 | if len(zone) > 0 && len(zone[0]) > 0 { 97 | tmpZone := zone[0] 98 | result.Rr = libdns.RelativeName(tmpRR.Name, tmpZone) 99 | result.DName = strings.Trim(tmpZone, ".") 100 | } else { 101 | result.Rr = tmpRR.Name 102 | } 103 | result.DTyp = tmpRR.Type 104 | result.DVal = tmpRR.Data 105 | result.TTL = ttl_t(tmpRR.TTL.Seconds()) 106 | if svcb, svcbok := r.(libdns.ServiceBinding); svcbok { 107 | result.Priority = ttl_t(svcb.Priority) 108 | result.DVal = fmt.Sprintf("%s %s",svcb.Target,svcb.Params) 109 | } 110 | return result 111 | } 112 | -------------------------------------------------------------------------------- /models_test.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/libdns/libdns" 7 | ) 8 | 9 | func Test_aliDomainRecordWithZone(t *testing.T) { 10 | type testCase struct { 11 | memo string 12 | record libdns.Record 13 | zone string 14 | result aliDomaRecord 15 | } 16 | 17 | cases := []testCase{ 18 | { 19 | memo: "record.Name without zone", 20 | record: libdns.RR{ 21 | Name: "sub", 22 | }, 23 | zone: "mydomain.com.", 24 | result: aliDomaRecord{ 25 | Rr: "sub", 26 | DName: "mydomain.com", 27 | }, 28 | }, 29 | { 30 | memo: "record.Name with zone", 31 | record: libdns.RR{ 32 | Name: "sub.mydomain.com", 33 | }, 34 | zone: "mydomain.com.", 35 | result: aliDomaRecord{ 36 | Rr: "sub", 37 | DName: "mydomain.com", 38 | }, 39 | }, 40 | } 41 | 42 | for _, c := range cases { 43 | rec := alidnsRecord(c.record, c.zone) 44 | if !rec.Equals(c.result) { 45 | t.Log("excepted:", c.result, "got:", rec) 46 | t.Fail() 47 | } 48 | t.Log("case ", c.memo, "was pass.") 49 | } 50 | 51 | } 52 | 53 | func Test_aliDomainRecord(t *testing.T) { 54 | type testCase struct { 55 | memo string 56 | record libdns.Record 57 | result aliDomaRecord 58 | } 59 | 60 | cases := []testCase{ 61 | { 62 | memo: "normal record", 63 | record: libdns.RR{ 64 | Name: "sub", 65 | Type: "A", 66 | Data: "1.1.1.1", 67 | }, 68 | result: aliDomaRecord{ 69 | Rr: "sub", 70 | DTyp: "A", 71 | DVal: "1.1.1.1", 72 | }, 73 | }, 74 | { 75 | memo: "HTTPS record", 76 | record: libdns.ServiceBinding{ 77 | Name: "sub", 78 | Scheme: "https", 79 | Target: "target.com", 80 | Priority: 100, 81 | Params: map[string][]string{ 82 | "alpn": {"333"}, 83 | }, 84 | }, 85 | result: aliDomaRecord{ 86 | Rr: "sub", 87 | DTyp: "HTTPS", 88 | Priority: 100, 89 | DVal: "target.com alpn=333", 90 | }, 91 | }, 92 | } 93 | 94 | for _, c := range cases { 95 | rec := alidnsRecord(c.record) 96 | if !rec.Equals(c.result) { 97 | t.Log("excepted:", c.result, "got:", rec) 98 | t.Fail() 99 | return 100 | } 101 | t.Log("case ", c.memo, "was pass.") 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package alidns 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/libdns/libdns" 10 | ) 11 | 12 | // Provider implements the libdns interfaces for Alicloud. 13 | type Provider struct { 14 | client mClient 15 | // The API Key ID Required by Aliyun's for accessing the Aliyun's API 16 | AccKeyID string `json:"access_key_id"` 17 | // The API Key Secret Required by Aliyun's for accessing the Aliyun's API 18 | AccKeySecret string `json:"access_key_secret"` 19 | // Optional for identifing the region of the Aliyun's Service,The default is zh-hangzhou 20 | RegionID string `json:"region_id,omitempty"` 21 | } 22 | 23 | // AppendRecords adds records to the zone. It returns the records that were added. 24 | func (p *Provider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { 25 | var rls []libdns.Record 26 | for _, rec := range recs { 27 | ar := alidnsRecord(rec, zone) 28 | rid, err := p.addDomainRecord(ctx, ar) 29 | if err != nil { 30 | return nil, err 31 | } 32 | ar.RecID = rid 33 | rls = append(rls, ar.LibdnsRecord()) 34 | } 35 | return rls, nil 36 | } 37 | 38 | // DeleteRecords deletes the records from the zone. If a record does not have an ID, 39 | // it will be looked up. It returns the records that were deleted. 40 | func (p *Provider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { 41 | var rls []libdns.Record 42 | for _, rec := range recs { 43 | ar := alidnsRecord(rec, zone) 44 | if ar.RecID == "" { 45 | r0, err := p.queryDomainRecord(ctx, ar.Rr, ar.DName, ar.DTyp, ar.DVal) 46 | if err != nil { 47 | return nil, err 48 | } 49 | ar.RecID = r0.RecID 50 | } 51 | _, err := p.delDomainRecord(ctx, ar) 52 | if err != nil { 53 | return nil, err 54 | } 55 | rls = append(rls, ar.LibdnsRecord()) 56 | } 57 | return rls, nil 58 | } 59 | 60 | // GetRecords lists all the records in the zone. 61 | func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) { 62 | var rls []libdns.Record 63 | recs, err := p.queryDomainRecords(ctx, zone) 64 | if err != nil { 65 | return nil, err 66 | } 67 | for _, rec := range recs { 68 | rls = append(rls, rec.LibdnsRecord()) 69 | } 70 | return rls, nil 71 | } 72 | 73 | // SetRecords sets the records in the zone, either by updating existing records 74 | // or creating new ones. It returns the updated records. 75 | func (p *Provider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { 76 | var rls []libdns.Record 77 | var err error 78 | for _, rec := range recs { 79 | ar := alidnsRecord(rec, zone) 80 | if ar.RecID == "" { 81 | r0, err := p.queryDomainRecord(ctx, ar.Rr, ar.DName, ar.DTyp, ar.DVal) 82 | if err != nil { 83 | ar.RecID, err = p.addDomainRecord(ctx, ar) 84 | if err != nil { 85 | return nil, err 86 | } 87 | } else { 88 | ar.RecID = r0.RecID 89 | } 90 | } else { 91 | _, err = p.setDomainRecord(ctx, ar) 92 | if err != nil { 93 | return nil, err 94 | } 95 | } 96 | rls = append(rls, ar.LibdnsRecord()) 97 | } 98 | return rls, nil 99 | } 100 | 101 | func (p *Provider) getClient() error { 102 | return p.getClientWithZone("") 103 | } 104 | 105 | func (p *Provider) getClientWithZone(zone string) error { 106 | cred := newCredInfo(p.AccKeyID, p.AccKeySecret, p.RegionID) 107 | return p.client.getAliClient(cred, zone) 108 | } 109 | 110 | func (p *Provider) addDomainRecord(ctx context.Context, rc aliDomaRecord) (recID string, err error) { 111 | p.client.mutex.Lock() 112 | defer p.client.mutex.Unlock() 113 | p.getClientWithZone(rc.DName) 114 | if rc.TTL <= 0 { 115 | rc.TTL = 600 116 | } 117 | p.client.aClient.addReqBody("Action", "AddDomainRecord") 118 | p.client.aClient.addReqBody("DomainName", rc.DName) 119 | p.client.aClient.addReqBody("RR", rc.Rr) 120 | p.client.aClient.addReqBody("Type", rc.DTyp) 121 | p.client.aClient.addReqBody("Value", rc.DVal) 122 | p.client.aClient.addReqBody("TTL", fmt.Sprintf("%d", rc.TTL)) 123 | rs := aliResult{} 124 | err = p.doAPIRequest(ctx, &rs) 125 | recID = rs.RecID 126 | if err != nil { 127 | return "", err 128 | } 129 | return recID, err 130 | } 131 | 132 | func (p *Provider) delDomainRecord(ctx context.Context, rc aliDomaRecord) (recID string, err error) { 133 | p.client.mutex.Lock() 134 | defer p.client.mutex.Unlock() 135 | p.getClient() 136 | p.client.aClient.addReqBody("Action", "DeleteDomainRecord") 137 | p.client.aClient.addReqBody("RecordId", rc.RecID) 138 | rs := aliResult{} 139 | err = p.doAPIRequest(ctx, &rs) 140 | recID = rs.RecID 141 | if err != nil { 142 | return "", err 143 | } 144 | return recID, err 145 | } 146 | 147 | func (p *Provider) setDomainRecord(ctx context.Context, rc aliDomaRecord) (recID string, err error) { 148 | p.client.mutex.Lock() 149 | defer p.client.mutex.Unlock() 150 | p.getClientWithZone(rc.DName) 151 | if rc.TTL <= 0 { 152 | rc.TTL = 600 153 | } 154 | p.client.aClient.addReqBody("Action", "UpdateDomainRecord") 155 | p.client.aClient.addReqBody("RecordId", rc.RecID) 156 | p.client.aClient.addReqBody("RR", rc.Rr) 157 | p.client.aClient.addReqBody("Type", rc.DTyp) 158 | p.client.aClient.addReqBody("Value", rc.DVal) 159 | p.client.aClient.addReqBody("TTL", fmt.Sprintf("%d", rc.TTL)) 160 | rs := aliResult{} 161 | err = p.doAPIRequest(ctx, &rs) 162 | recID = rs.RecID 163 | if err != nil { 164 | return "", err 165 | } 166 | return recID, err 167 | } 168 | 169 | func (p *Provider) getDomainRecord(ctx context.Context, recID string) (aliDomaRecord, error) { 170 | p.client.mutex.Lock() 171 | defer p.client.mutex.Unlock() 172 | p.getClient() 173 | p.client.aClient.addReqBody("Action", "DescribeDomainRecordInfo") 174 | p.client.aClient.addReqBody("RecordId", recID) 175 | rs := aliResult{} 176 | err := p.doAPIRequest(ctx, &rs) 177 | rec := rs.ToDomaRecord() 178 | if err != nil { 179 | return aliDomaRecord{}, err 180 | } 181 | return rec, err 182 | } 183 | 184 | func (p *Provider) queryDomainRecords(ctx context.Context, name string) ([]aliDomaRecord, error) { 185 | p.client.mutex.Lock() 186 | defer p.client.mutex.Unlock() 187 | p.getClient() 188 | p.client.aClient.addReqBody("Action", "DescribeDomainRecords") 189 | p.client.aClient.addReqBody("DomainName", strings.Trim(name, ".")) 190 | rs := aliResult{} 191 | err := p.doAPIRequest(ctx, &rs) 192 | if err != nil { 193 | return []aliDomaRecord{}, err 194 | } 195 | return rs.DRecords.Record, err 196 | } 197 | 198 | func (p *Provider) queryDomainRecord(ctx context.Context, rr, name string, recType string, recVal ...string) (aliDomaRecord, error) { 199 | p.client.mutex.Lock() 200 | defer p.client.mutex.Unlock() 201 | p.getClient() 202 | p.client.aClient.addReqBody("Action", "DescribeDomainRecords") 203 | p.client.aClient.addReqBody("DomainName", strings.Trim(name, ".")) 204 | p.client.aClient.addReqBody("RRKeyWord", rr) 205 | if recType != "" { 206 | p.client.aClient.addReqBody("TypeKeyWord", recType) 207 | } 208 | if len(recVal) > 0 && recVal[0] != "" { 209 | p.client.aClient.addReqBody("ValueKeyWord", recVal[0]) 210 | } 211 | p.client.aClient.addReqBody("SearchMode", "ADVANCED") 212 | rs := aliResult{} 213 | err := p.doAPIRequest(ctx, &rs) 214 | if err != nil { 215 | return aliDomaRecord{}, err 216 | } 217 | if len(rs.DRecords.Record) == 0 { 218 | return aliDomaRecord{}, errors.New("the Record Name of the domain not found") 219 | } 220 | return rs.DRecords.Record[0], err 221 | } 222 | 223 | // REVERSED:queryMainDomain rseserved for absolute names to name,zone 224 | func (p *Provider) queryMainDomain(ctx context.Context, name string) (string, string, error) { 225 | p.client.mutex.Lock() 226 | defer p.client.mutex.Unlock() 227 | p.getClient() 228 | p.client.aClient.addReqBody("Action", "GetMainDomainName") 229 | p.client.aClient.addReqBody("InputString", strings.Trim(name, ".")) 230 | rs := aliResult{} 231 | err := p.doAPIRequest(ctx, &rs) 232 | if err != nil { 233 | return "", "", err 234 | } 235 | return rs.Rr, rs.DName, err 236 | } 237 | 238 | func (p *Provider) doAPIRequest(ctx context.Context, result interface{}) error { 239 | return p.client.doAPIRequest(ctx, "GET", result) 240 | } 241 | 242 | // Interface guards 243 | var ( 244 | _ libdns.RecordGetter = (*Provider)(nil) 245 | _ libdns.RecordAppender = (*Provider)(nil) 246 | _ libdns.RecordSetter = (*Provider)(nil) 247 | _ libdns.RecordDeleter = (*Provider)(nil) 248 | ) 249 | --------------------------------------------------------------------------------