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

This commit is contained in:
Fu Diwei
2026-05-27 23:51:41 +08:00
committed by RHQYZ
parent 0ede750917
commit 4d1fb385f6
6 changed files with 5 additions and 410 deletions
@@ -1,149 +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"
xinnetsdk "github.com/certimate-go/certimate/pkg/sdk3rd/xinnet"
)
const (
envNamespace = "XINNET_"
EnvAgentId = envNamespace + "AGENT_ID"
EnvAppSecret = envNamespace + "APP_SECRET"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AgentID string
AppSecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
config *Config
client *xinnetsdk.Client
recordIDs map[string]*int64 // Key: ChallengeToken; Value: RecordID
recordIDsMu sync.Mutex
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAgentId, EnvAppSecret)
if err != nil {
return nil, fmt.Errorf("xinnet: %w", err)
}
config := NewDefaultConfig()
config.AgentID = values[EnvAgentId]
config.AppSecret = values[EnvAppSecret]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, fmt.Errorf("xinnet: the configuration of the DNS provider is nil")
}
client, err := xinnetsdk.NewClient(config.AgentID, config.AppSecret)
if err != nil {
return nil, fmt.Errorf("xinnet: %w", err)
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
config: config,
client: client,
recordIDs: make(map[string]*int64),
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("xinnet: could not find zone for domain %q: %w", domain, err)
}
// REF: https://apidoc.xin.cn/doc-7283900
request := &xinnetsdk.DnsCreateRequest{
DomainName: lo.ToPtr(dns01.UnFqdn(authZone)),
RecordName: lo.ToPtr(dns01.UnFqdn(info.EffectiveFQDN)),
Type: lo.ToPtr("TXT"),
Value: lo.ToPtr(info.Value),
Line: lo.ToPtr("默认"),
Ttl: lo.ToPtr(int32(d.config.TTL)),
}
response, err := d.client.DnsCreateWithContext(ctx, request)
if err != nil {
return fmt.Errorf("xinnet: error when create record: %w", err)
}
d.recordIDsMu.Lock()
d.recordIDs[token] = response.Data
d.recordIDsMu.Unlock()
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("xinnet: could not find zone for domain %q: %w", domain, err)
}
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("xinnet: unknown record ID for '%s'", info.EffectiveFQDN)
}
// REF: https://apidoc.xin.cn/doc-7283901
request := &xinnetsdk.DnsDeleteRequest{
DomainName: lo.ToPtr(dns01.UnFqdn(authZone)),
RecordId: recordID,
}
if _, err := d.client.DnsDeleteWithContext(ctx, request); err != nil {
return fmt.Errorf("xinnet: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
@@ -4,8 +4,9 @@ import (
"fmt"
"time"
"github.com/go-acme/lego/v5/providers/dns/xinnet"
"github.com/certimate-go/certimate/pkg/core/certifier"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/xinnet/internal"
)
type ChallengerConfig struct {
@@ -20,9 +21,9 @@ func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig := xinnet.NewDefaultConfig()
providerConfig.AgentID = config.AgentId
providerConfig.AppSecret = config.ApiPassword
providerConfig.Secret = config.ApiPassword
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 := xinnet.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
-42
View File
@@ -1,42 +0,0 @@
package xinnet
import (
"context"
)
type DnsCreateRequest struct {
DomainName *string `json:"domainName,omitempty"`
RecordName *string `json:"recordName,omitempty"`
Type *string `json:"type,omitempty"`
Value *string `json:"value,omitempty"`
Line *string `json:"line,omitempty"`
Ttl *int32 `json:"ttl,omitempty"`
Mx *int32 `json:"mx,omitempty"`
Status *int32 `json:"status,omitempty"`
}
type DnsCreateResponse struct {
sdkResponseBase
Data *int64 `json:"data,omitempty"`
}
func (c *Client) DnsCreate(req *DnsCreateRequest) (*DnsCreateResponse, error) {
return c.DnsCreateWithContext(context.Background(), req)
}
func (c *Client) DnsCreateWithContext(ctx context.Context, req *DnsCreateRequest) (*DnsCreateResponse, error) {
httpreq, err := c.newRequest("/dns/create/")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &DnsCreateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-35
View File
@@ -1,35 +0,0 @@
package xinnet
import (
"context"
)
type DnsDeleteRequest struct {
DomainName *string `json:"domainName,omitempty"`
RecordId *int64 `json:"recordId,omitempty"`
}
type DnsDeleteResponse struct {
sdkResponseBase
}
func (c *Client) DnsDelete(req *DnsDeleteRequest) (*DnsDeleteResponse, error) {
return c.DnsDeleteWithContext(context.Background(), req)
}
func (c *Client) DnsDeleteWithContext(ctx context.Context, req *DnsDeleteRequest) (*DnsDeleteResponse, error) {
httpreq, err := c.newRequest("/dns/delete/")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &DnsDeleteResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
-150
View File
@@ -1,150 +0,0 @@
package xinnet
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
client *resty.Client
}
func NewClient(agentId, appSecret string) (*Client, error) {
if agentId == "" {
return nil, fmt.Errorf("sdkerr: unset agentId")
}
if appSecret == "" {
return nil, fmt.Errorf("sdkerr: unset appSecret")
}
client := resty.New().
SetBaseURL("https://apiv2.xinnet.com/api").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// 生成时间戳
timestamp := time.Now().UTC().Format("20060102T150405Z")
// 获取请求路径,注意结尾必须是 "/"
urlPath := "/"
if req.URL != nil {
urlPath = req.URL.Path
if !strings.HasSuffix(urlPath, "/") {
urlPath += "/"
}
}
// 获取请求方法
requestMethod := req.Method
// 获取请求体
requestBody := ""
if req.Body != nil {
reader, err := req.GetBody()
if err != nil {
return err
}
defer reader.Close()
payloadb, err := io.ReadAll(reader)
if err != nil {
return err
}
requestBody = string(payloadb)
}
// 计算签名
algorithm := "HMAC-SHA256"
stringToSign := algorithm + "\n" +
timestamp + "\n" +
requestMethod + "\n" +
urlPath + "\n" +
requestBody
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(stringToSign))
signature := hex.EncodeToString(h.Sum(nil))
// 设置请求头
req.Header.Set("timestamp", timestamp)
req.Header.Set("authorization", fmt.Sprintf("%s Access=%s, Signature=%s", algorithm, agentId, signature))
return nil
})
return &Client{client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(path string) (*resty.Request, error) {
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
req := c.client.R()
req.Method = http.MethodPost
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 != "0" {
return resp, fmt.Errorf("sdkerr: code='%s', msg='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
-30
View File
@@ -1,30 +0,0 @@
package xinnet
type sdkResponse interface {
GetCode() string
GetMessage() string
}
type sdkResponseBase struct {
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
RequestId *string `json:"requestId,omitempty"`
}
func (r *sdkResponseBase) GetCode() string {
if r.Code == nil {
return ""
}
return *r.Code
}
func (r *sdkResponseBase) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
var _ sdkResponse = (*sdkResponseBase)(nil)