From 51dfdf55bc4a5398c5b4b445e194df3f5cbea6bb Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 27 May 2026 23:47:14 +0800 Subject: [PATCH] refactor(provider): use lego to implement dns-01 challenger of dynadot --- .../challengers/dns01/dynadot/dynadot.go | 7 +- .../dns01/dynadot/internal/lego.go | 155 ------------------ pkg/sdk3rd/dynadot/api_remove_dns.go | 40 ----- pkg/sdk3rd/dynadot/api_set_dns.go | 42 ----- pkg/sdk3rd/dynadot/client.go | 149 ----------------- pkg/sdk3rd/dynadot/types.go | 34 ---- 6 files changed, 4 insertions(+), 423 deletions(-) delete mode 100644 pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go delete mode 100644 pkg/sdk3rd/dynadot/api_remove_dns.go delete mode 100644 pkg/sdk3rd/dynadot/api_set_dns.go delete mode 100644 pkg/sdk3rd/dynadot/client.go delete mode 100644 pkg/sdk3rd/dynadot/types.go diff --git a/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go b/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go index 0b124b3c9..cbe493af4 100644 --- a/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go +++ b/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go @@ -4,8 +4,9 @@ import ( "fmt" "time" + "github.com/go-acme/lego/v5/providers/dns/dynadot" + "github.com/certimate-go/certimate/pkg/core/certifier" - "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynadot/internal" ) type ChallengerConfig struct { @@ -20,7 +21,7 @@ func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { return nil, fmt.Errorf("the configuration of the acme challenge provider is nil") } - providerConfig := internal.NewDefaultConfig() + providerConfig := dynadot.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { @@ -30,7 +31,7 @@ func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { providerConfig.TTL = config.DnsTTL } - provider, err := internal.NewDNSProviderConfig(providerConfig) + provider, err := dynadot.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } diff --git a/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go b/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go deleted file mode 100644 index 7066147cc..000000000 --- a/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go +++ /dev/null @@ -1,155 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "time" - - "github.com/go-acme/lego/v5/challenge" - "github.com/go-acme/lego/v5/challenge/dns01" - "github.com/go-acme/lego/v5/platform/env" - "github.com/samber/lo" - - dynadotsdk "github.com/certimate-go/certimate/pkg/sdk3rd/dynadot" -) - -const ( - envNamespace = "DYNADOT_" - - EnvAPIKey = envNamespace + "API_KEY" - EnvAPISecret = envNamespace + "API_SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -type Config struct { - APIKey string - APISecret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPTimeout time.Duration -} - -type DNSProvider struct { - config *Config - client *dynadotsdk.Client -} - -type dnsRecordCacheEntry struct { - Zone string - SubHost string - Value string -} - -func NewDefaultConfig() *Config { - return &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), - } -} - -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey, EnvAPISecret) - if err != nil { - return nil, fmt.Errorf("dynadot: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - config.APISecret = values[EnvAPISecret] - - return NewDNSProviderConfig(config) -} - -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, fmt.Errorf("dynadot: the configuration of the DNS provider is nil") - } - - client, err := dynadotsdk.NewClient(config.APIKey, config.APISecret) - if err != nil { - return nil, fmt.Errorf("dynadot: %w", err) - } else { - client.SetTimeout(config.HTTPTimeout) - } - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(ctx, domain, keyAuth) - - authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("dynadot: %w", err) - } - - // REF: https://www.dynadot.com/domain/api-document (set_dns) - request := &dynadotsdk.SetDnsRequest{ - SubList: []*dynadotsdk.DnsSubRecord{ - { - SubHost: subDomain, - RecordType: "TXT", - RecordValue1: info.Value, - }, - }, - TTL: lo.ToPtr(int64(d.config.TTL)), - AddDnsToCurrentSetting: lo.ToPtr(true), - } - if _, err := d.client.SetDnsWithContext(ctx, dns01.UnFqdn(authZone), request); err != nil { - return fmt.Errorf("dynadot: error when create record: %w", err) - } - - return nil -} - -func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(ctx, domain, keyAuth) - - authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("dynadot: %w", err) - } - - // REF: https://www.dynadot.com/domain/api-document (remove_dns) - request := &dynadotsdk.RemoveDnsRequest{ - SubList: []*dynadotsdk.DnsSubRecord{ - { - SubHost: subDomain, - RecordType: "TXT", - RecordValue1: info.Value, - }, - }, - } - if _, err := d.client.RemoveDnsWithContext(ctx, dns01.UnFqdn(authZone), request); err != nil { - return fmt.Errorf("dynadot: error when delete record: %w", err) - } - - return nil -} - -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/pkg/sdk3rd/dynadot/api_remove_dns.go b/pkg/sdk3rd/dynadot/api_remove_dns.go deleted file mode 100644 index adc975676..000000000 --- a/pkg/sdk3rd/dynadot/api_remove_dns.go +++ /dev/null @@ -1,40 +0,0 @@ -package dynadot - -import ( - "context" - "fmt" - "net/http" -) - -type RemoveDnsRequest struct { - DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"` - SubList []*DnsSubRecord `json:"sub_list,omitempty"` -} - -type RemoveDnsResponse struct { - sdkResponseBase -} - -func (c *Client) RemoveDns(domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) { - return c.RemoveDnsWithContext(context.Background(), domain, req) -} - -func (c *Client) RemoveDnsWithContext(ctx context.Context, domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) { - if domain == "" { - return nil, fmt.Errorf("sdkerr: unset domain") - } - - httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req) - if err != nil { - return nil, err - } else { - httpreq.SetContext(ctx) - } - - result := &RemoveDnsResponse{} - if _, err := c.doRequestWithResult(httpreq, result); err != nil { - return result, err - } - - return result, nil -} diff --git a/pkg/sdk3rd/dynadot/api_set_dns.go b/pkg/sdk3rd/dynadot/api_set_dns.go deleted file mode 100644 index 7fcb4e51a..000000000 --- a/pkg/sdk3rd/dynadot/api_set_dns.go +++ /dev/null @@ -1,42 +0,0 @@ -package dynadot - -import ( - "context" - "fmt" - "net/http" -) - -type SetDnsRequest struct { - DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"` - SubList []*DnsSubRecord `json:"sub_list,omitempty"` - TTL *int64 `json:"ttl,omitempty"` - AddDnsToCurrentSetting *bool `json:"add_dns_to_current_setting,omitempty"` -} - -type SetDnsResponse struct { - sdkResponseBase -} - -func (c *Client) SetDns(domain string, req *SetDnsRequest) (*SetDnsResponse, error) { - return c.SetDnsWithContext(context.Background(), domain, req) -} - -func (c *Client) SetDnsWithContext(ctx context.Context, domain string, req *SetDnsRequest) (*SetDnsResponse, error) { - if domain == "" { - return nil, fmt.Errorf("sdkerr: unset domain") - } - - httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req) - if err != nil { - return nil, err - } else { - httpreq.SetContext(ctx) - } - - result := &SetDnsResponse{} - if _, err := c.doRequestWithResult(httpreq, result); err != nil { - return result, err - } - - return result, nil -} diff --git a/pkg/sdk3rd/dynadot/client.go b/pkg/sdk3rd/dynadot/client.go deleted file mode 100644 index 754b898df..000000000 --- a/pkg/sdk3rd/dynadot/client.go +++ /dev/null @@ -1,149 +0,0 @@ -package dynadot - -import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/go-resty/resty/v2" - - "github.com/certimate-go/certimate/internal/app" -) - -type Client struct { - apiKey string - apiSecret string - - client *resty.Client -} - -func NewClient(apiKey, apiSecret string) (*Client, error) { - if apiKey == "" { - return nil, fmt.Errorf("sdkerr: unset apiKey") - } - if apiSecret == "" { - return nil, fmt.Errorf("sdkerr: unset apiSecret") - } - - client := resty.New(). - SetBaseURL("https://api.dynadot.com"). - SetHeader("Accept", "application/json"). - SetHeader("Content-Type", "application/json"). - SetHeader("User-Agent", app.AppUserAgent) - - return &Client{ - apiKey: apiKey, - apiSecret: apiSecret, - 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, params any) (*resty.Request, error) { - if method == "" { - return nil, fmt.Errorf("sdkerr: unset method") - } - if path == "" { - return nil, fmt.Errorf("sdkerr: unset path") - } - - var body string - if params != nil { - jsonb, err := json.Marshal(params) - if err != nil { - return nil, fmt.Errorf("sdkerr: failed to marshal request body: %w", err) - } - body = string(jsonb) - } - - req := c.client.R() - req.Method = method - req.URL = path - req.SetHeader("Authorization", "Bearer "+c.apiKey) - req.SetHeader("X-Signature", generateSignature(c.apiKey, c.apiSecret, path, "", body)) - if body != "" { - req.SetBody(body) - } - 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.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD. - // 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 sdkResponse) (*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()) - } else { - // NOTE: Unlike most providers in this project (which use `0` for success), - // Dynadot mirrors HTTP-style status codes inside the response body and - // always returns `200` on success. - // REF: https://www.dynadot.com/domain/api-document - if tcode := res.GetCode(); tcode != 200 { - return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage()) - } - } - } - - return resp, nil -} - -// generateSignature builds the X-Signature header value as required by the -// Dynadot RESTful v2 API. It signs the concatenation of: -// -// apiKey + "\n" + fullPathAndQuery + "\n" + xRequestId + "\n" + requestBody -// -// using HMAC-SHA256 with the API secret as key, then encodes the result with -// standard Base64. -// -// REF: https://www.dynadot.com/domain/api-document -func generateSignature(apiKey, apiSecret, fullPathAndQuery, xRequestId, requestBody string) string { - var sb strings.Builder - sb.WriteString(apiKey) - sb.WriteString("\n") - sb.WriteString(fullPathAndQuery) - sb.WriteString("\n") - sb.WriteString(xRequestId) - sb.WriteString("\n") - sb.WriteString(requestBody) - - mac := hmac.New(sha256.New, []byte(apiSecret)) - mac.Write([]byte(sb.String())) - return base64.StdEncoding.EncodeToString(mac.Sum(nil)) -} diff --git a/pkg/sdk3rd/dynadot/types.go b/pkg/sdk3rd/dynadot/types.go deleted file mode 100644 index 56738b4a1..000000000 --- a/pkg/sdk3rd/dynadot/types.go +++ /dev/null @@ -1,34 +0,0 @@ -package dynadot - -type sdkResponse interface { - GetCode() int - GetMessage() string -} - -type sdkResponseBase struct { - Code int `json:"code"` - Message string `json:"message"` -} - -func (r *sdkResponseBase) GetCode() int { - return r.Code -} - -func (r *sdkResponseBase) GetMessage() string { - return r.Message -} - -var _ sdkResponse = (*sdkResponseBase)(nil) - -type DnsMainRecord struct { - RecordType string `json:"record_type"` - RecordValue1 string `json:"record_value1"` - RecordValue2 string `json:"record_value2,omitempty"` -} - -type DnsSubRecord struct { - SubHost string `json:"sub_host"` - RecordType string `json:"record_type"` - RecordValue1 string `json:"record_value1"` - RecordValue2 string `json:"record_value2,omitempty"` -}