diff --git a/internal/dnsclients/provider_edge_dns_api.go b/internal/dnsclients/provider_edge_dns_api.go new file mode 100644 index 00000000..9813c05c --- /dev/null +++ b/internal/dnsclients/provider_edge_dns_api.go @@ -0,0 +1,441 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package dnsclients + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + 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 { + 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 +} + +// 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, + }) + } + } + + // 自定义 + { + 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 +} + +// 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 + } + + 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 errors.New("decode response failed: " + err.Error() + ", JSON: " + 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 +} diff --git a/internal/dnsclients/provider_edge_dns_api_test.go b/internal/dnsclients/provider_edge_dns_api_test.go new file mode 100644 index 00000000..db2430db --- /dev/null +++ b/internal/dnsclients/provider_edge_dns_api_test.go @@ -0,0 +1,163 @@ +// 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_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": "JOvsyXIFqkQbh5kl", + "accessKeySecret": "t0RY8YO3R58VbJJNp0RqKw9KWNpObwtE", + }) + if err != nil { + return nil, err + } + return provider, nil +} diff --git a/internal/dnsclients/provider_user_edge_dns.go b/internal/dnsclients/provider_user_edge_dns.go deleted file mode 100644 index 79a0ad82..00000000 --- a/internal/dnsclients/provider_user_edge_dns.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. - -package dnsclients - -import ( - "github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes" - "github.com/iwind/TeaGo/maps" -) - -type UserEdgeDNSProvider struct { -} - -// Auth 认证 -func (this *UserEdgeDNSProvider) Auth(params maps.Map) error { - // TODO - return nil -} - -// GetDomains 获取所有域名列表 -func (this *UserEdgeDNSProvider) GetDomains() (domains []string, err error) { - // TODO - return -} - -// GetRecords 获取域名解析记录列表 -func (this *UserEdgeDNSProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) { - // TODO - return -} - -// GetRoutes 读取域名支持的线路数据 -func (this *UserEdgeDNSProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) { - // TODO - return -} - -// QueryRecord 查询单个记录 -func (this *UserEdgeDNSProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) { - // TODO - return nil, nil -} - -// AddRecord 设置记录 -func (this *UserEdgeDNSProvider) AddRecord(domain string, newRecord *dnstypes.Record) error { - // TODO - return nil -} - -// UpdateRecord 修改记录 -func (this *UserEdgeDNSProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error { - // TODO - return nil -} - -// DeleteRecord 删除记录 -func (this *UserEdgeDNSProvider) DeleteRecord(domain string, record *dnstypes.Record) error { - // TODO - return nil -} - -// DefaultRoute 默认线路 -func (this *UserEdgeDNSProvider) DefaultRoute() string { - // TODO - return "" -} diff --git a/internal/dnsclients/types.go b/internal/dnsclients/types.go index 2ea6ecf0..3f261149 100644 --- a/internal/dnsclients/types.go +++ b/internal/dnsclients/types.go @@ -13,13 +13,13 @@ const ( ProviderTypeHuaweiDNS ProviderType = "huaweiDNS" // 华为DNS ProviderTypeCloudFlare ProviderType = "cloudFlare" // CloudFlare DNS ProviderTypeLocalEdgeDNS ProviderType = "localEdgeDNS" // 和当前系统集成的EdgeDNS - ProviderTypeUserEdgeDNS ProviderType = "userEdgeDNS" // 通过API连接的EdgeDNS + ProviderTypeEdgeDNSAPI ProviderType = "edgeDNSAPI" // 通过API连接的EdgeDNS ProviderTypeCustomHTTP ProviderType = "customHTTP" // 自定义HTTP接口 ) // FindAllProviderTypes 所有的服务商类型 func FindAllProviderTypes() []maps.Map { - typeMaps := []maps.Map{ + var typeMaps = []maps.Map{ { "name": "阿里云DNS", "code": ProviderTypeAliDNS, @@ -40,6 +40,11 @@ func FindAllProviderTypes() []maps.Map { "code": ProviderTypeCloudFlare, "description": "CloudFlare提供的DNS服务。", }, + { + "name": "EdgeDNS API", + "code": ProviderTypeEdgeDNSAPI, + "description": "通过API连接GoEdge商业版系统提供的DNS服务。", + }, } typeMaps = filterTypeMaps(typeMaps) diff --git a/internal/dnsclients/types_ext.go b/internal/dnsclients/types_ext.go index d64628ce..97987d27 100644 --- a/internal/dnsclients/types_ext.go +++ b/internal/dnsclients/types_ext.go @@ -19,6 +19,8 @@ func FindProvider(providerType ProviderType) ProviderInterface { return &CloudFlareProvider{} case ProviderTypeCustomHTTP: return &CustomHTTPProvider{} + case ProviderTypeUserEdgeDNS: + return &EdgeDNSAPIProvider{} } return nil