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 xinnet
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user