From 9dca278198a5af0bc76dddb76cca28cfe309c454 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 26 Jan 2026 11:13:10 +0800 Subject: [PATCH] fix: #1172 --- go.mod | 2 - go.sum | 4 - .../dns01/51dnscom/internal/lego.go | 2 +- .../dns01/cmcccloud/internal/lego.go | 2 +- .../dns01/ctcccloud/internal/lego.go | 2 +- .../challengers/dns01/dnsla/internal/lego.go | 2 +- .../challengers/dns01/dynv6/internal/lego.go | 89 +++++++++++++------ .../challengers/dns01/gname/internal/lego.go | 2 +- .../dns01/qingcloud/internal/lego.go | 2 +- .../challengers/dns01/xinnet/internal/lego.go | 2 +- pkg/sdk3rd/dynv6/api_add_record.go | 45 ++++++++++ pkg/sdk3rd/dynv6/api_delete_record.go | 36 ++++++++ pkg/sdk3rd/dynv6/api_list_records.go | 33 +++++++ pkg/sdk3rd/dynv6/api_list_zones.go | 28 ++++++ pkg/sdk3rd/dynv6/client.go | 89 +++++++++++++++++++ pkg/sdk3rd/dynv6/types.go | 24 +++++ 16 files changed, 325 insertions(+), 39 deletions(-) create mode 100644 pkg/sdk3rd/dynv6/api_add_record.go create mode 100644 pkg/sdk3rd/dynv6/api_delete_record.go create mode 100644 pkg/sdk3rd/dynv6/api_list_records.go create mode 100644 pkg/sdk3rd/dynv6/api_list_zones.go create mode 100644 pkg/sdk3rd/dynv6/client.go create mode 100644 pkg/sdk3rd/dynv6/types.go diff --git a/go.mod b/go.mod index ae42b3412..1b399e019 100644 --- a/go.mod +++ b/go.mod @@ -48,8 +48,6 @@ require ( github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 github.com/jdcloud-api/jdcloud-sdk-go v1.64.0 github.com/kong/go-kong v0.71.0 - github.com/libdns/dynv6 v1.1.1 - github.com/libdns/libdns v1.1.1 github.com/luthermonson/go-proxmox v0.3.2 github.com/minio/minio-go/v7 v7.0.97 github.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47 diff --git a/go.sum b/go.sum index 1876d6970..45f464a19 100644 --- a/go.sum +++ b/go.sum @@ -623,10 +623,6 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/libdns/dynv6 v1.1.1 h1:X+P/5tNeBo+MLf080kyy1aJG2FPdon0/fVOd58AlfZU= -github.com/libdns/dynv6 v1.1.1/go.mod h1:XhpjX1oZ0TdzH0nZaP5hpW3x/HCMB0T7loZw65on5Iw= -github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= -github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY= github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc= diff --git a/pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go b/pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go index 479bcddbd..6360255f7 100644 --- a/pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go @@ -41,7 +41,7 @@ type DNSProvider struct { config *Config client *dnscomsdk.Client - recordCache map[string]dnsRecordCacheEntry + recordCache map[string]dnsRecordCacheEntry // Key: ChallengeToken recordCacheMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go b/pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go index 2f66d1b5e..74ba73bd0 100644 --- a/pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go @@ -45,7 +45,7 @@ type DNSProvider struct { config *Config client *ecloudsdkclouddns.Client - recordIDs map[string]string + recordIDs map[string]string // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go b/pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go index 79ab88aa6..3bc365fc0 100644 --- a/pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go @@ -42,7 +42,7 @@ type DNSProvider struct { client *ctyundns.Client config *Config - recordIDs map[string]int32 + recordIDs map[string]int32 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go b/pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go index cdbd77bfe..ea065869f 100644 --- a/pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go @@ -43,7 +43,7 @@ type DNSProvider struct { config *Config client *dnslasdk.Client - recordIDs map[string]string + recordIDs map[string]string // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go b/pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go index e59e8cbf0..21e22d6b5 100644 --- a/pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go @@ -1,16 +1,17 @@ package internal import ( - "context" "errors" "fmt" + "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/libdns/dynv6" - "github.com/libdns/libdns" + "github.com/samber/lo" + + dynv6sdk "github.com/certimate-go/certimate/pkg/sdk3rd/dynv6" ) const ( @@ -21,6 +22,7 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -31,11 +33,17 @@ type Config struct { PropagationTimeout time.Duration PollingInterval time.Duration TTL int + HTTPTimeout time.Duration } type DNSProvider struct { config *Config - client *dynv6.Provider + client *dynv6sdk.Client + + zoneIDs map[string]int64 // Key: ZoneName; Value: ZoneID + zoneIDsMu sync.Mutex + recordIDs map[string]int64 // Key: ChallengeToken; Value: RecordID + recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { @@ -43,6 +51,7 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } @@ -63,11 +72,20 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("dynv6: the configuration of the DNS provider is nil") } - client := &dynv6.Provider{Token: config.HTTPToken} + client, err := dynv6sdk.NewClient(config.HTTPToken) + if err != nil { + return nil, fmt.Errorf("dnsexit: %w", err) + } else { + client.SetTimeout(config.HTTPTimeout) + } return &DNSProvider{ - config: config, - client: client, + config: config, + client: client, + zoneIDs: make(map[string]int64), + zoneIDsMu: sync.Mutex{}, + recordIDs: make(map[string]int64), + recordIDsMu: sync.Mutex{}, }, nil } @@ -84,14 +102,29 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("dynv6: %w", err) } - if _, err := d.client.AppendRecords(context.Background(), dns01.UnFqdn(authZone), []libdns.Record{&libdns.TXT{ - Name: subDomain, - Text: info.Value, - TTL: time.Duration(d.config.TTL), - }}); err != nil { + zone, err := d.findZone(dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("dynv6: error when list zones: %w", err) + } + + // REF: https://dynv6.github.io/api-spec/#tag/records/operation/addRecord + response, err := d.client.AddRecord(zone.ID, &dynv6sdk.AddRecordRequest{ + Type: lo.ToPtr("TXT"), + Name: lo.ToPtr(subDomain), + Data: lo.ToPtr(info.Value), + }) + if err != nil { return fmt.Errorf("dynv6: error when create record: %w", err) } + d.zoneIDsMu.Lock() + d.zoneIDs[zone.Name] = zone.ID + d.zoneIDsMu.Unlock() + + d.recordIDsMu.Lock() + d.recordIDs[token] = response.ID + d.recordIDsMu.Unlock() + return nil } @@ -103,17 +136,21 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("dynv6: could not find zone for domain %q: %w", domain, err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("dynv6: %w", err) + d.zoneIDsMu.Lock() + zoneId, ok := d.zoneIDs[dns01.UnFqdn(authZone)] + d.zoneIDsMu.Unlock() + if !ok { + return fmt.Errorf("dynv6: unknown zone ID for '%s'", dns01.UnFqdn(authZone)) } - record, err := d.findDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value) - if err != nil { - return fmt.Errorf("dynv6: error when find record: %w", err) + d.recordIDsMu.Lock() + recordId, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + if !ok { + return fmt.Errorf("dynv6: unknown record ID for '%s'", info.EffectiveFQDN) } - if _, err := d.client.DeleteRecords(context.Background(), dns01.UnFqdn(authZone), []libdns.Record{record}); err != nil { + if _, err := d.client.DeleteRecord(zoneId, recordId); err != nil { return fmt.Errorf("dynv6: error when delete record: %w", err) } @@ -124,18 +161,18 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) findDNSRecord(zoneName, subDomain, tokenValue string) (libdns.Record, error) { - records, err := d.client.GetRecords(context.Background(), zoneName) +func (d *DNSProvider) findZone(zoneName string) (*dynv6sdk.ZoneRecord, error) { + // REF: https://dynv6.github.io/api-spec/#tag/zones/operation/findZones + zones, err := d.client.ListZones() if err != nil { return nil, err } - for _, record := range records { - rr := record.RR() - if rr.Type == "TXT" && rr.Name == subDomain && rr.Data == tokenValue { - return record, nil + for _, zone := range *zones { + if zone.Name == zoneName { + return zone, nil } } - return nil, errors.New("could not find record") + return nil, fmt.Errorf("could not find zone: '%s'", zoneName) } diff --git a/pkg/core/certifier/challengers/dns01/gname/internal/lego.go b/pkg/core/certifier/challengers/dns01/gname/internal/lego.go index 03cff586a..70ee2f149 100644 --- a/pkg/core/certifier/challengers/dns01/gname/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/gname/internal/lego.go @@ -42,7 +42,7 @@ type DNSProvider struct { config *Config client *gnamesdk.Client - recordIDs map[string]int64 + recordIDs map[string]int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go b/pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go index f7ca1515e..d469c6564 100644 --- a/pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go @@ -42,7 +42,7 @@ type DNSProvider struct { config *Config client *qingcloudsdk.Client - recordIDs map[string]*int64 + recordIDs map[string]*int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go b/pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go index 00d9d96e3..25b9b0cb5 100644 --- a/pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go @@ -42,7 +42,7 @@ type DNSProvider struct { config *Config client *xinnetsdk.Client - recordIDs map[string]*int64 + recordIDs map[string]*int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } diff --git a/pkg/sdk3rd/dynv6/api_add_record.go b/pkg/sdk3rd/dynv6/api_add_record.go new file mode 100644 index 000000000..25e4542dc --- /dev/null +++ b/pkg/sdk3rd/dynv6/api_add_record.go @@ -0,0 +1,45 @@ +package dynv6 + +import ( + "context" + "fmt" + "net/http" +) + +type AddRecordRequest struct { + Type *string `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + Port *int `json:"port,omitempty"` + Weight *int `json:"weight,omitempty"` + Priority *int `json:"priority,omitempty"` + Data *string `json:"data,omitempty"` + Flags *int `json:"flags,omitempty"` + Tag *string `json:"tag,omitempty"` +} + +type AddRecordResponse DNSRecord + +func (c *Client) AddRecord(zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) { + return c.AddRecordWithContext(context.Background(), zoneID, req) +} + +func (c *Client) AddRecordWithContext(ctx context.Context, zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) { + if zoneID == 0 { + return nil, fmt.Errorf("sdkerr: unset zoneID") + } + + httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/zones/%d/records", zoneID)) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &AddRecordResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/dynv6/api_delete_record.go b/pkg/sdk3rd/dynv6/api_delete_record.go new file mode 100644 index 000000000..061267265 --- /dev/null +++ b/pkg/sdk3rd/dynv6/api_delete_record.go @@ -0,0 +1,36 @@ +package dynv6 + +import ( + "context" + "fmt" + "net/http" +) + +type DeleteRecordResponse DNSRecord + +func (c *Client) DeleteRecord(zoneID int64, recordID int64) (*DeleteRecordResponse, error) { + return c.DeleteRecordWithContext(context.Background(), zoneID, recordID) +} + +func (c *Client) DeleteRecordWithContext(ctx context.Context, zoneID int64, recordID int64) (*DeleteRecordResponse, error) { + if zoneID == 0 { + return nil, fmt.Errorf("sdkerr: unset zoneID") + } + if recordID == 0 { + return nil, fmt.Errorf("sdkerr: unset recordID") + } + + httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/zones/%d/records/%d", zoneID, recordID)) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &DeleteRecordResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/dynv6/api_list_records.go b/pkg/sdk3rd/dynv6/api_list_records.go new file mode 100644 index 000000000..7042f27d9 --- /dev/null +++ b/pkg/sdk3rd/dynv6/api_list_records.go @@ -0,0 +1,33 @@ +package dynv6 + +import ( + "context" + "fmt" + "net/http" +) + +type ListRecordsResponse []*DNSRecord + +func (c *Client) ListRecords(zoneID int64) (*ListRecordsResponse, error) { + return c.ListRecordsWithContext(context.Background(), zoneID) +} + +func (c *Client) ListRecordsWithContext(ctx context.Context, zoneID int64) (*ListRecordsResponse, error) { + if zoneID == 0 { + return nil, fmt.Errorf("sdkerr: unset zoneID") + } + + httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/zones/%d/records", zoneID)) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &ListRecordsResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/dynv6/api_list_zones.go b/pkg/sdk3rd/dynv6/api_list_zones.go new file mode 100644 index 000000000..1d51df073 --- /dev/null +++ b/pkg/sdk3rd/dynv6/api_list_zones.go @@ -0,0 +1,28 @@ +package dynv6 + +import ( + "context" + "net/http" +) + +type ListZonesResponse []*ZoneRecord + +func (c *Client) ListZones() (*ListZonesResponse, error) { + return c.ListZonesWithContext(context.Background()) +} + +func (c *Client) ListZonesWithContext(ctx context.Context) (*ListZonesResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/zones") + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &ListZonesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/dynv6/client.go b/pkg/sdk3rd/dynv6/client.go new file mode 100644 index 000000000..8ddd0edad --- /dev/null +++ b/pkg/sdk3rd/dynv6/client.go @@ -0,0 +1,89 @@ +package dynv6 + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + client *resty.Client +} + +func NewClient(httpToken string) (*Client, error) { + if httpToken == "" { + return nil, fmt.Errorf("sdkerr: unset httpToken") + } + + client := resty.New(). + SetBaseURL("https://dynv6.com/api/v2"). + SetHeader("Accept", "application/json"). + SetHeader("Authorization", "Bearer "+httpToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent) + + return &Client{client}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.client.R() + req.Method = method + req.URL = path + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + // WARN: + // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &res) + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/dynv6/types.go b/pkg/sdk3rd/dynv6/types.go new file mode 100644 index 000000000..7233862a6 --- /dev/null +++ b/pkg/sdk3rd/dynv6/types.go @@ -0,0 +1,24 @@ +package dynv6 + +type ZoneRecord struct { + ID int64 `json:"id"` + Name string `json:"name"` + IPv4Address string `json:"ipv4address"` + IPv6Prefix string `json:"ipv6prefix"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type DNSRecord struct { + ID int64 `json:"id"` + ZoneID int64 `json:"zoneID"` + Type string `json:"type"` + Name string `json:"name"` + Port int `json:"port"` + Weight int `json:"weight"` + Priority int `json:"priority"` + Data string `json:"data"` + ExpandedData string `json:"expandedData"` + Flags int `json:"flags,omitempty"` + Tag string `json:"tag,omitempty"` +}