mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
refactor(provider): use lego to implement dns-01 challenger of 51dnscom
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user