// 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 } }