This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bunnynet
type DNSZoneResponse struct {
Id int64 `json:"Id"`
Domain string `json:"Domain"`
Records []struct {
Id int64 `json:"Id"`
Type int `json:"Type"`
Ttl int32 `json:"Ttl"`
Value string `json:"Value"`
Name string `json:"Name"`
Weight int32 `json:"Weight"`
LatencyZone string `json:"LatencyZone"`
SmartRoutingType int `json:"SmartRoutingType"`
} `json:"Records"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bunnynet
type ListDNSZonesResponse struct {
Items []*DNSZoneResponse `json:"Items"`
}

View File

@@ -0,0 +1,14 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bunnynet
type RecordResponse struct {
Id int64 `json:"Id"`
Type int `json:"Type"`
Ttl int32 `json:"Ttl"`
Value string `json:"Value"`
Name string `json:"Name"`
Weight int32 `json:"Weight"`
LatencyZone string `json:"LatencyZone"`
SmartRoutingType int `json:"SmartRoutingType"`
}

View File

@@ -0,0 +1,9 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bunnynet
type RegionListResponse []struct {
Id int `json:"Id"`
Name string `json:"Name"`
RegionCode string `json:"RegionCode"`
}

View File

@@ -0,0 +1,22 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type BaseResponse struct {
Success bool `json:"success"`
Errors []struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"errors"`
}
func (this *BaseResponse) IsOk() bool {
return this.Success
}
func (this *BaseResponse) LastError() (code int, message string) {
if len(this.Errors) == 0 {
return 0, ""
}
return this.Errors[0].Code, this.Errors[0].Message
}

View File

@@ -0,0 +1,7 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type CreateDNSRecordResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,7 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type DeleteDNSRecordResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,17 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type GetDNSRecordsResponse struct {
BaseResponse
Result []struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
Ttl int `json:"ttl"`
ZoneId string `json:"zoneId"`
ZoneName string `json:"zoneName"`
} `json:"result"`
}

View File

@@ -0,0 +1,8 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type ResponseInterface interface {
IsOk() bool
LastError() (code int, message string)
}

View File

