diff --git a/README.md b/README.md index e9a8caacc..1ad4f4fb6 100644 --- a/README.md +++ b/README.md @@ -61,230 +61,230 @@ If your DNS provider is not supported, please open an [issue](https://github.com + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + -
35.com/三五互联 Active24 Akamai EdgeDNS Alibaba Cloud DNSAlibabaCloud ESA
AlibabaCloud ESA all-inkl Alwaysdata Amazon LightsailAmazon Route 53
Amazon Route 53 Anexia CloudDNS ArvanCloud Aurora DNSAutodns
Autodns Axelname Azion Azure (deprecated)Azure DNS
Azure DNS Baidu Cloud Beget.com Binary LaneBindman
Bindman Bluecat BookMyName Brandit (deprecated)Bunny
Bunny Checkdomain Civo Cloud.ruCloudDNS
CloudDNS Cloudflare ClouDNS CloudXNS (Deprecated)ConoHa v2
ConoHa v2 ConoHa v3 Constellix Core-NetworksCPanel/WHM
CPanel/WHM Derak Cloud deSEC.io Designate DNSaaS for OpenstackDigital Ocean
Digital Ocean DirectAdmin DNS Made Easy dnsHome.deDNSimple
DNSimple DNSPod (deprecated) Domain Offensive (do.de) DomeneshopDreamHost
DreamHost Duck DNS Dyn DynDnsFree.deDynu
Dynu EasyDNS EdgeCenter Efficient IPEpik
Epik Exoscale External program F5 XCfreemyip.com
freemyip.com G-Core Gandi Gandi Live DNS (v5)Gigahost.no
Gigahost.no Glesys Go Daddy Google CloudGoogle Domains
Google Domains Gravity Hetzner Hosting.deHosting.nl
Hosting.nl Hostinger Hosttech HTTP requesthttp.net
http.net Huawei Cloud Hurricane Electric DNS HyperOneIBM Cloud (SoftLayer)
IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox InfomaniakInternet Initiative Japan
Internet Initiative Japan Internet.bs INWX IonosIonos Cloud
Ionos Cloud IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Moduleiwantmyname (Deprecated)
iwantmyname (Deprecated) Joker Joohoi's ACME-DNS KeyHelpLiara
Liara Lima-City Linode (v4) Liquid WebLoopia
Loopia LuaDNS Mail-in-a-Box ManageEngine CloudDNSManual
Manual Metaname Metaregistrar mijn.hostMittwald
Mittwald myaddr.{tools,dev,io} MyDNS.jp MythicBeastsName.com
Name.com Namecheap Namesilo NearlyFreeSpeech.NETNeodigit
Neodigit Netcup Netlify NicmanagerNIFCloud
NIFCloud Njalla Nodion NS1Octenium
Octenium Open Telekom Cloud Oracle Cloud OVHplesk.com
plesk.com Porkbun PowerDNS RackspaceRain Yun/雨云
Rain Yun/雨云 RcodeZero reg.ru RegfishRFC2136
RFC2136 RimuHosting RU CENTER Sakura CloudScaleway
Scaleway Selectel Selectel v2 SelfHost.(de|eu)Servercow
Servercow Shellrent Simply.com SonicSpaceship
Spaceship Stackpath Syse TechnitiumTencent Cloud DNS
Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TransIPUKFast SafeDNS
UKFast SafeDNS Ultradns United-Domains VariomediaVegaDNS
VegaDNS Vercel Versio.[nl|eu|uk] VinylDNSVirtualname
Virtualname VK Cloud Volcano Engine/火山引擎 VscaleVultr
Vultr webnames.ca webnames.ru WebsupportWEDOS
WEDOS West.cn/西部数码 Yandex 360 Yandex CloudYandex PDD
Yandex PDD Zone.ee ZoneEdit Zonomi
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 220289242..44fec8e54 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -41,6 +41,7 @@ func allDNSCodes() string { "cloudns", "cloudru", "cloudxns", + "com35", "conoha", "conohav3", "constellix", @@ -836,6 +837,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudxns`) + case "com35": + // generated from: providers/dns/com35/com35.toml + ew.writeln(`Configuration for 35.com/三五互联.`) + ew.writeln(`Code: 'com35'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "COM35_PASSWORD": API password`) + ew.writeln(` - "COM35_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "COM35_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "COM35_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "COM35_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "COM35_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/com35`) + case "conoha": // generated from: providers/dns/conoha/conoha.toml ew.writeln(`Configuration for ConoHa v2.`) diff --git a/docs/content/dns/zz_gen_com35.md b/docs/content/dns/zz_gen_com35.md new file mode 100644 index 000000000..e2552e57c --- /dev/null +++ b/docs/content/dns/zz_gen_com35.md @@ -0,0 +1,69 @@ +--- +title: "35.com/三五互联" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: com35 +dnsprovider: + since: "v4.31.0" + code: "com35" + url: "https://www.35.cn/" +--- + + + + + + +Configuration for [35.com/三五互联](https://www.35.cn/). + + + + +- Code: `com35` +- Since: v4.31.0 + + +Here is an example bash command using the 35.com/三五互联 provider: + +```bash +COM35_USERNAME="xxx" \ +COM35_PASSWORD="yyy" \ +lego --dns com35 -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `COM35_PASSWORD` | API password | +| `COM35_USERNAME` | Username | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `COM35_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `COM35_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `COM35_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `COM35_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://api.35.cn/CustomerCenter/doc/domain_v2.html) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index ab9ff31c9..2ee3e9006 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run: $ lego dnshelp -c code Supported DNS providers: - acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi More information: https://go-acme.github.io/lego/dns """ diff --git a/providers/dns/com35/com35.go b/providers/dns/com35/com35.go new file mode 100644 index 000000000..4a9de3a18 --- /dev/null +++ b/providers/dns/com35/com35.go @@ -0,0 +1,104 @@ +// Package com35 implements a DNS provider for solving the DNS-01 challenge using 35.com/三五互联. +package com35 + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn" +) + +// Environment variables names. +const ( + envNamespace = "COM35_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const defaultBaseURL = "https://api.35.cn/api/v2" + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config = westcn.Config + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 60), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + prv challenge.ProviderTimeout +} + +// NewDNSProvider returns a DNSProvider instance configured for 35.com/三五互联. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("35com: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for 35.com/三五互联. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("35com: the configuration of the DNS provider is nil") + } + + provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) + if err != nil { + return nil, fmt.Errorf("35com: %w", err) + } + + return &DNSProvider{prv: provider}, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + err := d.prv.Present(domain, token, keyAuth) + if err != nil { + return fmt.Errorf("35com: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + err := d.prv.CleanUp(domain, token, keyAuth) + if err != nil { + return fmt.Errorf("35com: %w", err) + } + + return 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.prv.Timeout() +} diff --git a/providers/dns/com35/com35.toml b/providers/dns/com35/com35.toml new file mode 100644 index 000000000..386ee0043 --- /dev/null +++ b/providers/dns/com35/com35.toml @@ -0,0 +1,24 @@ +Name = "35.com/三五互联" +Description = '''''' +URL = "https://www.35.cn/" +Code = "com35" +Since = "v4.31.0" + +Example = ''' +COM35_USERNAME="xxx" \ +COM35_PASSWORD="yyy" \ +lego --dns com35 -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + COM35_USERNAME = "Username" + COM35_PASSWORD = "API password" + [Configuration.Additional] + COM35_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + COM35_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + COM35_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + COM35_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://api.35.cn/CustomerCenter/doc/domain_v2.html" diff --git a/providers/dns/com35/com35_test.go b/providers/dns/com35/com35_test.go new file mode 100644 index 000000000..78fd8f829 --- /dev/null +++ b/providers/dns/com35/com35_test.go @@ -0,0 +1,144 @@ +package com35 + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "secret", + }, + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvUsername: "", + EnvPassword: "secret", + }, + expected: "35com: some credentials information are missing: COM35_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "", + }, + expected: "35com: some credentials information are missing: COM35_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "35com: some credentials information are missing: COM35_USERNAME,COM35_PASSWORD", + }, + } + + 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.prv) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + password string + expected string + }{ + { + desc: "success", + username: "user", + password: "secret", + }, + { + desc: "missing username", + password: "secret", + expected: "35com: credentials missing", + }, + { + desc: "missing password", + username: "user", + expected: "35com: credentials missing", + }, + { + desc: "missing credentials", + expected: "35com: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Username = test.username + config.Password = test.password + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.prv) + } 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/westcn/internal/client.go b/providers/dns/internal/westcn/internal/client.go similarity index 98% rename from providers/dns/westcn/internal/client.go rename to providers/dns/internal/westcn/internal/client.go index bfed159ae..621c7865f 100644 --- a/providers/dns/westcn/internal/client.go +++ b/providers/dns/internal/westcn/internal/client.go @@ -30,7 +30,7 @@ type Client struct { encoder *encoding.Encoder - baseURL *url.URL + BaseURL *url.URL HTTPClient *http.Client } @@ -46,7 +46,7 @@ func NewClient(username, password string) (*Client, error) { username: username, password: password, encoder: simplifiedchinese.GBK.NewEncoder(), - baseURL: baseURL, + BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, }, nil } @@ -116,7 +116,7 @@ func (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) return nil, err } - endpoint := c.baseURL.JoinPath(p, "/") + endpoint := c.BaseURL.JoinPath(p, "/") query := endpoint.Query() query.Set("act", act) diff --git a/providers/dns/westcn/internal/client_test.go b/providers/dns/internal/westcn/internal/client_test.go similarity index 98% rename from providers/dns/westcn/internal/client_test.go rename to providers/dns/internal/westcn/internal/client_test.go index f7bdac5c0..53fd6ed8f 100644 --- a/providers/dns/westcn/internal/client_test.go +++ b/providers/dns/internal/westcn/internal/client_test.go @@ -21,7 +21,7 @@ func mockBuilder() *servermock.Builder[*Client] { } client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + client.BaseURL, _ = url.Parse(server.URL) return client, nil }, @@ -69,7 +69,8 @@ func TestClientAddRecord_error(t *testing.T) { servermock.ResponseFromFixture("error.json"). WithHeader("Content-Type", "application/json", "Charset=gb2312"), servermock.CheckQueryParameter().Strict(). - With("act", "adddnsrecord")). + With("act", "adddnsrecord"), + ). Build(t) record := Record{ diff --git a/providers/dns/westcn/internal/fixtures/adddnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/adddnsrecord.json rename to providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json diff --git a/providers/dns/westcn/internal/fixtures/deldnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/deldnsrecord.json rename to providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json diff --git a/providers/dns/westcn/internal/fixtures/error.json b/providers/dns/internal/westcn/internal/fixtures/error.json similarity index 100% rename from providers/dns/westcn/internal/fixtures/error.json rename to providers/dns/internal/westcn/internal/fixtures/error.json diff --git a/providers/dns/westcn/internal/types.go b/providers/dns/internal/westcn/internal/types.go similarity index 100% rename from providers/dns/westcn/internal/types.go rename to providers/dns/internal/westcn/internal/types.go diff --git a/providers/dns/internal/westcn/provider.go b/providers/dns/internal/westcn/provider.go new file mode 100644 index 000000000..a9e6dad58 --- /dev/null +++ b/providers/dns/internal/westcn/provider.go @@ -0,0 +1,140 @@ +package westcn + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn/internal" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.Username, config.Password) + if err != nil { + return nil, fmt.Errorf("%w", err) + } + + if baseURL != "" { + client.BaseURL, err = url.Parse(baseURL) + if err != nil { + return nil, err + } + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int), + }, nil +} + +// Present creates a TXT record using the specified parameters. +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("could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("%w", err) + } + + record := internal.Record{ + Domain: dns01.UnFqdn(authZone), + Host: subDomain, + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + } + + recordID, err := d.client.AddRecord(context.Background(), record) + if err != nil { + return fmt.Errorf("add record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = recordID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("could not find zone for domain %q: %w", domain, err) + } + + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) + if err != nil { + return fmt.Errorf("delete record: %w", err) + } + + // deletes record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return 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 +} diff --git a/providers/dns/internal/westcn/provider_test.go b/providers/dns/internal/westcn/provider_test.go new file mode 100644 index 000000000..2ae0f09cb --- /dev/null +++ b/providers/dns/internal/westcn/provider_test.go @@ -0,0 +1,127 @@ +package westcn + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + password string + expected string + }{ + { + desc: "success", + username: "user", + password: "secret", + }, + { + desc: "missing username", + password: "secret", + expected: "credentials missing", + }, + { + desc: "missing password", + username: "user", + expected: "credentials missing", + }, + { + desc: "missing credentials", + expected: "credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.Username = test.username + config.Password = test.password + + 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 mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := &Config{ + Username: "user", + Password: "secret", + PropagationTimeout: 10 * time.Second, + PollingInterval: 1 * time.Second, + TTL: 120, + HTTPClient: server.Client(), + } + + p, err := NewDNSProviderConfig(config, server.URL) + if err != nil { + return nil, err + } + + return p, nil + }, + servermock.CheckHeader(). + WithContentTypeFromURLEncoded()) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /domain/", + servermock.ResponseFromInternal("adddnsrecord.json"). + WithHeader("Content-Type", "application/json", "Charset=gb2312"), + servermock.CheckQueryParameter().Strict(). + With("act", "adddnsrecord"), + servermock.CheckForm().UsePostForm().Strict(). + With("domain", "example.com"). + With("host", "_acme-challenge"). + With("ttl", "120"). + With("type", "TXT"). + With("value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + // With("act", "adddnsrecord"). + With("username", "user"). + WithRegexp("time", `\d+`). + WithRegexp("token", `[a-z0-9]{32}`), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /domain/", + servermock.ResponseFromInternal("deldnsrecord.json"). + WithHeader("Content-Type", "application/json", "Charset=gb2312"), + servermock.CheckQueryParameter().Strict(). + With("act", "deldnsrecord"), + servermock.CheckForm().UsePostForm().Strict(). + With("id", "123"). + With("domain", "example.com"). + With("username", "user"). + WithRegexp("time", `\d+`). + WithRegexp("token", `[a-z0-9]{32}`), + ). + Build(t) + + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/westcn/westcn.go b/providers/dns/westcn/westcn.go index c641f661d..1906f9737 100644 --- a/providers/dns/westcn/westcn.go +++ b/providers/dns/westcn/westcn.go @@ -2,18 +2,14 @@ package westcn import ( - "context" "errors" "fmt" "net/http" - "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/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/westcn/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/westcn" ) // Environment variables names. @@ -29,18 +25,12 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.west.cn/api/v2" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = westcn.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -56,11 +46,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for West.cn/西部数码. @@ -83,91 +69,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("westcn: the configuration of the DNS provider is nil") } - client, err := internal.NewClient(config.Username, config.Password) + provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("westcn: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - }, nil + return &DNSProvider{prv: provider}, nil } // Present creates a TXT record using the specified parameters. 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("westcn: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("westcn: %w", err) } - record := internal.Record{ - Domain: dns01.UnFqdn(authZone), - Host: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - } - - recordID, err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("westcn: add record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("westcn: could not find zone for domain %q: %w", domain, err) + return fmt.Errorf("westcn: %w", err) } - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("westcn: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("westcn: delete record: %w", err) - } - - // deletes record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return 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 + return d.prv.Timeout() } diff --git a/providers/dns/westcn/westcn_test.go b/providers/dns/westcn/westcn_test.go index 36827fd06..a546d518e 100644 --- a/providers/dns/westcn/westcn_test.go +++ b/providers/dns/westcn/westcn_test.go @@ -60,8 +60,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -108,8 +107,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index c5db54109..0d9ad26e8 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -35,6 +35,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/cloudru" "github.com/go-acme/lego/v4/providers/dns/cloudxns" + "github.com/go-acme/lego/v4/providers/dns/com35" "github.com/go-acme/lego/v4/providers/dns/conoha" "github.com/go-acme/lego/v4/providers/dns/conohav3" "github.com/go-acme/lego/v4/providers/dns/constellix" @@ -248,6 +249,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return cloudru.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() + case "com35": + return com35.NewDNSProvider() case "conoha": return conoha.NewDNSProvider() case "conohav3":