feat(provider): new acme dns-01 provider: qingcloud

This commit is contained in:
Fu Diwei
2025-11-15 20:23:05 +08:00
parent 614d4770e0
commit 99961aab46
29 changed files with 626 additions and 24 deletions
@@ -0,0 +1,30 @@
package applicators
import (
"fmt"
"github.com/go-acme/lego/v4/challenge"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeQingCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
credentials := domain.AccessConfigForQingCloud{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := qingcloud.NewChallengeProvider(&qingcloud.ChallengeProviderConfig{
AccessKeyId: credentials.AccessKeyId,
SecretAccessKey: credentials.SecretAccessKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return provider, err
})
ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeQingCloud, domain.ACMEDns01ProviderTypeQingCloudDNS)
}
@@ -6,7 +6,7 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/certimate-go/certimate/internal/domain"
udnr "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
@@ -17,7 +17,7 @@ func init() {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := udnr.NewChallengeProvider(&udnr.ChallengeProviderConfig{
provider, err := ucloud.NewChallengeProvider(&ucloud.ChallengeProviderConfig{
PrivateKey: credentials.PrivateKey,
PublicKey: credentials.PublicKey,
ProjectId: credentials.ProjectId,
@@ -26,4 +26,6 @@ func init() {
})
return provider, err
})
ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeUCloud, domain.ACMEDns01ProviderTypeUCloudUDNR)
}
+5
View File
@@ -396,6 +396,11 @@ type AccessConfigForProxmoxVE struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForQingCloud struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForQiniu struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
+4 -1
View File
@@ -85,7 +85,7 @@ const (
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve")
AccessProviderTypeQiniu = AccessProviderType("qiniu")
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
AccessProviderTypeQingCloud = AccessProviderType("qingcloud")
AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
AccessProviderTypeRFC2136 = AccessProviderType("rfc2136")
@@ -197,6 +197,8 @@ const (
ACMEDns01ProviderTypeOVHcloud = ACMEDns01ProviderType(AccessProviderTypeOVHcloud)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeQingCloud = ACMEDns01ProviderType(AccessProviderTypeQingCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeQingCloudDNS]
ACMEDns01ProviderTypeQingCloudDNS = ACMEDns01ProviderType(AccessProviderTypeQingCloud + "-dns")
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136)
ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship)
@@ -204,6 +206,7 @@ const (
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
ACMEDns01ProviderTypeUCloud = ACMEDns01ProviderType(AccessProviderTypeUCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeUCloudUDNR]
ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
@@ -40,8 +40,8 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 60),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
@@ -54,7 +54,7 @@ func NewDefaultConfig() *Config {
ReadTimeout: env.GetOrDefaultInt(EnvReadTimeout, 30),
ConnectTimeout: env.GetOrDefaultInt(EnvConnectTimeout, 30),
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
}
}
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
@@ -50,7 +50,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -41,7 +41,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
}
}
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -51,7 +51,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -0,0 +1,152 @@
package internal
import (
"errors"
"fmt"
"sync"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/samber/lo"
qingcloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/qingcloud/dns"
)
const (
envNamespace = "DNSLA_"
EnvAccessKey = envNamespace + "ACCESS_KEY"
EnvAccessSecret = envNamespace + "ACCESS_SECRET"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AccessKey string
AccessSecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *qingcloudsdk.Client
config *Config
recordIDs map[string]*int64
recordIDsMu 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(EnvAccessKey, EnvAccessSecret)
if err != nil {
return nil, fmt.Errorf("qingcloud: %w", err)
}
config := NewDefaultConfig()
config.AccessKey = values[EnvAccessKey]
config.AccessSecret = values[EnvAccessSecret]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("qingcloud: the configuration of the DNS provider is nil")
}
client, err := qingcloudsdk.NewClient(config.AccessKey, config.AccessSecret)
if err != nil {
return nil, err
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
client: client,
config: config,
recordIDs: make(map[string]*int64),
recordIDsMu: sync.Mutex{},
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("qingcloud: could not find zone for domain %q: %w", domain, err)
}
// REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_createrecord
qingcloudCreateRecordReq := &qingcloudsdk.CreateRecordRequest{
ZoneName: lo.ToPtr(authZone),
DomainName: lo.ToPtr(info.EffectiveFQDN),
ViewId: lo.ToPtr(int32(0)),
Type: lo.ToPtr("TXT"),
Ttl: lo.ToPtr(int32(d.config.TTL)),
Records: []*qingcloudsdk.CreateRecordRequestRecord{
{
Values: []*qingcloudsdk.CreateRecordRequestRecordValue{
{
Value: lo.ToPtr(info.Value),
Status: lo.ToPtr(int32(1)),
},
},
Weight: lo.ToPtr(int32(0)),
},
},
Mode: lo.ToPtr(int32(1)),
AutoMerge: lo.ToPtr(int32(1)),
}
qingcloudCreateRecordResp, err := d.client.CreateRecord(qingcloudCreateRecordReq)
if err != nil {
return fmt.Errorf("qingcloud: error when create record: %w", err)
}
d.recordIDsMu.Lock()
d.recordIDs[token] = qingcloudCreateRecordResp.DomainRecordId
d.recordIDsMu.Unlock()
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("qingcloud: unknown record ID for '%s'", info.EffectiveFQDN)
}
// REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_deleterecord
if _, err := d.client.DeleteRecord([]*int64{recordID}); err != nil {
return fmt.Errorf("qingcloud: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
@@ -0,0 +1,39 @@
package qingcloud
import (
"errors"
"time"
"github.com/certimate-go/certimate/pkg/core"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal"
)
type ChallengeProviderConfig struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"apiPassword"`
DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
if config == nil {
return nil, errors.New("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.AccessKey = config.AccessKeyId
providerConfig.AccessSecret = config.SecretAccessKey
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}
@@ -48,7 +48,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -57,7 +57,7 @@ func NewDefaultConfig() *Config {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvPrivateKey, EnvPublicKey, EnvProjectId)
if err != nil {
return nil, fmt.Errorf("ucloud-udnr: %w", err)
return nil, fmt.Errorf("ucloud: %w", err)
}
config := NewDefaultConfig()
@@ -70,7 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) {
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ucloud-udnr: the configuration of the DNS provider is nil")
return nil, errors.New("ucloud: the configuration of the DNS provider is nil")
}
cfg := ucloud.NewConfig()
@@ -91,7 +91,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_add
@@ -105,7 +105,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
udnrDomainDNSAddReq.SetProjectId(d.config.ProjectId)
}
if _, err := d.client.AddDomainDNS(udnrDomainDNSAddReq); err != nil {
return fmt.Errorf("ucloud-udnr: error when create record: %w", err)
return fmt.Errorf("ucloud: error when create record: %w", err)
}
return nil
@@ -116,7 +116,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_query
@@ -127,7 +127,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq)
if err != nil {
return fmt.Errorf("ucloud-udnr: error when list records: %w", err)
return fmt.Errorf("ucloud: error when list records: %w", err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_delete_dns_record
@@ -143,7 +143,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
_, err := d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
if err != nil {
return fmt.Errorf("ucloud-udnr: error when delete record: %w", err)
return fmt.Errorf("ucloud: error when delete record: %w", err)
}
break
}
@@ -1,11 +1,11 @@
package ucloududnr
package ucloud
import (
"errors"
"time"
"github.com/certimate-go/certimate/pkg/core"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/internal"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/internal"
)
type ChallengeProviderConfig struct {
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -0,0 +1,69 @@
package dns
import (
"context"
"net/http"
)
type CreateRecordRequest struct {
ZoneName *string `json:"zone_name,omitempty"`
DomainName *string `json:"domain_name,omitempty"`
ViewId *int32 `json:"view_id,omitempty"`
Type *string `json:"type,omitempty"`
Records []*CreateRecordRequestRecord `json:"record,omitempty"`
Ttl *int32 `json:"ttl,omitempty"`
Mode *int32 `json:"mode,omitempty"`
AutoMerge *int32 `json:"auto_merge,omitempty"`
}
type CreateRecordRequestRecord struct {
Values []*CreateRecordRequestRecordValue `json:"values,omitempty"`
Weight *int32 `json:"weight,omitempty"`
}
type CreateRecordRequestRecordValue struct {
Value *string `json:"value,omitempty"`
Status *int32 `json:"status,omitempty"`
}
type CreateRecordResponse struct {
apiResponseBase
DomainName *string `json:"domain_name,omitempty"`
DomainRecordId *int64 `json:"domain_record_id,omitempty"`
ViewId *int64 `json:"view_id,omitempty"`
Records []*CreateRecordResponseRecord `json:"records,omitempty"`
}
type CreateRecordResponseRecord struct {
GroupId *int64 `json:"group_id,omitempty"`
GroupStatus *int32 `json:"group_status,omitempty"`
Values []*CreateRecordResponseRecordValue `json:"value,omitempty"`
Weight *int32 `json:"weight,omitempty"`
}
type CreateRecordResponseRecordValue struct {
ValueId *int64 `json:"id,omitempty"`
Value *string `json:"value,omitempty"`
Status *int32 `json:"status,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, "/v1/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
}
@@ -0,0 +1,40 @@
package dns
import (
"context"
"fmt"
"net/http"
)
type DeleteRecordResponse struct {
apiResponseBase
}
func (c *Client) DeleteRecord(recordIds []*int64) (*DeleteRecordResponse, error) {
return c.DeleteRecordWithContext(context.Background(), recordIds)
}
func (c *Client) DeleteRecordWithContext(ctx context.Context, recordIds []*int64) (*DeleteRecordResponse, error) {
if len(recordIds) == 0 {
return nil, fmt.Errorf("sdkerr: unset recordIds")
}
httpreq, err := c.newRequest(http.MethodPost, "/v1/change_record_status/")
if err != nil {
return nil, err
} else {
httpreq.SetBody(map[string]any{
"ids": recordIds,
"action": "delete",
"target": "record",
})
httpreq.SetContext(ctx)
}
result := &DeleteRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+130
View File
@@ -0,0 +1,130 @@
package dns
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
client *resty.Client
}
func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
if accessKeyId == "" {
return nil, fmt.Errorf("sdkerr: unset accessKeyId")
}
if secretAccessKey == "" {
return nil, fmt.Errorf("sdkerr: unset secretAccessKey")
}
client := resty.New().
SetBaseURL("http://api.routewize.com").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("Host", "api.routewize.com").
SetHeader("User-Agent", "certimate").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// 生成时间
date := time.Now().UTC().Format(time.RFC1123)
// 获取请求谓词
verb := req.Method
// 获取访问资源
canonicalizedResource := "/"
if req.URL != nil {
canonicalizedResource = req.URL.Path
if req.URL.RawQuery != "" {
values, _ := url.ParseQuery(req.URL.RawQuery)
canonicalizedResource += "?" + values.Encode()
}
}
// 计算签名
stringToSign := verb + "\n" +
date + "\n" +
canonicalizedResource
h := hmac.New(sha256.New, []byte(secretAccessKey))
h.Write([]byte(stringToSign))
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
// 设置请求头
req.Header.Set("Date", date)
req.Header.Set("Authorization", fmt.Sprintf("QC-HMAC-SHA256 %s:%s", accessKeyId, sign))
return nil
})
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 apiResponse) (*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", err)
} else {
if tcode := res.GetCode(); tcode != 0 {
return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
+44
View File
@@ -0,0 +1,44 @@
package dns
type apiResponse interface {
GetCode() int
GetMessage() string
}
type apiResponseBase struct {
Code *int `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
func (r *apiResponseBase) GetCode() int {
if r.Code == nil {
return 0
}
return *r.Code
}
func (r *apiResponseBase) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
var _ apiResponse = (*apiResponseBase)(nil)
type DnsRecord struct {
GroupId *int64 `json:"group_id,omitempty"`
GroupStatus *int32 `json:"group_status,omitempty"`
Value []*DnsRecordValue `json:"value,omitempty"`
Weight *int32 `json:"weight,omitempty"`
}
type DnsRecordValue struct {
Id *int64 `json:"id,omitempty"`
Type *string `json:"type,omitempty"`
Value *string `json:"value,omitempty"`
Line *string `json:"line,omitempty"`
Ttl *int32 `json:"ttl,omitempty"`
}
+1 -2
View File
@@ -73,8 +73,7 @@ func NewClient(agentId, appSecret string) (*Client, error) {
requestMethod + "\n" +
urlPath + "\n" +
requestBody
key := []byte(appSecret)
h := hmac.New(sha256.New, key)
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(stringToSign))
signature := hex.EncodeToString(h.Sum(nil))
+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M222.293333 210.218667l0.128 0.426666v334.549334l289.109334 166.997333h0.896l289.109333-166.997333 0.085333-0.426667-0.085333-334.122667 0.085333-0.426666L512.426667 42.666667h-0.896L222.421333 209.792l-0.128 0.426667zM42.666667 981.333333h938.666666v-187.818666H42.666667V981.333333z" fill="#0FA06D"></path></svg>
@@ -72,6 +72,7 @@ import AccessConfigFieldsProviderOVHcloud from "./AccessConfigFieldsProviderOVHc
import AccessConfigFieldsProviderPorkbun from "./AccessConfigFieldsProviderPorkbun";
import AccessConfigFieldsProviderPowerDNS from "./AccessConfigFieldsProviderPowerDNS";
import AccessConfigFieldsProviderProxmoxVE from "./AccessConfigFieldsProviderProxmoxVE";
import AccessConfigFieldsProviderQingCloud from "./AccessConfigFieldsProviderQingCloud";
import AccessConfigFieldsProviderQiniu from "./AccessConfigFieldsProviderQiniu";
import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainYun";
import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel";
@@ -173,6 +174,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.PORKBUN]: AccessConfigFieldsProviderPorkbun,
[ACCESS_PROVIDERS.POWERDNS]: AccessConfigFieldsProviderPowerDNS,
[ACCESS_PROVIDERS.PROXMOXVE]: AccessConfigFieldsProviderProxmoxVE,
[ACCESS_PROVIDERS.QINGCLOUD]: AccessConfigFieldsProviderQingCloud,
[ACCESS_PROVIDERS.QINIU]: AccessConfigFieldsProviderQiniu,
[ACCESS_PROVIDERS.RAINYUN]: AccessConfigFieldsProviderRainYun,
[ACCESS_PROVIDERS.RATPANEL]: AccessConfigFieldsProviderRatPanel,
@@ -0,0 +1,64 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderQingCloud = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "accessKeyId"]}
initialValue={initialValues.accessKeyId}
label={t("access.form.qingcloud_access_key_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.qingcloud_access_key_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.qingcloud_access_key_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "secretAccessKey"]}
initialValue={initialValues.secretAccessKey}
label={t("access.form.qingcloud_secret_access_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.qingcloud_secret_access_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.qingcloud_secret_access_key.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
accessKeyId: "",
secretAccessKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
accessKeyId: z.string().nonempty(t("access.form.qingcloud_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.qingcloud_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderQingCloud, {
getInitialValues,
getSchema,
});
export default _default;
+6
View File
@@ -88,6 +88,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
PORKBUN: "porkbun",
POWERDNS: "powerdns",
PROXMOXVE: "proxmoxve",
QINGCLOUD: "qingcloud",
QINIU: "qiniu",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
@@ -213,6 +214,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VULTR, "provider.vultr", "/imgs/providers/vultr.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.QINGCLOUD, "provider.qingcloud", "/imgs/providers/qingcloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS["35CN"], "provider.35cn", "/imgs/providers/35cn.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.XINNET, "provider.xinnet", "/imgs/providers/xinnet.png", [ACCESS_USAGES.DNS]],
@@ -365,9 +367,12 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
OVHCLOUD: `${ACCESS_PROVIDERS.OVHCLOUD}`,
PORKBUN: `${ACCESS_PROVIDERS.PORKBUN}`,
POWERDNS: `${ACCESS_PROVIDERS.POWERDNS}`,
QINGCLOUD: `${ACCESS_PROVIDERS.QINGCLOUD}`, // 兼容旧值,等同于 `QINGCLOUD_DNS`
QINGCLOUD_DNS: `${ACCESS_PROVIDERS.QINGCLOUD}-dns`,
RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,
RFC2136: `${ACCESS_PROVIDERS.RFC2136}`,
SPACESHIP: `${ACCESS_PROVIDERS.SPACESHIP}`,
UCLOUD: `${ACCESS_PROVIDERS.UCLOUD}`, // 兼容旧值,等同于 `UCLOUD_UDNR`
UCLOUD_UDNR: `${ACCESS_PROVIDERS.UCLOUD}-udnr`,
TECHNITIUMDNS: `${ACCESS_PROVIDERS.TECHNITIUMDNS}`,
TENCENTCLOUD: `${ACCESS_PROVIDERS.TENCENTCLOUD}`, // 兼容旧值,等同于 `TENCENTCLOUD_DNS`
@@ -441,6 +446,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.CTCCCLOUD_SMARTDNS, "provider.ctcccloud.smartdns"],
[ACME_DNS01_PROVIDERS.RAINYUN, "provider.rainyun"],
[ACME_DNS01_PROVIDERS.UCLOUD_UDNR, "provider.ucloud.udnr"],
[ACME_DNS01_PROVIDERS.QINGCLOUD_DNS, "provider.qingcloud.dns"],
[ACME_DNS01_PROVIDERS.WESTCN, "provider.westcn"],
[ACME_DNS01_PROVIDERS["35CN"], "provider.35cn"],
[ACME_DNS01_PROVIDERS.XINNET, "provider.xinnet"],
+6
View File
@@ -471,6 +471,12 @@
"access.form.proxmoxve_api_token_secret.label": "Proxmox VE API token secret (Optional)",
"access.form.proxmoxve_api_token_secret.placeholder": "Please enter Proxmox VE API token secret",
"access.form.proxmoxve_api_token_secret.tooltip": "For more information, see <a href=\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\" target=\"_blank\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>",
"access.form.qingcloud_access_key_id.label": "QingCloud AccessKeyID",
"access.form.qingcloud_access_key_id.placeholder": "Please enter QingCloud AccessKeyID",
"access.form.qingcloud_access_key_id.tooltip": "For more information, see <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
"access.form.qingcloud_secret_access_key.label": "QingCloud SecretAccessKey",
"access.form.qingcloud_secret_access_key.placeholder": "Please enter QingCloud SecretAccessKey",
"access.form.qingcloud_secret_access_key.tooltip": "For more information, see <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
"access.form.qiniu_access_key.label": "Qiniu AccessKey",
"access.form.qiniu_access_key.placeholder": "Please enter Qiniu AccessKey",
"access.form.qiniu_access_key.tooltip": "For more information, see <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>",
+2
View File
@@ -139,6 +139,8 @@
"provider.porkbun": "Porkbun",
"provider.powerdns": "PowerDNS",
"provider.proxmoxve": "Proxmox VE",
"provider.qingcloud": "QingCloud",
"provider.qingcloud.dns": "QingCloud - DNS",
"provider.qiniu": "Qiniu",
"provider.qiniu.cdn": "Qiniu - CDN (Content Delivery Network)",
"provider.qiniu.kodo": "Qiniu - Kodo",
+6
View File
@@ -470,6 +470,12 @@
"access.form.proxmoxve_api_token_secret.label": "Proxmox VE API Token Secret(可选)",
"access.form.proxmoxve_api_token_secret.placeholder": "请输入 Proxmox VE API Token Secret",
"access.form.proxmoxve_api_token_secret.tooltip": "这是什么?请参阅 <a href=\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\" target=\"_blank\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>",
"access.form.qingcloud_access_key_id.label": "青云 AccessKeyID",
"access.form.qingcloud_access_key_id.placeholder": "请输入青云 AccessKeyID",
"access.form.qingcloud_access_key_id.tooltip": "这是什么?请参阅 <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
"access.form.qingcloud_secret_access_key.label": "青云 SecretAccessKey",
"access.form.qingcloud_secret_access_key.placeholder": "请输入青云 SecretAccessKey",
"access.form.qingcloud_secret_access_key.tooltip": "这是什么?请参阅 <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
"access.form.qiniu_access_key.label": "七牛云 AccessKey",
"access.form.qiniu_access_key.placeholder": "请输入七牛云 AccessKey",
"access.form.qiniu_access_key.tooltip": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>",
+2
View File
@@ -139,6 +139,8 @@
"provider.porkbun": "Porkbun",
"provider.powerdns": "PowerDNS",
"provider.proxmoxve": "Proxmox VE",
"provider.qingcloud": "青云",
"provider.qingcloud.dns": "青云 - 云解析 DNS",
"provider.qiniu": "七牛云",
"provider.qiniu.cdn": "七牛云 - 内容分发网络 CDN",
"provider.qiniu.kodo": "七牛云 - 对象存储 Kodo",