@@ -0,0 +1,7 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type UpdateDNSRecordResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,12 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cloudflare
type ZonesResponse struct {
BaseResponse
Result []struct {
Id string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package cloudns
type RecordResponse struct {
Id string `json:"id"`
Type string `json:"type"`
Host string `json:"host"`
Record string `json:"record"`
TTL string `json:"ttl"`
Status int `json:"status"`
}

View File

@@ -0,0 +1,5 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package cloudns
type RecordsResponse map[string]*RecordResponse

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package cloudns
type StatusResponse struct {
Status string `json:"status"`
StatusDescription string `json:"statusDescription"`
}

View File

@@ -0,0 +1,10 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package cloudns
type ZoneResponse struct {
Name string `json:"name"`
Type string `json:"type"`
Zone string `json:"zone"`
Status string `json:"status"`
}

View File

@@ -0,0 +1,5 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package cloudns
type ZonesResponse []*ZoneResponse

View File

@@ -0,0 +1,16 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type CreateRecordResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
DomainID int64 `json:"domainID"`
RecordID int64 `json:"recordID"`
Record string `json:"record"`
Type string `json:"type"`
TTL int `json:"TTL"`
State int `json:"state"`
} `json:"data"`
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type DomainListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Data []struct {
Domains string `json:"domains"`
DomainsID string `json:"domainsID"`
State int `json:"state"`
} `json:"data"`
Page int `json:"page"`
PageCount int `json:"pageCount"`
} `json:"data"`
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type DomainSearchResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Data []struct {
Domains string `json:"domains"`
DomainsID string `json:"domainsID"`
State int `json:"state"`
} `json:"data"`
Page int `json:"page"`
PageCount int `json:"pageCount"`
} `json:"data"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type IPAreaViewListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data []struct {
Name string `json:"Name"`
ViewID int64 `json:"viewID"`
} `json:"data"`
}

View File

@@ -0,0 +1,12 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type IPISPViewListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data []struct {
Name string `json:"Name"`
ViewID int64 `json:"viewID"`
} `json:"data"`
}

View File

@@ -0,0 +1,24 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type RecordListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Data []struct {
RecordID int `json:"recordID"`
Record string `json:"record"`
Type string `json:"type"`
State int `json:"state"`
ViewID int64 `json:"viewID"`
AreaViewID int64 `json:"areaViewID"`
ISPViewID int64 `json:"ISPViewID"`
Value string `json:"value"`
TTL int `json:"TTL"`
} `json:"data"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
PageCount int `json:"pageCount"`
} `json:"data"`
}

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type RecordModifyResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnscom
type RecordRemoveResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,16 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type AllLineListResponseChild struct {
Id string `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Children []AllLineListResponseChild `json:"children"`
}
type AllLineListResponse struct {
BaseResponse
Data []AllLineListResponseChild `json:"data"`
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
import (
"errors"
"github.com/iwind/TeaGo/types"
)
type BaseResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (this *BaseResponse) Success() bool {
return this.Code == 200
}
func (this *BaseResponse) Error() error {
return errors.New("code:" + types.String(this.Code) + ", message:" + this.Msg)
}

View File

@@ -0,0 +1,11 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type DomainResponse struct {
BaseResponse
Data struct {
Id string `json:"id"`
} `json:"data"`
}

View File

@@ -0,0 +1,15 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type DomainListResponse struct {
BaseResponse
Data struct {
Total int `json:"total"`
Results []struct {
Id string `json:"id"`
Domain string `json:"domain"`
} `json:"results"`
} `json:"data"`
}

View File

@@ -0,0 +1,66 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
import (
"encoding/json"
"testing"
)
func TestDomainListResponse(t *testing.T) {
var bodyJSON = []byte(`{
"status": {
"code": 300,
"name": "操作成功",
"message": "",
"request_time": "2022-08-07 07:43:34"
},
"total": {
"all_total": 3,
"data_total": 3,
"skip_number": 0
},
"datas": [
{
"domainid": 6772732,
"userid": 459662,
"domainname": "hello3.com",
"grade": "免费套餐",
"domain_status": "正常",
"domain_active": "yes",
"groupid": 0,
"nsstate": "未知",
"createtime": "2022-08-07 07:42:27"
},
{
"domainid": 6772731,
"userid": 459662,
"domainname": "hello2.com",
"grade": "免费套餐",
"domain_status": "正常",
"domain_active": "yes",
"groupid": 0,
"nsstate": "未知",
"createtime": "2022-08-07 07:42:19"
},
{
"domainid": 6772358,
"userid": 459662,
"domainname": "hello1234.com",
"grade": "免费套餐",
"domain_status": "正常",
"domain_active": "yes",
"groupid": 0,
"nsstate": "错误",
"createtime": "2022-08-07 11:42:05"
}
]
}`)
var resp = &DomainListResponse{}
err := json.Unmarshal(bodyJSON, resp)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
t.Log(resp.Success(), resp.Error())
}

View File

@@ -0,0 +1,11 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type RecordCreateResponse struct {
BaseResponse
Data struct {
Id string `json:"id"`
} `json:"data"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type RecordDeleteResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,22 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type RecordListResponse struct {
BaseResponse
Data struct {
Total int `json:"total"`
Results []struct {
Id string `json:"id"`
Host string `json:"host"`
Data string `json:"data"`
TTL int `json:"ttl"`
Type int `json:"type"`
LineId string `json:"lineId"`
LineCode string `json:"lineCode"`
LineName string `json:"lineName"`
Weight int `json:"weight"`
} `json:"results"`
} `json:"data"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsla
type RecordUpdateResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,18 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnspod
type BaseResponse struct {
Status struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"status"`
}
func (this *BaseResponse) IsOk() bool {
return this.Status.Code == "1"
}
func (this *BaseResponse) LastError() (code string, message string) {
return this.Status.Code, this.Status.Message
}

View File

@@ -0,0 +1,17 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type CustomLineGroupListResponse struct {
BaseResponse
Data struct {
LineGroups []struct {
Name string `json:"name"`
} `json:"line_groups"`
Info struct {
NowTotal int `json:"now_total"`
Total int `json:"total"`
} `json:"info"`
} `json:"data"`
}

View File

@@ -0,0 +1,13 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type DomainInfoResponse struct {
BaseResponse
Domain struct {
Id any `json:"id"`
Name string `json:"name"`
Grade string `json:"grade"`
} `json:"domain"`
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type DomainListResponse struct {
BaseResponse
Info struct {
DomainTotal int `json:"domain_total"`
AllTotal int `json:"all_total"`
MineTotal int `json:"mine_total"`
} `json:"info"`
Domains []struct {
Name string `json:"name"`
} `json:"domains"`
}

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnspod
type ResponseInterface interface {
IsOk() bool
LastError() (code string, message string)
}

View File

@@ -0,0 +1,13 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type RecordCreateResponse struct {
BaseResponse
Record struct {
Id any `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
} `json:"record"`
}

View File

@@ -0,0 +1,9 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type RecordLineResponse struct {
BaseResponse
Lines []string `json:"lines"`
}

View File

@@ -0,0 +1,23 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type RecordListResponse struct {
BaseResponse
Info struct {
SubDomains string `json:"sub_domains"`
RecordTotal string `json:"record_total"`
RecordsNum string `json:"records_num"`
} `json:"info"`
Records []struct {
Id any `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
Line string `json:"line"`
LineId string `json:"line_id"`
TTL string `json:"ttl"`
} `json:"records"`
}

View File

@@ -0,0 +1,14 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type RecordModifyResponse struct {
BaseResponse
Record struct {
Id any `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
Status string `json:"status"`
} `json:"record"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnspod
type RecordRemoveResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,42 @@
package dnstypes
type RecordType = string
const (
RecordTypeA RecordType = "A"
RecordTypeAAAA RecordType = "AAAA"
RecordTypeCNAME RecordType = "CNAME"
RecordTypeTXT RecordType = "TXT"
)
type Record struct {
Id string `json:"id"`
Name string `json:"name"`
Type RecordType `json:"type"`
Value string `json:"value"`
Route string `json:"route"`
TTL int32 `json:"ttl"`
}
func (this *Record) Clone() *Record {
return &Record{
Id: this.Id,
Name: this.Name,
Type: this.Type,
Value: this.Value,
Route: this.Route,
TTL: this.TTL,
}
}
func (this *Record) Copy(anotherRecord *Record) {
if anotherRecord == nil {
return
}
this.Id = anotherRecord.Id
this.Name = anotherRecord.Name
this.Type = anotherRecord.Type
this.Value = anotherRecord.Value
this.Route = anotherRecord.Route
this.TTL = anotherRecord.TTL
}

View File

@@ -0,0 +1,7 @@
package dnstypes
// Route 线路描述
type Route struct {
Name string `json:"name"`
Code string `json:"code"`
}

View File

@@ -0,0 +1,244 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsclients
import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
"sync"
"time"
)
func init() {
dbs.OnReadyDone(func() {
go func() {
var ticker = time.NewTicker(1 * time.Hour)
for range ticker.C {
sharedDomainRecordsCache.Clean()
}
}()
})
}
type recordList struct {
version int64
updatedAt int64
records []*dnstypes.Record
}
var sharedDomainRecordsCache = NewDomainRecordsCache()
// DomainRecordsCache 域名记录缓存
type DomainRecordsCache struct {
domainRecordsMap map[string]*recordList // domain@providerId => record
locker sync.Mutex
}
func NewDomainRecordsCache() *DomainRecordsCache {
return &DomainRecordsCache{
domainRecordsMap: map[string]*recordList{},
}
}
// WriteDomainRecords 写入域名记录缓存
func (this *DomainRecordsCache) WriteDomainRecords(providerId int64, domain string, records []*dnstypes.Record) {
if providerId <= 0 || len(domain) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
// 版本号
var key = "DomainRecordsCache" + "@" + types.String(providerId) + "@" + domain
version, err := models.SharedSysLockerDAO.Increase(nil, key, 1)
if err != nil {
remotelogs.Error("dnsclients.BaseProvider", "WriteDomainRecordsCache: "+err.Error())
return
}
var clonedRecords = append([]*dnstypes.Record{}, records...)
this.domainRecordsMap[domain] = &recordList{
version: version,
updatedAt: time.Now().Unix(),
records: clonedRecords,
}
}
// QueryDomainRecord 从缓存中读取单条域名记录
func (this *DomainRecordsCache) QueryDomainRecord(providerId int64, domain string, recordName string, recordType string) (record *dnstypes.Record, hasRecords bool, ok bool) {
if providerId <= 0 || len(domain) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
// check version
var key = "DomainRecordsCache" + "@" + types.String(providerId) + "@" + domain
version, err := models.SharedSysLockerDAO.Read(nil, key)
if err != nil {
remotelogs.Error("dnsclients.BaseProvider", "ReadDomainRecordsCache: "+err.Error())
return
}
// find list
list, recordsOk := this.domainRecordsMap[domain]
if !recordsOk {
return
}
if version != list.version {
delete(this.domainRecordsMap, domain)
return
}
// check timestamp
if list.updatedAt < time.Now().Unix()-86400 /** 缓存有效期为一天 **/ {
delete(this.domainRecordsMap, domain)
return
}
hasRecords = true
for _, r := range list.records {
if r.Name == recordName && r.Type == recordType {
return r, true, true
}
}
return
}
// QueryDomainRecords 从缓存中读取多条域名记录
func (this *DomainRecordsCache) QueryDomainRecords(providerId int64, domain string, recordName string, recordType string) (records []*dnstypes.Record, hasRecords bool, ok bool) {
if providerId <= 0 || len(domain) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
// check version
var key = "DomainRecordsCache" + "@" + types.String(providerId) + "@" + domain
version, err := models.SharedSysLockerDAO.Read(nil, key)
if err != nil {
remotelogs.Error("dnsclients.BaseProvider", "ReadDomainRecordsCache: "+err.Error())
return
}
// find list
list, recordsOk := this.domainRecordsMap[domain]
if !recordsOk {
return
}
if version != list.version {
delete(this.domainRecordsMap, domain)
return
}
// check timestamp
if list.updatedAt < time.Now().Unix()-86400 /** 缓存有效期为一天 **/ {
delete(this.domainRecordsMap, domain)
return
}
hasRecords = true
for _, r := range list.records {
if r.Name == recordName && r.Type == recordType {
records = append(records, r)
ok = true
}
}
return
}
// DeleteDomainRecord 删除域名记录缓存
func (this *DomainRecordsCache) DeleteDomainRecord(providerId int64, domain string, recordId string) {
if providerId <= 0 || len(domain) == 0 || len(recordId) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
list, ok := this.domainRecordsMap[domain]
if !ok {
return
}
var found = false
var newRecords = []*dnstypes.Record{}
for _, record := range list.records {
if record.Id == recordId {
found = true
continue
}
newRecords = append(newRecords, record)
}
if found {
list.records = newRecords
}
}
// AddDomainRecord 添加域名记录缓存
func (this *DomainRecordsCache) AddDomainRecord(providerId int64, domain string, record *dnstypes.Record) {
if providerId <= 0 || len(domain) == 0 || record == nil || len(record.Id) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
list, ok := this.domainRecordsMap[domain]
if ok {
list.records = append(list.records, record.Clone())
}
// 如果完全没有记录,则不保存
}
// UpdateDomainRecord 修改域名记录缓存
func (this *DomainRecordsCache) UpdateDomainRecord(providerId int64, domain string, record *dnstypes.Record) {
if providerId <= 0 || len(domain) == 0 || record == nil || len(record.Id) == 0 {
return
}
domain = types.String(providerId) + "@" + domain
this.locker.Lock()
defer this.locker.Unlock()
list, ok := this.domainRecordsMap[domain]
if !ok {
return
}
for _, r := range list.records {
if r.Id == record.Id {
r.Copy(record)
break
}
}
}
// Clean 清除过期缓存
func (this *DomainRecordsCache) Clean() {
this.locker.Lock()
defer this.locker.Unlock()
for domain, list := range this.domainRecordsMap {
if list.updatedAt < time.Now().Unix()-86400 {
delete(this.domainRecordsMap, domain)
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsclients_test
import (
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"testing"
)
func TestDomainRecordsCache_WriteDomainRecords(t *testing.T) {
dbs.NotifyReady()
var cache = dnsclients.NewDomainRecordsCache()
cache.WriteDomainRecords(1, "a", []*dnstypes.Record{
{
Id: "1",
Name: "hello",
Type: "A",
Value: "192.168.1.100",
},
})
//time.Sleep(30 * time.Second)
{
t.Log(cache.QueryDomainRecord(1, "a", "hello", "A"))
}
{
t.Log(cache.QueryDomainRecord(1, "a", "hello", "AAAA"))
}
{
t.Log(cache.QueryDomainRecord(1, "a", "hello2", "A"))
}
t.Log("======")
cache.DeleteDomainRecord(1, "a", "2")
cache.UpdateDomainRecord(1, "a", &dnstypes.Record{
Id: "1",
Name: "hello2",
Type: "A",
Value: "192.168.1.200",
})
{
t.Log(cache.QueryDomainRecord(1, "a", "hello2", "A"))
}
t.Log("======")
cache.AddDomainRecord(1, "a", &dnstypes.Record{
Id: "2",
Name: "hello",
Type: "AAAA",
Value: "::1",
})
{
t.Log(cache.QueryDomainRecord(1, "a", "hello", "AAAA"))
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
import (
"errors"
"github.com/iwind/TeaGo/types"
)
type BaseResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (this *BaseResponse) IsValid() bool {
return this.Code == 200
}
func (this *BaseResponse) Error() error {
return errors.New("code: " + types.String(this.Code) + ", message: " + this.Message)
}

View File

@@ -0,0 +1,11 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type CreateNSRecordResponse struct {
BaseResponse
Data struct {
NSRecordId int64 `json:"nsRecordId"`
} `json:"data"`
}

View File

@@ -0,0 +1,14 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type FindAllNSRoutesResponse struct {
BaseResponse
Data struct {
NSRoutes []struct {
Name string `json:"name"`
Code string `json:"code"`
} `json:"nsRoutes"`
} `json:"data"`
}

View File

@@ -0,0 +1,14 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type FindDomainWithNameResponse struct {
BaseResponse
Data struct {
NSDomain struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
} `json:"data"`
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type FindNSRecordWithNameAndTypeResponse struct {
BaseResponse
Data struct {
NSRecord struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
NSRoutes []struct {
Name string `json:"name"`
Code string `json:"code"`
} `json:"nsRoutes"`
} `json:"nsRecord"`
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type FindNSRecordsWithNameAndTypeResponse struct {
BaseResponse
Data struct {
NSRecords []struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
NSRoutes []struct {
Name string `json:"name"`
Code string `json:"code"`
} `json:"nsRoutes"`
} `json:"nsRecords"`
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type GetAPIAccessToken struct {
BaseResponse
Data struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expiresAt"`
} `json:"data"`
}

View File

@@ -0,0 +1,8 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type ResponseInterface interface {
IsValid() bool
Error() error
}

View File

@@ -0,0 +1,16 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type ListNSDomainsResponse struct {
BaseResponse
Data struct {
NSDomains []struct {
Id int64 `json:"id"`
Name string `json:"name"`
IsOn bool `json:"isOn"`
IsDeleted bool `json:"isDeleted"`
} `json:"nsDomains"`
} `json:"data"`
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type ListNSRecordsResponse struct {
BaseResponse
Data struct {
NSRecords []struct {
Id int64 `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
Type string `json:"type"`
NSRoutes []struct {
Name string `json:"name"`
Code string `json:"code"`
} `json:"nsRoutes"`
} `json:"nsRecords"`
} `json:"data"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type SuccessResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,7 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package edgeapi
type UpdateNSRecordResponse struct {
BaseResponse
}

View File

@@ -0,0 +1,7 @@
package gname
type BaseResponse struct {
Code int `json:"code"` // 1: 成功, -1: 失败
Msg string `json:"msg"` // 返回描述
Data interface{} `json:"data"` // 返回数据
}

View File

@@ -0,0 +1,9 @@
package gname
type DomainsResponse []Domain
type Domain struct {
Id string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}

View File

@@ -0,0 +1,12 @@
package gname
type RecordsResponse []Record
type Record struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
Route string `json:"route"` // 线路
}

View File

@@ -0,0 +1 @@
package gname

View File

@@ -0,0 +1,9 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package godaddy
type DomainsResponse []struct {
DomainId int64 `json:"domainId"`
Domain string `json:"domain"`
Status string `json:"status"`
}

View File

@@ -0,0 +1,10 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package godaddy
type RecordsResponse []struct {
Data string `json:"data"`
Name string `json:"name"`
TTL int32 `json:"ttl"`
Type string `json:"type"`
}

View File

@@ -0,0 +1,6 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type BaseResponse struct {
}

View File

@@ -0,0 +1,10 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type CustomLinesResponse struct {
Lines []struct {
LineId string `json:"line_id"`
Name string `json:"name"`
} `json:"lines"`
}

View File

@@ -0,0 +1,14 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type RecordSetsResponse struct {
RecordSets []struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Ttl int `json:"ttl"`
Line string `json:"line"`
Records []string `json:"records"`
} `json:"recordsets"`
}

View File

@@ -0,0 +1,17 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type ZoneRecordSetsResponse struct {
RecordSets []struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Ttl int `json:"ttl"`
Records []string `json:"records"`
Line string `json:"line"`
} `json:"recordsets"`
Metadata struct {
TotalCount int `json:"total_count"`
} `json:"metadata"`
}

View File

@@ -0,0 +1,17 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type ZonesResponse struct {
Links struct{} `json:"links"`
Zones []struct {
Id string `json:"id"`
Name string `json:"name"`
ZoneType string `json:"zone_type"`
Status string `json:"status"`
RecordNum int `json:"record_num"`
} `json:"zones"`
Metadata struct {
TotalCount int `json:"total_count"`
} `json:"metadata"`
}

View File

@@ -0,0 +1,9 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type ZonesCreateRecordSetResponse struct {
Id string `json:"id"`
Line string `json:"line"`
Records []string `json:"records"`
}

View File

@@ -0,0 +1,7 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type ZonesDeleteRecordSetResponse struct {
Id string `json:"id"`
}

View File

@@ -0,0 +1,9 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package huaweidns
type ZonesUpdateRecordSetResponse struct {
Id string `json:"id"`
Line string `json:"line"`
Records []string `json:"records"`
}

View File

@@ -0,0 +1,292 @@
package dnsclients
import (
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"strings"
)
// AliDNSProvider 阿里云服务商
type AliDNSProvider struct {
BaseProvider
ProviderId int64
accessKeyId string
accessKeySecret string
regionId string
}
// Auth 认证
func (this *AliDNSProvider) Auth(params maps.Map) error {
this.accessKeyId = params.GetString("accessKeyId")
this.accessKeySecret = params.GetString("accessKeySecret")
this.regionId = params.GetString("regionId")
if len(this.accessKeyId) == 0 {
return errors.New("'accessKeyId' should not be empty")
}
if len(this.accessKeySecret) == 0 {
return errors.New("'accessKeySecret' should not be empty")
}
if len(this.regionId) == 0 {
this.regionId = "cn-hangzhou"
}
return nil
}
// MaskParams 对参数进行掩码
func (this *AliDNSProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["accessKeySecret"] = MaskString(params.GetString("accessKeySecret"))
}
// GetDomains 获取所有域名列表
func (this *AliDNSProvider) GetDomains() (domains []string, err error) {
var pageNumber = 1
var size = 100
for {
var req = alidns.CreateDescribeDomainsRequest()
req.PageNumber = requests.NewInteger(pageNumber)
req.PageSize = requests.NewInteger(size)
var resp = alidns.CreateDescribeDomainsResponse()
err = this.doAPI(req, resp)
if err != nil {
return nil, err
}
for _, domain := range resp.Domains.Domain {
domains = append(domains, domain.DomainName)
}
pageNumber++
if int64((pageNumber-1)*size) >= resp.TotalCount {
break
}
}
return
}
// GetRecords 获取域名列表
func (this *AliDNSProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var pageNumber = 1
var size = 100
for {
var req = alidns.CreateDescribeDomainRecordsRequest()
req.DomainName = domain
req.PageNumber = requests.NewInteger(pageNumber)
req.PageSize = requests.NewInteger(size)
var resp = alidns.CreateDescribeDomainRecordsResponse()
err = this.doAPI(req, resp)
if err != nil {
return nil, err
}
for _, record := range resp.DomainRecords.Record {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
records = append(records, &dnstypes.Record{
Id: record.RecordId,
Name: record.RR,
Type: record.Type,
Value: record.Value,
Route: record.Line,
TTL: types.Int32(record.TTL),
})
}
pageNumber++
if int64((pageNumber-1)*size) >= resp.TotalCount {
break
}
}
// 写入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.WriteDomainRecords(this.ProviderId, domain, records)
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *AliDNSProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
var req = alidns.CreateDescribeSupportLinesRequest()
req.DomainName = domain
var resp = alidns.CreateDescribeSupportLinesResponse()
err = this.doAPI(req, resp)
if err != nil {
return nil, err
}
for _, line := range resp.RecordLines.RecordLine {
routes = append(routes, &dnstypes.Route{
Name: line.LineDisplayName,
Code: line.LineCode,
})
}
return
}
// QueryRecord 查询单个记录
func (this *AliDNSProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
record, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecord(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return record, nil
}
}
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
for _, record := range records {
if record.Name == name && record.Type == recordType {
return record, nil
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *AliDNSProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
records, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecords(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return records, nil
}
}
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
var result = []*dnstypes.Record{}
for _, record := range records {
if record.Name == name && record.Type == recordType {
result = append(result, record)
}
}
return result, nil
}
// AddRecord 设置记录
func (this *AliDNSProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
var req = alidns.CreateAddDomainRecordRequest()
req.RR = newRecord.Name
req.Type = newRecord.Type
req.Value = newRecord.Value
req.DomainName = domain
req.Line = newRecord.Route
if newRecord.TTL > 0 {
req.TTL = requests.NewInteger(types.Int(newRecord.TTL))
}
var resp = alidns.CreateAddDomainRecordResponse()
err := this.doAPI(req, resp)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
if resp.IsSuccess() {
newRecord.Id = resp.RecordId
// 加入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.AddDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
return this.WrapError(errors.New(resp.GetHttpContentString()), domain, newRecord)
}
// UpdateRecord 修改记录
func (this *AliDNSProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
var req = alidns.CreateUpdateDomainRecordRequest()
req.RecordId = record.Id
req.RR = newRecord.Name
req.Type = newRecord.Type
req.Value = newRecord.Value
req.Line = newRecord.Route
if newRecord.TTL > 0 {
req.TTL = requests.NewInteger(types.Int(newRecord.TTL))
}
var resp = alidns.CreateUpdateDomainRecordResponse()
err := this.doAPI(req, resp)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
newRecord.Id = record.Id
// 修改缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.UpdateDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// DeleteRecord 删除记录
func (this *AliDNSProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
var req = alidns.CreateDeleteDomainRecordRequest()
req.RecordId = record.Id
var resp = alidns.CreateDeleteDomainRecordResponse()
err := this.doAPI(req, resp)
if err != nil {
return this.WrapError(err, domain, record)
}
// 删除缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.DeleteDomainRecord(this.ProviderId, domain, record.Id)
}
return nil
}
// DefaultRoute 默认线路
func (this *AliDNSProvider) DefaultRoute() string {
return "default"
}
// 执行请求
func (this *AliDNSProvider) doAPI(req requests.AcsRequest, resp responses.AcsResponse) error {
req.SetScheme("https")
client, err := alidns.NewClientWithAccessKey(this.regionId, this.accessKeyId, this.accessKeySecret)
if err != nil {
return err
}
err = client.DoAction(req, resp)
if err != nil {
return err
}
if !resp.IsSuccess() {
return errors.New(resp.GetHttpContentString())
}
return nil
}

View File

@@ -0,0 +1,161 @@
package dnsclients
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
_ "github.com/go-sql-driver/mysql"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestAliDNSProvider_GetDomains(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
t.Log(provider.GetDomains())
}
func TestAliDNSProvider_GetRecords(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords("meloy.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
func TestAliDNSProvider_QueryRecord(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
{
record, err := provider.QueryRecord("meloy.cn", "www", "A")
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
{
record, err := provider.QueryRecord("meloy.cn", "www1", "A")
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
}
func TestAliDNSProvider_QueryRecords(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
{
records, err := provider.QueryRecords("meloy.cn", "www", "A")
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", records)
}
}
func TestAliDNSProvider_DeleteRecord(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("meloy.cn", &dnstypes.Record{
Id: "20746603318032384",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAliDNSProvider_GetRoutes(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes("meloy.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestAliDNSProvider_AddRecord(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
var record = &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.100",
Route: "aliyun_r_cn-beijing",
}
err = provider.AddRecord("meloy.cn", record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAliDNSProvider_UpdateRecord(t *testing.T) {
provider, err := testAliDNSProvider()
if err != nil {
t.Fatal(err)
}
err = provider.UpdateRecord("meloy.cn", &dnstypes.Record{Id: "20746664455255040"}, &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.101",
Route: "unicom",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func testAliDNSProvider() (ProviderInterface, error) {
dbs.NotifyReady()
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='alidns' AND state=1 ORDER BY id DESC")
if err != nil {
return nil, err
}
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider := &AliDNSProvider{
ProviderId: one.GetInt64("id"),
}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,807 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"encoding/base64"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"strings"
)
// AmazonRoute53Provider Amazon Route 53
// 其中我们总是判断 recordSet.SetIdentifier != nil 是为了只关注我们自己创建的记录集
type AmazonRoute53Provider struct {
BaseProvider
ProviderId int64
client *route53.Route53
}
func NewAmazonRoute53Provider() *AmazonRoute53Provider {
return &AmazonRoute53Provider{}
}
// Auth 认证
func (this *AmazonRoute53Provider) Auth(params maps.Map) error {
var accessKeyId = params.GetString("accessKeyId")
var accessKeySecret = params.GetString("accessKeySecret")
var region = params.GetString("region")
if len(accessKeyId) == 0 {
return errors.New("'accessKeyId' required")
}
if len(accessKeySecret) == 0 {
return errors.New("'accessKeySecret' required")
}
var regionPtr *string
if len(region) > 0 {
regionPtr = aws.String(region)
}
sess, err := session.NewSession(&aws.Config{
Credentials: credentials.NewCredentials(NewAmazonCredentialProvider(accessKeyId, accessKeySecret)),
Region: regionPtr,
})
if err != nil {
return err
}
this.client = route53.New(sess)
return nil
}
// MaskParams 对参数进行掩码
func (this *AmazonRoute53Provider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["accessKeySecret"] = MaskString(params.GetString("accessKeySecret"))
}
// GetDomains 获取所有域名列表
func (this *AmazonRoute53Provider) GetDomains() (domains []string, err error) {
var nextMarker *string
for {
var input = &route53.ListHostedZonesInput{
Marker: nextMarker,
}
output, err := this.client.ListHostedZones(input)
if err != nil {
return nil, err
}
for _, zone := range output.HostedZones {
domains = append(domains, strings.TrimSuffix(*zone.Name, "."))
}
if *output.IsTruncated {
nextMarker = output.NextMarker
} else {
break
}
}
return
}
// GetRecords 获取域名列表
func (this *AmazonRoute53Provider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return nil, err
}
var nextRecordIdentifier *string
var nextRecordName *string
var nextRecordType *string
for {
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordIdentifier: nextRecordIdentifier,
StartRecordName: nextRecordName,
StartRecordType: nextRecordType,
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return nil, err
}
for _, recordSet := range output.ResourceRecordSets {
// 检查返回值是否正常
if recordSet.SetIdentifier == nil || recordSet.Name == nil || recordSet.Type == nil || recordSet.TTL == nil {
continue
}
for _, rawRecord := range recordSet.ResourceRecords {
if rawRecord.Value == nil {
continue
}
var recordName = strings.TrimSuffix(strings.TrimSuffix(*recordSet.Name, domain+"."), ".")
records = append(records, &dnstypes.Record{
Id: this.composeRecordId(*recordSet.SetIdentifier, recordName, *recordSet.Type, *rawRecord.Value),
Name: recordName,
Type: *recordSet.Type,
Value: *rawRecord.Value,
Route: this.composeGeoLocationCode(recordSet.GeoLocation),
TTL: types.Int32(*recordSet.TTL),
})
}
}
if *output.IsTruncated {
nextRecordIdentifier = output.NextRecordIdentifier
nextRecordName = output.NextRecordName
nextRecordType = output.NextRecordType
} else {
break
}
}
// 写入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.WriteDomainRecords(this.ProviderId, domain, records)
}
return
}
// GetRoutes 读取线路数据
func (this *AmazonRoute53Provider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
var nextContinentCode *string
var nextCountryCode *string
var nextSubdivisionCode *string
for {
var input = &route53.ListGeoLocationsInput{
MaxItems: nil,
StartContinentCode: nextContinentCode,
StartCountryCode: nextCountryCode,
StartSubdivisionCode: nextSubdivisionCode,
}
output, err := this.client.ListGeoLocations(input)
if err != nil {
return nil, err
}
for _, location := range output.GeoLocationDetailsList {
locationName, locationCode := this.composeGeoLocationDetail(location)
routes = append(routes, &dnstypes.Route{
Name: locationName,
Code: locationCode,
})
}
if *output.IsTruncated {
nextContinentCode = output.NextContinentCode
nextCountryCode = output.NextCountryCode
nextSubdivisionCode = output.NextSubdivisionCode
} else {
break
}
}
return
}
// QueryRecord 查询单个记录
func (this *AmazonRoute53Provider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
record, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecord(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return record, nil
}
}
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return nil, err
}
var fullname = name + "." + domain + "."
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordName: aws.String(fullname),
StartRecordType: aws.String(recordType),
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return nil, err
}
for _, recordSet := range output.ResourceRecordSets {
if recordSet.SetIdentifier == nil {
continue
}
if *recordSet.Name == fullname && *recordSet.Type == recordType {
for _, rawRecord := range recordSet.ResourceRecords {
return &dnstypes.Record{
Id: this.composeRecordId(*recordSet.SetIdentifier, name, *recordSet.Type, *rawRecord.Value),
Name: name,
Type: *recordSet.Type,
Value: *rawRecord.Value,
Route: this.composeGeoLocationCode(recordSet.GeoLocation),
TTL: types.Int32(*recordSet.TTL),
}, nil
}
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *AmazonRoute53Provider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
records, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecords(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return records, nil
}
}
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return nil, err
}
var fullname = name + "." + domain + "."
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordName: aws.String(fullname),
StartRecordType: aws.String(recordType),
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return nil, err
}
var result []*dnstypes.Record
for _, recordSet := range output.ResourceRecordSets {
if recordSet.SetIdentifier == nil {
continue
}
if *recordSet.Name == fullname && *recordSet.Type == recordType {
for _, rawRecord := range recordSet.ResourceRecords {
result = append(result, &dnstypes.Record{
Id: this.composeRecordId(*recordSet.SetIdentifier, name, *recordSet.Type, *rawRecord.Value),
Name: name,
Type: *recordSet.Type,
Value: *rawRecord.Value,
Route: this.composeGeoLocationCode(recordSet.GeoLocation),
TTL: types.Int32(*recordSet.TTL),
})
}
}
}
return result, nil
}
// AddRecord 设置记录
func (this *AmazonRoute53Provider) AddRecord(domain string, newRecord *dnstypes.Record) error {
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return err
}
var recordSetId = newRecord.Name + stringutil.Md5(newRecord.Name+"@"+newRecord.Type+"@"+newRecord.Route)
// 检查是否已存在
var recordValues []*route53.ResourceRecord
var existRecordValue = false
{
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordName: aws.String(newRecord.Name + "." + domain + "."),
StartRecordType: aws.String(newRecord.Type),
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return fmt.Errorf("ListResourceRecordSets failed: %w", err)
}
if output.ResourceRecordSets != nil {
for _, recordSet := range output.ResourceRecordSets {
if recordSet.SetIdentifier == nil {
continue
}
if *recordSet.SetIdentifier == recordSetId {
for _, rawRecord := range recordSet.ResourceRecords {
recordValues = append(recordValues, &route53.ResourceRecord{
Value: aws.String(*rawRecord.Value),
})
if *rawRecord.Value == newRecord.Value {
existRecordValue = true
}
}
break
}
}
}
}
if existRecordValue {
return nil
}
recordValues = append(recordValues, &route53.ResourceRecord{
Value: aws.String(newRecord.Value),
})
var geoLocation *route53.GeoLocation
var recordRoute = newRecord.Route
if len(recordRoute) == 0 {
recordRoute = this.DefaultRoute()
}
if recordRoute != this.DefaultRoute() {
for _, piece := range strings.Split(recordRoute, "@") {
if strings.Contains(piece, ":") {
var pieces2 = strings.SplitN(piece, ":", 2)
if len(pieces2) == 2 && pieces2[1] != "*" {
if geoLocation == nil {
geoLocation = &route53.GeoLocation{}
}
switch pieces2[0] {
case "CONTINENT":
geoLocation.ContinentCode = aws.String(pieces2[1])
case "COUNTRY":
geoLocation.CountryCode = aws.String(pieces2[1])
case "SUBDIVISION":
geoLocation.SubdivisionCode = aws.String(pieces2[1])
}
}
}
}
} else {
geoLocation = &route53.GeoLocation{
CountryCode: aws.String(recordRoute),
}
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
var input = &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
AliasTarget: nil,
Failover: nil,
GeoLocation: geoLocation,
HealthCheckId: nil,
MultiValueAnswer: nil,
Name: aws.String(newRecord.Name + "." + domain + "."),
Region: nil,
ResourceRecords: recordValues,
SetIdentifier: aws.String(recordSetId),
TTL: aws.Int64(int64(ttl)),
TrafficPolicyInstanceId: nil,
Type: aws.String(newRecord.Type),
Weight: nil,
},
},
},
Comment: nil,
},
HostedZoneId: zoneId,
}
_, err = this.client.ChangeResourceRecordSets(input)
if err != nil {
return err
}
newRecord.Id = this.composeRecordId(recordSetId, newRecord.Name, newRecord.Type, newRecord.Value)
// 加入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.AddDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// UpdateRecord 修改记录
func (this *AmazonRoute53Provider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
if record == nil {
return errors.New("invalid record")
}
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var newRoute = newRecord.Route
if len(newRoute) == 0 {
newRoute = this.DefaultRoute()
}
recordSetId, recordName, recordType, _, err := this.decodeRecordId(record.Id)
if err != nil {
return err
}
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return err
}
// 检查是否已存在
var recordValues []*route53.ResourceRecord
var shouldChanged = false
{
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordName: aws.String(newRecord.Name + "." + domain + "."),
StartRecordType: aws.String(newRecord.Type),
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return fmt.Errorf("ListResourceRecordSets failed: %w", err)
}
if output.ResourceRecordSets != nil {
for _, recordSet := range output.ResourceRecordSets {
if recordSet.SetIdentifier == nil {
continue
}
if *recordSet.SetIdentifier == recordSetId {
shouldChanged = true
for _, rawRecord := range recordSet.ResourceRecords {
// skip old
if record.Id == this.composeRecordId(recordSetId, recordName, recordType, *rawRecord.Value) || *rawRecord.Value == newRecord.Value {
continue
}
recordValues = append(recordValues, &route53.ResourceRecord{
Value: aws.String(*rawRecord.Value),
})
}
break
}
}
}
}
if !shouldChanged {
return nil
}
recordValues = append(recordValues, &route53.ResourceRecord{
Value: aws.String(newRecord.Value),
})
var geoLocation *route53.GeoLocation
var recordRoute = newRecord.Route
if len(recordRoute) == 0 {
recordRoute = this.DefaultRoute()
}
if recordRoute != this.DefaultRoute() {
for _, piece := range strings.Split(recordRoute, "@") {
if strings.Contains(piece, ":") {
var pieces2 = strings.SplitN(piece, ":", 2)
if len(pieces2) == 2 && pieces2[1] != "*" {
if geoLocation == nil {
geoLocation = &route53.GeoLocation{}
}
switch pieces2[0] {
case "CONTINENT":
geoLocation.ContinentCode = aws.String(pieces2[1])
case "COUNTRY":
geoLocation.CountryCode = aws.String(pieces2[1])
case "SUBDIVISION":
geoLocation.SubdivisionCode = aws.String(pieces2[1])
}
}
}
}
} else {
geoLocation = &route53.GeoLocation{
CountryCode: aws.String(recordRoute),
}
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
var input = &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
AliasTarget: nil,
Failover: nil,
GeoLocation: geoLocation,
HealthCheckId: nil,
MultiValueAnswer: nil,
Name: aws.String(newRecord.Name + "." + domain + "."),
Region: nil,
ResourceRecords: recordValues,
SetIdentifier: aws.String(recordSetId),
TTL: aws.Int64(int64(ttl)),
TrafficPolicyInstanceId: nil,
Type: aws.String(newRecord.Type),
Weight: nil,
},
},
},
Comment: nil,
},
HostedZoneId: zoneId,
}
_, err = this.client.ChangeResourceRecordSets(input)
if err != nil {
return err
}
newRecord.Id = this.composeRecordId(recordSetId, newRecord.Name, newRecord.Type, newRecord.Value)
// 修改缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.UpdateDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// DeleteRecord 删除记录
func (this *AmazonRoute53Provider) DeleteRecord(domain string, record *dnstypes.Record) error {
if record == nil {
return errors.New("invalid record to delete")
}
recordSetId, recordName, recordType, recordValue, err := this.decodeRecordId(record.Id)
if err != nil {
return fmt.Errorf("decode record id failed: %w", err)
}
zoneId, err := this.getZoneId(domain)
if err != nil || zoneId == nil {
return err
}
var newRecordValues []*route53.ResourceRecord
var foundRecordSet *route53.ResourceRecordSet
{
var input = &route53.ListResourceRecordSetsInput{
HostedZoneId: zoneId,
StartRecordIdentifier: aws.String(recordSetId),
StartRecordName: aws.String(recordName + "." + domain + "."),
StartRecordType: aws.String(recordType),
}
output, err := this.client.ListResourceRecordSets(input)
if err != nil {
return fmt.Errorf("ListResourceRecordSets failed: %w", err)
}
if output.ResourceRecordSets != nil {
for _, recordSet := range output.ResourceRecordSets {
if recordSet.SetIdentifier == nil {
continue
}
if *recordSet.SetIdentifier == recordSetId {
foundRecordSet = recordSet
var foundRecord = false
for _, rawRecord := range recordSet.ResourceRecords {
if *rawRecord.Value == recordValue {
foundRecord = true
} else {
newRecordValues = append(newRecordValues, &route53.ResourceRecord{
Value: aws.String(*rawRecord.Value),
})
}
}
if !foundRecord {
return nil
}
break
}
}
}
}
if foundRecordSet == nil {
return nil
}
var action = "UPSERT"
if len(newRecordValues) == 0 {
action = "DELETE"
} else {
foundRecordSet.ResourceRecords = newRecordValues
}
var input = &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
{
Action: aws.String(action),
ResourceRecordSet: foundRecordSet,
},
},
Comment: nil,
},
HostedZoneId: zoneId,
}
_, err = this.client.ChangeResourceRecordSets(input)
if err != nil {
return err
}
// 删除缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.DeleteDomainRecord(this.ProviderId, domain, record.Id)
}
return nil
}
// DefaultRoute 默认线路
func (this *AmazonRoute53Provider) DefaultRoute() string {
return "*"
}
func (this *AmazonRoute53Provider) getZoneId(domain string) (*string, error) {
var input = &route53.ListHostedZonesByNameInput{
DNSName: aws.String(domain + "."),
HostedZoneId: nil,
MaxItems: nil,
}
output, err := this.client.ListHostedZonesByName(input)
if err != nil {
return nil, err
}
for _, zone := range output.HostedZones {
if strings.TrimSuffix(*zone.Name, ".") == domain {
return zone.Id, nil
}
}
return nil, nil
}
func (this *AmazonRoute53Provider) composeGeoLocationCode(location *route53.GeoLocation) (locationCode string) {
if location == nil {
return
}
var codes []string
if location.ContinentCode != nil && len(*location.ContinentCode) > 0 {
codes = append(codes, "CONTINENT:"+(*location.ContinentCode))
}
if location.CountryCode != nil && len(*location.CountryCode) > 0 {
if *location.CountryCode == "*" {
codes = append(codes, "*")
} else {
codes = append(codes, "COUNTRY:"+(*location.CountryCode))
}
}
if location.SubdivisionCode != nil && len(*location.SubdivisionCode) > 0 {
codes = append(codes, "SUBDIVISION:"+(*location.SubdivisionCode))
}
return strings.Join(codes, "@")
}
func (this *AmazonRoute53Provider) composeGeoLocationDetail(location *route53.GeoLocationDetails) (locationName string, locationCode string) {
if location == nil {
return
}
var names []string
var codes []string
if location.ContinentName != nil && len(*location.ContinentName) > 0 {
names = append(names, "CONTINENT:"+(*location.ContinentName))
}
if location.ContinentCode != nil && len(*location.ContinentCode) > 0 {
codes = append(codes, "CONTINENT:"+(*location.ContinentCode))
}
if location.CountryName != nil && len(*location.CountryName) > 0 {
if *location.CountryName == "Default" {
names = append(names, "Default")
} else {
names = append(names, "COUNTRY/REGION:"+(*location.CountryName))
}
}
if location.CountryCode != nil && len(*location.CountryCode) > 0 {
if *location.CountryCode == "*" {
codes = append(codes, "*")
} else {
codes = append(codes, "COUNTRY:"+(*location.CountryCode))
}
}
if location.SubdivisionName != nil && len(*location.SubdivisionName) > 0 {
names = append(names, "SUBDIVISION:"+(*location.SubdivisionName))
}
if location.SubdivisionCode != nil && len(*location.SubdivisionCode) > 0 {
codes = append(codes, "SUBDIVISION:"+(*location.SubdivisionCode))
}
return strings.Join(names, " "), strings.Join(codes, "@")
}
func (this *AmazonRoute53Provider) composeRecordId(recordSetId string, recordName string, recordType string, recordValue string) string {
return base64.StdEncoding.EncodeToString([]byte(recordSetId + "$" + recordName + "$" + recordType + "$" + recordValue))
}
func (this *AmazonRoute53Provider) decodeRecordId(encodedRecordId string) (recordSetId string, recordName string, recordType string, recordValue string, err error) {
data, err := base64.StdEncoding.DecodeString(encodedRecordId)
if err != nil {
return "", "", "", "", err
}
if len(data) == 0 {
err = errors.New("invalid record id")
return
}
var pieces = strings.SplitN(string(data), "$", 4)
if len(pieces) != 4 {
err = errors.New("invalid record id")
return
}
recordSetId = pieces[0]
recordName = pieces[1]
recordType = pieces[2]
recordValue = pieces[3]
return
}
func (this *AmazonRoute53Provider) fixCNAME(recordType string, recordValue string) string {
// 修正Record
if strings.ToUpper(recordType) == dnstypes.RecordTypeCNAME && !strings.HasSuffix(recordValue, ".") {
recordValue += "."
}
return recordValue
}
// AmazonCredentialProvider Amazon认证服务
type AmazonCredentialProvider struct {
accessKeyId string
secretAccessKey string
}
func NewAmazonCredentialProvider(accessKeyId string, secretAccessKey string) *AmazonCredentialProvider {
return &AmazonCredentialProvider{
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
}
}
func (this *AmazonCredentialProvider) Retrieve() (credentials.Value, error) {
return credentials.Value{
AccessKeyID: this.accessKeyId,
SecretAccessKey: this.secretAccessKey,
SessionToken: "",
ProviderName: "",
}, nil
}
func (this *AmazonCredentialProvider) IsExpired() bool {
return false
}

View File

@@ -0,0 +1,233 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients_test
import (
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
const AmazonRoute53TestDomain = "goedge.cloud"
func TestAmazonRoute53Provider_GetDomains(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestAmazonRoute53Provider_GetRoutes(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes(AmazonRoute53TestDomain)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestAmazonRoute53Provider_GetRecords(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords(AmazonRoute53TestDomain)
if err != nil {
t.Fatal(err)
}
for _, record := range records {
t.Log(record.Id, record.Type, record.Name, record.Value, record.Route, record.TTL)
}
}
func TestAmazonRoute53Provider_AddRecord_CNAME(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
var route = "*"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeCNAME,
Name: "hello-forward",
Value: "hello." + AmazonRoute53TestDomain,
Route: route,
TTL: 600,
}
err = provider.AddRecord(AmazonRoute53TestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAmazonRoute53Provider_AddRecord_A(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
//var route = "CONTINENT:AS@COUNTRY:CN@SUBDIVISION:13"
var route = "COUNTRY:JP"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test2",
Value: "192.168.1.113",
Route: route,
TTL: 600,
}
err = provider.AddRecord(AmazonRoute53TestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAmazonRoute53Provider_AddRecord_A_Default(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
//var route = "CONTINENT:AS@COUNTRY:CN@SUBDIVISION:13"
var route = "*"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test3",
Value: "192.168.1.203",
Route: route,
TTL: 600,
}
err = provider.AddRecord(AmazonRoute53TestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAmazonRoute53Provider_QueryRecord(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
{
record, err := provider.QueryRecord(AmazonRoute53TestDomain, "test2", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
{
record, err := provider.QueryRecord(AmazonRoute53TestDomain, "test2", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
}
func TestAmazonRoute53Provider_QueryRecords(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
{
records, err := provider.QueryRecords(AmazonRoute53TestDomain, "test2", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestAmazonRoute53Provider_UpdateRecord(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
var route = "COUNTRY:HK"
var id = "dGVzdDJmNWVjMzBhNzI1OTI1YzFhMTk0OTI0MzU3YTdmMDQ4NCR0ZXN0MiRBJDE5Mi4xNjguMS4xMTM="
err = provider.UpdateRecord(AmazonRoute53TestDomain, &dnstypes.Record{
Id: id,
}, &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test2",
Value: "192.168.1.114",
Route: route,
TTL: 1800,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAmazonRoute53Provider_DeleteRecord(t *testing.T) {
provider, err := testAmazonRoute53Provider()
if err != nil {
t.Fatal(err)
}
var id = "dGVzdDJmNWVjMzBhNzI1OTI1YzFhMTk0OTI0MzU3YTdmMDQ4NCR0ZXN0MiRBJDE5Mi4xNjguMS4xMTI="
id = "dGVzdDJmNWVjMzBhNzI1OTI1YzFhMTk0OTI0MzU3YTdmMDQ4NCR0ZXN0MiRBJDE5Mi4xNjguMS4xMTE="
id = "dGVzdDJmNWVjMzBhNzI1OTI1YzFhMTk0OTI0MzU3YTdmMDQ4NCR0ZXN0MiRBJDE5Mi4xNjguMS4xMTM="
err = provider.DeleteRecord(AmazonRoute53TestDomain, &dnstypes.Record{
Id: id,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func testAmazonRoute53Provider() (provider *dnsclients.AmazonRoute53Provider, err error) {
dbs.NotifyReady()
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='amazonRoute53' AND id='44' ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
err = errors.New("PROVIDER NOT FOUND")
return
}
var apiParams = maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider = &dnsclients.AmazonRoute53Provider{
ProviderId: one.GetInt64("id"),
}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,744 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"strings"
)
// AzureDNSProvider 微软Azure DNS zone
type AzureDNSProvider struct {
BaseProvider
ProviderId int64
zonesClient *armdns.ZonesClient
recordSetsClient *armdns.RecordSetsClient
resourceGroupName string
}
func NewAzureDNSProvider() *AzureDNSProvider {
return &AzureDNSProvider{}
}
// Auth 认证
func (this *AzureDNSProvider) Auth(params maps.Map) error {
var subscriptionId = params.GetString("subscriptionId")
var tenantId = params.GetString("tenantId")
var clientId = params.GetString("clientId")
var clientSecret = params.GetString("clientSecret")
var resourceGroupName = params.GetString("resourceGroupName")
if len(subscriptionId) == 0 {
return errors.New("'subscriptionId' required")
}
if len(tenantId) == 0 {
return errors.New("'tenantId' required")
}
if len(clientId) == 0 {
return errors.New("'clientId' required")
}
if len(clientSecret) == 0 {
return errors.New("'clientSecret' required")
}
if len(resourceGroupName) == 0 {
return errors.New("'resourceGroupName' required")
}
this.resourceGroupName = resourceGroupName
cred, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret, nil)
if err != nil {
return fmt.Errorf("NewClientSecretCredential: %w", err)
}
clientFactory, err := armdns.NewClientFactory(subscriptionId, cred, nil)
if err != nil {
return fmt.Errorf("NewClientFactory: %w", err)
}
this.zonesClient = clientFactory.NewZonesClient()
this.recordSetsClient = clientFactory.NewRecordSetsClient()
return nil
}
// MaskParams 对参数进行掩码
func (this *AzureDNSProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["clientSecret"] = MaskString(params.GetString("clientSecret"))
}
// GetDomains 获取所有域名列表
func (this *AzureDNSProvider) GetDomains() (domains []string, err error) {
var pager = this.zonesClient.NewListPager(nil)
for pager.More() {
page, err := pager.NextPage(context.Background())
if err != nil {
return nil, err
}
for _, zone := range page.Value {
if zone.Name != nil && !lists.ContainsString(domains, *zone.Name) {
domains = append(domains, *zone.Name)
}
}
}
return
}
// GetRecords 获取域名列表
func (this *AzureDNSProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var pager = this.recordSetsClient.NewListByDNSZonePager(this.resourceGroupName, domain, nil)
for pager.More() {
page, err := pager.NextPage(context.Background())
if err != nil {
return nil, err
}
for _, recordSet := range page.Value {
if recordSet.Name == nil || recordSet.Properties == nil {
continue
}
records = append(records, this.recordSetToRecords(recordSet)...)
}
}
// 写入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.WriteDomainRecords(this.ProviderId, domain, records)
}
return
}
// GetRoutes 读取线路数据
func (this *AzureDNSProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
routes = []*dnstypes.Route{
{
Name: "默认",
Code: "default",
},
}
return
}
// QueryRecord 查询单个记录
func (this *AzureDNSProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
record, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecord(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return record, nil
}
}
resp, err := this.recordSetsClient.Get(context.Background(), this.resourceGroupName, domain, name, armdns.RecordType(recordType), nil)
if err != nil {
if this.isNotFound(err) {
return nil, nil
}
return nil, err
}
var records = this.recordSetToRecords(&resp.RecordSet)
if len(records) > 0 {
return records[0], nil
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *AzureDNSProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
records, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecords(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return records, nil
}
}
resp, err := this.recordSetsClient.Get(context.Background(), this.resourceGroupName, domain, name, armdns.RecordType(recordType), nil)
if err != nil {
if this.isNotFound(err) {
return nil, nil
}
return nil, err
}
return this.recordSetToRecords(&resp.RecordSet), nil
}
// AddRecord 设置记录
func (this *AzureDNSProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
var recordSet = armdns.RecordSet{
Etag: nil,
Properties: nil,
ID: nil,
Name: this.stringVal(newRecord.Name),
Type: this.stringVal(newRecord.Type),
}
resp, err := this.recordSetsClient.Get(context.Background(), this.resourceGroupName, domain, newRecord.Name, armdns.RecordType(newRecord.Type), nil)
if err != nil {
if this.isNotFound(err) {
err = nil
} else {
return err
}
}
var ttlInt64 = int64(ttl)
if resp.RecordSet.Properties != nil {
recordSet.Properties = resp.RecordSet.Properties
recordSet.Properties.TTL = &ttlInt64
recordSet.ID = resp.RecordSet.ID
} else {
recordSet.Properties = &armdns.RecordSetProperties{
TTL: &ttlInt64,
}
}
exists, err := this.recordSetAddRecordValue(&recordSet, newRecord.Value)
if exists || err != nil {
if exists {
newRecord.Id = this.ComposeRecordId(newRecord.Name, newRecord.Type, newRecord.Value)
}
return err
}
_, err = this.recordSetsClient.CreateOrUpdate(context.Background(), this.resourceGroupName, domain, newRecord.Name, armdns.RecordType(newRecord.Type), recordSet, nil)
if err != nil {
return err
}
newRecord.Id = this.ComposeRecordId(newRecord.Name, newRecord.Type, newRecord.Value)
// 加入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.AddDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// UpdateRecord 修改记录
func (this *AzureDNSProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
if record == nil {
return errors.New("invalid record")
}
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var newRoute = newRecord.Route
if len(newRoute) == 0 {
newRoute = this.DefaultRoute()
}
_, oldRecordName, oldRecordType, oldRecordValue, err := this.decodeRecordId(record.Id)
if err != nil {
return err
}
if oldRecordName == newRecord.Name && oldRecordType == newRecord.Type && oldRecordValue == newRecord.Value {
return nil
}
resp, err := this.recordSetsClient.Get(context.Background(), this.resourceGroupName, domain, newRecord.Name, armdns.RecordType(newRecord.Type), nil)
if err != nil {
if this.isNotFound(err) {
return nil
} else {
return err
}
}
var recordSet = resp.RecordSet
if recordSet.Properties == nil {
return nil
}
found, err := this.recordSetUpdateRecordValue(&recordSet, newRecord.Type, oldRecordValue, newRecord.Value)
if err != nil {
return err
}
if !found {
return nil
}
var ttlInt64 = int64(newRecord.TTL)
if ttlInt64 <= 0 {
ttlInt64 = 600
}
recordSet.Properties.TTL = &ttlInt64
_, err = this.recordSetsClient.Update(context.Background(), this.resourceGroupName, domain, newRecord.Name, armdns.RecordType(newRecord.Type), recordSet, nil)
if err != nil {
return err
}
newRecord.Id = this.ComposeRecordId(newRecord.Name, newRecord.Type, newRecord.Value)
// 修改缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.UpdateDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// DeleteRecord 删除记录
func (this *AzureDNSProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
if record == nil {
return errors.New("invalid record to delete")
}
_, recordName, recordType, recordValue, err := this.decodeRecordId(record.Id)
if err != nil {
return err
}
resp, err := this.recordSetsClient.Get(context.Background(), this.resourceGroupName, domain, recordName, armdns.RecordType(recordType), nil)
if err != nil {
if this.isNotFound(err) {
return nil
} else {
return err
}
}
var recordSet = resp.RecordSet
if recordSet.Properties == nil {
return nil
}
shouldUpdate, shouldDelete, err := this.recordSetDeleteRecordValue(&recordSet, recordType, recordValue)
if err != nil {
return err
}
if shouldDelete {
_, err = this.recordSetsClient.Delete(context.Background(), this.resourceGroupName, domain, recordName, armdns.RecordType(recordType), nil)
if err != nil {
return err
}
} else if shouldUpdate {
_, err = this.recordSetsClient.Update(context.Background(), this.resourceGroupName, domain, recordName, armdns.RecordType(recordType), recordSet, nil)
if err != nil {
return err
}
} else {
return nil
}
// 删除缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.DeleteDomainRecord(this.ProviderId, domain, record.Id)
}
return nil
}
// DefaultRoute 默认线路
func (this *AzureDNSProvider) DefaultRoute() string {
return "default"
}
func (this *AzureDNSProvider) ComposeRecordId(recordName string, recordType string, recordValue string) string {
return base64.StdEncoding.EncodeToString([]byte("$" + recordName + "$" + recordType + "$" + recordValue))
}
func (this *AzureDNSProvider) decodeRecordId(encodedRecordId string) (recordSetId string, recordName string, recordType string, recordValue string, err error) {
data, err := base64.StdEncoding.DecodeString(encodedRecordId)
if err != nil {
return "", "", "", "", err
}
if len(data) == 0 {
err = errors.New("invalid record id")
return
}
var pieces = strings.SplitN(string(data), "$", 4)
if len(pieces) != 4 {
err = errors.New("invalid record id")
return
}
recordSetId = pieces[0]
recordName = pieces[1]
recordType = pieces[2]
recordValue = pieces[3]
return
}
func (this *AzureDNSProvider) fixCNAME(recordType string, recordValue string) string {
// 修正Record
if strings.ToUpper(recordType) == dnstypes.RecordTypeCNAME && !strings.HasSuffix(recordValue, ".") {
recordValue += "."
}
return recordValue
}
func (this *AzureDNSProvider) isNotFound(err error) bool {
if err == nil {
return false
}
var respErr *azcore.ResponseError
if errors.As(err, &respErr) && respErr.ErrorCode == "NotFound" {
return true
}
return false
}
func (this *AzureDNSProvider) recordSetToRecords(recordSet *armdns.RecordSet) (records []*dnstypes.Record) {
if recordSet == nil || recordSet.Properties == nil {
return
}
// A
for _, record := range recordSet.Properties.ARecords {
var recordType = "A"
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *record.IPv4Address),
Name: *recordSet.Name,
Type: recordType,
Value: *record.IPv4Address,
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
// AAAA
for _, record := range recordSet.Properties.AaaaRecords {
var recordType = "AAAA"
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *record.IPv6Address),
Name: *recordSet.Name,
Type: recordType,
Value: *record.IPv6Address,
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
// CNAME
if recordSet.Properties.CnameRecord != nil {
var recordType = "CNAME"
var record = recordSet.Properties.CnameRecord
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *record.Cname),
Name: *recordSet.Name,
Type: recordType,
Value: *record.Cname,
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
// TXT
if recordSet.Properties.TxtRecords != nil {
var recordType = "TXT"
for _, record := range recordSet.Properties.TxtRecords {
if len(record.Value) == 0 {
continue
}
for _, value := range record.Value {
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *value),
Name: *recordSet.Name,
Type: recordType,
Value: *value,
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
}
}
// NS
for _, record := range recordSet.Properties.NsRecords {
var recordType = "NS"
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *record.Nsdname),
Name: *recordSet.Name,
Type: recordType,
Value: *record.Nsdname,
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
// SOA
if recordSet.Properties.SoaRecord != nil {
var recordType = "SOA"
var record = recordSet.Properties.SoaRecord
records = append(records, &dnstypes.Record{
Id: this.ComposeRecordId(*recordSet.Name, recordType, *record.Host),
Name: *recordSet.Name,
Type: recordType,
Value: fmt.Sprintf("%s %s %d %d %d %d %d", *record.Host, *record.Email, *record.SerialNumber, *record.RefreshTime, *record.RetryTime, *record.ExpireTime, *record.MinimumTTL),
Route: this.DefaultRoute(),
TTL: types.Int32(*recordSet.Properties.TTL),
})
}
// we don't support other record type yet
return
}
func (this *AzureDNSProvider) recordSetAddRecordValue(recordSet *armdns.RecordSet, value string) (exists bool, err error) {
if recordSet.Properties == nil {
recordSet.Properties = &armdns.RecordSetProperties{}
}
var newProperties = this.recordValueToProperties(*recordSet.Type, value)
if newProperties == nil {
return false, errors.New("recordSetMergeRecordValue: invalid properties")
}
switch *recordSet.Type {
case "A":
for _, record := range recordSet.Properties.ARecords {
if *record.IPv4Address == value {
return true, nil
}
}
recordSet.Properties.ARecords = append(recordSet.Properties.ARecords, newProperties.ARecords...)
case "AAAA":
for _, record := range recordSet.Properties.AaaaRecords {
if *record.IPv6Address == value {
return true, nil
}
}
recordSet.Properties.AaaaRecords = append(recordSet.Properties.AaaaRecords, newProperties.AaaaRecords...)
case "CNAME":
var record = recordSet.Properties.CnameRecord
if record != nil && record.Cname != nil {
if *record.Cname == value {
return true, nil
}
}
recordSet.Properties.CnameRecord = newProperties.CnameRecord
case "TXT":
for _, record := range recordSet.Properties.TxtRecords {
for _, txtValue := range record.Value {
if *txtValue == value {
return true, nil
}
}
}
recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, newProperties.TxtRecords...)
default:
// ignore
return false, errors.New("not supported record type '" + (*recordSet.Type) + "'")
}
return false, nil
}
func (this *AzureDNSProvider) recordSetUpdateRecordValue(recordSet *armdns.RecordSet, recordType string, oldRecordValue string, newRecordValue string) (foundRecord bool, err error) {
if recordSet.Properties == nil {
return false, nil
}
switch recordType {
case "A":
var newRecords = []*armdns.ARecord{}
for _, record := range recordSet.Properties.ARecords {
if *record.IPv4Address == oldRecordValue {
foundRecord = true
continue
}
newRecords = append(newRecords, record)
}
newRecords = append(newRecords, &armdns.ARecord{IPv4Address: this.stringVal(newRecordValue)})
recordSet.Properties.ARecords = newRecords
case "AAAA":
var newRecords = []*armdns.AaaaRecord{}
for _, record := range recordSet.Properties.AaaaRecords {
if *record.IPv6Address == oldRecordValue {
foundRecord = true
continue
}
newRecords = append(newRecords, record)
}
newRecords = append(newRecords, &armdns.AaaaRecord{IPv6Address: this.stringVal(newRecordValue)})
recordSet.Properties.AaaaRecords = newRecords
case "CNAME":
recordSet.Properties.CnameRecord = &armdns.CnameRecord{Cname: this.stringVal(newRecordValue)}
foundRecord = true
case "TXT":
var newRecords = []*armdns.TxtRecord{}
var shouldAdd = true
for _, record := range recordSet.Properties.TxtRecords {
var oldValues = []*string{}
var found = false
for _, oldValue := range record.Value {
if *oldValue == oldRecordValue {
found = true
shouldAdd = false
foundRecord = true
continue
}
oldValues = append(oldValues, oldValue)
}
if found {
oldValues = append(oldValues, this.stringVal(newRecordValue))
record.Value = oldValues // overwrite
}
if len(oldValues) == 0 {
continue
}
newRecords = append(newRecords, record)
}
if shouldAdd {
newRecords = append(newRecords, &armdns.TxtRecord{Value: []*string{this.stringVal(newRecordValue)}})
}
recordSet.Properties.TxtRecords = newRecords
default:
// ignore
return false, errors.New("not supported record type '" + (*recordSet.Type) + "'")
}
return
}
func (this *AzureDNSProvider) recordSetDeleteRecordValue(recordSet *armdns.RecordSet, recordType string, recordValue string) (shouldUpdate bool, shouldDelete bool, err error) {
if recordSet.Properties == nil {
shouldDelete = true
return
}
switch recordType {
case "A":
var newRecords = []*armdns.ARecord{}
for _, record := range recordSet.Properties.ARecords {
if *record.IPv4Address == recordValue {
shouldUpdate = true
continue
}
newRecords = append(newRecords, record)
}
recordSet.Properties.ARecords = newRecords
if !shouldUpdate {
return
}
if len(newRecords) == 0 {
shouldDelete = true
}
case "AAAA":
var newRecords = []*armdns.AaaaRecord{}
for _, record := range recordSet.Properties.AaaaRecords {
if *record.IPv6Address == recordValue {
shouldUpdate = true
continue
}
newRecords = append(newRecords, record)
}
if !shouldUpdate {
return
}
if len(newRecords) == 0 {
shouldDelete = true
}
recordSet.Properties.AaaaRecords = newRecords
case "CNAME":
shouldDelete = true
case "TXT":
var newRecords = []*armdns.TxtRecord{}
for _, record := range recordSet.Properties.TxtRecords {
var oldValues = []*string{}
for _, oldValue := range record.Value {
if *oldValue == recordValue {
shouldUpdate = true
continue
}
oldValues = append(oldValues, oldValue)
}
if len(oldValues) == 0 {
continue
}
record.Value = oldValues
newRecords = append(newRecords, record)
}
recordSet.Properties.TxtRecords = newRecords
if !shouldUpdate {
return
}
if len(newRecords) == 0 {
shouldDelete = true
}
default:
// ignore
return false, false, errors.New("not supported record type '" + (*recordSet.Type) + "'")
}
return
}
func (this *AzureDNSProvider) recordValueToProperties(recordType string, recordValue string) *armdns.RecordSetProperties {
var properties = &armdns.RecordSetProperties{}
switch recordType {
case "A":
properties.ARecords = []*armdns.ARecord{
{
IPv4Address: this.stringVal(recordValue),
},
}
case "AAAA":
properties.AaaaRecords = []*armdns.AaaaRecord{
{
IPv6Address: this.stringVal(recordValue),
},
}
case "CNAME":
properties.CnameRecord = &armdns.CnameRecord{Cname: this.stringVal(recordValue)}
case "TXT":
properties.TxtRecords = []*armdns.TxtRecord{
{
Value: []*string{this.stringVal(recordValue)},
},
}
default:
// ignore
return nil
}
return properties
}
func (this *AzureDNSProvider) stringVal(s string) *string {
return &s
}

View File

@@ -0,0 +1,319 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients_test
import (
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
const AzureDNSTestDomain = "goedge.dev"
func TestAzureDNSProvider_GetDomains(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestAzureDNSProvider_GetRoutes(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes(AzureDNSTestDomain)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestAzureDNSProvider_GetRecords(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords(AzureDNSTestDomain)
if err != nil {
t.Fatal(err)
}
for _, record := range records {
t.Log(record.Id, record.Type, record.Name, record.Value, record.Route, record.TTL)
}
}
func TestAzureDNSProvider_AddRecord_CNAME(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var route = "*"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeCNAME,
Name: "test10",
Value: "hello." + AzureDNSTestDomain,
Route: route,
TTL: 600,
}
err = provider.AddRecord(AzureDNSTestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAzureDNSProvider_AddRecord_TXT(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
//var route = "CONTINENT:AS@COUNTRY:CN@SUBDIVISION:13"
var route = "COUNTRY:JP"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeTXT,
Name: "test-txt",
Value: "i-am-txt",
Route: route,
TTL: 600,
}
err = provider.AddRecord(AzureDNSTestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAzureDNSProvider_AddRecord_A(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
//var route = "CONTINENT:AS@COUNTRY:CN@SUBDIVISION:13"
var route = "COUNTRY:JP"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test2",
Value: "192.168.1.113",
Route: route,
TTL: 600,
}
err = provider.AddRecord(AzureDNSTestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAzureDNSProvider_AddRecord_A_Default(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
//var route = "CONTINENT:AS@COUNTRY:CN@SUBDIVISION:13"
var route = "*"
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test3",
Value: "192.168.1.201",
Route: route,
TTL: 600,
}
err = provider.AddRecord(AzureDNSTestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestAzureDNSProvider_QueryRecord(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
{
record, err := provider.QueryRecord(AzureDNSTestDomain, "test2", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
{
record, err := provider.QueryRecord(AzureDNSTestDomain, "test2", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
}
func TestAzureDNSProvider_QueryRecords(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
{
records, err := provider.QueryRecords(AzureDNSTestDomain, "test2", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestAzureDNSProvider_UpdateRecord(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var route = "COUNTRY:HK"
var id = provider.ComposeRecordId("test2", "A", "192.168.1.115")
err = provider.UpdateRecord(AzureDNSTestDomain, &dnstypes.Record{
Id: id,
}, &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "test2",
Value: "192.168.1.114",
Route: route,
TTL: 1800,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAzureDNSProvider_UpdateRecord_CNAME(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var route = "COUNTRY:HK"
var id = provider.ComposeRecordId("test10", "CNAME", "hello1."+AzureDNSTestDomain+".")
err = provider.UpdateRecord(AzureDNSTestDomain, &dnstypes.Record{
Id: id,
}, &dnstypes.Record{
Type: dnstypes.RecordTypeCNAME,
Name: "test10",
Value: "hello2." + AzureDNSTestDomain + ".",
Route: route,
TTL: 1800,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAzureDNSProvider_UpdateRecord_TXT(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var route = "COUNTRY:HK"
var id = provider.ComposeRecordId("test-txt", "TXT", "i-am-txt")
err = provider.UpdateRecord(AzureDNSTestDomain, &dnstypes.Record{
Id: id,
}, &dnstypes.Record{
Type: dnstypes.RecordTypeTXT,
Name: "test-txt",
Value: "i-am-txt2",
Route: route,
TTL: 1800,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAzureDNSProvider_DeleteRecord(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var id = "JHRlc3QzJEEkMTkyLjE2OC4xLjIwMQ=="
id = "JHRlc3QzJEEkMTkyLjE2OC4xLjIwMw=="
err = provider.DeleteRecord(AzureDNSTestDomain, &dnstypes.Record{
Id: id,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestAzureDNSProvider_DeleteRecord_CNAME(t *testing.T) {
provider, err := testAzureDNSProvider()
if err != nil {
t.Fatal(err)
}
var id = "JGhlbGxvLWZvcndhcmQkQ05BTUUkaGVsbG8uZ29lZGdlLmRldi4="
err = provider.DeleteRecord(AzureDNSTestDomain, &dnstypes.Record{
Id: id,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func testAzureDNSProvider() (provider *dnsclients.AzureDNSProvider, err error) {
dbs.NotifyReady()
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='azureDNS' AND id='47' ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
err = errors.New("PROVIDER NOT FOUND")
return
}
var apiParams = maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider = &dnsclients.AzureDNSProvider{
ProviderId: one.GetInt64("id"),
}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,42 @@
package dnsclients
import (
"fmt"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
)
type BaseProvider struct {
minTTL int32
}
// WrapError 封装解析相关错误
func (this *BaseProvider) WrapError(err error, domain string, record *dnstypes.Record) error {
if err == nil {
return nil
}
if record == nil {
return err
}
var fullname string
if len(record.Name) == 0 {
fullname = domain
} else {
fullname = record.Name + "." + domain
}
return fmt.Errorf("record operation failed: '%s %s %s %d': %w", fullname, record.Type, record.Value, record.TTL, err)
}
// SetMinTTL 设置最小TTL
func (this *BaseProvider) SetMinTTL(ttl int32) {
this.minTTL = ttl
}
// MinTTL 最小TTL
func (this *BaseProvider) MinTTL() int32 {
if this.minTTL > 0 {
return this.minTTL
}
return 0
}

View File

@@ -0,0 +1,38 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnsclients_test
import (
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"testing"
)
func TestBaseProvider_WrapError(t *testing.T) {
var provider = &dnsclients.BaseProvider{}
t.Log(provider.WrapError(nil, "example.com", &dnstypes.Record{
Id: "",
Name: "a",
Type: "A",
Value: "192.168.1.100",
Route: "",
TTL: 3600,
}))
t.Log(provider.WrapError(errors.New("fake error"), "example.com", &dnstypes.Record{
Id: "",
Name: "a",
Type: "A",
Value: "192.168.1.100",
Route: "",
TTL: 3600,
}))
t.Log(provider.WrapError(errors.New("fake error"), "example.com", &dnstypes.Record{
Id: "",
Name: "",
Type: "A",
Value: "192.168.1.100",
Route: "",
TTL: 3600,
}))
}

View File

@@ -0,0 +1,417 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/bunnynet"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const BunnyNetAPIEndpoint = "https://api.bunny.net/"
const BunnyNetDefaultRoute = "default"
var bunnyNetHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
/**Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse("socks5://127.0.0.1:7890")
},**/
},
}
type BunnyNetProvider struct {
BaseProvider
ProviderId int64
apiKey string
}
// Auth 认证
func (this *BunnyNetProvider) Auth(params maps.Map) error {
this.apiKey = params.GetString("apiKey")
if len(this.apiKey) == 0 {
return errors.New("'apiKey' should not be empty")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *BunnyNetProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["apiKey"] = MaskString(params.GetString("apiKey"))
}
// GetDomains 获取所有域名列表
func (this *BunnyNetProvider) GetDomains() (domains []string, err error) {
for i := 1; i <= 1000; i++ {
var resp = new(bunnynet.ListDNSZonesResponse)
err = this.doAPI(http.MethodGet, "/dnszone?perPage=1000&page="+types.String(i), nil, nil, resp)
if err != nil {
return
}
if len(resp.Items) == 0 {
break
}
for _, item := range resp.Items {
domains = append(domains, item.Domain)
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *BunnyNetProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
if len(domain) == 0 {
return
}
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return nil, err
}
for _, record := range domainResp.Records {
var recordType = this.recordTypeString(record.Type)
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
if len(record.LatencyZone) == 0 {
record.LatencyZone = BunnyNetDefaultRoute
}
records = append(records, &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: recordType,
Value: record.Value,
Route: record.LatencyZone,
TTL: record.Ttl,
})
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *BunnyNetProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
if len(domain) == 0 {
return
}
var resp = new(bunnynet.RegionListResponse)
err = this.doAPI(http.MethodGet, "/region", nil, nil, resp)
if err != nil {
return
}
for _, region := range *resp {
routes = append(routes, &dnstypes.Route{
Name: region.Name,
Code: region.RegionCode,
})
}
return
}
// QueryRecord 查询单个记录
func (this *BunnyNetProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
if len(domain) == 0 {
return nil, nil
}
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return nil, err
}
for _, record := range domainResp.Records {
if record.Name == name && this.recordTypeString(record.Type) == recordType {
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
if len(record.LatencyZone) == 0 {
record.LatencyZone = BunnyNetDefaultRoute
}
return &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: recordType,
Value: record.Value,
Route: record.LatencyZone,
TTL: record.Ttl,
}, nil
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *BunnyNetProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
if len(domain) == 0 {
return nil, nil
}
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return nil, err
}
var resultRecords []*dnstypes.Record
for _, record := range domainResp.Records {
if record.Name == name && this.recordTypeString(record.Type) == recordType {
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
if len(record.LatencyZone) == 0 {
record.LatencyZone = BunnyNetDefaultRoute
}
resultRecords = append(resultRecords, &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: recordType,
Value: record.Value,
Route: record.LatencyZone,
TTL: record.Ttl,
})
}
}
return resultRecords, nil
}
// AddRecord 设置记录
func (this *BunnyNetProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return err
}
if newRecord.Route == BunnyNetDefaultRoute {
newRecord.Route = ""
}
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var recordMap = maps.Map{
"Type": this.recordTypeInt(newRecord.Type),
"Ttl": newRecord.TTL,
"Value": newRecord.Value,
"Name": newRecord.Name,
}
if len(newRecord.Route) > 0 {
recordMap["SmartRoutingType"] = 1
recordMap["LatencyZone"] = newRecord.Route
}
var resp = new(bunnynet.RecordResponse)
err = this.doAPI(http.MethodPut, "/dnszone/"+types.String(domainResp.Id)+"/records", nil, recordMap, resp)
if err != nil {
return err
}
newRecord.Id = types.String(resp.Id)
return nil
}
// UpdateRecord 修改记录
func (this *BunnyNetProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return err
}
if newRecord.Route == BunnyNetDefaultRoute {
newRecord.Route = ""
}
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var recordMap = maps.Map{
"Type": this.recordTypeInt(newRecord.Type),
"Ttl": newRecord.TTL,
"Value": newRecord.Value,
"Name": newRecord.Name,
}
if len(newRecord.Route) > 0 {
recordMap["SmartRoutingType"] = 1
recordMap["LatencyZone"] = newRecord.Route
}
err = this.doAPI(http.MethodPost, "/dnszone/"+types.String(domainResp.Id)+"/records/"+record.Id, nil, recordMap, nil)
if err != nil {
return err
}
return nil
}
// DeleteRecord 删除记录
func (this *BunnyNetProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
domainResp, err := this.findDomain(domain)
if err != nil || domainResp == nil {
return err
}
return this.doAPI(http.MethodDelete, "/dnszone/"+types.String(domainResp.Id)+"/records/"+record.Id, nil, nil, nil)
}
func (this *BunnyNetProvider) findDomain(domainName string) (*bunnynet.DNSZoneResponse, error) {
for i := 1; i <= 1000; i++ {
var resp = new(bunnynet.ListDNSZonesResponse)
err := this.doAPI(http.MethodGet, "/dnszone?search="+domainName+"&perPage=1000&page="+types.String(i), nil, nil, resp)
if err != nil {
return nil, err
}
if len(resp.Items) == 0 {
break
}
for _, item := range resp.Items {
if item.Domain == domainName {
return item, nil
}
}
}
return nil, nil
}
// DefaultRoute 默认线路
func (this *BunnyNetProvider) DefaultRoute() string {
return BunnyNetDefaultRoute
}
func (this *BunnyNetProvider) doAPI(method string, apiPath string, args map[string]string, bodyMap maps.Map, respPtr any) error {
var apiURL = BunnyNetAPIEndpoint + strings.TrimLeft(apiPath, "/")
if len(args) > 0 {
apiURL += "?"
argStrings := []string{}
for k, v := range args {
argStrings = append(argStrings, k+"="+url.QueryEscape(v))
}
apiURL += strings.Join(argStrings, "&")
}
method = strings.ToUpper(method)
var bodyReader io.Reader = nil
if bodyMap != nil {
bodyData, err := json.Marshal(bodyMap)
if err != nil {
return err
}
bodyReader = bytes.NewReader(bodyData)
}
req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("AccessKey", this.apiKey)
resp, err := bunnyNetHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if respPtr == nil {
return nil
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK &&
resp.StatusCode != http.StatusCreated &&
resp.StatusCode != http.StatusNoContent {
return errors.New("response error: " + string(data))
}
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w, response text: %s", err, string(data))
}
return nil
}
func (this *BunnyNetProvider) recordTypeString(recordType int) dnstypes.RecordType {
switch recordType {
case 0:
return dnstypes.RecordTypeA
case 1:
return dnstypes.RecordTypeAAAA
case 2:
return dnstypes.RecordTypeCNAME
case 3:
return dnstypes.RecordTypeTXT
default:
// other record types has been not supported yet
return ""
}
}
func (this *BunnyNetProvider) recordTypeInt(recordType string) int {
switch recordType {
case dnstypes.RecordTypeA:
return 0
case dnstypes.RecordTypeAAAA:
return 1
case dnstypes.RecordTypeCNAME:
return 2
case dnstypes.RecordTypeTXT:
return 3
default:
// other record types has been not supported yet
return -1
}
}

View File

@@ -0,0 +1,246 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestBunnyNetProvider_GetDomains(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
t.Log(provider.GetDomains())
}
func TestBunnyNetProvider_GetRecords(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
t.Log("===meloy.cn===")
{
records, err := provider.GetRecords("meloy.cn")
if err != nil {
t.Fatal(err)
}
if len(records) > 0 {
t.Log(len(records), "records")
}
logs.PrintAsJSON(records, t)
}
t.Log("===goedge.cn===")
{
records, err := provider.GetRecords("goedge.cn")
if err != nil {
t.Fatal(err)
}
if len(records) > 0 {
t.Log(len(records), "records")
}
logs.PrintAsJSON(records, t)
}
}
func TestBunnyNetProvider_QueryRecord(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
{
t.Log("== goedge.cn/A ==")
record, err := provider.QueryRecord("goedge.cn", "", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== www.goedge.cn/A ==")
record, err := provider.QueryRecord("goedge.cn", "www", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== cname.goedge.cn/CNAME ==")
record, err := provider.QueryRecord("goedge.cn", "cname", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== test.goedge.cn ==")
record, err := provider.QueryRecord("goedge.cn", "test", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
}
func TestBunnyNetProvider_QueryRecords(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
{
t.Log("== www.goedge.cn/A ==")
records, err := provider.QueryRecords("goedge.cn", "www", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
{
t.Log("== goedge.cn/A ==")
records, err := provider.QueryRecords("goedge.cn", "", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestBunnyNetProvider_AddRecord(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
{
var newRecord = &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "1.2.3.4",
Route: "DE",
TTL: 300,
}
err = provider.AddRecord("goedge.cn", newRecord)
if err != nil {
t.Fatal(err)
}
t.Log("new record id:", newRecord.Id)
}
{
var newRecord = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeCNAME,
Value: "cdn.goedge.cn.",
Route: "",
}
err = provider.AddRecord("goedge.cn", newRecord)
if err != nil {
t.Fatal(err)
}
t.Log("new record id:", newRecord.Id)
}
t.Log("ok")
}
func TestBunnyNetProvider_AddRecord_Route(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
{
var newRecord = &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "1.2.3.4",
Route: "HK",
TTL: 300,
}
err = provider.AddRecord("goedge.cn", newRecord)
if err != nil {
t.Fatal(err)
}
t.Log("new record id:", newRecord.Id)
}
}
func TestBunnyNetProvider_UpdateRecord(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
err = provider.UpdateRecord("goedge.cn", &dnstypes.Record{Id: "3792811"}, &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "1.1.1.1",
Route: "AT",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestBunnyNetProvider_DeleteRecord(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("goedge.cn", &dnstypes.Record{
Id: "3792789",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestBunnyNetProvider_GetRoutes(t *testing.T) {
provider, err := testBunnyNetProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes("goedge.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func testBunnyNetProvider() (ProviderInterface, error) {
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='bunnyNet' AND state=1 ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
return nil, errors.New("can not find providers with type 'bunnyNet'")
}
var apiParams = maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
var provider = &BunnyNetProvider{}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,380 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnsclients
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/cloudflare"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
const CloudFlareAPIEndpoint = "https://api.cloudflare.com/client/v4/"
const CloudFlareDefaultRoute = "default"
var cloudFlareHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
/**Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse("socks5://127.0.0.1:7890")
},**/
},
}
type CloudFlareProvider struct {
BaseProvider
ProviderId int64
apiKey string // API密钥
email string // 账号邮箱
zoneMap map[string]string // domain => zoneId
zoneLocker sync.Mutex
}
// Auth 认证
func (this *CloudFlareProvider) Auth(params maps.Map) error {
this.apiKey = params.GetString("apiKey")
if len(this.apiKey) == 0 {
return errors.New("'apiKey' should not be empty")
}
this.email = params.GetString("email")
if len(this.email) == 0 {
return errors.New("'email' should not be empty")
}
this.zoneMap = map[string]string{}
return nil
}
// MaskParams 对参数进行掩码
func (this *CloudFlareProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["apiKey"] = MaskString(params.GetString("apiKey"))
}
// GetDomains 获取所有域名列表
func (this *CloudFlareProvider) GetDomains() (domains []string, err error) {
for page := 1; page <= 500; page++ {
var resp = new(cloudflare.ZonesResponse)
err = this.doAPI(http.MethodGet, "zones", map[string]string{
"per_page": "50",
"page": types.String(page),
}, nil, resp)
if err != nil {
return nil, err
}
if len(resp.Result) == 0 {
break
}
for _, zone := range resp.Result {
domains = append(domains, zone.Name)
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *CloudFlareProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return nil, err
}
// 这个页数限制预示着每次最多只能获取 500 * 100 即5万个数据
for page := 1; page <= 500; page++ {
resp := new(cloudflare.GetDNSRecordsResponse)
err = this.doAPI(http.MethodGet, "zones/"+zoneId+"/dns_records", map[string]string{
"per_page": "100",
"page": strconv.Itoa(page),
}, nil, resp)
if err != nil {
return nil, err
}
if len(resp.Result) == 0 {
break
}
for _, record := range resp.Result {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Content, ".") {
record.Content += "."
}
record.Name = strings.TrimSuffix(record.Name, "."+domain)
records = append(records, &dnstypes.Record{
Id: record.Id,
Name: record.Name,
Type: record.Type,
Value: record.Content,
Route: CloudFlareDefaultRoute,
})
}
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *CloudFlareProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
routes = []*dnstypes.Route{
{Name: "默认", Code: CloudFlareDefaultRoute},
}
return
}
// QueryRecord 查询单个记录
func (this *CloudFlareProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return nil, err
}
var resp = new(cloudflare.GetDNSRecordsResponse)
err = this.doAPI(http.MethodGet, "zones/"+zoneId+"/dns_records", map[string]string{
"per_page": "100",
"name": name + "." + domain,
"type": recordType,
}, nil, resp)
if err != nil {
return nil, err
}
if len(resp.Result) == 0 {
return nil, nil
}
var record = resp.Result[0]
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Content, ".") {
record.Content += "."
}
record.Name = strings.TrimSuffix(record.Name, "."+domain)
return &dnstypes.Record{
Id: record.Id,
Name: record.Name,
Type: record.Type,
Value: record.Content,
TTL: types.Int32(record.Ttl),
Route: CloudFlareDefaultRoute,
}, nil
}
// QueryRecords 查询多个记录
func (this *CloudFlareProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) (records []*dnstypes.Record, err error) {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return nil, err
}
var resp = new(cloudflare.GetDNSRecordsResponse)
err = this.doAPI(http.MethodGet, "zones/"+zoneId+"/dns_records", map[string]string{
"per_page": "100",
"name": name + "." + domain,
"type": recordType,
}, nil, resp)
if err != nil {
return nil, err
}
if len(resp.Result) == 0 {
return nil, nil
}
for _, record := range resp.Result {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Content, ".") {
record.Content += "."
}
record.Name = strings.TrimSuffix(record.Name, "."+domain)
records = append(records, &dnstypes.Record{
Id: record.Id,
Name: record.Name,
Type: record.Type,
Value: record.Content,
TTL: types.Int32(record.Ttl),
Route: CloudFlareDefaultRoute,
})
}
return records, nil
}
// AddRecord 设置记录
func (this *CloudFlareProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
resp := new(cloudflare.CreateDNSRecordResponse)
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 1 // 自动默认
}
err = this.doAPI(http.MethodPost, "zones/"+zoneId+"/dns_records", nil, maps.Map{
"type": newRecord.Type,
"name": newRecord.Name + "." + domain,
"content": newRecord.Value,
"ttl": ttl,
}, resp)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
return nil
}
// UpdateRecord 修改记录
func (this *CloudFlareProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 1 // 自动默认
}
resp := new(cloudflare.UpdateDNSRecordResponse)
return this.doAPI(http.MethodPut, "zones/"+zoneId+"/dns_records/"+record.Id, nil, maps.Map{
"type": newRecord.Type,
"name": newRecord.Name + "." + domain,
"content": newRecord.Value,
"ttl": ttl,
}, resp)
}
// DeleteRecord 删除记录
func (this *CloudFlareProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
zoneId, err := this.findZoneIdWithDomain(domain)
if err != nil {
return this.WrapError(err, domain, record)
}
resp := new(cloudflare.DeleteDNSRecordResponse)
err = this.doAPI(http.MethodDelete, "zones/"+zoneId+"/dns_records/"+record.Id, map[string]string{}, nil, resp)
if err != nil {
return this.WrapError(err, domain, record)
}
return nil
}
// DefaultRoute 默认线路
func (this *CloudFlareProvider) DefaultRoute() string {
return CloudFlareDefaultRoute
}
// 执行API
func (this *CloudFlareProvider) doAPI(method string, apiPath string, args map[string]string, bodyMap maps.Map, respPtr cloudflare.ResponseInterface) error {
apiURL := CloudFlareAPIEndpoint + strings.TrimLeft(apiPath, "/")
if len(args) > 0 {
apiURL += "?"
argStrings := []string{}
for k, v := range args {
argStrings = append(argStrings, k+"="+url.QueryEscape(v))
}
apiURL += strings.Join(argStrings, "&")
}
method = strings.ToUpper(method)
var bodyReader io.Reader = nil
if bodyMap != nil {
bodyData, err := json.Marshal(bodyMap)
if err != nil {
return err
}
bodyReader = bytes.NewReader(bodyData)
}
req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Auth-Key", this.apiKey)
req.Header.Set("x-Auth-Email", this.email)
resp, err := cloudFlareHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w, response text: %s", err, string(data))
}
return nil
}
// 列出一个域名对应的区域
func (this *CloudFlareProvider) findZoneIdWithDomain(domain string) (zoneId string, err error) {
this.zoneLocker.Lock()
cacheZonedId, ok := this.zoneMap[domain]
if ok {
this.zoneLocker.Unlock()
return cacheZonedId, nil
}
this.zoneLocker.Unlock()
resp := new(cloudflare.ZonesResponse)
err = this.doAPI(http.MethodGet, "zones", map[string]string{
"name": domain,
}, nil, resp)
if err != nil {
return "", err
}
if len(resp.Result) == 0 {
return "", errors.New("can not found zone for domain '" + domain + "'")
}
zoneId = resp.Result[0].Id
this.zoneLocker.Lock()
this.zoneMap[domain] = zoneId
this.zoneLocker.Unlock()
return zoneId, nil
}

View File

@@ -0,0 +1,192 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnsclients
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestCloudFlareProvider_GetDomains(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
t.Log(provider.GetDomains())
}
func TestCloudFlareProvider_GetRecords(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
t.Log("===meloy.cn===")
{
records, err := provider.GetRecords("meloy.cn")
if err != nil {
t.Fatal(err)
}
if len(records) > 0 {
t.Log(len(records), "records")
}
logs.PrintAsJSON(records, t)
}
t.Log("===teaos.cn===")
{
records, err := provider.GetRecords("teaos.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestCloudFlareProvider_QueryRecord(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
{
t.Log("== www.meloy.cn/A ==")
record, err := provider.QueryRecord("meloy.cn", "www", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== www.meloy.cn/CNAME ==")
record, err := provider.QueryRecord("meloy.cn", "www", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== hello.meloy.cn ==")
record, err := provider.QueryRecord("meloy.cn", "hello", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
{
t.Log("== test.meloy.cn ==")
record, err := provider.QueryRecord("meloy.cn", "test", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
}
func TestCloudFlareProvider_QueryRecords(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
{
t.Log("== www.meloy.cn/A ==")
records, err := provider.QueryRecords("meloy.cn", "www", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestCloudFlareProvider_AddRecord(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
{
err = provider.AddRecord("meloy.cn", &dnstypes.Record{
Id: "",
Name: "test",
Type: dnstypes.RecordTypeA,
Value: "182.92.212.46",
Route: "",
TTL: 300,
})
if err != nil {
t.Fatal(err)
}
}
{
err = provider.AddRecord("meloy.cn", &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeCNAME,
Value: "cdn.meloy.cn.",
Route: "",
})
if err != nil {
t.Fatal(err)
}
}
t.Log("ok")
}
func TestCloudFlareProvider_UpdateRecord(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
err = provider.UpdateRecord("meloy.cn", &dnstypes.Record{Id: "b4da7ad9f90173ec37c80ba6bb70641a"}, &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeCNAME,
Value: "cdn123.meloy.cn.",
Route: "",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestCloudFlareProvider_DeleteRecord(t *testing.T) {
provider, err := testCloudFlareProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("meloy.cn", &dnstypes.Record{
Id: "86282d89bbd1f66a69ca409da84f34b1",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func testCloudFlareProvider() (ProviderInterface, error) {
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='cloudFlare' AND state=1 ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
return nil, errors.New("can not find providers with type 'cloudFlare'")
}
var apiParams = maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
var provider = &CloudFlareProvider{}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,371 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"crypto/tls"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/cloudns"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const (
ClouDNSDefaultRoute = "default"
ClouDNSAPIEndpoint = "https://api.cloudns.net"
)
var clouDNSHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// ClouDNSProvider ClouDNS.net
// 参考文档https://www.cloudns.net/wiki/article/41/
type ClouDNSProvider struct {
BaseProvider
ProviderId int64
authId int64
subAuthId int64
authPassword string
}
// Auth 认证
func (this *ClouDNSProvider) Auth(params maps.Map) error {
this.authId = params.GetInt64("authId")
this.subAuthId = params.GetInt64("subAuthId")
this.authPassword = params.GetString("authPassword")
return nil
}
// MaskParams 对参数进行掩码
func (this *ClouDNSProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["authPassword"] = MaskString(params.GetString("authPassword"))
}
// GetDomains 获取所有域名列表
func (this *ClouDNSProvider) GetDomains() (domains []string, err error) {
var page = 1
for {
var zones = cloudns.ZonesResponse{}
err = this.doAPI(http.MethodPost, "/dns/list-zones.json", map[string]string{
"page": types.String(page),
"rows-per-page": "100",
}, &zones)
if err != nil {
return
}
if len(zones) == 0 {
break
}
for _, zone := range zones {
if zone.Zone == "domain" {
domains = append(domains, zone.Name)
}
}
page++
}
return
}
// GetRecords 获取域名解析记录列表
func (this *ClouDNSProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var page = 1
for {
var respRecords = cloudns.RecordsResponse{}
err = this.doAPI(http.MethodPost, "/dns/records.json", map[string]string{
"domain-name": domain,
"page": types.String(page),
"rows-per-page": "100",
}, &respRecords)
if err != nil {
return
}
if len(respRecords) == 0 {
break
}
for _, record := range respRecords {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Record, ".") {
record.Record += "."
}
records = append(records, &dnstypes.Record{
Id: record.Id,
Name: record.Host,
Type: record.Type,
Value: record.Record,
Route: ClouDNSDefaultRoute,
TTL: types.Int32(record.TTL),
})
}
page++
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *ClouDNSProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
routes = []*dnstypes.Route{
{Name: "默认", Code: ClouDNSDefaultRoute},
}
// TODO 支持GeoDNS
return
}
// QueryRecord 查询单个记录
func (this *ClouDNSProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
var respRecords = cloudns.RecordsResponse{}
err := this.doAPI(http.MethodPost, "/dns/records.json", map[string]string{
"domain-name": domain,
"host": name,
"type": recordType,
}, &respRecords)
if err != nil {
return nil, err
}
if len(respRecords) == 0 {
return nil, nil
}
for _, record := range respRecords {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Record, ".") {
record.Record += "."
}
return &dnstypes.Record{
Id: record.Id,
Name: record.Host,
Type: record.Type,
Value: record.Record,
Route: ClouDNSDefaultRoute,
TTL: types.Int32(record.TTL),
}, nil
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *ClouDNSProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
var respRecords = cloudns.RecordsResponse{}
err := this.doAPI(http.MethodPost, "/dns/records.json", map[string]string{
"domain-name": domain,
"host": name,
"type": recordType,
}, &respRecords)
if err != nil {
return nil, err
}
if len(respRecords) == 0 {
return nil, nil
}
var result = []*dnstypes.Record{}
for _, record := range respRecords {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Record, ".") {
record.Record += "."
}
result = append(result, &dnstypes.Record{
Id: record.Id,
Name: record.Host,
Type: record.Type,
Value: record.Record,
Route: ClouDNSDefaultRoute,
TTL: types.Int32(record.TTL),
})
}
return result, nil
}
// AddRecord 设置记录
func (this *ClouDNSProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 1800
}
var availableTTLs = []int32{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600, 2592000}
var ttlFound = false
for _, aTTL := range availableTTLs {
if aTTL == ttl {
ttlFound = true
break
}
}
if !ttlFound {
ttl = 1800
}
var statusResp = &cloudns.StatusResponse{}
err := this.doAPI(http.MethodPost, "/dns/add-record.json", map[string]string{
"domain-name": domain,
"record-type": newRecord.Type,
"host": newRecord.Name,
"record": newRecord.Value,
"ttl": types.String(ttl),
}, statusResp)
if err != nil {
return err
}
if statusResp.Status != "Success" {
return errors.New("Failed: " + statusResp.StatusDescription)
}
return nil
}
// UpdateRecord 修改记录
func (this *ClouDNSProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 1800
}
var availableTTLs = []int32{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600, 2592000}
var ttlFound = false
for _, aTTL := range availableTTLs {
if aTTL == ttl {
ttlFound = true
break
}
}
if !ttlFound {
ttl = 1800
}
var statusResp = &cloudns.StatusResponse{}
err := this.doAPI(http.MethodPost, "/dns/mod-record.json", map[string]string{
"domain-name": domain,
"record-id": record.Id,
"record-type": newRecord.Type,
"host": newRecord.Name,
"record": newRecord.Value,
"ttl": types.String(ttl),
}, statusResp)
if err != nil {
return err
}
if statusResp.Status != "Success" {
return errors.New("Failed: " + statusResp.StatusDescription)
}
return nil
}
// DeleteRecord 删除记录
func (this *ClouDNSProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
var statusResp = &cloudns.StatusResponse{}
err := this.doAPI(http.MethodPost, "/dns/delete-record.json", map[string]string{
"domain-name": domain,
"record-id": record.Id,
}, statusResp)
if err != nil {
return err
}
if statusResp.Status != "Success" {
return errors.New("Failed: " + statusResp.StatusDescription)
}
return nil
}
// DefaultRoute 默认线路
func (this *ClouDNSProvider) DefaultRoute() string {
return ClouDNSDefaultRoute
}
// 发送请求
func (this *ClouDNSProvider) doAPI(method string, apiPath string, params map[string]string, respPtr interface{}) error {
var apiURL = ClouDNSAPIEndpoint + apiPath
method = strings.ToUpper(method)
var query = url.Values{}
if this.authId > 0 {
query.Set("auth-id", types.String(this.authId))
} else if this.subAuthId > 0 {
query.Set("sub-auth-id", types.String(this.subAuthId))
}
query.Set("auth-password", this.authPassword)
for k, v := range params {
query.Set(k, v)
}
req, err := http.NewRequest(method, apiURL, strings.NewReader(query.Encode()))
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
if method == http.MethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := clouDNSHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
if respPtr != nil {
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w: %s", err, string(data))
}
}
return nil
}

View File

@@ -0,0 +1,173 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients_test
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestClouDNSProvider_GetDomains(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestClouDNSProvider_GetRecords(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords("goedge.org")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
func TestClouDNSProvider_GetRoutes(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes("goedge.org")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestClouDNSProvider_QueryRecord(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
for _, recordName := range []string{"www", "test", "@", ""} {
t.Log("===", recordName, "===")
record, err := provider.QueryRecord("goedge.org", recordName, dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
}
func TestClouDNSProvider_AddRecord(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
{
var record = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.100",
Route: "",
TTL: 7200,
}
err := provider.AddRecord("goedge.org", record)
if err != nil {
t.Fatal(err)
}
}
{
var record = &dnstypes.Record{
Id: "",
Name: "test2",
Type: dnstypes.RecordTypeCNAME,
Value: "goedge.org.",
Route: "",
TTL: 0,
}
err := provider.AddRecord("goedge.org", record)
if err != nil {
t.Fatal(err)
}
}
}
func TestClouDNSProvider_UpdateRecord(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
var record = &dnstypes.Record{
Id: "262076455",
}
var newRecord = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.101",
Route: "",
TTL: 3600,
}
err = provider.UpdateRecord("goedge.org", record, newRecord)
if err != nil {
t.Fatal(err)
}
}
func TestClouDNSProvider_DeleteRecord(t *testing.T) {
provider, err := testClouDNSProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("goedge.org", &dnstypes.Record{
Id: "262075770",
Name: "",
Type: "",
Value: "",
Route: "",
TTL: 0,
})
if err != nil {
t.Fatal(err)
}
}
func testClouDNSProvider() (dnsclients.ProviderInterface, error) {
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='cloudns' ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
return nil, errors.New("can not find providers with type 'cloudns'")
}
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider := &dnsclients.ClouDNSProvider{}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,215 @@
package dnsclients
import (
"bytes"
"crypto/sha1"
"crypto/tls"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"io"
"net/http"
"strconv"
"time"
)
var customHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// CustomHTTPProvider HTTP自定义DNS
type CustomHTTPProvider struct {
url string
secret string
ProviderId int64
BaseProvider
}
// Auth 认证
// 参数:
// - url
// - secret
func (this *CustomHTTPProvider) Auth(params maps.Map) error {
this.url = params.GetString("url")
if len(this.url) == 0 {
return errors.New("'url' should not be empty")
}
this.secret = params.GetString("secret")
if len(this.secret) == 0 {
return errors.New("'secret' should not be empty")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *CustomHTTPProvider) MaskParams(params maps.Map) {
// 这里暂时不要掩码,避免用户忘记
}
// GetDomains 获取所有域名列表
func (this *CustomHTTPProvider) GetDomains() (domains []string, err error) {
resp, err := this.post(maps.Map{
"action": "GetDomains",
})
if err != nil {
return nil, err
}
err = json.Unmarshal(resp, &domains)
return
}
// GetRecords 获取域名解析记录列表
func (this *CustomHTTPProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
resp, err := this.post(maps.Map{
"action": "GetRecords",
"domain": domain,
})
if err != nil {
return nil, err
}
err = json.Unmarshal(resp, &records)
return
}
// GetRoutes 读取域名支持的线路数据
func (this *CustomHTTPProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
resp, err := this.post(maps.Map{
"action": "GetRoutes",
"domain": domain,
})
if err != nil {
return nil, err
}
err = json.Unmarshal(resp, &routes)
return
}
// QueryRecord 查询单个记录
func (this *CustomHTTPProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
resp, err := this.post(maps.Map{
"action": "QueryRecord",
"domain": domain,
"name": name,
"recordType": recordType,
})
if err != nil {
return nil, err
}
if len(resp) == 0 || string(resp) == "null" {
return nil, nil
}
var record = &dnstypes.Record{}
err = json.Unmarshal(resp, record)
if err != nil {
return nil, err
}
if len(record.Value) == 0 {
return nil, nil
}
return record, nil
}
// QueryRecords 查询多个记录
func (this *CustomHTTPProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) (result []*dnstypes.Record, err error) {
resp, err := this.post(maps.Map{
"action": "QueryRecords",
"domain": domain,
"name": name,
"recordType": recordType,
})
if err != nil {
return nil, err
}
if len(resp) == 0 || string(resp) == "null" {
return nil, nil
}
result = []*dnstypes.Record{}
err = json.Unmarshal(resp, &result)
if err != nil {
return nil, err
}
return result, nil
}
// AddRecord 设置记录
func (this *CustomHTTPProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
_, err := this.post(maps.Map{
"action": "AddRecord",
"domain": domain,
"newRecord": newRecord,
})
return this.WrapError(err, domain, newRecord)
}
// UpdateRecord 修改记录
func (this *CustomHTTPProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
_, err := this.post(maps.Map{
"action": "UpdateRecord",
"domain": domain,
"record": record,
"newRecord": newRecord,
})
return this.WrapError(err, domain, newRecord)
}
// DeleteRecord 删除记录
func (this *CustomHTTPProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
_, err := this.post(maps.Map{
"action": "DeleteRecord",
"domain": domain,
"record": record,
})
return this.WrapError(err, domain, record)
}
// DefaultRoute 默认线路
func (this *CustomHTTPProvider) DefaultRoute() string {
resp, err := this.post(maps.Map{
"action": "DefaultRoute",
})
if err != nil {
return ""
}
return string(resp)
}
// 执行操作
func (this *CustomHTTPProvider) post(params maps.Map) (respData []byte, err error) {
data, err := json.Marshal(params)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, this.url, bytes.NewReader(data))
if err != nil {
return nil, err
}
var timestamp = strconv.FormatInt(time.Now().Unix(), 10)
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Token", fmt.Sprintf("%x", sha1.Sum([]byte(this.secret+"@"+timestamp))))
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("Content-Type", "application/json")
resp, err := customHTTPClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("status should be 200, but got '" + strconv.Itoa(resp.StatusCode) + "'")
}
return io.ReadAll(resp.Body)
}

View File

@@ -0,0 +1,62 @@
package dnsclients
import (
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestCustomHTTPProvider_GetDomains(t *testing.T) {
provider := CustomHTTPProvider{}
err := provider.Auth(maps.Map{
"url": "http://127.0.0.1:2345/dns",
"secret": "123456",
})
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestCustomHTTPProvider_AddRecord(t *testing.T) {
provider := CustomHTTPProvider{}
err := provider.Auth(maps.Map{
"url": "http://127.0.0.1:1234/dns",
"secret": "123456",
})
if err != nil {
t.Fatal(err)
}
err = provider.AddRecord("hello.com", &dnstypes.Record{
Id: "",
Name: "world",
Type: dnstypes.RecordTypeA,
Value: "127.0.0.1",
Route: "default",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestCustomHTTPProvider_GetRecords(t *testing.T) {
provider := CustomHTTPProvider{}
err := provider.Auth(maps.Map{
"url": "http://127.0.0.1:1234/dns",
"secret": "123456",
})
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords("hello.com")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}

View File

@@ -0,0 +1,635 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnscom"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
const (
DNSComAPIEndpoint = "https://www.51dns.com"
)
var goDNSComHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// DNSComProvider 51DNS.COM域名服务
// 参考文档https://www.51dns.com/document/api/4/81.html
type DNSComProvider struct {
BaseProvider
ProviderId int64
key string
secret string
domainMap map[string]string // domainName => id
locker sync.Mutex
}
// Auth 认证
func (this *DNSComProvider) Auth(params maps.Map) error {
this.domainMap = map[string]string{}
this.key = params.GetString("key")
if len(this.key) == 0 {
return errors.New("require 'key' parameter")
}
this.secret = params.GetString("secret")
if len(this.secret) == 0 {
return errors.New("require 'secret' parameter")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *DNSComProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["secret"] = MaskString(params.GetString("secret"))
}
// GetDomains 获取所有域名列表
func (this *DNSComProvider) GetDomains() (domains []string, err error) {
var pageSize = 100
var pageCount = 0
var queryPage = func(page int) error {
var resp = &dnscom.DomainListResponse{}
err := this.doAPI(http.MethodGet, "/api/domain/list/", map[string]string{
"page": types.String(page),
"pageSize": types.String(pageSize),
}, &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
if page == 1 {
pageCount = resp.Data.PageCount
}
for _, d := range resp.Data.Data {
domains = append(domains, d.Domains)
}
return nil
}
err = queryPage(1)
if err != nil {
return nil, err
}
// 其他页
if pageCount > 1 {
for page := 2; page <= pageCount; page++ {
err = queryPage(page)
if err != nil {
return nil, err
}
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *DNSComProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
// 获取域名ID
domainId, err := this.queryDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return nil, errors.New("can not find domain '" + domain + "'")
}
// 列出记录
var pageSize = 100
var pageCount = 0
var queryPage = func(page int) error {
var resp = &dnscom.RecordListResponse{}
err := this.doAPI(http.MethodGet, "/api/record/list/", map[string]string{
"domainID": domainId,
"page": types.String(page),
"pageSize": types.String(pageSize),
}, &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
if page == 1 {
pageCount = resp.Data.PageCount
}
for _, record := range resp.Data.Data {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
records = append(records, &dnstypes.Record{
Id: types.String(record.RecordID),
Name: record.Record,
Type: record.Type,
Value: record.Value,
Route: types.String(record.ViewID),
TTL: types.Int32(record.TTL),
})
}
return nil
}
err = queryPage(1)
if err != nil {
return nil, err
}
if pageCount > 1 {
for page := 2; page <= pageCount; page++ {
err = queryPage(page)
if err != nil {
return nil, err
}
}
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *DNSComProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
_ = domain
// 区域
{
var resp = &dnscom.IPAreaViewListResponse{}
err = this.doAPI(http.MethodGet, "/api/ip/areaviewlist/", map[string]string{}, resp)
if err != nil {
return
}
if resp.Code != 0 {
return nil, this.composeError(resp.Code, resp.Message)
}
for _, route := range resp.Data {
routes = append(routes, &dnstypes.Route{
Name: "[地区]" + route.Name,
Code: types.String(route.ViewID),
})
}
}
// ISP
{
var resp = &dnscom.IPISPViewListResponse{}
err = this.doAPI(http.MethodGet, "/api/ip/ispviewlist/", map[string]string{}, resp)
if err != nil {
return
}
if resp.Code != 0 {
return nil, this.composeError(resp.Code, resp.Message)
}
for _, route := range resp.Data {
routes = append(routes, &dnstypes.Route{
Name: "[ISP]" + route.Name,
Code: types.String(route.ViewID),
})
}
}
return
}
// QueryRecord 查询单个记录
func (this *DNSComProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
// 获取域名ID
domainId, err := this.queryDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return nil, errors.New("can not find domain '" + domain + "'")
}
// 列出记录
var pageSize = 100
var pageCount = 0
var recordResult *dnstypes.Record
var queryPage = func(page int) error {
var resp = &dnscom.RecordListResponse{}
err := this.doAPI(http.MethodGet, "/api/record/list/", map[string]string{
"domainID": domainId,
"host": name,
"page": types.String(page),
"pageSize": types.String(pageSize),
}, &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
if page == 1 {
pageCount = resp.Data.PageCount
}
for _, record := range resp.Data.Data {
// 仍然比对name因为搜索条件为空时API仍然返回了全部的记录
if record.Record == name && record.Type == recordType {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
recordResult = &dnstypes.Record{
Id: types.String(record.RecordID),
Name: record.Record,
Type: record.Type,
Value: record.Value,
Route: types.String(record.ViewID),
TTL: types.Int32(record.TTL),
}
break
}
}
return nil
}
err = queryPage(1)
if err != nil {
return nil, err
}
if recordResult != nil {
return recordResult, nil
}
if pageCount > 1 {
for page := 2; page <= pageCount; page++ {
err = queryPage(page)
if err != nil {
return nil, err
}
if recordResult != nil {
return recordResult, nil
}
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *DNSComProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 获取域名ID
domainId, err := this.queryDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return nil, errors.New("can not find domain '" + domain + "'")
}
// 列出记录
var pageSize = 100
var pageCount = 0
var result = []*dnstypes.Record{}
var queryPage = func(page int) error {
var resp = &dnscom.RecordListResponse{}
err := this.doAPI(http.MethodGet, "/api/record/list/", map[string]string{
"domainID": domainId,
"host": name,
"page": types.String(page),
"pageSize": types.String(pageSize),
}, &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
if page == 1 {
pageCount = resp.Data.PageCount
}
for _, record := range resp.Data.Data {
// 仍然比对name因为搜索条件为空时API仍然返回了全部的记录
if record.Record == name && record.Type == recordType {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
result = append(result, &dnstypes.Record{
Id: types.String(record.RecordID),
Name: record.Record,
Type: record.Type,
Value: record.Value,
Route: types.String(record.ViewID),
TTL: types.Int32(record.TTL),
})
}
}
return nil
}
err = queryPage(1)
if err != nil {
return nil, err
}
if pageCount > 1 {
for page := 2; page <= pageCount; page++ {
err = queryPage(page)
if err != nil {
return nil, err
}
}
}
return result, nil
}
// AddRecord 设置记录
func (this *DNSComProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
// 查找域名ID
domainId, err := this.queryDomainId(domain)
if err != nil {
return err
}
if len(domainId) == 0 {
return errors.New("can not find domain '" + domain + "'")
}
// 创建记录
var resp = &dnscom.CreateRecordResponse{}
var viewId = "0"
if len(newRecord.Route) > 0 {
viewId = newRecord.Route
}
err = this.doAPI(http.MethodGet, "/api/record/create/", map[string]string{
"domainID": domainId,
"type": newRecord.Type,
"viewID": viewId,
"host": newRecord.Name,
"value": newRecord.Value,
"TTL": types.String(newRecord.TTL),
}, resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
return nil
}
// UpdateRecord 修改记录
func (this *DNSComProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
domainId, err := this.queryDomainId(domain)
if err != nil {
return err
}
if len(domainId) == 0 {
return errors.New("can not find domain '" + domain + "'")
}
var resp = &dnscom.RecordModifyResponse{}
var newViewId = "0"
if len(newRecord.Route) > 0 {
newViewId = newRecord.Route
}
err = this.doAPI(http.MethodGet, "/api/record/modify/", map[string]string{
"domainID": domainId,
"recordID": record.Id,
"newhost": newRecord.Name,
"newtype": newRecord.Type,
"newvalue": newRecord.Value,
"newttl": types.String(newRecord.TTL),
"newviewID": newViewId,
}, resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
return nil
}
// DeleteRecord 删除记录
func (this *DNSComProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
domainId, err := this.queryDomainId(domain)
if err != nil {
return err
}
if len(domainId) == 0 {
return errors.New("can not find domain '" + domain + "'")
}
var resp = &dnscom.RecordRemoveResponse{}
err = this.doAPI(http.MethodGet, "/api/record/remove", map[string]string{
"domainID": domainId,
"recordID": record.Id,
}, resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
return nil
}
// DefaultRoute 默认线路
func (this *DNSComProvider) DefaultRoute() string {
return "0"
}
// 查找域名ID
func (this *DNSComProvider) queryDomainId(domain string) (string, error) {
this.locker.Lock()
domainId, ok := this.domainMap[domain]
if ok {
this.locker.Unlock()
return domainId, nil
}
this.locker.Unlock()
var pageSize = 100
var pageCount = 0
var queryPage = func(page int) error {
var resp = &dnscom.DomainSearchResponse{}
err := this.doAPI(http.MethodGet, "/api/domain/search/", map[string]string{
"query": domain,
"page": types.String(page),
"pageSize": types.String(pageSize),
}, &resp)
if err != nil {
return err
}
if resp.Code != 0 {
return this.composeError(resp.Code, resp.Message)
}
if page == 1 {
pageCount = resp.Data.PageCount
}
for _, d := range resp.Data.Data {
if d.Domains == domain {
domainId = d.DomainsID
return nil
}
}
return nil
}
err := queryPage(1)
if err != nil {
return "", err
}
if len(domainId) > 0 {
this.locker.Lock()
this.domainMap[domain] = domainId
this.locker.Unlock()
return domainId, nil
}
// 其他页
if pageCount > 1 {
for page := 2; page <= pageCount; page++ {
err = queryPage(page)
if err != nil {
return "", err
}
if len(domainId) > 0 {
this.locker.Lock()
this.domainMap[domain] = domainId
this.locker.Unlock()
return domainId, nil
}
}
}
return "", nil
}
// 发送请求
func (this *DNSComProvider) doAPI(method string, apiPath string, params map[string]string, respPtr interface{}) error {
var apiURL = DNSComAPIEndpoint + apiPath
method = strings.ToUpper(method)
params["apiKey"] = this.key
params["timestamp"] = types.String(time.Now().Unix())
params["hash"] = this.hashParams(params)
var query = url.Values{}
for k, v := range params {
query.Set(k, v)
}
var reader io.Reader
if method == http.MethodPost {
reader = strings.NewReader(query.Encode())
} else {
apiURL += "?" + query.Encode()
}
req, err := http.NewRequest(method, apiURL, reader)
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
if method == http.MethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := goDNSComHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
if respPtr != nil {
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w: %s", err, string(data))
}
}
return nil
}
// 构造错误提示
func (this *DNSComProvider) composeError(code int, message string) error {
return errors.New("error code:" + types.String(code) + ", message:" + message)
}
// 计算参数Hsh值
func (this *DNSComProvider) hashParams(params map[string]string) string {
var keys = []string{}
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var source string
for _, key := range keys {
if source == "" {
source += key + "=" + params[key]
} else {
source += "&" + key + "=" + params[key]
}
}
var md = md5.Sum([]byte(source + this.secret))
return hex.EncodeToString(md[:])
}

View File

@@ -0,0 +1,189 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients_test
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestDNSComProvider_GetDomains(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestDNSComProvider_GetRecords(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords("goedge.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
func TestDNSComProvider_GetRoutes(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes("goedge.cn")
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestDNSComProvider_QueryRecord(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
for _, recordName := range []string{"www", "test", "@", ""} {
t.Log("===", recordName, "===")
record, err := provider.QueryRecord("goedge.cn", recordName, dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
}
func TestDNSComProvider_QueryRecords(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
for _, recordName := range []string{"www", "test", "@", ""} {
t.Log("===", recordName, "===")
records, err := provider.QueryRecords("goedge.cn", recordName, dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestDNSComProvider_AddRecord(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
{
var record = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.100",
Route: "285344768", // 285344768
TTL: 7200,
}
err := provider.AddRecord("goedge.cn", record)
if err != nil {
t.Fatal(err)
}
}
/**{
var record = &dnstypes.Record{
Id: "",
Name: "test2",
Type: dnstypes.RecordTypeCNAME,
Value: "goedge.cn.",
Route: "",
TTL: 0,
}
err := provider.AddRecord("goedge.cn", record)
if err != nil {
t.Fatal(err)
}
}**/
}
func TestDNSComProvider_UpdateRecord(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
var record = &dnstypes.Record{
Id: "535669373",
}
var newRecord = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.101",
Route: "285345792",
TTL: 3600,
}
err = provider.UpdateRecord("goedge.cn", record, newRecord)
if err != nil {
t.Fatal(err)
}
}
func TestDNSComProvider_DeleteRecord(t *testing.T) {
provider, err := testDNSComProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("goedge.cn", &dnstypes.Record{
Id: "535669356",
Name: "",
Type: "",
Value: "",
Route: "",
TTL: 0,
})
if err != nil {
t.Fatal(err)
}
}
func testDNSComProvider() (dnsclients.ProviderInterface, error) {
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='dnscom' ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
return nil, errors.New("can not find providers with type 'dnscom'")
}
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider := &dnsclients.DNSComProvider{}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,607 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnsla"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
const DNSLaAPIEndpoint = "https://api.dns.la"
var dnsLAHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
type DNSLaProvider struct {
BaseProvider
ProviderId int64
apiId string
secret string
routesLocker sync.Mutex
cachedRoutes map[string][]*dnstypes.Route // domain => []Route
}
// Auth 认证
func (this *DNSLaProvider) Auth(params maps.Map) error {
this.apiId = params.GetString("apiId")
this.secret = params.GetString("secret")
if len(this.apiId) == 0 {
return errors.New("'apiId' should not be empty")
}
if len(this.secret) == 0 {
return errors.New("'secret' should not be empty")
}
this.cachedRoutes = map[string][]*dnstypes.Route{}
return nil
}
// MaskParams 对参数进行掩码
func (this *DNSLaProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["secret"] = MaskString(params.GetString("secret"))
}
// GetDomains 获取所有域名列表
func (this *DNSLaProvider) GetDomains() (domains []string, err error) {
for i := 1; i < 5000; i++ {
var resp = &dnsla.DomainListResponse{}
err = this.doAPI(http.MethodGet, "/api/domainList", map[string]string{
"pageSize": "100",
"pageIndex": types.String(i),
}, nil, resp)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, resp.Error()
}
if len(resp.Data.Results) == 0 {
return
}
for _, data := range resp.Data.Results {
domains = append(domains, strings.TrimSuffix(data.Domain, "."))
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *DNSLaProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
domainId, err := this.getDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return
}
for i := 1; i < 5000; i++ {
var resp = &dnsla.RecordListResponse{}
err = this.doAPI(http.MethodGet, "/api/recordList", map[string]string{
"domainId": domainId,
"pageSize": "100",
"pageIndex": types.String(i),
}, nil, resp)
if err != nil {
return
}
if !resp.Success() {
return nil, resp.Error()
}
if len(resp.Data.Results) == 0 {
break
}
for _, rawRecord := range resp.Data.Results {
var recordType = this.recordTypeName(rawRecord.Type)
// 修正Record
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(rawRecord.Data, ".") {
rawRecord.Data += "."
}
records = append(records, &dnstypes.Record{
Id: rawRecord.Id,
Name: rawRecord.Host,
Type: recordType,
Value: rawRecord.Data,
Route: rawRecord.LineCode,
TTL: types.Int32(rawRecord.TTL),
})
}
}
// 写入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.WriteDomainRecords(this.ProviderId, domain, records)
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *DNSLaProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
var resp = &dnsla.AllLineListResponse{}
err = this.doAPI(http.MethodGet, "/api/allLineList", nil, nil, resp)
if err != nil {
return
}
if !resp.Success() {
return nil, resp.Error()
}
for _, data := range resp.Data {
routes = append(routes, &dnstypes.Route{
Name: data.Name,
Code: data.Id + "$" + data.Code, // ID + $ + CODE
})
routes = append(routes, this.travelLines(data.Children)...)
}
this.routesLocker.Lock()
this.cachedRoutes[domain] = routes
this.routesLocker.Unlock()
return
}
// QueryRecord 查询单个记录
func (this *DNSLaProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
record, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecord(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return record, nil
}
}
domainId, err := this.getDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return nil, nil
}
var resp = &dnsla.RecordListResponse{}
err = this.doAPI(http.MethodGet, "/api/recordList", map[string]string{
"domainId": domainId,
"pageSize": "100",
"pageIndex": "1",
"host": name,
"type": types.String(this.recordTypeId(recordType)),
}, nil, resp)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, resp.Error()
}
if len(resp.Data.Results) == 0 {
return nil, nil
}
for _, rawRecord := range resp.Data.Results {
var recordTypeName = this.recordTypeName(rawRecord.Type)
if rawRecord.Host == name && recordTypeName == recordType {
// 修正Record
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(rawRecord.Data, ".") {
rawRecord.Data += "."
}
return &dnstypes.Record{
Id: rawRecord.Id,
Name: rawRecord.Host,
Type: recordTypeName,
Value: rawRecord.Data,
Route: rawRecord.LineCode,
TTL: types.Int32(rawRecord.TTL),
}, nil
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *DNSLaProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 从缓存中读取
if this.ProviderId > 0 {
records, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecords(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return records, nil
}
}
domainId, err := this.getDomainId(domain)
if err != nil {
return nil, err
}
if len(domainId) == 0 {
return nil, nil
}
var result []*dnstypes.Record
for pageIndex := 1; pageIndex < 5000; pageIndex++ {
var resp = &dnsla.RecordListResponse{}
err = this.doAPI(http.MethodGet, "/api/recordList", map[string]string{
"domainId": domainId,
"pageSize": "100",
"pageIndex": types.String(pageIndex),
"host": name,
"type": types.String(this.recordTypeId(recordType)),
}, nil, resp)
if err != nil {
return nil, err
}
if !resp.Success() {
return nil, resp.Error()
}
if len(resp.Data.Results) == 0 {
break
}
for _, rawRecord := range resp.Data.Results {
var recordTypeName = this.recordTypeName(rawRecord.Type)
if rawRecord.Host == name && recordTypeName == recordType {
// 修正Record
if recordType == dnstypes.RecordTypeCNAME && !strings.HasSuffix(rawRecord.Data, ".") {
rawRecord.Data += "."
}
result = append(result, &dnstypes.Record{
Id: rawRecord.Id,
Name: rawRecord.Host,
Type: recordTypeName,
Value: rawRecord.Data,
Route: rawRecord.LineCode,
TTL: types.Int32(rawRecord.TTL),
})
}
}
}
return result, nil
}
// AddRecord 设置记录
func (this *DNSLaProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
routeId, err := this.routeToId(domain, newRecord.Route)
if err != nil {
return err
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
domainId, err := this.getDomainId(domain)
if err != nil {
return err
}
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
recordJSON, err := json.Marshal(map[string]any{
"domainId": domainId,
"host": newRecord.Name,
"type": this.recordTypeId(newRecord.Type),
"data": newRecord.Value,
"ttl": ttl,
"lineId": routeId,
})
if err != nil {
return err
}
var resp = &dnsla.RecordCreateResponse{}
err = this.doAPI(http.MethodPost, "/api/record", nil, recordJSON, resp)
if err != nil {
return err
}
if !resp.Success() {
return resp.Error()
}
newRecord.Id = types.String(resp.Data.Id)
// 加入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.AddDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// UpdateRecord 修改记录
func (this *DNSLaProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
if len(record.Id) == 0 {
return errors.New("record id required")
}
routeId, err := this.routeToId(domain, newRecord.Route)
if err != nil {
return err
}
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
domainId, err := this.getDomainId(domain)
if err != nil {
return err
}
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
recordJSON, err := json.Marshal(map[string]any{
"id": record.Id,
"domainId": domainId,
"host": newRecord.Name,
"type": this.recordTypeId(newRecord.Type),
"data": newRecord.Value,
"ttl": ttl,
"lineId": routeId,
})
if err != nil {
return err
}
var resp = &dnsla.RecordUpdateResponse{}
err = this.doAPI(http.MethodPut, "/api/record", nil, recordJSON, resp)
if err != nil {
return err
}
if !resp.Success() {
return resp.Error()
}
newRecord.Id = record.Id
// 修改缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.UpdateDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// DeleteRecord 删除记录
func (this *DNSLaProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
var resp = &dnsla.RecordDeleteResponse{}
err := this.doAPI(http.MethodDelete, "/api/record", map[string]string{
"id": record.Id,
}, nil, resp)
if err != nil {
return err
}
if !resp.Success() {
// ignore not found error
if resp.Code == 404 {
return nil
}
return resp.Error()
}
// 删除缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.DeleteDomainRecord(this.ProviderId, domain, record.Id)
}
return nil
}
// DefaultRoute 默认线路
func (this *DNSLaProvider) DefaultRoute() string {
return "default"
}
// 发送请求
func (this *DNSLaProvider) doAPI(method string, path string, params map[string]string, postJSONData []byte, respPtr interface{}) error {
var apiURL = DNSLaAPIEndpoint + path
if len(params) > 0 {
var query = &url.Values{}
for k, v := range params {
query.Set(k, v)
}
apiURL += "?" + query.Encode()
}
var bodyReader io.Reader
if len(postJSONData) > 0 {
bodyReader = bytes.NewReader(postJSONData)
}
req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(this.apiId+":"+this.secret)))
if len(postJSONData) > 0 {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
}
resp, err := dnsLAHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
if respPtr != nil {
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w: %s", err, string(data))
}
}
return nil
}
func (this *DNSLaProvider) getDomainId(domain string) (string, error) {
var resp = &dnsla.DomainResponse{}
err := this.doAPI(http.MethodGet, "/api/domain", map[string]string{
"domain": domain,
}, nil, resp)
if err != nil {
return "", err
}
return resp.Data.Id, nil
}
func (this *DNSLaProvider) recordTypeName(recordTypeId int) string {
switch recordTypeId {
case 1:
return "A"
case 2:
return "NS"
case 5:
return "CNAME"
case 15:
return "MX"
case 16:
return "TXT"
case 28:
return "AAAA"
case 33:
return "SRV"
case 257:
return "CAA"
case 256:
return "URL转发"
}
return "UNKNOWN"
}
func (this *DNSLaProvider) recordTypeId(recordTypeName string) int {
switch recordTypeName {
case "A":
return 1
case "NS":
return 2
case "CNAME":
return 5
case "MX":
return 15
case "TXT":
return 16
case "AAAA":
return 28
case "SRV":
return 33
case "CAA":
return 257
case "URL转发":
return 256
}
return 0
}
func (this *DNSLaProvider) travelLines(children []dnsla.AllLineListResponseChild) (result []*dnstypes.Route) {
if len(children) == 0 {
return
}
for _, child := range children {
result = append(result, &dnstypes.Route{
Name: child.Name,
Code: child.Id + "$" + child.Code,
})
result = append(result, this.travelLines(child.Children)...)
}
return
}
func (this *DNSLaProvider) routeToId(domain string, routeCode string) (string, error) {
if len(routeCode) == 0 {
return "", nil
}
if routeCode == "default" {
return "", nil
}
// 新的线路id@code
if strings.Contains(routeCode, "$") {
return strings.Split(routeCode, "$")[0], nil
}
// 兼容老的线路
this.routesLocker.Lock()
var hasCachedRoutes = len(this.cachedRoutes[domain]) > 0
this.routesLocker.Unlock()
if !hasCachedRoutes {
_, err := this.GetRoutes(domain)
if err != nil {
return "", err
}
}
this.routesLocker.Lock()
defer this.routesLocker.Unlock()
if len(this.cachedRoutes) == 0 {
return "", nil
}
for _, cachedRoute := range this.cachedRoutes[domain] {
if strings.HasSuffix(cachedRoute.Code, "$"+routeCode) {
return strings.Split(cachedRoute.Code, "$")[0], nil
}
}
return "", errors.New("invalid route code '" + routeCode + "'")
}

View File

@@ -0,0 +1,216 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package dnsclients_test
import (
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestDNSLaProvider_GetDomains(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestDNSLAProvider_GetRecords(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords("hello2.com")
if err != nil {
t.Fatal(err)
}
for _, record := range records {
t.Log(record.Id, record.Name, record.Type, record.Value, record.Route, record.TTL)
}
//logs.PrintAsJSON(records, t)
}
func TestDNSLaProvider_GetRoutes(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes("hello2.com")
if err != nil {
t.Fatal(err)
}
t.Log(len(routes), "routes")
logs.PrintAsJSON(routes, t)
}
func TestDNSLaProvider_QueryRecord(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
for _, recordName := range []string{"www", "test", "@", ""} {
t.Log("===", recordName, "===")
record, err := provider.QueryRecord("hello2.com", recordName, dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record, t)
}
}
func TestDNSLaProvider_QueryRecords(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
for _, recordName := range []string{"www", "test", "@", ""} {
t.Log("===", recordName, "===")
records, err := provider.QueryRecords("hello2.com", recordName, dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestDNSLaProvider_AddRecord(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
// 116234436886664192
{
var record = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.100",
Route: "mobi",
TTL: 600,
}
err := provider.AddRecord("hello2.com", record)
if err != nil {
t.Fatal(err)
}
t.Log("id:", record.Id)
}
/**{
var record = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.101",
Route: "unic",
TTL: 600,
}
err := provider.AddRecord("hello2.com", record)
if err != nil {
t.Fatal(err)
}
t.Log("id:", record.Id)
}**/
/**{
var record = &dnstypes.Record{
Id: "",
Name: "test2",
Type: dnstypes.RecordTypeCNAME,
Value: "goedge.cn.",
Route: "",
TTL: 0,
}
err := provider.AddRecord("goedge.cn", record)
if err != nil {
t.Fatal(err)
}
}**/
}
func TestDNSLaProvider_UpdateRecord(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
var record = &dnstypes.Record{
Id: "116234436886664192",
}
var newRecord = &dnstypes.Record{
Id: "",
Name: "test1",
Type: dnstypes.RecordTypeA,
Value: "192.168.1.102",
Route: "mobi",
TTL: 3600,
}
err = provider.UpdateRecord("hello2.com", record, newRecord)
if err != nil {
t.Fatal(err)
}
}
func TestDNSLaProvider_DeleteRecord(t *testing.T) {
provider, err := testDNSLaProvider()
if err != nil {
t.Fatal(err)
}
err = provider.DeleteRecord("hello2.com", &dnstypes.Record{
Id: "116223920176894976",
Name: "",
Type: "",
Value: "",
Route: "",
TTL: 0,
})
if err != nil {
t.Fatal(err)
}
}
func testDNSLaProvider() (dnsclients.ProviderInterface, error) {
dbs.NotifyReady()
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='dnsla' ORDER BY id DESC")
if err != nil {
return nil, err
}
if one == nil {
return nil, errors.New("can not find providers with type 'dnsla'")
}
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider := &dnsclients.DNSLaProvider{
ProviderId: one.GetInt64("id"),
}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,481 @@
package dnsclients
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnspod"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const (
DNSPodMaxTTL int32 = 604800
DNSPodInternational = "international"
)
var dnsPodHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// DNSPodProvider DNSPod服务商
type DNSPodProvider struct {
BaseProvider
ProviderId int64
region string
apiId string
apiToken string
tencentDNSProvider *TencentDNSProvider
}
// Auth 认证
func (this *DNSPodProvider) Auth(params maps.Map) error {
// 兼容腾讯云API
var apiType = params.GetString("apiType")
switch apiType {
case "tencentDNS":
this.tencentDNSProvider = NewTencentDNSProvider()
return this.tencentDNSProvider.Auth(params)
default:
this.apiId = params.GetString("id")
this.apiToken = params.GetString("token")
this.region = params.GetString("region")
if len(this.apiId) == 0 {
return errors.New("'id' should be not empty")
}
if len(this.apiToken) == 0 {
return errors.New("'token' should not be empty")
}
}
return nil
}
// MaskParams 对参数进行掩码
func (this *DNSPodProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
if params.GetString("apiType") == "tencentDNS" {
params["accessKeySecret"] = MaskString(params.GetString("accessKeySecret"))
} else {
params["token"] = MaskString(params.GetString("token"))
}
}
// GetDomains 获取所有域名列表
func (this *DNSPodProvider) GetDomains() (domains []string, err error) {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.GetDomains()
}
var offset = 0
var size = 3000
for {
var resp = new(dnspod.DomainListResponse)
err := this.doAPI("/Domain.List", map[string]string{
"offset": numberutils.FormatInt(offset),
"length": numberutils.FormatInt(size),
}, resp)
if err != nil {
return nil, err
}
offset += size
for _, domain := range resp.Domains {
domains = append(domains, domain.Name)
}
// 检查是否到头
var recordTotal = resp.Info.AllTotal
if offset >= recordTotal {
break
}
}
return
}
// GetRecords 获取域名列表
func (this *DNSPodProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.GetRecords(domain)
}
var offset = 0
var size = 3000
for {
var resp = new(dnspod.RecordListResponse)
err := this.doAPI("/Record.List", map[string]string{
"domain": domain,
"offset": numberutils.FormatInt(offset),
"length": numberutils.FormatInt(size),
}, resp)
if err != nil {
return nil, err
}
offset += size
// 记录
for _, record := range resp.Records {
records = append(records, &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: record.Type,
Value: record.Value,
Route: record.Line,
TTL: types.Int32(record.TTL),
})
}
// 检查是否到头
var recordTotal = types.Int(resp.Info.RecordTotal)
if offset >= recordTotal {
break
}
}
// 写入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.WriteDomainRecords(this.ProviderId, domain, records)
}
return
}
// GetRoutes 读取线路数据
func (this *DNSPodProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.GetRoutes(domain)
}
var domainInfoResp = new(dnspod.DomainInfoResponse)
err = this.doAPI("/Domain.Info", map[string]string{
"domain": domain,
}, domainInfoResp)
if err != nil {
return nil, err
}
var grade = domainInfoResp.Domain.Grade
var linesResp = new(dnspod.RecordLineResponse)
err = this.doAPI("/Record.Line", map[string]string{
"domain": domain,
"domain_grade": grade,
}, linesResp)
if err != nil {
return nil, err
}
var lines = linesResp.Lines
if len(lines) == 0 {
return nil, nil
}
for _, line := range lines {
lineString := types.String(line)
routes = append(routes, &dnstypes.Route{
Name: lineString,
Code: lineString,
})
}
// 自定义线路分组
var groupsPerPage = 100
var customLineGroupListResponse = new(dnspod.CustomLineGroupListResponse)
err = this.doAPI("/Custom.Line.Group.List", map[string]string{
"domain": domain,
"offset": "0",
"length": types.String(groupsPerPage),
}, customLineGroupListResponse)
if err != nil {
// 忽略自定义分组错误
err = nil
} else {
if len(customLineGroupListResponse.Data.LineGroups) > 0 {
for _, lineGroup := range customLineGroupListResponse.Data.LineGroups {
routes = append(routes, &dnstypes.Route{
Name: "分组:" + lineGroup.Name,
Code: lineGroup.Name,
})
}
}
if customLineGroupListResponse.Data.Info.Total > customLineGroupListResponse.Data.Info.NowTotal {
for page := 1; page <= 100; /** 最多100页 **/ page++ {
err = this.doAPI("/Custom.Line.Group.List", map[string]string{
"domain": domain,
"offset": types.String(page * groupsPerPage),
"length": types.String(groupsPerPage),
}, customLineGroupListResponse)
if err != nil {
// 忽略错误
err = nil
} else {
if len(customLineGroupListResponse.Data.LineGroups) == 0 {
break
}
for _, lineGroup := range customLineGroupListResponse.Data.LineGroups {
routes = append(routes, &dnstypes.Route{
Name: "分组:" + lineGroup.Name,
Code: lineGroup.Name,
})
}
}
}
}
}
return routes, nil
}
// QueryRecord 查询单个记录
func (this *DNSPodProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.QueryRecord(domain, name, recordType)
}
// 从缓存中读取
if this.ProviderId > 0 {
record, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecord(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return record, nil
}
}
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
for _, record := range records {
if record.Name == name && record.Type == recordType {
return record, nil
}
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *DNSPodProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.QueryRecords(domain, name, recordType)
}
// 从缓存中读取
if this.ProviderId > 0 {
records, hasRecords, _ := sharedDomainRecordsCache.QueryDomainRecords(this.ProviderId, domain, name, recordType)
if hasRecords { // 有效的搜索
return records, nil
}
}
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
var result = []*dnstypes.Record{}
for _, record := range records {
if record.Name == name && record.Type == recordType {
result = append(result, record)
}
}
return result, nil
}
// AddRecord 设置记录
func (this *DNSPodProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.AddRecord(domain, newRecord)
}
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var args = map[string]string{
"domain": domain,
"sub_domain": newRecord.Name,
"record_type": newRecord.Type,
"value": newRecord.Value,
"record_line": newRecord.Route,
}
if newRecord.TTL > 0 && newRecord.TTL <= DNSPodMaxTTL {
args["ttl"] = types.String(newRecord.TTL)
}
var resp = new(dnspod.RecordCreateResponse)
err := this.doAPI("/Record.Create", args, resp)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
newRecord.Id = types.String(resp.Record.Id)
// 加入缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.AddDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// UpdateRecord 修改记录
func (this *DNSPodProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.UpdateRecord(domain, record, newRecord)
}
if record == nil {
return errors.New("invalid record")
}
if newRecord == nil {
return errors.New("invalid new record")
}
// 在CHANGE记录后面加入点
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var args = map[string]string{
"domain": domain,
"record_id": record.Id,
"sub_domain": newRecord.Name,
"record_type": newRecord.Type,
"value": newRecord.Value,
"record_line": newRecord.Route,
}
if newRecord.TTL > 0 && newRecord.TTL <= DNSPodMaxTTL {
args["ttl"] = types.String(newRecord.TTL)
}
var resp = new(dnspod.RecordModifyResponse)
err := this.doAPI("/Record.Modify", args, resp)
if err != nil {
return this.WrapError(err, domain, newRecord)
}
newRecord.Id = record.Id
// 修改缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.UpdateDomainRecord(this.ProviderId, domain, newRecord)
}
return nil
}
// DeleteRecord 删除记录
func (this *DNSPodProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.DeleteRecord(domain, record)
}
if record == nil {
return errors.New("invalid record to delete")
}
var resp = new(dnspod.RecordRemoveResponse)
err := this.doAPI("/Record.Remove", map[string]string{
"domain": domain,
"record_id": record.Id,
}, resp)
if err != nil {
return this.WrapError(err, domain, record)
}
// 删除缓存
if this.ProviderId > 0 {
sharedDomainRecordsCache.DeleteDomainRecord(this.ProviderId, domain, record.Id)
}
return nil
}
// 发送请求
func (this *DNSPodProvider) doAPI(path string, params map[string]string, respPtr dnspod.ResponseInterface) error {
var apiHost = "https://dnsapi.cn"
var lang = "cn"
if this.isInternational() { // 国际版
apiHost = "https://api.dnspod.com"
lang = "en"
}
var query = url.Values{
"login_token": []string{this.apiId + "," + this.apiToken},
"format": []string{"json"},
"lang": []string{lang},
}
for p, v := range params {
query[p] = []string{v}
}
req, err := http.NewRequest(http.MethodPost, apiHost+path, strings.NewReader(query.Encode()))
if err != nil {
return fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "GoEdge-Client/1.0.0 (iwind.liu@gmail.com)")
req.Header.Set("Accept", "*/*")
resp, err := dnsPodHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &respPtr)
if err != nil {
return err
}
if !respPtr.IsOk() {
code, message := respPtr.LastError()
return errors.New("API response error: code: " + code + ", message: " + message)
}
return nil
}
// DefaultRoute 默认线路
func (this *DNSPodProvider) DefaultRoute() string {
if this.tencentDNSProvider != nil {
return this.tencentDNSProvider.DefaultRoute()
}
if this.isInternational() {
return "Default"
}
return "默认"
}
func (this *DNSPodProvider) isInternational() bool {
return this.region == DNSPodInternational
}

View File

@@ -0,0 +1,186 @@
package dnsclients_test
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
const DNSPodTestDomain = "goedge.cloud"
func TestDNSPodProvider_GetDomains(t *testing.T) {
provider, _, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log(domains)
}
func TestDNSPodProvider_GetRoutes(t *testing.T) {
provider, _, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes(DNSPodTestDomain)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestDNSPodProvider_GetRecords(t *testing.T) {
provider, _, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords(DNSPodTestDomain)
if err != nil {
t.Fatal(err)
}
for _, record := range records {
t.Log(record.Id, record.Type, record.Name, record.Value, record.Route)
}
}
func TestDNSPodProvider_AddRecord(t *testing.T) {
provider, isInternational, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
var route = "联通"
if isInternational {
route = "Default"
}
var record = &dnstypes.Record{
Type: dnstypes.RecordTypeCNAME,
Name: "hello-forward",
Value: "hello." + DNSPodTestDomain,
Route: route,
TTL: 600,
}
err = provider.AddRecord(DNSPodTestDomain, record)
if err != nil {
t.Fatal(err)
}
t.Log("ok, record id:", record.Id)
}
func TestDNSPodProvider_QueryRecord(t *testing.T) {
provider, _, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
{
record, err := provider.QueryRecord(DNSPodTestDomain, "hello-forward", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
{
record, err := provider.QueryRecord(DNSPodTestDomain, "hello-forward2", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
t.Log(record)
}
}
func TestDNSPodProvider_QueryRecords(t *testing.T) {
provider, _, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
{
records, err := provider.QueryRecords(DNSPodTestDomain, "hello-forward", dnstypes.RecordTypeCNAME)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
}
func TestDNSPodProvider_UpdateRecord(t *testing.T) {
provider, isInternational, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
var route = "联通"
var id = "1224507933"
if isInternational {
route = "Default"
id = "28507333"
}
err = provider.UpdateRecord(DNSPodTestDomain, &dnstypes.Record{
Id: id,
}, &dnstypes.Record{
Type: dnstypes.RecordTypeA,
Name: "hello",
Value: "192.168.1.102",
Route: route,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestDNSPodProvider_DeleteRecord(t *testing.T) {
provider, isInternational, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
var id = "1224507933"
if isInternational {
id = "28507333"
}
err = provider.DeleteRecord(DNSPodTestDomain, &dnstypes.Record{
Id: id,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func testDNSPodProvider() (provider dnsclients.ProviderInterface, isInternational bool, err error) {
dbs.NotifyReady()
db, err := dbs.Default()
if err != nil {
return nil, false, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='dnspod' AND id='14' ORDER BY id DESC")
if err != nil {
return nil, false, err
}
var apiParams = maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, false, err
}
provider = &dnsclients.DNSPodProvider{
ProviderId: one.GetInt64("id"),
}
err = provider.Auth(apiParams)
if err != nil {
return nil, false, err
}
return provider, apiParams.GetString("region") == "international", nil
}

View File

@@ -0,0 +1,524 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnsclients
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/edgeapi"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"regexp"
"strings"
"time"
)
var edgeDNSHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
type EdgeDNSAPIProvider struct {
BaseProvider
ProviderId int64
host string
accessKeyId string
accessKeySecret string
role string // admin | user
accessToken string
accessTokenExpiresAt int64
}
// Auth 认证
func (this *EdgeDNSAPIProvider) Auth(params maps.Map) error {
this.role = params.GetString("role")
this.host = params.GetString("host")
this.accessKeyId = params.GetString("accessKeyId")
this.accessKeySecret = params.GetString("accessKeySecret")
if len(this.role) == 0 {
this.role = "user"
}
if len(this.host) == 0 {
return errors.New("'host' should not be empty")
}
if !regexp.MustCompile(`^(?i)(http|https):`).MatchString(this.host) {
this.host = "http://" + this.host
}
if len(this.accessKeyId) == 0 {
return errors.New("'accessKeyId' should not be empty")
}
if len(this.accessKeySecret) == 0 {
return errors.New("'accessKeySecret' should not be empty")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *EdgeDNSAPIProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["accessKeySecret"] = MaskString(params.GetString("accessKeySecret"))
}
// GetDomains 获取所有域名列表
func (this *EdgeDNSAPIProvider) GetDomains() (domains []string, err error) {
var offset = 0
var size = 100
for {
var resp = &edgeapi.ListNSDomainsResponse{}
err = this.doAPI("/NSDomainService/ListNSDomains", map[string]any{
"offset": offset,
"size": size,
}, resp)
if err != nil {
return
}
for _, domain := range resp.Data.NSDomains {
domains = append(domains, domain.Name)
}
if len(resp.Data.NSDomains) < size {
break
}
offset += size
}
return
}
// GetRecords 获取域名解析记录列表
func (this *EdgeDNSAPIProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var domainResp = &edgeapi.FindDomainWithNameResponse{}
err = this.doAPI("/NSDomainService/FindNSDomainWithName", map[string]any{
"name": domain,
}, domainResp)
if err != nil {
return nil, err
}
var domainId = domainResp.Data.NSDomain.Id
if domainId == 0 {
return nil, nil
}
var offset = 0
var size = 100
for {
var recordsResp = &edgeapi.ListNSRecordsResponse{}
err = this.doAPI("/NSRecordService/ListNSRecords", map[string]any{
"nsDomainId": domainId,
"offset": offset,
"size": size,
}, recordsResp)
if err != nil {
return nil, err
}
var nsRecords = recordsResp.Data.NSRecords
for _, record := range nsRecords {
var routeCode = this.DefaultRoute()
if len(record.NSRoutes) > 0 {
routeCode = record.NSRoutes[0].Code
}
records = append(records, &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: record.Type,
Value: record.Value,
Route: routeCode,
TTL: record.TTL,
})
}
if len(nsRecords) < size {
break
}
offset += size
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *EdgeDNSAPIProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
// default
routes = append(routes, &dnstypes.Route{
Name: "默认线路",
Code: this.DefaultRoute(),
})
// 世界区域
{
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
err = this.doAPI("/NSRouteService/FindAllDefaultWorldRegionRoutes", map[string]any{}, routesResp)
if err != nil {
return nil, err
}
for _, route := range routesResp.Data.NSRoutes {
routes = append(routes, &dnstypes.Route{
Name: route.Name,
Code: route.Code,
})
}
}
// 中国省份
{
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
err = this.doAPI("/NSRouteService/FindAllDefaultChinaProvinceRoutes", map[string]any{}, routesResp)
if err != nil {
return nil, err
}
for _, route := range routesResp.Data.NSRoutes {
routes = append(routes, &dnstypes.Route{
Name: route.Name,
Code: route.Code,
})
}
}
// ISP
{
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
err = this.doAPI("/NSRouteService/FindAllDefaultISPRoutes", map[string]any{}, routesResp)
if err != nil {
return nil, err
}
for _, route := range routesResp.Data.NSRoutes {
routes = append(routes, &dnstypes.Route{
Name: route.Name,
Code: route.Code,
})
}
}
// Agent
{
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
err = this.doAPI("/NSRouteService/FindAllAgentNSRoutes", map[string]any{}, routesResp)
if err != nil {
// 忽略错误因为老版本的EdgeDNS没有提供这个接口
err = nil
} else {
for _, route := range routesResp.Data.NSRoutes {
routes = append(routes, &dnstypes.Route{
Name: route.Name,
Code: route.Code,
})
}
}
}
// 自定义
{
var routesResp = &edgeapi.FindAllNSRoutesResponse{}
err = this.doAPI("/NSRouteService/FindAllNSRoutes", map[string]any{}, routesResp)
if err != nil {
return nil, err
}
for _, route := range routesResp.Data.NSRoutes {
routes = append(routes, &dnstypes.Route{
Name: route.Name,
Code: route.Code,
})
}
}
return
}
// QueryRecord 查询单个记录
func (this *EdgeDNSAPIProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
var domainResp = &edgeapi.FindDomainWithNameResponse{}
err := this.doAPI("/NSDomainService/FindNSDomainWithName", map[string]any{
"name": domain,
}, domainResp)
if err != nil {
return nil, err
}
var domainId = domainResp.Data.NSDomain.Id
if domainId == 0 {
return nil, errors.New("can not find domain '" + domain + "'")
}
var recordResp = &edgeapi.FindNSRecordWithNameAndTypeResponse{}
err = this.doAPI("/NSRecordService/FindNSRecordWithNameAndType", map[string]any{
"nsDomainId": domainId,
"name": name,
"type": recordType,
}, recordResp)
if err != nil {
return nil, err
}
var record = recordResp.Data.NSRecord
if record.Id <= 0 {
return nil, nil
}
var routeCode = this.DefaultRoute()
if len(record.NSRoutes) > 0 {
routeCode = record.NSRoutes[0].Code
}
return &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: record.Type,
Value: record.Value,
Route: routeCode,
TTL: record.TTL,
}, nil
}
// QueryRecords 查询多个记录
func (this *EdgeDNSAPIProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
var domainResp = &edgeapi.FindDomainWithNameResponse{}
err := this.doAPI("/NSDomainService/FindNSDomainWithName", map[string]any{
"name": domain,
}, domainResp)
if err != nil {
return nil, err
}
var domainId = domainResp.Data.NSDomain.Id
if domainId == 0 {
return nil, errors.New("can not find domain '" + domain + "'")
}
var recordResp = &edgeapi.FindNSRecordsWithNameAndTypeResponse{}
err = this.doAPI("/NSRecordService/FindNSRecordsWithNameAndType", map[string]any{
"nsDomainId": domainId,
"name": name,
"type": recordType,
}, recordResp)
if err != nil {
return nil, err
}
var result = []*dnstypes.Record{}
for _, record := range recordResp.Data.NSRecords {
if record.Id <= 0 {
return nil, nil
}
var routeCode = this.DefaultRoute()
if len(record.NSRoutes) > 0 {
routeCode = record.NSRoutes[0].Code
}
result = append(result, &dnstypes.Record{
Id: types.String(record.Id),
Name: record.Name,
Type: record.Type,
Value: record.Value,
Route: routeCode,
TTL: record.TTL,
})
}
return result, nil
}
// AddRecord 设置记录
func (this *EdgeDNSAPIProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
var domainResp = &edgeapi.FindDomainWithNameResponse{}
err := this.doAPI("/NSDomainService/FindNSDomainWithName", map[string]any{
"name": domain,
}, domainResp)
if err != nil {
return err
}
var domainId = domainResp.Data.NSDomain.Id
if domainId == 0 {
return errors.New("can not find domain '" + domain + "'")
}
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var createResp = &edgeapi.CreateNSRecordResponse{}
var routes = []string{}
if len(newRecord.Route) > 0 {
routes = []string{newRecord.Route}
}
err = this.doAPI("/NSRecordService/CreateNSRecord", map[string]any{
"nsDomainId": domainId,
"name": newRecord.Name,
"type": strings.ToUpper(newRecord.Type),
"value": newRecord.Value,
"ttl": newRecord.TTL,
"nsRouteCodes": routes,
}, createResp)
if err != nil {
return err
}
newRecord.Id = types.String(createResp.Data.NSRecordId)
return nil
}
// UpdateRecord 修改记录
func (this *EdgeDNSAPIProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
var createResp = &edgeapi.UpdateNSRecordResponse{}
var routes = []string{}
if len(newRecord.Route) > 0 {
routes = []string{newRecord.Route}
}
err := this.doAPI("/NSRecordService/UpdateNSRecord", map[string]any{
"nsRecordId": types.Int64(record.Id),
"name": newRecord.Name,
"type": strings.ToUpper(newRecord.Type),
"value": newRecord.Value,
"ttl": newRecord.TTL,
"nsRouteCodes": routes,
"isOn": true, // important
}, createResp)
return err
}
// DeleteRecord 删除记录
func (this *EdgeDNSAPIProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
var resp = &edgeapi.SuccessResponse{}
err := this.doAPI("/NSRecordService/DeleteNSRecord", map[string]any{
"nsRecordId": types.Int64(record.Id),
}, resp)
return err
}
// DefaultRoute 默认线路
func (this *EdgeDNSAPIProvider) DefaultRoute() string {
return "default"
}
func (this *EdgeDNSAPIProvider) doAPI(path string, params map[string]any, respPtr edgeapi.ResponseInterface) error {
accessToken, err := this.getToken()
if err != nil {
return err
}
paramsJSON, err := json.Marshal(params)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, this.host+path, bytes.NewReader(paramsJSON))
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("X-Edge-Access-Token", accessToken)
resp, err := edgeDNSHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
if resp.Body != nil {
_ = resp.Body.Close()
}
}()
if resp.StatusCode != http.StatusOK {
return errors.New("invalid response status code '" + types.String(resp.StatusCode) + "'")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode response failed: %w, JSON: %s", err, string(data))
}
if !respPtr.IsValid() {
return respPtr.Error()
}
return err
}
func (this *EdgeDNSAPIProvider) getToken() (string, error) {
if len(this.accessToken) > 0 && this.accessTokenExpiresAt > time.Now().Unix()+600 /** 600秒是防止当前服务器和API服务器之间有时间差 **/ {
return this.accessToken, nil
}
var params = maps.Map{
"type": this.role,
"accessKeyId": this.accessKeyId,
"accessKey": this.accessKeySecret,
}
paramsJSON, err := json.Marshal(params)
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, this.host+"/APIAccessTokenService/getAPIAccessToken", bytes.NewReader(paramsJSON))
if err != nil {
return "", err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
resp, err := edgeDNSHTTPClient.Do(req)
if err != nil {
return "", err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return "", errors.New("invalid response code '" + types.String(resp.StatusCode) + "'")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tokenResp = &edgeapi.GetAPIAccessToken{}
err = json.Unmarshal(data, tokenResp)
if err != nil {
return "", err
}
if tokenResp.Code != 200 {
return "", errors.New("invalid code '" + types.String(tokenResp.Code) + "', message: " + tokenResp.Message)
}
this.accessToken = tokenResp.Data.Token
this.accessTokenExpiresAt = tokenResp.Data.ExpiresAt
return this.accessToken, nil
}

View File

@@ -0,0 +1,175 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dnsclients_test
import (
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"testing"
)
const edgeDNSAPIDomainName = "hello2.com"
func TestEdgeDNSAPIProvider_GetDomains(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log("domains:", domains)
}
func TestEdgeDNSAPIProvider_GetRecords(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords(edgeDNSAPIDomainName)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(records, t)
}
func TestEdgeDNSAPIProvider_GetRoutes(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes(edgeDNSAPIDomainName)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(routes, t)
}
func TestEdgeDNSAPIProvider_QueryRecord(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
record, err := provider.QueryRecord(edgeDNSAPIDomainName, "cdn", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record)
}
func TestEdgeDNSAPIProvider_QueryRecords(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
record, err := provider.QueryRecords(edgeDNSAPIDomainName, "cdn", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
logs.PrintAsJSON(record)
}
func TestEdgeDNSAPIProvider_AddRecord(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
err = provider.AddRecord(edgeDNSAPIDomainName, &dnstypes.Record{
Id: "",
Name: "example",
Type: dnstypes.RecordTypeA,
Value: "10.0.0.1",
Route: "china:province:beijing",
TTL: 300,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestEdgeDNSAPIProvider_UpdateRecord(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
record, err := provider.QueryRecord(edgeDNSAPIDomainName, "cdn", dnstypes.RecordTypeA)
if err != nil {
t.Fatal(err)
}
if record == nil {
t.Log("not found record")
return
}
//record.Id = ""
err = provider.UpdateRecord(edgeDNSAPIDomainName, record, &dnstypes.Record{
Id: "",
Name: record.Name,
Type: record.Type,
Value: "127.0.0.3",
Route: record.Route,
TTL: 30,
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestEdgeDNSAPIProvider_DeleteRecord(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
record, err := provider.QueryRecord(edgeDNSAPIDomainName, "example", "A")
if err != nil {
t.Fatal(err)
}
if record == nil {
t.Log("not found")
return
}
err = provider.DeleteRecord(edgeDNSAPIDomainName, &dnstypes.Record{
Id: record.Id,
Name: "example",
Type: "A",
Value: "",
Route: "",
})
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}
func TestEdgeDNSAPIProvider_DefaultRoute(t *testing.T) {
provider, err := testEdgeDNSAPIProvider()
if err != nil {
t.Fatal(err)
}
t.Log(provider.DefaultRoute())
}
func testEdgeDNSAPIProvider() (dnsclients.ProviderInterface, error) {
provider := &dnsclients.EdgeDNSAPIProvider{}
err := provider.Auth(maps.Map{
"role": "user",
"host": "http://127.0.0.1:8004",
"accessKeyId": "zr9cmR42AEZxRyIV",
"accessKeySecret": "2w5p5NSZZuplUPsfPMzM7dFmTrI7xyja",
})
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,537 @@
package dnsclients
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/gname"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
const (
GnameDefaultRoute = "0" // 默认线路代码,根据文档为 "0"
GnameAPIEndpoint = "https://api.gname.com" // Gname API 基础地址
)
// gnameNormalizeRouteCode 将 Gname 列表接口返回的线路(可能是名称或代码)统一为 AddRecord 使用的代码,
// 避免 oldRecordsMap 的 keyroute@value与任务生成的 key 不一致导致重复添加、触发「相同主机记录及相同记录值」错误。
func gnameNormalizeRouteCode(xl string) string {
if xl == "" {
return GnameDefaultRoute
}
// Gname 列表可能返回线路名称或代码,与 GetRoutes 中的 Name/Code 对应
switch xl {
case "中国大陆":
return "asia-cn"
case "非中国大陆":
return "non-cn"
case "默认":
return "0"
case "搜索引擎":
return "seo"
case "搜索引擎-Google":
return "seo-google"
case "搜索引擎-Baidu":
return "seo-baidu"
case "搜索引擎-Bing":
return "seo-bing"
case "亚洲":
return "asia"
case "新加坡":
return "asia-sg"
case "欧洲":
return "eu"
case "美洲":
return "na-usa"
}
// 已是代码或未知值,原样返回
return xl
}
var gnameHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// GnameProvider Gname DNS 提供商
// 参考文档https://www.gname.vip/domain/api/jiexi/add
type GnameProvider struct {
BaseProvider
ProviderId int64
// Gname API 认证信息
appid string // APPID (Application Client Id)
secret string // API Secret (用于签名)
}
// Auth 认证
func (this *GnameProvider) Auth(params maps.Map) error {
this.appid = params.GetString("appid")
if len(this.appid) == 0 {
// 兼容旧参数名
this.appid = params.GetString("apiKey")
}
if len(this.appid) == 0 {
return errors.New("'appid' or 'apiKey' should not be empty")
}
this.secret = params.GetString("secret")
if len(this.secret) == 0 {
// 兼容旧参数名
this.secret = params.GetString("apiSecret")
}
if len(this.secret) == 0 {
return errors.New("'secret' or 'apiSecret' should not be empty")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *GnameProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
if params.Has("secret") {
params["secret"] = MaskString(params.GetString("secret"))
}
if params.Has("apiSecret") {
params["apiSecret"] = MaskString(params.GetString("apiSecret"))
}
}
// GetDomains 获取所有域名列表
func (this *GnameProvider) GetDomains() (domains []string, err error) {
var resp = &gname.BaseResponse{}
err = this.doAPI(http.MethodPost, "/api/domain/list", map[string]string{}, resp)
if err != nil {
return
}
if resp.Code != 1 {
return nil, errors.New("Failed: " + resp.Msg)
}
// 解析响应数据 - 支持多种可能的数据格式
if resp.Data != nil {
// 方式1: 直接是字符串数组
if dataArray, ok := resp.Data.([]interface{}); ok {
for _, item := range dataArray {
switch v := item.(type) {
case string:
// 直接是域名字符串
if len(v) > 0 {
domains = append(domains, v)
}
case map[string]interface{}:
// 是对象,尝试多个可能的字段名
if domainName, ok := v["domain"].(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
} else if domainName, ok := v["name"].(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
} else if domainName, ok := v["ym"].(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
}
}
}
} else if dataMap, ok := resp.Data.(map[string]interface{}); ok {
// 方式2: 数据在某个字段中(如 data.list, data.domains 等)
if list, ok := dataMap["list"].([]interface{}); ok {
for _, item := range list {
if domainName, ok := item.(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
} else if itemMap, ok := item.(map[string]interface{}); ok {
if domainName, ok := itemMap["domain"].(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
} else if domainName, ok := itemMap["name"].(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
}
}
}
}
if domainsList, ok := dataMap["domains"].([]interface{}); ok {
for _, item := range domainsList {
if domainName, ok := item.(string); ok && len(domainName) > 0 {
domains = append(domains, domainName)
}
}
}
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *GnameProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var resp = &gname.BaseResponse{}
err = this.doAPI(http.MethodPost, "/api/resolution/list", map[string]string{
"ym": domain, // 域名
}, resp)
if err != nil {
return
}
if resp.Code != 1 {
return nil, errors.New("Failed: " + resp.Msg)
}
// 解析响应数据
if resp.Data != nil {
if dataList, ok := resp.Data.([]interface{}); ok {
for _, item := range dataList {
if itemMap, ok := item.(map[string]interface{}); ok {
record := &dnstypes.Record{}
if id, ok := itemMap["id"].(float64); ok {
record.Id = types.String(int64(id))
}
if zj, ok := itemMap["zj"].(string); ok {
record.Name = zj // 主机记录
}
if lx, ok := itemMap["lx"].(string); ok {
record.Type = lx // 记录类型
}
if jlz, ok := itemMap["jlz"].(string); ok {
record.Value = jlz // 记录值
}
if ttl, ok := itemMap["ttl"].(float64); ok {
record.TTL = int32(ttl)
}
if xl, ok := itemMap["xl"].(string); ok {
record.Route = gnameNormalizeRouteCode(xl) // 线路:统一为代码,与 AddRecord 一致
} else {
record.Route = GnameDefaultRoute
}
// 修正 CNAME 记录值
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
record.Value += "."
}
records = append(records, record)
}
}
}
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *GnameProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
// 根据 Gname API 文档,线路列表
routes = []*dnstypes.Route{
{Name: "默认", Code: "0"},
{Name: "搜索引擎", Code: "seo"},
{Name: "搜索引擎-Google", Code: "seo-google"},
{Name: "搜索引擎-Baidu", Code: "seo-baidu"},
{Name: "搜索引擎-Bing", Code: "seo-bing"},
{Name: "亚洲", Code: "asia"},
{Name: "中国大陆", Code: "asia-cn"},
{Name: "非中国大陆", Code: "non-cn"},
{Name: "新加坡", Code: "asia-sg"},
{Name: "欧洲", Code: "eu"},
{Name: "美洲", Code: "na-usa"},
}
return
}
// QueryRecord 查询单个记录
func (this *GnameProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
records, err := this.QueryRecords(domain, name, recordType)
if err != nil {
return nil, err
}
if len(records) == 0 {
return nil, nil
}
return records[0], nil
}
// QueryRecords 查询多个记录
func (this *GnameProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
// 先获取所有记录,然后过滤
allRecords, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
var result = []*dnstypes.Record{}
for _, record := range allRecords {
if record.Name == name && record.Type == recordType {
result = append(result, record)
}
}
return result, nil
}
// AddRecord 设置记录
// 参考文档https://www.gname.vip/domain/api/jiexi/add
func (this *GnameProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
// 修正 CNAME 记录
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
// 设置默认 TTL
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
// 使用 Gname API 文档中的参数名
params := map[string]string{
"ym": domain, // 域名
"lx": newRecord.Type, // 记录类型A,CNAME,MX,URL,TXT
"zj": newRecord.Name, // 主机记录
"jlz": newRecord.Value, // 记录值
"ttl": types.String(ttl), // TTL值
"xl": GnameDefaultRoute, // 线路,默认使用 "0"
}
// 如果有线路支持
if len(newRecord.Route) > 0 && newRecord.Route != this.DefaultRoute() {
params["xl"] = newRecord.Route
}
// MX 记录需要 MX 值
if newRecord.Type == "MX" {
params["mx"] = "10" // 默认 MX 优先级
}
var resp = &gname.BaseResponse{}
err := this.doAPI(http.MethodPost, "/api/resolution/add", params, resp)
if err != nil {
return err
}
// 检查响应状态code=1 表示成功,-1 表示失败
if resp.Code != 1 {
return errors.New("Failed: " + resp.Msg)
}
// 如果返回了记录ID可以保存到 newRecord.Id
if resp.Data != nil {
if id, ok := resp.Data.(float64); ok {
newRecord.Id = types.String(int64(id))
} else if idStr, ok := resp.Data.(string); ok {
newRecord.Id = idStr
}
}
return nil
}
// UpdateRecord 修改记录
func (this *GnameProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
// 修正 CNAME 记录
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
// 设置默认 TTL
var ttl = newRecord.TTL
if ttl <= 0 {
ttl = 600
}
// 使用 Gname API 文档中的参数名
params := map[string]string{
"ym": domain, // 域名
"id": record.Id, // 记录ID
"lx": newRecord.Type, // 记录类型
"zj": newRecord.Name, // 主机记录
"jlz": newRecord.Value, // 记录值
"ttl": types.String(ttl), // TTL值
"xl": GnameDefaultRoute, // 线路
}
// 如果有线路支持
if len(newRecord.Route) > 0 && newRecord.Route != this.DefaultRoute() {
params["xl"] = newRecord.Route
}
// MX 记录需要 MX 值
if newRecord.Type == "MX" {
params["mx"] = "10" // 默认 MX 优先级
}
var resp = &gname.BaseResponse{}
err := this.doAPI(http.MethodPost, "/api/resolution/modify", params, resp)
if err != nil {
return err
}
// 检查响应状态code=1 表示成功,-1 表示失败
if resp.Code != 1 {
return errors.New("Failed: " + resp.Msg)
}
return nil
}
// DeleteRecord 删除记录
func (this *GnameProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
params := map[string]string{
"ym": domain, // 域名
"id": record.Id, // 记录ID
}
var resp = &gname.BaseResponse{}
err := this.doAPI(http.MethodPost, "/api/resolution/delete", params, resp)
if err != nil {
return err
}
// 检查响应状态code=1 表示成功,-1 表示失败
if resp.Code != 1 {
return errors.New("Failed: " + resp.Msg)
}
return nil
}
// DefaultRoute 默认线路
func (this *GnameProvider) DefaultRoute() string {
return "0" // 根据文档,默认线路代码为 "0"
}
// doAPI 发送 API 请求
// 根据 Gname API 文档,所有请求都需要包含 appid, gntime, gntoken 参数
func (this *GnameProvider) doAPI(method string, apiPath string, params map[string]string, respPtr interface{}) error {
apiURL := GnameAPIEndpoint + apiPath
method = strings.ToUpper(method)
// 添加公共参数
if params == nil {
params = map[string]string{}
}
params["appid"] = this.appid
params["gntime"] = types.String(time.Now().Unix())
// 生成签名
params["gntoken"] = this.generateSignature(params)
// 构建请求体POST 方式)
var bodyReader io.Reader = nil
if method == http.MethodPost {
var formData = url.Values{}
for k, v := range params {
formData.Set(k, v)
}
bodyReader = strings.NewReader(formData.Encode())
} else {
// GET 方式使用查询参数
var query = url.Values{}
for k, v := range params {
query.Set(k, v)
}
apiURL += "?" + query.Encode()
}
req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return err
}
// 设置请求头
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
if method == http.MethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := gnameHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// 检查 HTTP 状态码
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
// 解析响应
if respPtr != nil {
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w: %s", err, string(data))
}
}
return nil
}
// generateSignature 生成签名
// 根据 Gname API 文档的签名方法:
// 1. 将所有参数(除了 gntoken按字母顺序排序
// 2. 拼接成字符串key1=value1&key2=value2&...
// 3. 加上密钥:字符串 + secret
// 4. MD5 加密并转大写
func (this *GnameProvider) generateSignature(params map[string]string) string {
// 复制参数(排除 gntoken
var signParams = map[string]string{}
for k, v := range params {
if k != "gntoken" {
signParams[k] = v
}
}
// 按字母顺序排序
var keys = []string{}
for k := range signParams {
keys = append(keys, k)
}
sort.Strings(keys)
// 拼接字符串
var source strings.Builder
for i, key := range keys {
if i > 0 {
source.WriteString("&")
}
source.WriteString(key)
source.WriteString("=")
source.WriteString(url.QueryEscape(signParams[key]))
}
// 加上密钥
source.WriteString(this.secret)
// MD5 加密并转大写
md := md5.Sum([]byte(source.String()))
return strings.ToUpper(hex.EncodeToString(md[:]))
}

View File

@@ -0,0 +1,184 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dnsclients_test
import (
"encoding/json"
"testing"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
)
const GnameTestDomain = "xyyp.xyz" // 请修改为您的测试域名(使用您账户下实际拥有的域名)
func TestGnameProvider_GetDomains(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
domains, err := provider.GetDomains()
if err != nil {
t.Fatal(err)
}
t.Log("domains count:", len(domains))
if len(domains) > 0 {
t.Log("first domain:", domains[0])
}
logs.PrintAsJSON(domains, t)
}
func TestGnameProvider_GetRecords(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
records, err := provider.GetRecords(GnameTestDomain)
if err != nil {
t.Fatal(err)
}
t.Log("records count:", len(records))
if len(records) > 0 {
t.Log("first record:", records[0].Name, records[0].Type, records[0].Value)
}
logs.PrintAsJSON(records, t)
}
func TestGnameProvider_GetRoutes(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
routes, err := provider.GetRoutes(GnameTestDomain)
if err != nil {
t.Fatal(err)
}
t.Log("routes count:", len(routes))
logs.PrintAsJSON(routes, t)
}
func TestGnameProvider_QueryRecord(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
// 测试查询不同类型的记录
testCases := []struct {
name string
recordType dnstypes.RecordType
}{
{"www", dnstypes.RecordTypeA},
{"@", dnstypes.RecordTypeA},
{"", dnstypes.RecordTypeA},
{"www", dnstypes.RecordTypeCNAME},
{"mail", dnstypes.RecordTypeA},
}
for _, tc := range testCases {
t.Logf("=== Query: %s %s ===", tc.name, tc.recordType)
record, err := provider.QueryRecord(GnameTestDomain, tc.name, tc.recordType)
if err != nil {
t.Logf("Error querying %s %s: %v", tc.name, tc.recordType, err)
continue
}
if record == nil {
t.Logf("Record not found: %s %s", tc.name, tc.recordType)
} else {
logs.PrintAsJSON(record, t)
}
}
}
func TestGnameProvider_QueryRecords(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
// 测试查询多个记录
testCases := []struct {
name string
recordType dnstypes.RecordType
}{
{"www", dnstypes.RecordTypeA},
{"@", dnstypes.RecordTypeA},
{"", dnstypes.RecordTypeA},
{"www", dnstypes.RecordTypeCNAME},
}
for _, tc := range testCases {
t.Logf("=== QueryRecords: %s %s ===", tc.name, tc.recordType)
records, err := provider.QueryRecords(GnameTestDomain, tc.name, tc.recordType)
if err != nil {
t.Logf("Error querying %s %s: %v", tc.name, tc.recordType, err)
continue
}
if len(records) == 0 {
t.Logf("No records found: %s %s", tc.name, tc.recordType)
} else {
t.Logf("Found %d records", len(records))
logs.PrintAsJSON(records, t)
}
}
}
func TestGnameProvider_DefaultRoute(t *testing.T) {
provider, err := testGnameProvider()
if err != nil {
t.Fatal(err)
}
defaultRoute := provider.DefaultRoute()
t.Log("default route:", defaultRoute)
if len(defaultRoute) == 0 {
t.Error("default route should not be empty")
}
}
// testGnameProvider 创建测试用的 Gname Provider
// 方式1: 从数据库读取配置(如果数据库中有配置)
func testGnameProvider() (dnsclients.ProviderInterface, error) {
// 尝试从数据库读取配置
db, err := dbs.Default()
if err == nil {
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='gname' ORDER BY id DESC")
if err == nil && one != nil {
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err == nil {
provider := &dnsclients.GnameProvider{}
err = provider.Auth(apiParams)
if err == nil {
return provider, nil
}
}
}
}
// 方式2: 使用硬编码配置(请修改为您的实际 APPID 和 Secret
provider := &dnsclients.GnameProvider{}
// 请在此处填写您的 Gname API 凭证
// 可以使用 appid/secret 或 apiKey/apiSecret兼容旧参数名
authParams := maps.Map{
"appid": "1519156931639cd973f", // 请修改为您的 APPID
"secret": "5uBTJCwYFNQBjbYsc8ak", // 请修改为您的 Secret
// 或者使用旧参数名(二选一):
// "apiKey": "your-api-key-here",
// "apiSecret": "your-api-secret-here",
}
err = provider.Auth(authParams)
if err != nil {
return nil, errors.New("please set your Gname API credentials (appid/secret or apiKey/apiSecret) in testGnameProvider() function or in database")
}
return provider, nil
}

View File

@@ -0,0 +1,413 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build plus
package dnsclients
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/godaddy"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const (
GoDaddyAPIEndpoint = "https://api.godaddy.com/v1"
GoDaddyDefaultRoute = "default"
GoDaddyIdDelim = "$"
GoDaddyDefaultTTL = 600
)
var goDaddyHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
/**Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse("socks5://127.0.0.1:7890")
},**/
},
}
// GoDaddyProvider
//
// 参考文档https://developer.godaddy.com/doc/endpoint/domains
type GoDaddyProvider struct {
BaseProvider
ProviderId int64
key string
secret string
}
// Auth 认证
func (this *GoDaddyProvider) Auth(params maps.Map) error {
this.key = params.GetString("key")
if len(this.key) == 0 {
return errors.New("'key' should not be empty")
}
this.secret = params.GetString("secret")
if len(this.secret) == 0 {
return errors.New("'secret' should not be empty")
}
return nil
}
// MaskParams 对参数进行掩码
func (this *GoDaddyProvider) MaskParams(params maps.Map) {
if params == nil {
return
}
params["secret"] = MaskString(params.GetString("secret"))
}
// GetDomains 获取所有域名列表
func (this *GoDaddyProvider) GetDomains() (domains []string, err error) {
var respDomains = godaddy.DomainsResponse{}
err = this.doAPI(http.MethodGet, "/domains", nil, &respDomains)
if err != nil {
return
}
for _, domain := range respDomains {
if domain.Status == "ACTIVE" {
domains = append(domains, domain.Domain)
}
}
return
}
// GetRecords 获取域名解析记录列表
func (this *GoDaddyProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
var respRecords = godaddy.RecordsResponse{}
err = this.doAPI(http.MethodGet, "/domains/"+domain+"/records", nil, &respRecords)
if err != nil {
return
}
for _, record := range respRecords {
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Data, ".") {
record.Data += "."
}
var recordObj = &dnstypes.Record{
Name: record.Name,
Type: record.Type,
Value: record.Data,
Route: GoDaddyDefaultRoute,
TTL: record.TTL,
}
this.addRecordId(recordObj)
records = append(records, recordObj)
}
return
}
// GetRoutes 读取域名支持的线路数据
func (this *GoDaddyProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
routes = []*dnstypes.Route{
{Name: "默认", Code: GoDaddyDefaultRoute},
}
return
}
// QueryRecord 查询单个记录
func (this *GoDaddyProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
var respRecords = godaddy.RecordsResponse{}
err := this.doAPI(http.MethodGet, "/domains/"+domain+"/records/"+recordType+"/"+name, nil, &respRecords)
if err != nil {
return nil, err
}
for _, record := range respRecords {
// 再次检查名称
if record.Name != name {
continue
}
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Data, ".") {
record.Data += "."
}
return &dnstypes.Record{
Id: record.Name + GoDaddyIdDelim + record.Type + GoDaddyIdDelim + stringutil.Md5(record.Data),
Name: record.Name,
Type: record.Type,
Value: record.Data,
Route: GoDaddyDefaultRoute,
TTL: record.TTL,
}, nil
}
return nil, nil
}
// QueryRecords 查询多个记录
func (this *GoDaddyProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
var respRecords = godaddy.RecordsResponse{}
err := this.doAPI(http.MethodGet, "/domains/"+domain+"/records/"+recordType+"/"+name, nil, &respRecords)
if err != nil {
return nil, err
}
var result = []*dnstypes.Record{}
for _, record := range respRecords {
// 再次检查名称
if record.Name != name {
continue
}
// 修正Record
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Data, ".") {
record.Data += "."
}
result = append(result, &dnstypes.Record{
Id: record.Name + GoDaddyIdDelim + record.Type + GoDaddyIdDelim + stringutil.Md5(record.Data),
Name: record.Name,
Type: record.Type,
Value: record.Data,
Route: GoDaddyDefaultRoute,
TTL: record.TTL,
})
}
return result, nil
}
// AddRecord 设置记录
func (this *GoDaddyProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
if newRecord.TTL <= 0 {
newRecord.TTL = GoDaddyDefaultTTL
}
if newRecord.Type == dnstypes.RecordTypeCNAME {
if !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
}
var recordMaps = []maps.Map{
{
"data": newRecord.Value,
"name": newRecord.Name,
"ttl": newRecord.TTL,
"type": newRecord.Type,
"priority": 0,
"weight": 0,
"port": 65535,
},
}
recordMapsJSON, err := json.Marshal(recordMaps)
if err != nil {
return fmt.Errorf("encode records failed: %w", err)
}
err = this.doAPI(http.MethodPatch, "/domains/"+domain+"/records", recordMapsJSON, nil)
if err != nil {
return err
}
this.addRecordId(newRecord)
return nil
}
// UpdateRecord 修改记录
func (this *GoDaddyProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
var recordType = record.Type
var recordName = record.Name
var recordValueMd5 = stringutil.Md5(record.Value)
if len(recordType) == 0 || len(recordName) == 0 {
if len(record.Id) == 0 {
return errors.New("invalid record to delete")
}
recordName, recordType, recordValueMd5 = this.splitRecordId(record.Id)
if len(recordType) == 0 || len(recordName) == 0 {
return errors.New("invalid record to delete")
}
}
var respRecords = godaddy.RecordsResponse{}
err := this.doAPI(http.MethodGet, "/domains/"+domain+"/records/"+recordType+"/"+recordName, nil, &respRecords)
if err != nil {
return err
}
this.addRecordId(newRecord)
var found = false
for index, gRecord := range respRecords {
var gRecordValue = gRecord.Data
if gRecord.Type == dnstypes.RecordTypeCNAME {
if !strings.HasSuffix(gRecordValue, ".") {
gRecordValue += "."
}
}
if gRecord.Name == recordName && gRecord.Type == recordType && stringutil.Md5(gRecordValue) == recordValueMd5 {
gRecord.Name = newRecord.Name
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
newRecord.Value += "."
}
gRecord.Data = newRecord.Value
gRecord.Type = newRecord.Type
gRecord.TTL = newRecord.TTL
if newRecord.TTL <= 0 {
gRecord.TTL = GoDaddyDefaultTTL
}
respRecords[index] = gRecord
found = true
break
}
}
if found {
newRecordsJSON, err := json.Marshal(respRecords)
if err != nil {
return err
}
err = this.doAPI(http.MethodPut, "/domains/"+domain+"/records/"+recordType+"/"+recordName, newRecordsJSON, nil)
if err != nil {
return err
}
}
return nil
}
// DeleteRecord 删除记录
func (this *GoDaddyProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
var recordType = record.Type
var recordName = record.Name
var recordValueMd5 = stringutil.Md5(record.Value)
if len(recordType) == 0 || len(recordName) == 0 {
if len(record.Id) == 0 {
return errors.New("invalid record to delete")
}
recordName, recordType, recordValueMd5 = this.splitRecordId(record.Id)
if len(recordType) == 0 || len(recordName) == 0 {
return errors.New("invalid record to delete")
}
}
var respRecords = godaddy.RecordsResponse{}
err := this.doAPI(http.MethodGet, "/domains/"+domain+"/records/"+recordType+"/"+recordName, nil, &respRecords)
if err != nil {
return err
}
var newRecords = godaddy.RecordsResponse{}
for _, gRecord := range respRecords {
var gRecordValue = gRecord.Data
if gRecord.Type == dnstypes.RecordTypeCNAME {
if !strings.HasSuffix(gRecordValue, ".") {
gRecordValue += "."
}
}
if gRecord.Name == recordName && gRecord.Type == recordType && stringutil.Md5(gRecordValue) == recordValueMd5 {
continue
}
newRecords = append(newRecords, gRecord)
}
if len(newRecords) > 0 {
newRecordsJSON, err := json.Marshal(newRecords)
if err != nil {
return err
}
err = this.doAPI(http.MethodPut, "/domains/"+domain+"/records/"+recordType+"/"+recordName, newRecordsJSON, nil)
if err != nil {
return err
}
} else {
err = this.doAPI(http.MethodDelete, "/domains/"+domain+"/records/"+recordType+"/"+recordName, nil, nil)
if err != nil {
return err
}
}
return nil
}
// DefaultRoute 默认线路
func (this *GoDaddyProvider) DefaultRoute() string {
return GoDaddyDefaultRoute
}
func (this *GoDaddyProvider) addRecordId(record *dnstypes.Record) {
record.Id = record.Name + GoDaddyIdDelim + record.Type + GoDaddyIdDelim + stringutil.Md5(record.Value)
}
func (this *GoDaddyProvider) splitRecordId(recordId string) (recordName string, recordType string, valueMd5 string) {
var pieces = strings.Split(recordId, GoDaddyIdDelim)
if len(pieces) < 3 {
return
}
return pieces[0], pieces[1], pieces[2]
}
// 发送请求
func (this *GoDaddyProvider) doAPI(method string, apiPath string, bodyJSON []byte, respPtr interface{}) error {
apiURL := GoDaddyAPIEndpoint + apiPath
method = strings.ToUpper(method)
var bodyReader io.Reader = nil
if len(bodyJSON) > 0 {
bodyReader = bytes.NewReader(bodyJSON)
}
req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return err
}
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "sso-key "+this.key+":"+this.secret)
resp, err := goDaddyHTTPClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == 0 {
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
}
if resp.StatusCode != http.StatusOK {
return errors.New("response error: " + string(data))
}
if respPtr != nil {
err = json.Unmarshal(data, respPtr)
if err != nil {
return fmt.Errorf("decode json failed: %w, response text: %s", err, string(data))
}
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More