diff --git a/README.md b/README.md index fc0f442b..7e7243b6 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | -| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | -| [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | -| [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | +| [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | +| [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | +| [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index bc34bc4c..7c9309c3 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -102,6 +102,7 @@ func allDNSCodes() string { "simply", "sonic", "stackpath", + "tencentcloud", "transip", "vegadns", "versio", @@ -1997,6 +1998,28 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/stackpath`) + case "tencentcloud": + // generated from: providers/dns/tencentcloud/tencentcloud.toml + ew.writeln(`Configuration for Tencent Cloud DNS.`) + ew.writeln(`Code: 'tencentcloud'`) + ew.writeln(`Since: 'v4.6.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "TENCENTCLOUD_SECRET_ID": Access key ID`) + ew.writeln(` - "TENCENTCLOUD_SECRET_KEY": Access Key secret`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "TENCENTCLOUD_HTTP_TIMEOUT": API request timeout`) + 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_TTL": The TTL of the TXT record used for the DNS challenge`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/tencentcloud`) + case "transip": // generated from: providers/dns/transip/transip.toml ew.writeln(`Configuration for TransIP.`) diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md new file mode 100644 index 00000000..9c52cdc9 --- /dev/null +++ b/docs/content/dns/zz_gen_tencentcloud.md @@ -0,0 +1,66 @@ +--- +title: "Tencent Cloud DNS" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: tencentcloud +--- + + + + + +Since: v4.6.0 + +Configuration for [Tencent Cloud DNS](https://cloud.tencent.com/product/cns). + + + + +- Code: `tencentcloud` + +Here is an example bash command using the Tencent Cloud DNS provider: + +```bash +TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ +TENCENTCLOUD_SECRET_KEY=your-secret-key \ +lego --email myemail@example.com --dns tencentcloud --domains my.example.org run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `TENCENTCLOUD_SECRET_ID` | Access key ID | +| `TENCENTCLOUD_SECRET_KEY` | Access Key secret | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `TENCENTCLOUD_HTTP_TIMEOUT` | API request timeout | +| `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `TENCENTCLOUD_REGION` | Region | +| `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. +More information [here](/lego/dns/#configuration-and-credentials). + + + + +## More information + +- [API documentation](https://cloud.tencent.com/document/product/1427/56153) +- [Go client](https://github.com/tencentcloud/tencentcloud-sdk-go) + + + + diff --git a/go.mod b/go.mod index 5040f9f8..651b01b5 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,8 @@ require ( github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f github.com/softlayer/softlayer-go v1.0.3 github.com/stretchr/testify v1.7.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.287 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.287 github.com/transip/gotransip/v6 v6.6.1 github.com/urfave/cli v1.22.5 github.com/vinyldns/go-vinyldns v0.9.16 diff --git a/go.sum b/go.sum index d78a7955..2e2d569f 100644 --- a/go.sum +++ b/go.sum @@ -465,6 +465,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.287 h1:ohsyW4WffPdd2JLPio2Sd0qGr93hzkawAt9vWdCFLgY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.287/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.287 h1:O/ycBVvdOAmwFlXm0fCtLz2WOr1EaWZQTDM/4pmVT+s= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.287/go.mod h1:CuOaLxOQr477GhMWAQPYQFUJrsZbW+ZqkAgP2uHDZXg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.6.1 h1:nsCU1ErZS5G0FeOpgGXc4FsWvBff9GPswSMggsC4564= github.com/transip/gotransip/v6 v6.6.1/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index f8f9b317..b21411ae 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -93,6 +93,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/simply" "github.com/go-acme/lego/v4/providers/dns/sonic" "github.com/go-acme/lego/v4/providers/dns/stackpath" + "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/vegadns" "github.com/go-acme/lego/v4/providers/dns/versio" @@ -286,6 +287,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return sonic.NewDNSProvider() case "stackpath": return stackpath.NewDNSProvider() + case "tencentcloud": + return tencentcloud.NewDNSProvider() case "transip": return transip.NewDNSProvider() case "vegadns": diff --git a/providers/dns/tencentcloud/client.go b/providers/dns/tencentcloud/client.go new file mode 100644 index 00000000..1d417ce4 --- /dev/null +++ b/providers/dns/tencentcloud/client.go @@ -0,0 +1,70 @@ +package tencentcloud + +import ( + "strings" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" +) + +type domainData struct { + domain string + subDomain string +} + +func getDomainData(fqdn string) (*domainData, error) { + zone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return nil, err + } + + return &domainData{ + domain: 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 + } + + return nil +} + +func (d *DNSProvider) listRecordData(domainData *domainData) ([]*dnspod.RecordListItem, error) { + request := dnspod.NewDescribeRecordListRequest() + request.Domain = common.StringPtr(domainData.domain) + request.Subdomain = common.StringPtr(domainData.subDomain) + request.RecordType = common.StringPtr("TXT") + + response, err := d.client.DescribeRecordList(request) + if err != 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) + if err != nil { + return err + } + + return nil +} diff --git a/providers/dns/tencentcloud/tencentcloud.go b/providers/dns/tencentcloud/tencentcloud.go new file mode 100644 index 00000000..3cffe770 --- /dev/null +++ b/providers/dns/tencentcloud/tencentcloud.go @@ -0,0 +1,142 @@ +// Package tencentcloud implements a DNS provider for solving the DNS-01 challenge using Tencent Cloud DNS. +package tencentcloud + +import ( + "errors" + "fmt" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" +) + +// Environment variables names. +const ( + envNamespace = "TENCENTCLOUD_" + + EnvSecretID = envNamespace + "SECRET_ID" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvRegion = envNamespace + "REGION" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + SecretID string + SecretKey string + Region string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPTimeout time.Duration +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &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), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *dnspod.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Tencent Cloud DNS. +// Credentials must be passed in the environment variable: TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvSecretID, EnvSecretKey) + if err != nil { + return nil, fmt.Errorf("tencentcloud: %w", err) + } + + config := NewDefaultConfig() + config.SecretID = values[EnvSecretID] + config.SecretKey = values[EnvSecretKey] + config.Region = env.GetOrDefaultString(EnvRegion, "") + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Tencent Cloud DNS. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("tencentcloud: the configuration of the DNS provider is nil") + } + + if config.SecretID == "" || config.SecretKey == "" { + return nil, errors.New("tencentcloud: credentials missing") + } + + credential := common.NewCredential(config.SecretID, config.SecretKey) + + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "dnspod.tencentcloudapi.com" + + client, err := dnspod.NewClient(credential, config.Region, cpf) + if err != nil { + return nil, fmt.Errorf("tencentcloud: %w", err) + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + domainData, err := getDomainData(fqdn) + if err != nil { + return fmt.Errorf("tencentcloud: failed to get domain data: %w", err) + } + + err = d.createRecordData(domainData, value) + if err != nil { + return fmt.Errorf("tencentcloud: create record failed: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + + domainData, err := getDomainData(fqdn) + if err != nil { + return fmt.Errorf("tencentcloud: failed to get domain data: %w", err) + } + + records, err := d.listRecordData(domainData) + if err != nil { + return fmt.Errorf("tencentcloud: list records failed: %w", err) + } + + for _, item := range records { + err := d.deleteRecordData(domainData, item) + if err != nil { + return fmt.Errorf("tencentcloud: delete record failed: %w", err) + } + } + + return nil +} diff --git a/providers/dns/tencentcloud/tencentcloud.toml b/providers/dns/tencentcloud/tencentcloud.toml new file mode 100644 index 00000000..98b327f1 --- /dev/null +++ b/providers/dns/tencentcloud/tencentcloud.toml @@ -0,0 +1,26 @@ +Name = "Tencent Cloud DNS" +Description = '''''' +URL = "https://cloud.tencent.com/product/cns" +Code = "tencentcloud" +Since = "v4.6.0" + +Example = ''' +TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ +TENCENTCLOUD_SECRET_KEY=your-secret-key \ +lego --email myemail@example.com --dns tencentcloud --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + TENCENTCLOUD_SECRET_ID = "Access key ID" + TENCENTCLOUD_SECRET_KEY = "Access Key secret" + [Configuration.Additional] + TENCENTCLOUD_REGION = "Region" + TENCENTCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + TENCENTCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + TENCENTCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + TENCENTCLOUD_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://cloud.tencent.com/document/product/1427/56153" + GoClient = "https://github.com/tencentcloud/tencentcloud-sdk-go" diff --git a/providers/dns/tencentcloud/tencentcloud_test.go b/providers/dns/tencentcloud/tencentcloud_test.go new file mode 100644 index 00000000..c5a2fd97 --- /dev/null +++ b/providers/dns/tencentcloud/tencentcloud_test.go @@ -0,0 +1,150 @@ +package tencentcloud + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvSecretID, EnvSecretKey). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvSecretID: "123", + EnvSecretKey: "456", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvSecretID: "", + EnvSecretKey: "", + }, + expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID,TENCENTCLOUD_SECRET_KEY", + }, + { + desc: "missing access id", + envVars: map[string]string{ + EnvSecretID: "", + EnvSecretKey: "456", + }, + expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID", + }, + { + desc: "missing secret key", + envVars: map[string]string{ + EnvSecretID: "123", + EnvSecretKey: "", + }, + expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + secretID string + secretKey string + expected string + }{ + { + desc: "success", + secretID: "123", + secretKey: "456", + }, + { + desc: "missing credentials", + expected: "tencentcloud: credentials missing", + }, + { + desc: "missing secret id", + secretKey: "456", + expected: "tencentcloud: credentials missing", + }, + { + desc: "missing secret key", + secretID: "123", + expected: "tencentcloud: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.SecretID = test.secretID + config.SecretKey = test.secretKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +}