1.4.5.2
This commit is contained in:
18
EdgeAPI/internal/dnsclients/bunnynet/response_dns_zone.go
Normal file
18
EdgeAPI/internal/dnsclients/bunnynet/response_dns_zone.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
14
EdgeAPI/internal/dnsclients/bunnynet/response_record.go
Normal file
14
EdgeAPI/internal/dnsclients/bunnynet/response_record.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
22
EdgeAPI/internal/dnsclients/cloudflare/response_base.go
Normal file
22
EdgeAPI/internal/dnsclients/cloudflare/response_base.go
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package cloudflare
|
||||
|
||||
type CreateDNSRecordResponse struct {
|
||||
BaseResponse
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package cloudflare
|
||||
|
||||
type DeleteDNSRecordResponse struct {
|
||||
BaseResponse
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package cloudflare
|
||||
|
||||
type UpdateDNSRecordResponse struct {
|
||||
BaseResponse
|
||||
}
|
||||
12
EdgeAPI/internal/dnsclients/cloudflare/response_zones.go
Normal file
12
EdgeAPI/internal/dnsclients/cloudflare/response_zones.go
Normal 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"`
|
||||
}
|
||||
12
EdgeAPI/internal/dnsclients/cloudns/response_record.go
Normal file
12
EdgeAPI/internal/dnsclients/cloudns/response_record.go
Normal 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"`
|
||||
}
|
||||
5
EdgeAPI/internal/dnsclients/cloudns/response_records.go
Normal file
5
EdgeAPI/internal/dnsclients/cloudns/response_records.go
Normal 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
|
||||
8
EdgeAPI/internal/dnsclients/cloudns/response_status.go
Normal file
8
EdgeAPI/internal/dnsclients/cloudns/response_status.go
Normal 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"`
|
||||
}
|
||||
10
EdgeAPI/internal/dnsclients/cloudns/response_zone.go
Normal file
10
EdgeAPI/internal/dnsclients/cloudns/response_zone.go
Normal 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"`
|
||||
}
|
||||
5
EdgeAPI/internal/dnsclients/cloudns/response_zones.go
Normal file
5
EdgeAPI/internal/dnsclients/cloudns/response_zones.go
Normal 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
|
||||
16
EdgeAPI/internal/dnsclients/dnscom/response_create_record.go
Normal file
16
EdgeAPI/internal/dnsclients/dnscom/response_create_record.go
Normal 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"`
|
||||
}
|
||||
17
EdgeAPI/internal/dnsclients/dnscom/response_domain_list.go
Normal file
17
EdgeAPI/internal/dnsclients/dnscom/response_domain_list.go
Normal 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"`
|
||||
}
|
||||
17
EdgeAPI/internal/dnsclients/dnscom/response_domain_search.go
Normal file
17
EdgeAPI/internal/dnsclients/dnscom/response_domain_search.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
24
EdgeAPI/internal/dnsclients/dnscom/response_record_list.go
Normal file
24
EdgeAPI/internal/dnsclients/dnscom/response_record_list.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
16
EdgeAPI/internal/dnsclients/dnsla/response_all_line_list.go
Normal file
16
EdgeAPI/internal/dnsclients/dnsla/response_all_line_list.go
Normal 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"`
|
||||
}
|
||||
21
EdgeAPI/internal/dnsclients/dnsla/response_base.go
Normal file
21
EdgeAPI/internal/dnsclients/dnsla/response_base.go
Normal 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)
|
||||
}
|
||||
11
EdgeAPI/internal/dnsclients/dnsla/response_domain.go
Normal file
11
EdgeAPI/internal/dnsclients/dnsla/response_domain.go
Normal 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"`
|
||||
}
|
||||
15
EdgeAPI/internal/dnsclients/dnsla/response_domain_list.go
Normal file
15
EdgeAPI/internal/dnsclients/dnsla/response_domain_list.go
Normal 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"`
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
11
EdgeAPI/internal/dnsclients/dnsla/response_record_create.go
Normal file
11
EdgeAPI/internal/dnsclients/dnsla/response_record_create.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
22
EdgeAPI/internal/dnsclients/dnsla/response_record_list.go
Normal file
22
EdgeAPI/internal/dnsclients/dnsla/response_record_list.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
18
EdgeAPI/internal/dnsclients/dnspod/response_base.go
Normal file
18
EdgeAPI/internal/dnsclients/dnspod/response_base.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
13
EdgeAPI/internal/dnsclients/dnspod/response_domain_info.go
Normal file
13
EdgeAPI/internal/dnsclients/dnspod/response_domain_info.go
Normal 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"`
|
||||
}
|
||||
17
EdgeAPI/internal/dnsclients/dnspod/response_domain_list.go
Normal file
17
EdgeAPI/internal/dnsclients/dnspod/response_domain_list.go
Normal 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"`
|
||||
}
|
||||
8
EdgeAPI/internal/dnsclients/dnspod/response_interface.go
Normal file
8
EdgeAPI/internal/dnsclients/dnspod/response_interface.go
Normal 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)
|
||||
}
|
||||
13
EdgeAPI/internal/dnsclients/dnspod/response_record_create.go
Normal file
13
EdgeAPI/internal/dnsclients/dnspod/response_record_create.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
23
EdgeAPI/internal/dnsclients/dnspod/response_record_list.go
Normal file
23
EdgeAPI/internal/dnsclients/dnspod/response_record_list.go
Normal 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"`
|
||||
}
|
||||
14
EdgeAPI/internal/dnsclients/dnspod/response_record_modify.go
Normal file
14
EdgeAPI/internal/dnsclients/dnspod/response_record_modify.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
42
EdgeAPI/internal/dnsclients/dnstypes/record.go
Normal file
42
EdgeAPI/internal/dnsclients/dnstypes/record.go
Normal 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
|
||||
}
|
||||
7
EdgeAPI/internal/dnsclients/dnstypes/route.go
Normal file
7
EdgeAPI/internal/dnsclients/dnstypes/route.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package dnstypes
|
||||
|
||||
// Route 线路描述
|
||||
type Route struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
244
EdgeAPI/internal/dnsclients/domain_records_cache.go
Normal file
244
EdgeAPI/internal/dnsclients/domain_records_cache.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
EdgeAPI/internal/dnsclients/domain_records_cahce_test.go
Normal file
58
EdgeAPI/internal/dnsclients/domain_records_cahce_test.go
Normal 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"))
|
||||
}
|
||||
}
|
||||
21
EdgeAPI/internal/dnsclients/edgeapi/response_base.go
Normal file
21
EdgeAPI/internal/dnsclients/edgeapi/response_base.go
Normal 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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
7
EdgeAPI/internal/dnsclients/edgeapi/response_success.go
Normal file
7
EdgeAPI/internal/dnsclients/edgeapi/response_success.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
7
EdgeAPI/internal/dnsclients/gname/response_base.go
Normal file
7
EdgeAPI/internal/dnsclients/gname/response_base.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package gname
|
||||
|
||||
type BaseResponse struct {
|
||||
Code int `json:"code"` // 1: 成功, -1: 失败
|
||||
Msg string `json:"msg"` // 返回描述
|
||||
Data interface{} `json:"data"` // 返回数据
|
||||
}
|
||||
9
EdgeAPI/internal/dnsclients/gname/response_domains.go
Normal file
9
EdgeAPI/internal/dnsclients/gname/response_domains.go
Normal 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"`
|
||||
}
|
||||
12
EdgeAPI/internal/dnsclients/gname/response_records.go
Normal file
12
EdgeAPI/internal/dnsclients/gname/response_records.go
Normal 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"` // 线路
|
||||
}
|
||||
1
EdgeAPI/internal/dnsclients/gname/response_routes.go
Normal file
1
EdgeAPI/internal/dnsclients/gname/response_routes.go
Normal file
@@ -0,0 +1 @@
|
||||
package gname
|
||||
9
EdgeAPI/internal/dnsclients/godaddy/response_domains.go
Normal file
9
EdgeAPI/internal/dnsclients/godaddy/response_domains.go
Normal 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"`
|
||||
}
|
||||
10
EdgeAPI/internal/dnsclients/godaddy/response_records.go
Normal file
10
EdgeAPI/internal/dnsclients/godaddy/response_records.go
Normal 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"`
|
||||
}
|
||||
6
EdgeAPI/internal/dnsclients/huaweidns/response_base.go
Normal file
6
EdgeAPI/internal/dnsclients/huaweidns/response_base.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package huaweidns
|
||||
|
||||
type BaseResponse struct {
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
17
EdgeAPI/internal/dnsclients/huaweidns/response_zones.go
Normal file
17
EdgeAPI/internal/dnsclients/huaweidns/response_zones.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package huaweidns
|
||||
|
||||
type ZonesDeleteRecordSetResponse struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
292
EdgeAPI/internal/dnsclients/provider_alidns.go
Normal file
292
EdgeAPI/internal/dnsclients/provider_alidns.go
Normal 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
|
||||
}
|
||||
161
EdgeAPI/internal/dnsclients/provider_alidns_test.go
Normal file
161
EdgeAPI/internal/dnsclients/provider_alidns_test.go
Normal 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
|
||||
}
|
||||
807
EdgeAPI/internal/dnsclients/provider_amazon_route53_plus.go
Normal file
807
EdgeAPI/internal/dnsclients/provider_amazon_route53_plus.go
Normal 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
|
||||
}
|
||||
233
EdgeAPI/internal/dnsclients/provider_amazon_route53_plus_test.go
Normal file
233
EdgeAPI/internal/dnsclients/provider_amazon_route53_plus_test.go
Normal 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
|
||||
}
|
||||
744
EdgeAPI/internal/dnsclients/provider_azure_dns_plus.go
Normal file
744
EdgeAPI/internal/dnsclients/provider_azure_dns_plus.go
Normal 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
|
||||
}
|
||||
319
EdgeAPI/internal/dnsclients/provider_azure_dns_plus_test.go
Normal file
319
EdgeAPI/internal/dnsclients/provider_azure_dns_plus_test.go
Normal 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
|
||||
}
|
||||
42
EdgeAPI/internal/dnsclients/provider_base.go
Normal file
42
EdgeAPI/internal/dnsclients/provider_base.go
Normal 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
|
||||
}
|
||||
38
EdgeAPI/internal/dnsclients/provider_base_test.go
Normal file
38
EdgeAPI/internal/dnsclients/provider_base_test.go
Normal 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,
|
||||
}))
|
||||
}
|
||||
417
EdgeAPI/internal/dnsclients/provider_bunny_net_plus.go
Normal file
417
EdgeAPI/internal/dnsclients/provider_bunny_net_plus.go
Normal 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
|
||||
}
|
||||
}
|
||||
246
EdgeAPI/internal/dnsclients/provider_bunny_net_plus_test.go
Normal file
246
EdgeAPI/internal/dnsclients/provider_bunny_net_plus_test.go
Normal 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
|
||||
}
|
||||
380
EdgeAPI/internal/dnsclients/provider_cloud_flare.go
Normal file
380
EdgeAPI/internal/dnsclients/provider_cloud_flare.go
Normal 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
|
||||
}
|
||||
192
EdgeAPI/internal/dnsclients/provider_cloud_flare_test.go
Normal file
192
EdgeAPI/internal/dnsclients/provider_cloud_flare_test.go
Normal 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
|
||||
}
|
||||
371
EdgeAPI/internal/dnsclients/provider_cloudns_plus.go
Normal file
371
EdgeAPI/internal/dnsclients/provider_cloudns_plus.go
Normal 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
|
||||
}
|
||||
173
EdgeAPI/internal/dnsclients/provider_cloudns_plus_test.go
Normal file
173
EdgeAPI/internal/dnsclients/provider_cloudns_plus_test.go
Normal 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
|
||||
}
|
||||
215
EdgeAPI/internal/dnsclients/provider_custom_http.go
Normal file
215
EdgeAPI/internal/dnsclients/provider_custom_http.go
Normal 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)
|
||||
}
|
||||
62
EdgeAPI/internal/dnsclients/provider_custom_http_test.go
Normal file
62
EdgeAPI/internal/dnsclients/provider_custom_http_test.go
Normal 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)
|
||||
}
|
||||
635
EdgeAPI/internal/dnsclients/provider_dns_com_plus.go
Normal file
635
EdgeAPI/internal/dnsclients/provider_dns_com_plus.go
Normal 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[:])
|
||||
}
|
||||
189
EdgeAPI/internal/dnsclients/provider_dns_com_plus_test.go
Normal file
189
EdgeAPI/internal/dnsclients/provider_dns_com_plus_test.go
Normal 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
|
||||
}
|
||||
607
EdgeAPI/internal/dnsclients/provider_dns_la_plus.go
Normal file
607
EdgeAPI/internal/dnsclients/provider_dns_la_plus.go
Normal 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 + "'")
|
||||
}
|
||||
216
EdgeAPI/internal/dnsclients/provider_dns_la_plus_test.go
Normal file
216
EdgeAPI/internal/dnsclients/provider_dns_la_plus_test.go
Normal 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
|
||||
}
|
||||
481
EdgeAPI/internal/dnsclients/provider_dnspod.go
Normal file
481
EdgeAPI/internal/dnsclients/provider_dnspod.go
Normal 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
|
||||
}
|
||||
186
EdgeAPI/internal/dnsclients/provider_dnspod_test.go
Normal file
186
EdgeAPI/internal/dnsclients/provider_dnspod_test.go
Normal 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
|
||||
}
|
||||
524
EdgeAPI/internal/dnsclients/provider_edge_dns_api.go
Normal file
524
EdgeAPI/internal/dnsclients/provider_edge_dns_api.go
Normal 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
|
||||
}
|
||||
175
EdgeAPI/internal/dnsclients/provider_edge_dns_api_test.go
Normal file
175
EdgeAPI/internal/dnsclients/provider_edge_dns_api_test.go
Normal 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
|
||||
}
|
||||
537
EdgeAPI/internal/dnsclients/provider_gname.go
Normal file
537
EdgeAPI/internal/dnsclients/provider_gname.go
Normal 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 的 key(route@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[:]))
|
||||
}
|
||||
184
EdgeAPI/internal/dnsclients/provider_gname_test.go
Normal file
184
EdgeAPI/internal/dnsclients/provider_gname_test.go
Normal 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
|
||||
}
|
||||
413
EdgeAPI/internal/dnsclients/provider_godaddy_plus.go
Normal file
413
EdgeAPI/internal/dnsclients/provider_godaddy_plus.go
Normal 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
Reference in New Issue
Block a user