refactor(provider): use lego to implement dns-01 challenger of 51dnscom

This commit is contained in:
Fu Diwei
2026-05-27 23:51:41 +08:00
committed by RHQYZ
parent 4824a23492
commit 0c3861b8c8
7 changed files with 4 additions and 512 deletions
@@ -4,8 +4,9 @@ import (
"fmt"
"time"
"github.com/go-acme/lego/v5/providers/dns/dns51"
"github.com/certimate-go/certimate/pkg/core/certifier"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/51dnscom/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 := dns51.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 := dns51.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
@@ -1,191 +0,0 @@
package internal
import (
"context"
"fmt"
"sync"
"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"
dnscomsdk "github.com/certimate-go/certimate/pkg/sdk3rd/51dnscom"
)
const (
envNamespace = "51DNSCOM_"
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 *dnscomsdk.Client
recordCache map[string]dnsRecordCacheEntry // Key: ChallengeToken
recordCacheMu sync.Mutex
}
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("51dnscom: %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("51dnscom: the configuration of the DNS provider is nil")
}
client, err := dnscomsdk.NewClient(config.APIKey, config.APISecret)
if err != nil {
return nil, fmt.Errorf("51dnscom: %w", err)
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
config: config,
client: client,
recordCache: make(map[string]dnsRecordCacheEntry),
recordCacheMu: sync.Mutex{},
}, 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("51dnscom: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("51dnscom: %w", err)
}
zone, err := d.findZone(ctx, dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("51dnscom: error when list zones: %w", err)
}
// REF: https://www.51dns.com/document/api/4/12.html
request := &dnscomsdk.RecordCreateRequest{
DomainID: lo.ToPtr(zone.DomainID.String()),
Type: lo.ToPtr("TXT"),
Host: lo.ToPtr(subDomain),
Value: lo.ToPtr(info.Value),
TTL: lo.ToPtr(int32(d.config.TTL)),
}
response, err := d.client.RecordCreateWithContext(ctx, request)
if err != nil {
return fmt.Errorf("51dnscom: error when create record: %w", err)
}
d.recordCacheMu.Lock()
d.recordCache[token] = dnsRecordCacheEntry{DomainID: zone.DomainID.String(), RecordID: response.Data.RecordID.String()}
d.recordCacheMu.Unlock()
return nil
}
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
d.recordCacheMu.Lock()
record, ok := d.recordCache[token]
d.recordCacheMu.Unlock()
if !ok {
return fmt.Errorf("51dnscom: unknown record ID for '%s'", info.EffectiveFQDN)
}
// REF: https://www.51dns.com/document/api/4/27.html
request := &dnscomsdk.RecordRemoveRequest{
DomainID: lo.ToPtr(record.DomainID),
RecordID: lo.ToPtr(record.RecordID),
}
if _, err := d.client.RecordRemoveWithContext(ctx, request); err != nil {
return fmt.Errorf("51dnscom: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
type dnsRecordCacheEntry struct {
DomainID string
RecordID string
}
func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*dnscomsdk.DomainRecord, error) {
page := 1
pageSize := 10
for {
// REF: https://www.51dns.com/document/api/74/88.html
request := &dnscomsdk.DomainListRequest{
Page: lo.ToPtr(int32(page)),
PageSize: lo.ToPtr(int32(pageSize)),
}
response, err := d.client.DomainListWithContext(ctx, request)
if err != nil {
return nil, err
}
if response.Data == nil {
break
}
for _, domainItem := range response.Data.Data {
if domainItem.Domain == zoneName {
return domainItem, nil
}
}
if len(response.Data.Data) < pageSize || response.Data.PageCount <= int32(page) {
break
}
page++
}
return nil, fmt.Errorf("could not find zone '%s'", zoneName)
}
-43
View File
@@ -1,43 +0,0 @@
package dnscom
import (
"context"
"net/http"
)
type DomainListRequest struct {
GroupID *string `json:"groupID,omitempty"`
Page *int32 `json:"page,omitempty"`
PageSize *int32 `json:"pageSize,omitempty"`
}
type DomainListResponse struct {
sdkResponseBase
Data *struct {
Data []*DomainRecord `json:"data"`
Page int32 `json:"page"`
PageSize int32 `json:"pageSize"`
PageCount int32 `json:"pageCount"`
} `json:"data"`
}
func (c *Client) DomainList(req *DomainListRequest) (*DomainListResponse, error) {
return c.DomainListWithContext(context.Background(), req)
}
func (c *Client) DomainListWithContext(ctx context.Context, req *DomainListRequest) (*DomainListResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/domain/list/", req)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &DomainListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-43
View File
@@ -1,43 +0,0 @@
package dnscom
import (
"context"
"net/http"
)
type RecordCreateRequest struct {
DomainID *string `json:"domainID,omitempty"`
ViewID *string `json:"viewID,omitempty"`
Type *string `json:"type,omitempty"`
Host *string `json:"host,omitempty"`
Value *string `json:"value,omitempty"`
TTL *int32 `json:"ttl,omitempty"`
MX *int32 `json:"mx,omitempty"`
Remark *string `json:"remark,omitempty"`
}
type RecordCreateResponse struct {
sdkResponseBase
Data *DNSRecord `json:"data"`
}
func (c *Client) RecordCreate(req *RecordCreateRequest) (*RecordCreateResponse, error) {
return c.RecordCreateWithContext(context.Background(), req)
}
func (c *Client) RecordCreateWithContext(ctx context.Context, req *RecordCreateRequest) (*RecordCreateResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/record/create/", req)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &RecordCreateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-35
View File
@@ -1,35 +0,0 @@
package dnscom
import (
"context"
"net/http"
)
type RecordRemoveRequest struct {
DomainID *string `json:"domainID,omitempty"`
RecordID *string `json:"recordID,omitempty"`
}
type RecordRemoveResponse struct {
sdkResponseBase
}
func (c *Client) RecordRemove(req *RecordRemoveRequest) (*RecordRemoveResponse, error) {
return c.RecordRemoveWithContext(context.Background(), req)
}
func (c *Client) RecordRemoveWithContext(ctx context.Context, req *RecordRemoveRequest) (*RecordRemoveResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/record/remove/", req)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &RecordRemoveResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-147
View File
@@ -1,147 +0,0 @@
package dnscom
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"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://www.51dns.com/api").
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")
}
data := make(map[string]string)
if params != nil {
temp := make(map[string]any)
jsonb, _ := json.Marshal(params)
json.Unmarshal(jsonb, &temp)
for k, v := range temp {
if v == nil {
continue
}
data[k] = fmt.Sprintf("%v", v)
}
}
data["apiKey"] = c.apiKey
data["timestamp"] = fmt.Sprintf("%d", time.Now().Unix())
data["hash"] = generateHash(data, c.apiSecret)
req := c.client.R()
req.Method = method
req.URL = path
req.SetBody(data)
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 {
if tcode := res.GetCode(); tcode != 0 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
func generateHash(params map[string]string, secert string) string {
var keyList []string
for k := range params {
keyList = append(keyList, k)
}
sort.Strings(keyList)
var hashString string
for _, key := range keyList {
if hashString == "" {
hashString += key + "=" + params[key]
} else {
hashString += "&" + key + "=" + params[key]
}
}
m := md5.New()
m.Write([]byte(hashString + secert))
cipherStr := m.Sum(nil)
return hex.EncodeToString(cipherStr)
}
-50
View File
@@ -1,50 +0,0 @@
package dnscom
import (
"encoding/json"
)
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 DomainRecord struct {
GroupID json.Number `json:"groupID"`
DomainID json.Number `json:"domainsID"`
Domain string `json:"domains"`
State int32 `json:"state"`
UserLockState int32 `json:"userLock"`
AdminLockState int32 `json:"adminLock"`
HealthState int32 `json:"healthState"`
ViewType string `json:"view_type"`
}
type DNSRecord struct {
DomainID json.Number `json:"domainID"`
RecordID json.Number `json:"recordID"`
ViewID json.Number `json:"viewID"`
Record string `json:"record"`
Type string `json:"type"`
Host string `json:"host"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
MX int32 `json:"mx"`
State int32 `json:"state"`
Remark string `json:"remark"`
}