diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index db49a00e..177b479e 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -2086,6 +2086,7 @@ func displayDNSHelp(name string) error { ew.writeln(` - "TENCENTCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "TENCENTCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "TENCENTCLOUD_REGION": Region`) + ew.writeln(` - "TENCENTCLOUD_SESSION_TOKEN": Access Key token`) ew.writeln(` - "TENCENTCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() diff --git a/docs/content/dns/zz_gen_auroradns.md b/docs/content/dns/zz_gen_auroradns.md index b03ea30f..326e6113 100644 --- a/docs/content/dns/zz_gen_auroradns.md +++ b/docs/content/dns/zz_gen_auroradns.md @@ -27,7 +27,7 @@ Here is an example bash command using the Aurora DNS provider: ```bash AURORA_API_KEY=xxxxx \ -AURORA_SECRET_KEY=yyyyyy \ +AURORA_SECRET=yyyyyy \ lego --email you@example.com --dns auroradns --domains my.example.org run ``` diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md index b2c5c3d6..993100a6 100644 --- a/docs/content/dns/zz_gen_tencentcloud.md +++ b/docs/content/dns/zz_gen_tencentcloud.md @@ -53,6 +53,7 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}). | `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `TENCENTCLOUD_REGION` | Region | +| `TENCENTCLOUD_SESSION_TOKEN` | Access Key token | | `TENCENTCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. diff --git a/providers/dns/tencentcloud/client.go b/providers/dns/tencentcloud/client.go index 1259cef8..83dc8086 100644 --- a/providers/dns/tencentcloud/client.go +++ b/providers/dns/tencentcloud/client.go @@ -1,70 +1,92 @@ package tencentcloud import ( + "errors" + "fmt" "strings" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + errorsdk "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + "golang.org/x/net/idna" ) -type domainData struct { - domain string - subDomain string -} +func (d *DNSProvider) getHostedZone(domain string) (*dnspod.DomainListItem, error) { + request := dnspod.NewDescribeDomainListRequest() -func getDomainData(fqdn string) (*domainData, error) { - zone, err := dns01.FindZoneByFqdn(fqdn) + var domains []*dnspod.DomainListItem + + for { + response, err := d.client.DescribeDomainList(request) + if err != nil { + return nil, fmt.Errorf("API call failed: %w", err) + } + + domains = append(domains, response.Response.DomainList...) + + if uint64(len(domains)) >= *response.Response.DomainCountInfo.AllTotal { + break + } + + request.Offset = common.Int64Ptr(int64(len(domains))) + } + + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return nil, err } - return &domainData{ - domain: dns01.UnFqdn(zone), - subDomain: dns01.UnFqdn(strings.TrimSuffix(fqdn, zone)), - }, nil -} - -func (d *DNSProvider) createRecordData(domainData *domainData, value string) error { - request := dnspod.NewCreateRecordRequest() - request.Domain = common.StringPtr(domainData.domain) - request.SubDomain = common.StringPtr(domainData.subDomain) - request.RecordType = common.StringPtr("TXT") - request.RecordLine = common.StringPtr("默认") - request.Value = common.StringPtr(value) - request.TTL = common.Uint64Ptr(uint64(d.config.TTL)) - - _, err := d.client.CreateRecord(request) - if err != nil { - return err + var hostedZone *dnspod.DomainListItem + for _, zone := range domains { + if *zone.Name == dns01.UnFqdn(authZone) { + hostedZone = zone + } } - return nil + if hostedZone == nil { + return nil, fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) + } + + return hostedZone, nil } -func (d *DNSProvider) listRecordData(domainData *domainData) ([]*dnspod.RecordListItem, error) { +func (d *DNSProvider) findTxtRecords(zone *dnspod.DomainListItem, fqdn string) ([]*dnspod.RecordListItem, error) { + recordName, err := extractRecordName(fqdn, *zone.Name) + if err != nil { + return nil, err + } + request := dnspod.NewDescribeRecordListRequest() - request.Domain = common.StringPtr(domainData.domain) - request.Subdomain = common.StringPtr(domainData.subDomain) + request.Domain = zone.Name + request.DomainId = zone.DomainId + request.Subdomain = common.StringPtr(recordName) request.RecordType = common.StringPtr("TXT") + request.RecordLine = common.StringPtr("默认") response, err := d.client.DescribeRecordList(request) if err != nil { + var sdkError *errorsdk.TencentCloudSDKError + if errors.As(err, &sdkError) { + if sdkError.Code == dnspod.RESOURCENOTFOUND_NODATAOFRECORD { + return nil, nil + } + } return nil, err } return response.Response.RecordList, nil } -func (d *DNSProvider) deleteRecordData(domainData *domainData, item *dnspod.RecordListItem) error { - request := dnspod.NewDeleteRecordRequest() - request.Domain = common.StringPtr(domainData.domain) - request.RecordId = item.RecordId - - _, err := d.client.DeleteRecord(request) +func extractRecordName(fqdn, zone string) (string, error) { + asciiDomain, err := idna.ToASCII(zone) if err != nil { - return err + return "", fmt.Errorf("fail to convert punycode: %w", err) } - return nil + name := dns01.UnFqdn(fqdn) + if idx := strings.Index(name, "."+asciiDomain); idx != -1 { + return name[:idx], nil + } + return name, nil } diff --git a/providers/dns/tencentcloud/tencentcloud.go b/providers/dns/tencentcloud/tencentcloud.go index 3cffe770..86f64939 100644 --- a/providers/dns/tencentcloud/tencentcloud.go +++ b/providers/dns/tencentcloud/tencentcloud.go @@ -4,6 +4,7 @@ package tencentcloud import ( "errors" "fmt" + "math" "time" "github.com/go-acme/lego/v4/challenge/dns01" @@ -17,9 +18,10 @@ import ( const ( envNamespace = "TENCENTCLOUD_" - EnvSecretID = envNamespace + "SECRET_ID" - EnvSecretKey = envNamespace + "SECRET_KEY" - EnvRegion = envNamespace + "REGION" + EnvSecretID = envNamespace + "SECRET_ID" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvRegion = envNamespace + "REGION" + EnvSessionToken = envNamespace + "SESSION_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -29,9 +31,10 @@ const ( // Config is used to configure the creation of the DNSProvider. type Config struct { - SecretID string - SecretKey string - Region string + SecretID string + SecretKey string + Region string + SessionToken string PropagationTimeout time.Duration PollingInterval time.Duration @@ -45,7 +48,7 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } @@ -67,6 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) { config.SecretID = values[EnvSecretID] config.SecretKey = values[EnvSecretKey] config.Region = env.GetOrDefaultString(EnvRegion, "") + config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "") return NewDNSProviderConfig(config) } @@ -77,14 +81,20 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("tencentcloud: the configuration of the DNS provider is nil") } - if config.SecretID == "" || config.SecretKey == "" { + var credential *common.Credential + + switch { + case config.SecretID != "" && config.SecretKey != "" && config.SessionToken != "": + credential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken) + case config.SecretID != "" && config.SecretKey != "": + credential = common.NewCredential(config.SecretID, config.SecretKey) + default: return nil, errors.New("tencentcloud: credentials missing") } - credential := common.NewCredential(config.SecretID, config.SecretKey) - cpf := profile.NewClientProfile() cpf.HttpProfile.Endpoint = "dnspod.tencentcloudapi.com" + cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds())) client, err := dnspod.NewClient(credential, config.Region, cpf) if err != nil { @@ -104,14 +114,28 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) - domainData, err := getDomainData(fqdn) + zone, err := d.getHostedZone(domain) if err != nil { - return fmt.Errorf("tencentcloud: failed to get domain data: %w", err) + return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } - err = d.createRecordData(domainData, value) + recordName, err := extractRecordName(fqdn, *zone.Name) if err != nil { - return fmt.Errorf("tencentcloud: create record failed: %w", err) + return fmt.Errorf("tencentcloud: failed to extract record name: %w", err) + } + + request := dnspod.NewCreateRecordRequest() + request.Domain = zone.Name + request.DomainId = zone.DomainId + request.SubDomain = common.StringPtr(recordName) + request.RecordType = common.StringPtr("TXT") + request.RecordLine = common.StringPtr("默认") + request.Value = common.StringPtr(value) + request.TTL = common.Uint64Ptr(uint64(d.config.TTL)) + + _, err = d.client.CreateRecord(request) + if err != nil { + return fmt.Errorf("dnspod: API call failed: %w", err) } return nil @@ -121,18 +145,23 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) - domainData, err := getDomainData(fqdn) + zone, err := d.getHostedZone(domain) if err != nil { - return fmt.Errorf("tencentcloud: failed to get domain data: %w", err) + return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } - records, err := d.listRecordData(domainData) + records, err := d.findTxtRecords(zone, fqdn) if err != nil { - return fmt.Errorf("tencentcloud: list records failed: %w", err) + return fmt.Errorf("tencentcloud: failed to find TXT records: %w", err) } - for _, item := range records { - err := d.deleteRecordData(domainData, item) + for _, record := range records { + request := dnspod.NewDeleteRecordRequest() + request.Domain = zone.Name + request.DomainId = zone.DomainId + request.RecordId = record.RecordId + + _, err := d.client.DeleteRecord(request) if err != nil { return fmt.Errorf("tencentcloud: delete record failed: %w", err) } diff --git a/providers/dns/tencentcloud/tencentcloud.toml b/providers/dns/tencentcloud/tencentcloud.toml index 8d8a91d5..4338e1da 100644 --- a/providers/dns/tencentcloud/tencentcloud.toml +++ b/providers/dns/tencentcloud/tencentcloud.toml @@ -15,6 +15,7 @@ lego --email you@example.com --dns tencentcloud --domains my.example.org run TENCENTCLOUD_SECRET_ID = "Access key ID" TENCENTCLOUD_SECRET_KEY = "Access Key secret" [Configuration.Additional] + TENCENTCLOUD_SESSION_TOKEN = "Access Key token" TENCENTCLOUD_REGION = "Region" TENCENTCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" TENCENTCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"