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

This commit is contained in:
Fu Diwei
2026-05-27 23:51:41 +08:00
committed by RHQYZ
parent 0c3861b8c8
commit 7fce3c7c3e
9 changed files with 5 additions and 569 deletions
@@ -4,8 +4,9 @@ import (
"fmt"
"time"
"github.com/go-acme/lego/v5/providers/dns/dnsla"
"github.com/certimate-go/certimate/pkg/core/certifier"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsla/internal"
)
type ChallengerConfig struct {
@@ -20,8 +21,8 @@ func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.APIId = config.ApiId
providerConfig := dnsla.NewDefaultConfig()
providerConfig.APIID = config.ApiId
providerConfig.APISecret = config.ApiSecret
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
@@ -30,7 +31,7 @@ func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
providerConfig.TTL = config.DnsTTL
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
provider, err := dnsla.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
@@ -1,184 +0,0 @@
package internal
import (
"context"
"fmt"
"strings"
"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"
dnslasdk "github.com/certimate-go/certimate/pkg/sdk3rd/dnsla"
)
const (
envNamespace = "DNSLA_"
EnvAPIId = envNamespace + "API_ID"
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 {
APIId string
APISecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
config *Config
client *dnslasdk.Client
recordIDs map[string]string // Key: ChallengeToken; Value: RecordID
recordIDsMu sync.Mutex
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIId, EnvAPISecret)
if err != nil {
return nil, fmt.Errorf("dnsla: %w", err)
}
config := NewDefaultConfig()
config.APIId = values[EnvAPIId]
config.APISecret = values[EnvAPISecret]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, fmt.Errorf("dnsla: the configuration of the DNS provider is nil")
}
client, err := dnslasdk.NewClient(config.APIId, config.APISecret)
if err != nil {
return nil, fmt.Errorf("dnsla: %w", err)
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
config: config,
client: client,
recordIDs: make(map[string]string),
recordIDsMu: 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("dnsla: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("dnsla: %w", err)
}
zone, err := d.findZone(ctx, dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("dnsla: error when list zones: %w", err)
}
// REF: https://www.dnsla.cn/docs/ApiDoc
request := &dnslasdk.CreateRecordRequest{
DomainId: lo.ToPtr(zone.Id),
Type: lo.ToPtr(int32(16)),
Host: lo.ToPtr(subDomain),
Data: lo.ToPtr(info.Value),
Ttl: lo.ToPtr(int32(d.config.TTL)),
}
response, err := d.client.CreateRecordWithContext(ctx, request)
if err != nil {
return fmt.Errorf("dnsla: error when create record: %w", err)
}
d.recordIDsMu.Lock()
d.recordIDs[token] = response.Data.Id
d.recordIDsMu.Unlock()
return nil
}
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("dnsla: unknown record ID for '%s'", info.EffectiveFQDN)
}
// REF: https://www.dnsla.cn/docs/ApiDoc
if _, err := d.client.DeleteRecordWithContext(ctx, recordID); err != nil {
return fmt.Errorf("dnsla: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*dnslasdk.DomainRecord, error) {
pageIndex := 1
pageSize := 100
for {
// REF: https://www.dnsla.cn/docs/ApiDoc
request := &dnslasdk.ListDomainsRequest{
PageIndex: lo.ToPtr(int32(pageIndex)),
PageSize: lo.ToPtr(int32(pageSize)),
}
response, err := d.client.ListDomainsWithContext(ctx, request)
if err != nil {
return nil, err
}
if response.Data == nil {
break
}
for _, domainItem := range response.Data.Results {
if strings.TrimSuffix(domainItem.Domain, ".") == zoneName || strings.TrimSuffix(domainItem.DisplayDomain, ".") == zoneName {
return domainItem, nil
}
}
if len(response.Data.Results) < pageSize {
break
}
pageIndex++
}
return nil, fmt.Errorf("could not find zone '%s'", zoneName)
}
-46
View File
@@ -1,46 +0,0 @@
package dnsla
import (
"context"
"net/http"
)
type CreateRecordRequest struct {
DomainId *string `json:"domainId"`
GroupId *string `json:"groupId,omitempty"`
LineId *string `json:"lineId,omitempty"`
Type *int32 `json:"type"`
Host *string `json:"host"`
Data *string `json:"data"`
Ttl *int32 `json:"ttl"`
Weight *int32 `json:"weight,omitempty"`
Preference *int32 `json:"preference,omitempty"`
}
type CreateRecordResponse struct {
sdkResponseBase
Data *struct {
Id string `json:"id"`
} `json:"data,omitempty"`
}
func (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) {
return c.CreateRecordWithContext(context.Background(), req)
}
func (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/record")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &CreateRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-36
View File
@@ -1,36 +0,0 @@
package dnsla
import (
"context"
"fmt"
"net/http"
)
type DeleteRecordResponse struct {
sdkResponseBase
}
func (c *Client) DeleteRecord(recordId string) (*DeleteRecordResponse, error) {
return c.DeleteRecordWithContext(context.Background(), recordId)
}
func (c *Client) DeleteRecordWithContext(ctx context.Context, recordId string) (*DeleteRecordResponse, error) {
if recordId == "" {
return nil, fmt.Errorf("sdkerr: unset recordId")
}
httpreq, err := c.newRequest(http.MethodDelete, "/record")
if err != nil {
return nil, err
} else {
httpreq.SetQueryParam("id", recordId)
httpreq.SetContext(ctx)
}
result := &DeleteRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-48
View File
@@ -1,48 +0,0 @@
package dnsla
import (
"context"
"net/http"
qs "github.com/google/go-querystring/query"
)
type ListDomainsRequest struct {
GroupId *string `json:"groupId,omitempty" url:"groupId,omitempty"`
PageIndex *int32 `json:"pageIndex,omitempty" url:"pageIndex,omitempty"`
PageSize *int32 `json:"pageSize,omitempty" url:"pageSize,omitempty"`
}
type ListDomainsResponse struct {
sdkResponseBase
Data *struct {
Total int32 `json:"total"`
Results []*DomainRecord `json:"results"`
} `json:"data,omitempty"`
}
func (c *Client) ListDomains(req *ListDomainsRequest) (*ListDomainsResponse, error) {
return c.ListDomainsWithContext(context.Background(), req)
}
func (c *Client) ListDomainsWithContext(ctx context.Context, req *ListDomainsRequest) (*ListDomainsResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/domainList")
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &ListDomainsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-53
View File
@@ -1,53 +0,0 @@
package dnsla
import (
"context"
"net/http"
qs "github.com/google/go-querystring/query"
)
type ListRecordsRequest struct {
DomainId *string `json:"domainId,omitempty" url:"domainId,omitempty"`
GroupId *string `json:"groupId,omitempty" url:"groupId,omitempty"`
LineId *string `json:"lineId,omitempty" url:"lineId,omitempty"`
Type *int32 `json:"type,omitempty" url:"type,omitempty"`
Host *string `json:"host,omitempty" url:"host,omitempty"`
Data *string `json:"data,omitempty" url:"data,omitempty"`
PageIndex *int32 `json:"pageIndex,omitempty" url:"pageIndex,omitempty"`
PageSize *int32 `json:"pageSize,omitempty" url:"pageSize,omitempty"`
}
type ListRecordsResponse struct {
sdkResponseBase
Data *struct {
Total int32 `json:"total"`
Results []*DnsRecord `json:"results"`
} `json:"data,omitempty"`
}
func (c *Client) ListRecords(req *ListRecordsRequest) (*ListRecordsResponse, error) {
return c.ListRecordsWithContext(context.Background(), req)
}
func (c *Client) ListRecordsWithContext(ctx context.Context, req *ListRecordsRequest) (*ListRecordsResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/recordList")
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &ListRecordsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-43
View File
@@ -1,43 +0,0 @@
package dnsla
import (
"context"
"net/http"
)
type UpdateRecordRequest struct {
Id *string `json:"id"`
GroupId *string `json:"groupId,omitempty"`
LineId *string `json:"lineId,omitempty"`
Type *int32 `json:"type,omitempty"`
Host *string `json:"host,omitempty"`
Data *string `json:"data,omitempty"`
Ttl *int32 `json:"ttl,omitempty"`
Weight *int32 `json:"weight,omitempty"`
Preference *int32 `json:"preference,omitempty"`
}
type UpdateRecordResponse struct {
sdkResponseBase
}
func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
return c.UpdateRecordWithContext(context.Background(), req)
}
func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
httpreq, err := c.newRequest(http.MethodPut, "/record")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &UpdateRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-96
View File
@@ -1,96 +0,0 @@
package dnsla
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(apiId, apiSecret string) (*Client, error) {
if apiId == "" {
return nil, fmt.Errorf("sdkerr: unset apiId")
}
if apiSecret == "" {
return nil, fmt.Errorf("sdkerr: unset apiSecret")
}
client := resty.New().
SetBaseURL("https://api.dns.la/api").
SetBasicAuth(apiId, apiSecret).
SetHeader("Accept", "application/json").
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 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/100 != 2 {
return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
-59
View File
@@ -1,59 +0,0 @@
package dnsla
type sdkResponse interface {
GetCode() int
GetMessage() string
}
type sdkResponseBase struct {
Code *int `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
func (r *sdkResponseBase) GetCode() int {
if r.Code == nil {
return 0
}
return *r.Code
}
func (r *sdkResponseBase) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
var _ sdkResponse = (*sdkResponseBase)(nil)
type DomainRecord struct {
Id string `json:"id"`
GroupId string `json:"groupId"`
GroupName string `json:"groupName"`
Domain string `json:"domain"`
DisplayDomain string `json:"displayDomain"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
type DnsRecord struct {
Id string `json:"id"`
DomainId string `json:"domainId"`
GroupId string `json:"groupId"`
GroupName string `json:"groupName"`
LineId string `json:"lineId"`
LineCode string `json:"lineCode"`
LineName string `json:"lineName"`
Type int32 `json:"type"`
Host string `json:"host"`
DisplayHost string `json:"displayHost"`
Data string `json:"data"`
DisplayData string `json:"displayData"`
Ttl int32 `json:"ttl"`
Weight int32 `json:"weight"`
Preference int32 `json:"preference"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}