From e12c9fc637c302f1d4075b25c519852f2e3c453d Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Fri, 31 Oct 2025 11:16:12 +0100 Subject: [PATCH 01/95] Detach v4.28.0 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 11f6edf99..903fd7483 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index 8eed28947..afee84317 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.28.0+dev-release" +const defaultVersion = "v4.28.0+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 1fdaef71a..2a48e1942 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From 14778cc1f11156a5ab4253252d5ec96e3248cccd Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 6 Nov 2025 13:19:09 +0100 Subject: [PATCH 02/95] fix: skip nil response (#2705) --- cmd/setup.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/setup.go b/cmd/setup.go index 4d17f2e27..319b7680e 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -180,6 +180,10 @@ func checkRetry(ctx context.Context, resp *http.Response, err error) (bool, erro return rt, err } + if resp == nil { + return rt, nil + } + if resp.StatusCode/100 == 2 { return rt, nil } From b704b26e6c49a540cbb5dd51c2f3a9d70053c86c Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 6 Nov 2025 16:53:15 +0100 Subject: [PATCH 03/95] Prepare release v4.28.1 --- .github/workflows/release.yml | 2 ++ CHANGELOG.md | 9 +++++++++ acme/api/internal/sender/useragent.go | 4 ++-- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 4 ++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a83c85909..ca2e1867e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,6 +81,8 @@ jobs: - uses: actions/attest-build-provenance@v3 with: subject-checksums: ./dist/lego_*_checksums.txt + github-token: ${{ secrets.GH_TOKEN_REPO }} - uses: actions/attest-build-provenance@v3 with: subject-checksums: ./dist/digests.txt + github-token: ${{ secrets.GH_TOKEN_REPO }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfd912d5..31f8ff569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.28.1 + +- Release date: 2025-11-06 +- Tag: [v4.28.1](https://github.com/go-acme/lego/releases/tag/v4.28.1) + +### Fixed + +- **[cli]** fix: skip nil response + ## v4.28.0 - Release date: 2025-10-31 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 903fd7483..2e5f84d82 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.28.0" + ourUserAgent = "xenolf-acme/4.28.1" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index afee84317..1f4a291dd 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.28.0+dev-detach" +const defaultVersion = "v4.28.1+dev-release" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 2a48e1942..57e2a727a 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.28.0" + ourUserAgent = "goacme-lego/4.28.1" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From 22955739a15861793e814c044e102502d61c5e3e Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 6 Nov 2025 17:05:33 +0100 Subject: [PATCH 04/95] Detach v4.28.1 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 2e5f84d82..bcfbebb2a 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index 1f4a291dd..b564906c1 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.28.1+dev-release" +const defaultVersion = "v4.28.1+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 57e2a727a..1e176ab9a 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From d5dc3866e666984c954695f246b8ea3dcd1dfbe4 Mon Sep 17 00:00:00 2001 From: oliverbr <5881331+oliverbr@users.noreply.github.com> Date: Sat, 8 Nov 2025 03:29:10 +0100 Subject: [PATCH 05/95] gandiv5: update base API URL (#2708) --- providers/dns/gandiv5/internal/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/dns/gandiv5/internal/client.go b/providers/dns/gandiv5/internal/client.go index 018a05799..36e0dafb1 100644 --- a/providers/dns/gandiv5/internal/client.go +++ b/providers/dns/gandiv5/internal/client.go @@ -15,7 +15,7 @@ import ( ) // defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp. -const defaultBaseURL = "https://dns.api.gandi.net/api/v5" +const defaultBaseURL = "https://api.gandi.net/v5/livedns" // APIKeyHeader API key header. const APIKeyHeader = "X-Api-Key" From 1c33fba18007f3f63cba61c82d81663210042b8d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sat, 8 Nov 2025 13:38:05 +0100 Subject: [PATCH 06/95] chore: add major version tag for Docker images (#2709) --- .goreleaser.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 073997209..c358f8a38 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -90,8 +90,9 @@ dockers_v2: - linux/arm/v7 tags: - 'latest' - - '{{ .Tag }}' + - 'v{{ .Major }}' - 'v{{ .Major }}.{{ .Minor }}' + - '{{ .Tag }}' labels: # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys 'org.opencontainers.image.title': '{{.ProjectName}}' From 877738cef3c705d6bdcfd85daa476fccffa20e24 Mon Sep 17 00:00:00 2001 From: Evgeniy Medvedev Date: Sun, 9 Nov 2025 23:56:16 +0400 Subject: [PATCH 07/95] Add DNS provider for EdgeCenter (#2710) Co-authored-by: Fernandez Ludovic --- README.md | 60 +++---- cmd/zz_gen_cmd_dnshelp.go | 21 +++ docs/content/dns/zz_gen_edgecenter.md | 67 ++++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/edgecenter/edgecenter.go | 160 ++++++++++++++++++ providers/dns/edgecenter/edgecenter.toml | 22 +++ providers/dns/edgecenter/edgecenter_test.go | 116 +++++++++++++ providers/dns/gcore/gcore.go | 30 +--- providers/dns/gcore/gcore_test.go | 28 --- .../internal => internal/gcore}/client.go | 23 +-- .../gcore}/client_test.go | 4 +- .../internal => internal/gcore}/types.go | 2 +- providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 444 insertions(+), 94 deletions(-) create mode 100644 docs/content/dns/zz_gen_edgecenter.md create mode 100644 providers/dns/edgecenter/edgecenter.go create mode 100644 providers/dns/edgecenter/edgecenter.toml create mode 100644 providers/dns/edgecenter/edgecenter_test.go rename providers/dns/{gcore/internal => internal/gcore}/client.go (89%) rename providers/dns/{gcore/internal => internal/gcore}/client_test.go (98%) rename providers/dns/{gcore/internal => internal/gcore}/types.go (96%) diff --git a/README.md b/README.md index 1a8480b24..59fb983eb 100644 --- a/README.md +++ b/README.md @@ -122,152 +122,152 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). DynDnsFree.de Dynu EasyDNS - Efficient IP + EdgeCenter + Efficient IP Epik Exoscale External program - F5 XC + F5 XC freemyip.com G-Core Gandi - Gandi Live DNS (v5) + Gandi Live DNS (v5) Glesys Go Daddy Google Cloud - Google Domains + Google Domains Hetzner Hosting.de Hostinger - Hosttech + Hosttech HTTP request http.net Huawei Cloud - Hurricane Electric DNS + Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service - Infoblox + Infoblox Infomaniak Internet Initiative Japan Internet.bs - INWX + INWX Ionos IPv64 iwantmyname (Deprecated) - Joker + Joker Joohoi's ACME-DNS KeyHelp Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Technitium + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TransIP + TransIP UKFast SafeDNS Ultradns Variomedia - VegaDNS + VegaDNS Vercel Versio.[nl|eu|uk] VinylDNS - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index e21f37e63..2859afd9b 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -62,6 +62,7 @@ func allDNSCodes() string { "dyndnsfree", "dynu", "easydns", + "edgecenter", "edgedns", "edgeone", "efficientip", @@ -1252,6 +1253,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/easydns`) + case "edgecenter": + // generated from: providers/dns/edgecenter/edgecenter.toml + ew.writeln(`Configuration for EdgeCenter.`) + ew.writeln(`Code: 'edgecenter'`) + ew.writeln(`Since: 'v4.29.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "EDGECENTER_PERMANENT_API_TOKEN": Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EDGECENTER_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "EDGECENTER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) + ew.writeln(` - "EDGECENTER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) + ew.writeln(` - "EDGECENTER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgecenter`) + case "edgedns": // generated from: providers/dns/edgedns/edgedns.toml ew.writeln(`Configuration for Akamai EdgeDNS.`) diff --git a/docs/content/dns/zz_gen_edgecenter.md b/docs/content/dns/zz_gen_edgecenter.md new file mode 100644 index 000000000..7c7dd9379 --- /dev/null +++ b/docs/content/dns/zz_gen_edgecenter.md @@ -0,0 +1,67 @@ +--- +title: "EdgeCenter" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: edgecenter +dnsprovider: + since: "v4.29.0" + code: "edgecenter" + url: "https://edgecenter.ru/dns" +--- + + + + + + +Configuration for [EdgeCenter](https://edgecenter.ru/dns). + + + + +- Code: `edgecenter` +- Since: v4.29.0 + + +Here is an example bash command using the EdgeCenter provider: + +```bash +EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ +lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EDGECENTER_PERMANENT_API_TOKEN` | Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/) | + +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 | +|--------------------------------|-------------| +| `EDGECENTER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `EDGECENTER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | +| `EDGECENTER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | +| `EDGECENTER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://apidocs.edgecenter.ru/dns) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 80cbcaac0..071c0f344 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, allinkl, 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, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, allinkl, 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, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, 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/edgecenter/edgecenter.go b/providers/dns/edgecenter/edgecenter.go new file mode 100644 index 000000000..2040a304c --- /dev/null +++ b/providers/dns/edgecenter/edgecenter.go @@ -0,0 +1,160 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "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/internal/gcore" +) + +// Environment variables names. +const ( + envNamespace = "EDGECENTER_" + + EnvPermanentAPIToken = envNamespace + "PERMANENT_API_TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const ( + defaultPropagationTimeout = 360 * time.Second + defaultPollingInterval = 20 * time.Second +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config for DNSProvider. +type Config struct { + APIToken string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + }, + } +} + +// DNSProvider an implementation of challenge.Provider contract. +type DNSProvider struct { + config *Config + client *gcore.Client +} + +// NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvPermanentAPIToken) + if err != nil { + return nil, fmt.Errorf("edgecenter: %w", err) + } + + config := NewDefaultConfig() + config.APIToken = values[EnvPermanentAPIToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("edgecenter: the configuration of the DNS provider is nil") + } + + if config.APIToken == "" { + return nil, errors.New("edgecenter: incomplete credentials provided") + } + + client := gcore.NewClient(config.APIToken) + client.BaseURL, _ = url.Parse(gcore.DefaultEdgeCenterBaseURL) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("edgecenter: %w", err) + } + + err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) + if err != nil { + return fmt.Errorf("edgecenter: add txt record: %w", err) + } + + return nil +} + +// CleanUp removes the record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("edgecenter: %w", err) + } + + err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("edgecenter: remove txt record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { + var lastErr error + + for zone := range dns01.UnFqdnDomainsSeq(fqdn) { + dnsZone, err := d.client.GetZone(ctx, zone) + if err != nil { + lastErr = err + continue + } + + return dnsZone.Name, nil + } + + return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) +} diff --git a/providers/dns/edgecenter/edgecenter.toml b/providers/dns/edgecenter/edgecenter.toml new file mode 100644 index 000000000..0cd4b0cb6 --- /dev/null +++ b/providers/dns/edgecenter/edgecenter.toml @@ -0,0 +1,22 @@ +Name = "EdgeCenter" +Description = '''''' +URL = "https://edgecenter.ru/dns" +Code = "edgecenter" +Since = "v4.29.0" + +Example = ''' +EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ +lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + EDGECENTER_PERMANENT_API_TOKEN = "Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)" + [Configuration.Additional] + EDGECENTER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" + EDGECENTER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" + EDGECENTER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + EDGECENTER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + +[Links] + API = "https://apidocs.edgecenter.ru/dns" diff --git a/providers/dns/edgecenter/edgecenter_test.go b/providers/dns/edgecenter/edgecenter_test.go new file mode 100644 index 000000000..79814680d --- /dev/null +++ b/providers/dns/edgecenter/edgecenter_test.go @@ -0,0 +1,116 @@ +package edgecenter + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +var envTest = tester.NewEnvTest(EnvPermanentAPIToken).WithDomain(envNamespace + "DOMAIN") + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvPermanentAPIToken: "A", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvPermanentAPIToken: "", + }, + expected: "edgecenter: some credentials information are missing: EDGECENTER_PERMANENT_API_TOKEN", + }, + } + + 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 + apiToken string + expected string + }{ + { + desc: "success", + apiToken: "A", + }, + { + desc: "missing credentials", + expected: "edgecenter: incomplete credentials provided", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIToken = test.apiToken + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/gcore/gcore.go b/providers/dns/gcore/gcore.go index 19a548810..6400dc0b3 100644 --- a/providers/dns/gcore/gcore.go +++ b/providers/dns/gcore/gcore.go @@ -5,14 +5,13 @@ import ( "errors" "fmt" "net/http" - "strings" "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/gcore/internal" "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/go-acme/lego/v4/providers/dns/internal/gcore" ) // Environment variables names. @@ -58,7 +57,7 @@ func NewDefaultConfig() *Config { // DNSProvider an implementation of challenge.Provider contract. type DNSProvider struct { config *Config - client *internal.Client + client *gcore.Client } // NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. @@ -84,7 +83,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("gcore: incomplete credentials provided") } - client := internal.NewClient(config.APIToken) + client := gcore.NewClient(config.APIToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient @@ -145,28 +144,15 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { var lastErr error - for _, zone := range extractAllZones(fqdn) { + for zone := range dns01.UnFqdnDomainsSeq(fqdn) { dnsZone, err := d.client.GetZone(ctx, zone) - if err == nil { - return dnsZone.Name, nil + if err != nil { + lastErr = err + continue } - lastErr = err + return dnsZone.Name, nil } return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) } - -func extractAllZones(fqdn string) []string { - parts := strings.Split(dns01.UnFqdn(fqdn), ".") - if len(parts) < 3 { - return nil - } - - var zones []string - for i := 1; i < len(parts)-1; i++ { - zones = append(zones, strings.Join(parts[i:], ".")) - } - - return zones -} diff --git a/providers/dns/gcore/gcore_test.go b/providers/dns/gcore/gcore_test.go index 88769df21..88c1d02a5 100644 --- a/providers/dns/gcore/gcore_test.go +++ b/providers/dns/gcore/gcore_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -115,30 +114,3 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func Test_extractAllZones(t *testing.T) { - testCases := []struct { - desc string - fqdn string - expected []string - }{ - { - desc: "success", - fqdn: "_acme-challenge.my.test.domain.com.", - expected: []string{"my.test.domain.com", "test.domain.com", "domain.com"}, - }, - { - desc: "empty", - fqdn: "_acme-challenge.com.", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - got := extractAllZones(test.fqdn) - assert.Equal(t, test.expected, got) - }) - } -} diff --git a/providers/dns/gcore/internal/client.go b/providers/dns/internal/gcore/client.go similarity index 89% rename from providers/dns/gcore/internal/client.go rename to providers/dns/internal/gcore/client.go index 638aaf0d7..93f17c0d2 100644 --- a/providers/dns/gcore/internal/client.go +++ b/providers/dns/internal/gcore/client.go @@ -1,4 +1,4 @@ -package internal +package gcore import ( "bytes" @@ -14,7 +14,10 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultBaseURL = "https://api.gcore.com/dns" +const ( + DefaultGCoreBaseURL = "https://api.gcore.com/dns" + DefaultEdgeCenterBaseURL = "https://api.edgecenter.ru/dns" +) const ( authorizationHeader = "Authorization" @@ -27,17 +30,17 @@ const txtRecordType = "TXT" type Client struct { token string - baseURL *url.URL + BaseURL *url.URL HTTPClient *http.Client } // NewClient constructor of Client. func NewClient(token string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) + baseURL, _ := url.Parse(DefaultGCoreBaseURL) return &Client{ token: token, - baseURL: baseURL, + BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -45,7 +48,7 @@ func NewClient(token string) *Client { // GetZone gets zone information. // https://api.gcore.com/docs/dns#tag/zones/operation/Zone func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { - endpoint := c.baseURL.JoinPath("v2", "zones", name) + endpoint := c.BaseURL.JoinPath("v2", "zones", name) zone := Zone{} @@ -60,7 +63,7 @@ func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { // GetRRSet gets RRSet item. // https://api.gcore.com/docs/dns#tag/rrsets/operation/RRSet func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) { - endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) var result RRSet @@ -75,7 +78,7 @@ func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) // DeleteRRSet removes RRSet record. // https://api.gcore.com/docs/dns#tag/rrsets/operation/DeleteRRSet func (c *Client) DeleteRRSet(ctx context.Context, zone, name string) error { - endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) err := c.doRequest(ctx, http.MethodDelete, endpoint, nil, nil) if err != nil { @@ -106,14 +109,14 @@ func (c *Client) AddRRSet(ctx context.Context, zone, recordName, value string, t // https://api.gcore.com/docs/dns#tag/rrsets/operation/CreateRRSet func (c *Client) createRRSet(ctx context.Context, zone, name string, record RRSet) error { - endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) return c.doRequest(ctx, http.MethodPost, endpoint, record, nil) } // https://api.gcore.com/docs/dns#tag/rrsets/operation/UpdateRRSet func (c *Client) updateRRSet(ctx context.Context, zone, name string, record RRSet) error { - endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) return c.doRequest(ctx, http.MethodPut, endpoint, record, nil) } diff --git a/providers/dns/gcore/internal/client_test.go b/providers/dns/internal/gcore/client_test.go similarity index 98% rename from providers/dns/gcore/internal/client_test.go rename to providers/dns/internal/gcore/client_test.go index 4a0f83311..79289ef42 100644 --- a/providers/dns/gcore/internal/client_test.go +++ b/providers/dns/internal/gcore/client_test.go @@ -1,4 +1,4 @@ -package internal +package gcore import ( "net/http" @@ -21,7 +21,7 @@ func mockBuilder() *servermock.Builder[*Client] { return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { client := NewClient(testToken) - client.baseURL, _ = url.Parse(server.URL) + client.BaseURL, _ = url.Parse(server.URL) client.HTTPClient = server.Client() return client, nil diff --git a/providers/dns/gcore/internal/types.go b/providers/dns/internal/gcore/types.go similarity index 96% rename from providers/dns/gcore/internal/types.go rename to providers/dns/internal/gcore/types.go index 4245f5ba8..1c4fbd502 100644 --- a/providers/dns/gcore/internal/types.go +++ b/providers/dns/internal/gcore/types.go @@ -1,4 +1,4 @@ -package internal +package gcore import "fmt" diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 32de816a3..90bb2e13c 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -56,6 +56,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/dyndnsfree" "github.com/go-acme/lego/v4/providers/dns/dynu" "github.com/go-acme/lego/v4/providers/dns/easydns" + "github.com/go-acme/lego/v4/providers/dns/edgecenter" "github.com/go-acme/lego/v4/providers/dns/edgedns" "github.com/go-acme/lego/v4/providers/dns/edgeone" "github.com/go-acme/lego/v4/providers/dns/efficientip" @@ -277,6 +278,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return dynu.NewDNSProvider() case "easydns": return easydns.NewDNSProvider() + case "edgecenter": + return edgecenter.NewDNSProvider() case "edgedns", "fastdns": return edgedns.NewDNSProvider() case "edgeone": From b338263c96bef5d1eefb77c40f1c87d2d57cdc6b Mon Sep 17 00:00:00 2001 From: RHQYZ Date: Tue, 11 Nov 2025 20:36:35 +0800 Subject: [PATCH 08/95] baiducloud: pagination and TTL (#2712) Co-authored-by: Fernandez Ludovic --- cmd/zz_gen_cmd_dnshelp.go | 2 +- docs/content/dns/zz_gen_baiducloud.md | 2 +- providers/dns/baiducloud/baiducloud.go | 38 ++++++++++++++++-------- providers/dns/baiducloud/baiducloud.toml | 2 +- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 2859afd9b..24a2470bb 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -472,7 +472,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "BAIDUCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "BAIDUCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BAIDUCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "BAIDUCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/baiducloud`) diff --git a/docs/content/dns/zz_gen_baiducloud.md b/docs/content/dns/zz_gen_baiducloud.md index 11a71c1ab..9f59aa156 100644 --- a/docs/content/dns/zz_gen_baiducloud.md +++ b/docs/content/dns/zz_gen_baiducloud.md @@ -51,7 +51,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). |--------------------------------|-------------| | `BAIDUCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | | `BAIDUCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BAIDUCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `BAIDUCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | 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" %}}). diff --git a/providers/dns/baiducloud/baiducloud.go b/providers/dns/baiducloud/baiducloud.go index fc317904a..1dc8d90ed 100644 --- a/providers/dns/baiducloud/baiducloud.go +++ b/providers/dns/baiducloud/baiducloud.go @@ -24,6 +24,9 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) +// 300 is the minimum TTL for free users. +const defaultTTL = 300 + // Config is used to configure the creation of the DNSProvider. type Config struct { AccessKeyID string @@ -37,7 +40,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } @@ -103,6 +106,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Rr: subDomain, Type: "TXT", Value: info.Value, + Ttl: ptr.Pointer(int32(d.config.TTL)), } err = d.client.CreateRecord(dns01.UnFqdn(authZone), crr, "") @@ -122,14 +126,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("baiducloud: could not find zone for domain %q: %w", domain, err) } - lrr := &baidudns.ListRecordRequest{} - - recordResponse, err := d.client.ListRecord(dns01.UnFqdn(authZone), lrr) - if err != nil { - return fmt.Errorf("baiducloud: list record: %w", err) - } - - recordID, err := findRecordID(recordResponse, info) + recordID, err := d.findRecordID(dns01.UnFqdn(authZone), info.Value) if err != nil { return fmt.Errorf("baiducloud: find record: %w", err) } @@ -142,11 +139,26 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } -func findRecordID(recordResponse *baidudns.ListRecordResponse, info dns01.ChallengeInfo) (string, error) { - for _, record := range recordResponse.Records { - if record.Type == "TXT" && record.Value == info.Value { - return record.Id, nil +func (d *DNSProvider) findRecordID(zoneName, tokenValue string) (string, error) { + lrr := &baidudns.ListRecordRequest{} + + for { + recordResponse, err := d.client.ListRecord(zoneName, lrr) + if err != nil { + return "", fmt.Errorf("baiducloud: list record: %w", err) } + + for _, record := range recordResponse.Records { + if record.Type == "TXT" && record.Value == tokenValue { + return record.Id, nil + } + } + + if !recordResponse.IsTruncated { + break + } + + lrr.Marker = recordResponse.NextMarker } return "", errors.New("record not found") diff --git a/providers/dns/baiducloud/baiducloud.toml b/providers/dns/baiducloud/baiducloud.toml index 941d90b2c..8422eafd5 100644 --- a/providers/dns/baiducloud/baiducloud.toml +++ b/providers/dns/baiducloud/baiducloud.toml @@ -17,7 +17,7 @@ lego --email you@example.com --dns baiducloud -d '*.example.com' -d example.com [Configuration.Additional] BAIDUCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" BAIDUCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BAIDUCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + BAIDUCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" [Links] API = "https://cloud.baidu.com/doc/DNS/s/El4s7lssr" From a8226a67133d2127f8b309e36210af7bd5002e5f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 14 Nov 2025 14:12:57 +0100 Subject: [PATCH 09/95] namecheap: add experimental proxy support (#2715) --- .golangci.yml | 10 +-- providers/dns/internal/clientdebug/client.go | 3 + providers/dns/namecheap/namecheap.go | 3 +- providers/dns/namecheap/transport.go | 71 ++++++++++++++++++++ providers/dns/namecheap/transport_test.go | 39 +++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 providers/dns/namecheap/transport.go create mode 100644 providers/dns/namecheap/transport_test.go diff --git a/.golangci.yml b/.golangci.yml index a6f0c4bfa..2b4bcc41b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -216,11 +216,7 @@ linters: text: load is a global variable linters: - gochecknoglobals - - path: providers/dns/([\d\w]+/)*[\d\w]+_test.go - text: envTest is a global variable - linters: - - gochecknoglobals - - path: providers/http/([\d\w]+/)*[\d\w]+_test.go + - path: providers/(dns|http)/([\d\w]+/)*[\d\w]+_test.go text: envTest is a global variable linters: - gochecknoglobals @@ -228,6 +224,10 @@ linters: text: testCases is a global variable linters: - gochecknoglobals + - path: providers/dns/namecheap/transport.go + text: (envProxyOnce|envProxyFuncValue) is a global variable + linters: + - gochecknoglobals - path: providers/dns/acmedns/mock_test.go text: egTestAccount is a global variable linters: diff --git a/providers/dns/internal/clientdebug/client.go b/providers/dns/internal/clientdebug/client.go index ad2a06405..342577b93 100644 --- a/providers/dns/internal/clientdebug/client.go +++ b/providers/dns/internal/clientdebug/client.go @@ -91,6 +91,9 @@ func (d *DumpTransport) RoundTrip(h *http.Request) (*http.Response, error) { _, _ = fmt.Fprintln(d.writer, d.redact(data)) resp, err := d.rt.RoundTrip(h) + if err != nil { + return nil, err + } data, _ = httputil.DumpResponse(resp, true) diff --git a/providers/dns/namecheap/namecheap.go b/providers/dns/namecheap/namecheap.go index cf8520546..54640f8e0 100644 --- a/providers/dns/namecheap/namecheap.go +++ b/providers/dns/namecheap/namecheap.go @@ -76,7 +76,8 @@ func NewDefaultConfig() *Config { PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, time.Hour), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), + Transport: defaultTransport(envNamespace), }, } } diff --git a/providers/dns/namecheap/transport.go b/providers/dns/namecheap/transport.go new file mode 100644 index 000000000..584dc6e50 --- /dev/null +++ b/providers/dns/namecheap/transport.go @@ -0,0 +1,71 @@ +package namecheap + +import ( + "net/http" + "net/url" + "strings" + "sync" + + "github.com/go-acme/lego/v4/platform/config/env" + "golang.org/x/net/http/httpproxy" +) + +const ( + envHTTPProxy = "HTTP_PROXY" + envHTTPProxyLower = "http_proxy" + envHTTPSProxy = "HTTPS_PROXY" + envHTTPSProxyLower = "https_proxy" + envNoProxy = "NO_PROXY" + envNoProxyLower = "no_proxy" + envRequestMethod = "REQUEST_METHOD" +) + +// Allows lazy loading of the proxy. +var ( + envProxyOnce sync.Once + envProxyFuncValue func(*url.URL) (*url.URL, error) +) + +func defaultTransport(namespace string) http.RoundTripper { + tr, ok := http.DefaultTransport.(*http.Transport) + if !ok { + return nil + } + + clone := tr.Clone() + clone.Proxy = proxyFromEnvironment(namespace) + + return clone +} + +// Inspired by: +// - https://pkg.go.dev/net/http#ProxyFromEnvironment +// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment +func envProxyFunc(namespace string) func(*url.URL) (*url.URL, error) { + envProxyOnce.Do(func() { + cfg := &httpproxy.Config{ + HTTPProxy: getEnv(namespace, envHTTPProxy, envHTTPProxyLower), + HTTPSProxy: getEnv(namespace, envHTTPSProxy, envHTTPSProxyLower), + NoProxy: getEnv(namespace, envNoProxy, envNoProxyLower), + CGI: env.GetOneWithFallback(namespace+envRequestMethod, "", env.ParseString, envRequestMethod) != "", + } + + envProxyFuncValue = cfg.ProxyFunc() + }) + + return envProxyFuncValue +} + +// Inspired by: +// - https://pkg.go.dev/net/http#ProxyFromEnvironment +// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment +func proxyFromEnvironment(namespace string) func(req *http.Request) (*url.URL, error) { + return func(req *http.Request) (*url.URL, error) { + return envProxyFunc(namespace)(req.URL) + } +} + +func getEnv(namespace, baseEnvName, baseEnvNameLower string) string { + return env.GetOneWithFallback(namespace+baseEnvName, "", env.ParseString, + strings.ToLower(namespace)+baseEnvNameLower, baseEnvName, baseEnvNameLower) +} diff --git a/providers/dns/namecheap/transport_test.go b/providers/dns/namecheap/transport_test.go new file mode 100644 index 000000000..cd3e9ff17 --- /dev/null +++ b/providers/dns/namecheap/transport_test.go @@ -0,0 +1,39 @@ +package namecheap + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_defaultTransport(t *testing.T) { + client := servermock.NewBuilder( + func(server *httptest.Server) (*http.Client, error) { + cl := server.Client() + + t.Setenv("NAMECHEAP_HTTP_PROXY", server.URL) + + cl.Transport = defaultTransport(envNamespace) + + return cl, nil + }). + Route("/", + servermock.Noop().WithStatusCode(http.StatusTeapot)). + Build(t) + + req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + + t.Cleanup(func() { + _ = resp.Body.Close() + }) + + assert.Equal(t, http.StatusTeapot, resp.StatusCode) +} From ea8aca4366818773d79540a6fc2da7aff1cc20bb Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 14 Nov 2025 14:33:54 +0100 Subject: [PATCH 10/95] Add DNS provider for AlibabaCloud ESA (#2703) --- README.md | 84 ++++----- cmd/zz_gen_cmd_dnshelp.go | 24 +++ docs/content/dns/zz_gen_aliesa.md | 78 ++++++++ docs/data/zz_cli_help.toml | 2 +- go.mod | 1 + go.sum | 2 + providers/dns/aliesa/aliesa.go | 251 ++++++++++++++++++++++++++ providers/dns/aliesa/aliesa.toml | 33 ++++ providers/dns/aliesa/aliesa_test.go | 151 ++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 10 files changed, 586 insertions(+), 43 deletions(-) create mode 100644 docs/content/dns/zz_gen_aliesa.md create mode 100644 providers/dns/aliesa/aliesa.go create mode 100644 providers/dns/aliesa/aliesa.toml create mode 100644 providers/dns/aliesa/aliesa_test.go diff --git a/README.md b/README.md index 59fb983eb..aadf28eb3 100644 --- a/README.md +++ b/README.md @@ -62,212 +62,212 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Active24 Akamai EdgeDNS Alibaba Cloud DNS - all-inkl + AlibabaCloud ESA + all-inkl Amazon Lightsail Amazon Route 53 Anexia CloudDNS - ArvanCloud + ArvanCloud Aurora DNS Autodns Axelname - Azion + Azion Azure (deprecated) Azure DNS Baidu Cloud - Beget.com + Beget.com Binary Lane Bindman Bluecat - BookMyName + BookMyName Brandit (deprecated) Bunny Checkdomain - Civo + Civo Cloud.ru CloudDNS Cloudflare - ClouDNS + ClouDNS CloudXNS (Deprecated) ConoHa v2 ConoHa v3 - Constellix + Constellix Core-Networks CPanel/WHM Derak Cloud - deSEC.io + deSEC.io Designate DNSaaS for Openstack Digital Ocean DirectAdmin - DNS Made Easy + DNS Made Easy dnsHome.de DNSimple DNSPod (deprecated) - Domain Offensive (do.de) + Domain Offensive (do.de) Domeneshop DreamHost Duck DNS - Dyn + Dyn DynDnsFree.de Dynu EasyDNS - EdgeCenter + EdgeCenter Efficient IP Epik Exoscale - External program + External program F5 XC freemyip.com G-Core - Gandi + Gandi Gandi Live DNS (v5) Glesys Go Daddy - Google Cloud + Google Cloud Google Domains Hetzner Hosting.de - Hostinger + Hostinger Hosttech HTTP request http.net - Huawei Cloud + Huawei Cloud Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) - IIJ DNS Platform Service + IIJ DNS Platform Service Infoblox Infomaniak Internet Initiative Japan - Internet.bs + Internet.bs INWX Ionos IPv64 - iwantmyname (Deprecated) + iwantmyname (Deprecated) Joker Joohoi's ACME-DNS KeyHelp - Liara + Liara Lima-City Linode (v4) Liquid Web - Loopia + Loopia LuaDNS Mail-in-a-Box ManageEngine CloudDNS - Manual + Manual Metaname Metaregistrar mijn.host - Mittwald + Mittwald myaddr.{tools,dev,io} MyDNS.jp MythicBeasts - Name.com + Name.com Namecheap Namesilo NearlyFreeSpeech.NET - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TransIP UKFast SafeDNS Ultradns - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS VK Cloud Volcano Engine/火山引擎 Vscale - Vultr + Vultr webnames.ca webnames.ru Websupport - WEDOS + WEDOS West.cn/西部数码 Yandex 360 Yandex Cloud - Yandex PDD + Yandex PDD Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 24a2470bb..77f9a4176 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -16,6 +16,7 @@ func allDNSCodes() string { "acme-dns", "active24", "alidns", + "aliesa", "allinkl", "anexia", "arvancloud", @@ -252,6 +253,29 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`) + case "aliesa": + // generated from: providers/dns/aliesa/aliesa.toml + ew.writeln(`Configuration for AlibabaCloud ESA.`) + ew.writeln(`Code: 'aliesa'`) + ew.writeln(`Since: 'v4.29.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ALIESA_ACCESS_KEY": Access key ID`) + ew.writeln(` - "ALIESA_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`) + ew.writeln(` - "ALIESA_SECRET_KEY": Access Key secret`) + ew.writeln(` - "ALIESA_SECURITY_TOKEN": STS Security Token (optional)`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ALIESA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ALIESA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ALIESA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ALIESA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/aliesa`) + case "allinkl": // generated from: providers/dns/allinkl/allinkl.toml ew.writeln(`Configuration for all-inkl.`) diff --git a/docs/content/dns/zz_gen_aliesa.md b/docs/content/dns/zz_gen_aliesa.md new file mode 100644 index 000000000..b286a718a --- /dev/null +++ b/docs/content/dns/zz_gen_aliesa.md @@ -0,0 +1,78 @@ +--- +title: "AlibabaCloud ESA" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: aliesa +dnsprovider: + since: "v4.29.0" + code: "aliesa" + url: "https://www.alibabacloud.com/en/product/esa" +--- + + + + + + +Configuration for [AlibabaCloud ESA](https://www.alibabacloud.com/en/product/esa). + + + + +- Code: `aliesa` +- Since: v4.29.0 + + +Here is an example bash command using the AlibabaCloud ESA provider: + +```bash +# Setup using instance RAM role +ALIESA_RAM_ROLE=lego \ +lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run + +# Or, using credentials +ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ +ALIESA_SECRET_KEY=your-secret-key \ +ALIESA_SECURITY_TOKEN=your-sts-token \ +lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ALIESA_ACCESS_KEY` | Access key ID | +| `ALIESA_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) | +| `ALIESA_SECRET_KEY` | Access Key secret | +| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) | + +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 | +|--------------------------------|-------------| +| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records) +- [Go client](https://github.com/alibabacloud-go/esa-20240910) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 071c0f344..62ed20102 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, allinkl, 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, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, 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/go.mod b/go.mod index 3dc72c1ff..702da550f 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/dnsimple/dnsimple-go/v4 v4.0.0 github.com/exoscale/egoscale/v3 v3.1.27 github.com/go-acme/alidns-20150109/v4 v4.6.1 + github.com/go-acme/esa-20240910/v2 v2.40.1 github.com/go-acme/tencentclouddnspod v1.1.10 github.com/go-acme/tencentedgdeone v1.1.48 github.com/go-jose/go-jose/v4 v4.1.3 diff --git a/go.sum b/go.sum index a8d61029d..337db59b3 100644 --- a/go.sum +++ b/go.sum @@ -314,6 +314,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/alidns-20150109/v4 v4.6.1 h1:Dch3aWRcw4U62+jKPjPQN3iW3TPvgIywATbvHzojXeo= github.com/go-acme/alidns-20150109/v4 v4.6.1/go.mod h1:RBcqBA5IvUWtlpjx6dC6EkPVyBNLQ+mR18XoaP38BFY= +github.com/go-acme/esa-20240910/v2 v2.40.1 h1:pog3UlF5d+3LPoo1L8u8PqB189recIXX8T7pGoEz18A= +github.com/go-acme/esa-20240910/v2 v2.40.1/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= github.com/go-acme/tencentclouddnspod v1.1.10 h1:ERVJ4mc3cT4Nb3+n6H/c1AwZnChGBqLoymE0NVYscKI= github.com/go-acme/tencentclouddnspod v1.1.10/go.mod h1:Bo/0YQJ/99FM+44HmCQkByuptX1tJsJ9V14MGV/2Qco= github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk= diff --git a/providers/dns/aliesa/aliesa.go b/providers/dns/aliesa/aliesa.go new file mode 100644 index 000000000..deb8162da --- /dev/null +++ b/providers/dns/aliesa/aliesa.go @@ -0,0 +1,251 @@ +// Package aliesa implements a DNS provider for solving the DNS-01 challenge using AlibabaCloud ESA. +package aliesa + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + "github.com/alibabacloud-go/tea/dara" + "github.com/aliyun/credentials-go/credentials" + esa "github.com/go-acme/esa-20240910/v2/client" + "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/ptr" +) + +// Environment variables names. +const ( + envNamespace = "ALIESA_" + + EnvRAMRole = envNamespace + "RAM_ROLE" + EnvAccessKey = envNamespace + "ACCESS_KEY" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvSecurityToken = envNamespace + "SECURITY_TOKEN" + EnvRegionID = envNamespace + "REGION_ID" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const defaultRegionID = "cn-hangzhou" + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + RAMRole string + APIKey string + SecretKey string + SecurityToken string + RegionID 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, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *esa.Client + + recordIDs map[string]int64 + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for AlibabaCloud ESA. +func NewDNSProvider() (*DNSProvider, error) { + config := NewDefaultConfig() + config.RegionID = env.GetOrFile(EnvRegionID) + + values, err := env.Get(EnvRAMRole) + if err == nil { + config.RAMRole = values[EnvRAMRole] + return NewDNSProviderConfig(config) + } + + values, err = env.Get(EnvAccessKey, EnvSecretKey) + if err != nil { + return nil, fmt.Errorf("aliesa: %w", err) + } + + config.APIKey = values[EnvAccessKey] + config.SecretKey = values[EnvSecretKey] + config.SecurityToken = env.GetOrFile(EnvSecurityToken) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for AlibabaCloud ESA. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("aliesa: the configuration of the DNS provider is nil") + } + + if config.RegionID == "" { + config.RegionID = defaultRegionID + } + + cfg := new(openapi.Config). + SetRegionId(config.RegionID). + SetReadTimeout(int(config.HTTPTimeout.Milliseconds())) + + switch { + case config.RAMRole != "": + // https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance + credentialsCfg := new(credentials.Config). + SetType("ecs_ram_role"). + SetRoleName(config.RAMRole) + + credentialClient, err := credentials.NewCredential(credentialsCfg) + if err != nil { + return nil, fmt.Errorf("aliesa: new credential: %w", err) + } + + cfg = cfg.SetCredential(credentialClient) + + case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "": + cfg = cfg. + SetAccessKeyId(config.APIKey). + SetAccessKeySecret(config.SecretKey). + SetSecurityToken(config.SecurityToken) + + case config.APIKey != "" && config.SecretKey != "": + cfg = cfg. + SetAccessKeyId(config.APIKey). + SetAccessKeySecret(config.SecretKey) + + default: + return nil, errors.New("aliesa: ram role or credentials missing") + } + + client, err := esa.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("aliesa: new client: %w", err) + } + + // Workaround to get a regional URL. + // https://github.com/alibabacloud-go/esa-20240910/blame/7660e3aab2045d4820e4b83427a154efe0c79319/client/client.go#L27 + // The `EndpointRule` is hardcoded with an empty string, so the region is ignored. + client.Endpoint = nil + client.EndpointRule = ptr.Pointer("regional") + + client.Endpoint, err = esa.GetEndpoint(client, dara.String("esa"), client.RegionId, client.EndpointRule, client.Network, client.Suffix, client.EndpointMap, client.Endpoint) + if err != nil { + return nil, fmt.Errorf("aliesa: get endpoint: %w", err) + } + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int64), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + siteID, err := d.getSiteID(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("aliesa: %w", err) + } + + crReq := new(esa.CreateRecordRequest). + SetSiteId(siteID). + SetType("TXT"). + SetRecordName(dns01.UnFqdn(info.EffectiveFQDN)). + SetTtl(int32(d.config.TTL)). + SetData(new(esa.CreateRecordRequestData).SetValue(info.Value)) + + // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord + crResp, err := esa.CreateRecordWithContext(ctx, d.client, crReq, &dara.RuntimeOptions{}) + if err != nil { + return fmt.Errorf("aliesa: create record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = ptr.Deref(crResp.Body.GetRecordId()) + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("aliesa: unknown record ID for '%s'", info.EffectiveFQDN) + } + + drReq := new(esa.DeleteRecordRequest). + SetRecordId(recordID) + + // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord + _, err := esa.DeleteRecordWithContext(ctx, d.client, drReq, &dara.RuntimeOptions{}) + if err != nil { + return fmt.Errorf("aliesa: delete record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getSiteID(ctx context.Context, fqdn string) (int64, error) { + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return 0, fmt.Errorf("aliesa: could not find zone for domain %q: %w", fqdn, err) + } + + lsReq := new(esa.ListSitesRequest). + SetSiteName(dns01.UnFqdn(authZone)). + SetSiteSearchType("suffix") + + // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-listsites + lsResp, err := esa.ListSitesWithContext(ctx, d.client, lsReq, &dara.RuntimeOptions{}) + if err != nil { + return 0, fmt.Errorf("list sites: %w", err) + } + + for f := range dns01.UnFqdnDomainsSeq(fqdn) { + domain := dns01.UnFqdn(f) + + for _, site := range lsResp.Body.GetSites() { + if ptr.Deref(site.GetSiteName()) == domain { + return ptr.Deref(site.GetSiteId()), nil + } + } + } + + return 0, fmt.Errorf("site not found (fqdn: %q)", fqdn) +} diff --git a/providers/dns/aliesa/aliesa.toml b/providers/dns/aliesa/aliesa.toml new file mode 100644 index 000000000..d0f6cdb91 --- /dev/null +++ b/providers/dns/aliesa/aliesa.toml @@ -0,0 +1,33 @@ +Name = "AlibabaCloud ESA" +Description = '''''' +URL = "https://www.alibabacloud.com/en/product/esa" +Code = "aliesa" +Since = "v4.29.0" + +Example = ''' +# Setup using instance RAM role +ALIESA_RAM_ROLE=lego \ +lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run + +# Or, using credentials +ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ +ALIESA_SECRET_KEY=your-secret-key \ +ALIESA_SECURITY_TOKEN=your-sts-token \ +lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ALIESA_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)" + ALIESA_ACCESS_KEY = "Access key ID" + ALIESA_SECRET_KEY = "Access Key secret" + ALIESA_SECURITY_TOKEN = "STS Security Token (optional)" + [Configuration.Additional] + ALIESA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ALIESA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ALIESA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ALIESA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records" + GoClient = "https://github.com/alibabacloud-go/esa-20240910" diff --git a/providers/dns/aliesa/aliesa_test.go b/providers/dns/aliesa/aliesa_test.go new file mode 100644 index 000000000..025529409 --- /dev/null +++ b/providers/dns/aliesa/aliesa_test.go @@ -0,0 +1,151 @@ +package aliesa + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAccessKey, + EnvSecretKey, + EnvRAMRole). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAccessKey: "123", + EnvSecretKey: "456", + }, + }, + { + desc: "success (RAM role)", + envVars: map[string]string{ + EnvRAMRole: "LegoInstanceRole", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAccessKey: "", + EnvSecretKey: "", + }, + expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY,ALIESA_SECRET_KEY", + }, + { + desc: "missing access key", + envVars: map[string]string{ + EnvAccessKey: "", + EnvSecretKey: "456", + }, + expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY", + }, + { + desc: "missing secret key", + envVars: map[string]string{ + EnvAccessKey: "123", + EnvSecretKey: "", + }, + expected: "aliesa: some credentials information are missing: ALIESA_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 + ramRole string + apiKey string + secretKey string + expected string + }{ + { + desc: "success", + apiKey: "123", + secretKey: "456", + }, + { + desc: "success", + ramRole: "LegoInstanceRole", + }, + { + desc: "missing credentials", + expected: "aliesa: ram role or credentials missing", + }, + { + desc: "missing api key", + secretKey: "456", + expected: "aliesa: ram role or credentials missing", + }, + { + desc: "missing secret key", + apiKey: "123", + expected: "aliesa: ram role or credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.SecretKey = test.secretKey + config.RAMRole = test.ramRole + + 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) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 90bb2e13c..ff6ab0c28 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -10,6 +10,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/acmedns" "github.com/go-acme/lego/v4/providers/dns/active24" "github.com/go-acme/lego/v4/providers/dns/alidns" + "github.com/go-acme/lego/v4/providers/dns/aliesa" "github.com/go-acme/lego/v4/providers/dns/allinkl" "github.com/go-acme/lego/v4/providers/dns/anexia" "github.com/go-acme/lego/v4/providers/dns/arvancloud" @@ -186,6 +187,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return active24.NewDNSProvider() case "alidns": return alidns.NewDNSProvider() + case "aliesa": + return aliesa.NewDNSProvider() case "allinkl": return allinkl.NewDNSProvider() case "anexia": From 57c14f8d2ab22bcd3d3bc7e36827f2b833da001f Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Fri, 14 Nov 2025 18:35:15 +0100 Subject: [PATCH 11/95] chore: add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..8b1690de5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + From 0abf391bd1db8a24cf7b1910ff4be514d8581cd9 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 19 Nov 2025 01:27:40 +0100 Subject: [PATCH 12/95] docs: remove author names --- docs/hugo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hugo.toml b/docs/hugo.toml index b17206d43..fe076a306 100644 --- a/docs/hugo.toml +++ b/docs/hugo.toml @@ -15,6 +15,7 @@ title = "Lego" custom_css = ["css/theme-custom.css"] disableLandingPageButton = true hideAuthorEmail = true + hideAuthorName = true # Author of the site, will be used in meta information [params.author] From 93b8bb71ca5e6de001b28e3d01c83f4362ca851b Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sat, 22 Nov 2025 01:11:55 +0100 Subject: [PATCH 13/95] hetzner: use int64 for IDs (#2720) --- providers/dns/hetzner/internal/hetznerv1/hetznerv1.go | 2 +- providers/dns/hetzner/internal/hetznerv1/internal/client.go | 4 ++-- .../dns/hetzner/internal/hetznerv1/internal/client_test.go | 6 +++--- .../hetznerv1/internal/fixtures/add_rrset_records.json | 2 +- .../internal/hetznerv1/internal/fixtures/get_action.json | 4 ++-- providers/dns/hetzner/internal/hetznerv1/internal/types.go | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go b/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go index 4fb95eb6f..b31c766ce 100644 --- a/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go +++ b/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go @@ -184,7 +184,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) waitAction(ctx context.Context, actionID int) error { +func (d *DNSProvider) waitAction(ctx context.Context, actionID int64) error { return wait.Retry(ctx, func() error { result, err := d.client.GetAction(ctx, actionID) diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/client.go b/providers/dns/hetzner/internal/hetznerv1/internal/client.go index 35c3d461b..2f29f642a 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/client.go +++ b/providers/dns/hetzner/internal/hetznerv1/internal/client.go @@ -85,8 +85,8 @@ func (c *Client) RemoveRRSetRecords(ctx context.Context, zoneIDName, recordType, // GetAction gets an action. // https://docs.hetzner.cloud/reference/cloud#actions-get-an-action -func (c *Client) GetAction(ctx context.Context, id int) (*Action, error) { - endpoint := c.BaseURL.JoinPath("actions", strconv.Itoa(id)) +func (c *Client) GetAction(ctx context.Context, id int64) (*Action, error) { + endpoint := c.BaseURL.JoinPath("actions", strconv.FormatInt(id, 10)) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go b/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go index fcbc7636f..6fd3d77a7 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go +++ b/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go @@ -49,7 +49,7 @@ func TestClient_AddRRSetRecords(t *testing.T) { Command: "add_rrset_records", Status: "running", Progress: 50, - Resources: []Resources{{ID: 42, Type: "zone"}}, + Resources: []Resources{{ID: 590000000000000, Type: "zone"}}, } assert.Equal(t, expected, result) @@ -139,11 +139,11 @@ func TestClient_GetAction(t *testing.T) { require.NoError(t, err) expected := &Action{ - ID: 42, + ID: 590000000000000, Command: "start_resource", Status: "running", Progress: 100, - Resources: []Resources{{ID: 42, Type: "server"}}, + Resources: []Resources{{ID: 590000000000000, Type: "server"}}, ErrorInfo: &ErrorInfo{ Code: "action_failed", Message: "Action failed", diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json index 2341c7e6e..7267b02cb 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json +++ b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json @@ -8,7 +8,7 @@ "finished": null, "resources": [ { - "id": 42, + "id": 590000000000000, "type": "zone" } ], diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json index 05f003b1e..19278fc51 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json +++ b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json @@ -1,6 +1,6 @@ { "action": { - "id": 42, + "id": 590000000000000, "command": "start_resource", "status": "running", "started": "2016-01-30T23:55:00+00:00", @@ -8,7 +8,7 @@ "progress": 100, "resources": [ { - "id": 42, + "id": 590000000000000, "type": "server" } ], diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/types.go b/providers/dns/hetzner/internal/hetznerv1/internal/types.go index 08d1684c0..47e8a3f91 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/types.go +++ b/providers/dns/hetzner/internal/hetznerv1/internal/types.go @@ -79,7 +79,7 @@ type ActionResponse struct { } type Action struct { - ID int `json:"id,omitempty"` + ID int64 `json:"id,omitempty"` Command string `json:"command,omitempty"` // It can be: `running`, `success`, `error`. @@ -93,6 +93,6 @@ type Action struct { } type Resources struct { - ID int `json:"id,omitempty"` + ID int64 `json:"id,omitempty"` Type string `json:"type,omitempty"` } From aea6afe2d66f403096b96d563ed8c572cc7c3f33 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 24 Nov 2025 18:44:43 +0100 Subject: [PATCH 14/95] Add DNS provider for Gigahost.no (#2723) --- README.md | 57 ++-- cmd/zz_gen_cmd_dnshelp.go | 23 ++ docs/content/dns/zz_gen_gigahostno.md | 70 +++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/gigahostno/gigahostno.go | 233 +++++++++++++++ providers/dns/gigahostno/gigahostno.toml | 25 ++ providers/dns/gigahostno/gigahostno_test.go | 277 ++++++++++++++++++ providers/dns/gigahostno/internal/client.go | 172 +++++++++++ .../dns/gigahostno/internal/client_test.go | 179 +++++++++++ .../fixtures/authenticate-request.json | 4 + .../internal/fixtures/authenticate.json | 23 ++ .../fixtures/create_record-request.json | 6 + .../internal/fixtures/create_record.json | 7 + .../internal/fixtures/delete_record.json | 7 + .../gigahostno/internal/fixtures/error.json | 9 + .../internal/fixtures/zone_records.json | 39 +++ .../gigahostno/internal/fixtures/zones.json | 97 ++++++ providers/dns/gigahostno/internal/identity.go | 122 ++++++++ .../dns/gigahostno/internal/identity_test.go | 108 +++++++ providers/dns/gigahostno/internal/types.go | 83 ++++++ providers/dns/zz_gen_dns_providers.go | 3 + 21 files changed, 1519 insertions(+), 27 deletions(-) create mode 100644 docs/content/dns/zz_gen_gigahostno.md create mode 100644 providers/dns/gigahostno/gigahostno.go create mode 100644 providers/dns/gigahostno/gigahostno.toml create mode 100644 providers/dns/gigahostno/gigahostno_test.go create mode 100644 providers/dns/gigahostno/internal/client.go create mode 100644 providers/dns/gigahostno/internal/client_test.go create mode 100644 providers/dns/gigahostno/internal/fixtures/authenticate-request.json create mode 100644 providers/dns/gigahostno/internal/fixtures/authenticate.json create mode 100644 providers/dns/gigahostno/internal/fixtures/create_record-request.json create mode 100644 providers/dns/gigahostno/internal/fixtures/create_record.json create mode 100644 providers/dns/gigahostno/internal/fixtures/delete_record.json create mode 100644 providers/dns/gigahostno/internal/fixtures/error.json create mode 100644 providers/dns/gigahostno/internal/fixtures/zone_records.json create mode 100644 providers/dns/gigahostno/internal/fixtures/zones.json create mode 100644 providers/dns/gigahostno/internal/identity.go create mode 100644 providers/dns/gigahostno/internal/identity_test.go create mode 100644 providers/dns/gigahostno/internal/types.go diff --git a/README.md b/README.md index aadf28eb3..d40afc579 100644 --- a/README.md +++ b/README.md @@ -136,138 +136,143 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Gandi Gandi Live DNS (v5) + Gigahost.no Glesys - Go Daddy + Go Daddy Google Cloud Google Domains Hetzner - Hosting.de + Hosting.de Hostinger Hosttech HTTP request - http.net + http.net Huawei Cloud Hurricane Electric DNS HyperOne - IBM Cloud (SoftLayer) + IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox Infomaniak - Internet Initiative Japan + Internet Initiative Japan Internet.bs INWX Ionos - IPv64 + IPv64 iwantmyname (Deprecated) Joker Joohoi's ACME-DNS - KeyHelp + KeyHelp Liara Lima-City Linode (v4) - Liquid Web + Liquid Web Loopia LuaDNS Mail-in-a-Box - ManageEngine CloudDNS + ManageEngine CloudDNS Manual Metaname Metaregistrar - mijn.host + mijn.host Mittwald myaddr.{tools,dev,io} MyDNS.jp - MythicBeasts + MythicBeasts Name.com Namecheap Namesilo - NearlyFreeSpeech.NET + NearlyFreeSpeech.NET Netcup Netlify Nicmanager - NIFCloud + NIFCloud Njalla Nodion NS1 - Octenium + Octenium Open Telekom Cloud Oracle Cloud OVH - plesk.com + plesk.com Porkbun PowerDNS Rackspace - Rain Yun/雨云 + Rain Yun/雨云 RcodeZero reg.ru Regfish - RFC2136 + RFC2136 RimuHosting RU CENTER Sakura Cloud - Scaleway + Scaleway Selectel Selectel v2 SelfHost.(de|eu) - Servercow + Servercow Shellrent Simply.com Sonic - Spaceship + Spaceship Stackpath Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TransIP UKFast SafeDNS - Ultradns + Ultradns Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 77f9a4176..deb93e6eb 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -76,6 +76,7 @@ func allDNSCodes() string { "gandiv5", "gcloud", "gcore", + "gigahostno", "glesys", "godaddy", "googledomains", @@ -1550,6 +1551,28 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gcore`) + case "gigahostno": + // generated from: providers/dns/gigahostno/gigahostno.toml + ew.writeln(`Configuration for Gigahost.no.`) + ew.writeln(`Code: 'gigahostno'`) + ew.writeln(`Since: 'v4.29.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "GIGAHOSTNO_PASSWORD": Password`) + ew.writeln(` - "GIGAHOSTNO_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "GIGAHOSTNO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "GIGAHOSTNO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "GIGAHOSTNO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "GIGAHOSTNO_SECRET": TOTP secret`) + ew.writeln(` - "GIGAHOSTNO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/gigahostno`) + case "glesys": // generated from: providers/dns/glesys/glesys.toml ew.writeln(`Configuration for Glesys.`) diff --git a/docs/content/dns/zz_gen_gigahostno.md b/docs/content/dns/zz_gen_gigahostno.md new file mode 100644 index 000000000..afb7c64c9 --- /dev/null +++ b/docs/content/dns/zz_gen_gigahostno.md @@ -0,0 +1,70 @@ +--- +title: "Gigahost.no" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: gigahostno +dnsprovider: + since: "v4.29.0" + code: "gigahostno" + url: "https://gigahost.no/" +--- + + + + + + +Configuration for [Gigahost.no](https://gigahost.no/). + + + + +- Code: `gigahostno` +- Since: v4.29.0 + + +Here is an example bash command using the Gigahost.no provider: + +```bash +GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ +GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ +lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `GIGAHOSTNO_PASSWORD` | Password | +| `GIGAHOSTNO_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 | +|--------------------------------|-------------| +| `GIGAHOSTNO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `GIGAHOSTNO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `GIGAHOSTNO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `GIGAHOSTNO_SECRET` | TOTP secret | +| `GIGAHOSTNO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://gigahost.no/api-dokumentasjon) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 62ed20102..552539fb0 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, 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, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, 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/gigahostno/gigahostno.go b/providers/dns/gigahostno/gigahostno.go new file mode 100644 index 000000000..b9ed23f3f --- /dev/null +++ b/providers/dns/gigahostno/gigahostno.go @@ -0,0 +1,233 @@ +// Package gigahostno implements a DNS provider for solving the DNS-01 challenge using Gigahost.no. +package gigahostno + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "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/gigahostno/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "GIGAHOSTNO_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + EnvSecret = envNamespace + "SECRET" + + 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 { + Username string + Password string + Secret string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + + identifier *internal.Identifier + client *internal.Client + + tokenMu sync.Mutex + token *internal.Token +} + +// NewDNSProvider returns a DNSProvider instance configured for Gigahost. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("gigahostno: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.Secret = env.GetOrFile(EnvSecret) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Gigahost. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("gigahostno: the configuration of the DNS provider is nil") + } + + identifier, err := internal.NewIdentifier(config.Username, config.Password, config.Secret) + if err != nil { + return nil, fmt.Errorf("gigahostno: %w", err) + } + + if config.HTTPClient != nil { + identifier.HTTPClient = config.HTTPClient + } + + identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient) + + client := internal.NewClient() + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + identifier: identifier, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + err := d.authenticate(ctx) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + ctx = internal.WithContext(ctx, d.token.Token) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + record := internal.Record{ + Name: subDomain, + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + } + + err = d.client.CreateNewRecord(ctx, zone.ID, record) + if err != nil { + return fmt.Errorf("gigahostno: create new record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + err := d.authenticate(ctx) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + ctx = internal.WithContext(ctx, d.token.Token) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("gigahostno: %w", err) + } + + records, err := d.client.GetZoneRecords(ctx, zone.ID) + if err != nil { + return fmt.Errorf("gigahostno: get zone records: %w", err) + } + + for _, record := range records { + if record.Type == "TXT" && record.Name == subDomain && record.Value == info.Value { + err := d.client.DeleteRecord(ctx, zone.ID, record.ID, record.Name, record.Type) + if err != nil { + return fmt.Errorf("gigahostno: delete record: %w", err) + } + + break + } + } + + 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 +} + +func (d *DNSProvider) authenticate(ctx context.Context) error { + d.tokenMu.Lock() + defer d.tokenMu.Unlock() + + if !d.token.IsExpired() { + return nil + } + + tok, err := d.identifier.Authenticate(ctx) + if err != nil { + return fmt.Errorf("authenticate: %w", err) + } + + d.token = tok + + return nil +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Zone, error) { + zones, err := d.client.GetZones(ctx) + if err != nil { + return nil, fmt.Errorf("get zones: %w", err) + } + + for d := range dns01.UnFqdnDomainsSeq(fqdn) { + for _, zone := range zones { + if zone.Name == d && zone.Active == "1" { + return &zone, nil + } + } + } + + return nil, fmt.Errorf("zone not found for %q", fqdn) +} diff --git a/providers/dns/gigahostno/gigahostno.toml b/providers/dns/gigahostno/gigahostno.toml new file mode 100644 index 000000000..689b96569 --- /dev/null +++ b/providers/dns/gigahostno/gigahostno.toml @@ -0,0 +1,25 @@ +Name = "Gigahost.no" +Description = '''''' +URL = "https://gigahost.no/" +Code = "gigahostno" +Since = "v4.29.0" + +Example = ''' +GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ +GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ +lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + GIGAHOSTNO_USERNAME = "Username" + GIGAHOSTNO_PASSWORD = "Password" + [Configuration.Additional] + GIGAHOSTNO_SECRET = "TOTP secret" + GIGAHOSTNO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + GIGAHOSTNO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + GIGAHOSTNO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + GIGAHOSTNO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://gigahost.no/api-dokumentasjon" diff --git a/providers/dns/gigahostno/gigahostno_test.go b/providers/dns/gigahostno/gigahostno_test.go new file mode 100644 index 000000000..7aaac0159 --- /dev/null +++ b/providers/dns/gigahostno/gigahostno_test.go @@ -0,0 +1,277 @@ +package gigahostno + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/gigahostno/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvUsername, + EnvPassword, + EnvSecret, +).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", + EnvSecret: "super-secret", + }, + }, + { + desc: "missing GIGAHOSTNO_USERNAME", + envVars: map[string]string{ + EnvPassword: "secret", + }, + expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME", + }, + { + desc: "missing GIGAHOSTNO_PASSWORD", + envVars: map[string]string{ + EnvUsername: "user", + }, + expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME,GIGAHOSTNO_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.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + password string + secret string + expected string + }{ + { + desc: "success", + username: "user", + password: "secret", + secret: "super-secret", + }, + { + desc: "missing username", + password: "secret", + expected: "gigahostno: credentials missing", + }, + { + desc: "missing password", + username: "user", + expected: "gigahostno: credentials missing", + }, + { + desc: "missing credentials", + expected: "gigahostno: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Username = test.username + config.Password = test.password + config.Secret = test.secret + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Username = "user" + config.Password = "secret" + config.Secret = "JBSWY3DPEHPK3PXP" + + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + p.identifier.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /authenticate", + servermock.ResponseFromInternal("authenticate.json")). + Route("GET /dns/zones", + servermock.ResponseFromInternal("zones.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secrettoken")). + Route("POST /dns/zones/123/records", + servermock.ResponseFromInternal("create_record.json"), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secrettoken")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_Present_token_not_expired(t *testing.T) { + provider := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromInternal("zones.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret-token")). + Route("POST /dns/zones/123/records", + servermock.ResponseFromInternal("create_record.json"), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret-token")). + Build(t) + + provider.token = &internal.Token{ + Token: "secret-token", + TokenExpire: 65322892800, // 2040-01-01 + CustomerID: "123", + } + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /authenticate", + servermock.ResponseFromInternal("authenticate.json")). + Route("GET /dns/zones", + servermock.ResponseFromInternal("zones.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secrettoken")). + Route("GET /dns/zones/123/records", + servermock.ResponseFromInternal("zone_records.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secrettoken")). + Route("DELETE /dns/zones/123/records/jkl012", + servermock.ResponseFromInternal("delete_record.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "_acme-challenge"). + With("type", "TXT"), + servermock.CheckHeader(). + WithAuthorization("Bearer secrettoken")). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp_token_not_expired(t *testing.T) { + provider := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromInternal("zones.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret-token")). + Route("GET /dns/zones/123/records", + servermock.ResponseFromInternal("zone_records.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret-token")). + Route("DELETE /dns/zones/123/records/jkl012", + servermock.ResponseFromInternal("delete_record.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "_acme-challenge"). + With("type", "TXT"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret-token")). + Build(t) + + provider.token = &internal.Token{ + Token: "secret-token", + TokenExpire: 65322892800, // 2040-01-01 + CustomerID: "123", + } + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/gigahostno/internal/client.go b/providers/dns/gigahostno/internal/client.go new file mode 100644 index 000000000..cfff3a7b8 --- /dev/null +++ b/providers/dns/gigahostno/internal/client.go @@ -0,0 +1,172 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.gigahost.no/api/v0" + +const authorizationHeader = "Authorization" + +// Client the Gigahost.no API client. +type Client struct { + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient() *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// GetZones returns all zones. +func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { + endpoint := c.BaseURL.JoinPath("dns", "zones") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result APIResponse[[]Zone] + + err = c.do(ctx, req, &result) + if err != nil { + return nil, err + } + + return result.Data, nil +} + +// GetZoneRecords returns all records for a zone. +func (c *Client) GetZoneRecords(ctx context.Context, zoneID string) ([]Record, error) { + endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result APIResponse[[]Record] + + err = c.do(ctx, req, &result) + if err != nil { + return nil, err + } + + return result.Data, nil +} + +// CreateNewRecord creates a new record. +func (c *Client) CreateNewRecord(ctx context.Context, zoneID string, record Record) error { + endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + return c.do(ctx, req, nil) +} + +// DeleteRecord deletes a record. +func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID, name, recordType string) error { + endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records", recordID) + + query := endpoint.Query() + query.Set("name", name) + query.Set("type", recordType) + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(ctx, req, nil) +} + +func (c *Client) do(ctx context.Context, req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set(authorizationHeader, "Bearer "+getToken(ctx)) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/gigahostno/internal/client_test.go b/providers/dns/gigahostno/internal/client_test.go new file mode 100644 index 000000000..aac65bceb --- /dev/null +++ b/providers/dns/gigahostno/internal/client_test.go @@ -0,0 +1,179 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client := NewClient() + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithAuthorization("Bearer secret"), + ) +} + +func TestClient_GetZones(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromFixture("zones.json")). + Build(t) + + zones, err := client.GetZones(mockContext(t)) + require.NoError(t, err) + + expected := []Zone{ + { + ID: "123", + Name: "example.com", + NameDisplay: "example.com", + Type: "NATIVE", + Active: "1", + Protected: "1", + IsRegistered: "1", + Updated: false, + CustomerID: "16030", + DomainRegistrar: "norid", + DomainStatus: "active", + DomainExpiryDate: "2026-11-23 15:17:38", + DomainAutoRenew: "1", + ExternalDNS: "0", + RecordCount: 4, + }, + { + ID: "226", + Name: "example.org", + NameDisplay: "example.org", + Type: "NATIVE", + Active: "1", + Protected: "1", + IsRegistered: "1", + Updated: false, + CustomerID: "16030", + DomainRegistrar: "norid", + DomainStatus: "active", + DomainExpiryDate: "2026-11-23 14:15:01", + DomainAutoRenew: "1", + ExternalDNS: "0", + RecordCount: 5, + }, + { + ID: "229", + Name: "example.xn--zckzah", + NameDisplay: "example.テスト", + Type: "NATIVE", + Active: "1", + Protected: "1", + IsRegistered: "1", + Updated: false, + CustomerID: "16030", + DomainRegistrar: "norid", + DomainStatus: "active", + DomainExpiryDate: "2026-12-01 12:40:48", + DomainAutoRenew: "1", + ExternalDNS: "0", + RecordCount: 4, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestClient_GetZones_error(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + _, err := client.GetZones(mockContext(t)) + require.EqualError(t, err, "401: 401 Unauthorized: 401 Unauthorized") +} + +func TestClient_GetZoneRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones/123/records", + servermock.ResponseFromFixture("zone_records.json")). + Build(t) + + zones, err := client.GetZoneRecords(mockContext(t), "123") + require.NoError(t, err) + + expected := []Record{ + { + ID: "abc123", + Name: "@", + Type: "A", + Value: "185.125.168.166", + TTL: 3600, + }, + { + ID: "def456", + Name: "www", + Type: "A", + Value: "185.125.168.166", + TTL: 3600, + }, + { + ID: "ghi789", + Name: "@", + Type: "MX", + Value: "mail.example.no", + TTL: 3600, + }, + { + ID: "jkl012", + Name: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestClient_CreateNewRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/zones/example.com/records", + servermock.ResponseFromFixture("create_record.json"), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). + Build(t) + + record := Record{ + Name: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + } + + err := client.CreateNewRecord(mockContext(t), "example.com", record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("/dns/zones/123/records/abc123", + servermock.ResponseFromFixture("delete_record.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "_acme-challenge"). + With("type", "TXT")). + Build(t) + + err := client.DeleteRecord(mockContext(t), "123", "abc123", "_acme-challenge", "TXT") + require.NoError(t, err) +} diff --git a/providers/dns/gigahostno/internal/fixtures/authenticate-request.json b/providers/dns/gigahostno/internal/fixtures/authenticate-request.json new file mode 100644 index 000000000..c641cd3e5 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/authenticate-request.json @@ -0,0 +1,4 @@ +{ + "username": "user", + "password": "secret" +} diff --git a/providers/dns/gigahostno/internal/fixtures/authenticate.json b/providers/dns/gigahostno/internal/fixtures/authenticate.json new file mode 100644 index 000000000..2c43ccbfe --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/authenticate.json @@ -0,0 +1,23 @@ +{ + "meta": { + "status": 200, + "status_message": "200 OK", + "maintenance": false + }, + "data": { + "token": "secrettoken", + "token_expire": 1577836800, + "customer_id": "16030", + "contact_id": "15182", + "customer_name": "Cloudline AS", + "contact_username": "test@example.com", + "contact_access_level": "admin", + "customer_address": "Grønland 14", + "customer_zipcode": "5918", + "customer_city": "Frekhaug", + "customer_province": "Vestland", + "ga_secret": "ga_secret", + "ga_enabled": "1", + "vat": 1 + } +} diff --git a/providers/dns/gigahostno/internal/fixtures/create_record-request.json b/providers/dns/gigahostno/internal/fixtures/create_record-request.json new file mode 100644 index 000000000..f8f0b5b11 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/create_record-request.json @@ -0,0 +1,6 @@ +{ + "record_name": "_acme-challenge", + "record_type": "TXT", + "record_value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "record_ttl": 120 +} diff --git a/providers/dns/gigahostno/internal/fixtures/create_record.json b/providers/dns/gigahostno/internal/fixtures/create_record.json new file mode 100644 index 000000000..9232677d7 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/create_record.json @@ -0,0 +1,7 @@ +{ + "meta": { + "status": 201, + "status_message": "201 Created", + "message": "Record created successfully." + } +} diff --git a/providers/dns/gigahostno/internal/fixtures/delete_record.json b/providers/dns/gigahostno/internal/fixtures/delete_record.json new file mode 100644 index 000000000..9d87f2f42 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/delete_record.json @@ -0,0 +1,7 @@ +{ + "meta": { + "status": 200, + "status_message": "200 OK", + "message": "Record deleted successfully." + } +} diff --git a/providers/dns/gigahostno/internal/fixtures/error.json b/providers/dns/gigahostno/internal/fixtures/error.json new file mode 100644 index 000000000..f2fcfd437 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/error.json @@ -0,0 +1,9 @@ +{ + "meta": { + "status": 401, + "status_message": "401 Unauthorized", + "maintenance": false, + "message": "401 Unauthorized" + }, + "data": [] +} diff --git a/providers/dns/gigahostno/internal/fixtures/zone_records.json b/providers/dns/gigahostno/internal/fixtures/zone_records.json new file mode 100644 index 000000000..e67ff83f4 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/zone_records.json @@ -0,0 +1,39 @@ +{ + "meta": { + "status": 200, + "status_message": "200 OK" + }, + "data": [ + { + "record_id": "abc123", + "record_name": "@", + "record_type": "A", + "record_value": "185.125.168.166", + "record_ttl": 3600, + "record_priority": null + }, + { + "record_id": "def456", + "record_name": "www", + "record_type": "A", + "record_value": "185.125.168.166", + "record_ttl": 3600, + "record_priority": null + }, + { + "record_id": "ghi789", + "record_name": "@", + "record_type": "MX", + "record_value": "mail.example.no", + "record_ttl": 3600, + "record_priority": 10 + }, + { + "record_id": "jkl012", + "record_name": "_acme-challenge", + "record_type": "TXT", + "record_value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "record_ttl": 120 + } + ] +} diff --git a/providers/dns/gigahostno/internal/fixtures/zones.json b/providers/dns/gigahostno/internal/fixtures/zones.json new file mode 100644 index 000000000..f4d927335 --- /dev/null +++ b/providers/dns/gigahostno/internal/fixtures/zones.json @@ -0,0 +1,97 @@ +{ + "meta": { + "status": 200, + "status_message": "200 OK", + "maintenance": false, + "message": "200 OK" + }, + "data": [ + { + "zone_id": "123", + "cust_id": "16030", + "order_id": "26117", + "zone_name": "example.com", + "zone_type": "NATIVE", + "zone_active": "1", + "zone_protected": "1", + "zone_is_registered": "1", + "domain_registrar": "norid", + "domain_status": "active", + "domain_registered_date": "2025-11-23 15:17:38", + "domain_expiry_date": "2026-11-23 15:17:38", + "domain_updated_date": "2025-11-23 16:17:38", + "domain_auto_renew": "1", + "domain_epp_id": "LEG2175D-NORID", + "domain_registrant_id": "CA19777O", + "domain_tech_id": "GH295R", + "domain_auth_info": "XXXXXXXXXXXXXXX", + "domain_locked": "0", + "domain_dnssec": "0", + "domain_dnssec_data": null, + "domain_protected_email": null, + "zone_created": "2025-11-23 16:17:29", + "zone_updated": false, + "external_dns": "0", + "record_count": 4, + "zone_name_display": "example.com" + }, + { + "zone_id": "226", + "cust_id": "16030", + "order_id": "26114", + "zone_name": "example.org", + "zone_type": "NATIVE", + "zone_active": "1", + "zone_protected": "1", + "zone_is_registered": "1", + "domain_registrar": "norid", + "domain_status": "active", + "domain_registered_date": "2025-11-23 14:15:01", + "domain_expiry_date": "2026-11-23 14:15:01", + "domain_updated_date": "2025-11-23 15:15:02", + "domain_auto_renew": "1", + "domain_epp_id": "TEO218D-NORID", + "domain_registrant_id": "CA19774O", + "domain_tech_id": "GH295R", + "domain_auth_info": "XXXXXXXXXXXXXX", + "domain_locked": "0", + "domain_dnssec": "0", + "domain_dnssec_data": null, + "domain_protected_email": null, + "zone_created": "2025-11-23 15:13:27", + "zone_updated": false, + "external_dns": "0", + "record_count": 5, + "zone_name_display": "example.org" + }, + { + "zone_id": "229", + "cust_id": "16030", + "order_id": "26119", + "zone_name": "example.xn--zckzah", + "zone_type": "NATIVE", + "zone_active": "1", + "zone_protected": "1", + "zone_is_registered": "1", + "domain_registrar": "norid", + "domain_status": "active", + "domain_registered_date": "2014-12-01 12:40:48", + "domain_expiry_date": "2026-12-01 12:40:48", + "domain_updated_date": "2025-11-23 15:37:36", + "domain_auto_renew": "1", + "domain_epp_id": "DIT1003D-NORID", + "domain_registrant_id": "DCA822O", + "domain_tech_id": "GH295R", + "domain_auth_info": "XXXXXXXXXXXXXX", + "domain_locked": "0", + "domain_dnssec": "0", + "domain_dnssec_data": null, + "domain_protected_email": null, + "zone_created": "2025-11-23 16:37:15", + "zone_updated": false, + "external_dns": "0", + "record_count": 4, + "zone_name_display": "example.\u30C6\u30B9\u30C8" + } + ] +} diff --git a/providers/dns/gigahostno/internal/identity.go b/providers/dns/gigahostno/internal/identity.go new file mode 100644 index 000000000..262dfabdd --- /dev/null +++ b/providers/dns/gigahostno/internal/identity.go @@ -0,0 +1,122 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + "github.com/pquerna/otp/totp" +) + +type token string + +const tokenKey token = "token" + +type Identifier struct { + username string + password string + Secret string + + BaseURL *url.URL + HTTPClient *http.Client +} + +func NewIdentifier(username, password, secret string) (*Identifier, error) { + if username == "" || password == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Identifier{ + username: username, + password: password, + Secret: secret, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Identifier) Authenticate(ctx context.Context) (*Token, error) { + endpoint := c.BaseURL.JoinPath("authenticate") + + auth := Auth{Username: c.username, Password: c.password} + + if c.Secret != "" { + tan, err := totp.GenerateCode(c.Secret, time.Now()) + if err != nil { + return nil, fmt.Errorf("generate TOTP: %w", err) + } + + auth.Code, err = strconv.Atoi(tan) + if err != nil { + return nil, fmt.Errorf("parse TOTP: %w", err) + } + } + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, auth) + if err != nil { + return nil, err + } + + var result APIResponse[*Token] + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result.Data, nil +} + +func (c *Identifier) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func WithContext(ctx context.Context, credential string) context.Context { + return context.WithValue(ctx, tokenKey, credential) +} + +func getToken(ctx context.Context) string { + credential, ok := ctx.Value(tokenKey).(string) + if !ok { + return "" + } + + return credential +} diff --git a/providers/dns/gigahostno/internal/identity_test.go b/providers/dns/gigahostno/internal/identity_test.go new file mode 100644 index 000000000..09d72746a --- /dev/null +++ b/providers/dns/gigahostno/internal/identity_test.go @@ -0,0 +1,108 @@ +package internal + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupIdentifierClient(server *httptest.Server) (*Identifier, error) { + client, err := NewIdentifier("user", "secret", "") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil +} + +func mockContext(t *testing.T) context.Context { + t.Helper() + + return context.WithValue(t.Context(), tokenKey, "secret") +} + +func TestIdentifier_Authenticate(t *testing.T) { + identifier := servermock.NewBuilder[*Identifier](setupIdentifierClient). + Route("POST /authenticate", + servermock.ResponseFromFixture("authenticate.json"), + servermock.CheckRequestJSONBodyFromFixture("authenticate-request.json")). + Build(t) + + token, err := identifier.Authenticate(context.Background()) + require.NoError(t, err) + + expected := &Token{ + Token: "secrettoken", + TokenExpire: 1577836800, + CustomerID: "16030", + ContactID: "15182", + CustomerName: "Cloudline AS", + ContactUsername: "test@example.com", + ContactAccessLevel: "admin", + CustomerAddress: "Grønland 14", + CustomerZipcode: "5918", + CustomerCity: "Frekhaug", + CustomerProvince: "Vestland", + GASecret: "ga_secret", + GAEnabled: "1", + VAT: 1, + } + + assert.Equal(t, expected, token) +} + +func TestToken_IsExpired(t *testing.T) { + testCases := []struct { + desc string + token *Token + assert assert.BoolAssertionFunc + }{ + { + desc: "nil", + assert: assert.True, + }, + { + desc: "empty", + token: &Token{}, + assert: assert.True, + }, + { + desc: "not expired", + token: &Token{ + TokenExpire: 65322892800, // 2040-01-01 + }, + assert: assert.False, + }, + { + desc: "now", + token: &Token{ + TokenExpire: time.Now().Unix(), + }, + assert: assert.True, + }, + { + desc: "now + 2 minutes", + token: &Token{ + TokenExpire: time.Now().Add(2 * time.Minute).Unix(), + }, + assert: assert.False, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + test.assert(t, test.token.IsExpired()) + }) + } +} diff --git a/providers/dns/gigahostno/internal/types.go b/providers/dns/gigahostno/internal/types.go new file mode 100644 index 000000000..cbb7b8b23 --- /dev/null +++ b/providers/dns/gigahostno/internal/types.go @@ -0,0 +1,83 @@ +package internal + +import ( + "fmt" + "time" +) + +type APIError struct { + Meta MetaData `json:"meta"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("%d: %s: %s", a.Meta.Status, a.Meta.StatusMessage, a.Meta.Message) +} + +type MetaData struct { + Status int `json:"status,omitempty"` + StatusMessage string `json:"status_message,omitempty"` + Maintenance bool `json:"maintenance"` + Message string `json:"message,omitempty"` +} + +type APIResponse[T any] struct { + Meta MetaData `json:"meta"` + Data T `json:"data,omitempty"` +} + +type Zone struct { + ID string `json:"zone_id,omitempty"` + Name string `json:"zone_name,omitempty"` + NameDisplay string `json:"zone_name_display,omitempty"` + Type string `json:"zone_type,omitempty"` + Active string `json:"zone_active,omitempty"` + Protected string `json:"zone_protected,omitempty"` + IsRegistered string `json:"zone_is_registered,omitempty"` + Updated bool `json:"zone_updated,omitempty"` + CustomerID string `json:"cust_id,omitempty"` + DomainRegistrar string `json:"domain_registrar,omitempty"` + DomainStatus string `json:"domain_status,omitempty"` + DomainExpiryDate string `json:"domain_expiry_date,omitempty"` + DomainAutoRenew string `json:"domain_auto_renew,omitempty"` + ExternalDNS string `json:"external_dns,omitempty"` + RecordCount int `json:"record_count,omitempty"` +} + +type Record struct { + ID string `json:"record_id,omitempty"` + Name string `json:"record_name,omitempty"` + Type string `json:"record_type,omitempty"` + Value string `json:"record_value,omitempty"` + TTL int `json:"record_ttl,omitempty"` +} + +type Auth struct { + Username string `json:"username"` + Password string `json:"password"` + Code int `json:"code,omitempty"` +} + +type Token struct { + Token string `json:"token,omitempty"` + TokenExpire int64 `json:"token_expire,omitempty"` + CustomerID string `json:"customer_id,omitempty"` + ContactID string `json:"contact_id,omitempty"` + CustomerName string `json:"customer_name,omitempty"` + ContactUsername string `json:"contact_username,omitempty"` + ContactAccessLevel string `json:"contact_access_level,omitempty"` + CustomerAddress string `json:"customer_address,omitempty"` + CustomerZipcode string `json:"customer_zipcode,omitempty"` + CustomerCity string `json:"customer_city,omitempty"` + CustomerProvince string `json:"customer_province,omitempty"` + GASecret string `json:"ga_secret,omitempty"` + GAEnabled string `json:"ga_enabled,omitempty"` + VAT int `json:"vat,omitempty"` +} + +func (t *Token) IsExpired() bool { + if t == nil { + return true + } + + return time.Now().UTC().Add(1 * time.Minute).After(time.Unix(t.TokenExpire, 0).UTC()) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index ff6ab0c28..8ecbb76cc 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -70,6 +70,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/gandiv5" "github.com/go-acme/lego/v4/providers/dns/gcloud" "github.com/go-acme/lego/v4/providers/dns/gcore" + "github.com/go-acme/lego/v4/providers/dns/gigahostno" "github.com/go-acme/lego/v4/providers/dns/glesys" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/googledomains" @@ -307,6 +308,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return gcloud.NewDNSProvider() case "gcore": return gcore.NewDNSProvider() + case "gigahostno": + return gigahostno.NewDNSProvider() case "glesys": return glesys.NewDNSProvider() case "godaddy": From 56cb356ef2d95275cc0bc5c7fa599396a32d23bb Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 25 Nov 2025 19:29:47 +0100 Subject: [PATCH 15/95] edgeone: add zones mapping (#2728) --- cmd/zz_gen_cmd_dnshelp.go | 1 + docs/content/dns/zz_gen_edgeone.md | 1 + providers/dns/dnspod/dnspod.go | 2 +- providers/dns/edgeone/edgeone.go | 19 +++++++++++---- providers/dns/edgeone/edgeone.toml | 1 + providers/dns/edgeone/edgeone_test.go | 24 +++++++++++++++++-- providers/dns/edgeone/wrapper.go | 33 ++++++++++++++++----------- 7 files changed, 61 insertions(+), 20 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index deb93e6eb..2cba1b73a 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -1342,6 +1342,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "EDGEONE_REGION": Region`) ew.writeln(` - "EDGEONE_SESSION_TOKEN": Access Key token`) ew.writeln(` - "EDGEONE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "EDGEONE_ZONES_MAPPING": Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgeone`) diff --git a/docs/content/dns/zz_gen_edgeone.md b/docs/content/dns/zz_gen_edgeone.md index b7b5b1eec..227127d65 100644 --- a/docs/content/dns/zz_gen_edgeone.md +++ b/docs/content/dns/zz_gen_edgeone.md @@ -55,6 +55,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `EDGEONE_REGION` | Region | | `EDGEONE_SESSION_TOKEN` | Access Key token | | `EDGEONE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `EDGEONE_ZONES_MAPPING` | Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2') | 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" %}}). diff --git a/providers/dns/dnspod/dnspod.go b/providers/dns/dnspod/dnspod.go index c9376b956..52a873c7b 100644 --- a/providers/dns/dnspod/dnspod.go +++ b/providers/dns/dnspod/dnspod.go @@ -165,7 +165,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { } if hostedZone.ID == "" || hostedZone.ID == "0" { - return "", "", fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) + return "", "", fmt.Errorf("zone %s not found for domain %s", authZone, domain) } return hostedZone.ID.String(), hostedZone.Name, nil diff --git a/providers/dns/edgeone/edgeone.go b/providers/dns/edgeone/edgeone.go index 3402122bb..509a75c77 100644 --- a/providers/dns/edgeone/edgeone.go +++ b/providers/dns/edgeone/edgeone.go @@ -26,6 +26,7 @@ const ( EnvSecretKey = envNamespace + "SECRET_KEY" EnvRegion = envNamespace + "REGION" EnvSessionToken = envNamespace + "SESSION_TOKEN" + EnvZonesMapping = envNamespace + "ZONES_MAPPING" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -40,6 +41,8 @@ type Config struct { Region string SessionToken string + ZonesMapping map[string]string + PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -78,6 +81,14 @@ func NewDNSProvider() (*DNSProvider, error) { config.Region = env.GetOrDefaultString(EnvRegion, "") config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "") + mapping := env.GetOrDefaultString(EnvZonesMapping, "") + if mapping != "" { + config.ZonesMapping, err = env.ParsePairs(mapping) + if err != nil { + return nil, fmt.Errorf("edgeone: zones mapping: %w", err) + } + } + return NewDNSProviderConfig(config) } @@ -121,7 +132,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() - zone, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("edgeone: failed to get hosted zone: %w", err) } @@ -133,7 +144,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { request := teo.NewCreateDnsRecordRequest() request.Name = ptr.Pointer(punnyCoded) - request.ZoneId = zone.ZoneId + request.ZoneId = zoneID request.Type = ptr.Pointer("TXT") request.Content = ptr.Pointer(info.Value) request.TTL = ptr.Pointer(int64(d.config.TTL)) @@ -156,7 +167,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() - zone, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("edgeone: failed to get hosted zone: %w", err) } @@ -171,7 +182,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } request := teo.NewDeleteDnsRecordsRequest() - request.ZoneId = zone.ZoneId + request.ZoneId = zoneID request.RecordIds = []*string{recordID} _, err = teo.DeleteDnsRecordsWithContext(ctx, d.client, request) diff --git a/providers/dns/edgeone/edgeone.toml b/providers/dns/edgeone/edgeone.toml index 120756da6..a33af75b2 100644 --- a/providers/dns/edgeone/edgeone.toml +++ b/providers/dns/edgeone/edgeone.toml @@ -17,6 +17,7 @@ lego --email you@example.com --dns edgeone -d '*.example.com' -d example.com run [Configuration.Additional] EDGEONE_SESSION_TOKEN = "Access Key token" EDGEONE_REGION = "Region" + EDGEONE_ZONES_MAPPING = "Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')" EDGEONE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" EDGEONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" EDGEONE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" diff --git a/providers/dns/edgeone/edgeone_test.go b/providers/dns/edgeone/edgeone_test.go index 1c92118dc..7bd4f6f6d 100644 --- a/providers/dns/edgeone/edgeone_test.go +++ b/providers/dns/edgeone/edgeone_test.go @@ -9,8 +9,11 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvSecretID, EnvSecretKey). - WithDomain(envDomain) +var envTest = tester.NewEnvTest( + EnvSecretID, + EnvSecretKey, + EnvZonesMapping, +).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -25,6 +28,14 @@ func TestNewDNSProvider(t *testing.T) { EnvSecretKey: "456", }, }, + { + desc: "success with zones mapping", + envVars: map[string]string{ + EnvSecretID: "123", + EnvSecretKey: "456", + EnvZonesMapping: "example.org:id1,example.com:id2", + }, + }, { desc: "missing credentials", envVars: map[string]string{ @@ -49,6 +60,15 @@ func TestNewDNSProvider(t *testing.T) { }, expected: "edgeone: some credentials information are missing: EDGEONE_SECRET_KEY", }, + { + desc: "invalid mapping", + envVars: map[string]string{ + EnvSecretID: "123", + EnvSecretKey: "456", + EnvZonesMapping: "example.org:id1,example.com", + }, + expected: "edgeone: zones mapping: incorrect pair: example.com", + }, } for _, test := range testCases { diff --git a/providers/dns/edgeone/wrapper.go b/providers/dns/edgeone/wrapper.go index c3e9d965b..53fae9427 100644 --- a/providers/dns/edgeone/wrapper.go +++ b/providers/dns/edgeone/wrapper.go @@ -9,10 +9,22 @@ import ( teo "github.com/go-acme/tencentedgdeone/v20220901" ) -func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*teo.Zone, error) { +func (d *DNSProvider) getHostedZoneID(ctx context.Context, domain string) (*string, error) { + authZone, err := dns01.FindZoneByFqdn(domain) + if err != nil { + return nil, fmt.Errorf("could not find zone: %w", err) + } + + if d.config.ZonesMapping != nil { + zoneID, ok := d.config.ZonesMapping[authZone] + if ok { + return ptr.Pointer(zoneID), nil + } + } + request := teo.NewDescribeZonesRequest() - var domains []*teo.Zone + var zones []*teo.Zone for { response, err := teo.DescribeZonesWithContext(ctx, d.client, request) @@ -20,23 +32,18 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*teo.Zo return nil, fmt.Errorf("API call failed: %w", err) } - domains = append(domains, response.Response.Zones...) + zones = append(zones, response.Response.Zones...) - if int64(len(domains)) >= ptr.Deref(response.Response.TotalCount) { + if int64(len(zones)) >= ptr.Deref(response.Response.TotalCount) { break } - request.Offset = ptr.Pointer(int64(len(domains))) - } - - authZone, err := dns01.FindZoneByFqdn(domain) - if err != nil { - return nil, fmt.Errorf("could not find zone: %w", err) + request.Offset = ptr.Pointer(int64(len(zones))) } var hostedZone *teo.Zone - for _, zone := range domains { + for _, zone := range zones { unfqdn := dns01.UnFqdn(authZone) if ptr.Deref(zone.ZoneName) == unfqdn { hostedZone = zone @@ -44,8 +51,8 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*teo.Zo } if hostedZone == nil { - return nil, fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) + return nil, fmt.Errorf("zone %s not found for domain %s", authZone, domain) } - return hostedZone, nil + return hostedZone.ZoneId, nil } From ad6adbffd4eddb2e15a250af437850a7ec11551c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 25 Nov 2025 19:30:22 +0100 Subject: [PATCH 16/95] tests: fix flaky test (#2729) --- providers/dns/oraclecloud/oraclecloud_test.go | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/providers/dns/oraclecloud/oraclecloud_test.go b/providers/dns/oraclecloud/oraclecloud_test.go index c646e90f2..74ee06eac 100644 --- a/providers/dns/oraclecloud/oraclecloud_test.go +++ b/providers/dns/oraclecloud/oraclecloud_test.go @@ -61,7 +61,7 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success file", envVars: map[string]string{ - EnvPrivKeyFile: mustGeneratePrivateKeyFile("secret1"), + EnvPrivKeyFile: mustGeneratePrivateKeyFile(t, "secret1"), EnvPrivKeyPass: "secret1", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", @@ -383,21 +383,21 @@ func mustGeneratePrivateKey(pwd string) string { return base64.StdEncoding.EncodeToString(pem.EncodeToMemory(block)) } -func mustGeneratePrivateKeyFile(pwd string) string { - block, err := generatePrivateKey(pwd) - if err != nil { - panic(err) - } +func mustGeneratePrivateKeyFile(t *testing.T, pwd string) string { + t.Helper() - file, err := os.CreateTemp("", "lego_oci_*.pem") - if err != nil { - panic(err) - } + block, err := generatePrivateKey(pwd) + require.NoError(t, err) + + file, err := os.CreateTemp(t.TempDir(), "lego_oci_*.pem") + require.NoError(t, err) + + defer func() { + _ = file.Close() + }() err = pem.Encode(file, block) - if err != nil { - panic(err) - } + require.NoError(t, err) return file.Name() } From 42fb4346e258b9c3361394341f6e1adf6a612317 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 27 Nov 2025 20:40:23 +0100 Subject: [PATCH 17/95] chore: update dependencies (#2733) --- go.mod | 103 ++++++------- go.sum | 214 ++++++++++++++-------------- providers/dns/gcloud/googlecloud.go | 2 +- providers/dns/inwx/inwx.go | 4 +- 4 files changed, 162 insertions(+), 161 deletions(-) diff --git a/go.mod b/go.mod index 702da550f..f68c2c9eb 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.0 require ( cloud.google.com/go/compute/metadata v0.9.0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 @@ -18,21 +18,21 @@ require ( github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 github.com/alibabacloud-go/tea v1.3.13 github.com/aliyun/credentials-go v1.4.7 - github.com/aws/aws-sdk-go-v2 v1.39.4 - github.com/aws/aws-sdk-go-v2/config v1.31.15 - github.com/aws/aws-sdk-go-v2/credentials v1.18.19 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2 - github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.89.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 - github.com/aziontech/azionapi-go-sdk v0.143.0 - github.com/baidubce/bce-sdk-go v0.9.250 + github.com/aws/aws-sdk-go-v2 v1.40.0 + github.com/aws/aws-sdk-go-v2/config v1.32.2 + github.com/aws/aws-sdk-go-v2/credentials v1.19.2 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 + github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 + github.com/aziontech/azionapi-go-sdk v0.144.0 + github.com/baidubce/bce-sdk-go v0.9.252 github.com/cenkalti/backoff/v5 v5.0.3 github.com/dnsimple/dnsimple-go/v4 v4.0.0 - github.com/exoscale/egoscale/v3 v3.1.27 - github.com/go-acme/alidns-20150109/v4 v4.6.1 + github.com/exoscale/egoscale/v3 v3.1.31 + github.com/go-acme/alidns-20150109/v4 v4.7.0 github.com/go-acme/esa-20240910/v2 v2.40.1 - github.com/go-acme/tencentclouddnspod v1.1.10 + github.com/go-acme/tencentclouddnspod v1.1.25 github.com/go-acme/tencentedgdeone v1.1.48 github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-viper/mapstructure/v2 v2.4.0 @@ -42,12 +42,12 @@ require ( github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-version v1.7.0 - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/ldez/grignotin v0.10.1 - github.com/linode/linodego v1.60.0 + github.com/linode/linodego v1.61.0 github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.68 @@ -59,12 +59,12 @@ require ( github.com/nrdcg/dnspod-go v0.4.0 github.com/nrdcg/freemyip v0.3.0 github.com/nrdcg/goacmedns v0.2.0 - github.com/nrdcg/goinwx v0.11.0 + github.com/nrdcg/goinwx v0.12.0 github.com/nrdcg/mailinabox v0.3.0 github.com/nrdcg/namesilo v0.5.0 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0 + github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 + github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 github.com/nrdcg/porkbun v0.4.0 github.com/nrdcg/vegadns v0.3.0 github.com/nzdjb/go-metaname v1.0.0 @@ -73,29 +73,29 @@ require ( github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 github.com/regfish/regfish-dnsapi-go v0.1.1 github.com/sacloud/api-client-go v0.3.3 - github.com/sacloud/iaas-api-go v1.20.0 + github.com/sacloud/iaas-api-go v1.22.0 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 github.com/selectel/domains-go v1.1.0 github.com/selectel/go-selvpcclient/v4 v4.1.0 github.com/softlayer/softlayer-go v1.2.1 github.com/stretchr/testify v1.11.1 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 github.com/transip/gotransip/v6 v6.26.1 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 github.com/urfave/cli/v2 v2.27.7 github.com/vinyldns/go-vinyldns v0.9.16 - github.com/volcengine/volc-sdk-golang v1.0.224 - github.com/vultr/govultr/v3 v3.24.0 - github.com/yandex-cloud/go-genproto v0.34.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.16 - github.com/yandex-cloud/go-sdk/v2 v2.24.0 - golang.org/x/crypto v0.43.0 - golang.org/x/net v0.46.0 - golang.org/x/oauth2 v0.32.0 - golang.org/x/text v0.30.0 + github.com/volcengine/volc-sdk-golang v1.0.229 + github.com/vultr/govultr/v3 v3.25.0 + github.com/yandex-cloud/go-genproto v0.38.0 + github.com/yandex-cloud/go-sdk/services/dns v0.0.20 + github.com/yandex-cloud/go-sdk/v2 v2.28.0 + golang.org/x/crypto v0.45.0 + golang.org/x/net v0.47.0 + golang.org/x/oauth2 v0.33.0 + golang.org/x/text v0.31.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.254.0 - gopkg.in/ns1/ns1-go.v2 v2.15.1 + google.golang.org/api v0.256.0 + gopkg.in/ns1/ns1-go.v2 v2.15.2 gopkg.in/yaml.v2 v2.4.0 software.sslmate.com/src/go-pkcs12 v0.6.0 ) @@ -111,24 +111,25 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.11 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -157,7 +158,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -210,13 +211,13 @@ require ( go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 337db59b3..af9e95e02 100644 --- a/go.sum +++ b/go.sum @@ -42,10 +42,10 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -85,8 +85,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -121,7 +121,6 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.12/go.mod h1:f2wDpbM7hK9SvLIH09zSKVU1TsyemUNOqErMscMMl7c= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= @@ -144,7 +143,6 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= -github.com/alibabacloud-go/tea v1.3.12/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94= github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= @@ -170,52 +168,54 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.39.4 h1:qTsQKcdQPHnfGYBBs+Btl8QwxJeoWcOcPcixK90mRhg= -github.com/aws/aws-sdk-go-v2 v1.39.4/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 h1:t9yYsydLYNBk9cJ73rgPhPWqOh/52fcWDQB5b1JsKSY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2/go.mod h1:IusfVNTmiSN3t4rhxWFaBAqn+mcNdwKtPcV16eYdgko= -github.com/aws/aws-sdk-go-v2/config v1.31.15 h1:gE3M4xuNXfC/9bG4hyowGm/35uQTi7bUKeYs5e/6uvU= -github.com/aws/aws-sdk-go-v2/config v1.31.15/go.mod h1:HvnvGJoE2I95KAIW8kkWVPJ4XhdrlvwJpV6pEzFQa8o= -github.com/aws/aws-sdk-go-v2/credentials v1.18.19 h1:Jc1zzwkSY1QbkEcLujwqRTXOdvW8ppND3jRBb/VhBQc= -github.com/aws/aws-sdk-go-v2/credentials v1.18.19/go.mod h1:DIfQ9fAk5H0pGtnqfqkbSIzky82qYnGvh06ASQXXg6A= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 h1:X7X4YKb+c0rkI6d4uJ5tEMxXgCZ+jZ/D6mvkno8c8Uw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11/go.mod h1:EqM6vPZQsZHYvC4Cai35UDg/f5NCEU+vp0WfbVqVcZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 h1:7AANQZkF3ihM8fbdftpjhken0TP9sBzFbV/Ze/Y4HXA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11/go.mod h1:NTF4QCGkm6fzVwncpkFQqoquQyOolcyXfbpC98urj+c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 h1:ShdtWUZT37LCAA4Mw2kJAJtzaszfSHFb5n25sdcv4YE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11/go.mod h1:7bUb2sSr2MZ3M/N+VyETLTQtInemHXb/Fl3s8CLzm0Y= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= +github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk= +github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.11 h1:bKgSxk1TW//00PGQqYmrq83c+2myGidEclp+t9pPqVI= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.11/go.mod h1:vrPYCQ6rFHL8jzQA8ppu3gWX18zxjLIDGTeqDxkBmSI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.2 h1:DGFpGybmutVsCuF6vSuLZ25Vh55E3VmsnJmFfjeBx4M= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.2/go.mod h1:hm/wU1HDvXCFEDzOLorQnZZ/CVvPXvWEmHMSmqgQRuA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 h1:GpMf3z2KJa4RnJ0ew3Hac+hRFYLZ9DDjfgXjuW+pB54= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11/go.mod h1:6MZP3ZI4QQsgUCFTwMZA2V0sEriNQ8k2hmoHF3qjimQ= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.11 h1:weapBOuuFIBEQ9OX/NVW3tFQCvSutyjZYk/ga5jDLPo= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.11/go.mod h1:3C1gN4FmIVLwYSh8etngUS+f1viY6nLCDVtZmrFbDy0= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2 h1:pr1dQ9vamhAf2mYOgiRRC/w9Ht4POFhy6+xXw7hOqwY= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.2/go.mod h1:A4Ch93K7Wam4Qe0Wl0XbPgcgoL5KIJtFIe7wHw6OPWE= -github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1 h1:KuoA/cmy/yK8n9v/d6WH36cZwGxFOrn0TmZ4lNN3MKQ= -github.com/aws/aws-sdk-go-v2/service/route53 v1.59.1/go.mod h1:BymbICXBfXQHO6i+yTBhocA9a6DM0uMDQqYelqa9wzs= -github.com/aws/aws-sdk-go-v2/service/s3 v1.89.0 h1:JbCUlVDEjmhpvpIgXP9QN+/jW61WWWj99cGmxMC49hM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.89.0/go.mod h1:UHKgcRSx8PVtvsc1Poxb/Co3PD3wL7P+f49P0+cWtuY= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 h1:M5nimZmugcZUO9wG7iVtROxPhiqyZX6ejS1lxlDPbTU= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.8/go.mod h1:mbef/pgKhtKRwrigPPs7SSSKZgytzP8PQ6P6JAAdqyM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 h1:S5GuJZpYxE0lKeMHKn+BRTz6PTFpgThyJ+5mYfux7BM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3/go.mod h1:X4OF+BTd7HIb3L+tc4UlWHVrpgwZZIVENU15pRDVTI0= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 h1:Ekml5vGg6sHSZLZJQJagefnVe6PmqC2oiRkBq4F7fU0= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.9/go.mod h1:/e15V+o1zFHWdH3u7lpI3rVBcxszktIKuHKCY2/py+k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 h1:jhwva7OKpYXrTQmCG4L7lF2FvB2irs1oRyGAwmQ4lmA= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8/go.mod h1:x+omzRoqYYFX+H8/va+Gt2Yg4xGaHZMRowr77Y/UGIA= +github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 h1:W3+0Cbc9awFBr9Yt7nFUkvB4N4e7vVIGtKD1qDttXn4= +github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0/go.mod h1:Wa3q5R2uwIfIL3HZH+vG1/P9y7CjjfzTgcz5IWXlsZs= +github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/aziontech/azionapi-go-sdk v0.143.0 h1:4eEBlYT10prgeCVTNR9FIc7f59Crbl2zrH1a4D1BUqU= -github.com/aziontech/azionapi-go-sdk v0.143.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA= -github.com/baidubce/bce-sdk-go v0.9.250 h1:fnvV5clsNCAP6pCauj0eNaUnoLVmjQGnco7rcMqp984= -github.com/baidubce/bce-sdk-go v0.9.250/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo= +github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU= +github.com/baidubce/bce-sdk-go v0.9.252 h1:TONS/utgfEkDjvHllVZFBrTsjsNhk51rhWuj3ppcL4s= +github.com/baidubce/bce-sdk-go v0.9.252/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -284,8 +284,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.27 h1:vKdWZG8QFDc7rY7lCfcuudO+ovyp5psYjFwKVqmkhCE= -github.com/exoscale/egoscale/v3 v3.1.27/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= +github.com/exoscale/egoscale/v3 v3.1.31 h1:/dySEUSAxU+hlAS/eLxAoY8ZYmtOtaoL1P+lDwH7ojY= +github.com/exoscale/egoscale/v3 v3.1.31/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -312,12 +312,12 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-acme/alidns-20150109/v4 v4.6.1 h1:Dch3aWRcw4U62+jKPjPQN3iW3TPvgIywATbvHzojXeo= -github.com/go-acme/alidns-20150109/v4 v4.6.1/go.mod h1:RBcqBA5IvUWtlpjx6dC6EkPVyBNLQ+mR18XoaP38BFY= +github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= +github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= github.com/go-acme/esa-20240910/v2 v2.40.1 h1:pog3UlF5d+3LPoo1L8u8PqB189recIXX8T7pGoEz18A= github.com/go-acme/esa-20240910/v2 v2.40.1/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= -github.com/go-acme/tencentclouddnspod v1.1.10 h1:ERVJ4mc3cT4Nb3+n6H/c1AwZnChGBqLoymE0NVYscKI= -github.com/go-acme/tencentclouddnspod v1.1.10/go.mod h1:Bo/0YQJ/99FM+44HmCQkByuptX1tJsJ9V14MGV/2Qco= +github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s= +github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE= github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk= github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= @@ -461,8 +461,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= @@ -531,8 +531,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173 h1:Y4ixGadyrK9xHw6Z+cyiiME3SBXepEcUoiT+B8C5FoQ= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.173/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 h1:eNVkjzdPMgM2qih9aECiFUI8S9zgpOwXxeBPAwQqtvU= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -606,8 +606,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI= -github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs= +github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM= +github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -697,18 +697,18 @@ github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= -github.com/nrdcg/goinwx v0.11.0 h1:GER0SE3POub7rxARt3Y3jRy1OON1hwF1LRxHz5xsFBw= -github.com/nrdcg/goinwx v0.11.0/go.mod h1:0BXSC0FxVtU4aTjX0Zw3x0DK32tjugLzeNIAGtwXvPQ= +github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4= +github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0= github.com/nrdcg/mailinabox v0.3.0 h1:PHkC1elKXKAjEvdx2HHFMgcEGZFqudAl7aU3L2JDhM4= github.com/nrdcg/mailinabox v0.3.0/go.mod h1:1eFIGcM4lI+AfFOUpbs548SFGz1ZWoMOGbECBmkghw4= github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0 h1:GPwwX9GFIBjV4u1M3Cr8eKCP6drW01IsfQSDIz6SUk8= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0 h1:MjHla6lf1jpjGXORLpzMeo/tSmx0ejmjMjdjTByaDGY= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0/go.mod h1:o1/kMADX0SlB4hJjWtcs3M6VIUOGR78yhPyiBv6oBkk= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 h1:bppmFqrJ87U4gWilemAW9oa4Qepf2JBTK/mPgaZLP2A= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 h1:IHPZs4Mo/lxyo+gYB+baheb2kGmHtNGQk2DKPDHqPjA= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0/go.mod h1:yELd0uJLiIyv9sGIh5ZRCHEB1B2QFNURWkQIMqb3ZwE= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU= @@ -816,8 +816,8 @@ github.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sq github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo= github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE= github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE= -github.com/sacloud/iaas-api-go v1.20.0 h1:L4TfAzoFSwxrD3QXX8UxJa2o+GZrP9b863K+voTy3tQ= -github.com/sacloud/iaas-api-go v1.20.0/go.mod h1:XV995RM1I7k5AHb7UZrCVyDF/8bZXDxa+uk1EXoj/Zs= +github.com/sacloud/iaas-api-go v1.22.0 h1:nvLQNuxcfxILvoxA6WcnTjU9A8yv8dPI1OSYHAPxBJk= +github.com/sacloud/iaas-api-go v1.22.0/go.mod h1:PLcolyFlby/0ExZTOdUf9xzhkEMBuVzORadXDNN21no= github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo= github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -903,9 +903,10 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.10/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48 h1:aoRUrz2ag27jQWcOKHgeE+toSti6/xPqHKMLruOtJuM= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 h1:r05ohLc0LVEpiEQeOJ5QwCiKk6XM9kjTca6+UAbNR/8= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= @@ -920,10 +921,10 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= -github.com/volcengine/volc-sdk-golang v1.0.224 h1:k9Vtg64tQAgFTOGWzhyL0b0axuTuExXbLNVlslWlBZI= -github.com/volcengine/volc-sdk-golang v1.0.224/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M= -github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= +github.com/volcengine/volc-sdk-golang v1.0.229 h1:gOkDltTS6Fta8OyfYrbeY9bqCHHyiJuGYNJpR5MR+Fo= +github.com/volcengine/volc-sdk-golang v1.0.229/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= +github.com/vultr/govultr/v3 v3.25.0 h1:rS8/Vdy8HlHArwmD4MtLY+hbbpYAbcnZueZrE6b0oUg= +github.com/vultr/govultr/v3 v3.25.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= @@ -932,12 +933,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.34.0 h1:qhTJpPxOTKQbV44rIqoZSdzxDtZW27fkFjAcipEy8Zs= -github.com/yandex-cloud/go-genproto v0.34.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.16 h1:0UYrBlQjTO2ct5xcSx6rqkQB95wRBPMVwxfqLQD1sUE= -github.com/yandex-cloud/go-sdk/services/dns v0.0.16/go.mod h1:HlS3aIAdYEmJu2Ska/nzpcuv9LLVSMMXKGhzyLQwf5s= -github.com/yandex-cloud/go-sdk/v2 v2.24.0 h1:G53N/RB5g/jw2xNN0egspnwd2ByHA1OVH6wbTx/tIlo= -github.com/yandex-cloud/go-sdk/v2 v2.24.0/go.mod h1:ZRdpyOig8c/W3bNhwvkeXWWPeDScd9nmXv4AJzKvOsk= +github.com/yandex-cloud/go-genproto v0.38.0 h1:uB3btG7mLOnu53ehYtRARCk04+80sBpxDrSkP3qC6G8= +github.com/yandex-cloud/go-genproto v0.38.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk/services/dns v0.0.20 h1:xHBRa+IIYpTgMbTbmZf7aEKIqrJMcZGIF8ea4XIyLX0= +github.com/yandex-cloud/go-sdk/services/dns v0.0.20/go.mod h1:8nYQULLJbbe51qdBY7Ay5v8wtDgdH7nHCMZs4XkwzLg= +github.com/yandex-cloud/go-sdk/v2 v2.28.0 h1:KDOrN75xokZBYbgjq6Pjvo+hEpu32xFhErtomLBML5s= +github.com/yandex-cloud/go-sdk/v2 v2.28.0/go.mod h1:6vmAhqoCVYSJEb5OuhHUqIdxDy2b9uUXp1e5sqMhTmo= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= @@ -1029,8 +1030,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1074,8 +1075,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1133,17 +1134,16 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1160,8 +1160,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1247,8 +1247,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1263,8 +1263,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1283,8 +1283,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1350,8 +1350,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1380,8 +1380,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4= -google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1424,8 +1424,8 @@ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuO google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1477,8 +1477,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.15.1 h1:8rri2TzAPYcVbBGXn48+dz1Xg30PzHfZ4k8A9JOS0Z0= -gopkg.in/ns1/ns1-go.v2 v2.15.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.15.2 h1:aBVyKeEH3rBFWwX72xPPjEuRL4+Lp5P9GlAcrzu0Y5M= +gopkg.in/ns1/ns1-go.v2 v2.15.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/providers/dns/gcloud/googlecloud.go b/providers/dns/gcloud/googlecloud.go index ff317946d..61e8ee66f 100644 --- a/providers/dns/gcloud/googlecloud.go +++ b/providers/dns/gcloud/googlecloud.go @@ -2,6 +2,7 @@ package gcloud import ( + "context" "encoding/json" "errors" "fmt" @@ -19,7 +20,6 @@ import ( "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/miekg/dns" - "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" gdns "google.golang.org/api/dns/v1" diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go index 794db84b3..0e79d71e0 100644 --- a/providers/dns/inwx/inwx.go +++ b/providers/dns/inwx/inwx.go @@ -177,7 +177,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("inwx: %w", err) } - var recordID int + var recordID string for _, record := range response.Records { if record.Content != info.Value { @@ -189,7 +189,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { break } - if recordID == 0 { + if recordID == "" { return errors.New("inwx: TXT record not found") } From dc0a595a9f7c3e861bc244bc9021e704c774b381 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 27 Nov 2025 20:40:55 +0100 Subject: [PATCH 18/95] Add DNS provider for United-Domains (#2731) --- README.md | 12 +- cmd/zz_gen_cmd_dnshelp.go | 21 ++ docs/content/dns/zz_gen_uniteddomains.md | 67 ++++++ docs/data/zz_cli_help.toml | 2 +- .../internal => internal/ionos}/client.go | 10 +- .../ionos}/client_test.go | 2 +- .../ionos}/fixtures/get_records.json | 0 .../ionos}/fixtures/get_records_error.json | 0 .../ionos}/fixtures/list_zones.json | 0 .../ionos}/fixtures/list_zones_error.json | 0 .../ionos}/fixtures/remove_record_error.json | 0 .../fixtures/replace_records_error.json | 0 .../internal => internal/ionos}/types.go | 2 +- providers/dns/ionos/ionos.go | 16 +- providers/dns/uniteddomains/uniteddomains.go | 210 ++++++++++++++++++ .../dns/uniteddomains/uniteddomains.toml | 22 ++ .../dns/uniteddomains/uniteddomains_test.go | 130 +++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 18 files changed, 476 insertions(+), 21 deletions(-) create mode 100644 docs/content/dns/zz_gen_uniteddomains.md rename providers/dns/{ionos/internal => internal/ionos}/client.go (95%) rename providers/dns/{ionos/internal => internal/ionos}/client_test.go (99%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/get_records.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/get_records_error.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/list_zones.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/list_zones_error.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/remove_record_error.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/fixtures/replace_records_error.json (100%) rename providers/dns/{ionos/internal => internal/ionos}/types.go (99%) create mode 100644 providers/dns/uniteddomains/uniteddomains.go create mode 100644 providers/dns/uniteddomains/uniteddomains.toml create mode 100644 providers/dns/uniteddomains/uniteddomains_test.go diff --git a/README.md b/README.md index d40afc579..42bf26c73 100644 --- a/README.md +++ b/README.md @@ -245,34 +245,34 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). UKFast SafeDNS Ultradns + United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 2cba1b73a..11cf01280 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -161,6 +161,7 @@ func allDNSCodes() string { "timewebcloud", "transip", "ultradns", + "uniteddomains", "variomedia", "vegadns", "vercel", @@ -3383,6 +3384,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ultradns`) + case "uniteddomains": + // generated from: providers/dns/uniteddomains/uniteddomains.toml + ew.writeln(`Configuration for United-Domains.`) + ew.writeln(`Code: 'uniteddomains'`) + ew.writeln(`Since: 'v4.29.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "UNITEDDOMAINS_API_KEY": API key '.' https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "UNITEDDOMAINS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "UNITEDDOMAINS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "UNITEDDOMAINS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 900)`) + ew.writeln(` - "UNITEDDOMAINS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/uniteddomains`) + case "variomedia": // generated from: providers/dns/variomedia/variomedia.toml ew.writeln(`Configuration for Variomedia.`) diff --git a/docs/content/dns/zz_gen_uniteddomains.md b/docs/content/dns/zz_gen_uniteddomains.md new file mode 100644 index 000000000..7f94dd09f --- /dev/null +++ b/docs/content/dns/zz_gen_uniteddomains.md @@ -0,0 +1,67 @@ +--- +title: "United-Domains" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: uniteddomains +dnsprovider: + since: "v4.29.0" + code: "uniteddomains" + url: "https://www.united-domains.de/" +--- + + + + + + +Configuration for [United-Domains](https://www.united-domains.de/). + + + + +- Code: `uniteddomains` +- Since: v4.29.0 + + +Here is an example bash command using the United-Domains provider: + +```bash +UNITEDDOMAINS_API_KEY=xxxxxxxx \ +lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `UNITEDDOMAINS_API_KEY` | API key `.` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/ | + +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 | +|--------------------------------|-------------| +| `UNITEDDOMAINS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `UNITEDDOMAINS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `UNITEDDOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) | +| `UNITEDDOMAINS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | + +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://www.united-domains.de/dns-apidoc/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 552539fb0..f9401b0e3 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, 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, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, 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/ionos/internal/client.go b/providers/dns/internal/ionos/client.go similarity index 95% rename from providers/dns/ionos/internal/client.go rename to providers/dns/internal/ionos/client.go index 935b6bbad..8ab6f15b9 100644 --- a/providers/dns/ionos/internal/client.go +++ b/providers/dns/internal/ionos/client.go @@ -1,4 +1,4 @@ -package internal +package ionos import ( "bytes" @@ -14,8 +14,10 @@ import ( querystring "github.com/google/go-querystring/query" ) -// defaultBaseURL represents the API endpoint to call. -const defaultBaseURL = "https://api.hosting.ionos.com/dns" +const ( + DefaultIonosBaseURL = "https://api.hosting.ionos.com/dns" + DefaultUnitedDomainsBaseURL = "https://dnsapi.united-domains.de/dns" +) // APIKeyHeader API key header. const APIKeyHeader = "X-Api-Key" @@ -30,7 +32,7 @@ type Client struct { // NewClient creates a new Client. func NewClient(apiKey string) (*Client, error) { - baseURL, err := url.Parse(defaultBaseURL) + baseURL, err := url.Parse(DefaultIonosBaseURL) if err != nil { return nil, err } diff --git a/providers/dns/ionos/internal/client_test.go b/providers/dns/internal/ionos/client_test.go similarity index 99% rename from providers/dns/ionos/internal/client_test.go rename to providers/dns/internal/ionos/client_test.go index 008d153bc..81e4ff289 100644 --- a/providers/dns/ionos/internal/client_test.go +++ b/providers/dns/internal/ionos/client_test.go @@ -1,4 +1,4 @@ -package internal +package ionos import ( "net/http" diff --git a/providers/dns/ionos/internal/fixtures/get_records.json b/providers/dns/internal/ionos/fixtures/get_records.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/get_records.json rename to providers/dns/internal/ionos/fixtures/get_records.json diff --git a/providers/dns/ionos/internal/fixtures/get_records_error.json b/providers/dns/internal/ionos/fixtures/get_records_error.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/get_records_error.json rename to providers/dns/internal/ionos/fixtures/get_records_error.json diff --git a/providers/dns/ionos/internal/fixtures/list_zones.json b/providers/dns/internal/ionos/fixtures/list_zones.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/list_zones.json rename to providers/dns/internal/ionos/fixtures/list_zones.json diff --git a/providers/dns/ionos/internal/fixtures/list_zones_error.json b/providers/dns/internal/ionos/fixtures/list_zones_error.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/list_zones_error.json rename to providers/dns/internal/ionos/fixtures/list_zones_error.json diff --git a/providers/dns/ionos/internal/fixtures/remove_record_error.json b/providers/dns/internal/ionos/fixtures/remove_record_error.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/remove_record_error.json rename to providers/dns/internal/ionos/fixtures/remove_record_error.json diff --git a/providers/dns/ionos/internal/fixtures/replace_records_error.json b/providers/dns/internal/ionos/fixtures/replace_records_error.json similarity index 100% rename from providers/dns/ionos/internal/fixtures/replace_records_error.json rename to providers/dns/internal/ionos/fixtures/replace_records_error.json diff --git a/providers/dns/ionos/internal/types.go b/providers/dns/internal/ionos/types.go similarity index 99% rename from providers/dns/ionos/internal/types.go rename to providers/dns/internal/ionos/types.go index 35bfe0966..3fc74c054 100644 --- a/providers/dns/ionos/internal/types.go +++ b/providers/dns/internal/ionos/types.go @@ -1,4 +1,4 @@ -package internal +package ionos import ( "fmt" diff --git a/providers/dns/ionos/ionos.go b/providers/dns/ionos/ionos.go index a512e8bfd..fd35f502e 100644 --- a/providers/dns/ionos/ionos.go +++ b/providers/dns/ionos/ionos.go @@ -14,7 +14,7 @@ import ( "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/ionos/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/ionos" ) // Environment variables names. @@ -57,7 +57,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *internal.Client + client *ionos.Client } // NewDNSProvider returns a DNSProvider instance configured for Ionos. @@ -88,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("ionos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - client, err := internal.NewClient(config.APIKey) + client, err := ionos.NewClient(config.APIKey) if err != nil { return nil, fmt.Errorf("ionos: %w", err) } @@ -126,7 +126,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { return errors.New("ionos: no matching zone found for domain") } - filter := &internal.RecordsFilter{ + filter := &ionos.RecordsFilter{ Suffix: name, RecordType: "TXT", } @@ -136,7 +136,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) } - records = append(records, internal.Record{ + records = append(records, ionos.Record{ Name: name, Content: info.Value, TTL: d.config.TTL, @@ -169,7 +169,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return errors.New("ionos: no matching zone found for domain") } - filter := &internal.RecordsFilter{ + filter := &ionos.RecordsFilter{ Suffix: name, RecordType: "TXT", } @@ -193,8 +193,8 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return fmt.Errorf("ionos: failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) } -func findZone(zones []internal.Zone, domain string) *internal.Zone { - var result *internal.Zone +func findZone(zones []ionos.Zone, domain string) *ionos.Zone { + var result *ionos.Zone for _, zone := range zones { if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { diff --git a/providers/dns/uniteddomains/uniteddomains.go b/providers/dns/uniteddomains/uniteddomains.go new file mode 100644 index 000000000..0cb50c2af --- /dev/null +++ b/providers/dns/uniteddomains/uniteddomains.go @@ -0,0 +1,210 @@ +// Package uniteddomains implements a DNS provider for solving the DNS-01 challenge using United-Domains. +package uniteddomains + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "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/internal/ionos" +) + +// Environment variables names. +const ( + envNamespace = "UNITEDDOMAINS_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const minTTL = 300 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *ionos.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for United-Domains. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("uniteddomains: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for United-Domains. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("uniteddomains: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("uniteddomains: credentials missing") + } + + if config.TTL < minTTL { + return nil, fmt.Errorf("uniteddomains: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client, err := ionos.NewClient(config.APIKey) + if err != nil { + return nil, fmt.Errorf("uniteddomains: %w", err) + } + + client.BaseURL, _ = url.Parse(ionos.DefaultUnitedDomainsBaseURL) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + 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 using the specified parameters. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) + if err != nil { + return fmt.Errorf("uniteddomains: failed to get zones: %w", err) + } + + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("uniteddomains: no matching zone found for domain") + } + + filter := &ionos.RecordsFilter{ + Suffix: name, + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("uniteddomains: failed to get records (zone=%s): %w", zone.ID, err) + } + + records = append(records, ionos.Record{ + Name: name, + Content: info.Value, + TTL: d.config.TTL, + Type: "TXT", + }) + + err = d.client.ReplaceRecords(ctx, zone.ID, records) + if err != nil { + return fmt.Errorf("uniteddomains: failed to create/update records (zone=%s): %w", zone.ID, err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) + if err != nil { + return fmt.Errorf("uniteddomains: failed to get zones: %w", err) + } + + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("uniteddomains: no matching zone found for domain") + } + + filter := &ionos.RecordsFilter{ + Suffix: name, + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("uniteddomains: failed to get records (zone=%s): %w", zone.ID, err) + } + + for _, record := range records { + if record.Name == name && record.Content == strconv.Quote(info.Value) { + err = d.client.RemoveRecord(ctx, zone.ID, record.ID) + if err != nil { + return fmt.Errorf("uniteddomains: failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) + } + + return nil + } + } + + return fmt.Errorf("uniteddomains: failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) +} + +func findZone(zones []ionos.Zone, domain string) *ionos.Zone { + var result *ionos.Zone + + for _, zone := range zones { + if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { + if result == nil || len(zone.Name) > len(result.Name) { + result = &zone + } + } + } + + return result +} diff --git a/providers/dns/uniteddomains/uniteddomains.toml b/providers/dns/uniteddomains/uniteddomains.toml new file mode 100644 index 000000000..3663cb867 --- /dev/null +++ b/providers/dns/uniteddomains/uniteddomains.toml @@ -0,0 +1,22 @@ +Name = "United-Domains" +Description = '''''' +URL = "https://www.united-domains.de/" +Code = "uniteddomains" +Since = "v4.29.0" + +Example = ''' +UNITEDDOMAINS_API_KEY=xxxxxxxx \ +lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + UNITEDDOMAINS_API_KEY = "API key `.` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/" + [Configuration.Additional] + UNITEDDOMAINS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + UNITEDDOMAINS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 900)" + UNITEDDOMAINS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + UNITEDDOMAINS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://www.united-domains.de/dns-apidoc/" diff --git a/providers/dns/uniteddomains/uniteddomains_test.go b/providers/dns/uniteddomains/uniteddomains_test.go new file mode 100644 index 000000000..841268ca2 --- /dev/null +++ b/providers/dns/uniteddomains/uniteddomains_test.go @@ -0,0 +1,130 @@ +package uniteddomains + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAPIKey). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "uniteddomains: some credentials information are missing: UNITEDDOMAINS_API_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 + apiKey string + tll int + expected string + }{ + { + desc: "success", + apiKey: "123", + tll: minTTL, + }, + { + desc: "missing credentials", + tll: minTTL, + expected: "uniteddomains: credentials missing", + }, + { + desc: "invalid TTL", + apiKey: "123", + tll: 30, + expected: "uniteddomains: invalid TTL, TTL (30) must be greater than 300", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.TTL = test.tll + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 8ecbb76cc..2add1f75f 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -155,6 +155,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/timewebcloud" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/ultradns" + "github.com/go-acme/lego/v4/providers/dns/uniteddomains" "github.com/go-acme/lego/v4/providers/dns/variomedia" "github.com/go-acme/lego/v4/providers/dns/vegadns" "github.com/go-acme/lego/v4/providers/dns/vercel" @@ -478,6 +479,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return transip.NewDNSProvider() case "ultradns": return ultradns.NewDNSProvider() + case "uniteddomains": + return uniteddomains.NewDNSProvider() case "variomedia": return variomedia.NewDNSProvider() case "vegadns": From 1757cdeaee03b534e4952c22aa1705c879730069 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sat, 29 Nov 2025 14:20:42 +0100 Subject: [PATCH 19/95] chore: use common implementations of the providers instead of the API clients (#2734) --- providers/dns/active24/active24.go | 136 +----------- providers/dns/active24/active24_test.go | 6 +- providers/dns/edgecenter/edgecenter.go | 91 ++------ providers/dns/edgecenter/edgecenter_test.go | 6 +- providers/dns/gcore/gcore.go | 89 ++------ providers/dns/gcore/gcore_test.go | 6 +- providers/dns/hostingde/hostingde.go | 154 ++------------ providers/dns/hostingde/hostingde_test.go | 6 +- providers/dns/httpnet/httpnet.go | 158 ++------------ providers/dns/httpnet/httpnet_test.go | 6 +- .../active24/{ => internal}/client.go | 2 +- .../active24/{ => internal}/client_test.go | 2 +- .../{ => internal}/fixtures/error_403.json | 0 .../{ => internal}/fixtures/error_422.json | 0 .../{ => internal}/fixtures/error_v1.json | 0 .../{ => internal}/fixtures/records.json | 0 .../{ => internal}/fixtures/services.json | 0 .../internal/active24/{ => internal}/types.go | 2 +- providers/dns/internal/active24/provider.go | 179 ++++++++++++++++ .../dns/internal/active24/provider_test.go | 57 +++++ .../internal/gcore/{ => internal}/client.go | 9 +- .../gcore/{ => internal}/client_test.go | 2 +- .../internal/gcore/{ => internal}/types.go | 2 +- providers/dns/internal/gcore/provider.go | 126 +++++++++++ providers/dns/internal/gcore/provider_test.go | 42 ++++ .../hostingde/{ => internal}/client.go | 9 +- .../hostingde/{ => internal}/client_test.go | 2 +- .../fixtures/zoneConfigsFind-request.json | 0 .../fixtures/zoneConfigsFind.json | 0 .../fixtures/zoneConfigsFind_error.json | 0 .../fixtures/zoneUpdate-request.json | 0 .../{ => internal}/fixtures/zoneUpdate.json | 0 .../fixtures/zoneUpdate_error.json | 0 .../hostingde/{ => internal}/types.go | 2 +- providers/dns/internal/hostingde/provider.go | 196 ++++++++++++++++++ .../dns/internal/hostingde/provider_test.go | 50 +++++ .../internal/ionos/{ => internal}/client.go | 9 +- .../ionos/{ => internal}/client_test.go | 2 +- .../{ => internal}/fixtures/get_records.json | 0 .../fixtures/get_records_error.json | 0 .../{ => internal}/fixtures/list_zones.json | 0 .../fixtures/list_zones_error.json | 0 .../fixtures/remove_record_error.json | 0 .../fixtures/replace_records_error.json | 0 .../internal/ionos/{ => internal}/types.go | 2 +- providers/dns/internal/ionos/provider.go | 173 ++++++++++++++++ providers/dns/internal/ionos/provider_test.go | 52 +++++ .../rimuhosting/{ => internal}/client.go | 10 +- .../rimuhosting/{ => internal}/client_test.go | 2 +- .../{ => internal}/fixtures/add_record.xml | 0 .../fixtures/add_record_error.xml | 0 .../fixtures/add_record_same_domain.xml | 0 .../{ => internal}/fixtures/delete_record.xml | 0 .../fixtures/delete_record_error.xml | 0 .../fixtures/delete_record_nothing.xml | 0 .../{ => internal}/fixtures/find_records.xml | 0 .../fixtures/find_records_empty.xml | 0 .../fixtures/find_records_pattern.xml | 0 .../rimuhosting/{ => internal}/types.go | 2 +- .../dns/internal/rimuhosting/provider.go | 107 ++++++++++ .../dns/internal/rimuhosting/provider_test.go | 46 ++++ .../selectel/{ => internal}/client.go | 12 +- .../selectel/{ => internal}/client_test.go | 2 +- .../fixtures/add_record-request.json | 0 .../{ => internal}/fixtures/add_record.json | 0 .../{ => internal}/fixtures/domains.json | 0 .../{ => internal}/fixtures/error.json | 0 .../{ => internal}/fixtures/list_records.json | 0 .../internal/selectel/{ => internal}/types.go | 2 +- providers/dns/internal/selectel/provider.go | 137 ++++++++++++ .../dns/internal/selectel/provider_test.go | 55 +++++ providers/dns/ionos/ionos.go | 130 ++---------- providers/dns/ionos/ionos_test.go | 10 +- providers/dns/rimuhosting/rimuhosting.go | 73 ++----- providers/dns/rimuhosting/rimuhosting_test.go | 4 +- providers/dns/selectel/selectel.go | 106 ++-------- providers/dns/selectel/selectel_test.go | 9 +- providers/dns/uniteddomains/uniteddomains.go | 133 ++---------- .../dns/uniteddomains/uniteddomains_test.go | 10 +- providers/dns/vscale/vscale.go | 106 ++-------- providers/dns/vscale/vscale_test.go | 9 +- providers/dns/websupport/websupport.go | 136 +----------- providers/dns/websupport/websupport_test.go | 6 +- providers/dns/zonomi/zonomi.go | 75 ++----- providers/dns/zonomi/zonomi_test.go | 4 +- 85 files changed, 1458 insertions(+), 1306 deletions(-) rename providers/dns/internal/active24/{ => internal}/client.go (99%) rename providers/dns/internal/active24/{ => internal}/client_test.go (99%) rename providers/dns/internal/active24/{ => internal}/fixtures/error_403.json (100%) rename providers/dns/internal/active24/{ => internal}/fixtures/error_422.json (100%) rename providers/dns/internal/active24/{ => internal}/fixtures/error_v1.json (100%) rename providers/dns/internal/active24/{ => internal}/fixtures/records.json (100%) rename providers/dns/internal/active24/{ => internal}/fixtures/services.json (100%) rename providers/dns/internal/active24/{ => internal}/types.go (99%) create mode 100644 providers/dns/internal/active24/provider.go create mode 100644 providers/dns/internal/active24/provider_test.go rename providers/dns/internal/gcore/{ => internal}/client.go (96%) rename providers/dns/internal/gcore/{ => internal}/client_test.go (99%) rename providers/dns/internal/gcore/{ => internal}/types.go (96%) create mode 100644 providers/dns/internal/gcore/provider.go create mode 100644 providers/dns/internal/gcore/provider_test.go rename providers/dns/internal/hostingde/{ => internal}/client.go (94%) rename providers/dns/internal/hostingde/{ => internal}/client_test.go (99%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneConfigsFind-request.json (100%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneConfigsFind.json (100%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneConfigsFind_error.json (100%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneUpdate-request.json (100%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneUpdate.json (100%) rename providers/dns/internal/hostingde/{ => internal}/fixtures/zoneUpdate_error.json (100%) rename providers/dns/internal/hostingde/{ => internal}/types.go (99%) create mode 100644 providers/dns/internal/hostingde/provider.go create mode 100644 providers/dns/internal/hostingde/provider_test.go rename providers/dns/internal/ionos/{ => internal}/client.go (95%) rename providers/dns/internal/ionos/{ => internal}/client_test.go (99%) rename providers/dns/internal/ionos/{ => internal}/fixtures/get_records.json (100%) rename providers/dns/internal/ionos/{ => internal}/fixtures/get_records_error.json (100%) rename providers/dns/internal/ionos/{ => internal}/fixtures/list_zones.json (100%) rename providers/dns/internal/ionos/{ => internal}/fixtures/list_zones_error.json (100%) rename providers/dns/internal/ionos/{ => internal}/fixtures/remove_record_error.json (100%) rename providers/dns/internal/ionos/{ => internal}/fixtures/replace_records_error.json (100%) rename providers/dns/internal/ionos/{ => internal}/types.go (99%) create mode 100644 providers/dns/internal/ionos/provider.go create mode 100644 providers/dns/internal/ionos/provider_test.go rename providers/dns/internal/rimuhosting/{ => internal}/client.go (94%) rename providers/dns/internal/rimuhosting/{ => internal}/client_test.go (99%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/add_record.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/add_record_error.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/add_record_same_domain.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/delete_record.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/delete_record_error.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/delete_record_nothing.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/find_records.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/find_records_empty.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/fixtures/find_records_pattern.xml (100%) rename providers/dns/internal/rimuhosting/{ => internal}/types.go (98%) create mode 100644 providers/dns/internal/rimuhosting/provider.go create mode 100644 providers/dns/internal/rimuhosting/provider_test.go rename providers/dns/internal/selectel/{ => internal}/client.go (93%) rename providers/dns/internal/selectel/{ => internal}/client_test.go (99%) rename providers/dns/internal/selectel/{ => internal}/fixtures/add_record-request.json (100%) rename providers/dns/internal/selectel/{ => internal}/fixtures/add_record.json (100%) rename providers/dns/internal/selectel/{ => internal}/fixtures/domains.json (100%) rename providers/dns/internal/selectel/{ => internal}/fixtures/error.json (100%) rename providers/dns/internal/selectel/{ => internal}/fixtures/list_records.json (100%) rename providers/dns/internal/selectel/{ => internal}/types.go (98%) create mode 100644 providers/dns/internal/selectel/provider.go create mode 100644 providers/dns/internal/selectel/provider_test.go diff --git a/providers/dns/active24/active24.go b/providers/dns/active24/active24.go index c8107cab6..0b925de6a 100644 --- a/providers/dns/active24/active24.go +++ b/providers/dns/active24/active24.go @@ -2,17 +2,15 @@ package active24 import ( - "context" "errors" "fmt" "net/http" - "strconv" "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/active24" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) const baseAPIDomain = "active24.cz" @@ -31,15 +29,7 @@ const ( ) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - Secret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = active24.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -55,8 +45,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *active24.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Active24. @@ -79,83 +68,29 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("active24: the configuration of the DNS provider is nil") } - client, err := active24.NewClient(baseAPIDomain, config.APIKey, config.Secret) + provider, err := active24.NewDNSProviderConfig(config, baseAPIDomain) if err != nil { return nil, fmt.Errorf("active24: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil + return &DNSProvider{prv: provider}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("active24: 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("active24: %w", err) } - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("active24: find service ID: %w", err) - } - - record := active24.Record{ - Type: "TXT", - Name: subDomain, - Content: info.Value, - TTL: d.config.TTL, - } - - err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record) - if err != nil { - return fmt.Errorf("active24: create record: %w", err) - } - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("active24: could not find zone for domain %q: %w", domain, err) - } - - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("active24: find service ID: %w", err) - } - - recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info) - if err != nil { - return fmt.Errorf("active24: find record ID: %w", err) - } - - err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID)) - if err != nil { - return fmt.Errorf("active24: delete record %w", err) + return fmt.Errorf("active24: %w", err) } return nil @@ -164,58 +99,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // 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 -} - -func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) { - services, err := d.client.GetServices(ctx) - if err != nil { - return 0, fmt.Errorf("get services: %w", err) - } - - for _, service := range services { - if service.ServiceName != "domain" { - continue - } - - if service.Name != domain { - continue - } - - return service.ID, nil - } - - return 0, fmt.Errorf("service not found for domain: %s", domain) -} - -func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) { - // NOTE(ldez): Despite the API documentation, the filter doesn't seem to work. - filter := active24.RecordFilter{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: []string{"TXT"}, - Content: info.Value, - } - - records, err := d.client.GetRecords(ctx, serviceID, filter) - if err != nil { - return 0, fmt.Errorf("get records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" { - continue - } - - if record.Name != dns01.UnFqdn(info.EffectiveFQDN) { - continue - } - - if record.Content != info.Value { - continue - } - - return record.ID, nil - } - - return 0, errors.New("no record found") + return d.prv.Timeout() } diff --git a/providers/dns/active24/active24_test.go b/providers/dns/active24/active24_test.go index 363e0229a..2987fb27b 100644 --- a/providers/dns/active24/active24_test.go +++ b/providers/dns/active24/active24_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) } @@ -110,8 +109,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/edgecenter/edgecenter.go b/providers/dns/edgecenter/edgecenter.go index 2040a304c..cfc75b521 100644 --- a/providers/dns/edgecenter/edgecenter.go +++ b/providers/dns/edgecenter/edgecenter.go @@ -1,17 +1,15 @@ +// Package edgecenter implements a DNS provider for solving the DNS-01 challenge using EdgeCenter. package edgecenter import ( - "context" "errors" "fmt" "net/http" - "net/url" "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/internal/gcore" ) @@ -27,28 +25,19 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - defaultPropagationTimeout = 360 * time.Second - defaultPollingInterval = 20 * time.Second -) +const defaultBaseURL = "https://api.edgecenter.ru/dns" var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config for DNSProvider. -type Config struct { - APIToken string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = gcore.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, @@ -57,8 +46,7 @@ func NewDefaultConfig() *Config { // DNSProvider an implementation of challenge.Provider contract. type DNSProvider struct { - config *Config - client *gcore.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. @@ -80,81 +68,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("edgecenter: the configuration of the DNS provider is nil") } - if config.APIToken == "" { - return nil, errors.New("edgecenter: incomplete credentials provided") + provider, err := gcore.NewDNSProviderConfig(config, defaultBaseURL) + if err != nil { + return nil, fmt.Errorf("edgecenter: %w", err) } - client := gcore.NewClient(config.APIToken) - client.BaseURL, _ = url.Parse(gcore.DefaultEdgeCenterBaseURL) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil + return &DNSProvider{prv: provider}, nil } -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) +// 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("edgecenter: %w", err) } - err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) - if err != nil { - return fmt.Errorf("edgecenter: add txt record: %w", err) - } - return nil } -// CleanUp removes the record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) +// 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("edgecenter: %w", err) } - err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("edgecenter: remove txt record: %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.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { - var lastErr error - - for zone := range dns01.UnFqdnDomainsSeq(fqdn) { - dnsZone, err := d.client.GetZone(ctx, zone) - if err != nil { - lastErr = err - continue - } - - return dnsZone.Name, nil - } - - return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) + return d.prv.Timeout() } diff --git a/providers/dns/edgecenter/edgecenter_test.go b/providers/dns/edgecenter/edgecenter_test.go index 79814680d..e3ec43981 100644 --- a/providers/dns/edgecenter/edgecenter_test.go +++ b/providers/dns/edgecenter/edgecenter_test.go @@ -43,8 +43,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) } @@ -78,8 +77,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/gcore/gcore.go b/providers/dns/gcore/gcore.go index 6400dc0b3..9b98f28d4 100644 --- a/providers/dns/gcore/gcore.go +++ b/providers/dns/gcore/gcore.go @@ -1,7 +1,7 @@ +// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core. package gcore import ( - "context" "errors" "fmt" "net/http" @@ -10,7 +10,6 @@ import ( "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/internal/gcore" ) @@ -26,28 +25,17 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - defaultPropagationTimeout = 360 * time.Second - defaultPollingInterval = 20 * time.Second -) - var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config for DNSProvider. -type Config struct { - APIToken string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = gcore.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, @@ -56,8 +44,7 @@ func NewDefaultConfig() *Config { // DNSProvider an implementation of challenge.Provider contract. type DNSProvider struct { - config *Config - client *gcore.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. @@ -79,80 +66,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("gcore: the configuration of the DNS provider is nil") } - if config.APIToken == "" { - return nil, errors.New("gcore: incomplete credentials provided") + provider, err := gcore.NewDNSProviderConfig(config, "") + if err != nil { + return nil, fmt.Errorf("gcore: %w", err) } - client := gcore.NewClient(config.APIToken) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil + return &DNSProvider{prv: provider}, nil } -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) +// 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("gcore: %w", err) } - err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) - if err != nil { - return fmt.Errorf("gcore: add txt record: %w", err) - } - return nil } -// CleanUp removes the record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) +// 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("gcore: %w", err) } - err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("gcore: remove txt record: %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.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { - var lastErr error - - for zone := range dns01.UnFqdnDomainsSeq(fqdn) { - dnsZone, err := d.client.GetZone(ctx, zone) - if err != nil { - lastErr = err - continue - } - - return dnsZone.Name, nil - } - - return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) + return d.prv.Timeout() } diff --git a/providers/dns/gcore/gcore_test.go b/providers/dns/gcore/gcore_test.go index 88c1d02a5..6f8e38c12 100644 --- a/providers/dns/gcore/gcore_test.go +++ b/providers/dns/gcore/gcore_test.go @@ -43,8 +43,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) } @@ -78,8 +77,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/hostingde/hostingde.go b/providers/dns/hostingde/hostingde.go index 48c44998f..1e022b630 100644 --- a/providers/dns/hostingde/hostingde.go +++ b/providers/dns/hostingde/hostingde.go @@ -2,17 +2,14 @@ package hostingde 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/internal/hostingde" ) @@ -32,14 +29,7 @@ const ( var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - ZoneName string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = hostingde.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 *hostingde.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for hosting.de. @@ -84,130 +70,27 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("hostingde: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("hostingde: API key missing") + provider, err := hostingde.NewDNSProviderConfig(config, "") + if err != nil { + return nil, fmt.Errorf("hostingde: %w", err) } - client := hostingde.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil + return &DNSProvider{prv: provider}, 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. +// Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostingde: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := hostingde.ZoneConfigsFindRequest{ - Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("hostingde: %w", err) } - zoneConfig.Name = zoneName - - rec := []hostingde.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: info.Value, - TTL: d.config.TTL, - }} - - req := hostingde.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToAdd: rec, - } - - response, err := d.client.UpdateZone(ctx, req) - if err != nil { - return fmt.Errorf("hostingde: %w", err) - } - - for _, record := range response.Records { - if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { - d.recordIDsMu.Lock() - d.recordIDs[info.EffectiveFQDN] = record.ID - d.recordIDsMu.Unlock() - } - } - - if d.recordIDs[info.EffectiveFQDN] == "" { - return fmt.Errorf("hostingde: error getting ID of just created record, for domain %s", domain) - } - 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) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostingde: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := hostingde.ZoneConfigsFindRequest{ - Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) - if err != nil { - return fmt.Errorf("hostingde: %w", err) - } - - zoneConfig.Name = zoneName - - rec := []hostingde.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: `"` + info.Value + `"`, - }} - - req := hostingde.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToDelete: rec, - } - - // Delete record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, info.EffectiveFQDN) - d.recordIDsMu.Unlock() - - _, err = d.client.UpdateZone(ctx, req) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { return fmt.Errorf("hostingde: %w", err) } @@ -215,19 +98,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } -func (d *DNSProvider) getZoneName(fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil - } - - zoneName, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if zoneName == "" { - return "", errors.New("empty zone name") - } - - return dns01.UnFqdn(zoneName), 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/hostingde/hostingde_test.go b/providers/dns/hostingde/hostingde_test.go index 1611cb51b..a92006f81 100644 --- a/providers/dns/hostingde/hostingde_test.go +++ b/providers/dns/hostingde/hostingde_test.go @@ -59,8 +59,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.recordIDs) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -102,8 +101,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.recordIDs) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/httpnet/httpnet.go b/providers/dns/httpnet/httpnet.go index f18eefd97..4a88f1092 100644 --- a/providers/dns/httpnet/httpnet.go +++ b/providers/dns/httpnet/httpnet.go @@ -2,18 +2,14 @@ package httpnet 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/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/internal/hostingde" ) @@ -30,17 +26,12 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://partner.http.net/api/dns/v1/json" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - ZoneName string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = hostingde.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -57,11 +48,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *hostingde.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for http.net. @@ -85,131 +72,27 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("httpnet: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("httpnet: API key missing") + provider, err := hostingde.NewDNSProviderConfig(config, defaultBaseURL) + if err != nil { + return nil, fmt.Errorf("httpnet: %w", err) } - client := hostingde.NewClient(config.APIKey) - client.BaseURL, _ = url.Parse(hostingde.DefaultHTTPNetBaseURL) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil + return &DNSProvider{prv: provider}, 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. +// Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := hostingde.ZoneConfigsFindRequest{ - Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("httpnet: %w", err) } - zoneConfig.Name = zoneName - - rec := []hostingde.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: info.Value, - TTL: d.config.TTL, - }} - - req := hostingde.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToAdd: rec, - } - - response, err := d.client.UpdateZone(ctx, req) - if err != nil { - return fmt.Errorf("httpnet: %w", err) - } - - for _, record := range response.Records { - if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { - d.recordIDsMu.Lock() - d.recordIDs[info.EffectiveFQDN] = record.ID - d.recordIDsMu.Unlock() - } - } - - if d.recordIDs[info.EffectiveFQDN] == "" { - return fmt.Errorf("httpnet: error getting ID of just created record, for domain %s", domain) - } - 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) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := hostingde.ZoneConfigsFindRequest{ - Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) - if err != nil { - return fmt.Errorf("httpnet: %w", err) - } - - zoneConfig.Name = zoneName - - rec := []hostingde.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: `"` + info.Value + `"`, - }} - - req := hostingde.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToDelete: rec, - } - - // Delete record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, info.EffectiveFQDN) - d.recordIDsMu.Unlock() - - _, err = d.client.UpdateZone(ctx, req) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { return fmt.Errorf("httpnet: %w", err) } @@ -217,19 +100,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } -func (d *DNSProvider) getZoneName(fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil - } - - zoneName, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if zoneName == "" { - return "", errors.New("empty zone name") - } - - return dns01.UnFqdn(zoneName), 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/httpnet/httpnet_test.go b/providers/dns/httpnet/httpnet_test.go index 64a94f80c..ef1d2a1b7 100644 --- a/providers/dns/httpnet/httpnet_test.go +++ b/providers/dns/httpnet/httpnet_test.go @@ -59,8 +59,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.recordIDs) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -102,8 +101,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.recordIDs) + require.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/internal/active24/client.go b/providers/dns/internal/active24/internal/client.go similarity index 99% rename from providers/dns/internal/active24/client.go rename to providers/dns/internal/active24/internal/client.go index 10aaa4666..69e94b367 100644 --- a/providers/dns/internal/active24/client.go +++ b/providers/dns/internal/active24/internal/client.go @@ -1,4 +1,4 @@ -package active24 +package internal import ( "bytes" diff --git a/providers/dns/internal/active24/client_test.go b/providers/dns/internal/active24/internal/client_test.go similarity index 99% rename from providers/dns/internal/active24/client_test.go rename to providers/dns/internal/active24/internal/client_test.go index ad2a8126b..f62f78785 100644 --- a/providers/dns/internal/active24/client_test.go +++ b/providers/dns/internal/active24/internal/client_test.go @@ -1,4 +1,4 @@ -package active24 +package internal import ( "net/http" diff --git a/providers/dns/internal/active24/fixtures/error_403.json b/providers/dns/internal/active24/internal/fixtures/error_403.json similarity index 100% rename from providers/dns/internal/active24/fixtures/error_403.json rename to providers/dns/internal/active24/internal/fixtures/error_403.json diff --git a/providers/dns/internal/active24/fixtures/error_422.json b/providers/dns/internal/active24/internal/fixtures/error_422.json similarity index 100% rename from providers/dns/internal/active24/fixtures/error_422.json rename to providers/dns/internal/active24/internal/fixtures/error_422.json diff --git a/providers/dns/internal/active24/fixtures/error_v1.json b/providers/dns/internal/active24/internal/fixtures/error_v1.json similarity index 100% rename from providers/dns/internal/active24/fixtures/error_v1.json rename to providers/dns/internal/active24/internal/fixtures/error_v1.json diff --git a/providers/dns/internal/active24/fixtures/records.json b/providers/dns/internal/active24/internal/fixtures/records.json similarity index 100% rename from providers/dns/internal/active24/fixtures/records.json rename to providers/dns/internal/active24/internal/fixtures/records.json diff --git a/providers/dns/internal/active24/fixtures/services.json b/providers/dns/internal/active24/internal/fixtures/services.json similarity index 100% rename from providers/dns/internal/active24/fixtures/services.json rename to providers/dns/internal/active24/internal/fixtures/services.json diff --git a/providers/dns/internal/active24/types.go b/providers/dns/internal/active24/internal/types.go similarity index 99% rename from providers/dns/internal/active24/types.go rename to providers/dns/internal/active24/internal/types.go index b9a7ea427..ed8dfc9d3 100644 --- a/providers/dns/internal/active24/types.go +++ b/providers/dns/internal/active24/internal/types.go @@ -1,4 +1,4 @@ -package active24 +package internal import "fmt" diff --git a/providers/dns/internal/active24/provider.go b/providers/dns/internal/active24/provider.go new file mode 100644 index 000000000..ae79b8b17 --- /dev/null +++ b/providers/dns/internal/active24/provider.go @@ -0,0 +1,179 @@ +// Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24. +package active24 + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/active24/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + Secret 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 +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Active24. +func NewDNSProviderConfig(config *Config, baseAPIDomain string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(baseAPIDomain, config.APIKey, config.Secret) + 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, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + 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 err + } + + serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("find service ID: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + TTL: d.config.TTL, + } + + err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record) + if err != nil { + return fmt.Errorf("create record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + 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) + } + + serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("find service ID: %w", err) + } + + recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info) + if err != nil { + return fmt.Errorf("find record ID: %w", err) + } + + err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID)) + if err != nil { + return fmt.Errorf("delete record %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) { + services, err := d.client.GetServices(ctx) + if err != nil { + return 0, fmt.Errorf("get services: %w", err) + } + + for _, service := range services { + if service.ServiceName != "domain" { + continue + } + + if service.Name != domain { + continue + } + + return service.ID, nil + } + + return 0, fmt.Errorf("service not found for domain: %s", domain) +} + +func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) { + // NOTE(ldez): Despite the API documentation, the filter doesn't seem to work. + filter := internal.RecordFilter{ + Name: dns01.UnFqdn(info.EffectiveFQDN), + Type: []string{"TXT"}, + Content: info.Value, + } + + records, err := d.client.GetRecords(ctx, serviceID, filter) + if err != nil { + return 0, fmt.Errorf("get records: %w", err) + } + + for _, record := range records { + if record.Type != "TXT" { + continue + } + + if record.Name != dns01.UnFqdn(info.EffectiveFQDN) { + continue + } + + if record.Content != info.Value { + continue + } + + return record.ID, nil + } + + return 0, errors.New("no record found") +} diff --git a/providers/dns/internal/active24/provider_test.go b/providers/dns/internal/active24/provider_test.go new file mode 100644 index 000000000..e2959fd6e --- /dev/null +++ b/providers/dns/internal/active24/provider_test.go @@ -0,0 +1,57 @@ +package active24 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + secret string + expected string + }{ + { + desc: "success", + apiKey: "user", + secret: "secret", + }, + { + desc: "missing API key", + apiKey: "", + secret: "secret", + expected: "credentials missing", + }, + { + desc: "missing secret", + apiKey: "user", + secret: "", + expected: "credentials missing", + }, + { + desc: "missing credentials", + expected: "credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.APIKey = test.apiKey + config.Secret = test.secret + + p, err := NewDNSProviderConfig(config, "example.com") + + 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) + } + }) + } +} diff --git a/providers/dns/internal/gcore/client.go b/providers/dns/internal/gcore/internal/client.go similarity index 96% rename from providers/dns/internal/gcore/client.go rename to providers/dns/internal/gcore/internal/client.go index 93f17c0d2..f3ad4e461 100644 --- a/providers/dns/internal/gcore/client.go +++ b/providers/dns/internal/gcore/internal/client.go @@ -1,4 +1,4 @@ -package gcore +package internal import ( "bytes" @@ -14,10 +14,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const ( - DefaultGCoreBaseURL = "https://api.gcore.com/dns" - DefaultEdgeCenterBaseURL = "https://api.edgecenter.ru/dns" -) +const defaultBaseURL = "https://api.gcore.com/dns" const ( authorizationHeader = "Authorization" @@ -36,7 +33,7 @@ type Client struct { // NewClient constructor of Client. func NewClient(token string) *Client { - baseURL, _ := url.Parse(DefaultGCoreBaseURL) + baseURL, _ := url.Parse(defaultBaseURL) return &Client{ token: token, diff --git a/providers/dns/internal/gcore/client_test.go b/providers/dns/internal/gcore/internal/client_test.go similarity index 99% rename from providers/dns/internal/gcore/client_test.go rename to providers/dns/internal/gcore/internal/client_test.go index 79289ef42..7d70c9308 100644 --- a/providers/dns/internal/gcore/client_test.go +++ b/providers/dns/internal/gcore/internal/client_test.go @@ -1,4 +1,4 @@ -package gcore +package internal import ( "net/http" diff --git a/providers/dns/internal/gcore/types.go b/providers/dns/internal/gcore/internal/types.go similarity index 96% rename from providers/dns/internal/gcore/types.go rename to providers/dns/internal/gcore/internal/types.go index 1c4fbd502..4245f5ba8 100644 --- a/providers/dns/internal/gcore/types.go +++ b/providers/dns/internal/gcore/internal/types.go @@ -1,4 +1,4 @@ -package gcore +package internal import "fmt" diff --git a/providers/dns/internal/gcore/provider.go b/providers/dns/internal/gcore/provider.go new file mode 100644 index 000000000..b2078eba5 --- /dev/null +++ b/providers/dns/internal/gcore/provider.go @@ -0,0 +1,126 @@ +// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core. +package gcore + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "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/gcore/internal" +) + +const ( + DefaultPropagationTimeout = 360 * time.Second + DefaultPollingInterval = 20 * time.Second +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config for DNSProvider. +type Config struct { + APIToken string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// DNSProvider an implementation of challenge.Provider contract. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.APIToken == "" { + return nil, errors.New("incomplete credentials provided") + } + + client := internal.NewClient(config.APIToken) + + if baseURL != "" { + client.BaseURL, _ = url.Parse(baseURL) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) + if err != nil { + return err + } + + err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) + if err != nil { + return fmt.Errorf("add txt record: %w", err) + } + + return nil +} + +// CleanUp removes the record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) + if err != nil { + return err + } + + err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("remove txt record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { + var lastErr error + + for zone := range dns01.UnFqdnDomainsSeq(fqdn) { + dnsZone, err := d.client.GetZone(ctx, zone) + if err != nil { + lastErr = err + continue + } + + return dnsZone.Name, nil + } + + return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) +} diff --git a/providers/dns/internal/gcore/provider_test.go b/providers/dns/internal/gcore/provider_test.go new file mode 100644 index 000000000..f29dadff9 --- /dev/null +++ b/providers/dns/internal/gcore/provider_test.go @@ -0,0 +1,42 @@ +package gcore + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiToken string + expected string + }{ + { + desc: "success", + apiToken: "A", + }, + { + desc: "missing credentials", + expected: "incomplete credentials provided", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.APIToken = test.apiToken + + 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) + } + }) + } +} diff --git a/providers/dns/internal/hostingde/client.go b/providers/dns/internal/hostingde/internal/client.go similarity index 94% rename from providers/dns/internal/hostingde/client.go rename to providers/dns/internal/hostingde/internal/client.go index 43354384f..133c3479c 100644 --- a/providers/dns/internal/hostingde/client.go +++ b/providers/dns/internal/hostingde/internal/client.go @@ -1,4 +1,4 @@ -package hostingde +package internal import ( "bytes" @@ -14,10 +14,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const ( - DefaultHostingdeBaseURL = "https://secure.hosting.de/api/dns/v1/json" - DefaultHTTPNetBaseURL = "https://partner.http.net/api/dns/v1/json" -) +const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" // Client the API client for Hosting.de. type Client struct { @@ -29,7 +26,7 @@ type Client struct { // NewClient creates new Client. func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(DefaultHostingdeBaseURL) + baseURL, _ := url.Parse(defaultBaseURL) return &Client{ apiKey: apiKey, diff --git a/providers/dns/internal/hostingde/client_test.go b/providers/dns/internal/hostingde/internal/client_test.go similarity index 99% rename from providers/dns/internal/hostingde/client_test.go rename to providers/dns/internal/hostingde/internal/client_test.go index 93e0c76e1..d55bbf690 100644 --- a/providers/dns/internal/hostingde/client_test.go +++ b/providers/dns/internal/hostingde/internal/client_test.go @@ -1,4 +1,4 @@ -package hostingde +package internal import ( "encoding/json" diff --git a/providers/dns/internal/hostingde/fixtures/zoneConfigsFind-request.json b/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneConfigsFind-request.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json diff --git a/providers/dns/internal/hostingde/fixtures/zoneConfigsFind.json b/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneConfigsFind.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind.json diff --git a/providers/dns/internal/hostingde/fixtures/zoneConfigsFind_error.json b/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind_error.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneConfigsFind_error.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind_error.json diff --git a/providers/dns/internal/hostingde/fixtures/zoneUpdate-request.json b/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneUpdate-request.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json diff --git a/providers/dns/internal/hostingde/fixtures/zoneUpdate.json b/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneUpdate.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneUpdate.json diff --git a/providers/dns/internal/hostingde/fixtures/zoneUpdate_error.json b/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate_error.json similarity index 100% rename from providers/dns/internal/hostingde/fixtures/zoneUpdate_error.json rename to providers/dns/internal/hostingde/internal/fixtures/zoneUpdate_error.json diff --git a/providers/dns/internal/hostingde/types.go b/providers/dns/internal/hostingde/internal/types.go similarity index 99% rename from providers/dns/internal/hostingde/types.go rename to providers/dns/internal/hostingde/internal/types.go index 86b69ec42..330eab27d 100644 --- a/providers/dns/internal/hostingde/types.go +++ b/providers/dns/internal/hostingde/internal/types.go @@ -1,4 +1,4 @@ -package hostingde +package internal import "encoding/json" diff --git a/providers/dns/internal/hostingde/provider.go b/providers/dns/internal/hostingde/provider.go new file mode 100644 index 000000000..644dc8aaf --- /dev/null +++ b/providers/dns/internal/hostingde/provider.go @@ -0,0 +1,196 @@ +// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de. +package hostingde + +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/hostingde/internal" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + ZoneName 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]string + recordIDsMu sync.Mutex +} + +// NewDNSProviderConfig return a DNSProvider instance configured for hosting.de. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("API key missing") + } + + client := internal.NewClient(config.APIKey) + + if baseURL != "" { + client.BaseURL, _ = url.Parse(baseURL) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, 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 { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := internal.ZoneConfigsFindRequest{ + Filter: internal.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) + if err != nil { + return err + } + + zoneConfig.Name = zoneName + + rec := []internal.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: info.Value, + TTL: d.config.TTL, + }} + + req := internal.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToAdd: rec, + } + + response, err := d.client.UpdateZone(ctx, req) + if err != nil { + return err + } + + for _, record := range response.Records { + if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { + d.recordIDsMu.Lock() + d.recordIDs[info.EffectiveFQDN] = record.ID + d.recordIDsMu.Unlock() + } + } + + if d.recordIDs[info.EffectiveFQDN] == "" { + return fmt.Errorf("error getting ID of just created record, for domain %s", domain) + } + + 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) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := internal.ZoneConfigsFindRequest{ + Filter: internal.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) + if err != nil { + return err + } + + zoneConfig.Name = zoneName + + rec := []internal.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: `"` + info.Value + `"`, + }} + + req := internal.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToDelete: rec, + } + + // Delete record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, info.EffectiveFQDN) + d.recordIDsMu.Unlock() + + _, err = d.client.UpdateZone(ctx, req) + if err != nil { + return err + } + + return nil +} + +func (d *DNSProvider) getZoneName(fqdn string) (string, error) { + if d.config.ZoneName != "" { + return d.config.ZoneName, nil + } + + zoneName, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) + } + + if zoneName == "" { + return "", errors.New("empty zone name") + } + + return dns01.UnFqdn(zoneName), nil +} diff --git a/providers/dns/internal/hostingde/provider_test.go b/providers/dns/internal/hostingde/provider_test.go new file mode 100644 index 000000000..3cdabf702 --- /dev/null +++ b/providers/dns/internal/hostingde/provider_test.go @@ -0,0 +1,50 @@ +package hostingde + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + zoneName string + expected string + }{ + { + desc: "success", + apiKey: "123", + zoneName: "example.org", + }, + { + desc: "missing credentials", + expected: "API key missing", + }, + { + desc: "missing api key", + zoneName: "456", + expected: "API key missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.APIKey = test.apiKey + config.ZoneName = test.zoneName + + p, err := NewDNSProviderConfig(config, "") + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} diff --git a/providers/dns/internal/ionos/client.go b/providers/dns/internal/ionos/internal/client.go similarity index 95% rename from providers/dns/internal/ionos/client.go rename to providers/dns/internal/ionos/internal/client.go index 8ab6f15b9..2a556a49b 100644 --- a/providers/dns/internal/ionos/client.go +++ b/providers/dns/internal/ionos/internal/client.go @@ -1,4 +1,4 @@ -package ionos +package internal import ( "bytes" @@ -14,10 +14,7 @@ import ( querystring "github.com/google/go-querystring/query" ) -const ( - DefaultIonosBaseURL = "https://api.hosting.ionos.com/dns" - DefaultUnitedDomainsBaseURL = "https://dnsapi.united-domains.de/dns" -) +const defaultBaseURL = "https://api.hosting.ionos.com/dns" // APIKeyHeader API key header. const APIKeyHeader = "X-Api-Key" @@ -32,7 +29,7 @@ type Client struct { // NewClient creates a new Client. func NewClient(apiKey string) (*Client, error) { - baseURL, err := url.Parse(DefaultIonosBaseURL) + baseURL, err := url.Parse(defaultBaseURL) if err != nil { return nil, err } diff --git a/providers/dns/internal/ionos/client_test.go b/providers/dns/internal/ionos/internal/client_test.go similarity index 99% rename from providers/dns/internal/ionos/client_test.go rename to providers/dns/internal/ionos/internal/client_test.go index 81e4ff289..008d153bc 100644 --- a/providers/dns/internal/ionos/client_test.go +++ b/providers/dns/internal/ionos/internal/client_test.go @@ -1,4 +1,4 @@ -package ionos +package internal import ( "net/http" diff --git a/providers/dns/internal/ionos/fixtures/get_records.json b/providers/dns/internal/ionos/internal/fixtures/get_records.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/get_records.json rename to providers/dns/internal/ionos/internal/fixtures/get_records.json diff --git a/providers/dns/internal/ionos/fixtures/get_records_error.json b/providers/dns/internal/ionos/internal/fixtures/get_records_error.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/get_records_error.json rename to providers/dns/internal/ionos/internal/fixtures/get_records_error.json diff --git a/providers/dns/internal/ionos/fixtures/list_zones.json b/providers/dns/internal/ionos/internal/fixtures/list_zones.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/list_zones.json rename to providers/dns/internal/ionos/internal/fixtures/list_zones.json diff --git a/providers/dns/internal/ionos/fixtures/list_zones_error.json b/providers/dns/internal/ionos/internal/fixtures/list_zones_error.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/list_zones_error.json rename to providers/dns/internal/ionos/internal/fixtures/list_zones_error.json diff --git a/providers/dns/internal/ionos/fixtures/remove_record_error.json b/providers/dns/internal/ionos/internal/fixtures/remove_record_error.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/remove_record_error.json rename to providers/dns/internal/ionos/internal/fixtures/remove_record_error.json diff --git a/providers/dns/internal/ionos/fixtures/replace_records_error.json b/providers/dns/internal/ionos/internal/fixtures/replace_records_error.json similarity index 100% rename from providers/dns/internal/ionos/fixtures/replace_records_error.json rename to providers/dns/internal/ionos/internal/fixtures/replace_records_error.json diff --git a/providers/dns/internal/ionos/types.go b/providers/dns/internal/ionos/internal/types.go similarity index 99% rename from providers/dns/internal/ionos/types.go rename to providers/dns/internal/ionos/internal/types.go index 3fc74c054..35bfe0966 100644 --- a/providers/dns/internal/ionos/types.go +++ b/providers/dns/internal/ionos/internal/types.go @@ -1,4 +1,4 @@ -package ionos +package internal import ( "fmt" diff --git a/providers/dns/internal/ionos/provider.go b/providers/dns/internal/ionos/provider.go new file mode 100644 index 000000000..a7d145840 --- /dev/null +++ b/providers/dns/internal/ionos/provider.go @@ -0,0 +1,173 @@ +package ionos + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "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" + ionos "github.com/go-acme/lego/v4/providers/dns/internal/ionos/internal" +) + +const MinTTL = 300 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *ionos.Client +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Ionos. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("credentials missing") + } + + if config.TTL < MinTTL { + return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL) + } + + client, err := ionos.NewClient(config.APIKey) + if err != nil { + return nil, err + } + + if baseURL != "" { + client.BaseURL, _ = url.Parse(baseURL) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + 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 using the specified parameters. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) + if err != nil { + return fmt.Errorf("failed to get zones: %w", err) + } + + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("no matching zone found for domain") + } + + filter := &ionos.RecordsFilter{ + Suffix: name, + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err) + } + + records = append(records, ionos.Record{ + Name: name, + Content: info.Value, + TTL: d.config.TTL, + Type: "TXT", + }) + + err = d.client.ReplaceRecords(ctx, zone.ID, records) + if err != nil { + return fmt.Errorf("failed to create/update records (zone=%s): %w", zone.ID, err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) + if err != nil { + return fmt.Errorf("failed to get zones: %w", err) + } + + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("no matching zone found for domain") + } + + filter := &ionos.RecordsFilter{ + Suffix: name, + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err) + } + + for _, record := range records { + if record.Name == name && record.Content == strconv.Quote(info.Value) { + err = d.client.RemoveRecord(ctx, zone.ID, record.ID) + if err != nil { + return fmt.Errorf("failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) + } + + return nil + } + } + + return fmt.Errorf("failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) +} + +func findZone(zones []ionos.Zone, domain string) *ionos.Zone { + var result *ionos.Zone + + for _, zone := range zones { + if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { + if result == nil || len(zone.Name) > len(result.Name) { + result = &zone + } + } + } + + return result +} diff --git a/providers/dns/internal/ionos/provider_test.go b/providers/dns/internal/ionos/provider_test.go new file mode 100644 index 000000000..6b4df5cc7 --- /dev/null +++ b/providers/dns/internal/ionos/provider_test.go @@ -0,0 +1,52 @@ +package ionos + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + tll int + expected string + }{ + { + desc: "success", + apiKey: "123", + tll: MinTTL, + }, + { + desc: "missing credentials", + tll: MinTTL, + expected: "credentials missing", + }, + { + desc: "invalid TTL", + apiKey: "123", + tll: 30, + expected: "invalid TTL, TTL (30) must be greater than 300", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.APIKey = test.apiKey + config.TTL = test.tll + + 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) + } + }) + } +} diff --git a/providers/dns/internal/rimuhosting/client.go b/providers/dns/internal/rimuhosting/internal/client.go similarity index 94% rename from providers/dns/internal/rimuhosting/client.go rename to providers/dns/internal/rimuhosting/internal/client.go index c46afc544..5bf7393e7 100644 --- a/providers/dns/internal/rimuhosting/client.go +++ b/providers/dns/internal/rimuhosting/internal/client.go @@ -1,4 +1,4 @@ -package rimuhosting +package internal import ( "context" @@ -15,11 +15,7 @@ import ( querystring "github.com/google/go-querystring/query" ) -// Base URL for the RimuHosting DNS services. -const ( - DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" - DefaultRimuHostingBaseURL = "https://rimuhosting.com/dns/dyndns.jsp" -) +const defaultBaseURL = "https://rimuhosting.com/dns/dyndns.jsp" // Action names. const ( @@ -40,7 +36,7 @@ type Client struct { func NewClient(apiKey string) *Client { return &Client{ apiKey: apiKey, - BaseURL: DefaultZonomiBaseURL, + BaseURL: defaultBaseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } diff --git a/providers/dns/internal/rimuhosting/client_test.go b/providers/dns/internal/rimuhosting/internal/client_test.go similarity index 99% rename from providers/dns/internal/rimuhosting/client_test.go rename to providers/dns/internal/rimuhosting/internal/client_test.go index 6ee9ea3f7..00126dfbe 100644 --- a/providers/dns/internal/rimuhosting/client_test.go +++ b/providers/dns/internal/rimuhosting/internal/client_test.go @@ -1,4 +1,4 @@ -package rimuhosting +package internal import ( "encoding/xml" diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record.xml b/providers/dns/internal/rimuhosting/internal/fixtures/add_record.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/add_record.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/add_record.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml b/providers/dns/internal/rimuhosting/internal/fixtures/add_record_error.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/add_record_error.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/add_record_error.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml b/providers/dns/internal/rimuhosting/internal/fixtures/add_record_same_domain.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/add_record_same_domain.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record.xml b/providers/dns/internal/rimuhosting/internal/fixtures/delete_record.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/delete_record.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/delete_record.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml b/providers/dns/internal/rimuhosting/internal/fixtures/delete_record_error.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/delete_record_error.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml b/providers/dns/internal/rimuhosting/internal/fixtures/delete_record_nothing.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/delete_record_nothing.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records.xml b/providers/dns/internal/rimuhosting/internal/fixtures/find_records.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/find_records.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/find_records.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml b/providers/dns/internal/rimuhosting/internal/fixtures/find_records_empty.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/find_records_empty.xml diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml b/providers/dns/internal/rimuhosting/internal/fixtures/find_records_pattern.xml similarity index 100% rename from providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml rename to providers/dns/internal/rimuhosting/internal/fixtures/find_records_pattern.xml diff --git a/providers/dns/internal/rimuhosting/types.go b/providers/dns/internal/rimuhosting/internal/types.go similarity index 98% rename from providers/dns/internal/rimuhosting/types.go rename to providers/dns/internal/rimuhosting/internal/types.go index bdb333032..c3df886a2 100644 --- a/providers/dns/internal/rimuhosting/types.go +++ b/providers/dns/internal/rimuhosting/internal/types.go @@ -1,4 +1,4 @@ -package rimuhosting +package internal import "encoding/xml" diff --git a/providers/dns/internal/rimuhosting/provider.go b/providers/dns/internal/rimuhosting/provider.go new file mode 100644 index 000000000..3be764cbf --- /dev/null +++ b/providers/dns/internal/rimuhosting/provider.go @@ -0,0 +1,107 @@ +// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS. +package rimuhosting + +import ( + "context" + "errors" + "fmt" + "net/http" + "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/rimuhosting/internal" +) + +const DefaultTTL = 3600 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey 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 +} + +// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("incomplete credentials, missing API key") + } + + client := internal.NewClient(config.APIKey) + + if baseURL != "" { + client.BaseURL = baseURL + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + 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 using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("failed to find record(s) for %s: %w", domain, err) + } + + actions := []internal.ActionParameter{ + internal.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), + } + + for _, record := range records { + actions = append(actions, internal.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) + } + + _, err = d.client.DoActions(ctx, actions...) + if err != nil { + return fmt.Errorf("failed to add record(s) for %s: %w", domain, err) + } + + 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) + + action := internal.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) + + _, err := d.client.DoActions(context.Background(), action) + if err != nil { + return fmt.Errorf("failed to delete record for %s: %w", domain, err) + } + + return nil +} diff --git a/providers/dns/internal/rimuhosting/provider_test.go b/providers/dns/internal/rimuhosting/provider_test.go new file mode 100644 index 000000000..d1569af31 --- /dev/null +++ b/providers/dns/internal/rimuhosting/provider_test.go @@ -0,0 +1,46 @@ +package rimuhosting + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + expected string + apiKey string + secretKey string + }{ + { + desc: "success", + apiKey: "api_key", + secretKey: "api_secret", + }, + { + desc: "missing api key", + apiKey: "", + secretKey: "api_secret", + expected: "incomplete credentials, missing API key", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.APIKey = test.apiKey + + 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) + } + }) + } +} diff --git a/providers/dns/internal/selectel/client.go b/providers/dns/internal/selectel/internal/client.go similarity index 93% rename from providers/dns/internal/selectel/client.go rename to providers/dns/internal/selectel/internal/client.go index fe810ebc5..b17df6d83 100644 --- a/providers/dns/internal/selectel/client.go +++ b/providers/dns/internal/selectel/internal/client.go @@ -1,4 +1,4 @@ -package selectel +package internal import ( "bytes" @@ -15,15 +15,11 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -// Base URL for the Selectel/VScale DNS services. -const ( - DefaultSelectelBaseURL = "https://api.selectel.ru/domains/v1" - DefaultVScaleBaseURL = "https://api.vscale.io/v1/domains" -) +const defaultBaseURL = "https://api.selectel.ru/domains/v1" const tokenHeader = "X-Token" -// Client represents DNS client. +// Client represents the DNS client. type Client struct { token string @@ -33,7 +29,7 @@ type Client struct { // NewClient returns a client instance. func NewClient(token string) *Client { - baseURL, _ := url.Parse(DefaultVScaleBaseURL) + baseURL, _ := url.Parse(defaultBaseURL) return &Client{ token: token, diff --git a/providers/dns/internal/selectel/client_test.go b/providers/dns/internal/selectel/internal/client_test.go similarity index 99% rename from providers/dns/internal/selectel/client_test.go rename to providers/dns/internal/selectel/internal/client_test.go index 292f70142..edabe0130 100644 --- a/providers/dns/internal/selectel/client_test.go +++ b/providers/dns/internal/selectel/internal/client_test.go @@ -1,4 +1,4 @@ -package selectel +package internal import ( "net/http" diff --git a/providers/dns/internal/selectel/fixtures/add_record-request.json b/providers/dns/internal/selectel/internal/fixtures/add_record-request.json similarity index 100% rename from providers/dns/internal/selectel/fixtures/add_record-request.json rename to providers/dns/internal/selectel/internal/fixtures/add_record-request.json diff --git a/providers/dns/internal/selectel/fixtures/add_record.json b/providers/dns/internal/selectel/internal/fixtures/add_record.json similarity index 100% rename from providers/dns/internal/selectel/fixtures/add_record.json rename to providers/dns/internal/selectel/internal/fixtures/add_record.json diff --git a/providers/dns/internal/selectel/fixtures/domains.json b/providers/dns/internal/selectel/internal/fixtures/domains.json similarity index 100% rename from providers/dns/internal/selectel/fixtures/domains.json rename to providers/dns/internal/selectel/internal/fixtures/domains.json diff --git a/providers/dns/internal/selectel/fixtures/error.json b/providers/dns/internal/selectel/internal/fixtures/error.json similarity index 100% rename from providers/dns/internal/selectel/fixtures/error.json rename to providers/dns/internal/selectel/internal/fixtures/error.json diff --git a/providers/dns/internal/selectel/fixtures/list_records.json b/providers/dns/internal/selectel/internal/fixtures/list_records.json similarity index 100% rename from providers/dns/internal/selectel/fixtures/list_records.json rename to providers/dns/internal/selectel/internal/fixtures/list_records.json diff --git a/providers/dns/internal/selectel/types.go b/providers/dns/internal/selectel/internal/types.go similarity index 98% rename from providers/dns/internal/selectel/types.go rename to providers/dns/internal/selectel/internal/types.go index df7bb3fa7..e6ca792c0 100644 --- a/providers/dns/internal/selectel/types.go +++ b/providers/dns/internal/selectel/internal/types.go @@ -1,4 +1,4 @@ -package selectel +package internal import "fmt" diff --git a/providers/dns/internal/selectel/provider.go b/providers/dns/internal/selectel/provider.go new file mode 100644 index 000000000..495735736 --- /dev/null +++ b/providers/dns/internal/selectel/provider.go @@ -0,0 +1,137 @@ +// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API. +package selectel + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "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/selectel/internal" +) + +const MinTTL = 60 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client + + // TODO(ldez): remove in v5? + BaseURL string +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProviderConfig return a DNSProvider instance configured for selectel. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("credentials missing") + } + + if config.TTL < MinTTL { + return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL) + } + + client := internal.NewClient(config.Token) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + var err error + + client.BaseURL, err = url.Parse(config.BaseURL) + if err != nil { + return nil, fmt.Errorf("%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 DNS-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("get domain by name: %w", err) + } + + txtRecord := internal.Record{ + Type: "TXT", + TTL: d.config.TTL, + Name: info.EffectiveFQDN, + Content: info.Value, + } + + _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) + if err != nil { + return fmt.Errorf("add record: %w", err) + } + + return nil +} + +// CleanUp removes a TXT record used for DNS-01 challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + recordName := dns01.UnFqdn(info.EffectiveFQDN) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("%w", err) + } + + records, err := d.client.ListRecords(ctx, domainObj.ID) + if err != nil { + return fmt.Errorf("list records: %w", err) + } + + // Delete records with specific FQDN + var lastErr error + + for _, record := range records { + if record.Name == recordName { + err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) + if err != nil { + lastErr = fmt.Errorf("delete record: %w", err) + } + } + } + + return lastErr +} diff --git a/providers/dns/internal/selectel/provider_test.go b/providers/dns/internal/selectel/provider_test.go new file mode 100644 index 000000000..75a032bf4 --- /dev/null +++ b/providers/dns/internal/selectel/provider_test.go @@ -0,0 +1,55 @@ +package selectel + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + token string + ttl int + expected string + }{ + { + desc: "success", + token: "123", + ttl: 60, + }, + { + desc: "missing api key", + token: "", + ttl: 60, + expected: "credentials missing", + }, + { + desc: "bad TTL value", + token: "123", + ttl: 59, + expected: fmt.Sprintf("invalid TTL, TTL (59) must be greater than %d", MinTTL), + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.TTL = test.ttl + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + assert.NotNil(t, p.config) + assert.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} diff --git a/providers/dns/ionos/ionos.go b/providers/dns/ionos/ionos.go index fd35f502e..892370f5d 100644 --- a/providers/dns/ionos/ionos.go +++ b/providers/dns/ionos/ionos.go @@ -2,18 +2,14 @@ package ionos import ( - "context" "errors" "fmt" "net/http" - "strconv" - "strings" "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/internal/ionos" ) @@ -34,18 +30,12 @@ const minTTL = 300 var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = ionos.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + TTL: env.GetOrDefaultInt(EnvTTL, ionos.MinTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -56,8 +46,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *ionos.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Ionos. @@ -80,129 +69,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("ionos: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("ionos: credentials missing") - } - - if config.TTL < minTTL { - return nil, fmt.Errorf("ionos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client, err := ionos.NewClient(config.APIKey) + provider, err := ionos.NewDNSProviderConfig(config, "") if err != nil { return nil, fmt.Errorf("ionos: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil + return &DNSProvider{prv: provider}, 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() } // Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + err := d.prv.Present(domain, token, keyAuth) if err != nil { - return fmt.Errorf("ionos: failed to get zones: %w", err) - } - - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("ionos: no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) - } - - records = append(records, ionos.Record{ - Name: name, - Content: info.Value, - TTL: d.config.TTL, - Type: "TXT", - }) - - err = d.client.ReplaceRecords(ctx, zone.ID, records) - if err != nil { - return fmt.Errorf("ionos: failed to create/update records (zone=%s): %w", zone.ID, err) + return fmt.Errorf("ionos: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("ionos: failed to get zones: %w", err) + return fmt.Errorf("ionos: %w", err) } - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("ionos: no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) - } - - for _, record := range records { - if record.Name == name && record.Content == strconv.Quote(info.Value) { - err = d.client.RemoveRecord(ctx, zone.ID, record.ID) - if err != nil { - return fmt.Errorf("ionos: failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) - } - - return nil - } - } - - return fmt.Errorf("ionos: failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) -} - -func findZone(zones []ionos.Zone, domain string) *ionos.Zone { - var result *ionos.Zone - - for _, zone := range zones { - if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { - if result == nil || len(zone.Name) > len(result.Name) { - result = &zone - } - } - } - - return result + return nil } diff --git a/providers/dns/ionos/ionos_test.go b/providers/dns/ionos/ionos_test.go index 7b1f5af11..39dc0c511 100644 --- a/providers/dns/ionos/ionos_test.go +++ b/providers/dns/ionos/ionos_test.go @@ -9,9 +9,7 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest( - EnvAPIKey). - WithDomain(envDomain) +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -47,8 +45,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) } @@ -92,8 +89,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/rimuhosting/rimuhosting.go b/providers/dns/rimuhosting/rimuhosting.go index 08d7ad413..7a7e99f60 100644 --- a/providers/dns/rimuhosting/rimuhosting.go +++ b/providers/dns/rimuhosting/rimuhosting.go @@ -2,7 +2,6 @@ package rimuhosting import ( - "context" "errors" "fmt" "net/http" @@ -11,7 +10,6 @@ import ( "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/internal/rimuhosting" ) @@ -30,19 +28,12 @@ const ( var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = rimuhosting.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 3600), + TTL: env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -53,8 +44,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *rimuhosting.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for RimuHosting. @@ -77,50 +67,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("rimuhosting: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("rimuhosting: incomplete credentials, missing API key") + provider, err := rimuhosting.NewDNSProviderConfig(config, "") + if err != nil { + return nil, fmt.Errorf("rimuhosting: %w", err) } - client := rimuhosting.NewClient(config.APIKey) - client.BaseURL = rimuhosting.DefaultRimuHostingBaseURL - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - 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 + 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) - - ctx := context.Background() - - records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + err := d.prv.Present(domain, token, keyAuth) if err != nil { - return fmt.Errorf("rimuhosting: failed to find record(s) for %s: %w", domain, err) - } - - actions := []rimuhosting.ActionParameter{ - rimuhosting.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), - } - - for _, record := range records { - actions = append(actions, rimuhosting.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) - } - - _, err = d.client.DoActions(ctx, actions...) - if err != nil { - return fmt.Errorf("rimuhosting: failed to add record(s) for %s: %w", domain, err) + return fmt.Errorf("rimuhosting: %w", err) } return nil @@ -128,14 +87,16 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - action := rimuhosting.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) - - _, err := d.client.DoActions(context.Background(), action) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("rimuhosting: failed to delete record for %s: %w", domain, err) + return fmt.Errorf("rimuhosting: %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/rimuhosting/rimuhosting_test.go b/providers/dns/rimuhosting/rimuhosting_test.go index d8b086e25..878ec14da 100644 --- a/providers/dns/rimuhosting/rimuhosting_test.go +++ b/providers/dns/rimuhosting/rimuhosting_test.go @@ -46,7 +46,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.prv) } else { require.EqualError(t, err, test.expected) } @@ -84,7 +84,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.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/selectel/selectel.go b/providers/dns/selectel/selectel.go index 804ef04d5..63ddd81ac 100644 --- a/providers/dns/selectel/selectel.go +++ b/providers/dns/selectel/selectel.go @@ -4,17 +4,14 @@ package selectel import ( - "context" "errors" "fmt" "net/http" - "net/url" "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/internal/selectel" ) @@ -31,25 +28,16 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 60 - var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - Token string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = selectel.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL), - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + BaseURL: env.GetOrDefaultString(EnvBaseURL, ""), + TTL: env.GetOrDefaultInt(EnvTTL, selectel.MinTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -60,8 +48,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *selectel.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API. @@ -84,58 +71,17 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("selectel: the configuration of the DNS provider is nil") } - if config.Token == "" { - return nil, errors.New("selectel: credentials missing") - } - - if config.TTL < minTTL { - return nil, fmt.Errorf("selectel: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client := selectel.NewClient(config.Token) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - var err error - - client.BaseURL, err = url.Parse(config.BaseURL) + provider, err := selectel.NewDNSProviderConfig(config) if err != nil { return nil, fmt.Errorf("selectel: %w", err) } - return &DNSProvider{config: config, client: client}, nil + return &DNSProvider{prv: provider}, 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 DNS-01 challenge. +// Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) - if err != nil { - return fmt.Errorf("selectel: %w", err) - } - - txtRecord := selectel.Record{ - Type: "TXT", - TTL: d.config.TTL, - Name: info.EffectiveFQDN, - Content: info.Value, - } - - _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("selectel: %w", err) } @@ -143,36 +89,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } -// CleanUp removes a TXT record used for DNS-01 challenge. +// CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - recordName := dns01.UnFqdn(info.EffectiveFQDN) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { return fmt.Errorf("selectel: %w", err) } - records, err := d.client.ListRecords(ctx, domainObj.ID) - if err != nil { - return fmt.Errorf("selectel: %w", err) - } - - // Delete records with specific FQDN - var lastErr error - - for _, record := range records { - if record.Name == recordName { - err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) - if err != nil { - lastErr = fmt.Errorf("selectel: %w", err) - } - } - } - - return lastErr + 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/selectel/selectel_test.go b/providers/dns/selectel/selectel_test.go index e3c36e226..a456f1358 100644 --- a/providers/dns/selectel/selectel_test.go +++ b/providers/dns/selectel/selectel_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/providers/dns/internal/selectel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -46,8 +47,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) + assert.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -77,7 +77,7 @@ func TestNewDNSProviderConfig(t *testing.T) { desc: "bad TTL value", token: "123", ttl: 59, - expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", minTTL), + expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", selectel.MinTTL), }, } @@ -92,8 +92,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) + assert.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/uniteddomains/uniteddomains.go b/providers/dns/uniteddomains/uniteddomains.go index 0cb50c2af..683cab1fe 100644 --- a/providers/dns/uniteddomains/uniteddomains.go +++ b/providers/dns/uniteddomains/uniteddomains.go @@ -2,19 +2,14 @@ package uniteddomains import ( - "context" "errors" "fmt" "net/http" - "net/url" - "strconv" - "strings" "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/internal/ionos" ) @@ -30,18 +25,14 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://dnsapi.united-domains.de/dns" + const minTTL = 300 var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = ionos.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -57,8 +48,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *ionos.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for United-Domains. @@ -80,131 +70,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("uniteddomains: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("uniteddomains: credentials missing") - } - - if config.TTL < minTTL { - return nil, fmt.Errorf("uniteddomains: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client, err := ionos.NewClient(config.APIKey) + provider, err := ionos.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("uniteddomains: %w", err) } - client.BaseURL, _ = url.Parse(ionos.DefaultUnitedDomainsBaseURL) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil + return &DNSProvider{prv: provider}, 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() } // Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + err := d.prv.Present(domain, token, keyAuth) if err != nil { - return fmt.Errorf("uniteddomains: failed to get zones: %w", err) - } - - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("uniteddomains: no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("uniteddomains: failed to get records (zone=%s): %w", zone.ID, err) - } - - records = append(records, ionos.Record{ - Name: name, - Content: info.Value, - TTL: d.config.TTL, - Type: "TXT", - }) - - err = d.client.ReplaceRecords(ctx, zone.ID, records) - if err != nil { - return fmt.Errorf("uniteddomains: failed to create/update records (zone=%s): %w", zone.ID, err) + return fmt.Errorf("uniteddomains: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("uniteddomains: failed to get zones: %w", err) + return fmt.Errorf("uniteddomains: %w", err) } - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("uniteddomains: no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("uniteddomains: failed to get records (zone=%s): %w", zone.ID, err) - } - - for _, record := range records { - if record.Name == name && record.Content == strconv.Quote(info.Value) { - err = d.client.RemoveRecord(ctx, zone.ID, record.ID) - if err != nil { - return fmt.Errorf("uniteddomains: failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) - } - - return nil - } - } - - return fmt.Errorf("uniteddomains: failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) -} - -func findZone(zones []ionos.Zone, domain string) *ionos.Zone { - var result *ionos.Zone - - for _, zone := range zones { - if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { - if result == nil || len(zone.Name) > len(result.Name) { - result = &zone - } - } - } - - return result + return nil } diff --git a/providers/dns/uniteddomains/uniteddomains_test.go b/providers/dns/uniteddomains/uniteddomains_test.go index 841268ca2..93afb01ab 100644 --- a/providers/dns/uniteddomains/uniteddomains_test.go +++ b/providers/dns/uniteddomains/uniteddomains_test.go @@ -9,9 +9,7 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest( - EnvAPIKey). - WithDomain(envDomain) +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -47,8 +45,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) } @@ -92,8 +89,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/vscale/vscale.go b/providers/dns/vscale/vscale.go index 01fae946d..a159db307 100644 --- a/providers/dns/vscale/vscale.go +++ b/providers/dns/vscale/vscale.go @@ -4,17 +4,14 @@ package vscale import ( - "context" "errors" "fmt" "net/http" - "net/url" "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/internal/selectel" ) @@ -31,25 +28,18 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 60 +const defaultBaseURL = "https://api.vscale.io/v1/domains" var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - Token string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = selectel.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultVScaleBaseURL), - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + BaseURL: env.GetOrDefaultString(EnvBaseURL, defaultBaseURL), + TTL: env.GetOrDefaultInt(EnvTTL, selectel.MinTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -60,8 +50,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *selectel.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API. @@ -84,58 +73,21 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("vscale: the configuration of the DNS provider is nil") } - if config.Token == "" { - return nil, errors.New("vscale: credentials missing") + if config.BaseURL == "" { + config.BaseURL = defaultBaseURL } - if config.TTL < minTTL { - return nil, fmt.Errorf("vscale: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client := selectel.NewClient(config.Token) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - var err error - - client.BaseURL, err = url.Parse(config.BaseURL) + provider, err := selectel.NewDNSProviderConfig(config) if err != nil { return nil, fmt.Errorf("vscale: %w", err) } - return &DNSProvider{config: config, client: client}, nil + return &DNSProvider{prv: provider}, 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 DNS-01 challenge. +// Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) - if err != nil { - return fmt.Errorf("vscale: %w", err) - } - - txtRecord := selectel.Record{ - Type: "TXT", - TTL: d.config.TTL, - Name: info.EffectiveFQDN, - Content: info.Value, - } - - _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("vscale: %w", err) } @@ -143,36 +95,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } -// CleanUp removes a TXT record used for DNS-01 challenge. +// CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - recordName := dns01.UnFqdn(info.EffectiveFQDN) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { return fmt.Errorf("vscale: %w", err) } - records, err := d.client.ListRecords(ctx, domainObj.ID) - if err != nil { - return fmt.Errorf("vscale: %w", err) - } - - // Delete records with specific FQDN - var lastErr error - - for _, record := range records { - if record.Name == recordName { - err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) - if err != nil { - lastErr = fmt.Errorf("vscale: %w", err) - } - } - } - - return lastErr + 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/vscale/vscale_test.go b/providers/dns/vscale/vscale_test.go index f3bc15531..9012c7563 100644 --- a/providers/dns/vscale/vscale_test.go +++ b/providers/dns/vscale/vscale_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/providers/dns/internal/selectel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -46,8 +47,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) + assert.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } @@ -77,7 +77,7 @@ func TestNewDNSProviderConfig(t *testing.T) { desc: "bad TTL value", token: "123", ttl: 59, - expected: fmt.Sprintf("vscale: invalid TTL, TTL (59) must be greater than %d", minTTL), + expected: fmt.Sprintf("vscale: invalid TTL, TTL (59) must be greater than %d", selectel.MinTTL), }, } @@ -92,8 +92,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) + assert.NotNil(t, p.prv) } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/websupport/websupport.go b/providers/dns/websupport/websupport.go index 7f93653c9..4187ba32b 100644 --- a/providers/dns/websupport/websupport.go +++ b/providers/dns/websupport/websupport.go @@ -2,17 +2,15 @@ package websupport import ( - "context" "errors" "fmt" "net/http" - "strconv" "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/active24" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) const baseAPIDomain = "websupport.sk" @@ -31,15 +29,7 @@ const ( ) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - Secret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = active24.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -55,8 +45,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *active24.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Websupport. @@ -80,83 +69,29 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("websupport: the configuration of the DNS provider is nil") } - client, err := active24.NewClient(baseAPIDomain, config.APIKey, config.Secret) + provider, err := active24.NewDNSProviderConfig(config, baseAPIDomain) if err != nil { return nil, fmt.Errorf("websupport: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil + return &DNSProvider{prv: provider}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("websupport: 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("websupport: %w", err) } - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("websupport: find service ID: %w", err) - } - - record := active24.Record{ - Type: "TXT", - Name: subDomain, - Content: info.Value, - TTL: d.config.TTL, - } - - err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record) - if err != nil { - return fmt.Errorf("websupport: create record: %w", err) - } - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("websupport: could not find zone for domain %q: %w", domain, err) - } - - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("websupport: find service ID: %w", err) - } - - recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info) - if err != nil { - return fmt.Errorf("websupport: find record ID: %w", err) - } - - err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID)) - if err != nil { - return fmt.Errorf("websupport: delete record %w", err) + return fmt.Errorf("websupport: %w", err) } return nil @@ -165,58 +100,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // 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 -} - -func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) { - services, err := d.client.GetServices(ctx) - if err != nil { - return 0, fmt.Errorf("get services: %w", err) - } - - for _, service := range services { - if service.ServiceName != "domain" { - continue - } - - if service.Name != domain { - continue - } - - return service.ID, nil - } - - return 0, fmt.Errorf("service not found for domain: %s", domain) -} - -func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) { - // NOTE(ldez): Despite the API documentation, the filter doesn't seem to work. - filter := active24.RecordFilter{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: []string{"TXT"}, - Content: info.Value, - } - - records, err := d.client.GetRecords(ctx, serviceID, filter) - if err != nil { - return 0, fmt.Errorf("get records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" { - continue - } - - if record.Name != dns01.UnFqdn(info.EffectiveFQDN) { - continue - } - - if record.Content != info.Value { - continue - } - - return record.ID, nil - } - - return 0, errors.New("no record found") + return d.prv.Timeout() } diff --git a/providers/dns/websupport/websupport_test.go b/providers/dns/websupport/websupport_test.go index c7b8572b5..196c9bab8 100644 --- a/providers/dns/websupport/websupport_test.go +++ b/providers/dns/websupport/websupport_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) } @@ -110,8 +109,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/zonomi/zonomi.go b/providers/dns/zonomi/zonomi.go index e6eae08de..fe54b80fc 100644 --- a/providers/dns/zonomi/zonomi.go +++ b/providers/dns/zonomi/zonomi.go @@ -2,7 +2,6 @@ package zonomi import ( - "context" "errors" "fmt" "net/http" @@ -11,7 +10,6 @@ import ( "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/internal/rimuhosting" ) @@ -27,22 +25,17 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = rimuhosting.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 3600), + TTL: env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -53,8 +46,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *rimuhosting.Client + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Zonomi. @@ -77,50 +69,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("zonomi: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("zonomi: incomplete credentials, missing API key") + provider, err := rimuhosting.NewDNSProviderConfig(config, defaultBaseURL) + if err != nil { + return nil, fmt.Errorf("zonomi: %w", err) } - client := rimuhosting.NewClient(config.APIKey) - client.BaseURL = rimuhosting.DefaultZonomiBaseURL - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - 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 + 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) - - ctx := context.Background() - - records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + err := d.prv.Present(domain, token, keyAuth) if err != nil { - return fmt.Errorf("zonomi: failed to find record(s) for %s: %w", domain, err) - } - - actions := []rimuhosting.ActionParameter{ - rimuhosting.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), - } - - for _, record := range records { - actions = append(actions, rimuhosting.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) - } - - _, err = d.client.DoActions(ctx, actions...) - if err != nil { - return fmt.Errorf("zonomi: failed to add record(s) for %s: %w", domain, err) + return fmt.Errorf("zonomi: %w", err) } return nil @@ -128,14 +89,16 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - action := rimuhosting.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) - - _, err := d.client.DoActions(context.Background(), action) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("zonomi: failed to delete record for %s: %w", domain, err) + return fmt.Errorf("zonomi: %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/zonomi/zonomi_test.go b/providers/dns/zonomi/zonomi_test.go index 0583f4a1c..2e13e937e 100644 --- a/providers/dns/zonomi/zonomi_test.go +++ b/providers/dns/zonomi/zonomi_test.go @@ -46,7 +46,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.prv) } else { require.EqualError(t, err, test.expected) } @@ -84,7 +84,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.prv) } else { require.EqualError(t, err, test.expected) } From 3f2ebf7ef1bba374ca8ae225ac3ed82deb6d0fd2 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 29 Nov 2025 14:24:38 +0100 Subject: [PATCH 20/95] chore: improve issue templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/ISSUE_TEMPLATE/new_dns_provider.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ecd9cb6a5..ea3fd9a3a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -45,6 +45,7 @@ body: - Through Bitnami - Through 1Panel - Through Zoraxy + - Through Certimate - go install - Other validations: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 33f7be155..7f6793167 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -24,6 +24,7 @@ body: - Through Bitnami - Through 1Panel - Through Zoraxy + - Through Certimate - go install - Other validations: diff --git a/.github/ISSUE_TEMPLATE/new_dns_provider.yml b/.github/ISSUE_TEMPLATE/new_dns_provider.yml index 9e9fe3c03..902951ed8 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -34,6 +34,7 @@ body: - Through Bitnami - Through 1Panel - Through Zoraxy + - Through Certimate - go install - Other validations: From fc5e0174b8baa7453a266132a9efded3a0ae7ab7 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 29 Nov 2025 14:25:21 +0100 Subject: [PATCH 21/95] docs: update the number of supported DNS --- docs/content/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index 229435e7d..ba90ddc97 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -14,7 +14,7 @@ Let's Encrypt client and ACME library written in Go. - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [150 DNS providers]({{% ref "dns" %}}) +- Comes with about [170 DNS providers]({{% ref "dns" %}}) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates From 5488fdf856e4ef8070459ba95c8b5285c351fa7c Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 29 Nov 2025 14:29:35 +0100 Subject: [PATCH 22/95] Prepare release v4.29.0 --- CHANGELOG.md | 24 +++++++++++++++++++ acme/api/internal/sender/useragent.go | 4 ++-- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 4 ++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f8ff569..d3b7909e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.29.0 + +- Release date: 2025-11-29 +- Tag: [v4.29.0](https://github.com/go-acme/lego/releases/tag/v4.29.0) + +### Added + +- **[dnsprovider]** Add DNS provider for United-Domains +- **[dnsprovider]** Add DNS provider for Gigahost.no +- **[dnsprovider]** Add DNS provider for EdgeCenter +- **[dnsprovider]** Add DNS provider for AlibabaCloud ESA +- **[dnsprovider]** edgeone: add zones mapping +- **[dnsprovider]** namecheap: add experimental proxy support + +### Changed + +- **[dnsprovider]** gandiv5: update base API URL + +### Fixed + +- **[dnsprovider]** hetzner: use int64 for IDs +- **[dnsprovider]** baiducloud: pagination and TTL +- **[dnsprovider]** inwx: fix API breaking changes with record IDs + ## v4.28.1 - Release date: 2025-11-06 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index bcfbebb2a..eb5a3bb5e 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.28.1" + ourUserAgent = "xenolf-acme/4.29.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index b564906c1..3b0201927 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.28.1+dev-detach" +const defaultVersion = "v4.29.0+dev-release" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 1e176ab9a..bcab98bfd 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.28.1" + ourUserAgent = "goacme-lego/4.29.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From 742741fe05743d86d02cc9856f8950d7d9c42061 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 29 Nov 2025 14:30:15 +0100 Subject: [PATCH 23/95] Detach v4.29.0 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index eb5a3bb5e..1c2078b38 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index 3b0201927..e0fbb90e2 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.29.0+dev-release" +const defaultVersion = "v4.29.0+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index bcab98bfd..bbffcdbd4 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From cc83c025b547f7aba231316cbe0f711c669dfb70 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 1 Dec 2025 20:50:46 +0100 Subject: [PATCH 24/95] autodns: use the right response structure (#2737) --- providers/dns/autodns/autodns.go | 5 +- providers/dns/autodns/internal/client.go | 35 +++-- providers/dns/autodns/internal/client_test.go | 140 ++++++++++++++--- .../internal/fixtures/add_record-request.json | 8 +- .../autodns/internal/fixtures/add_record.json | 45 ++++-- .../dns/autodns/internal/fixtures/error.json | 21 +++ .../fixtures/remove_record-request.json | 8 +- .../internal/fixtures/remove_record.json | 45 ++++-- providers/dns/autodns/internal/types.go | 142 +++++++++++++++--- 9 files changed, 370 insertions(+), 79 deletions(-) create mode 100644 providers/dns/autodns/internal/fixtures/error.json diff --git a/providers/dns/autodns/autodns.go b/providers/dns/autodns/autodns.go index 770bac99b..fc8e793b6 100644 --- a/providers/dns/autodns/autodns.go +++ b/providers/dns/autodns/autodns.go @@ -128,7 +128,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Value: info.Value, }} - _, err := d.client.AddTxtRecords(context.Background(), info.EffectiveFQDN, records) + _, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { return fmt.Errorf("autodns: %w", err) } @@ -147,7 +147,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { Value: info.Value, }} - if err := d.client.RemoveTXTRecords(context.Background(), info.EffectiveFQDN, records); err != nil { + _, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records) + if err != nil { return fmt.Errorf("autodns: %w", err) } diff --git a/providers/dns/autodns/internal/client.go b/providers/dns/autodns/internal/client.go index 547596f81..d92490a60 100644 --- a/providers/dns/autodns/internal/client.go +++ b/providers/dns/autodns/internal/client.go @@ -43,24 +43,22 @@ func NewClient(username, password string, clientContext int) *Client { } } -// AddTxtRecords adds TXT records. -func (c *Client) AddTxtRecords(ctx context.Context, domain string, records []*ResourceRecord) (*Zone, error) { +// AddRecords adds records. +func (c *Client) AddRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) { zoneStream := &ZoneStream{Adds: records} return c.updateZone(ctx, domain, zoneStream) } -// RemoveTXTRecords removes TXT records. -func (c *Client) RemoveTXTRecords(ctx context.Context, domain string, records []*ResourceRecord) error { +// RemoveRecords removes records. +func (c *Client) RemoveRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) { zoneStream := &ZoneStream{Removes: records} - _, err := c.updateZone(ctx, domain, zoneStream) - - return err + return c.updateZone(ctx, domain, zoneStream) } // https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L21090 -func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*Zone, error) { +func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*DataZoneResponse, error) { endpoint := c.BaseURL.JoinPath("zone", domain, "_stream") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zoneStream) @@ -68,12 +66,12 @@ func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *Zone return nil, err } - var zone *Zone - if err := c.do(req, &zone); err != nil { + var resp *DataZoneResponse + if err := c.do(req, &resp); err != nil { return nil, err } - return zone, nil + return resp, nil } func (c *Client) do(req *http.Request, result any) error { @@ -88,7 +86,7 @@ func (c *Client) do(req *http.Request, result any) error { defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) + return parseError(req, resp) } if result == nil { @@ -131,3 +129,16 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo return req, nil } + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/autodns/internal/client_test.go b/providers/dns/autodns/internal/client_test.go index 6fc31ca34..9b0968fdc 100644 --- a/providers/dns/autodns/internal/client_test.go +++ b/providers/dns/autodns/internal/client_test.go @@ -1,6 +1,7 @@ package internal import ( + "net/http" "net/http/httptest" "net/url" "testing" @@ -24,7 +25,7 @@ func mockBuilder() *servermock.Builder[*Client] { WithJSONHeaders()) } -func TestClient_AddTxtRecords(t *testing.T) { +func TestClient_AddRecords(t *testing.T) { client := mockBuilder(). Route("POST /zone/example.com/_stream", servermock.ResponseFromFixture("add_record.json"), @@ -33,28 +34,81 @@ func TestClient_AddTxtRecords(t *testing.T) { With("X-Domainrobot-Context", "123")). Build(t) - records := []*ResourceRecord{{}} + records := []*ResourceRecord{{ + Name: "example.com", + TTL: 600, + Type: "TXT", + Value: "txtTXTtxt", + }} - zone, err := client.AddTxtRecords(t.Context(), "example.com", records) + resp, err := client.AddRecords(t.Context(), "example.com", records) require.NoError(t, err) - expected := &Zone{ - Name: "example.com", - ResourceRecords: []*ResourceRecord{{ - Name: "example.com", - TTL: 120, - Type: "TXT", - Value: "txt", - Pref: 1, - }}, - Action: "xxx", - VirtualNameServer: "yyy", + expected := &DataZoneResponse{ + STID: "20251121-appf4923-126284", + CTID: "", + Messages: []ResponseMessage{ + { + Text: "string", + Messages: []string{ + "string", + }, + Objects: []GenericObject{ + { + Type: "string", + Value: "string", + }, + }, + Code: "string", + Status: "SUCCESS", + }, + }, + Status: &ResponseStatus{ + Code: "S0301", + Text: "Zone was updated successfully on the name server.", + Type: "SUCCESS", + }, + Object: nil, + Data: []Zone{ + { + Name: "example.com", + ResourceRecords: []ResourceRecord{ + { + Name: "example.com", + TTL: 120, + Type: "TXT", + Value: "txt", + Pref: 1, + }, + }, + Action: "xxx", + VirtualNameServer: "yyy", + }, + }, } - assert.Equal(t, expected, zone) + assert.Equal(t, expected, resp) } -func TestClient_RemoveTXTRecords(t *testing.T) { +func TestClient_AddRecords_error(t *testing.T) { + client := mockBuilder(). + Route("POST /zone/example.com/_stream", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusBadRequest)). + Build(t) + + records := []*ResourceRecord{{ + Name: "example.com", + TTL: 600, + Type: "TXT", + Value: "txtTXTtxt", + }} + + _, err := client.AddRecords(t.Context(), "example.com", records) + require.EqualError(t, err, `STID: 20251121-appf4923-126284, status: code: E0202002, text: Zone konnte auf dem Nameserver nicht aktualisiert werden., type: ERROR, message: code: EF02022, text: Der Zusatzeintrag wurde doppelt eingetragen., status: ERROR, object: OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]: _acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT "rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc"`) +} + +func TestClient_RemoveRecords(t *testing.T) { client := mockBuilder(). Route("POST /zone/example.com/_stream", servermock.ResponseFromFixture("remove_record.json"), @@ -63,8 +117,58 @@ func TestClient_RemoveTXTRecords(t *testing.T) { With("X-Domainrobot-Context", "123")). Build(t) - records := []*ResourceRecord{{}} + records := []*ResourceRecord{{ + Name: "example.com", + TTL: 600, + Type: "TXT", + Value: "txtTXTtxt", + }} - err := client.RemoveTXTRecords(t.Context(), "example.com", records) + resp, err := client.RemoveRecords(t.Context(), "example.com", records) require.NoError(t, err) + + expected := &DataZoneResponse{ + STID: "20251121-appf4923-126284", + CTID: "", + Messages: []ResponseMessage{ + { + Text: "string", + Messages: []string{ + "string", + }, + Objects: []GenericObject{ + { + Type: "string", + Value: "string", + }, + }, + Code: "string", + Status: "SUCCESS", + }, + }, + Status: &ResponseStatus{ + Code: "S0301", + Text: "Zone was updated successfully on the name server.", + Type: "SUCCESS", + }, + Object: nil, + Data: []Zone{ + { + Name: "example.com", + ResourceRecords: []ResourceRecord{ + { + Name: "example.com", + TTL: 120, + Type: "TXT", + Value: "txt", + Pref: 1, + }, + }, + Action: "xxx", + VirtualNameServer: "yyy", + }, + }, + } + + assert.Equal(t, expected, resp) } diff --git a/providers/dns/autodns/internal/fixtures/add_record-request.json b/providers/dns/autodns/internal/fixtures/add_record-request.json index b798b4fbd..6105c77ac 100644 --- a/providers/dns/autodns/internal/fixtures/add_record-request.json +++ b/providers/dns/autodns/internal/fixtures/add_record-request.json @@ -1,10 +1,10 @@ { "adds": [ { - "name": "", - "ttl": 0, - "type": "", - "value": "" + "name": "example.com", + "ttl": 600, + "type": "TXT", + "value": "txtTXTtxt" } ], "rems": null diff --git a/providers/dns/autodns/internal/fixtures/add_record.json b/providers/dns/autodns/internal/fixtures/add_record.json index 4a95f0784..a0ce66ba6 100644 --- a/providers/dns/autodns/internal/fixtures/add_record.json +++ b/providers/dns/autodns/internal/fixtures/add_record.json @@ -1,14 +1,41 @@ { - "origin": "example.com", - "resourceRecords": [ + "stid": "20251121-appf4923-126284", + "messages": [ { - "name": "example.com", - "ttl": 120, - "type": "TXT", - "value": "txt", - "pref": 1 + "text": "string", + "notice": "string", + "messages": [ + "string" + ], + "objects": [ + { + "type": "string", + "value": "string" + } + ], + "code": "string", + "status": "SUCCESS" } ], - "action": "xxx", - "virtualNameServer": "yyy" + "status": { + "code": "S0301", + "text": "Zone was updated successfully on the name server.", + "type": "SUCCESS" + }, + "data": [ + { + "origin": "example.com", + "resourceRecords": [ + { + "name": "example.com", + "ttl": 120, + "type": "TXT", + "value": "txt", + "pref": 1 + } + ], + "action": "xxx", + "virtualNameServer": "yyy" + } + ] } diff --git a/providers/dns/autodns/internal/fixtures/error.json b/providers/dns/autodns/internal/fixtures/error.json new file mode 100644 index 000000000..2ed635d58 --- /dev/null +++ b/providers/dns/autodns/internal/fixtures/error.json @@ -0,0 +1,21 @@ +{ + "stid": "20251121-appf4923-126284", + "messages": [ + { + "text": "Der Zusatzeintrag wurde doppelt eingetragen.", + "objects": [ + { + "type": "OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]", + "value": "_acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT \"rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc\"" + } + ], + "code": "EF02022", + "status": "ERROR" + } + ], + "status": { + "code": "E0202002", + "text": "Zone konnte auf dem Nameserver nicht aktualisiert werden.", + "type": "ERROR" + } +} diff --git a/providers/dns/autodns/internal/fixtures/remove_record-request.json b/providers/dns/autodns/internal/fixtures/remove_record-request.json index 0702c7367..92361403e 100644 --- a/providers/dns/autodns/internal/fixtures/remove_record-request.json +++ b/providers/dns/autodns/internal/fixtures/remove_record-request.json @@ -2,10 +2,10 @@ "adds": null, "rems": [ { - "name": "", - "ttl": 0, - "type": "", - "value": "" + "name": "example.com", + "ttl": 600, + "type": "TXT", + "value": "txtTXTtxt" } ] } diff --git a/providers/dns/autodns/internal/fixtures/remove_record.json b/providers/dns/autodns/internal/fixtures/remove_record.json index 4a95f0784..a0ce66ba6 100644 --- a/providers/dns/autodns/internal/fixtures/remove_record.json +++ b/providers/dns/autodns/internal/fixtures/remove_record.json @@ -1,14 +1,41 @@ { - "origin": "example.com", - "resourceRecords": [ + "stid": "20251121-appf4923-126284", + "messages": [ { - "name": "example.com", - "ttl": 120, - "type": "TXT", - "value": "txt", - "pref": 1 + "text": "string", + "notice": "string", + "messages": [ + "string" + ], + "objects": [ + { + "type": "string", + "value": "string" + } + ], + "code": "string", + "status": "SUCCESS" } ], - "action": "xxx", - "virtualNameServer": "yyy" + "status": { + "code": "S0301", + "text": "Zone was updated successfully on the name server.", + "type": "SUCCESS" + }, + "data": [ + { + "origin": "example.com", + "resourceRecords": [ + { + "name": "example.com", + "ttl": 120, + "type": "TXT", + "value": "txt", + "pref": 1 + } + ], + "action": "xxx", + "virtualNameServer": "yyy" + } + ] } diff --git a/providers/dns/autodns/internal/types.go b/providers/dns/autodns/internal/types.go index 93fd678ca..8a06f4889 100644 --- a/providers/dns/autodns/internal/types.go +++ b/providers/dns/autodns/internal/types.go @@ -1,33 +1,133 @@ package internal +import ( + "fmt" + "strings" +) + +type APIResponse[T any] struct { + STID string `json:"stid"` + CTID string `json:"ctid"` + Messages []ResponseMessage `json:"messages"` + Status *ResponseStatus `json:"status"` + Object *ResponseObject `json:"object"` + Data T `json:"data"` +} + +type APIError APIResponse[any] + +func (a *APIError) Error() string { + var parts []string + + if a.STID != "" { + parts = append(parts, fmt.Sprintf("STID: %s", a.STID)) + } + + if a.CTID != "" { + parts = append(parts, fmt.Sprintf("CTID: %s", a.CTID)) + } + + if a.Status != nil { + parts = append(parts, "status: "+a.Status.String()) + } + + for _, message := range a.Messages { + parts = append(parts, "message: "+message.String()) + } + + if a.Object != nil { + parts = append(parts, "object: "+a.Object.String()) + } + + return strings.Join(parts, ", ") +} + +type DataZoneResponse APIResponse[[]Zone] + type ResponseMessage struct { - Text string `json:"text"` - Messages []string `json:"messages"` - Objects []string `json:"objects"` - Code string `json:"code"` - Status string `json:"status"` + Text string `json:"text"` + Code string `json:"code"` + Status string `json:"status"` + Messages []string `json:"messages"` + Objects []GenericObject `json:"objects"` +} + +func (r ResponseMessage) String() string { + var parts []string + + if r.Code != "" { + parts = append(parts, "code: "+r.Code) + } + + if r.Text != "" { + parts = append(parts, "text: "+r.Text) + } + + if r.Status != "" { + parts = append(parts, "status: "+r.Status) + } + + if len(r.Messages) > 0 { + parts = append(parts, "messages: "+strings.Join(r.Messages, ";")) + } + + for _, object := range r.Objects { + parts = append(parts, fmt.Sprintf("object: %s", object)) + } + + return strings.Join(parts, ", ") +} + +type GenericObject struct { + Type string `json:"type"` + Value string `json:"value"` +} + +func (g GenericObject) String() string { + return g.Type + ": " + g.Value } type ResponseStatus struct { Code string `json:"code"` Text string `json:"text"` - Type string `json:"type"` + Type string `json:"type"` // SUCCESS, ERROR, NOTIFY, NOTICE, NICCOM_NOTIFY +} + +func (r ResponseStatus) String() string { + return fmt.Sprintf("code: %s, text: %s, type: %s", r.Code, r.Text, r.Type) } type ResponseObject struct { - Type string `json:"type"` - Value string `json:"value"` - Summary int32 `json:"summary"` - Data string + Type string `json:"type"` + Value string `json:"value"` + Summary int32 `json:"summary"` + Data *ResponseObjectData `json:"data"` } -type DataZoneResponse struct { - STID string `json:"stid"` - CTID string `json:"ctid"` - Messages []*ResponseMessage `json:"messages"` - Status *ResponseStatus `json:"status"` - Object any `json:"object"` - Data []*Zone `json:"data"` +func (r ResponseObject) String() string { + var parts []string + + if r.Type != "" { + parts = append(parts, fmt.Sprintf("type: %s", r.Type)) + } + + if r.Value != "" { + parts = append(parts, fmt.Sprintf("value: %s", r.Value)) + } + + if r.Summary != 0 { + parts = append(parts, fmt.Sprintf("summary: %d", r.Summary)) + } + + if r.Data != nil { + parts = append(parts, fmt.Sprintf("data: %s", r.Data.Description)) + } + + return strings.Join(parts, ", ") +} + +type ResponseObjectData struct { + Description string `json:"description"` } // ResourceRecord holds a resource record. @@ -43,10 +143,10 @@ type ResourceRecord struct { // Zone is an autodns zone record with all for us relevant fields. // https://help.internetx.com/display/APIXMLEN/Zone+Object type Zone struct { - Name string `json:"origin"` - ResourceRecords []*ResourceRecord `json:"resourceRecords"` - Action string `json:"action"` - VirtualNameServer string `json:"virtualNameServer"` + Name string `json:"origin"` + ResourceRecords []ResourceRecord `json:"resourceRecords"` + Action string `json:"action"` + VirtualNameServer string `json:"virtualNameServer"` } // ZoneStream body of the requests. From ea97ce2f62d340f9261cbd003f742fdf5fcc4470 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 1 Dec 2025 20:51:43 +0100 Subject: [PATCH 25/95] chore: move provider "manual" into a dedicated package (#2739) --- .golangci.yml | 4 + challenge/dns01/dns_challenge_manual.go | 5 + cmd/zz_gen_cmd_dnshelp.go | 14 ++- docs/content/dns/zz_gen_manual.md | 98 +++++++++++++++++++ internal/dns/docs/generator.go | 11 +-- internal/dns/docs/templates/dns.go.tmpl | 3 - internal/dns/providers/dns_providers.go.tmpl | 3 - providers/dns/manual/manual.go | 13 +++ .../dns/manual/manual.toml | 23 ++--- .../dns/manual/manual_test.go | 12 +-- providers/dns/zz_gen_dns_providers.go | 6 +- 11 files changed, 149 insertions(+), 43 deletions(-) create mode 100644 docs/content/dns/zz_gen_manual.md create mode 100644 providers/dns/manual/manual.go rename docs/content/dns/manual.md => providers/dns/manual/manual.toml (91%) rename challenge/dns01/dns_challenge_manual_test.go => providers/dns/manual/manual_test.go (73%) diff --git a/.golangci.yml b/.golangci.yml index 2b4bcc41b..ceb7cad85 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -263,6 +263,10 @@ linters: text: cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high linters: - gocyclo + - path: providers/dns/manual/manual.go + text: 'SA1019: dns01.DNSProviderManual is deprecated' + linters: + - staticcheck # Those elements have been replaced by non-exposed structures. - path: providers/dns/linode/linode_test.go text: 'SA1019: linodego\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated' diff --git a/challenge/dns01/dns_challenge_manual.go b/challenge/dns01/dns_challenge_manual.go index c00d64041..3821fc157 100644 --- a/challenge/dns01/dns_challenge_manual.go +++ b/challenge/dns01/dns_challenge_manual.go @@ -12,9 +12,14 @@ const ( ) // DNSProviderManual is an implementation of the ChallengeProvider interface. +// TODO(ldez): move this to providers/dns/manual +// +// Deprecated: Use the manual.DNSProvider instead. type DNSProviderManual struct{} // NewDNSProviderManual returns a DNSProviderManual instance. +// +// Deprecated: Use the manual.NewDNSProvider instead. func NewDNSProviderManual() (*DNSProviderManual, error) { return &DNSProviderManual{}, nil } diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 11cf01280..022374d7a 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -12,7 +12,6 @@ import ( func allDNSCodes() string { providers := []string{ - "manual", "acme-dns", "active24", "alidns", @@ -110,6 +109,7 @@ func allDNSCodes() string { "luadns", "mailinabox", "manageengine", + "manual", "metaname", "metaregistrar", "mijnhost", @@ -2270,6 +2270,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/manageengine`) + case "manual": + // generated from: providers/dns/manual/manual.toml + ew.writeln(`Configuration for Manual.`) + ew.writeln(`Code: 'manual'`) + ew.writeln(`Since: 'v0.3.0'`) + ew.writeln() + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/manual`) + case "metaname": // generated from: providers/dns/metaname/metaname.toml ew.writeln(`Configuration for Metaname.`) @@ -3828,8 +3838,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zonomi`) - case "manual": - ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } diff --git a/docs/content/dns/zz_gen_manual.md b/docs/content/dns/zz_gen_manual.md new file mode 100644 index 000000000..0300d8400 --- /dev/null +++ b/docs/content/dns/zz_gen_manual.md @@ -0,0 +1,98 @@ +--- +title: "Manual" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: manual +dnsprovider: + since: "v0.3.0" + code: "manual" + url: "" +--- + + + + + +Solving the DNS-01 challenge using CLI prompt. + + + + +- Code: `manual` +- Since: v0.3.0 + + +Here is an example bash command using the Manual provider: + +```bash +lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +``` + + + + +## Example + +To start using the CLI prompt "provider", start lego with `--dns manual`: + +```console +$ lego --email "you@example.com" --domains="example.com" --dns "manual" run +``` + +What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: + +```txt +No key found for account you@example.com. Generating a P256 key. +Saved key to ./.lego/accounts/acme-v02.api.letsencrypt.org/you@example.com/keys/you@example.com.key +Please review the TOS at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf +Do you accept the TOS? Y/n +``` + +If you accept the linked Terms of Service, hit `Enter`. + +```txt +[INFO] acme: Registering account for you@example.com +!!!! HEADS UP !!!! + + Your account credentials have been saved in your Let's Encrypt + configuration directory at "./.lego/accounts". + + You should make a secure backup of this folder now. This + configuration directory will also contain certificates and + private keys obtained from Let's Encrypt so making regular + backups of this folder is ideal. +[INFO] [example.com] acme: Obtaining bundled SAN certificate +[INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 +[INFO] [example.com] acme: Could not find solver for: tls-alpn-01 +[INFO] [example.com] acme: Could not find solver for: http-01 +[INFO] [example.com] acme: use dns-01 solver +[INFO] [example.com] acme: Preparing to solve DNS-01 +lego: Please create the following TXT record in your example.com. zone: +_acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" +lego: Press 'Enter' when you are done +``` + +Do as instructed, and create the TXT records, and hit `Enter`. + +```txt +[INFO] [example.com] acme: Trying to solve DNS-01 +[INFO] [example.com] acme: Checking DNS record propagation using [192.168.8.1:53] +[INFO] Wait for propagation [timeout: 1m0s, interval: 2s] +[INFO] [example.com] acme: Waiting for DNS record propagation. +[INFO] [example.com] The server validated our request +[INFO] [example.com] acme: Cleaning DNS-01 challenge +lego: You can now remove this TXT record from your example.com. zone: +_acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" +[INFO] [example.com] acme: Validations succeeded; requesting certificates +[INFO] [example.com] Server responded with a certificate. +``` + +As mentioned, you can now remove the TXT record again. + + + + + + + + diff --git a/internal/dns/docs/generator.go b/internal/dns/docs/generator.go index c7f9ef8c7..9355d0d1b 100644 --- a/internal/dns/docs/generator.go +++ b/internal/dns/docs/generator.go @@ -190,14 +190,9 @@ func generateReadMe(models *descriptors.Providers) error { } func orderProviders(models *descriptors.Providers) [][]descriptors.Provider { - providers := append(models.Providers, descriptors.Provider{ - Name: "Manual", - Code: "manual", - }) - const nbCol = 4 - slices.SortFunc(providers, func(a, b descriptors.Provider) int { + slices.SortFunc(models.Providers, func(a, b descriptors.Provider) int { return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) @@ -206,13 +201,13 @@ func orderProviders(models *descriptors.Providers) [][]descriptors.Provider { row []descriptors.Provider ) - for i, p := range providers { + for i, p := range models.Providers { switch { case len(row) == nbCol: matrix = append(matrix, row) row = []descriptors.Provider{p} - case i == len(providers)-1: + case i == len(models.Providers)-1: row = append(row, p) for j := len(row); j < nbCol; j++ { row = append(row, descriptors.Provider{}) diff --git a/internal/dns/docs/templates/dns.go.tmpl b/internal/dns/docs/templates/dns.go.tmpl index e8b336254..c1896c91a 100644 --- a/internal/dns/docs/templates/dns.go.tmpl +++ b/internal/dns/docs/templates/dns.go.tmpl @@ -12,7 +12,6 @@ import ( func allDNSCodes() string { providers := []string{ - "manual", {{- range $provider := .Providers }} "{{ $provider.Code }}", {{- end}} @@ -48,8 +47,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/{{ $provider.Code }}`) {{end}} - case "manual": - ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } diff --git a/internal/dns/providers/dns_providers.go.tmpl b/internal/dns/providers/dns_providers.go.tmpl index 2030a3ed0..c974ef6a9 100644 --- a/internal/dns/providers/dns_providers.go.tmpl +++ b/internal/dns/providers/dns_providers.go.tmpl @@ -6,7 +6,6 @@ import ( "fmt" "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" {{- range $provider := .Providers }} "github.com/go-acme/lego/v4/providers/dns/{{ cleanName $provider.Code }}" {{- end}} @@ -15,8 +14,6 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { - case "manual": - return dns01.NewDNSProviderManual() {{- range $provider := .Providers }} case "{{ $provider.Code }}"{{range $alias := $provider.Aliases }},"{{ $alias }}"{{end}}: return {{ cleanName $provider.Code }}.NewDNSProvider() diff --git a/providers/dns/manual/manual.go b/providers/dns/manual/manual.go new file mode 100644 index 000000000..2985bc595 --- /dev/null +++ b/providers/dns/manual/manual.go @@ -0,0 +1,13 @@ +package manual + +import ( + "github.com/go-acme/lego/v4/challenge/dns01" +) + +// DNSProvider is an implementation of the ChallengeProvider interface. +type DNSProvider = dns01.DNSProviderManual + +// NewDNSProvider returns a DNSProvider instance. +func NewDNSProvider() (*DNSProvider, error) { + return &DNSProvider{}, nil +} diff --git a/docs/content/dns/manual.md b/providers/dns/manual/manual.toml similarity index 91% rename from docs/content/dns/manual.md rename to providers/dns/manual/manual.toml index 3f9cf0a8e..88acf4750 100644 --- a/docs/content/dns/manual.md +++ b/providers/dns/manual/manual.toml @@ -1,18 +1,13 @@ ---- -title: "Manual" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: manual -dnsprovider: - since: v0.3.0 - code: manual - url: ---- +Name = "Manual" +Description = '''Solving the DNS-01 challenge using CLI prompt.''' +Code = "manual" +Since = "v0.3.0" -Solving the DNS-01 challenge using CLI prompt. - - +Example = ''' +lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +''' +Additional = ''' ## Example To start using the CLI prompt "provider", start lego with `--dns manual`: @@ -70,3 +65,5 @@ _acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5 ``` As mentioned, you can now remove the TXT record again. + +''' diff --git a/challenge/dns01/dns_challenge_manual_test.go b/providers/dns/manual/manual_test.go similarity index 73% rename from challenge/dns01/dns_challenge_manual_test.go rename to providers/dns/manual/manual_test.go index c183822bb..7badd4b8b 100644 --- a/challenge/dns01/dns_challenge_manual_test.go +++ b/providers/dns/manual/manual_test.go @@ -1,22 +1,14 @@ -package dns01 +package manual import ( "io" "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/dnsmock" - "github.com/miekg/dns" "github.com/stretchr/testify/require" ) func TestDNSProviderManual(t *testing.T) { - useAsNameserver(t, dnsmock.NewServer(). - Query("_acme-challenge.example.com. CNAME", dnsmock.Noop). - Query("_acme-challenge.example.com. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("example.com. SOA", dnsmock.SOA("")). - Build(t)) - backupStdin := os.Stdin defer func() { os.Stdin = backupStdin }() @@ -52,7 +44,7 @@ func TestDNSProviderManual(t *testing.T) { os.Stdin = file - manualProvider, err := NewDNSProviderManual() + manualProvider, err := NewDNSProvider() require.NoError(t, err) err = manualProvider.Present("example.com", "", "") diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 2add1f75f..842d00c51 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/providers/dns/acmedns" "github.com/go-acme/lego/v4/providers/dns/active24" "github.com/go-acme/lego/v4/providers/dns/alidns" @@ -104,6 +103,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/luadns" "github.com/go-acme/lego/v4/providers/dns/mailinabox" "github.com/go-acme/lego/v4/providers/dns/manageengine" + "github.com/go-acme/lego/v4/providers/dns/manual" "github.com/go-acme/lego/v4/providers/dns/metaname" "github.com/go-acme/lego/v4/providers/dns/metaregistrar" "github.com/go-acme/lego/v4/providers/dns/mijnhost" @@ -181,8 +181,6 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { - case "manual": - return dns01.NewDNSProviderManual() case "acme-dns", "acmedns": return acmedns.NewDNSProvider() case "active24": @@ -377,6 +375,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return mailinabox.NewDNSProvider() case "manageengine": return manageengine.NewDNSProvider() + case "manual": + return manual.NewDNSProvider() case "metaname": return metaname.NewDNSProvider() case "metaregistrar": From bc163db9edd23bbfc3521086c0b570f468b9a87b Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 1 Dec 2025 20:55:41 +0100 Subject: [PATCH 26/95] feat: remove email requirement (#2736) --- cmd/accounts_storage.go | 43 ++++++++++++++++--------- cmd/cmd_list.go | 2 +- cmd/cmd_renew.go | 4 ++- cmd/setup.go | 11 +------ e2e/dnschallenge/dns_challenges_test.go | 1 - 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/cmd/accounts_storage.go b/cmd/accounts_storage.go index 1dbdfb84b..01db2faf8 100644 --- a/cmd/accounts_storage.go +++ b/cmd/accounts_storage.go @@ -16,6 +16,8 @@ import ( "github.com/urfave/cli/v2" ) +const userIDPlaceholder = "noemail@example.com" + const ( baseAccountsRootFolderName = "accounts" baseKeysFolderName = "keys" @@ -32,7 +34,7 @@ const ( // // rootUserPath: // -// ./.lego/accounts/localhost_14000/hubert@hubert.com/ +// ./.lego/accounts/localhost_14000/foo@example.com/ // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory @@ -40,7 +42,7 @@ const ( // // keysPath: // -// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ +// ./.lego/accounts/localhost_14000/foo@example.com/keys/ // │ │ │ │ └── root keys directory // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) @@ -49,7 +51,7 @@ const ( // // accountFilePath: // -// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json +// ./.lego/accounts/localhost_14000/foo@example.com/account.json // │ │ │ │ └── account file // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) @@ -57,6 +59,7 @@ const ( // └── "path" option type AccountsStorage struct { userID string + email string rootPath string rootUserPath string keysPath string @@ -66,8 +69,13 @@ type AccountsStorage struct { // NewAccountsStorage Creates a new AccountsStorage. func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { - // TODO: move to account struct? Currently MUST pass email. - email := getEmail(ctx) + // TODO: move to account struct? + email := ctx.String(flgEmail) + + userID := email + if userID == "" { + userID = userIDPlaceholder + } serverURL, err := url.Parse(ctx.String(flgServer)) if err != nil { @@ -77,10 +85,11 @@ func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName) serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) accountsPath := filepath.Join(rootPath, serverPath) - rootUserPath := filepath.Join(accountsPath, email) + rootUserPath := filepath.Join(accountsPath, userID) return &AccountsStorage{ - userID: email, + userID: userID, + email: email, rootPath: rootPath, rootUserPath: rootUserPath, keysPath: filepath.Join(rootUserPath, baseKeysFolderName), @@ -112,6 +121,10 @@ func (s *AccountsStorage) GetUserID() string { return s.userID } +func (s *AccountsStorage) GetEmail() string { + return s.email +} + func (s *AccountsStorage) Save(account *Account) error { jsonBytes, err := json.MarshalIndent(account, "", "\t") if err != nil { @@ -124,14 +137,14 @@ func (s *AccountsStorage) Save(account *Account) error { func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { fileBytes, err := os.ReadFile(s.accountFilePath) if err != nil { - log.Fatalf("Could not load file for account %s: %v", s.userID, err) + log.Fatalf("Could not load file for account %s: %v", s.GetUserID(), err) } var account Account err = json.Unmarshal(fileBytes, &account) if err != nil { - log.Fatalf("Could not parse file for account %s: %v", s.userID, err) + log.Fatalf("Could not parse file for account %s: %v", s.GetUserID(), err) } account.key = privateKey @@ -139,14 +152,14 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { if account.Registration == nil || account.Registration.Body.Status == "" { reg, err := tryRecoverRegistration(s.ctx, privateKey) if err != nil { - log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err) + log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.GetUserID(), err) } account.Registration = reg err = s.Save(&account) if err != nil { - log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err) + log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.GetUserID(), err) } } @@ -154,15 +167,15 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { } func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey { - accKeyPath := filepath.Join(s.keysPath, s.userID+".key") + accKeyPath := filepath.Join(s.keysPath, s.GetUserID()+".key") if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType) + log.Printf("No key found for account %s. Generating a %s key.", s.GetUserID(), keyType) s.createKeysFolder() privateKey, err := generatePrivateKey(accKeyPath, keyType) if err != nil { - log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err) + log.Fatalf("Could not generate RSA private account key for account %s: %v", s.GetUserID(), err) } log.Printf("Saved key to %s", accKeyPath) @@ -180,7 +193,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva func (s *AccountsStorage) createKeysFolder() { if err := createNonExistingFolder(s.keysPath); err != nil { - log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err) + log.Fatalf("Could not check/create directory for account %s: %v", s.GetUserID(), err) } } diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index 864b85977..483592d47 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -36,7 +36,7 @@ func createList() *cli.Command { // fake email, needed by NewAccountsStorage &cli.StringFlag{ Name: flgEmail, - Value: "unknown", + Value: "", Hidden: true, }, }, diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index 99bc5ebbd..4b41ebc78 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -144,7 +144,9 @@ func renew(ctx *cli.Context) error { bundle := !ctx.Bool(flgNoBundle) - meta := map[string]string{hookEnvAccountEmail: account.Email} + meta := map[string]string{ + hookEnvAccountEmail: account.Email, + } // CSR if ctx.IsSet(flgCSR) { diff --git a/cmd/setup.go b/cmd/setup.go index 319b7680e..6d15adad3 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -40,7 +40,7 @@ func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, if accountsStorage.ExistsAccountFilePath() { account = accountsStorage.LoadAccount(privateKey) } else { - account = &Account{Email: accountsStorage.GetUserID(), key: privateKey} + account = &Account{Email: accountsStorage.GetEmail(), key: privateKey} } return account, keyType @@ -118,15 +118,6 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType { return "" } -func getEmail(ctx *cli.Context) string { - email := ctx.String(flgEmail) - if email == "" { - log.Fatalf("You have to pass an account (email address) to the program using --%s or -m", flgEmail) - } - - return email -} - func getUserAgent(ctx *cli.Context) string { return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version)) } diff --git a/e2e/dnschallenge/dns_challenges_test.go b/e2e/dnschallenge/dns_challenges_test.go index 509b57bb1..9dd9ab0d6 100644 --- a/e2e/dnschallenge/dns_challenges_test.go +++ b/e2e/dnschallenge/dns_challenges_test.go @@ -58,7 +58,6 @@ func TestChallengeDNS_Run(t *testing.T) { loader.CleanLegoFiles() err := load.RunLego( - "-m", "hubert@hubert.com", "--accept-tos", "--dns", "exec", "--dns.resolvers", ":8053", From dea97e4dfaf3024edcbc959c3efa3556cdfa267c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 1 Dec 2025 21:37:20 +0100 Subject: [PATCH 27/95] Add DNS provider for Gravity (#2738) --- README.md | 54 ++-- cmd/zz_gen_cmd_dnshelp.go | 23 ++ docs/content/dns/zz_gen_gravity.md | 71 +++++ docs/data/zz_cli_help.toml | 2 +- go.mod | 2 +- providers/dns/gravity/gravity.go | 205 ++++++++++++++ providers/dns/gravity/gravity.toml | 26 ++ providers/dns/gravity/gravity_test.go | 254 ++++++++++++++++++ providers/dns/gravity/internal/client.go | 234 ++++++++++++++++ providers/dns/gravity/internal/client_test.go | 160 +++++++++++ .../fixtures/create_record-request.json | 6 + .../dns/gravity/internal/fixtures/error.json | 8 + .../internal/fixtures/login-request.json | 4 + .../dns/gravity/internal/fixtures/login.json | 3 + .../dns/gravity/internal/fixtures/me.json | 16 ++ .../internal/fixtures/me_unauthenticated.json | 5 + .../dns/gravity/internal/fixtures/zones.json | 19 ++ .../internal/fixtures/zones_empty.json | 3 + providers/dns/gravity/internal/types.go | 82 ++++++ providers/dns/zz_gen_dns_providers.go | 3 + 20 files changed, 1151 insertions(+), 29 deletions(-) create mode 100644 docs/content/dns/zz_gen_gravity.md create mode 100644 providers/dns/gravity/gravity.go create mode 100644 providers/dns/gravity/gravity.toml create mode 100644 providers/dns/gravity/gravity_test.go create mode 100644 providers/dns/gravity/internal/client.go create mode 100644 providers/dns/gravity/internal/client_test.go create mode 100644 providers/dns/gravity/internal/fixtures/create_record-request.json create mode 100644 providers/dns/gravity/internal/fixtures/error.json create mode 100644 providers/dns/gravity/internal/fixtures/login-request.json create mode 100644 providers/dns/gravity/internal/fixtures/login.json create mode 100644 providers/dns/gravity/internal/fixtures/me.json create mode 100644 providers/dns/gravity/internal/fixtures/me_unauthenticated.json create mode 100644 providers/dns/gravity/internal/fixtures/zones.json create mode 100644 providers/dns/gravity/internal/fixtures/zones_empty.json create mode 100644 providers/dns/gravity/internal/types.go diff --git a/README.md b/README.md index 42bf26c73..dddcec72d 100644 --- a/README.md +++ b/README.md @@ -142,137 +142,137 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Go Daddy Google Cloud Google Domains - Hetzner + Gravity + Hetzner Hosting.de Hostinger Hosttech - HTTP request + HTTP request http.net Huawei Cloud Hurricane Electric DNS - HyperOne + HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox - Infomaniak + Infomaniak Internet Initiative Japan Internet.bs INWX - Ionos + Ionos IPv64 iwantmyname (Deprecated) Joker - Joohoi's ACME-DNS + Joohoi's ACME-DNS KeyHelp Liara Lima-City - Linode (v4) + Linode (v4) Liquid Web Loopia LuaDNS - Mail-in-a-Box + Mail-in-a-Box ManageEngine CloudDNS Manual Metaname - Metaregistrar + Metaregistrar mijn.host Mittwald myaddr.{tools,dev,io} - MyDNS.jp + MyDNS.jp MythicBeasts Name.com Namecheap - Namesilo + Namesilo NearlyFreeSpeech.NET Netcup Netlify - Nicmanager + Nicmanager NIFCloud Njalla Nodion - NS1 + NS1 Octenium Open Telekom Cloud Oracle Cloud - OVH + OVH plesk.com Porkbun PowerDNS - Rackspace + Rackspace Rain Yun/雨云 RcodeZero reg.ru - Regfish + Regfish RFC2136 RimuHosting RU CENTER - Sakura Cloud + Sakura Cloud Scaleway Selectel Selectel v2 - SelfHost.(de|eu) + SelfHost.(de|eu) Servercow Shellrent Simply.com - Sonic + Sonic Spaceship Stackpath Technitium - Tencent Cloud DNS + Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TransIP - UKFast SafeDNS + UKFast SafeDNS Ultradns United-Domains Variomedia - VegaDNS + VegaDNS Vercel Versio.[nl|eu|uk] VinylDNS - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 022374d7a..2695f0dba 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -79,6 +79,7 @@ func allDNSCodes() string { "glesys", "godaddy", "googledomains", + "gravity", "hetzner", "hostingde", "hostinger", @@ -1636,6 +1637,28 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/googledomains`) + case "gravity": + // generated from: providers/dns/gravity/gravity.toml + ew.writeln(`Configuration for Gravity.`) + ew.writeln(`Code: 'gravity'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "GRAVITY_PASSWORD": Password`) + ew.writeln(` - "GRAVITY_SERVER_URL": URL of the server`) + ew.writeln(` - "GRAVITY_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "GRAVITY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "GRAVITY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "GRAVITY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "GRAVITY_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 1)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/gravity`) + case "hetzner": // generated from: providers/dns/hetzner/hetzner.toml ew.writeln(`Configuration for Hetzner.`) diff --git a/docs/content/dns/zz_gen_gravity.md b/docs/content/dns/zz_gen_gravity.md new file mode 100644 index 000000000..42d5e6128 --- /dev/null +++ b/docs/content/dns/zz_gen_gravity.md @@ -0,0 +1,71 @@ +--- +title: "Gravity" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: gravity +dnsprovider: + since: "v4.30.0" + code: "gravity" + url: "https://gravity.beryju.io/" +--- + + + + + + +Configuration for [Gravity](https://gravity.beryju.io/). + + + + +- Code: `gravity` +- Since: v4.30.0 + + +Here is an example bash command using the Gravity provider: + +```bash +GRAVITY_SERVER_URL="https://example.org:1234" \ +GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ +GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ +lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `GRAVITY_PASSWORD` | Password | +| `GRAVITY_SERVER_URL` | URL of the server | +| `GRAVITY_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 | +|--------------------------------|-------------| +| `GRAVITY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `GRAVITY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `GRAVITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `GRAVITY_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 1) | + +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://gravity.beryju.io/docs/api/reference/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index f9401b0e3..51c6d39f3 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, 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, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, 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/go.mod b/go.mod index f68c2c9eb..cd019fac3 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 github.com/google/go-cmp v0.7.0 github.com/google/go-querystring v1.1.0 + github.com/google/uuid v1.6.0 github.com/gophercloud/gophercloud v1.14.1 github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/hashicorp/go-retryablehttp v0.7.8 @@ -157,7 +158,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/providers/dns/gravity/gravity.go b/providers/dns/gravity/gravity.go new file mode 100644 index 000000000..c8594441a --- /dev/null +++ b/providers/dns/gravity/gravity.go @@ -0,0 +1,205 @@ +// Package gravity implements a DNS provider for solving the DNS-01 challenge using Gravity. +package gravity + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "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/gravity/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/google/uuid" +) + +// Environment variables names. +const ( + envNamespace = "GRAVITY_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + EnvServerURL = envNamespace + "SERVER_URL" + + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + Password string + ServerURL string + + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, 1*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + records map[string]internal.Record + recordsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Gravity. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword, EnvServerURL) + if err != nil { + return nil, fmt.Errorf("gravity: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.ServerURL = values[EnvServerURL] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Gravity. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("gravity: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.ServerURL, config.Username, config.Password) + if err != nil { + return nil, fmt.Errorf("gravity: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + records: make(map[string]internal.Record), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + _, err := d.client.Login(ctx) + if err != nil { + return fmt.Errorf("gravity: login: %w", err) + } + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("gravity: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("gravity: %w", err) + } + + id := uuid.New() + + record := internal.Record{ + Data: info.Value, + Hostname: subDomain, + Type: "TXT", + UID: id.String(), + } + + err = d.client.CreateDNSRecord(ctx, zone, record) + if err != nil { + return fmt.Errorf("gravity: create DNS record: %w", err) + } + + d.recordsMu.Lock() + + record.Fqdn = zone + d.records[token] = record + d.recordsMu.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) + + d.recordsMu.Lock() + record, ok := d.records[token] + d.recordsMu.Unlock() + + if !ok { + return fmt.Errorf("gravity: unknown record for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteDNSRecord(context.Background(), record.Fqdn, record) + if err != nil { + return fmt.Errorf("gravity: delete record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +// Sequential implements the [dns01.sequential] interface. +// It changes the behavior of the provider to resolve DNS challenges sequentially. +// Returns the interval between each iteration. +// +// Gravity supports adding multiple records for the same domain, but the DNS server doesn't work as expected: +// if you call the DNS server, it will answer only the latest record instead of all of them. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, effectiveFQDN string) (string, error) { + var zone string + + for fqdn := range dns01.DomainsSeq(effectiveFQDN) { + zones, err := d.client.GetDNSZones(ctx, fqdn) + if err != nil { + return "", fmt.Errorf("get DNS zones: %w", err) + } + + if len(zones) != 0 { + zone = zones[0].Name + break + } + } + + if zone == "" { + return "", fmt.Errorf("could not find zone for %q", effectiveFQDN) + } + + return zone, nil +} diff --git a/providers/dns/gravity/gravity.toml b/providers/dns/gravity/gravity.toml new file mode 100644 index 000000000..6010e26e1 --- /dev/null +++ b/providers/dns/gravity/gravity.toml @@ -0,0 +1,26 @@ +Name = "Gravity" +Description = '''''' +URL = "https://gravity.beryju.io/" +Code = "gravity" +Since = "v4.30.0" + +Example = ''' +GRAVITY_SERVER_URL="https://example.org:1234" \ +GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ +GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ +lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + GRAVITY_SERVER_URL = "URL of the server" + GRAVITY_USERNAME = "Username" + GRAVITY_PASSWORD = "Password" + [Configuration.Additional] + GRAVITY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + GRAVITY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + GRAVITY_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 1)" + GRAVITY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://gravity.beryju.io/docs/api/reference/" diff --git a/providers/dns/gravity/gravity_test.go b/providers/dns/gravity/gravity_test.go new file mode 100644 index 000000000..b59b856fe --- /dev/null +++ b/providers/dns/gravity/gravity_test.go @@ -0,0 +1,254 @@ +package gravity + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/gravity/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvUsername, + EnvPassword, + EnvServerURL, +).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", + EnvServerURL: "https://example.org:1234", + }, + }, + { + desc: "missing EnvUsername", + envVars: map[string]string{ + EnvUsername: "", + EnvPassword: "secret", + EnvServerURL: "https://example.org:1234", + }, + expected: "gravity: some credentials information are missing: GRAVITY_USERNAME", + }, + { + desc: "missing EnvPassword", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "", + EnvServerURL: "https://example.org:1234", + }, + expected: "gravity: some credentials information are missing: GRAVITY_PASSWORD", + }, + { + desc: "missing EnvServerURL", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "secret", + EnvServerURL: "", + }, + expected: "gravity: some credentials information are missing: GRAVITY_SERVER_URL", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "gravity: some credentials information are missing: GRAVITY_USERNAME,GRAVITY_PASSWORD,GRAVITY_SERVER_URL", + }, + } + + 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 + username string + password string + serverURL string + expected string + }{ + { + desc: "success", + username: "user", + password: "secret", + serverURL: "https://example.org:1234", + }, + { + desc: "missing username", + username: "", + password: "secret", + serverURL: "https://example.org:1234", + expected: "gravity: credentials missing", + }, + { + desc: "missing password", + username: "user", + password: "", + serverURL: "https://example.org:1234", + expected: "gravity: credentials missing", + }, + { + desc: "missing server URL", + username: "user", + password: "secret", + serverURL: "", + expected: "gravity: server URL missing", + }, + { + desc: "missing credentials", + expected: "gravity: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Username = test.username + config.Password = test.password + config.ServerURL = test.serverURL + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + + config.Username = "user" + config.Password = "secret" + config.ServerURL = server.URL + + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /api/v1/auth/login", + servermock.ResponseFromInternal("login.json"), + servermock.CheckRequestJSONBodyFromInternal("login-request.json")). + Route("GET /api/v1/dns/", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Query().Get("name") != "example.com." { + servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) + return + } + + servermock.ResponseFromInternal("zones_empty.json").ServeHTTP(rw, req) + }), + ). + Route("POST /api/v1/dns/zones/records", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckQueryParameter().Strict(). + With("zone", "example.com."). + WithRegexp("uid", `\w{8}-\w{4}-\w{4}-\w{4}-\w{12}`). + With("hostname", "_acme-challenge")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /api/v1/dns/zones/records", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckQueryParameter().Strict(). + With("zone", "example.com."). + With("uid", "123"). + With("type", "TXT"). + With("hostname", "_acme-challenge")). + Build(t) + + provider.records["abc"] = internal.Record{ + Fqdn: "example.com.", + Hostname: "_acme-challenge", + Type: "TXT", + UID: "123", + } + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/gravity/internal/client.go b/providers/dns/gravity/internal/client.go new file mode 100644 index 000000000..41c6294c3 --- /dev/null +++ b/providers/dns/gravity/internal/client.go @@ -0,0 +1,234 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + "golang.org/x/net/publicsuffix" +) + +// Client the Gravity API client. +type Client struct { + username string + password string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(serverURL, username, password string) (*Client, error) { + if username == "" || password == "" { + return nil, errors.New("credentials missing") + } + + if serverURL == "" { + return nil, errors.New("server URL missing") + } + + baseURL, err := url.Parse(serverURL) + if err != nil { + return nil, err + } + + return &Client{ + username: username, + password: password, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) Login(ctx context.Context) (*Auth, error) { + jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + if err != nil { + return nil, err + } + + c.HTTPClient.Jar = jar + + login := Login{ + Username: c.username, + Password: c.password, + } + + endpoint := c.baseURL.JoinPath("api", "v1", "auth", "login") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, login) + if err != nil { + return nil, err + } + + result := &Auth{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) Me(ctx context.Context) (*UserInfo, error) { + endpoint := c.baseURL.JoinPath("api", "v1", "auth", "me") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := &UserInfo{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, err +} + +func (c *Client) GetDNSZones(ctx context.Context, name string) ([]Zone, error) { + endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones") + + if name != "" { + query := endpoint.Query() + query.Set("name", name) + endpoint.RawQuery = query.Encode() + } + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := Zones{} + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result.Zones, nil +} + +func (c *Client) CreateDNSRecord(ctx context.Context, zone string, record Record) error { + endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records") + + query := endpoint.Query() + + query.Set("zone", zone) + query.Set("hostname", record.Hostname) + + // When the UID is the same as an existing one, the record is updated, else a new record is created. + // An explicit UID is not required to create a record. + if record.UID != "" { + query.Set("uid", record.UID) + } + + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) DeleteDNSRecord(ctx context.Context, zone string, record Record) error { + endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records") + + query := endpoint.Query() + + query.Set("zone", zone) + query.Set("hostname", record.Hostname) + query.Set("uid", record.UID) + query.Set("type", record.Type) + + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/gravity/internal/client_test.go b/providers/dns/gravity/internal/client_test.go new file mode 100644 index 000000000..98b17c59e --- /dev/null +++ b/providers/dns/gravity/internal/client_test.go @@ -0,0 +1,160 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "user", "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_Login(t *testing.T) { + client := mockBuilder(). + Route("POST /api/v1/auth/login", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + http.SetCookie(rw, &http.Cookie{ + Name: "gravity_session", + Value: "session_id", + Path: "/", + }) + + servermock.ResponseFromFixture("login.json").ServeHTTP(rw, req) + }), + servermock.CheckRequestJSONBodyFromFixture("login-request.json")). + Build(t) + + auth, err := client.Login(t.Context()) + require.NoError(t, err) + + cookies := client.HTTPClient.Jar.Cookies(client.baseURL) + + require.Len(t, cookies, 1) + + assert.Equal(t, "gravity_session", cookies[0].Name) + assert.Equal(t, "session_id", cookies[0].Value) + + expected := &Auth{Successful: true} + + assert.Equal(t, expected, auth) +} + +func TestClient_Login_error(t *testing.T) { + client := mockBuilder(). + Route("POST /api/v1/auth/login", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + _, err := client.Login(t.Context()) + require.EqualError(t, err, "status: UNAUTHENTICATED, error: unauthenticated, additionalProp1: string") +} + +func TestClient_Me(t *testing.T) { + client := mockBuilder(). + Route("GET /api/v1/auth/me", + servermock.ResponseFromFixture("me.json")). + Build(t) + + info, err := client.Me(t.Context()) + require.NoError(t, err) + + expected := &UserInfo{ + Username: "admin", + Authenticated: true, + Permissions: []Permission{{ + Methods: []string{"GET", "POST", "PUT", "HEAD", "DELETE"}, + Path: "/*", + }}, + } + + assert.Equal(t, expected, info) +} + +func TestClient_GetDNSZones(t *testing.T) { + client := mockBuilder(). + Route("GET /api/v1/dns/", + servermock.ResponseFromFixture("zones.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "example.com.")). + Build(t) + + zones, err := client.GetDNSZones(t.Context(), "example.com.") + require.NoError(t, err) + + expected := []Zone{{ + Name: "example.com.", + HandlerConfigs: []HandlerConfig{ + {Type: "memory"}, + {Type: "etcd"}, + }, + DefaultTTL: 86400, + RecordCount: 1, + }} + + assert.Equal(t, expected, zones) +} + +func TestClient_CreateDNSRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /api/v1/dns/zones/records", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json"), + servermock.CheckQueryParameter().Strict(). + With("zone", "example.com."). + With("uid", "123"). + With("hostname", "_acme-challenge")). + Build(t) + + record := Record{ + Data: "txtTXTtxt", + Hostname: "_acme-challenge", + Type: "TXT", + UID: "123", + } + + err := client.CreateDNSRecord(t.Context(), "example.com.", record) + require.NoError(t, err) +} + +func TestClient_DeleteDNSRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /api/v1/dns/zones/records", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckQueryParameter().Strict(). + With("zone", "example.com."). + With("uid", "123"). + With("type", "TXT"). + With("hostname", "_acme-challenge")). + Build(t) + + record := Record{ + Data: "txtTXTtxt", + Hostname: "_acme-challenge", + Type: "TXT", + UID: "123", + } + + err := client.DeleteDNSRecord(t.Context(), "example.com.", record) + require.NoError(t, err) +} diff --git a/providers/dns/gravity/internal/fixtures/create_record-request.json b/providers/dns/gravity/internal/fixtures/create_record-request.json new file mode 100644 index 000000000..d671d1342 --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/create_record-request.json @@ -0,0 +1,6 @@ +{ + "data": "txtTXTtxt", + "hostname": "_acme-challenge", + "type": "TXT", + "uid": "123" +} diff --git a/providers/dns/gravity/internal/fixtures/error.json b/providers/dns/gravity/internal/fixtures/error.json new file mode 100644 index 000000000..38b78fcca --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/error.json @@ -0,0 +1,8 @@ +{ + "code": 0, + "context": { + "additionalProp1": "string" + }, + "error": "unauthenticated", + "status": "UNAUTHENTICATED" +} diff --git a/providers/dns/gravity/internal/fixtures/login-request.json b/providers/dns/gravity/internal/fixtures/login-request.json new file mode 100644 index 000000000..c641cd3e5 --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/login-request.json @@ -0,0 +1,4 @@ +{ + "username": "user", + "password": "secret" +} diff --git a/providers/dns/gravity/internal/fixtures/login.json b/providers/dns/gravity/internal/fixtures/login.json new file mode 100644 index 000000000..b9ae7145f --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/login.json @@ -0,0 +1,3 @@ +{ + "successful": true +} diff --git a/providers/dns/gravity/internal/fixtures/me.json b/providers/dns/gravity/internal/fixtures/me.json new file mode 100644 index 000000000..881a2ca5f --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/me.json @@ -0,0 +1,16 @@ +{ + "username": "admin", + "authenticated": true, + "permissions": [ + { + "path": "/*", + "methods": [ + "GET", + "POST", + "PUT", + "HEAD", + "DELETE" + ] + } + ] +} diff --git a/providers/dns/gravity/internal/fixtures/me_unauthenticated.json b/providers/dns/gravity/internal/fixtures/me_unauthenticated.json new file mode 100644 index 000000000..67698b8e2 --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/me_unauthenticated.json @@ -0,0 +1,5 @@ +{ + "username": "", + "authenticated": false, + "permissions": null +} diff --git a/providers/dns/gravity/internal/fixtures/zones.json b/providers/dns/gravity/internal/fixtures/zones.json new file mode 100644 index 000000000..53a8df6c1 --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/zones.json @@ -0,0 +1,19 @@ +{ + "zones": [ + { + "name": "example.com.", + "handlerConfigs": [ + { + "type": "memory" + }, + { + "type": "etcd" + } + ], + "defaultTTL": 86400, + "authoritative": false, + "hook": "", + "recordCount": 1 + } + ] +} diff --git a/providers/dns/gravity/internal/fixtures/zones_empty.json b/providers/dns/gravity/internal/fixtures/zones_empty.json new file mode 100644 index 000000000..d8b70b45e --- /dev/null +++ b/providers/dns/gravity/internal/fixtures/zones_empty.json @@ -0,0 +1,3 @@ +{ + "zones": null +} diff --git a/providers/dns/gravity/internal/types.go b/providers/dns/gravity/internal/types.go new file mode 100644 index 000000000..cb6e99083 --- /dev/null +++ b/providers/dns/gravity/internal/types.go @@ -0,0 +1,82 @@ +package internal + +import ( + "fmt" + "strings" +) + +type APIError struct { + Status string `json:"status"` + ErrorMsg string `json:"error"` + Code int `json:"code"` + Context map[string]string `json:"context"` +} + +func (a *APIError) Error() string { + var msg strings.Builder + + msg.WriteString(fmt.Sprintf("status: %s, error: %s", a.Status, a.ErrorMsg)) + + if a.Code != 0 { + msg.WriteString(fmt.Sprintf(", code: %d", a.Code)) + } + + if len(a.Context) != 0 { + for k, v := range a.Context { + msg.WriteString(fmt.Sprintf(", %s: %s", k, v)) + } + } + + return msg.String() +} + +type Login struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type Auth struct { + Successful bool `json:"successful"` +} + +type UserInfo struct { + Username string `json:"username"` + Authenticated bool `json:"authenticated"` + Permissions []Permission `json:"permissions"` +} + +type Permission struct { + Methods []string `json:"methods"` + Path string `json:"path"` +} + +type Zones struct { + Zones []Zone `json:"zones"` +} + +type Zone struct { + Name string `json:"name"` + HandlerConfigs []HandlerConfig `json:"handlerConfigs"` + DefaultTTL int `json:"defaultTTL"` + Authoritative bool `json:"authoritative"` + Hook string `json:"hook"` + RecordCount int `json:"recordCount"` +} + +type HandlerConfig struct { + Type string `json:"type"` + CacheTTL int `json:"cache_ttl,omitempty"` + To []string `json:"to,omitempty"` +} + +type Record struct { + Data string `json:"data,omitempty"` + Fqdn string `json:"fqdn,omitempty"` + Hostname string `json:"hostname,omitempty"` + MxPreference int `json:"mxPreference,omitempty"` + SrvPort int `json:"srvPort,omitempty"` + SrvPriority int `json:"srvPriority,omitempty"` + SrvWeight int `json:"srvWeight,omitempty"` + Type string `json:"type,omitempty"` + UID string `json:"uid,omitempty"` +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 842d00c51..95937c986 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -73,6 +73,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/glesys" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/googledomains" + "github.com/go-acme/lego/v4/providers/dns/gravity" "github.com/go-acme/lego/v4/providers/dns/hetzner" "github.com/go-acme/lego/v4/providers/dns/hostingde" "github.com/go-acme/lego/v4/providers/dns/hostinger" @@ -315,6 +316,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return godaddy.NewDNSProvider() case "googledomains": return googledomains.NewDNSProvider() + case "gravity": + return gravity.NewDNSProvider() case "hetzner": return hetzner.NewDNSProvider() case "hostingde": From 36552da309c4faa028b8309252302f2a30de823d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 5 Dec 2025 16:45:51 +0100 Subject: [PATCH 28/95] chore: update workflows (#2741) --- .github/workflows/documentation.yml | 8 ++------ .github/workflows/go-cross.yml | 9 ++------- .github/workflows/pr.yml | 27 ++++++++++----------------- .github/workflows/release.yml | 8 +++----- .golangci.yml | 3 +++ 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index c0bbbfbdc..4f9d444fc 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,15 +17,11 @@ jobs: steps: - # https://github.com/marketplace/actions/checkout - - name: Check out code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - # https://github.com/marketplace/actions/setup-go-environment - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml index 30ec652a2..9dee85035 100644 --- a/.github/workflows/go-cross.yml +++ b/.github/workflows/go-cross.yml @@ -20,13 +20,8 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - # https://github.com/marketplace/actions/checkout - - name: Checkout code - uses: actions/checkout@v4 - - # https://github.com/marketplace/actions/setup-go-environment - - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 91977bc28..626d9f6e9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.6.0 + GOLANGCI_LINT_VERSION: v2.7.1 HUGO_VERSION: 0.148.2 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI @@ -21,40 +21,33 @@ jobs: steps: - # https://github.com/marketplace/actions/checkout - - name: Check out code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - # https://github.com/marketplace/actions/setup-go-environment - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - name: Check and get dependencies run: | - go mod tidy - git diff --exit-code go.mod - git diff --exit-code go.sum + go mod tidy --diff - name: Generate and Check generated elements run: | make generate-dns git diff --exit-code - # https://golangci-lint.run/usage/install#other-ci - - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} - golangci-lint --version + - uses: golangci/golangci-lint-action@v9 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + install-only: true - name: Install Pebble - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.7.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.8.0 - name: Install challtestsrv - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.7.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.8.0 - name: Set up a Memcached server uses: niden/actions-memcached@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca2e1867e..3ba055979 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,13 +42,11 @@ jobs: docker-images: true swap-storage: false - - name: Check out code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} @@ -71,7 +69,7 @@ jobs: - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: - version: v2.12.3 + version: v2.13.0 args: release -p 1 --clean --timeout=90m env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }} diff --git a/.golangci.yml b/.golangci.yml index ceb7cad85..b851169ff 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -180,6 +180,9 @@ linters: text: Error return value of `fmt.Fprintln` is not checked linters: - errcheck + - text: "var-naming: avoid meaningless package names" + linters: + - revive - path: certcrypto/crypto.go text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable linters: From 02dd7152f03ed630889e98a0ca2225495c4a13e7 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 8 Dec 2025 20:25:08 +0100 Subject: [PATCH 29/95] Add DNS provider for Syse.no (#2742) --- README.md | 16 +- cmd/zz_gen_cmd_dnshelp.go | 21 ++ docs/content/dns/zz_gen_syse.md | 70 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/syse/internal/client.go | 131 +++++++++++ providers/dns/syse/internal/client_test.go | 102 ++++++++ .../fixtures/create_record-request.json | 7 + .../syse/internal/fixtures/create_record.json | 8 + providers/dns/syse/internal/types.go | 11 + providers/dns/syse/syse.go | 182 +++++++++++++++ providers/dns/syse/syse.toml | 25 ++ providers/dns/syse/syse_test.go | 220 ++++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 789 insertions(+), 9 deletions(-) create mode 100644 docs/content/dns/zz_gen_syse.md create mode 100644 providers/dns/syse/internal/client.go create mode 100644 providers/dns/syse/internal/client_test.go create mode 100644 providers/dns/syse/internal/fixtures/create_record-request.json create mode 100644 providers/dns/syse/internal/fixtures/create_record.json create mode 100644 providers/dns/syse/internal/types.go create mode 100644 providers/dns/syse/syse.go create mode 100644 providers/dns/syse/syse.toml create mode 100644 providers/dns/syse/syse_test.go diff --git a/README.md b/README.md index dddcec72d..015d45ea6 100644 --- a/README.md +++ b/README.md @@ -237,42 +237,42 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Sonic Spaceship Stackpath - Technitium + Syse + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TransIP + TransIP UKFast SafeDNS Ultradns United-Domains - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS VK Cloud Volcano Engine/火山引擎 Vscale - Vultr + Vultr webnames.ca webnames.ru Websupport - WEDOS + WEDOS West.cn/西部数码 Yandex 360 Yandex Cloud - Yandex PDD + Yandex PDD Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 2695f0dba..dda63b2a3 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -157,6 +157,7 @@ func allDNSCodes() string { "sonic", "spaceship", "stackpath", + "syse", "technitium", "tencentcloud", "timewebcloud", @@ -3312,6 +3313,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/stackpath`) + case "syse": + // generated from: providers/dns/syse/syse.toml + ew.writeln(`Configuration for Syse.`) + ew.writeln(`Code: 'syse'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "SYSE_CREDENTIALS": Comma-separated list of 'zone:password' credential pairs`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "SYSE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "SYSE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "SYSE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 1200)`) + ew.writeln(` - "SYSE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/syse`) + case "technitium": // generated from: providers/dns/technitium/technitium.toml ew.writeln(`Configuration for Technitium.`) diff --git a/docs/content/dns/zz_gen_syse.md b/docs/content/dns/zz_gen_syse.md new file mode 100644 index 000000000..1d9d957d5 --- /dev/null +++ b/docs/content/dns/zz_gen_syse.md @@ -0,0 +1,70 @@ +--- +title: "Syse" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: syse +dnsprovider: + since: "v4.30.0" + code: "syse" + url: "https://www.syse.no/" +--- + + + + + + +Configuration for [Syse](https://www.syse.no/). + + + + +- Code: `syse` +- Since: v4.30.0 + + +Here is an example bash command using the Syse provider: + +```bash +SYSE_CREDENTIALS=example.com:password \ +lego --email you@example.com --dns syse -d '*.example.com' -d example.com run + +SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ +lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `SYSE_CREDENTIALS` | Comma-separated list of `zone:password` credential pairs | + +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 | +|--------------------------------|-------------| +| `SYSE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `SYSE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `SYSE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) | +| `SYSE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://www.syse.no/api/dns) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 51c6d39f3..37d8700e8 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, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, technitium, tencentcloud, timewebcloud, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, 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/syse/internal/client.go b/providers/dns/syse/internal/client.go new file mode 100644 index 000000000..8cb801469 --- /dev/null +++ b/providers/dns/syse/internal/client.go @@ -0,0 +1,131 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://www.syse.no/api" + +// Client the Syse API client. +type Client struct { + credentials map[string]string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(credentials map[string]string) (*Client, error) { + if len(credentials) == 0 { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + credentials: credentials, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) CreateRecord(ctx context.Context, zone string, record Record) (*Record, error) { + endpoint := c.BaseURL.JoinPath("dns", zone) + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return nil, err + } + + req.SetBasicAuth(zone, c.credentials[zone]) + + result := new(Record) + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { + endpoint := c.BaseURL.JoinPath("dns", zone, recordID) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + req.SetBasicAuth(zone, c.credentials[zone]) + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/syse/internal/client_test.go b/providers/dns/syse/internal/client_test.go new file mode 100644 index 000000000..88416aa88 --- /dev/null +++ b/providers/dns/syse/internal/client_test.go @@ -0,0 +1,102 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(map[string]string{ + "example.com": "secret", + }) + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_CreateRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/example.com", + servermock.ResponseFromFixture("create_record.json"), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). + Build(t) + + record := Record{ + Type: "TXT", + Prefix: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + Active: true, + TTL: 120, + } + + result, err := client.CreateRecord(t.Context(), "example.com", record) + require.NoError(t, err) + + expected := &Record{ + ID: "1234", + Type: "TXT", + Prefix: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + Active: true, + TTL: 120, + } + + assert.Equal(t, expected, result) +} + +func TestClient_CreateRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/example.com", + servermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + record := Record{ + Type: "TXT", + Prefix: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + Active: true, + TTL: 120, + } + + _, err := client.CreateRecord(t.Context(), "example.com", record) + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Unauthorized") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /dns/example.com/1234", + servermock.Noop()). + Build(t) + + err := client.DeleteRecord(t.Context(), "example.com", "1234") + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := mockBuilder(). + Route("DELETE /dns/example.com/1234", + servermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + err := client.DeleteRecord(t.Context(), "example.com", "1234") + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Unauthorized") +} diff --git a/providers/dns/syse/internal/fixtures/create_record-request.json b/providers/dns/syse/internal/fixtures/create_record-request.json new file mode 100644 index 000000000..549a0f60f --- /dev/null +++ b/providers/dns/syse/internal/fixtures/create_record-request.json @@ -0,0 +1,7 @@ +{ + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "active": true, + "ttl": 120, + "prefix": "_acme-challenge", + "type": "TXT" +} diff --git a/providers/dns/syse/internal/fixtures/create_record.json b/providers/dns/syse/internal/fixtures/create_record.json new file mode 100644 index 000000000..b598779c6 --- /dev/null +++ b/providers/dns/syse/internal/fixtures/create_record.json @@ -0,0 +1,8 @@ +{ + "id": "1234", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "active": true, + "ttl": 120, + "prefix": "_acme-challenge", + "type": "TXT" +} diff --git a/providers/dns/syse/internal/types.go b/providers/dns/syse/internal/types.go new file mode 100644 index 000000000..4b90205e1 --- /dev/null +++ b/providers/dns/syse/internal/types.go @@ -0,0 +1,11 @@ +package internal + +type Record struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Prefix string `json:"prefix,omitempty"` + Content string `json:"content,omitempty"` + Priority int `json:"prio,omitempty"` + TTL int `json:"ttl,omitempty"` + Active bool `json:"active,omitempty"` +} diff --git a/providers/dns/syse/syse.go b/providers/dns/syse/syse.go new file mode 100644 index 000000000..aab07131f --- /dev/null +++ b/providers/dns/syse/syse.go @@ -0,0 +1,182 @@ +// Package syse implements a DNS provider for solving the DNS-01 challenge using Syse. +package syse + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "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/syse/internal" +) + +// Environment variables names. +const ( + envNamespace = "SYSE_" + + EnvCredentials = envNamespace + "CREDENTIALS" + + 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 { + Credentials map[string]string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 1200*time.Second), + 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 { + config *Config + client *internal.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Syse. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvCredentials) + if err != nil { + return nil, fmt.Errorf("syse: %w", err) + } + + config := NewDefaultConfig() + + credentials, err := env.ParsePairs(values[EnvCredentials]) + if err != nil { + return nil, fmt.Errorf("syse: credentials: %w", err) + } + + config.Credentials = credentials + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Syse. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("syse: the configuration of the DNS provider is nil") + } + + if len(config.Credentials) == 0 { + return nil, errors.New("syse: missing credentials") + } + + for domain, password := range config.Credentials { + if domain == "" { + return nil, fmt.Errorf(`syse: missing domain: "%s:%s"`, domain, password) + } + + if password == "" { + return nil, fmt.Errorf(`syse: missing password: "%s:%s"`, domain, password) + } + } + + client, err := internal.NewClient(config.Credentials) + if err != nil { + return nil, fmt.Errorf("syse: %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]string), + }, 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("syse: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("syse: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Prefix: subDomain, + Content: info.Value, + TTL: d.config.TTL, + Active: true, + } + + newRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("syse: create record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = newRecord.ID + 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("syse: could not find zone for domain %q: %w", domain, err) + } + + // gets the record's unique ID from when we created it + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("syse: 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("syse: delete record: %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.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/syse/syse.toml b/providers/dns/syse/syse.toml new file mode 100644 index 000000000..0ae585854 --- /dev/null +++ b/providers/dns/syse/syse.toml @@ -0,0 +1,25 @@ +Name = "Syse" +Description = '''''' +URL = "https://www.syse.no/" +Code = "syse" +Since = "v4.30.0" + +Example = ''' +SYSE_CREDENTIALS=example.com:password \ +lego --email you@example.com --dns syse -d '*.example.com' -d example.com run + +SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ +lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +''' + +[Configuration] + [Configuration.Credentials] + SYSE_CREDENTIALS = "Comma-separated list of `zone:password` credential pairs" + [Configuration.Additional] + SYSE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + SYSE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" + SYSE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + SYSE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://www.syse.no/api/dns" diff --git a/providers/dns/syse/syse_test.go b/providers/dns/syse/syse_test.go new file mode 100644 index 000000000..a4472aa7c --- /dev/null +++ b/providers/dns/syse/syse_test.go @@ -0,0 +1,220 @@ +package syse + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvCredentials).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvCredentials: "example.org:123", + }, + }, + { + desc: "success multiple domains", + envVars: map[string]string{ + EnvCredentials: "example.org:123,example.com:456,example.net:789", + }, + }, + { + desc: "invalid credentials", + envVars: map[string]string{ + EnvCredentials: ",", + }, + expected: `syse: credentials: incorrect pair: `, + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvCredentials: "example.org:", + }, + expected: `syse: missing password: "example.org:"`, + }, + { + desc: "missing domain", + envVars: map[string]string{ + EnvCredentials: ":123", + }, + expected: `syse: missing domain: ":123"`, + }, + { + desc: "invalid credentials, partial", + envVars: map[string]string{ + EnvCredentials: "example.org:123,example.net", + }, + expected: "syse: credentials: incorrect pair: example.net", + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvCredentials: "", + }, + expected: "syse: some credentials information are missing: SYSE_CREDENTIALS", + }, + } + + 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 + creds map[string]string + expected string + }{ + { + desc: "success", + creds: map[string]string{"example.org": "123"}, + }, + { + desc: "success multiple domains", + creds: map[string]string{ + "example.org": "123", + "example.com": "456", + "example.net": "789", + }, + }, + { + desc: "missing credentials", + expected: "syse: missing credentials", + }, + { + desc: "missing domain", + creds: map[string]string{"": "123"}, + expected: `syse: missing domain: ":123"`, + }, + { + desc: "missing password", + creds: map[string]string{"example.org": ""}, + expected: `syse: missing password: "example.org:"`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Credentials = test.creds + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Credentials = map[string]string{ + "example.org": "secret", + } + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("/", servermock.DumpRequest()). + Route("POST /dns/example.com", + servermock.ResponseFromInternal("create_record.json"), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /dns/example.com/1234", + servermock.Noop()). + Build(t) + + provider.recordIDs["abc"] = "1234" + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 95937c986..9448e163f 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -151,6 +151,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/sonic" "github.com/go-acme/lego/v4/providers/dns/spaceship" "github.com/go-acme/lego/v4/providers/dns/stackpath" + "github.com/go-acme/lego/v4/providers/dns/syse" "github.com/go-acme/lego/v4/providers/dns/technitium" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/timewebcloud" @@ -472,6 +473,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return spaceship.NewDNSProvider() case "stackpath": return stackpath.NewDNSProvider() + case "syse": + return syse.NewDNSProvider() case "technitium": return technitium.NewDNSProvider() case "tencentcloud": From 961fd586d925a43c4e279a6a1bca815da3369a64 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 9 Dec 2025 16:25:30 +0100 Subject: [PATCH 30/95] docs: add notes --- docs/content/_index.md | 10 ++++++++++ docs/content/dns/_index.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/content/_index.md b/docs/content/_index.md index ba90ddc97..d3787cf19 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -7,6 +7,16 @@ chapter: false Let's Encrypt client and ACME library written in Go. +{{% notice important %}} +lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ + +This project is not owned by a company. I'm not an employee of a company. + +I don't have gifted domains/accounts from DNS companies. + +I've been maintaining it for about 10 years. +{{% /notice %}} + ## Features - ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html) diff --git a/docs/content/dns/_index.md b/docs/content/dns/_index.md index 7ccfeb53d..2b6f0489c 100644 --- a/docs/content/dns/_index.md +++ b/docs/content/dns/_index.md @@ -5,6 +5,16 @@ draft: false weight: 3 --- +{{% notice important %}} +lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ + +This project is not owned by a company. I'm not an employee of a company. + +I don't have gifted domains/accounts from DNS companies. + +I've been maintaining it for about 10 years. +{{% /notice %}} + ## Configuration and Credentials Credentials and DNS configuration for DNS providers must be passed through environment variables. From 1e57e29a9dd7617472fab3feac52b802b0f0f296 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 9 Dec 2025 16:32:14 +0100 Subject: [PATCH 31/95] chore: skip jekyll --- docs/static/.nojekyll | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/static/.nojekyll diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll new file mode 100644 index 000000000..e69de29bb From 9e2dffe8d222ceaeb8aa9b681263fd87189982b2 Mon Sep 17 00:00:00 2001 From: Adrian <161029+aalmenar@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:52:23 +0100 Subject: [PATCH 32/95] Add DNS Provider for Neodigit (#2747) Co-authored-by: Fernandez Ludovic --- README.md | 35 ++-- cmd/zz_gen_cmd_dnshelp.go | 21 ++ docs/content/dns/zz_gen_neodigit.md | 67 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/neodigit/internal/client.go | 182 ++++++++++++++++ .../dns/neodigit/internal/client_test.go | 174 ++++++++++++++++ .../fixtures/create_record-request.json | 8 + .../internal/fixtures/create_record.json | 10 + .../internal/fixtures/get_records.json | 26 +++ .../neodigit/internal/fixtures/get_zones.json | 16 ++ providers/dns/neodigit/internal/types.go | 23 +++ providers/dns/neodigit/neodigit.go | 195 ++++++++++++++++++ providers/dns/neodigit/neodigit.toml | 22 ++ providers/dns/neodigit/neodigit_test.go | 174 ++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 15 files changed, 942 insertions(+), 16 deletions(-) create mode 100644 docs/content/dns/zz_gen_neodigit.md create mode 100644 providers/dns/neodigit/internal/client.go create mode 100644 providers/dns/neodigit/internal/client_test.go create mode 100644 providers/dns/neodigit/internal/fixtures/create_record-request.json create mode 100644 providers/dns/neodigit/internal/fixtures/create_record.json create mode 100644 providers/dns/neodigit/internal/fixtures/get_records.json create mode 100644 providers/dns/neodigit/internal/fixtures/get_zones.json create mode 100644 providers/dns/neodigit/internal/types.go create mode 100644 providers/dns/neodigit/neodigit.go create mode 100644 providers/dns/neodigit/neodigit.toml create mode 100644 providers/dns/neodigit/neodigit_test.go diff --git a/README.md b/README.md index 015d45ea6..8ef958264 100644 --- a/README.md +++ b/README.md @@ -196,83 +196,88 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Namesilo NearlyFreeSpeech.NET + Neodigit Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Syse + Syse Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TransIP UKFast SafeDNS Ultradns - United-Domains + United-Domains Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index dda63b2a3..09667f572 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -122,6 +122,7 @@ func allDNSCodes() string { "namedotcom", "namesilo", "nearlyfreespeech", + "neodigit", "netcup", "netlify", "nicmanager", @@ -2534,6 +2535,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nearlyfreespeech`) + case "neodigit": + // generated from: providers/dns/neodigit/neodigit.toml + ew.writeln(`Configuration for Neodigit.`) + ew.writeln(`Code: 'neodigit'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "NEODIGIT_TOKEN": API token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "NEODIGIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "NEODIGIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "NEODIGIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "NEODIGIT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/neodigit`) + case "netcup": // generated from: providers/dns/netcup/netcup.toml ew.writeln(`Configuration for Netcup.`) diff --git a/docs/content/dns/zz_gen_neodigit.md b/docs/content/dns/zz_gen_neodigit.md new file mode 100644 index 000000000..70dfb6343 --- /dev/null +++ b/docs/content/dns/zz_gen_neodigit.md @@ -0,0 +1,67 @@ +--- +title: "Neodigit" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: neodigit +dnsprovider: + since: "v4.30.0" + code: "neodigit" + url: "https://www.neodigit.net" +--- + + + + + + +Configuration for [Neodigit](https://www.neodigit.net). + + + + +- Code: `neodigit` +- Since: v4.30.0 + + +Here is an example bash command using the Neodigit provider: + +```bash +NEODIGIT_TOKEN=xxxxxx \ +lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `NEODIGIT_TOKEN` | API token | + +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 | +|--------------------------------|-------------| +| `NEODIGIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `NEODIGIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `NEODIGIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `NEODIGIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://developers.neodigit.net/#dns) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 37d8700e8..b172beedb 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, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, 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, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, 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, 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/neodigit/internal/client.go b/providers/dns/neodigit/internal/client.go new file mode 100644 index 000000000..1e883c6d2 --- /dev/null +++ b/providers/dns/neodigit/internal/client.go @@ -0,0 +1,182 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +// DefaultBaseURL is the default API endpoint. +const DefaultBaseURL = "https://api.neodigit.net/v1" + +// Client is a Neodigit API client. +type Client struct { + token string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(token string) (*Client, error) { + if token == "" { + return nil, errors.New("credentials missing: token") + } + + baseURL, err := url.Parse(DefaultBaseURL) + if err != nil { + return nil, err + } + + return &Client{ + token: token, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 30 * time.Second}, + }, nil +} + +// GetZones lists all DNS zones. +func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { + endpoint := c.BaseURL.JoinPath("dns", "zones") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var zones []Zone + + err = c.do(req, &zones) + if err != nil { + return nil, err + } + + return zones, nil +} + +// GetRecords lists all records in a zone. +func (c *Client) GetRecords(ctx context.Context, zoneID int, recordType string) ([]Record, error) { + endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records") + + if recordType != "" { + query := endpoint.Query() + query.Set("type", recordType) + endpoint.RawQuery = query.Encode() + } + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var records []Record + + err = c.do(req, &records) + if err != nil { + return nil, err + } + + return records, nil +} + +// CreateRecord creates a new DNS record. +func (c *Client) CreateRecord(ctx context.Context, zoneID int, record Record) (*Record, error) { + endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records") + + payload := RecordRequest{Record: record} + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) + if err != nil { + return nil, err + } + + var result Record + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +// DeleteRecord deletes a DNS record. +func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID int) error { + endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records", strconv.Itoa(recordID)) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set("X-TCpanel-Token", c.token) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/neodigit/internal/client_test.go b/providers/dns/neodigit/internal/client_test.go new file mode 100644 index 000000000..4e9cf3e85 --- /dev/null +++ b/providers/dns/neodigit/internal/client_test.go @@ -0,0 +1,174 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader().WithJSONHeaders(). + With("X-TCpanel-Token", "secret")) +} + +func TestClient_GetZones(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromFixture("get_zones.json")). + Build(t) + + zones, err := client.GetZones(t.Context()) + require.NoError(t, err) + + expected := []Zone{ + { + ID: 6, + Name: "example.com", + HumanName: "example.com", + }, + { + ID: 7, + Name: "example.org", + HumanName: "example.org", + }, + } + + assert.Equal(t, expected, zones) +} + +func TestClient_GetZones_error(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones", + servermock.RawStringResponse(`{"error": "unauthorized"}`). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + zones, err := client.GetZones(t.Context()) + require.Error(t, err) + + assert.Nil(t, zones) +} + +func TestClient_GetRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/zones/6/records", + servermock.ResponseFromFixture("get_records.json")). + Build(t) + + records, err := client.GetRecords(t.Context(), 6, "") + require.NoError(t, err) + + expected := []Record{ + { + ID: 98, + Name: "", + Type: "SOA", + Content: "ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800", + TTL: 7200, + }, + { + ID: 99, + Name: "", + Type: "NS", + Content: "ns1.example.org", + TTL: 7200, + }, + { + ID: 100, + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + }, + } + + assert.Equal(t, expected, records) +} + +func TestClient_CreateRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/zones/6/records", + servermock.ResponseFromFixture("create_record.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). + Build(t) + + record := Record{ + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + } + + result, err := client.CreateRecord(t.Context(), 6, record) + require.NoError(t, err) + + expected := &Record{ + ID: 101, + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + } + + assert.Equal(t, expected, result) +} + +func TestClient_CreateRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/zones/6/records", + servermock.RawStringResponse(`{"error": "bad request"}`). + WithStatusCode(http.StatusBadRequest)). + Build(t) + + record := Record{ + Name: "_acme-challenge", + Type: "TXT", + Content: "test-value", + TTL: 120, + } + + result, err := client.CreateRecord(t.Context(), 6, record) + require.Error(t, err) + + assert.Nil(t, result) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /dns/zones/6/records/101", + servermock.Noop(). + WithStatusCode(http.StatusNoContent)). + Build(t) + + err := client.DeleteRecord(t.Context(), 6, 101) + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := mockBuilder(). + Route("DELETE /dns/zones/6/records/999", + servermock.RawStringResponse(`{"error": "not found"}`). + WithStatusCode(http.StatusNotFound)). + Build(t) + + err := client.DeleteRecord(t.Context(), 6, 999) + require.Error(t, err) +} diff --git a/providers/dns/neodigit/internal/fixtures/create_record-request.json b/providers/dns/neodigit/internal/fixtures/create_record-request.json new file mode 100644 index 000000000..4cd339c98 --- /dev/null +++ b/providers/dns/neodigit/internal/fixtures/create_record-request.json @@ -0,0 +1,8 @@ +{ + "record": { + "name": "_acme-challenge", + "type": "TXT", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120 + } +} diff --git a/providers/dns/neodigit/internal/fixtures/create_record.json b/providers/dns/neodigit/internal/fixtures/create_record.json new file mode 100644 index 000000000..6f30010ac --- /dev/null +++ b/providers/dns/neodigit/internal/fixtures/create_record.json @@ -0,0 +1,10 @@ +{ + "id": 101, + "name": "_acme-challenge", + "type": "TXT", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "prio": null, + "created_at": "2015-09-21T14:40:27.127+02:00", + "updated_at": "2015-09-21T14:40:27.127+02:00" +} diff --git a/providers/dns/neodigit/internal/fixtures/get_records.json b/providers/dns/neodigit/internal/fixtures/get_records.json new file mode 100644 index 000000000..00e09c37f --- /dev/null +++ b/providers/dns/neodigit/internal/fixtures/get_records.json @@ -0,0 +1,26 @@ +[ + { + "id": 98, + "name": "", + "type": "SOA", + "content": "ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800", + "ttl": 7200, + "prio": null + }, + { + "id": 99, + "name": "", + "type": "NS", + "content": "ns1.example.org", + "ttl": 7200, + "prio": null + }, + { + "id": 100, + "name": "_acme-challenge", + "type": "TXT", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "prio": null + } +] diff --git a/providers/dns/neodigit/internal/fixtures/get_zones.json b/providers/dns/neodigit/internal/fixtures/get_zones.json new file mode 100644 index 000000000..01a08dced --- /dev/null +++ b/providers/dns/neodigit/internal/fixtures/get_zones.json @@ -0,0 +1,16 @@ +[ + { + "id": 6, + "name": "example.com", + "created_at": "2015-09-21T12:19:04.000+02:00", + "updated_at": "2015-09-21T12:19:04.000+02:00", + "human_name": "example.com" + }, + { + "id": 7, + "name": "example.org", + "created_at": "2015-09-22T10:00:00.000+02:00", + "updated_at": "2015-09-22T10:00:00.000+02:00", + "human_name": "example.org" + } +] diff --git a/providers/dns/neodigit/internal/types.go b/providers/dns/neodigit/internal/types.go new file mode 100644 index 000000000..505bfbced --- /dev/null +++ b/providers/dns/neodigit/internal/types.go @@ -0,0 +1,23 @@ +package internal + +// Zone represents a DNS zone. +type Zone struct { + ID int `json:"id"` + Name string `json:"name"` + HumanName string `json:"human_name"` +} + +// Record represents a DNS record. +type Record struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` + Priority int `json:"prio,omitempty"` +} + +// RecordRequest is the request body for creating/updating a record. +type RecordRequest struct { + Record Record `json:"record"` +} diff --git a/providers/dns/neodigit/neodigit.go b/providers/dns/neodigit/neodigit.go new file mode 100644 index 000000000..08ff64e04 --- /dev/null +++ b/providers/dns/neodigit/neodigit.go @@ -0,0 +1,195 @@ +// Package neodigit implements a DNS provider for solving the DNS-01 challenge using Neodigit DNS. +package neodigit + +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/neodigit/internal" +) + +// Environment variables names. +const ( + envNamespace = "NEODIGIT_" + + EnvToken = envNamespace + "TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Token string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*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 { + config *Config + client *internal.Client + + zoneIDs map[string]int + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Neodigit. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvToken) + if err != nil { + return nil, fmt.Errorf("neodigit: %w", err) + } + + config := NewDefaultConfig() + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Neodigit. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("neodigit: the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("neodigit: missing credentials") + } + + client, err := internal.NewClient(config.Token) + if err != nil { + return nil, fmt.Errorf("neodigit: create client: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + zoneIDs: make(map[string]int), + recordIDs: make(map[string]int), + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("neodigit: could not find zone for domain %q: %w", domain, err) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.findZone(ctx, authZone) + if err != nil { + return fmt.Errorf("neodigit: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("neodigit: %w", err) + } + + record := internal.Record{ + Name: subDomain, + Type: "TXT", + Content: info.Value, + TTL: d.config.TTL, + } + + newRecord, err := d.client.CreateRecord(ctx, zone.ID, record) + if err != nil { + return fmt.Errorf("neodigit: create record: %w", err) + } + + d.recordIDsMu.Lock() + d.zoneIDs[token] = zone.ID + d.recordIDs[token] = newRecord.ID + 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) + + d.recordIDsMu.Lock() + zoneID, zoneOK := d.zoneIDs[token] + recordID, recordOK := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !zoneOK || !recordOK { + return fmt.Errorf("neodigit: unknown record ID or zone ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteRecord(context.Background(), zoneID, recordID) + if err != nil { + return fmt.Errorf("neodigit: delete record: fqdn=%s, zoneID=%d, recordID=%d: %w", + info.EffectiveFQDN, zoneID, recordID, err) + } + + d.recordIDsMu.Lock() + delete(d.zoneIDs, token) + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) { + zones, err := d.client.GetZones(ctx) + if err != nil { + return nil, fmt.Errorf("get zones: %w", err) + } + + for _, zone := range zones { + if zone.Name == zoneName || zone.HumanName == zoneName { + return &zone, nil + } + } + + return nil, fmt.Errorf("zone not found: %s", zoneName) +} diff --git a/providers/dns/neodigit/neodigit.toml b/providers/dns/neodigit/neodigit.toml new file mode 100644 index 000000000..b391a6512 --- /dev/null +++ b/providers/dns/neodigit/neodigit.toml @@ -0,0 +1,22 @@ +Name = "Neodigit" +Description = '''''' +URL = "https://www.neodigit.net" +Code = "neodigit" +Since = "v4.30.0" + +Example = ''' +NEODIGIT_TOKEN=xxxxxx \ +lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + NEODIGIT_TOKEN = "API token" + [Configuration.Additional] + NEODIGIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + NEODIGIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + NEODIGIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + NEODIGIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://developers.neodigit.net/#dns" diff --git a/providers/dns/neodigit/neodigit_test.go b/providers/dns/neodigit/neodigit_test.go new file mode 100644 index 000000000..1f32d31a3 --- /dev/null +++ b/providers/dns/neodigit/neodigit_test.go @@ -0,0 +1,174 @@ +package neodigit + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvToken: "secret", + }, + }, + { + desc: "missing credentials: token", + envVars: map[string]string{ + EnvToken: "", + }, + expected: "neodigit: some credentials information are missing: NEODIGIT_TOKEN", + }, + } + + 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 + token string + expected string + }{ + { + desc: "success", + token: "secret", + }, + { + desc: "missing token", + expected: "neodigit: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Token = test.token + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + + config.Token = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader().WithJSONHeaders(). + With("X-TCpanel-Token", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromInternal("get_zones.json")). + Route("POST /dns/zones/6/records", + servermock.ResponseFromInternal("create_record.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /dns/zones/456/records/123", + servermock.Noop(). + WithStatusCode(http.StatusNoContent)). + Build(t) + + token := "abc" + + provider.recordIDs[token] = 123 + provider.zoneIDs[token] = 456 + + err := provider.CleanUp("example.com", token, "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 9448e163f..5a068c3a5 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -116,6 +116,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech" + "github.com/go-acme/lego/v4/providers/dns/neodigit" "github.com/go-acme/lego/v4/providers/dns/netcup" "github.com/go-acme/lego/v4/providers/dns/netlify" "github.com/go-acme/lego/v4/providers/dns/nicmanager" @@ -403,6 +404,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return namesilo.NewDNSProvider() case "nearlyfreespeech": return nearlyfreespeech.NewDNSProvider() + case "neodigit": + return neodigit.NewDNSProvider() case "netcup": return netcup.NewDNSProvider() case "netlify": From e54598536ba25fb754bb2c0d68d77e61ea6ce6f5 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 11 Dec 2025 12:58:21 +0100 Subject: [PATCH 33/95] Add DNS provider for Virtualname (#2748) --- README.md | 10 +- cmd/zz_gen_cmd_dnshelp.go | 21 +++ docs/content/dns/zz_gen_virtualname.md | 67 +++++++ docs/data/zz_cli_help.toml | 2 +- .../tecnocratica}/internal/client.go | 8 +- .../tecnocratica}/internal/client_test.go | 0 .../fixtures/create_record-request.json | 0 .../internal/fixtures/create_record.json | 0 .../internal/fixtures/get_records.json | 0 .../internal/fixtures/get_zones.json | 0 .../tecnocratica}/internal/types.go | 0 .../dns/internal/tecnocratica/provider.go | 165 ++++++++++++++++++ .../internal/tecnocratica/provider_test.go | 99 +++++++++++ providers/dns/neodigit/neodigit.go | 120 ++----------- providers/dns/neodigit/neodigit_test.go | 62 +------ providers/dns/virtualname/virtualname.go | 101 +++++++++++ providers/dns/virtualname/virtualname.toml | 22 +++ providers/dns/virtualname/virtualname_test.go | 116 ++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 19 files changed, 619 insertions(+), 177 deletions(-) create mode 100644 docs/content/dns/zz_gen_virtualname.md rename providers/dns/{neodigit => internal/tecnocratica}/internal/client.go (95%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/client_test.go (100%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/fixtures/create_record-request.json (100%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/fixtures/create_record.json (100%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/fixtures/get_records.json (100%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/fixtures/get_zones.json (100%) rename providers/dns/{neodigit => internal/tecnocratica}/internal/types.go (100%) create mode 100644 providers/dns/internal/tecnocratica/provider.go create mode 100644 providers/dns/internal/tecnocratica/provider_test.go create mode 100644 providers/dns/virtualname/virtualname.go create mode 100644 providers/dns/virtualname/virtualname.toml create mode 100644 providers/dns/virtualname/virtualname_test.go diff --git a/README.md b/README.md index 8ef958264..570a340e5 100644 --- a/README.md +++ b/README.md @@ -256,28 +256,28 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Versio.[nl|eu|uk] VinylDNS + Virtualname VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 09667f572..ebd4cc72b 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -170,6 +170,7 @@ func allDNSCodes() string { "vercel", "versio", "vinyldns", + "virtualname", "vkcloud", "volcengine", "vscale", @@ -3588,6 +3589,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vinyldns`) + case "virtualname": + // generated from: providers/dns/virtualname/virtualname.toml + ew.writeln(`Configuration for Virtualname.`) + ew.writeln(`Code: 'virtualname'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "VIRTUALNAME_TOKEN": API token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "VIRTUALNAME_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "VIRTUALNAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "VIRTUALNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "VIRTUALNAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/virtualname`) + case "vkcloud": // generated from: providers/dns/vkcloud/vkcloud.toml ew.writeln(`Configuration for VK Cloud.`) diff --git a/docs/content/dns/zz_gen_virtualname.md b/docs/content/dns/zz_gen_virtualname.md new file mode 100644 index 000000000..afba24ad0 --- /dev/null +++ b/docs/content/dns/zz_gen_virtualname.md @@ -0,0 +1,67 @@ +--- +title: "Virtualname" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: virtualname +dnsprovider: + since: "v4.30.0" + code: "virtualname" + url: "https://www.virtualname.es/" +--- + + + + + + +Configuration for [Virtualname](https://www.virtualname.es/). + + + + +- Code: `virtualname` +- Since: v4.30.0 + + +Here is an example bash command using the Virtualname provider: + +```bash +VIRTUALNAME_TOKEN=xxxxxx \ +lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `VIRTUALNAME_TOKEN` | API token | + +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 | +|--------------------------------|-------------| +| `VIRTUALNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `VIRTUALNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `VIRTUALNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `VIRTUALNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://developers.virtualname.net/#dns) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index b172beedb..506c7e47d 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, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, 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, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, active24, alidns, aliesa, allinkl, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, 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/neodigit/internal/client.go b/providers/dns/internal/tecnocratica/internal/client.go similarity index 95% rename from providers/dns/neodigit/internal/client.go rename to providers/dns/internal/tecnocratica/internal/client.go index 1e883c6d2..5a529fa2f 100644 --- a/providers/dns/neodigit/internal/client.go +++ b/providers/dns/internal/tecnocratica/internal/client.go @@ -16,10 +16,10 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/useragent" ) -// DefaultBaseURL is the default API endpoint. -const DefaultBaseURL = "https://api.neodigit.net/v1" +// defaultBaseURL is the default API endpoint. +const defaultBaseURL = "https://api.neodigit.net/v1" -// Client is a Neodigit API client. +// Client is a Tecnocrática API client. type Client struct { token string @@ -33,7 +33,7 @@ func NewClient(token string) (*Client, error) { return nil, errors.New("credentials missing: token") } - baseURL, err := url.Parse(DefaultBaseURL) + baseURL, err := url.Parse(defaultBaseURL) if err != nil { return nil, err } diff --git a/providers/dns/neodigit/internal/client_test.go b/providers/dns/internal/tecnocratica/internal/client_test.go similarity index 100% rename from providers/dns/neodigit/internal/client_test.go rename to providers/dns/internal/tecnocratica/internal/client_test.go diff --git a/providers/dns/neodigit/internal/fixtures/create_record-request.json b/providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json similarity index 100% rename from providers/dns/neodigit/internal/fixtures/create_record-request.json rename to providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json diff --git a/providers/dns/neodigit/internal/fixtures/create_record.json b/providers/dns/internal/tecnocratica/internal/fixtures/create_record.json similarity index 100% rename from providers/dns/neodigit/internal/fixtures/create_record.json rename to providers/dns/internal/tecnocratica/internal/fixtures/create_record.json diff --git a/providers/dns/neodigit/internal/fixtures/get_records.json b/providers/dns/internal/tecnocratica/internal/fixtures/get_records.json similarity index 100% rename from providers/dns/neodigit/internal/fixtures/get_records.json rename to providers/dns/internal/tecnocratica/internal/fixtures/get_records.json diff --git a/providers/dns/neodigit/internal/fixtures/get_zones.json b/providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json similarity index 100% rename from providers/dns/neodigit/internal/fixtures/get_zones.json rename to providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json diff --git a/providers/dns/neodigit/internal/types.go b/providers/dns/internal/tecnocratica/internal/types.go similarity index 100% rename from providers/dns/neodigit/internal/types.go rename to providers/dns/internal/tecnocratica/internal/types.go diff --git a/providers/dns/internal/tecnocratica/provider.go b/providers/dns/internal/tecnocratica/provider.go new file mode 100644 index 000000000..17cfb8379 --- /dev/null +++ b/providers/dns/internal/tecnocratica/provider.go @@ -0,0 +1,165 @@ +// Package tecnocratica implements a DNS provider for solving the DNS-01 challenge using Tecnocrática. +package tecnocratica + +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/tecnocratica/internal" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Token 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 + + zoneIDs map[string]int + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Tecnocrática. +func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("missing credentials") + } + + client, err := internal.NewClient(config.Token) + if err != nil { + return nil, fmt.Errorf("create client: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + if baseURL != "" { + client.BaseURL, err = url.Parse(baseURL) + if err != nil { + return nil, err + } + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + zoneIDs: make(map[string]int), + recordIDs: make(map[string]int), + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + 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) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.findZone(ctx, authZone) + if err != nil { + return fmt.Errorf("%w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("%w", err) + } + + record := internal.Record{ + Name: subDomain, + Type: "TXT", + Content: info.Value, + TTL: d.config.TTL, + } + + newRecord, err := d.client.CreateRecord(ctx, zone.ID, record) + if err != nil { + return fmt.Errorf("create record: %w", err) + } + + d.recordIDsMu.Lock() + d.zoneIDs[token] = zone.ID + d.recordIDs[token] = newRecord.ID + 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) + + d.recordIDsMu.Lock() + zoneID, zoneOK := d.zoneIDs[token] + recordID, recordOK := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !zoneOK || !recordOK { + return fmt.Errorf("unknown record ID or zone ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteRecord(context.Background(), zoneID, recordID) + if err != nil { + return fmt.Errorf("delete record: fqdn=%s, zoneID=%d, recordID=%d: %w", + info.EffectiveFQDN, zoneID, recordID, err) + } + + d.recordIDsMu.Lock() + delete(d.zoneIDs, token) + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) { + zones, err := d.client.GetZones(ctx) + if err != nil { + return nil, fmt.Errorf("get zones: %w", err) + } + + for _, zone := range zones { + if zone.Name == zoneName || zone.HumanName == zoneName { + return &zone, nil + } + } + + return nil, fmt.Errorf("zone not found: %s", zoneName) +} diff --git a/providers/dns/internal/tecnocratica/provider_test.go b/providers/dns/internal/tecnocratica/provider_test.go new file mode 100644 index 000000000..33e5f7c67 --- /dev/null +++ b/providers/dns/internal/tecnocratica/provider_test.go @@ -0,0 +1,99 @@ +package tecnocratica + +import ( + "net/http" + "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 + token string + expected string + }{ + { + desc: "success", + token: "secret", + }, + { + desc: "missing token", + expected: "missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := &Config{} + config.Token = test.token + + 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{ + Token: "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().WithJSONHeaders(). + With("X-TCpanel-Token", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /dns/zones", + servermock.ResponseFromInternal("get_zones.json")). + Route("POST /dns/zones/6/records", + servermock.ResponseFromInternal("create_record.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /dns/zones/456/records/123", + servermock.Noop(). + WithStatusCode(http.StatusNoContent)). + Build(t) + + token := "abc" + + provider.recordIDs[token] = 123 + provider.zoneIDs[token] = 456 + + err := provider.CleanUp("example.com", token, "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/neodigit/neodigit.go b/providers/dns/neodigit/neodigit.go index 08ff64e04..eb4530479 100644 --- a/providers/dns/neodigit/neodigit.go +++ b/providers/dns/neodigit/neodigit.go @@ -2,18 +2,15 @@ package neodigit 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/neodigit/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica" ) // Environment variables names. @@ -31,14 +28,7 @@ const ( var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} +type Config = tecnocratica.Config // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { @@ -54,12 +44,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *internal.Client - - zoneIDs map[string]int - recordIDs map[string]int - recordIDsMu sync.Mutex + prv challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for Neodigit. @@ -81,115 +66,36 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("neodigit: the configuration of the DNS provider is nil") } - if config.Token == "" { - return nil, errors.New("neodigit: missing credentials") - } - - client, err := internal.NewClient(config.Token) + provider, err := tecnocratica.NewDNSProviderConfig(config, "") if err != nil { - return nil, fmt.Errorf("neodigit: create client: %w", err) + return nil, fmt.Errorf("neodigit: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - zoneIDs: make(map[string]int), - recordIDs: make(map[string]int), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval + return &DNSProvider{prv: provider}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("neodigit: could not find zone for domain %q: %w", domain, err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.findZone(ctx, authZone) + err := d.prv.Present(domain, token, keyAuth) if err != nil { return fmt.Errorf("neodigit: %w", err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("neodigit: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Content: info.Value, - TTL: d.config.TTL, - } - - newRecord, err := d.client.CreateRecord(ctx, zone.ID, record) - if err != nil { - return fmt.Errorf("neodigit: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.zoneIDs[token] = zone.ID - d.recordIDs[token] = newRecord.ID - 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) - - d.recordIDsMu.Lock() - zoneID, zoneOK := d.zoneIDs[token] - recordID, recordOK := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !zoneOK || !recordOK { - return fmt.Errorf("neodigit: unknown record ID or zone ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err := d.client.DeleteRecord(context.Background(), zoneID, recordID) + err := d.prv.CleanUp(domain, token, keyAuth) if err != nil { - return fmt.Errorf("neodigit: delete record: fqdn=%s, zoneID=%d, recordID=%d: %w", - info.EffectiveFQDN, zoneID, recordID, err) + return fmt.Errorf("neodigit: %w", err) } - d.recordIDsMu.Lock() - delete(d.zoneIDs, token) - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } -func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) { - zones, err := d.client.GetZones(ctx) - if err != nil { - return nil, fmt.Errorf("get zones: %w", err) - } - - for _, zone := range zones { - if zone.Name == zoneName || zone.HumanName == zoneName { - return &zone, nil - } - } - - return nil, fmt.Errorf("zone not found: %s", zoneName) +// 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/neodigit/neodigit_test.go b/providers/dns/neodigit/neodigit_test.go index 1f32d31a3..39f67c59c 100644 --- a/providers/dns/neodigit/neodigit_test.go +++ b/providers/dns/neodigit/neodigit_test.go @@ -1,13 +1,9 @@ package neodigit import ( - "net/http" - "net/http/httptest" - "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) @@ -49,8 +45,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) } @@ -84,8 +79,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) } @@ -120,55 +114,3 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - - config.Token = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-TCpanel-Token", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromInternal("get_zones.json")). - Route("POST /dns/zones/6/records", - servermock.ResponseFromInternal("create_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /dns/zones/456/records/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - token := "abc" - - provider.recordIDs[token] = 123 - provider.zoneIDs[token] = 456 - - err := provider.CleanUp("example.com", token, "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/virtualname/virtualname.go b/providers/dns/virtualname/virtualname.go new file mode 100644 index 000000000..6b04e8169 --- /dev/null +++ b/providers/dns/virtualname/virtualname.go @@ -0,0 +1,101 @@ +// Package virtualname implements a DNS provider for solving the DNS-01 challenge using Virtualname DNS. +package virtualname + +import ( + "errors" + "fmt" + "net/http" + "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/tecnocratica" +) + +// Environment variables names. +const ( + envNamespace = "VIRTUALNAME_" + + EnvToken = envNamespace + "TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config = tecnocratica.Config + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*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 Virtualname. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvToken) + if err != nil { + return nil, fmt.Errorf("virtualname: %w", err) + } + + config := NewDefaultConfig() + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Virtualname. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("virtualname: the configuration of the DNS provider is nil") + } + + provider, err := tecnocratica.NewDNSProviderConfig(config, "https://api.virtualname.net/v1") + if err != nil { + return nil, fmt.Errorf("virtualname: %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("virtualname: %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("virtualname: %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/virtualname/virtualname.toml b/providers/dns/virtualname/virtualname.toml new file mode 100644 index 000000000..7cc4c5344 --- /dev/null +++ b/providers/dns/virtualname/virtualname.toml @@ -0,0 +1,22 @@ +Name = "Virtualname" +Description = '''''' +URL = "https://www.virtualname.es/" +Code = "virtualname" +Since = "v4.30.0" + +Example = ''' +VIRTUALNAME_TOKEN=xxxxxx \ +lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + VIRTUALNAME_TOKEN = "API token" + [Configuration.Additional] + VIRTUALNAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + VIRTUALNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + VIRTUALNAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + VIRTUALNAME_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://developers.virtualname.net/#dns" diff --git a/providers/dns/virtualname/virtualname_test.go b/providers/dns/virtualname/virtualname_test.go new file mode 100644 index 000000000..da5867e86 --- /dev/null +++ b/providers/dns/virtualname/virtualname_test.go @@ -0,0 +1,116 @@ +package virtualname + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvToken: "secret", + }, + }, + { + desc: "missing credentials: token", + envVars: map[string]string{ + EnvToken: "", + }, + expected: "virtualname: some credentials information are missing: VIRTUALNAME_TOKEN", + }, + } + + 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 + token string + expected string + }{ + { + desc: "success", + token: "secret", + }, + { + desc: "missing token", + expected: "virtualname: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Token = test.token + + 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/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 5a068c3a5..2b1b14b24 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -164,6 +164,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/vercel" "github.com/go-acme/lego/v4/providers/dns/versio" "github.com/go-acme/lego/v4/providers/dns/vinyldns" + "github.com/go-acme/lego/v4/providers/dns/virtualname" "github.com/go-acme/lego/v4/providers/dns/vkcloud" "github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/go-acme/lego/v4/providers/dns/vscale" @@ -500,6 +501,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return versio.NewDNSProvider() case "vinyldns": return vinyldns.NewDNSProvider() + case "virtualname": + return virtualname.NewDNSProvider() case "vkcloud": return vkcloud.NewDNSProvider() case "volcengine": From c59d163e797f4f96cb0830648b1181ef8d8e25de Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 11 Dec 2025 14:16:34 +0100 Subject: [PATCH 34/95] chore: improves github templates --- .github/ISSUE_TEMPLATE/new_dns_provider.yml | 11 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 2 files changed, 14 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/new_dns_provider.yml b/.github/ISSUE_TEMPLATE/new_dns_provider.yml index 902951ed8..cfd6e5c8c 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -40,6 +40,17 @@ body: validations: required: true + - type: dropdown + id: profile + attributes: + label: Who are you? + options: + - A customer of this DNS provider + - An employee of this DNS provider + - Other (please explain) + validations: + required: true + - type: input id: provider-link attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8b1690de5..ac9fd4975 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,4 +6,7 @@ IMPORTANT: 2. Don't open a work-in-progress pull request. If you open a PR, the PR must be ready to be reviewed. 3. If a pull request doesn't follow the previous elements, it will close. +Also, pull requests from a fork inside a GitHub organization are not allowed because of access limitation on them. +Only pull requests from personal forks are allowed. + --> From 465d7918a80d1887d84f6eeb8aaee2e71622114c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 15 Dec 2025 20:16:31 +0100 Subject: [PATCH 35/95] Add DNS provider for hosting.nl (#1967) --- README.md | 54 +++--- cmd/zz_gen_cmd_dnshelp.go | 21 +++ docs/content/dns/zz_gen_hostingnl.md | 67 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/hostingnl/hostingnl.go | 168 ++++++++++++++++++ providers/dns/hostingnl/hostingnl.toml | 22 +++ providers/dns/hostingnl/hostingnl_test.go | 167 +++++++++++++++++ providers/dns/hostingnl/internal/client.go | 144 +++++++++++++++ .../dns/hostingnl/internal/client_test.go | 92 ++++++++++ .../internal/fixtures/add_record-request.json | 8 + .../internal/fixtures/add_record.json | 13 ++ .../fixtures/delete_record-request.json | 5 + .../internal/fixtures/delete_record.json | 8 + .../hostingnl/internal/fixtures/error.json | 5 + .../internal/fixtures/error_other.json | 3 + providers/dns/hostingnl/internal/types.go | 36 ++++ providers/dns/zz_gen_dns_providers.go | 3 + 17 files changed, 790 insertions(+), 28 deletions(-) create mode 100644 docs/content/dns/zz_gen_hostingnl.md create mode 100644 providers/dns/hostingnl/hostingnl.go create mode 100644 providers/dns/hostingnl/hostingnl.toml create mode 100644 providers/dns/hostingnl/hostingnl_test.go create mode 100644 providers/dns/hostingnl/internal/client.go create mode 100644 providers/dns/hostingnl/internal/client_test.go create mode 100644 providers/dns/hostingnl/internal/fixtures/add_record-request.json create mode 100644 providers/dns/hostingnl/internal/fixtures/add_record.json create mode 100644 providers/dns/hostingnl/internal/fixtures/delete_record-request.json create mode 100644 providers/dns/hostingnl/internal/fixtures/delete_record.json create mode 100644 providers/dns/hostingnl/internal/fixtures/error.json create mode 100644 providers/dns/hostingnl/internal/fixtures/error_other.json create mode 100644 providers/dns/hostingnl/internal/types.go diff --git a/README.md b/README.md index 570a340e5..3b395aad9 100644 --- a/README.md +++ b/README.md @@ -146,138 +146,138 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Hetzner Hosting.de + Hosting.nl Hostinger - Hosttech + Hosttech HTTP request http.net Huawei Cloud - Hurricane Electric DNS + Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service - Infoblox + Infoblox Infomaniak Internet Initiative Japan Internet.bs - INWX + INWX Ionos IPv64 iwantmyname (Deprecated) - Joker + Joker Joohoi's ACME-DNS KeyHelp Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Neodigit - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Syse Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TransIP UKFast SafeDNS - Ultradns + Ultradns United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS Virtualname - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index ebd4cc72b..89b3ce7e1 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -83,6 +83,7 @@ func allDNSCodes() string { "hetzner", "hostingde", "hostinger", + "hostingnl", "hosttech", "httpnet", "httpreq", @@ -1723,6 +1724,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostinger`) + case "hostingnl": + // generated from: providers/dns/hostingnl/hostingnl.toml + ew.writeln(`Configuration for Hosting.nl.`) + ew.writeln(`Code: 'hostingnl'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "HOSTINGNL_API_KEY": The API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "HOSTINGNL_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "HOSTINGNL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "HOSTINGNL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "HOSTINGNL_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingnl`) + case "hosttech": // generated from: providers/dns/hosttech/hosttech.toml ew.writeln(`Configuration for Hosttech.`) diff --git a/docs/content/dns/zz_gen_hostingnl.md b/docs/content/dns/zz_gen_hostingnl.md new file mode 100644 index 000000000..0577affd4 --- /dev/null +++ b/docs/content/dns/zz_gen_hostingnl.md @@ -0,0 +1,67 @@ +--- +title: "Hosting.nl" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: hostingnl +dnsprovider: + since: "v4.30.0" + code: "hostingnl" + url: "https://hosting.nl" +--- + + + + + + +Configuration for [Hosting.nl](https://hosting.nl). + + + + +- Code: `hostingnl` +- Since: v4.30.0 + + +Here is an example bash command using the Hosting.nl provider: + +```bash +HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `HOSTINGNL_API_KEY` | The API key | + +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 | +|--------------------------------|-------------| +| `HOSTINGNL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `HOSTINGNL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `HOSTINGNL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `HOSTINGNL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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.hosting.nl/api/documentation) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 506c7e47d..7962f0d73 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, 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, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, 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, 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, ipv64, 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/hostingnl/hostingnl.go b/providers/dns/hostingnl/hostingnl.go new file mode 100644 index 000000000..a49941817 --- /dev/null +++ b/providers/dns/hostingnl/hostingnl.go @@ -0,0 +1,168 @@ +// Package hostingnl implements a DNS provider for solving the DNS-01 challenge using hosting.nl. +package hostingnl + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "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/hostingnl/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "HOSTINGNL_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + HTTPClient *http.Client + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for hosting.nl. +// Credentials must be passed in the environment variables: +// HOSTINGNL_APIKEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("hostingnl: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for hosting.nl. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("hostingnl: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("hostingnl: APIKey is missing") + } + + client := internal.NewClient(config.APIKey) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, 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("hostingnl: could not find zone for domain %q: %w", domain, err) + } + + record := internal.Record{ + Name: dns01.UnFqdn(info.EffectiveFQDN), + Type: "TXT", + Content: strconv.Quote(info.Value), + TTL: d.config.TTL, + Priority: 0, + } + + newRecord, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("hostingnl: failed to create TXT record, fqdn=%s: %w", info.EffectiveFQDN, err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = newRecord.ID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT records 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("hostingnl: 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("hostingnl: 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("hostingnl: failed to delete TXT record, id=%s: %w", recordID, 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/hostingnl/hostingnl.toml b/providers/dns/hostingnl/hostingnl.toml new file mode 100644 index 000000000..a26c07ab2 --- /dev/null +++ b/providers/dns/hostingnl/hostingnl.toml @@ -0,0 +1,22 @@ +Name = "Hosting.nl" +Description = '''''' +URL = "https://hosting.nl" +Code = "hostingnl" +Since = "v4.30.0" + +Example = ''' +HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + HOSTINGNL_API_KEY = "The API key" + [Configuration.Additional] + HOSTINGNL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + HOSTINGNL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + HOSTINGNL_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + HOSTINGNL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + +[Links] + API = "https://api.hosting.nl/api/documentation" diff --git a/providers/dns/hostingnl/hostingnl_test.go b/providers/dns/hostingnl/hostingnl_test.go new file mode 100644 index 000000000..cef754c7c --- /dev/null +++ b/providers/dns/hostingnl/hostingnl_test.go @@ -0,0 +1,167 @@ +package hostingnl + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "key", + }, + }, + { + desc: "missing API key", + envVars: map[string]string{}, + expected: "hostingnl: some credentials information are missing: HOSTINGNL_API_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 + apiKey string + expected string + }{ + { + desc: "success", + apiKey: "key", + }, + { + desc: "missing API key", + expected: "hostingnl: APIKey is missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + provider.client.BaseURL, _ = url.Parse(server.URL) + + return provider, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("API-TOKEN", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /domains/example.com/dns", + servermock.ResponseFromInternal("add_record.json"), + servermock.CheckQueryParameter().Strict(), + servermock.CheckRequestJSONBodyFromInternal("add_record-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /domains/example.com/dns", + servermock.ResponseFromInternal("delete_record.json"), + servermock.CheckQueryParameter().Strict(), + servermock.CheckRequestJSONBodyFromInternal("delete_record-request.json")). + Build(t) + + provider.recordIDs["abc"] = "12345" + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/hostingnl/internal/client.go b/providers/dns/hostingnl/internal/client.go new file mode 100644 index 000000000..f2d7b5346 --- /dev/null +++ b/providers/dns/hostingnl/internal/client.go @@ -0,0 +1,144 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.hosting.nl" + +type Client struct { + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +func NewClient(apiKey string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +func (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) { + endpoint := c.BaseURL.JoinPath("domains", domain, "dns") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, []Record{record}) + if err != nil { + return nil, err + } + + var result APIResponse[Record] + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + if len(result.Data) != 1 { + return nil, fmt.Errorf("unexpected response data: %v", result.Data) + } + + return &result.Data[0], nil +} + +func (c Client) DeleteRecord(ctx context.Context, domain, recordID string) error { + endpoint := c.BaseURL.JoinPath("domains", domain, "dns") + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{{ID: recordID}}) + if err != nil { + return err + } + + var result APIResponse[Record] + + err = c.do(req, &result) + if err != nil { + return err + } + + return nil +} + +func (c Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set("API-TOKEN", c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var apiErr APIError + + err := json.Unmarshal(raw, &apiErr) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr) +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/hostingnl/internal/client_test.go b/providers/dns/hostingnl/internal/client_test.go new file mode 100644 index 000000000..efdb98980 --- /dev/null +++ b/providers/dns/hostingnl/internal/client_test.go @@ -0,0 +1,92 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder( + func(server *httptest.Server) (*Client, error) { + client := NewClient("secret") + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("API-TOKEN", "secret"), + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /domains/example.com/dns", + servermock.ResponseFromFixture("add_record.json"), + servermock.CheckQueryParameter().Strict(), + servermock.CheckRequestJSONBodyFromFixture("add_record-request.json")). + Build(t) + + record := Record{ + Name: "_acme-challenge.example.com", + Type: "TXT", + Content: strconv.Quote("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + TTL: 120, + } + + newRecord, err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) + + expected := &Record{ + ID: "12345", + Name: "_acme-challenge.example.com", + Type: "TXT", + Content: strconv.Quote("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + TTL: 120, + } + + assert.Equal(t, expected, newRecord) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /domains/example.com/dns", + servermock.ResponseFromFixture("delete_record.json"), + servermock.CheckQueryParameter().Strict(), + servermock.CheckRequestJSONBodyFromFixture("delete_record-request.json")). + Build(t) + + err := client.DeleteRecord(context.Background(), "example.com", "12345") + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := mockBuilder(). + Route("DELETE /domains/example.com/dns", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + err := client.DeleteRecord(context.Background(), "example.com", "12345") + require.EqualError(t, err, "[status code: 401] Something went wrong") +} + +func TestClient_DeleteRecord_error_other(t *testing.T) { + client := mockBuilder(). + Route("DELETE /domains/example.com/dns", + servermock.ResponseFromFixture("error_other.json"). + WithStatusCode(http.StatusNotFound)). + Build(t) + + err := client.DeleteRecord(context.Background(), "example.com", "12345") + require.EqualError(t, err, "[status code: 404] Resource not found") +} diff --git a/providers/dns/hostingnl/internal/fixtures/add_record-request.json b/providers/dns/hostingnl/internal/fixtures/add_record-request.json new file mode 100644 index 000000000..6b68ec3c6 --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/add_record-request.json @@ -0,0 +1,8 @@ +[ + { + "name": "_acme-challenge.example.com", + "type": "TXT", + "content": "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"", + "ttl": 120 + } +] diff --git a/providers/dns/hostingnl/internal/fixtures/add_record.json b/providers/dns/hostingnl/internal/fixtures/add_record.json new file mode 100644 index 000000000..a822a4f8d --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/add_record.json @@ -0,0 +1,13 @@ +{ + "success": true, + "data": [ + { + "id": "12345", + "type": "TXT", + "content": "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"", + "name": "_acme-challenge.example.com", + "prio": 0, + "ttl": 120 + } + ] +} diff --git a/providers/dns/hostingnl/internal/fixtures/delete_record-request.json b/providers/dns/hostingnl/internal/fixtures/delete_record-request.json new file mode 100644 index 000000000..cfc26d2b9 --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/delete_record-request.json @@ -0,0 +1,5 @@ +[ + { + "id": "12345" + } +] diff --git a/providers/dns/hostingnl/internal/fixtures/delete_record.json b/providers/dns/hostingnl/internal/fixtures/delete_record.json new file mode 100644 index 000000000..c041c1f6d --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/delete_record.json @@ -0,0 +1,8 @@ +{ + "success": true, + "data": [ + { + "id": "12345" + } + ] +} diff --git a/providers/dns/hostingnl/internal/fixtures/error.json b/providers/dns/hostingnl/internal/fixtures/error.json new file mode 100644 index 000000000..170587246 --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/error.json @@ -0,0 +1,5 @@ +{ + "errors": { + "message": "Something went wrong" + } +} diff --git a/providers/dns/hostingnl/internal/fixtures/error_other.json b/providers/dns/hostingnl/internal/fixtures/error_other.json new file mode 100644 index 000000000..ca7ecab28 --- /dev/null +++ b/providers/dns/hostingnl/internal/fixtures/error_other.json @@ -0,0 +1,3 @@ +{ + "error": "Resource not found" +} diff --git a/providers/dns/hostingnl/internal/types.go b/providers/dns/hostingnl/internal/types.go new file mode 100644 index 000000000..f324665fe --- /dev/null +++ b/providers/dns/hostingnl/internal/types.go @@ -0,0 +1,36 @@ +package internal + +type Record struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` + Priority int `json:"prio,omitempty"` +} + +type APIResponse[T any] struct { + Success bool `json:"success"` + Data []T `json:"data"` +} + +type APIError struct { + ErrorMsg string `json:"error"` + Errors Error `json:"errors"` +} + +func (e APIError) Error() string { + if e.ErrorMsg != "" { + return e.ErrorMsg + } + + return e.Errors.Error() +} + +type Error struct { + Message string `json:"message"` +} + +func (e Error) Error() string { + return e.Message +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 2b1b14b24..3b7b8c5fc 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -77,6 +77,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/hetzner" "github.com/go-acme/lego/v4/providers/dns/hostingde" "github.com/go-acme/lego/v4/providers/dns/hostinger" + "github.com/go-acme/lego/v4/providers/dns/hostingnl" "github.com/go-acme/lego/v4/providers/dns/hosttech" "github.com/go-acme/lego/v4/providers/dns/httpnet" "github.com/go-acme/lego/v4/providers/dns/httpreq" @@ -327,6 +328,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return hostingde.NewDNSProvider() case "hostinger": return hostinger.NewDNSProvider() + case "hostingnl": + return hostingnl.NewDNSProvider() case "hosttech": return hosttech.NewDNSProvider() case "httpnet": From a6e6b92d35eaf8f2f9dc49311d24331a036ce65c Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 10 Dec 2025 19:07:25 +0100 Subject: [PATCH 36/95] chore: clean maps --- providers/dns/aliesa/aliesa.go | 4 ++++ providers/dns/allinkl/allinkl.go | 4 ++++ providers/dns/auroradns/auroradns.go | 5 +++-- providers/dns/azion/azion.go | 22 ++------------------ providers/dns/binarylane/binarylane.go | 4 ++++ providers/dns/checkdomain/internal/client.go | 6 +++--- providers/dns/cloudru/cloudru.go | 5 +++-- providers/dns/designate/designate.go | 5 +++-- providers/dns/easydns/easydns.go | 10 ++++----- providers/dns/edgeone/edgeone.go | 11 ++++++---- providers/dns/gravity/gravity.go | 4 ++++ providers/dns/hosttech/hosttech.go | 4 ++++ providers/dns/huaweicloud/huaweicloud.go | 4 ++++ providers/dns/internal/hostingde/provider.go | 10 ++++----- providers/dns/limacity/limacity.go | 4 ++++ providers/dns/liquidweb/liquidweb.go | 5 +++-- providers/dns/luadns/luadns.go | 7 +++---- providers/dns/metaname/metaname.go | 4 ++++ providers/dns/mittwald/mittwald.go | 4 ++++ providers/dns/nodion/nodion.go | 4 ++++ providers/dns/octenium/octenium.go | 4 ++++ providers/dns/ovh/ovh.go | 5 +++-- providers/dns/plesk/plesk.go | 4 ++++ providers/dns/porkbun/porkbun.go | 4 ++++ providers/dns/selfhostde/selfhostde.go | 4 ++++ providers/dns/shellrent/shellrent.go | 4 ++++ providers/dns/syse/syse.go | 4 ++++ providers/dns/variomedia/variomedia.go | 4 ++++ providers/dns/volcengine/volcengine.go | 4 ++++ 29 files changed, 111 insertions(+), 52 deletions(-) diff --git a/providers/dns/aliesa/aliesa.go b/providers/dns/aliesa/aliesa.go index deb8162da..2a38389be 100644 --- a/providers/dns/aliesa/aliesa.go +++ b/providers/dns/aliesa/aliesa.go @@ -212,6 +212,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("aliesa: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index a5b27ff59..7e8f5ab4e 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -186,5 +186,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("allinkl: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/auroradns/auroradns.go b/providers/dns/auroradns/auroradns.go index 95d6ab759..50d2fbc25 100644 --- a/providers/dns/auroradns/auroradns.go +++ b/providers/dns/auroradns/auroradns.go @@ -53,10 +53,11 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { + config *Config + client *auroradns.Client + recordIDs map[string]string recordIDsMu sync.Mutex - config *Config - client *auroradns.Client } // NewDNSProvider returns a DNSProvider instance configured for AuroraDNS. diff --git a/providers/dns/azion/azion.go b/providers/dns/azion/azion.go index 8150d90d5..5584ece0b 100644 --- a/providers/dns/azion/azion.go +++ b/providers/dns/azion/azion.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "sync" "time" "github.com/aziontech/azionapi-go-sdk/idns" @@ -56,9 +55,6 @@ func NewDefaultConfig() *Config { type DNSProvider struct { config *Config client *idns.APIClient - - recordIDs map[string]int32 - recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Azion. @@ -98,9 +94,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := idns.NewAPIClient(clientConfig) return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int32), + config: config, + client: client, }, nil } @@ -161,12 +156,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return errors.New("azion: create zone record error") } - results := resp.GetResults() - - d.recordIDsMu.Lock() - d.recordIDs[token] = results.GetId() - d.recordIDsMu.Unlock() - return nil } @@ -186,13 +175,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("azion: %w", err) } - defer func() { - // Cleans the record ID. - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - }() - existingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain) if err != nil { return fmt.Errorf("azion: find existing record: %w", err) diff --git a/providers/dns/binarylane/binarylane.go b/providers/dns/binarylane/binarylane.go index 9ff80d698..5bbb7a16a 100644 --- a/providers/dns/binarylane/binarylane.go +++ b/providers/dns/binarylane/binarylane.go @@ -151,6 +151,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("binarylane: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/checkdomain/internal/client.go b/providers/dns/checkdomain/internal/client.go index d626275ab..68d090755 100644 --- a/providers/dns/checkdomain/internal/client.go +++ b/providers/dns/checkdomain/internal/client.go @@ -36,11 +36,11 @@ const maxInt = int((^uint(0)) >> 1) // Client the Autodns API client. type Client struct { - domainIDMapping map[string]int - domainIDMu sync.Mutex - BaseURL *url.URL httpClient *http.Client + + domainIDMapping map[string]int + domainIDMu sync.Mutex } // NewClient creates a new Client. diff --git a/providers/dns/cloudru/cloudru.go b/providers/dns/cloudru/cloudru.go index 287c12045..dd597952a 100644 --- a/providers/dns/cloudru/cloudru.go +++ b/providers/dns/cloudru/cloudru.go @@ -61,8 +61,9 @@ func NewDefaultConfig() *Config { } type DNSProvider struct { - config *Config - client *internal.Client + config *Config + client *internal.Client + records map[string]*internal.Record recordsMu sync.Mutex } diff --git a/providers/dns/designate/designate.go b/providers/dns/designate/designate.go index 47c8ad8f1..41bf251f6 100644 --- a/providers/dns/designate/designate.go +++ b/providers/dns/designate/designate.go @@ -68,8 +68,9 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *gophercloud.ServiceClient + config *Config + client *gophercloud.ServiceClient + dnsEntriesMu sync.Mutex } diff --git a/providers/dns/easydns/easydns.go b/providers/dns/easydns/easydns.go index ae0a0c3b8..205063e7b 100644 --- a/providers/dns/easydns/easydns.go +++ b/providers/dns/easydns/easydns.go @@ -190,16 +190,14 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID) - - d.recordIDsMu.Lock() - defer delete(d.recordIDs, key) - - d.recordIDsMu.Unlock() - if err != nil { return fmt.Errorf("easydns: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, key) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/edgeone/edgeone.go b/providers/dns/edgeone/edgeone.go index 509a75c77..6931c6715 100644 --- a/providers/dns/edgeone/edgeone.go +++ b/providers/dns/edgeone/edgeone.go @@ -119,10 +119,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } return &DNSProvider{ - config: config, - client: client, - recordIDs: map[string]*string{}, - recordIDsMu: sync.Mutex{}, + config: config, + client: client, + recordIDs: map[string]*string{}, }, nil } @@ -190,6 +189,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("edgeone: delete record failed: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/gravity/gravity.go b/providers/dns/gravity/gravity.go index c8594441a..b0bbb2fcb 100644 --- a/providers/dns/gravity/gravity.go +++ b/providers/dns/gravity/gravity.go @@ -163,6 +163,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("gravity: delete record: %w", err) } + d.recordsMu.Lock() + delete(d.records, token) + d.recordsMu.Unlock() + return nil } diff --git a/providers/dns/hosttech/hosttech.go b/providers/dns/hosttech/hosttech.go index fac64f054..73346f6cb 100644 --- a/providers/dns/hosttech/hosttech.go +++ b/providers/dns/hosttech/hosttech.go @@ -174,5 +174,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("hosttech: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/huaweicloud/huaweicloud.go b/providers/dns/huaweicloud/huaweicloud.go index 5a2773ab2..e47f3e2b5 100644 --- a/providers/dns/huaweicloud/huaweicloud.go +++ b/providers/dns/huaweicloud/huaweicloud.go @@ -209,6 +209,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("huaweicloud: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/internal/hostingde/provider.go b/providers/dns/internal/hostingde/provider.go index 644dc8aaf..b5277f042 100644 --- a/providers/dns/internal/hostingde/provider.go +++ b/providers/dns/internal/hostingde/provider.go @@ -165,16 +165,16 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { RecordsToDelete: rec, } - // Delete record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, info.EffectiveFQDN) - d.recordIDsMu.Unlock() - _, err = d.client.UpdateZone(ctx, req) if err != nil { return err } + // Delete record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, info.EffectiveFQDN) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/limacity/limacity.go b/providers/dns/limacity/limacity.go index 9e1f58f1a..3291faf66 100644 --- a/providers/dns/limacity/limacity.go +++ b/providers/dns/limacity/limacity.go @@ -193,6 +193,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("limacity: delete record (domain ID=%d, record ID=%d): %w", domainID, recordID, err) } + d.domainIDsMu.Lock() + delete(d.domainIDs, info.EffectiveFQDN) + d.domainIDsMu.Unlock() + return nil } diff --git a/providers/dns/liquidweb/liquidweb.go b/providers/dns/liquidweb/liquidweb.go index b56968fe3..6e93e2a12 100644 --- a/providers/dns/liquidweb/liquidweb.go +++ b/providers/dns/liquidweb/liquidweb.go @@ -62,8 +62,9 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *lw.API + config *Config + client *lw.API + recordIDs map[string]int recordIDsMu sync.Mutex } diff --git a/providers/dns/luadns/luadns.go b/providers/dns/luadns/luadns.go index 02108ce62..68b9c66b8 100644 --- a/providers/dns/luadns/luadns.go +++ b/providers/dns/luadns/luadns.go @@ -104,10 +104,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = clientdebug.Wrap(client.HTTPClient) return &DNSProvider{ - config: config, - client: client, - recordsMu: sync.Mutex{}, - records: make(map[string]*internal.DNSRecord), + config: config, + client: client, + records: make(map[string]*internal.DNSRecord), }, nil } diff --git a/providers/dns/metaname/metaname.go b/providers/dns/metaname/metaname.go index d5d87dc4d..d6e962024 100644 --- a/providers/dns/metaname/metaname.go +++ b/providers/dns/metaname/metaname.go @@ -153,6 +153,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("metaname: delete record: %w", err) } + d.recordsMu.Lock() + delete(d.records, token) + d.recordsMu.Unlock() + return nil } diff --git a/providers/dns/mittwald/mittwald.go b/providers/dns/mittwald/mittwald.go index 6292dd787..dcd882482 100644 --- a/providers/dns/mittwald/mittwald.go +++ b/providers/dns/mittwald/mittwald.go @@ -170,6 +170,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("mittwald: update/delete TXT record: %w", err) } + d.zoneIDsMu.Lock() + delete(d.zoneIDs, token) + d.zoneIDsMu.Unlock() + return nil } diff --git a/providers/dns/nodion/nodion.go b/providers/dns/nodion/nodion.go index e34d7db28..4bc887568 100644 --- a/providers/dns/nodion/nodion.go +++ b/providers/dns/nodion/nodion.go @@ -208,5 +208,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("regru: failed to remove TXT records [domain: %s]: %w", dns01.UnFqdn(authZone), err) } + d.zoneIDsMu.Lock() + delete(d.zoneIDs, token) + d.zoneIDsMu.Unlock() + return nil } diff --git a/providers/dns/octenium/octenium.go b/providers/dns/octenium/octenium.go index af469f5ed..6032dcce1 100644 --- a/providers/dns/octenium/octenium.go +++ b/providers/dns/octenium/octenium.go @@ -169,6 +169,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { break } + d.domainIDsMu.Lock() + delete(d.domainIDs, token) + d.domainIDsMu.Unlock() + return nil } diff --git a/providers/dns/ovh/ovh.go b/providers/dns/ovh/ovh.go index b7e522540..a8d12d819 100644 --- a/providers/dns/ovh/ovh.go +++ b/providers/dns/ovh/ovh.go @@ -102,8 +102,9 @@ func (c *Config) hasAppKeyAuth() bool { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *ovh.Client + config *Config + client *ovh.Client + recordIDs map[string]int64 recordIDsMu sync.Mutex } diff --git a/providers/dns/plesk/plesk.go b/providers/dns/plesk/plesk.go index b764dff33..5f07dcb50 100644 --- a/providers/dns/plesk/plesk.go +++ b/providers/dns/plesk/plesk.go @@ -173,5 +173,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("plesk: failed to delete record (%d): %w", recordID, err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/porkbun/porkbun.go b/providers/dns/porkbun/porkbun.go index dc9efb013..2f999ebcc 100644 --- a/providers/dns/porkbun/porkbun.go +++ b/providers/dns/porkbun/porkbun.go @@ -171,6 +171,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("porkbun: failed to delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/selfhostde/selfhostde.go b/providers/dns/selfhostde/selfhostde.go index bb475deea..035cd5363 100644 --- a/providers/dns/selfhostde/selfhostde.go +++ b/providers/dns/selfhostde/selfhostde.go @@ -186,5 +186,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("selfhostde: emptied DNS TXT record (id=%s): %w", recordID, err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/shellrent/shellrent.go b/providers/dns/shellrent/shellrent.go index 5a3a1f6de..0cd33e19a 100644 --- a/providers/dns/shellrent/shellrent.go +++ b/providers/dns/shellrent/shellrent.go @@ -172,6 +172,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("shellrent: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/syse/syse.go b/providers/dns/syse/syse.go index aab07131f..29633280c 100644 --- a/providers/dns/syse/syse.go +++ b/providers/dns/syse/syse.go @@ -172,6 +172,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("syse: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/variomedia/variomedia.go b/providers/dns/variomedia/variomedia.go index 90ac70a05..2d12fd975 100644 --- a/providers/dns/variomedia/variomedia.go +++ b/providers/dns/variomedia/variomedia.go @@ -180,6 +180,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("variomedia: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } diff --git a/providers/dns/volcengine/volcengine.go b/providers/dns/volcengine/volcengine.go index 9a5886e6d..765d38adb 100644 --- a/providers/dns/volcengine/volcengine.go +++ b/providers/dns/volcengine/volcengine.go @@ -171,6 +171,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("volcengine: delete record: %w", err) } + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + return nil } From bb5e70a4e5f682e2ef140dbe71665b539a62764e Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Mon, 15 Dec 2025 23:34:25 +0100 Subject: [PATCH 37/95] docs: improve contributing guide --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CONTRIBUTING.md | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ac9fd4975..795320a8d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,7 @@ IMPORTANT: 1. Create an issue and wait for a maintainer to approve it BEFORE opening a pull request. 2. Don't open a work-in-progress pull request. If you open a PR, the PR must be ready to be reviewed. -3. If a pull request doesn't follow the previous elements, it will close. +3. If a pull request doesn't follow one of the previous elements, it will be closed. Also, pull requests from a fork inside a GitHub organization are not allowed because of access limitation on them. Only pull requests from personal forks are allowed. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0005cff8..05e4fa994 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ To ensure a great and easy experience for everyone, please review the few guidel - If both of the above do not apply, create a new issue and include as much information as possible. Bug reports should include all information a person could need to reproduce your problem without the need to -follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour. +follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behavior and the actual behavior. ## Feature proposals and requests @@ -20,31 +20,26 @@ It is up to you to make a strong point about your proposal and convince us of th ## Pull requests +Create an issue and wait for a maintainer to approve it BEFORE opening a pull request. + Patches, new features and improvements are a great way to help the project. Please keep them focused on one thing and do not include unrelated commits. -All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests. +All pull requests that alter the behavior of the program, +add new behavior or somehow alter code in a non-trivial way should **always** include tests. -If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend -a lot of time on something the project's developers might not want to merge into the project. - -**IMPORTANT**: By submitting a patch, you agree to allow the project -owners to license your work under the terms of the [MIT License](LICENSE). +**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE). ### How to create a pull request Requirements: -- `go` v1.15+ +- `go` v1.24+ - environment variable: `GO111MODULE=on` First, you have to install [GoLang](https://golang.org/doc/install) and [golangci-lint](https://github.com/golangci/golangci-lint#install). ```bash -# Create the root folder -mkdir -p $GOPATH/src/github.com/go-acme -cd $GOPATH/src/github.com/go-acme - # clone your fork git clone git@github.com:YOUR_USERNAME/lego.git cd lego @@ -56,14 +51,12 @@ git fetch upstream ```bash # Create your branch -git checkout -b my-feature +git switch -c my-feature ## Create your code ## ``` ```bash -# Format -make fmt # Linters make checks # Tests From e21ba75da8da148f9b0cff3b298fdd63d151e51f Mon Sep 17 00:00:00 2001 From: Karl Fritsche Date: Mon, 15 Dec 2025 23:45:24 +0100 Subject: [PATCH 38/95] Add DNS provider for Ionos Cloud (#2752) Co-authored-by: Fernandez Ludovic --- README.md | 46 ++--- cmd/zz_gen_cmd_dnshelp.go | 21 ++ docs/content/dns/zz_gen_ionoscloud.md | 67 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/ionoscloud/internal/client.go | 172 ++++++++++++++++ .../dns/ionoscloud/internal/client_test.go | 134 +++++++++++++ .../fixtures/create_record-request.json | 8 + .../internal/fixtures/create_record.json | 25 +++ .../ionoscloud/internal/fixtures/error.json | 9 + .../ionoscloud/internal/fixtures/zones.json | 40 ++++ providers/dns/ionoscloud/internal/types.go | 97 +++++++++ providers/dns/ionoscloud/ionoscloud.go | 184 ++++++++++++++++++ providers/dns/ionoscloud/ionoscloud.toml | 22 +++ providers/dns/ionoscloud/ionoscloud_test.go | 173 ++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 15 files changed, 979 insertions(+), 24 deletions(-) create mode 100644 docs/content/dns/zz_gen_ionoscloud.md create mode 100644 providers/dns/ionoscloud/internal/client.go create mode 100644 providers/dns/ionoscloud/internal/client_test.go create mode 100644 providers/dns/ionoscloud/internal/fixtures/create_record-request.json create mode 100644 providers/dns/ionoscloud/internal/fixtures/create_record.json create mode 100644 providers/dns/ionoscloud/internal/fixtures/error.json create mode 100644 providers/dns/ionoscloud/internal/fixtures/zones.json create mode 100644 providers/dns/ionoscloud/internal/types.go create mode 100644 providers/dns/ionoscloud/ionoscloud.go create mode 100644 providers/dns/ionoscloud/ionoscloud.toml create mode 100644 providers/dns/ionoscloud/ionoscloud_test.go diff --git a/README.md b/README.md index 3b395aad9..ff9473e58 100644 --- a/README.md +++ b/README.md @@ -166,118 +166,118 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). INWX Ionos + Ionos Cloud IPv64 - iwantmyname (Deprecated) + iwantmyname (Deprecated) Joker Joohoi's ACME-DNS KeyHelp - Liara + Liara Lima-City Linode (v4) Liquid Web - Loopia + Loopia LuaDNS Mail-in-a-Box ManageEngine CloudDNS - Manual + Manual Metaname Metaregistrar mijn.host - Mittwald + Mittwald myaddr.{tools,dev,io} MyDNS.jp MythicBeasts - Name.com + Name.com Namecheap Namesilo NearlyFreeSpeech.NET - Neodigit + Neodigit Netcup Netlify Nicmanager - NIFCloud + NIFCloud Njalla Nodion NS1 - Octenium + Octenium Open Telekom Cloud Oracle Cloud OVH - plesk.com + plesk.com Porkbun PowerDNS Rackspace - Rain Yun/雨云 + Rain Yun/雨云 RcodeZero reg.ru Regfish - RFC2136 + RFC2136 RimuHosting RU CENTER Sakura Cloud - Scaleway + Scaleway Selectel Selectel v2 SelfHost.(de|eu) - Servercow + Servercow Shellrent Simply.com Sonic - Spaceship + Spaceship Stackpath Syse Technitium - Tencent Cloud DNS + Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TransIP - UKFast SafeDNS + UKFast SafeDNS Ultradns United-Domains Variomedia - VegaDNS + VegaDNS Vercel Versio.[nl|eu|uk] VinylDNS - Virtualname + Virtualname VK Cloud Volcano Engine/火山引擎 Vscale - Vultr + Vultr webnames.ca webnames.ru Websupport - WEDOS + WEDOS West.cn/西部数码 Yandex 360 Yandex Cloud - Yandex PDD + Yandex PDD Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 89b3ce7e1..e62c337ff 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -98,6 +98,7 @@ func allDNSCodes() string { "internetbs", "inwx", "ionos", + "ionoscloud", "ipv64", "iwantmyname", "joker", @@ -2043,6 +2044,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ionos`) + case "ionoscloud": + // generated from: providers/dns/ionoscloud/ionoscloud.toml + ew.writeln(`Configuration for Ionos Cloud.`) + ew.writeln(`Code: 'ionoscloud'`) + ew.writeln(`Since: 'v4.30.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "IONOSCLOUD_API_TOKEN": API token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "IONOSCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "IONOSCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "IONOSCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "IONOSCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/ionoscloud`) + case "ipv64": // generated from: providers/dns/ipv64/ipv64.toml ew.writeln(`Configuration for IPv64.`) diff --git a/docs/content/dns/zz_gen_ionoscloud.md b/docs/content/dns/zz_gen_ionoscloud.md new file mode 100644 index 000000000..9d33a95e5 --- /dev/null +++ b/docs/content/dns/zz_gen_ionoscloud.md @@ -0,0 +1,67 @@ +--- +title: "Ionos Cloud" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ionoscloud +dnsprovider: + since: "v4.30.0" + code: "ionoscloud" + url: "https://cloud.ionos.de/network/cloud-dns" +--- + + + + + + +Configuration for [Ionos Cloud](https://cloud.ionos.de/network/cloud-dns). + + + + +- Code: `ionoscloud` +- Since: v4.30.0 + + +Here is an example bash command using the Ionos Cloud provider: + +```bash +IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `IONOSCLOUD_API_TOKEN` | API token | + +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 | +|--------------------------------|-------------| +| `IONOSCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `IONOSCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `IONOSCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `IONOSCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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.ionos.com/docs/dns/v1/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 7962f0d73..fdb13f57a 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, 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, ipv64, 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, 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, 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/ionoscloud/internal/client.go b/providers/dns/ionoscloud/internal/client.go new file mode 100644 index 000000000..5b7d3a0fc --- /dev/null +++ b/providers/dns/ionoscloud/internal/client.go @@ -0,0 +1,172 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://dns.de-fra.ionos.com" + +const authorizationHeader = "Authorization" + +// Client the Ionos Cloud API client. +type Client struct { + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiKey string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// RetrieveZones returns a list of the DNS zones. +// https://api.ionos.com/docs/dns/v1/#tag/Zones/operation/zonesGet +func (c *Client) RetrieveZones(ctx context.Context, zoneName string) ([]Zone, error) { + endpoint := c.BaseURL.JoinPath("zones") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + query := req.URL.Query() + query.Add("filter.zoneName", zoneName) + req.URL.RawQuery = query.Encode() + + result := ZonesResponse{} + + if err := c.do(req, &result); err != nil { + return nil, err + } + + return result.Items, nil +} + +// CreateRecord creates a new record for the DNS zone. +// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsPost +func (c *Client) CreateRecord(ctx context.Context, zoneID string, record RecordProperties) (*RecordResponse, error) { + endpoint := c.BaseURL.JoinPath("zones", zoneID, "records") + + payload := map[string]RecordProperties{ + "properties": record, + } + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) + if err != nil { + return nil, err + } + + result := &RecordResponse{} + + if err := c.do(req, result); err != nil { + return nil, err + } + + return result, nil +} + +// DeleteRecord deletes a specified record from the DNS zone. +// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsDelete +func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error { + endpoint := c.BaseURL.JoinPath("zones", zoneID, "records", recordID) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set(authorizationHeader, "Bearer "+c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/ionoscloud/internal/client_test.go b/providers/dns/ionoscloud/internal/client_test.go new file mode 100644 index 000000000..dc478cc64 --- /dev/null +++ b/providers/dns/ionoscloud/internal/client_test.go @@ -0,0 +1,134 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithAuthorization("Bearer secret"), + ) +} + +func TestClient_RetrieveZones(t *testing.T) { + client := mockBuilder(). + Route("GET /zones", + servermock.ResponseFromFixture("zones.json"), + servermock.CheckQueryParameter().Strict(). + With("filter.zoneName", "example.com")). + Build(t) + + zones, err := client.RetrieveZones(t.Context(), "example.com") + require.NoError(t, err) + + expected := []Zone{{ + ID: "e74d0d15-f567-4b7b-9069-26ee1f93bae3", + Type: "zone", + Metadata: ZoneMetadata{ + CreatedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), + CreatedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + CreatedByUserID: "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + LastModifiedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), + LastModifiedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + LastModifiedByUserID: "63cef532-26fe-4a64-a4e0-de7c8a506c90", + ResourceURN: "ionos::::", + State: "PROVISIONING", + Nameservers: []string{"ns-ic.ui-dns.com", "ns-ic.ui-dns.de", "ns-ic.ui-dns.org", "ns-ic.ui-dns.biz"}, + }, + Properties: ZoneProperties{ + ZoneName: "example.com", + Description: "The hosted zone is used for example.com", + Enabled: true, + }, + }} + + assert.Equal(t, expected, zones) +} + +func TestClient_RetrieveZones_error(t *testing.T) { + client := mockBuilder(). + Route("GET /zones", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + _, err := client.RetrieveZones(t.Context(), "example.com") + require.EqualError(t, err, "401: paas-auth-1: Unauthorized, wrong or no api key provided to process this request") +} + +func TestClient_CreateRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /zones/abc/records", + servermock.ResponseFromFixture("create_record.json"), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). + Build(t) + + record := RecordProperties{ + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + } + + result, err := client.CreateRecord(t.Context(), "abc", record) + require.NoError(t, err) + + expected := &RecordResponse{ + ID: "90d81ac0-3a30-44d4-95a5-12959effa6ee", + Type: "record", + Metadata: RecordMetadata{ + CreatedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), + CreatedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + CreatedByUserID: "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + LastModifiedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), + LastModifiedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + LastModifiedByUserID: "63cef532-26fe-4a64-a4e0-de7c8a506c90", + ResourceURN: "ionos::::", + State: "PROVISIONING", + Fqdn: "app.example.com", + ZoneID: "a363f30c-4c0c-4552-9a07-298d87f219bf", + }, + Properties: RecordProperties{ + Name: "app", + Type: "A", + Content: "1.2.3.4", + TTL: 3600, + Priority: 3600, + Enabled: true, + }, + } + + assert.Equal(t, expected, result) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /zones/abc/records/def", + servermock.Noop(). + WithStatusCode(http.StatusAccepted)). + Build(t) + + err := client.DeleteRecord(t.Context(), "abc", "def") + require.NoError(t, err) +} diff --git a/providers/dns/ionoscloud/internal/fixtures/create_record-request.json b/providers/dns/ionoscloud/internal/fixtures/create_record-request.json new file mode 100644 index 000000000..d4f52bba8 --- /dev/null +++ b/providers/dns/ionoscloud/internal/fixtures/create_record-request.json @@ -0,0 +1,8 @@ +{ + "properties": { + "name": "_acme-challenge", + "type": "TXT", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120 + } +} diff --git a/providers/dns/ionoscloud/internal/fixtures/create_record.json b/providers/dns/ionoscloud/internal/fixtures/create_record.json new file mode 100644 index 000000000..d3094c3b2 --- /dev/null +++ b/providers/dns/ionoscloud/internal/fixtures/create_record.json @@ -0,0 +1,25 @@ +{ + "id": "90d81ac0-3a30-44d4-95a5-12959effa6ee", + "type": "record", + "href": "", + "metadata": { + "createdDate": "2022-08-21T15:52:53Z", + "createdBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "createdByUserId": "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "lastModifiedDate": "2022-08-21T15:52:53Z", + "lastModifiedBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "lastModifiedByUserId": "63cef532-26fe-4a64-a4e0-de7c8a506c90", + "resourceURN": "ionos::::", + "state": "PROVISIONING", + "fqdn": "app.example.com", + "zoneId": "a363f30c-4c0c-4552-9a07-298d87f219bf" + }, + "properties": { + "name": "app", + "type": "A", + "content": "1.2.3.4", + "ttl": 3600, + "priority": 3600, + "enabled": true + } +} diff --git a/providers/dns/ionoscloud/internal/fixtures/error.json b/providers/dns/ionoscloud/internal/fixtures/error.json new file mode 100644 index 000000000..bed0e5efb --- /dev/null +++ b/providers/dns/ionoscloud/internal/fixtures/error.json @@ -0,0 +1,9 @@ +{ + "httpStatus": 401, + "messages": [ + { + "errorCode": "paas-auth-1", + "message": "Unauthorized, wrong or no api key provided to process this request" + } + ] +} diff --git a/providers/dns/ionoscloud/internal/fixtures/zones.json b/providers/dns/ionoscloud/internal/fixtures/zones.json new file mode 100644 index 000000000..c9c2c62f9 --- /dev/null +++ b/providers/dns/ionoscloud/internal/fixtures/zones.json @@ -0,0 +1,40 @@ +{ + "id": "e74d0d15-f567-4b7b-9069-26ee1f93bae3", + "type": "collection", + "href": "", + "offset": 0, + "limit": 1000, + "_links": { + "prev": "http://PREVIOUS-PAGE-URI", + "self": "http://THIS-PAGE-URI", + "next": "http://NEXT-PAGE-URI" + }, + "items": [ + { + "id": "e74d0d15-f567-4b7b-9069-26ee1f93bae3", + "type": "zone", + "href": "", + "metadata": { + "createdDate": "2022-08-21T15:52:53Z", + "createdBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "createdByUserId": "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "lastModifiedDate": "2022-08-21T15:52:53Z", + "lastModifiedBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", + "lastModifiedByUserId": "63cef532-26fe-4a64-a4e0-de7c8a506c90", + "resourceURN": "ionos::::", + "state": "PROVISIONING", + "nameservers": [ + "ns-ic.ui-dns.com", + "ns-ic.ui-dns.de", + "ns-ic.ui-dns.org", + "ns-ic.ui-dns.biz" + ] + }, + "properties": { + "zoneName": "example.com", + "description": "The hosted zone is used for example.com", + "enabled": true + } + } + ] +} diff --git a/providers/dns/ionoscloud/internal/types.go b/providers/dns/ionoscloud/internal/types.go new file mode 100644 index 000000000..49348f4d1 --- /dev/null +++ b/providers/dns/ionoscloud/internal/types.go @@ -0,0 +1,97 @@ +package internal + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +type APIError struct { + HTTPStatus int `json:"httpStatus"` + Messages []ErrorMessage `json:"messages"` +} + +func (a *APIError) Error() string { + var msg strings.Builder + + msg.WriteString(strconv.Itoa(a.HTTPStatus)) + + for _, m := range a.Messages { + msg.WriteString(": ") + msg.WriteString(m.String()) + } + + return msg.String() +} + +type ErrorMessage struct { + ErrorCode string `json:"errorCode"` + Message string `json:"message"` +} + +func (e ErrorMessage) String() string { + return fmt.Sprintf("%s: %s", e.ErrorCode, e.Message) +} + +type ZonesResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Items []Zone `json:"items"` +} + +type Zone struct { + ID string `json:"id"` + Type string `json:"type"` + Metadata ZoneMetadata `json:"metadata"` + Properties ZoneProperties `json:"properties"` +} + +type ZoneMetadata struct { + CreatedDate time.Time `json:"createdDate"` + CreatedBy string `json:"createdBy"` + CreatedByUserID string `json:"createdByUserId"` + LastModifiedDate time.Time `json:"lastModifiedDate"` + LastModifiedBy string `json:"lastModifiedBy"` + LastModifiedByUserID string `json:"lastModifiedByUserId"` + ResourceURN string `json:"resourceURN"` + State string `json:"state"` + Nameservers []string `json:"nameservers"` +} + +type ZoneProperties struct { + ZoneName string `json:"zoneName"` + Description string `json:"description"` + Enabled bool `json:"enabled"` +} + +type RecordResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Metadata RecordMetadata `json:"metadata"` + Properties RecordProperties `json:"properties"` +} + +type RecordMetadata struct { + CreatedDate time.Time `json:"createdDate"` + CreatedBy string `json:"createdBy"` + CreatedByUserID string `json:"createdByUserId"` + LastModifiedDate time.Time `json:"lastModifiedDate"` + LastModifiedBy string `json:"lastModifiedBy"` + LastModifiedByUserID string `json:"lastModifiedByUserId"` + ResourceURN string `json:"resourceURN"` + State string `json:"state"` + Fqdn string `json:"fqdn"` + ZoneID string `json:"zoneId"` +} + +type RecordProperties struct { + Name string `json:"name"` + Type string `json:"type,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` + Priority int `json:"priority,omitempty"` + Enabled bool `json:"enabled,omitempty"` +} diff --git a/providers/dns/ionoscloud/ionoscloud.go b/providers/dns/ionoscloud/ionoscloud.go new file mode 100644 index 000000000..0c33fba9f --- /dev/null +++ b/providers/dns/ionoscloud/ionoscloud.go @@ -0,0 +1,184 @@ +// Package ionoscloud implements a DNS provider for solving the DNS-01 challenge using Ionos Cloud. +package ionoscloud + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "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/ionoscloud/internal" +) + +// Environment variables names. +const ( + envNamespace = "IONOSCLOUD_" + + EnvAPIToken = envNamespace + "API_TOKEN" + + 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 { + APIToken string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + zoneIDs map[string]string + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Ionos Cloud. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIToken) + if err != nil { + return nil, fmt.Errorf("ionoscloud: %w", err) + } + + config := NewDefaultConfig() + config.APIToken = values[EnvAPIToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Ionos Cloud. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ionoscloud: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIToken) + if err != nil { + return nil, fmt.Errorf("ionoscloud: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + zoneIDs: make(map[string]string), + recordIDs: make(map[string]string), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ionoscloud: could not find zone for domain %q: %w", domain, err) + } + + zones, err := d.client.RetrieveZones(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("ionoscloud: retrieve zones: %w", err) + } + + if len(zones) != 1 { + return fmt.Errorf("ionoscloud: zone ID not found for domain %q", domain) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("ionoscloud: %w", err) + } + + zoneID := zones[0].ID + + request := internal.RecordProperties{ + Name: subDomain, + Type: "TXT", + Content: info.Value, + TTL: d.config.TTL, + } + + record, err := d.client.CreateRecord(ctx, zoneID, request) + if err != nil { + return fmt.Errorf("ionoscloud: create record: %w", err) + } + + d.recordIDsMu.Lock() + d.zoneIDs[token] = zoneID + d.recordIDs[token] = record.ID + 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) + + d.recordIDsMu.Lock() + zoneID, ok := d.zoneIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("ionoscloud: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("ionoscloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteRecord(context.Background(), zoneID, recordID) + if err != nil { + return fmt.Errorf("ionoscloud: delete record: %w", err) + } + + d.recordIDsMu.Lock() + delete(d.zoneIDs, token) + 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/ionoscloud/ionoscloud.toml b/providers/dns/ionoscloud/ionoscloud.toml new file mode 100644 index 000000000..a8fedce6c --- /dev/null +++ b/providers/dns/ionoscloud/ionoscloud.toml @@ -0,0 +1,22 @@ +Name = "Ionos Cloud" +Description = '''''' +URL = "https://cloud.ionos.de/network/cloud-dns" +Code = "ionoscloud" +Since = "v4.30.0" + +Example = ''' +IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + IONOSCLOUD_API_TOKEN = "API token" + [Configuration.Additional] + IONOSCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + IONOSCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + IONOSCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + IONOSCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://api.ionos.com/docs/dns/v1/" diff --git a/providers/dns/ionoscloud/ionoscloud_test.go b/providers/dns/ionoscloud/ionoscloud_test.go new file mode 100644 index 000000000..8282e08fc --- /dev/null +++ b/providers/dns/ionoscloud/ionoscloud_test.go @@ -0,0 +1,173 @@ +package ionoscloud + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIToken: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ionoscloud: some credentials information are missing: IONOSCLOUD_API_TOKEN", + }, + } + + 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 + apiToken string + expected string + }{ + { + desc: "success", + apiToken: "secret", + }, + { + desc: "missing credentials", + expected: "ionoscloud: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIToken = test.apiToken + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIToken = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithAuthorization("Bearer secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /zones", + servermock.ResponseFromInternal("zones.json"), + servermock.CheckQueryParameter().Strict(). + With("filter.zoneName", "example.com")). + Route("POST /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records", + servermock.ResponseFromInternal("create_record.json"), + servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records/90d81ac0-3a30-44d4-95a5-12959effa6ee", + servermock.Noop(). + WithStatusCode(http.StatusAccepted)). + Build(t) + + token := "abc" + + provider.zoneIDs[token] = "e74d0d15-f567-4b7b-9069-26ee1f93bae3" + provider.recordIDs[token] = "90d81ac0-3a30-44d4-95a5-12959effa6ee" + + err := provider.CleanUp("example.com", token, "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 3b7b8c5fc..1270e0f9d 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -92,6 +92,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internetbs" "github.com/go-acme/lego/v4/providers/dns/inwx" "github.com/go-acme/lego/v4/providers/dns/ionos" + "github.com/go-acme/lego/v4/providers/dns/ionoscloud" "github.com/go-acme/lego/v4/providers/dns/ipv64" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" "github.com/go-acme/lego/v4/providers/dns/joker" @@ -358,6 +359,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return inwx.NewDNSProvider() case "ionos": return ionos.NewDNSProvider() + case "ionoscloud": + return ionoscloud.NewDNSProvider() case "ipv64": return ipv64.NewDNSProvider() case "iwantmyname": From 5b30df22b57dc9ae15fab96c8be5ef1d73b978e3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 16 Dec 2025 17:20:51 +0100 Subject: [PATCH 39/95] chore: update dependencies (#2753) --- go.mod | 115 ++++++++--------- go.sum | 244 +++++++++++++++++------------------ providers/dns/vultr/vultr.go | 2 +- 3 files changed, 178 insertions(+), 183 deletions(-) diff --git a/go.mod b/go.mod index cd019fac3..9def9b0eb 100644 --- a/go.mod +++ b/go.mod @@ -16,22 +16,22 @@ require ( github.com/BurntSushi/toml v1.5.0 github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 - github.com/alibabacloud-go/tea v1.3.13 - github.com/aliyun/credentials-go v1.4.7 - github.com/aws/aws-sdk-go-v2 v1.40.0 - github.com/aws/aws-sdk-go-v2/config v1.32.2 - github.com/aws/aws-sdk-go-v2/credentials v1.19.2 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 - github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 - github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 + github.com/alibabacloud-go/tea v1.3.14 + github.com/aliyun/credentials-go v1.4.10 + github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2/config v1.32.5 + github.com/aws/aws-sdk-go-v2/credentials v1.19.5 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 github.com/aziontech/azionapi-go-sdk v0.144.0 - github.com/baidubce/bce-sdk-go v0.9.252 + github.com/baidubce/bce-sdk-go v0.9.254 github.com/cenkalti/backoff/v5 v5.0.3 github.com/dnsimple/dnsimple-go/v4 v4.0.0 github.com/exoscale/egoscale/v3 v3.1.31 github.com/go-acme/alidns-20150109/v4 v4.7.0 - github.com/go-acme/esa-20240910/v2 v2.40.1 + github.com/go-acme/esa-20240910/v2 v2.40.3 github.com/go-acme/tencentclouddnspod v1.1.25 github.com/go-acme/tencentedgdeone v1.1.48 github.com/go-jose/go-jose/v4 v4.1.3 @@ -42,16 +42,16 @@ require ( github.com/gophercloud/gophercloud v1.14.1 github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/go-version v1.7.0 - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 + github.com/hashicorp/go-version v1.8.0 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/ldez/grignotin v0.10.1 - github.com/linode/linodego v1.61.0 + github.com/linode/linodego v1.62.0 github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.20 - github.com/miekg/dns v1.1.68 + github.com/miekg/dns v1.1.69 github.com/mimuret/golang-iij-dpf v0.9.1 github.com/namedotcom/go/v4 v4.0.2 github.com/nrdcg/auroradns v1.1.0 @@ -64,8 +64,8 @@ require ( github.com/nrdcg/mailinabox v0.3.0 github.com/nrdcg/namesilo v0.5.0 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 + github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1 + github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 github.com/nrdcg/porkbun v0.4.0 github.com/nrdcg/vegadns v0.3.0 github.com/nzdjb/go-metaname v1.0.0 @@ -74,29 +74,29 @@ require ( github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 github.com/regfish/regfish-dnsapi-go v0.1.1 github.com/sacloud/api-client-go v0.3.3 - github.com/sacloud/iaas-api-go v1.22.0 + github.com/sacloud/iaas-api-go v1.23.1 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 github.com/selectel/domains-go v1.1.0 github.com/selectel/go-selvpcclient/v4 v4.1.0 github.com/softlayer/softlayer-go v1.2.1 github.com/stretchr/testify v1.11.1 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12 github.com/transip/gotransip/v6 v6.26.1 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 github.com/urfave/cli/v2 v2.27.7 - github.com/vinyldns/go-vinyldns v0.9.16 - github.com/volcengine/volc-sdk-golang v1.0.229 - github.com/vultr/govultr/v3 v3.25.0 - github.com/yandex-cloud/go-genproto v0.38.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.20 - github.com/yandex-cloud/go-sdk/v2 v2.28.0 - golang.org/x/crypto v0.45.0 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.33.0 - golang.org/x/text v0.31.0 + github.com/vinyldns/go-vinyldns v0.9.17 + github.com/volcengine/volc-sdk-golang v1.0.230 + github.com/vultr/govultr/v3 v3.26.0 + github.com/yandex-cloud/go-genproto v0.41.0 + github.com/yandex-cloud/go-sdk/services/dns v0.0.23 + github.com/yandex-cloud/go-sdk/v2 v2.33.0 + golang.org/x/crypto v0.46.0 + golang.org/x/net v0.48.0 + golang.org/x/oauth2 v0.34.0 + golang.org/x/text v0.32.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.256.0 - gopkg.in/ns1/ns1-go.v2 v2.15.2 + google.golang.org/api v0.257.0 + gopkg.in/ns1/ns1-go.v2 v2.16.0 gopkg.in/yaml.v2 v2.4.0 software.sslmate.com/src/go-pkcs12 v0.6.0 ) @@ -117,20 +117,20 @@ require ( github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect - github.com/aws/smithy-go v1.23.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -151,7 +151,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-resty/resty/v2 v2.17.0 // indirect github.com/goccy/go-yaml v1.9.8 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -183,12 +183,11 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sacloud/go-http v0.1.9 // indirect - github.com/sacloud/packages-go v0.0.11 // indirect + github.com/sacloud/packages-go v0.0.12 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/sony/gobreaker v1.0.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -202,23 +201,23 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index af9e95e02..05aecdb9a 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,9 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= -github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94= github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c= +github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= @@ -154,8 +155,8 @@ github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6q github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= -github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw= -github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/aliyun/credentials-go v1.4.10 h1:4PtFGTW6eMpKd8YUNL6yVh52c/3PZdEOklELEbn2ui8= +github.com/aliyun/credentials-go v1.4.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -168,54 +169,54 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= -github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= -github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk= -github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= +github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8 h1:jhwva7OKpYXrTQmCG4L7lF2FvB2irs1oRyGAwmQ4lmA= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.8/go.mod h1:x+omzRoqYYFX+H8/va+Gt2Yg4xGaHZMRowr77Y/UGIA= -github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0 h1:W3+0Cbc9awFBr9Yt7nFUkvB4N4e7vVIGtKD1qDttXn4= -github.com/aws/aws-sdk-go-v2/service/route53 v1.61.0/go.mod h1:Wa3q5R2uwIfIL3HZH+vG1/P9y7CjjfzTgcz5IWXlsZs= -github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg= -github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo= github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU= -github.com/baidubce/bce-sdk-go v0.9.252 h1:TONS/utgfEkDjvHllVZFBrTsjsNhk51rhWuj3ppcL4s= -github.com/baidubce/bce-sdk-go v0.9.252/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/baidubce/bce-sdk-go v0.9.254 h1:A7GtBOt7z2lnV7fqlZPZefhcBFg7z6iliUAhEOiIhoE= +github.com/baidubce/bce-sdk-go v0.9.254/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -314,8 +315,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= -github.com/go-acme/esa-20240910/v2 v2.40.1 h1:pog3UlF5d+3LPoo1L8u8PqB189recIXX8T7pGoEz18A= -github.com/go-acme/esa-20240910/v2 v2.40.1/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= +github.com/go-acme/esa-20240910/v2 v2.40.3 h1:xXOMRex148wKEHbv7Izn73/HdAxSmz5GOaF4HdnqN+M= +github.com/go-acme/esa-20240910/v2 v2.40.3/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s= github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE= github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk= @@ -356,8 +357,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0= +github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -515,8 +516,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -531,8 +532,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178 h1:eNVkjzdPMgM2qih9aECiFUI8S9zgpOwXxeBPAwQqtvU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.178/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 h1:uia+R3K1izQRGpxTV+bS4q3/ueMW7ProAMWqM6OlqOU= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -606,8 +607,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM= -github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI= +github.com/linode/linodego v1.62.0 h1:eCo1sepsIPGkI66Cz9IaCylWxKDD2aSc5UYq20iBMfw= +github.com/linode/linodego v1.62.0/go.mod h1:FoIEsuZMRlXiUt6RnuGcPTek5iAO3VfE6bjMpGlcQ2U= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -643,8 +644,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= -github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= +github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= @@ -705,10 +706,10 @@ github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 h1:bppmFqrJ87U4gWilemAW9oa4Qepf2JBTK/mPgaZLP2A= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 h1:IHPZs4Mo/lxyo+gYB+baheb2kGmHtNGQk2DKPDHqPjA= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0/go.mod h1:yELd0uJLiIyv9sGIh5ZRCHEB1B2QFNURWkQIMqb3ZwE= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1 h1:yHD01L6wN7mhGikS08izrMuEp9PRtvingePXkjRHrSg= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 h1:9ApYlc4bjup9WnxOFmgvh00bDqd6KMqAbAR4klKzluA= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1/go.mod h1:iOzhDeDcQGJZVgSDKrl5p3HUWexNo3LOlicf0D9ltgk= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU= @@ -816,10 +817,10 @@ github.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sq github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo= github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE= github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE= -github.com/sacloud/iaas-api-go v1.22.0 h1:nvLQNuxcfxILvoxA6WcnTjU9A8yv8dPI1OSYHAPxBJk= -github.com/sacloud/iaas-api-go v1.22.0/go.mod h1:PLcolyFlby/0ExZTOdUf9xzhkEMBuVzORadXDNN21no= -github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo= -github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8= +github.com/sacloud/iaas-api-go v1.23.1 h1:rjYG0vVoxWyETiwc7R8YdD7CIzs9vVNEOzu7w6dgGzc= +github.com/sacloud/iaas-api-go v1.23.1/go.mod h1:EGIHOWRB9azOv7HPCVM8WpOEl28WIV9TNRbnEVg+Q3U= +github.com/sacloud/packages-go v0.0.12 h1:MKeZNN3FQn1heqUSRBrbZw89YusZA1n4kammjMFZYvQ= +github.com/sacloud/packages-go v0.0.12/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -841,13 +842,8 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= -github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= github.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw= github.com/softlayer/softlayer-go v1.2.1/go.mod h1:Gz9/ktcmB7Z8EJlu+QEJJpkv8lAmnhYdB9Tc6gedjmo= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= @@ -905,8 +901,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 h1:r05ohLc0LVEpiEQeOJ5QwCiKk6XM9kjTca6+UAbNR/8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12 h1:/ABtv4x4FSGxGW0d6Sc88iQn6Up2LalWKwt/Tj7Dtz8= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= @@ -919,12 +915,12 @@ github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= -github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= -github.com/volcengine/volc-sdk-golang v1.0.229 h1:gOkDltTS6Fta8OyfYrbeY9bqCHHyiJuGYNJpR5MR+Fo= -github.com/volcengine/volc-sdk-golang v1.0.229/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.25.0 h1:rS8/Vdy8HlHArwmD4MtLY+hbbpYAbcnZueZrE6b0oUg= -github.com/vultr/govultr/v3 v3.25.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= +github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI= +github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4= +github.com/volcengine/volc-sdk-golang v1.0.230 h1:84/MOF0zUPtAHt3e1+MsFq5qrnQRC+e3XzTUwIOzZxw= +github.com/volcengine/volc-sdk-golang v1.0.230/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= +github.com/vultr/govultr/v3 v3.26.0 h1:pm/GM+RZo9T1JLQzrUti5HiNAIFZFEHcPFMOWGvvNIY= +github.com/vultr/govultr/v3 v3.26.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= @@ -933,12 +929,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.38.0 h1:uB3btG7mLOnu53ehYtRARCk04+80sBpxDrSkP3qC6G8= -github.com/yandex-cloud/go-genproto v0.38.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.20 h1:xHBRa+IIYpTgMbTbmZf7aEKIqrJMcZGIF8ea4XIyLX0= -github.com/yandex-cloud/go-sdk/services/dns v0.0.20/go.mod h1:8nYQULLJbbe51qdBY7Ay5v8wtDgdH7nHCMZs4XkwzLg= -github.com/yandex-cloud/go-sdk/v2 v2.28.0 h1:KDOrN75xokZBYbgjq6Pjvo+hEpu32xFhErtomLBML5s= -github.com/yandex-cloud/go-sdk/v2 v2.28.0/go.mod h1:6vmAhqoCVYSJEb5OuhHUqIdxDy2b9uUXp1e5sqMhTmo= +github.com/yandex-cloud/go-genproto v0.41.0 h1:l0HWC7S82XgfioqOQ+d2wx7PRB5Eo71KiUb4PiWbDXQ= +github.com/yandex-cloud/go-genproto v0.41.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk/services/dns v0.0.23 h1:fR4tqSRKTpzh4RczXJbU7EOXh4+kJnp+dccRpL2PLPQ= +github.com/yandex-cloud/go-sdk/services/dns v0.0.23/go.mod h1:Lgly3dyKBGrAIpIo6nrkEpQOoSQYlnik1HLKMeZcA98= +github.com/yandex-cloud/go-sdk/v2 v2.33.0 h1:wuvpirhYcHSejLDXSxLGsNoZHqkjrXevzVxw7SrrN/0= +github.com/yandex-cloud/go-sdk/v2 v2.33.0/go.mod h1:OqkwauVaBxbrrfN+JOYBIuE8GrHz1g0Z42VIkbsGvPI= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= @@ -962,22 +958,22 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1030,8 +1026,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1075,8 +1071,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1134,16 +1130,16 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1160,8 +1156,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1247,8 +1243,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1263,8 +1259,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1283,8 +1279,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1350,8 +1346,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1380,8 +1376,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1422,10 +1418,10 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1443,8 +1439,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1477,8 +1473,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.15.2 h1:aBVyKeEH3rBFWwX72xPPjEuRL4+Lp5P9GlAcrzu0Y5M= -gopkg.in/ns1/ns1-go.v2 v2.15.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.16.0 h1:mUczKFnrCystSV7yIODzVSbENoud3T7DwstmyVZfqg4= +gopkg.in/ns1/ns1-go.v2 v2.16.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/providers/dns/vultr/vultr.go b/providers/dns/vultr/vultr.go index 2064cee19..f97a321c1 100644 --- a/providers/dns/vultr/vultr.go +++ b/providers/dns/vultr/vultr.go @@ -107,7 +107,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("vultr: %w", err) } - req := govultr.DomainRecordReq{ + req := govultr.DomainRecordCreateReq{ Name: subDomain, Type: "TXT", Data: `"` + info.Value + `"`, From 4e6426cb2ed62ccdd6ef218792e0b8ce6010947c Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 16 Dec 2025 02:31:35 +0100 Subject: [PATCH 40/95] Prepare release v4.30.0 --- CHANGELOG.md | 24 ++++++++++++++++++- acme/api/internal/sender/useragent.go | 4 ++-- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b7909e5..64e62717d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,33 @@ # Changelog -lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ +lego is an independent, free, open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.30.0 + +- Release date: 2025-12-16 +- Tag: [v4.30.0](https://github.com/go-acme/lego/releases/tag/v4.30.0) + +### Added + +- **[dnsprovider]** Add DNS provider for Ionos Cloud +- **[dnsprovider]** Add DNS provider for Virtualname +- **[dnsprovider]** Add DNS Provider for Neodigit +- **[dnsprovider]** Add DNS provider for Syse.no +- **[dnsprovider]** Add DNS provider for Gravity +- **[dnsprovider]** Add DNS provider for hosting.nl + +### Changed + +- **[cli]** feat: remove email requirement + +### Fixed + +- **[dnsprovider]** autodns: use the right response structure + ## v4.29.0 - Release date: 2025-11-29 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 1c2078b38..c6028c4b0 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.29.0" + ourUserAgent = "xenolf-acme/4.30.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index e0fbb90e2..f63ded320 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.29.0+dev-detach" +const defaultVersion = "v4.30.0+dev-release" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index bbffcdbd4..04e513138 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.29.0" + ourUserAgent = "goacme-lego/4.30.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From 222cd85cbc0b61dba99f4f65caaec404f0c6682a Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 16 Dec 2025 02:32:19 +0100 Subject: [PATCH 41/95] Detach v4.30.0 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index c6028c4b0..f720f0d61 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index f63ded320..69a570785 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.30.0+dev-release" +const defaultVersion = "v4.30.0+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 04e513138..1d5d08e2b 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From 43dc1aa835a07b83a45b8c56ec1b9b8af934474b Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 16 Dec 2025 19:04:42 +0100 Subject: [PATCH 42/95] chore: fix attest-build-provenance subject-checksums path (#2755) --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ba055979..6a0d3b703 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,7 @@ jobs: # https://goreleaser.com/ci/actions/ - name: Run GoReleaser + id: goreleaser uses: goreleaser/goreleaser-action@v6 with: version: v2.13.0 @@ -78,7 +79,7 @@ jobs: - uses: actions/attest-build-provenance@v3 with: - subject-checksums: ./dist/lego_*_checksums.txt + subject-checksums: ./dist/lego_${{ fromJSON(steps.goreleaser.outputs.metadata).version }}_checksums.txt github-token: ${{ secrets.GH_TOKEN_REPO }} - uses: actions/attest-build-provenance@v3 with: From 5574de68cd9dc3364cb03cf9951f5f28c21881be Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 16 Dec 2025 19:33:30 +0100 Subject: [PATCH 43/95] fix: downgrade aliyun credentials to v1.4.7 (#2756) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9def9b0eb..9aaf50171 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 github.com/alibabacloud-go/tea v1.3.14 - github.com/aliyun/credentials-go v1.4.10 + github.com/aliyun/credentials-go v1.4.7 github.com/aws/aws-sdk-go-v2 v1.41.0 github.com/aws/aws-sdk-go-v2/config v1.32.5 github.com/aws/aws-sdk-go-v2/credentials v1.19.5 diff --git a/go.sum b/go.sum index 05aecdb9a..5e63fdba3 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,8 @@ github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6q github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= -github.com/aliyun/credentials-go v1.4.10 h1:4PtFGTW6eMpKd8YUNL6yVh52c/3PZdEOklELEbn2ui8= -github.com/aliyun/credentials-go v1.4.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw= +github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= From 27075d562add17f782773c44dfd10992e9be9de0 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 16 Dec 2025 19:25:40 +0100 Subject: [PATCH 44/95] Prepare release v4.30.1 --- CHANGELOG.md | 9 +++++++++ acme/api/internal/sender/useragent.go | 4 ++-- cmd/lego/zz_gen_version.go | 2 +- go.mod | 2 ++ providers/dns/internal/useragent/useragent.go | 4 ++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e62717d..a9974d550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.30.1 + +- Release date: 2025-12-16 +- Tag: [v4.30.1](https://github.com/go-acme/lego/releases/tag/v4.30.1) + +Due to an error related to `aliyun/credentials-go`, some artifacts of the v4.30.0 release have not been published. + +This release contains the same things as v4.30.0. + ## v4.30.0 - Release date: 2025-12-16 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index f720f0d61..e4ff20980 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.30.0" + ourUserAgent = "xenolf-acme/4.30.1" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index 69a570785..d5963a601 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.30.0+dev-detach" +const defaultVersion = "v4.30.1+dev-release" var version = "" diff --git a/go.mod b/go.mod index 9aaf50171..dd1a5b4b7 100644 --- a/go.mod +++ b/go.mod @@ -222,3 +222,5 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +retract v4.30.0 // Problem related to misuse of sycalls by aliyun/credentials-go diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 1d5d08e2b..3db3877ef 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.30.0" + ourUserAgent = "goacme-lego/4.30.1" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From 7af0efdf722e1b1abf3e783b0be72ecf93bf308c Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 16 Dec 2025 19:28:51 +0100 Subject: [PATCH 45/95] Detach v4.30.1 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index e4ff20980..25443fa05 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index d5963a601..da867f0cd 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.30.1+dev-release" +const defaultVersion = "v4.30.1+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 3db3877ef..4f8e693c2 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From a5cc0e155518e825ec2ec017610822f06aebb767 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 18 Dec 2025 14:29:52 +0100 Subject: [PATCH 46/95] feat: improve ACME error types (#2761) --- acme/api/internal/sender/sender.go | 69 +++++++++++++----------- acme/api/internal/sender/sender_test.go | 70 +++++++++++++++++++++++++ acme/errors.go | 24 ++++++++- 3 files changed, 131 insertions(+), 32 deletions(-) diff --git a/acme/api/internal/sender/sender.go b/acme/api/internal/sender/sender.go index d5db5d410..d8859edf4 100644 --- a/acme/api/internal/sender/sender.go +++ b/acme/api/internal/sender/sender.go @@ -120,39 +120,46 @@ func (d *Doer) formatUserAgent() string { } func checkError(req *http.Request, resp *http.Response) error { - if resp.StatusCode >= http.StatusBadRequest { - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) - } - - var errorDetails *acme.ProblemDetails - - err = json.Unmarshal(body, &errorDetails) - if err != nil { - return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) - } - - errorDetails.Method = req.Method - errorDetails.URL = req.URL.String() - - if errorDetails.HTTPStatus == 0 { - errorDetails.HTTPStatus = resp.StatusCode - } - - // Check for errors we handle specifically - if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { - return &acme.NonceError{ProblemDetails: errorDetails} - } - - if errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr { - return &acme.AlreadyReplacedError{ProblemDetails: errorDetails} - } - - return errorDetails + if resp.StatusCode < http.StatusBadRequest { + return nil } - return nil + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) + } + + var errorDetails *acme.ProblemDetails + + err = json.Unmarshal(body, &errorDetails) + if err != nil { + return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) + } + + errorDetails.Method = req.Method + errorDetails.URL = req.URL.String() + + if errorDetails.HTTPStatus == 0 { + errorDetails.HTTPStatus = resp.StatusCode + } + + // Check for errors we handle specifically + switch { + case errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr: + return &acme.NonceError{ProblemDetails: errorDetails} + + case errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr: + return &acme.AlreadyReplacedError{ProblemDetails: errorDetails} + + case errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr: + return &acme.RateLimitedError{ + ProblemDetails: errorDetails, + RetryAfter: resp.Header.Get("Retry-After"), + } + + default: + return errorDetails + } } type httpsOnly struct { diff --git a/acme/api/internal/sender/sender_test.go b/acme/api/internal/sender/sender_test.go index 1f25c6d26..73701ab11 100644 --- a/acme/api/internal/sender/sender_test.go +++ b/acme/api/internal/sender/sender_test.go @@ -1,11 +1,14 @@ package sender import ( + "bytes" + "io" "net/http" "net/http/httptest" "strings" "testing" + "github.com/go-acme/lego/v4/acme" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -78,3 +81,70 @@ func TestDo_failWithHTTP(t *testing.T) { _, err := sender.Post(server.URL, strings.NewReader("data"), "text/plain", nil) require.ErrorContains(t, err, "HTTPS is required: http://") } + +func Test_checkError(t *testing.T) { + testCases := []struct { + desc string + resp *http.Response + assert func(t *testing.T, err error) + }{ + { + desc: "default", + resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:example","detail":"message","status":404}`)), + }, + assert: errorAs[*acme.ProblemDetails], + }, + { + desc: "badNonce", + resp: &http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:badNonce","detail":"message","status":400}`)), + }, + assert: errorAs[*acme.NonceError], + }, + { + desc: "alreadyReplaced", + resp: &http.Response{ + StatusCode: http.StatusConflict, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:alreadyReplaced","detail":"message","status":409}`)), + }, + assert: errorAs[*acme.AlreadyReplacedError], + }, + { + desc: "rateLimited", + resp: &http.Response{ + StatusCode: http.StatusConflict, + Header: http.Header{ + "Retry-After": []string{"1"}, + }, + Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:rateLimited","detail":"message","status":429}`)), + }, + assert: errorAs[*acme.RateLimitedError], + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "https://example.com", nil) + + err := checkError(req, test.resp) + require.Error(t, err) + + pb := &acme.ProblemDetails{} + assert.ErrorAs(t, err, &pb) + + test.assert(t, err) + }) + } +} + +func errorAs[T error](t *testing.T, err error) { + t.Helper() + + var zero T + assert.ErrorAs(t, err, &zero) +} diff --git a/acme/errors.go b/acme/errors.go index 161a47c38..be4721c9d 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -10,6 +10,7 @@ const ( errNS = "urn:ietf:params:acme:error:" BadNonceErr = errNS + "badNonce" AlreadyReplacedErr = errNS + "alreadyReplaced" + RateLimitedErr = errNS + "rateLimited" ) // ProblemDetails the problem details object. @@ -63,9 +64,30 @@ type NonceError struct { *ProblemDetails } +func (e *NonceError) Unwrap() error { + return e.ProblemDetails +} + // AlreadyReplacedError represents the error which is returned -// If the Server rejects the request because the identified certificate has already been marked as replaced. +// if the Server rejects the request because the identified certificate has already been marked as replaced. // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 type AlreadyReplacedError struct { *ProblemDetails } + +func (e *AlreadyReplacedError) Unwrap() error { + return e.ProblemDetails +} + +// RateLimitedError represents the error which is returned +// if the server rejects the request because the client has exceeded the rate limit. +// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.6 +type RateLimitedError struct { + *ProblemDetails + + RetryAfter string +} + +func (e *RateLimitedError) Unwrap() error { + return e.ProblemDetails +} From bb98b9a899764592a65873761a560818ad2f1597 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 19 Dec 2025 02:27:38 +0100 Subject: [PATCH 47/95] chore: update pebble to v2.9.0 (#2763) --- .github/workflows/pr.yml | 4 +-- e2e/fixtures/certs/localhost/cert.pem | 35 ++++++++++++++------------- e2e/fixtures/certs/pebble.minica.pem | 23 +++++++++--------- e2e/readme.md | 4 +-- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 626d9f6e9..151a2a6e0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -44,10 +44,10 @@ jobs: install-only: true - name: Install Pebble - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.8.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 - name: Install challtestsrv - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.8.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 - name: Set up a Memcached server uses: niden/actions-memcached@v7 diff --git a/e2e/fixtures/certs/localhost/cert.pem b/e2e/fixtures/certs/localhost/cert.pem index 2866a2b48..d81d29e70 100644 --- a/e2e/fixtures/certs/localhost/cert.pem +++ b/e2e/fixtures/certs/localhost/cert.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx -MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa -VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I -8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2 -FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj -i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B -PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud -DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T -AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq -hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE -D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB -7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW -/mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K -wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B -W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw== +MIIDMDCCAhigAwIBAgIILDt8c2fMw2IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MB4XDTI1MDkwMzIzNDAwNVoXDTI3MTAw +MzIzNDAwNVowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO +0BltMXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBp +FfSa2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6 +bl3tredTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u9 +5HVL7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4k +QMJGWxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABo3oweDAOBgNVHQ8B +Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNV +HSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDAiBgNVHREEGzAZgglsb2NhbGhv +c3SCBnBlYmJsZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAAB0gkekXCNOwqWmY +vQ2lLJ8Zk2WzQ9B+VOC27IgxEEuskZyCpyXAbJB9sCGQWZhAARyaI4SPRGGagcug +d1SwDWdPGeSJzF3aDnXDYoP9Zw2KqiqVZTngeoiw8Yn0F8PNriANwRLybouX7mMc +4V7T5+2k4SUs7pFH4KO0a0XBCcjXDjdKuBljftRTXCHzJzfRtmieCCuZlpnp5sHx +hKa/uxKGyyZB+4Y3MrzsiQSCBOr9G4TH9RofmNcawl+tsVe08zLV/XVhrbakKEs7 +Y7MGHSj3BkPFF32NObc0znqWzTaUD9hU+rXWGANM4sXd4dagdnxfrb7i0WYhcUFj +9Try8Q== -----END CERTIFICATE----- diff --git a/e2e/fixtures/certs/pebble.minica.pem b/e2e/fixtures/certs/pebble.minica.pem index a69a4c419..5578b5b55 100644 --- a/e2e/fixtures/certs/pebble.minica.pem +++ b/e2e/fixtures/certs/pebble.minica.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx -MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi +MIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw +OTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB -AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB -BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v -d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF -WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll -xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix -Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 -2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF -p9BI7gVKtWSZYegicA== +AAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV +HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf +BgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC +AQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y +bqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh +f9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn +DG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg +4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4 +v1lhy71EhBuXXwRQJry0lTdF+w== -----END CERTIFICATE----- diff --git a/e2e/readme.md b/e2e/readme.md index 7a2367c9b..171170507 100644 --- a/e2e/readme.md +++ b/e2e/readme.md @@ -2,8 +2,8 @@ - Install [Pebble](https://github.com/letsencrypt/pebble): ```bash -go install github.com/letsencrypt/pebble/v2/cmd/pebble@main -go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@main +go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 +go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 ``` - Launch tests: From 96168f78ded8a0db96b80d083834bf05d2bde313 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 21 Dec 2025 22:55:28 +0100 Subject: [PATCH 48/95] Add DNS provider for ISPConfig (#2762) --- README.md | 47 +-- cmd/zz_gen_cmd_dnshelp.go | 24 ++ docs/content/dns/zz_gen_ispconfig.md | 72 ++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/ispconfig/internal/client.go | 318 ++++++++++++++++++ .../dns/ispconfig/internal/client_test.go | 175 ++++++++++ .../fixtures/client_get_id-request.json | 4 + .../internal/fixtures/client_get_id.json | 5 + .../fixtures/dns_txt_add-request.json | 17 + .../internal/fixtures/dns_txt_add.json | 5 + .../fixtures/dns_txt_delete-request.json | 5 + .../internal/fixtures/dns_txt_delete.json | 5 + .../fixtures/dns_txt_get-request.json | 7 + .../internal/fixtures/dns_txt_get.json | 7 + .../fixtures/dns_zone_get-request.json | 4 + .../internal/fixtures/dns_zone_get.json | 32 ++ .../fixtures/dns_zone_get_id-request.json | 4 + .../internal/fixtures/dns_zone_get_id.json | 5 + .../ispconfig/internal/fixtures/error.json | 5 + .../internal/fixtures/login-request.json | 5 + .../ispconfig/internal/fixtures/login.json | 5 + providers/dns/ispconfig/internal/readme.md | 249 ++++++++++++++ providers/dns/ispconfig/internal/types.go | 95 ++++++ providers/dns/ispconfig/ispconfig.go | 220 ++++++++++++ providers/dns/ispconfig/ispconfig.toml | 27 ++ providers/dns/ispconfig/ispconfig_test.go | 173 ++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 27 files changed, 1498 insertions(+), 22 deletions(-) create mode 100644 docs/content/dns/zz_gen_ispconfig.md create mode 100644 providers/dns/ispconfig/internal/client.go create mode 100644 providers/dns/ispconfig/internal/client_test.go create mode 100644 providers/dns/ispconfig/internal/fixtures/client_get_id-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/client_get_id.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_add.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_txt_get.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_zone_get.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json create mode 100644 providers/dns/ispconfig/internal/fixtures/error.json create mode 100644 providers/dns/ispconfig/internal/fixtures/login-request.json create mode 100644 providers/dns/ispconfig/internal/fixtures/login.json create mode 100644 providers/dns/ispconfig/internal/readme.md create mode 100644 providers/dns/ispconfig/internal/types.go create mode 100644 providers/dns/ispconfig/ispconfig.go create mode 100644 providers/dns/ispconfig/ispconfig.toml create mode 100644 providers/dns/ispconfig/ispconfig_test.go diff --git a/README.md b/README.md index ff9473e58..5530028cb 100644 --- a/README.md +++ b/README.md @@ -169,115 +169,120 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Ionos Cloud IPv64 + ISPConfig iwantmyname (Deprecated) Joker Joohoi's ACME-DNS - KeyHelp + KeyHelp Liara Lima-City Linode (v4) - Liquid Web + Liquid Web Loopia LuaDNS Mail-in-a-Box - ManageEngine CloudDNS + ManageEngine CloudDNS Manual Metaname Metaregistrar - mijn.host + mijn.host Mittwald myaddr.{tools,dev,io} MyDNS.jp - MythicBeasts + MythicBeasts Name.com Namecheap Namesilo - NearlyFreeSpeech.NET + NearlyFreeSpeech.NET Neodigit Netcup Netlify - Nicmanager + Nicmanager NIFCloud Njalla Nodion - NS1 + NS1 Octenium Open Telekom Cloud Oracle Cloud - OVH + OVH plesk.com Porkbun PowerDNS - Rackspace + Rackspace Rain Yun/雨云 RcodeZero reg.ru - Regfish + Regfish RFC2136 RimuHosting RU CENTER - Sakura Cloud + Sakura Cloud Scaleway Selectel Selectel v2 - SelfHost.(de|eu) + SelfHost.(de|eu) Servercow Shellrent Simply.com - Sonic + Sonic Spaceship Stackpath Syse - Technitium + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TransIP + TransIP UKFast SafeDNS Ultradns United-Domains - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS Virtualname VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index e62c337ff..ac0a52427 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -100,6 +100,7 @@ func allDNSCodes() string { "ionos", "ionoscloud", "ipv64", + "ispconfig", "iwantmyname", "joker", "keyhelp", @@ -2083,6 +2084,29 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ipv64`) + case "ispconfig": + // generated from: providers/dns/ispconfig/ispconfig.toml + ew.writeln(`Configuration for ISPConfig.`) + ew.writeln(`Code: 'ispconfig'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ISPCONFIG_PASSWORD": Password`) + ew.writeln(` - "ISPCONFIG_SERVER_URL": Server URL`) + ew.writeln(` - "ISPCONFIG_USERNAME": Username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ISPCONFIG_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ISPCONFIG_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`) + ew.writeln(` - "ISPCONFIG_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ISPCONFIG_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ISPCONFIG_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfig`) + case "iwantmyname": // generated from: providers/dns/iwantmyname/iwantmyname.toml ew.writeln(`Configuration for iwantmyname (Deprecated).`) diff --git a/docs/content/dns/zz_gen_ispconfig.md b/docs/content/dns/zz_gen_ispconfig.md new file mode 100644 index 000000000..96b08a8e0 --- /dev/null +++ b/docs/content/dns/zz_gen_ispconfig.md @@ -0,0 +1,72 @@ +--- +title: "ISPConfig" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ispconfig +dnsprovider: + since: "v4.31.0" + code: "ispconfig" + url: "https://www.ispconfig.org/" +--- + + + + + + +Configuration for [ISPConfig](https://www.ispconfig.org/). + + + + +- Code: `ispconfig` +- Since: v4.31.0 + + +Here is an example bash command using the ISPConfig provider: + +```bash +ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ +ISPCONFIG_USERNAME="xxx" \ +ISPCONFIG_PASSWORD="yyy" \ +lego --email you@example.com --dns ispconfig -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ISPCONFIG_PASSWORD` | Password | +| `ISPCONFIG_SERVER_URL` | Server URL | +| `ISPCONFIG_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 | +|--------------------------------|-------------| +| `ISPCONFIG_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ISPCONFIG_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate | +| `ISPCONFIG_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ISPCONFIG_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ISPCONFIG_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index fdb13f57a..7de6ed5a1 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, 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, 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, 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, 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/ispconfig/internal/client.go b/providers/dns/ispconfig/internal/client.go new file mode 100644 index 000000000..9280fdec1 --- /dev/null +++ b/providers/dns/ispconfig/internal/client.go @@ -0,0 +1,318 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +type Client struct { + serverURL string + HTTPClient *http.Client +} + +func NewClient(serverURL string) (*Client, error) { + _, err := url.Parse(serverURL) + if err != nil { + return nil, fmt.Errorf("server URL: %w", err) + } + + return &Client{ + serverURL: serverURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) Login(ctx context.Context, username, password string) (string, error) { + payload := LoginRequest{ + Username: username, + Password: password, + ClientLogin: false, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return "", err + } + + endpoint.RawQuery = "login" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return "", err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return "", err + } + + return extractResponse[string](response) +} + +func (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) { + payload := ClientIDRequest{ + SessionID: sessionID, + SysUserID: sysUserID, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "client_get_id" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +// GetZoneID returns the zone ID for the given name. +func (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) { + payload := map[string]any{ + "session_id": sessionID, + "origin": name, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "dns_zone_get_id" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +// GetZone returns the zone information for the zone ID. +func (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) { + payload := map[string]any{ + "session_id": sessionID, + "primary_id": zoneID, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint.RawQuery = "dns_zone_get" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return nil, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return nil, err + } + + return extractResponse[*Zone](response) +} + +// GetTXT returns the TXT record for the given name. +// `name` must be a fully qualified domain name, e.g. "example.com.". +func (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) { + payload := GetTXTRequest{ + SessionID: sessionID, + PrimaryID: struct { + Name string `json:"name"` + Type string `json:"type"` + }{ + Name: name, + Type: "txt", + }, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint.RawQuery = "dns_txt_get" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return nil, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return nil, err + } + + return extractResponse[*Record](response) +} + +// AddTXT adds a TXT record. +// It returns the ID of the newly created record. +func (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) { + payload := AddTXTRequest{ + SessionID: sessionID, + ClientID: clientID, + Params: ¶ms, + UpdateSerial: true, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return "", err + } + + endpoint.RawQuery = "dns_txt_add" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return "", err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return "", err + } + + return extractResponse[string](response) +} + +// DeleteTXT deletes a TXT record. +// It returns the number of deleted records. +func (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) { + payload := DeleteTXTRequest{ + SessionID: sessionID, + PrimaryID: recordID, + UpdateSerial: true, + } + + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return 0, err + } + + endpoint.RawQuery = "dns_txt_delete" + + req, err := newJSONRequest(ctx, endpoint, payload) + if err != nil { + return 0, err + } + + var response APIResponse + + err = c.do(req, &response) + if err != nil { + return 0, err + } + + return extractResponse[int](response) +} + +func (c *Client) do(req *http.Request, result any) error { + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func extractResponse[T any](response APIResponse) (T, error) { + if response.Code != "ok" { + var zero T + + return zero, &APIError{APIResponse: response} + } + + var result T + + err := json.Unmarshal(response.Response, &result) + if err != nil { + var zero T + return zero, fmt.Errorf("unable to unmarshal response: %s, %w", string(response.Response), err) + } + + return result, nil +} diff --git a/providers/dns/ispconfig/internal/client_test.go b/providers/dns/ispconfig/internal/client_test.go new file mode 100644 index 000000000..a4db3d5f7 --- /dev/null +++ b/providers/dns/ispconfig/internal/client_test.go @@ -0,0 +1,175 @@ +package internal + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL) + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }) +} + +func TestClient_Login(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("login.json"), + servermock.CheckRequestJSONBodyFromFixture("login-request.json"), + servermock.CheckQueryParameter().Strict(). + With("login", ""), + ). + Build(t) + + sessionID, err := client.Login(t.Context(), "user", "secret") + require.NoError(t, err) + + assert.Equal(t, "abc", sessionID) +} + +func TestClient_Login_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + _, err := client.Login(t.Context(), "user", "secret") + require.EqualError(t, err, `code: remote_fault, message: The login failed. Username or password wrong., response: false`) +} + +func TestClient_GetClientID(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("client_get_id.json"), + servermock.CheckRequestJSONBodyFromFixture("client_get_id-request.json"), + servermock.CheckQueryParameter().Strict(). + With("client_get_id", ""), + ). + Build(t) + + id, err := client.GetClientID(t.Context(), "sessionA", "sysA") + require.NoError(t, err) + + assert.Equal(t, 123, id) +} + +func TestClient_GetZoneID(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_zone_get_id.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_zone_get_id-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_zone_get_id", ""), + ). + Build(t) + + zoneID, err := client.GetZoneID(t.Context(), "sessionA", "example.com") + require.NoError(t, err) + + assert.Equal(t, 123, zoneID) +} + +func TestClient_GetZone(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_zone_get.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_zone_get-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_zone_get", ""), + ). + Build(t) + + zone, err := client.GetZone(t.Context(), "sessionA", "example.com.") + require.NoError(t, err) + + expected := &Zone{ + ID: "456", + ServerID: "123", + SysUserID: "789", + SysGroupID: "2", + Origin: "example.com.", + Serial: "2025102902", + Active: "Y", + } + + assert.Equal(t, expected, zone) +} + +func TestClient_GetTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_get.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_get-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_get", ""), + ). + Build(t) + + record, err := client.GetTXT(t.Context(), "sessionA", "example.com.") + require.NoError(t, err) + + expected := &Record{ID: 123} + + assert.Equal(t, expected, record) +} + +func TestClient_AddTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_add.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_add-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_add", ""), + ). + Build(t) + + now := time.Date(2025, 12, 25, 1, 1, 1, 0, time.UTC) + + params := RecordParams{ + ServerID: "serverA", + Zone: "example.com.", + Name: "foo.example.com.", + Type: "txt", + Data: "txtTXTtxt", + Aux: "0", + TTL: "3600", + Active: "y", + Stamp: now.Format("2006-01-02 15:04:05"), + UpdateSerial: true, + } + + recordID, err := client.AddTXT(t.Context(), "sessionA", "clientA", params) + require.NoError(t, err) + + assert.Equal(t, "123", recordID) +} + +func TestClient_DeleteTXT(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("dns_txt_delete.json"), + servermock.CheckRequestJSONBodyFromFixture("dns_txt_delete-request.json"), + servermock.CheckQueryParameter().Strict(). + With("dns_txt_delete", ""), + ). + Build(t) + + count, err := client.DeleteTXT(t.Context(), "sessionA", "123") + require.NoError(t, err) + + assert.Equal(t, 1, count) +} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json new file mode 100644 index 000000000..ba573f824 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json @@ -0,0 +1,4 @@ +{ + "session_id": "sessionA", + "sys_userid": "sysA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id.json b/providers/dns/ispconfig/internal/fixtures/client_get_id.json new file mode 100644 index 000000000..7b9f667a0 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/client_get_id.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 123 +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json new file mode 100644 index 000000000..bf5242cd1 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json @@ -0,0 +1,17 @@ +{ + "session_id": "sessionA", + "client_id": "clientA", + "params": { + "server_id": "serverA", + "zone": "example.com.", + "name": "foo.example.com.", + "type": "txt", + "data": "txtTXTtxt", + "aux": "0", + "ttl": "3600", + "active": "y", + "stamp": "2025-12-25 01:01:01", + "update_serial": true + }, + "update_serial": true +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json new file mode 100644 index 000000000..7980619fe --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": "123" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json new file mode 100644 index 000000000..240976654 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json @@ -0,0 +1,5 @@ +{ + "session_id": "sessionA", + "primary_id": "123", + "update_serial": true +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json new file mode 100644 index 000000000..960b650bd --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 1 +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json new file mode 100644 index 000000000..8bda44067 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json @@ -0,0 +1,7 @@ +{ + "session_id": "sessionA", + "primary_id": { + "name": "example.com.", + "type": "txt" + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json new file mode 100644 index 000000000..f707d50c3 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json @@ -0,0 +1,7 @@ +{ + "code": "ok", + "message": "foo", + "response": { + "id": 123 + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json new file mode 100644 index 000000000..3d44d468f --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json @@ -0,0 +1,4 @@ +{ + "primary_id": "example.com.", + "session_id": "sessionA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json new file mode 100644 index 000000000..37975d0e6 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json @@ -0,0 +1,32 @@ +{ + "code": "ok", + "message": "foo", + "response": { + "id": "456", + "sys_userid": "789", + "sys_groupid": "2", + "sys_perm_user": "riud", + "sys_perm_group": "riud", + "sys_perm_other": "", + "server_id": "123", + "origin": "example.com.", + "ns": "ns1.example.org.", + "mbox": "support.example.net.", + "serial": "2025102902", + "refresh": "7200", + "retry": "540", + "expire": "604800", + "minimum": "3600", + "ttl": "3600", + "active": "Y", + "xfer": "", + "also_notify": "", + "update_acl": "", + "dnssec_initialized": "N", + "dnssec_wanted": "N", + "dnssec_algo": "ECDSAP256SHA256", + "dnssec_last_signed": "0", + "dnssec_info": "", + "rendered_zone": "" + } +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json new file mode 100644 index 000000000..e3084242e --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json @@ -0,0 +1,4 @@ +{ + "origin": "example.com", + "session_id": "sessionA" +} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json new file mode 100644 index 000000000..7b9f667a0 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": 123 +} diff --git a/providers/dns/ispconfig/internal/fixtures/error.json b/providers/dns/ispconfig/internal/fixtures/error.json new file mode 100644 index 000000000..a9c76546c --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/error.json @@ -0,0 +1,5 @@ +{ + "code": "remote_fault", + "message": "The login failed. Username or password wrong.", + "response": false +} diff --git a/providers/dns/ispconfig/internal/fixtures/login-request.json b/providers/dns/ispconfig/internal/fixtures/login-request.json new file mode 100644 index 000000000..c3293a2e8 --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/login-request.json @@ -0,0 +1,5 @@ +{ + "username": "user", + "password": "secret", + "client_login": false +} diff --git a/providers/dns/ispconfig/internal/fixtures/login.json b/providers/dns/ispconfig/internal/fixtures/login.json new file mode 100644 index 000000000..e380a86ec --- /dev/null +++ b/providers/dns/ispconfig/internal/fixtures/login.json @@ -0,0 +1,5 @@ +{ + "code": "ok", + "message": "foo", + "response": "abc" +} diff --git a/providers/dns/ispconfig/internal/readme.md b/providers/dns/ispconfig/internal/readme.md new file mode 100644 index 000000000..2284c338f --- /dev/null +++ b/providers/dns/ispconfig/internal/readme.md @@ -0,0 +1,249 @@ +## Error Response + +```json +{ + "code": "", + "message": "", + "response": false +} +``` + +## Login Endpoint + +* URL: `?login` +* HTTP Method: `POST` + +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/login.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/login.php + +### Request Body (JSON) + +```json +{ + "username": "", + "password": "", + "client_login": false +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": "abc" +} +``` + +- `response`: is the `sessionID` + +## Get Client ID Endpoint + +* URL: `?client_get_id` +* HTTP Method: `POST` + +- function `client_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/client.inc.php#L97 +- TABLE `sys_user`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L1852 +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/client_get_id.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/client_get_id.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "sys_userid": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 123 +} +``` + +## DNS Zone Get ID Endpoint + +* URL: `?dns_zone_get_id` +* HTTP Method: `POST` + +- function `dns_zone_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L142 +- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 + +### Request Body (JSON) + +```json +{ + "session_id": "", + "origin": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 123 +} +``` + +## DNS Zone Get Endpoint + +* URL: `?dns_zone_get` +* HTTP Method: `POST` + +- function `dns_zone_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L87 +- function `getDataRecord`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remoting_lib.inc.php#L248 +- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 +- Depending on the request, the response may be an array or an object (`primary_id` can be a string, an array or an object). +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_zone_get.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_zone_get.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": "" +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": { + "id": 456, + "server_id": 123, + "sys_userid": 789 + } +} +``` + +## DNS TXT Get Endpoint + +* URL: `?dns_txt_get` +* HTTP Method: `POST` + +- function `dns_txt_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L640 +- function `dns_rr_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L195 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- TABLE `dns_rr`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L490 +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_get.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_get.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": { + "name": ".", + "type": "TXT" + } +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": { + "id": 123 + } +} +``` + +## DNS TXT Add Endpoint + +* URL: `?dns_txt_add` +* HTTP Method: `POST` + +- function `dns_txt_add`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L645 +- function `dns_rr_add` https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L212 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_add.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_add.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "client_id": "", + "params": { + "server_id": "", + "zone": "", + "name": ".", + "type": "txt", + "data": "", + "aux": "0", + "ttl": "3600", + "active": "y", + "stamp": "", + "update_serial": true + }, + "update_serial": true +} +``` + +- `stamp`: (ex: `2025-12-17 23:35:58`) +- `serial`: (ex: `1766010947`) + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": "123" +} +``` + +## DNS TXT Delete Endpoint + +* URL: `?dns_txt_delete` +* HTTP Method: `POST` + +- function `dns_txt_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L655 +- function `dns_rr_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L247 +- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_delete.html +- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_delete.php + +### Request Body (JSON) + +```json +{ + "session_id": "", + "primary_id": "", + "update_serial": true +} +``` + +### Response Body (JSON) + +```json +{ + "code": "ok", + "message": "foo", + "response": 1 +} +``` + +--- + +https://www.ispconfig.org/ +https://git.ispconfig.org/ispconfig/ispconfig3 +https://forum.howtoforge.com/#ispconfig-3.23 diff --git a/providers/dns/ispconfig/internal/types.go b/providers/dns/ispconfig/internal/types.go new file mode 100644 index 000000000..7db0846cc --- /dev/null +++ b/providers/dns/ispconfig/internal/types.go @@ -0,0 +1,95 @@ +package internal + +import ( + "encoding/json" + "strings" +) + +type APIError struct { + APIResponse +} + +func (e *APIError) Error() string { + var msg strings.Builder + + msg.WriteString("code: " + e.Code) + + if e.Message != "" { + msg.WriteString(", message: " + e.Message) + } + + if len(e.Response) > 0 { + msg.WriteString(", response: " + string(e.Response)) + } + + return msg.String() +} + +type APIResponse struct { + Code string `json:"code"` + Message string `json:"message"` + Response json.RawMessage `json:"response"` +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + ClientLogin bool `json:"client_login"` +} + +type ClientIDRequest struct { + SessionID string `json:"session_id"` + SysUserID string `json:"sys_userid"` +} + +type Zone struct { + ID string `json:"id"` + ServerID string `json:"server_id"` + SysUserID string `json:"sys_userid"` + SysGroupID string `json:"sys_groupid"` + Origin string `json:"origin"` + Serial string `json:"serial"` + Active string `json:"active"` +} + +type GetTXTRequest struct { + SessionID string `json:"session_id"` + PrimaryID struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"primary_id"` +} + +type Record struct { + ID int `json:"id"` +} + +type AddTXTRequest struct { + SessionID string `json:"session_id"` + ClientID string `json:"client_id"` + Params *RecordParams `json:"params,omitempty"` + UpdateSerial bool `json:"update_serial"` +} + +type RecordParams struct { + ServerID string `json:"server_id"` + Zone string `json:"zone"` + Name string `json:"name"` + // 'a','aaaa','alias','cname','hinfo','mx','naptr','ns','ds','ptr','rp','srv','txt' + Type string `json:"type"` + Data string `json:"data"` + // "0" + Aux string `json:"aux"` + TTL string `json:"ttl"` + // 'n','y' + Active string `json:"active"` + // `2025-12-17 23:35:58` + Stamp string `json:"stamp"` + UpdateSerial bool `json:"update_serial"` +} + +type DeleteTXTRequest struct { + SessionID string `json:"session_id"` + PrimaryID string `json:"primary_id"` + UpdateSerial bool `json:"update_serial"` +} diff --git a/providers/dns/ispconfig/ispconfig.go b/providers/dns/ispconfig/ispconfig.go new file mode 100644 index 000000000..9396430b7 --- /dev/null +++ b/providers/dns/ispconfig/ispconfig.go @@ -0,0 +1,220 @@ +// Package ispconfig implements a DNS provider for solving the DNS-01 challenge using ISPConfig. +package ispconfig + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "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/ispconfig/internal" +) + +// Environment variables names. +const ( + envNamespace = "ISPCONFIG_" + + EnvServerURL = envNamespace + "SERVER_URL" + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ServerURL string + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client + InsecureSkipVerify bool +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for ISPConfig. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("ispconfig: %w", err) + } + + config := NewDefaultConfig() + config.ServerURL = values[EnvServerURL] + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ispconfig: the configuration of the DNS provider is nil") + } + + if config.ServerURL == "" { + return nil, errors.New("ispconfig: missing server URL") + } + + if config.Username == "" || config.Password == "" { + return nil, errors.New("ispconfig: credentials missing") + } + + client, err := internal.NewClient(config.ServerURL) + if err != nil { + return nil, fmt.Errorf("ispconfig: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + if config.InsecureSkipVerify { + client.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) + if err != nil { + return fmt.Errorf("ispconfig: login: %w", err) + } + + zoneID, err := d.findZone(ctx, sessionID, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig: get zone id: %w", err) + } + + zone, err := d.client.GetZone(ctx, sessionID, strconv.Itoa(zoneID)) + if err != nil { + return fmt.Errorf("ispconfig: get zone: %w", err) + } + + clientID, err := d.client.GetClientID(ctx, sessionID, zone.SysUserID) + if err != nil { + return fmt.Errorf("ispconfig: get client id: %w", err) + } + + params := internal.RecordParams{ + ServerID: "serverA", + Zone: zone.ID, + Name: info.EffectiveFQDN, + Type: "txt", + Data: info.Value, + Aux: "0", + TTL: strconv.Itoa(d.config.TTL), + Active: "y", + Stamp: time.Now().UTC().Format("2006-01-02 15:04:05"), + } + + recordID, err := d.client.AddTXT(ctx, sessionID, strconv.Itoa(clientID), params) + if err != nil { + return fmt.Errorf("ispconfig: add txt 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 { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("ispconfig: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) + if err != nil { + return fmt.Errorf("ispconfig: login: %w", err) + } + + _, err = d.client.DeleteTXT(ctx, sessionID, recordID) + if err != nil { + return fmt.Errorf("ispconfig: delete txt record: %w", err) + } + + 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 +} + +func (d *DNSProvider) findZone(ctx context.Context, sessionID, fqdn string) (int, error) { + for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + zoneID, err := d.client.GetZoneID(ctx, sessionID, domain) + if err == nil { + return zoneID, nil + } + } + + return 0, fmt.Errorf("zone not found for %q", fqdn) +} diff --git a/providers/dns/ispconfig/ispconfig.toml b/providers/dns/ispconfig/ispconfig.toml new file mode 100644 index 000000000..a1cb89210 --- /dev/null +++ b/providers/dns/ispconfig/ispconfig.toml @@ -0,0 +1,27 @@ +Name = "ISPConfig" +Description = '''''' +URL = "https://www.ispconfig.org/" +Code = "ispconfig" +Since = "v4.31.0" + +Example = ''' +ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ +ISPCONFIG_USERNAME="xxx" \ +ISPCONFIG_PASSWORD="yyy" \ +lego --email you@example.com --dns ispconfig -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ISPCONFIG_SERVER_URL = "Server URL" + ISPCONFIG_USERNAME = "Username" + ISPCONFIG_PASSWORD = "Password" + [Configuration.Additional] + ISPCONFIG_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" + ISPCONFIG_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ISPCONFIG_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ISPCONFIG_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ISPCONFIG_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html" diff --git a/providers/dns/ispconfig/ispconfig_test.go b/providers/dns/ispconfig/ispconfig_test.go new file mode 100644 index 000000000..b03463aee --- /dev/null +++ b/providers/dns/ispconfig/ispconfig_test.go @@ -0,0 +1,173 @@ +package ispconfig + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvServerURL, + 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{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "user", + EnvPassword: "secret", + }, + }, + { + desc: "missing server URL", + envVars: map[string]string{ + EnvServerURL: "", + EnvUsername: "user", + EnvPassword: "secret", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "", + EnvPassword: "secret", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvServerURL: "https://example.com:80/", + EnvUsername: "user", + EnvPassword: "", + }, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL,ISPCONFIG_USERNAME,ISPCONFIG_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.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + serverURL string + username string + password string + expected string + }{ + { + desc: "success", + serverURL: "https://example.com:80/", + username: "user", + password: "secret", + }, + { + desc: "missing server URL", + username: "user", + password: "secret", + expected: "ispconfig: missing server URL", + }, + { + desc: "missing username", + serverURL: "https://example.com:80/", + password: "secret", + expected: "ispconfig: credentials missing", + }, + { + desc: "missing password", + serverURL: "https://example.com:80/", + username: "user", + expected: "ispconfig: credentials missing", + }, + { + desc: "missing credentials", + expected: "ispconfig: missing server URL", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ServerURL = test.serverURL + 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 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/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 1270e0f9d..3a17594e2 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -94,6 +94,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/ionos" "github.com/go-acme/lego/v4/providers/dns/ionoscloud" "github.com/go-acme/lego/v4/providers/dns/ipv64" + "github.com/go-acme/lego/v4/providers/dns/ispconfig" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/keyhelp" @@ -363,6 +364,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return ionoscloud.NewDNSProvider() case "ipv64": return ipv64.NewDNSProvider() + case "ispconfig": + return ispconfig.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() case "joker": From 8b327005b3105a5a70c8ace5cfe7e1f83e148f7c Mon Sep 17 00:00:00 2001 From: Simon Merschjohann Date: Mon, 22 Dec 2025 03:50:29 +0100 Subject: [PATCH 49/95] Add DNS Provider for ISPConfig (DDNS Module) (#2760) Co-authored-by: Fernandez Ludovic --- README.md | 48 ++--- cmd/zz_gen_cmd_dnshelp.go | 24 ++- docs/content/dns/zz_gen_ispconfig.md | 6 +- docs/content/dns/zz_gen_ispconfigddns.md | 74 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/ispconfig/ispconfig.toml | 2 +- .../dns/ispconfigddns/internal/client.go | 111 ++++++++++ .../dns/ispconfigddns/internal/client_test.go | 83 ++++++++ providers/dns/ispconfigddns/internal/types.go | 9 + providers/dns/ispconfigddns/ispconfigddns.go | 145 +++++++++++++ .../dns/ispconfigddns/ispconfigddns.toml | 32 +++ .../dns/ispconfigddns/ispconfigddns_test.go | 193 ++++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 702 insertions(+), 30 deletions(-) create mode 100644 docs/content/dns/zz_gen_ispconfigddns.md create mode 100644 providers/dns/ispconfigddns/internal/client.go create mode 100644 providers/dns/ispconfigddns/internal/client_test.go create mode 100644 providers/dns/ispconfigddns/internal/types.go create mode 100644 providers/dns/ispconfigddns/ispconfigddns.go create mode 100644 providers/dns/ispconfigddns/ispconfigddns.toml create mode 100644 providers/dns/ispconfigddns/ispconfigddns_test.go diff --git a/README.md b/README.md index 5530028cb..5fa9c1ed1 100644 --- a/README.md +++ b/README.md @@ -169,120 +169,120 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). Ionos Cloud IPv64 - ISPConfig + ISPConfig 3 + ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) Joker - Joohoi's ACME-DNS + Joohoi's ACME-DNS KeyHelp Liara Lima-City - Linode (v4) + Linode (v4) Liquid Web Loopia LuaDNS - Mail-in-a-Box + Mail-in-a-Box ManageEngine CloudDNS Manual Metaname - Metaregistrar + Metaregistrar mijn.host Mittwald myaddr.{tools,dev,io} - MyDNS.jp + MyDNS.jp MythicBeasts Name.com Namecheap - Namesilo + Namesilo NearlyFreeSpeech.NET Neodigit Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Syse + Syse Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TransIP UKFast SafeDNS Ultradns - United-Domains + United-Domains Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS Virtualname VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index ac0a52427..7677818c9 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -101,6 +101,7 @@ func allDNSCodes() string { "ionoscloud", "ipv64", "ispconfig", + "ispconfigddns", "iwantmyname", "joker", "keyhelp", @@ -2086,7 +2087,7 @@ func displayDNSHelp(w io.Writer, name string) error { case "ispconfig": // generated from: providers/dns/ispconfig/ispconfig.toml - ew.writeln(`Configuration for ISPConfig.`) + ew.writeln(`Configuration for ISPConfig 3.`) ew.writeln(`Code: 'ispconfig'`) ew.writeln(`Since: 'v4.31.0'`) ew.writeln() @@ -2107,6 +2108,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfig`) + case "ispconfigddns": + // generated from: providers/dns/ispconfigddns/ispconfigddns.toml + ew.writeln(`Configuration for ISPConfig 3 - Dynamic DNS (DDNS) Module.`) + ew.writeln(`Code: 'ispconfigddns'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ISPCONFIG_DDNS_SERVER_URL": API server URL (ex: https://panel.example.com:8080)`) + ew.writeln(` - "ISPCONFIG_DDNS_TOKEN": DDNS API token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ISPCONFIG_DDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ISPCONFIG_DDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ISPCONFIG_DDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ISPCONFIG_DDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfigddns`) + case "iwantmyname": // generated from: providers/dns/iwantmyname/iwantmyname.toml ew.writeln(`Configuration for iwantmyname (Deprecated).`) diff --git a/docs/content/dns/zz_gen_ispconfig.md b/docs/content/dns/zz_gen_ispconfig.md index 96b08a8e0..bd3b375da 100644 --- a/docs/content/dns/zz_gen_ispconfig.md +++ b/docs/content/dns/zz_gen_ispconfig.md @@ -1,5 +1,5 @@ --- -title: "ISPConfig" +title: "ISPConfig 3" date: 2019-03-03T16:39:46+01:00 draft: false slug: ispconfig @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [ISPConfig](https://www.ispconfig.org/). +Configuration for [ISPConfig 3](https://www.ispconfig.org/). @@ -23,7 +23,7 @@ Configuration for [ISPConfig](https://www.ispconfig.org/). - Since: v4.31.0 -Here is an example bash command using the ISPConfig provider: +Here is an example bash command using the ISPConfig 3 provider: ```bash ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ diff --git a/docs/content/dns/zz_gen_ispconfigddns.md b/docs/content/dns/zz_gen_ispconfigddns.md new file mode 100644 index 000000000..c59bddda4 --- /dev/null +++ b/docs/content/dns/zz_gen_ispconfigddns.md @@ -0,0 +1,74 @@ +--- +title: "ISPConfig 3 - Dynamic DNS (DDNS) Module" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ispconfigddns +dnsprovider: + since: "v4.31.0" + code: "ispconfigddns" + url: "https://www.ispconfig.org/" +--- + + + + + + +Configuration for [ISPConfig 3 - Dynamic DNS (DDNS) Module](https://www.ispconfig.org/). + + + + +- Code: `ispconfigddns` +- Since: v4.31.0 + + +Here is an example bash command using the ISPConfig 3 - Dynamic DNS (DDNS) Module provider: + +```bash +ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ +ISPCONFIG_DDNS_TOKEN=xxxxxx \ +lego --email you@example.com --dns ispconfigddns -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ISPCONFIG_DDNS_SERVER_URL` | API server URL (ex: https://panel.example.com:8080) | +| `ISPCONFIG_DDNS_TOKEN` | DDNS API token | + +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 | +|--------------------------------|-------------| +| `ISPCONFIG_DDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ISPCONFIG_DDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ISPCONFIG_DDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ISPCONFIG_DDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | + +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" %}}). + +ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). + +Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ + +See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. + + + +## More information + +- [API documentation](https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 7de6ed5a1..ae2369a9a 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, 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, 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, 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 More information: https://go-acme.github.io/lego/dns """ diff --git a/providers/dns/ispconfig/ispconfig.toml b/providers/dns/ispconfig/ispconfig.toml index a1cb89210..399544742 100644 --- a/providers/dns/ispconfig/ispconfig.toml +++ b/providers/dns/ispconfig/ispconfig.toml @@ -1,4 +1,4 @@ -Name = "ISPConfig" +Name = "ISPConfig 3" Description = '''''' URL = "https://www.ispconfig.org/" Code = "ispconfig" diff --git a/providers/dns/ispconfigddns/internal/client.go b/providers/dns/ispconfigddns/internal/client.go new file mode 100644 index 000000000..700b58f89 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/client.go @@ -0,0 +1,111 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +const ( + addAction = "add" + deleteAction = "delete" +) + +type Client struct { + token string + serverURL string + + HTTPClient *http.Client +} + +func NewClient(serverURL, token string) (*Client, error) { + _, err := url.Parse(serverURL) + if err != nil { + return nil, fmt.Errorf("server URL: %w", err) + } + + return &Client{ + serverURL: serverURL, + token: token, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error { + return c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: "TXT", Record: fqdn, Data: content}) +} + +func (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error { + return c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: "TXT", Record: fqdn, Data: recordContent}) +} + +func (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error { + req, err := c.newRequest(ctx, action) + if err != nil { + return err + } + + return c.do(req) +} + +func (c *Client) do(req *http.Request) error { + useragent.SetHeader(req.Header) + + req.SetBasicAuth("anonymous", c.token) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + // The endpoint uses the `DefaultDdnsResponseWriter`, + // and this writer uses HTTP status code to determine if the request was successful or not. + // - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57 + // - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return nil +} + +func (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) { + endpoint, err := url.Parse(c.serverURL) + if err != nil { + return nil, err + } + + endpoint = endpoint.JoinPath("ddns", "update.php") + + values, err := querystring.Values(action) + if err != nil { + return nil, err + } + + endpoint.RawQuery = values.Encode() + + method := http.MethodPost + if action.Action == deleteAction { + method = http.MethodDelete + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + + return req, nil +} diff --git a/providers/dns/ispconfigddns/internal/client_test.go b/providers/dns/ispconfigddns/internal/client_test.go new file mode 100644 index 000000000..774e5ee46 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/client_test.go @@ -0,0 +1,83 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func setupClient(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("POST /ddns/update.php", + servermock.Noop(), + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + servermock.CheckQueryParameter().Strict(). + With("action", "add"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "token"), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.NoError(t, err) +} + +func TestClient_AddTXTRecord_error(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("POST /ddns/update.php", + servermock.RawStringResponse("Missing or invalid token."). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") +} + +func TestClient_DeleteTXTRecord(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("DELETE /ddns/update.php", + servermock.Noop(), + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + servermock.CheckQueryParameter().Strict(). + With("action", "delete"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "token"), + ). + Build(t) + + err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.NoError(t, err) +} + +func TestClient_DeleteTXTRecord_error(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("DELETE /ddns/update.php", + servermock.RawStringResponse("Missing or invalid token."). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") + require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") +} diff --git a/providers/dns/ispconfigddns/internal/types.go b/providers/dns/ispconfigddns/internal/types.go new file mode 100644 index 000000000..278738108 --- /dev/null +++ b/providers/dns/ispconfigddns/internal/types.go @@ -0,0 +1,9 @@ +package internal + +type UpdateRecord struct { + Action string `url:"action,omitempty"` + Zone string `url:"zone,omitempty"` + Type string `url:"type,omitempty"` + Record string `url:"record,omitempty"` + Data string `url:"data,omitempty"` +} diff --git a/providers/dns/ispconfigddns/ispconfigddns.go b/providers/dns/ispconfigddns/ispconfigddns.go new file mode 100644 index 000000000..eab5d413f --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns.go @@ -0,0 +1,145 @@ +// Package ispconfigddns implements a DNS provider for solving the DNS-01 challenge using ISPConfig 3 Dynamic DNS (DDNS) Module. +package ispconfigddns + +import ( + "context" + "errors" + "fmt" + "net/http" + "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/ispconfigddns/internal" +) + +// Environment variables names. +const ( + envNamespace = "ISPCONFIG_DDNS_" + + EnvServerURL = envNamespace + "SERVER_URL" + EnvToken = envNamespace + "TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ServerURL string + Token string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 3600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServerURL, EnvToken) + if err != nil { + return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) + } + + config := NewDefaultConfig() + config.ServerURL = values[EnvServerURL] + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ispconfig (DDNS module): the configuration of the DNS provider is nil") + } + + if config.ServerURL == "" { + return nil, errors.New("ispconfig (DDNS module): missing server URL") + } + + if config.Token == "" { + return nil, errors.New("ispconfig (DDNS module): missing token") + } + + client, err := internal.NewClient(config.ServerURL, config.Token) + if err != nil { + return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to control checking compliance to spec. +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 { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) + } + + err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): add record: %w", err) + } + + 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) + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) + } + + err = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) + if err != nil { + return fmt.Errorf("ispconfig (DDNS module): delete record: %w", err) + } + + return nil +} diff --git a/providers/dns/ispconfigddns/ispconfigddns.toml b/providers/dns/ispconfigddns/ispconfigddns.toml new file mode 100644 index 000000000..84e82904f --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns.toml @@ -0,0 +1,32 @@ +Name = "ISPConfig 3 - Dynamic DNS (DDNS) Module" +Description = '''''' +URL = "https://www.ispconfig.org/" +Code = "ispconfigddns" +Since = "v4.31.0" + +Example = ''' +ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ +ISPCONFIG_DDNS_TOKEN=xxxxxx \ +lego --email you@example.com --dns ispconfigddns -d '*.example.com' -d example.com run +''' + +Additional = ''' +ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). + +Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ + +See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. +''' + +[Configuration] + [Configuration.Credentials] + ISPCONFIG_DDNS_SERVER_URL = "API server URL (ex: https://panel.example.com:8080)" + ISPCONFIG_DDNS_TOKEN = "DDNS API token" + [Configuration.Additional] + ISPCONFIG_DDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ISPCONFIG_DDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ISPCONFIG_DDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" + ISPCONFIG_DDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater" diff --git a/providers/dns/ispconfigddns/ispconfigddns_test.go b/providers/dns/ispconfigddns/ispconfigddns_test.go new file mode 100644 index 000000000..58e7a8f54 --- /dev/null +++ b/providers/dns/ispconfigddns/ispconfigddns_test.go @@ -0,0 +1,193 @@ +package ispconfigddns + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvServerURL, EnvToken). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvServerURL: "https://example.com", + EnvToken: "secret", + }, + }, + { + desc: "missing server URL", + envVars: map[string]string{ + EnvServerURL: "", + EnvToken: "secret", + }, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL", + }, + { + desc: "missing token", + envVars: map[string]string{ + EnvServerURL: "https://example.com", + EnvToken: "", + }, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_TOKEN", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL,ISPCONFIG_DDNS_TOKEN", + }, + } + + 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) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + serverURL string + token string + expected string + }{ + { + desc: "success", + serverURL: "https://example.com", + token: "secret", + }, + { + desc: "missing server URL", + serverURL: "", + token: "secret", + expected: "ispconfig (DDNS module): missing server URL", + }, + { + desc: "missing token", + serverURL: "https://example.com", + token: "", + expected: "ispconfig (DDNS module): missing token", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ServerURL = test.serverURL + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } 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) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.HTTPClient = server.Client() + config.Token = "secret" + config.ServerURL = server.URL + + return NewDNSProviderConfig(config) + }, + servermock.CheckHeader(). + WithBasicAuth("anonymous", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /ddns/update.php", + servermock.DumpRequest(), + servermock.CheckQueryParameter().Strict(). + With("action", "add"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /ddns/update.php", + servermock.DumpRequest(), + servermock.CheckQueryParameter().Strict(). + With("action", "delete"). + With("zone", "example.com"). + With("type", "TXT"). + With("record", "_acme-challenge.example.com."). + With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 3a17594e2..38155e164 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -95,6 +95,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/ionoscloud" "github.com/go-acme/lego/v4/providers/dns/ipv64" "github.com/go-acme/lego/v4/providers/dns/ispconfig" + "github.com/go-acme/lego/v4/providers/dns/ispconfigddns" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/keyhelp" @@ -366,6 +367,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return ipv64.NewDNSProvider() case "ispconfig": return ispconfig.NewDNSProvider() + case "ispconfigddns": + return ispconfigddns.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() case "joker": From ee616417a181239b0b77f6fe00acff3a22ae35b9 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 23 Dec 2025 13:52:22 +0100 Subject: [PATCH 50/95] f5xc: add an option to configure the domain of the server (#2767) --- cmd/zz_gen_cmd_dnshelp.go | 1 + docs/content/dns/zz_gen_f5xc.md | 1 + providers/dns/f5xc/f5xc.go | 5 +- providers/dns/f5xc/f5xc.toml | 1 + providers/dns/f5xc/f5xc_test.go | 7 +- providers/dns/f5xc/internal/client.go | 29 ++++++-- providers/dns/f5xc/internal/client_test.go | 87 +++++++++++++++++++--- 7 files changed, 111 insertions(+), 20 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 7677818c9..601222903 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -1449,6 +1449,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "F5XC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) ew.writeln(` - "F5XC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "F5XC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "F5XC_SERVER": Server domain (Default: console.ves.volterra.io)`) ew.writeln(` - "F5XC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) ew.writeln() diff --git a/docs/content/dns/zz_gen_f5xc.md b/docs/content/dns/zz_gen_f5xc.md index c8a664a00..52488f1f7 100644 --- a/docs/content/dns/zz_gen_f5xc.md +++ b/docs/content/dns/zz_gen_f5xc.md @@ -54,6 +54,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `F5XC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | | `F5XC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | | `F5XC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `F5XC_SERVER` | Server domain (Default: console.ves.volterra.io) | | `F5XC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. diff --git a/providers/dns/f5xc/f5xc.go b/providers/dns/f5xc/f5xc.go index 6f8a8c493..76a6e0262 100644 --- a/providers/dns/f5xc/f5xc.go +++ b/providers/dns/f5xc/f5xc.go @@ -22,6 +22,7 @@ const ( EnvToken = envNamespace + "API_TOKEN" EnvTenantName = envNamespace + "TENANT_NAME" + EnvServer = envNamespace + "SERVER" EnvGroupName = envNamespace + "GROUP_NAME" EnvTTL = envNamespace + "TTL" @@ -34,6 +35,7 @@ const ( type Config struct { APIToken string TenantName string + Server string GroupName string PropagationTimeout time.Duration @@ -71,6 +73,7 @@ func NewDNSProvider() (*DNSProvider, error) { config.APIToken = values[EnvToken] config.TenantName = values[EnvTenantName] config.GroupName = values[EnvGroupName] + config.Server = env.GetOrFile(EnvServer) return NewDNSProviderConfig(config) } @@ -85,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("f5xc: missing group name") } - client, err := internal.NewClient(config.APIToken, config.TenantName) + client, err := internal.NewClient(config.APIToken, config.TenantName, config.Server) if err != nil { return nil, fmt.Errorf("f5xc: %w", err) } diff --git a/providers/dns/f5xc/f5xc.toml b/providers/dns/f5xc/f5xc.toml index 7a4cab419..f5a843c97 100644 --- a/providers/dns/f5xc/f5xc.toml +++ b/providers/dns/f5xc/f5xc.toml @@ -17,6 +17,7 @@ lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run F5XC_TENANT_NAME = "XC Tenant shortname" F5XC_GROUP_NAME = "Group name" [Configuration.Additional] + F5XC_SERVER = "Server domain (Default: console.ves.volterra.io)" F5XC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" F5XC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" F5XC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" diff --git a/providers/dns/f5xc/f5xc_test.go b/providers/dns/f5xc/f5xc_test.go index 98f7484e7..890a4cf09 100644 --- a/providers/dns/f5xc/f5xc_test.go +++ b/providers/dns/f5xc/f5xc_test.go @@ -9,7 +9,12 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvToken, EnvTenantName, EnvGroupName).WithDomain(envDomain) +var envTest = tester.NewEnvTest( + EnvToken, + EnvTenantName, + EnvServer, + EnvGroupName, +).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { diff --git a/providers/dns/f5xc/internal/client.go b/providers/dns/f5xc/internal/client.go index b0b5d0468..7beab0d03 100644 --- a/providers/dns/f5xc/internal/client.go +++ b/providers/dns/f5xc/internal/client.go @@ -14,7 +14,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultHost = "console.ves.volterra.io" +const defaultServer = "console.ves.volterra.io" const authorizationHeader = "Authorization" @@ -27,18 +27,14 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(apiToken, tenantName string) (*Client, error) { +func NewClient(apiToken, tenantName, server string) (*Client, error) { if apiToken == "" { return nil, errors.New("credentials missing") } - if tenantName == "" { - return nil, errors.New("missing tenant name") - } - - baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenantName, defaultHost)) + baseURL, err := createBaseURL(tenantName, server) if err != nil { - return nil, fmt.Errorf("parse base URL: %w", err) + return nil, err } return &Client{ @@ -209,3 +205,20 @@ func parseError(req *http.Request, resp *http.Response) error { return &apiErr } + +func createBaseURL(tenant, server string) (*url.URL, error) { + if tenant == "" { + return nil, errors.New("missing tenant name") + } + + if server == "" { + server = defaultServer + } + + baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenant, server)) + if err != nil { + return nil, fmt.Errorf("parse base URL: %w", err) + } + + return baseURL, nil +} diff --git a/providers/dns/f5xc/internal/client_test.go b/providers/dns/f5xc/internal/client_test.go index 0357abb16..bb188ef3f 100644 --- a/providers/dns/f5xc/internal/client_test.go +++ b/providers/dns/f5xc/internal/client_test.go @@ -14,7 +14,7 @@ import ( func mockBuilder() *servermock.Builder[*Client] { return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret", "shortname") + client, err := NewClient("secret", "shortname", "") if err != nil { return nil, err } @@ -28,7 +28,7 @@ func mockBuilder() *servermock.Builder[*Client] { WithAuthorization("APIToken secret")) } -func TestClient_Create(t *testing.T) { +func TestClient_CreateRRSet(t *testing.T) { client := mockBuilder(). Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", servermock.ResponseFromFixture("create.json"), @@ -62,7 +62,7 @@ func TestClient_Create(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Create_error(t *testing.T) { +func TestClient_CreateRRSet_error(t *testing.T) { client := mockBuilder(). Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -81,7 +81,7 @@ func TestClient_Create_error(t *testing.T) { require.Error(t, err) } -func TestClient_Get(t *testing.T) { +func TestClient_GetRRSet(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json")). @@ -108,7 +108,7 @@ func TestClient_Get(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Get_not_found(t *testing.T) { +func TestClient_GetRRSet_not_found(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("error_404.json").WithStatusCode(http.StatusNotFound)). @@ -120,7 +120,7 @@ func TestClient_Get_not_found(t *testing.T) { assert.Nil(t, result) } -func TestClient_Get_error(t *testing.T) { +func TestClient_GetRRSet_error(t *testing.T) { client := mockBuilder(). Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -130,7 +130,7 @@ func TestClient_Get_error(t *testing.T) { require.Error(t, err) } -func TestClient_Delete(t *testing.T) { +func TestClient_DeleteRRSet(t *testing.T) { client := mockBuilder(). Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json")). @@ -157,7 +157,7 @@ func TestClient_Delete(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Delete_error(t *testing.T) { +func TestClient_DeleteRRSet_error(t *testing.T) { client := mockBuilder(). Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -167,7 +167,7 @@ func TestClient_Delete_error(t *testing.T) { require.Error(t, err) } -func TestClient_Replace(t *testing.T) { +func TestClient_ReplaceRRSet(t *testing.T) { client := mockBuilder(). Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.ResponseFromFixture("get.json"), @@ -204,7 +204,7 @@ func TestClient_Replace(t *testing.T) { assert.Equal(t, expected, result) } -func TestClient_Replace_error(t *testing.T) { +func TestClient_ReplaceRRSet_error(t *testing.T) { client := mockBuilder(). Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", servermock.Noop().WithStatusCode(http.StatusBadRequest)). @@ -222,3 +222,70 @@ func TestClient_Replace_error(t *testing.T) { _, err := client.ReplaceRRSet(t.Context(), "example.com", "groupA", "www", "TXT", rrSet) require.Error(t, err) } + +func Test_createBaseURL(t *testing.T) { + testCases := []struct { + desc string + tenant string + server string + expected string + }{ + { + desc: "only tenant", + tenant: "foo", + expected: "https://foo.console.ves.volterra.io", + }, + { + desc: "custom server", + tenant: "foo", + server: "example.com", + expected: "https://foo.example.com", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + baseURL, err := createBaseURL(test.tenant, test.server) + require.NoError(t, err) + + assert.Equal(t, test.expected, baseURL.String()) + }) + } +} + +func Test_createBaseURL_error(t *testing.T) { + testCases := []struct { + desc string + tenant string + server string + expected string + }{ + { + desc: "no tenant", + tenant: "", + expected: "missing tenant name", + }, + { + desc: "invalid tenant", + tenant: "%31", + expected: `parse base URL: parse "https://%31.console.ves.volterra.io": invalid URL escape "%31"`, + }, + { + desc: "invalid host", + tenant: "foo", + server: "192.168.0.%31", + expected: `parse base URL: parse "https://foo.192.168.0.%31": invalid URL escape "%31"`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + _, err := createBaseURL(test.tenant, test.server) + require.EqualError(t, err, test.expected) + }) + } +} From ff885d99c2e45f166a0c9592ea047d8c938604d3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 23 Dec 2025 16:39:06 +0100 Subject: [PATCH 51/95] gandiv5: fix API Key header (#2769) --- providers/dns/gandiv5/internal/client.go | 5 +---- providers/dns/gandiv5/internal/client_test.go | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/providers/dns/gandiv5/internal/client.go b/providers/dns/gandiv5/internal/client.go index 36e0dafb1..bfb71c9f6 100644 --- a/providers/dns/gandiv5/internal/client.go +++ b/providers/dns/gandiv5/internal/client.go @@ -17,9 +17,6 @@ import ( // defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp. const defaultBaseURL = "https://api.gandi.net/v5/livedns" -// APIKeyHeader API key header. -const APIKeyHeader = "X-Api-Key" - // Related to Personal Access Token. const authorizationHeader = "Authorization" @@ -133,7 +130,7 @@ func (c *Client) DeleteTXTRecord(ctx context.Context, domain, name string) error func (c *Client) do(req *http.Request, result any) error { if c.apiKey != "" { - req.Header.Set(APIKeyHeader, c.apiKey) + req.Header.Set(authorizationHeader, "Apikey "+c.apiKey) } if c.pat != "" { diff --git a/providers/dns/gandiv5/internal/client_test.go b/providers/dns/gandiv5/internal/client_test.go index 2465566f9..6a4158dcb 100644 --- a/providers/dns/gandiv5/internal/client_test.go +++ b/providers/dns/gandiv5/internal/client_test.go @@ -9,23 +9,29 @@ import ( "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { +func mockBuilder(apiKey, pat string) *servermock.Builder[*Client] { + checkHeaders := servermock.CheckHeader().WithJSONHeaders() + + if apiKey != "" { + checkHeaders = checkHeaders.WithAuthorization("Apikey secret-apikey") + } else { + checkHeaders = checkHeaders.WithAuthorization("Bearer secret-pat") + } + return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { - client := NewClient("secret", "xxx") + client := NewClient(apiKey, pat) client.BaseURL, _ = url.Parse(server.URL) client.HTTPClient = server.Client() return client, nil }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-Api-Key", "secret"). - WithAuthorization("Bearer xxx"), + checkHeaders, ) } func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder("secret-apikey", ""). Route("GET /domains/example.com/records/foo/TXT", servermock.ResponseFromFixture("add_txt_record_get.json")). Route("PUT /domains/example.com/records/foo/TXT", @@ -38,7 +44,7 @@ func TestClient_AddTXTRecord(t *testing.T) { } func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder("", "secret-pat"). Route("DELETE /domains/example.com/records/foo/TXT", servermock.ResponseFromFixture("api_response.json")). Build(t) From a6a73754af31fb65963f53845bb9ce55d09466a1 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 29 Dec 2025 14:09:06 +0100 Subject: [PATCH 52/95] Add DNS provider for Alwaysdata (#2770) --- README.md | 88 ++++----- cmd/zz_gen_cmd_dnshelp.go | 22 +++ docs/content/dns/zz_gen_alwaysdata.md | 68 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/alwaysdata/alwaysdata.go | 185 +++++++++++++++++ providers/dns/alwaysdata/alwaysdata.toml | 26 +++ providers/dns/alwaysdata/alwaysdata_test.go | 187 ++++++++++++++++++ providers/dns/alwaysdata/internal/client.go | 177 +++++++++++++++++ .../dns/alwaysdata/internal/client_test.go | 124 ++++++++++++ .../alwaysdata/internal/fixtures/domains.json | 16 ++ .../internal/fixtures/record_add-request.json | 8 + .../alwaysdata/internal/fixtures/records.json | 28 +++ providers/dns/alwaysdata/internal/types.go | 33 ++++ providers/dns/zz_gen_dns_providers.go | 3 + 14 files changed, 922 insertions(+), 45 deletions(-) create mode 100644 docs/content/dns/zz_gen_alwaysdata.md create mode 100644 providers/dns/alwaysdata/alwaysdata.go create mode 100644 providers/dns/alwaysdata/alwaysdata.toml create mode 100644 providers/dns/alwaysdata/alwaysdata_test.go create mode 100644 providers/dns/alwaysdata/internal/client.go create mode 100644 providers/dns/alwaysdata/internal/client_test.go create mode 100644 providers/dns/alwaysdata/internal/fixtures/domains.json create mode 100644 providers/dns/alwaysdata/internal/fixtures/record_add-request.json create mode 100644 providers/dns/alwaysdata/internal/fixtures/records.json create mode 100644 providers/dns/alwaysdata/internal/types.go diff --git a/README.md b/README.md index 5fa9c1ed1..aff5052ca 100644 --- a/README.md +++ b/README.md @@ -65,224 +65,224 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). AlibabaCloud ESA all-inkl + Alwaysdata Amazon Lightsail Amazon Route 53 - Anexia CloudDNS + Anexia CloudDNS ArvanCloud Aurora DNS Autodns - Axelname + Axelname Azion Azure (deprecated) Azure DNS - Baidu Cloud + Baidu Cloud Beget.com Binary Lane Bindman - Bluecat + Bluecat BookMyName Brandit (deprecated) Bunny - Checkdomain + Checkdomain Civo Cloud.ru CloudDNS - Cloudflare + Cloudflare ClouDNS CloudXNS (Deprecated) ConoHa v2 - ConoHa v3 + ConoHa v3 Constellix Core-Networks CPanel/WHM - Derak Cloud + Derak Cloud deSEC.io Designate DNSaaS for Openstack Digital Ocean - DirectAdmin + DirectAdmin DNS Made Easy dnsHome.de DNSimple - DNSPod (deprecated) + DNSPod (deprecated) Domain Offensive (do.de) Domeneshop DreamHost - Duck DNS + Duck DNS Dyn DynDnsFree.de Dynu - EasyDNS + EasyDNS EdgeCenter Efficient IP Epik - Exoscale + Exoscale External program F5 XC freemyip.com - G-Core + G-Core Gandi Gandi Live DNS (v5) Gigahost.no - Glesys + Glesys Go Daddy Google Cloud Google Domains - Gravity + Gravity Hetzner Hosting.de Hosting.nl - Hostinger + Hostinger Hosttech HTTP request http.net - Huawei Cloud + Huawei Cloud Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) - IIJ DNS Platform Service + IIJ DNS Platform Service Infoblox Infomaniak Internet Initiative Japan - Internet.bs + Internet.bs INWX Ionos Ionos Cloud - IPv64 + IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) - Joker + Joker Joohoi's ACME-DNS KeyHelp Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Neodigit - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Syse Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TransIP UKFast SafeDNS - Ultradns + Ultradns United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS Virtualname - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 601222903..220289242 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -17,6 +17,7 @@ func allDNSCodes() string { "alidns", "aliesa", "allinkl", + "alwaysdata", "anexia", "arvancloud", "auroradns", @@ -306,6 +307,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`) + case "alwaysdata": + // generated from: providers/dns/alwaysdata/alwaysdata.toml + ew.writeln(`Configuration for Alwaysdata.`) + ew.writeln(`Code: 'alwaysdata'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ALWAYSDATA_API_KEY": API Key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ALWAYSDATA_ACCOUNT": Account name`) + ew.writeln(` - "ALWAYSDATA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ALWAYSDATA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ALWAYSDATA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ALWAYSDATA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/alwaysdata`) + case "anexia": // generated from: providers/dns/anexia/anexia.toml ew.writeln(`Configuration for Anexia CloudDNS.`) diff --git a/docs/content/dns/zz_gen_alwaysdata.md b/docs/content/dns/zz_gen_alwaysdata.md new file mode 100644 index 000000000..75f3cb859 --- /dev/null +++ b/docs/content/dns/zz_gen_alwaysdata.md @@ -0,0 +1,68 @@ +--- +title: "Alwaysdata" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: alwaysdata +dnsprovider: + since: "v4.31.0" + code: "alwaysdata" + url: "https://alwaysdata.com/" +--- + + + + + + +Configuration for [Alwaysdata](https://alwaysdata.com/). + + + + +- Code: `alwaysdata` +- Since: v4.31.0 + + +Here is an example bash command using the Alwaysdata provider: + +```bash +ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns alwaysdata -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ALWAYSDATA_API_KEY` | API Key | + +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 | +|--------------------------------|-------------| +| `ALWAYSDATA_ACCOUNT` | Account name | +| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ALWAYSDATA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://help.alwaysdata.com/en/api/resources/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index ae2369a9a..ab9ff31c9 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, 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, 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/alwaysdata/alwaysdata.go b/providers/dns/alwaysdata/alwaysdata.go new file mode 100644 index 000000000..b2e0f3957 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata.go @@ -0,0 +1,185 @@ +// Package alwaysdata implements a DNS provider for solving the DNS-01 challenge using Alwaysdata. +package alwaysdata + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/alwaysdata/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "ALWAYSDATA_" + + EnvAPIKey = envNamespace + "API_KEY" + EnvAccount = envNamespace + "ACCOUNT" + + 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 { + APIKey string + Account string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Alwaysdata. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("alwaysdata: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + config.Account = env.GetOrFile(EnvAccount) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Alwaysdata. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("alwaysdata: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey, config.Account) + if err != nil { + return nil, fmt.Errorf("alwaysdata: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + record := internal.RecordRequest{ + DomainID: zone.ID, + Name: subDomain, + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + Annotation: "lego", + } + + err = d.client.AddRecord(ctx, record) + if err != nil { + return fmt.Errorf("alwaysdata: add TXT record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) + if err != nil { + return fmt.Errorf("alwaysdata: %w", err) + } + + records, err := d.client.ListRecords(ctx, zone.ID, subDomain) + if err != nil { + return fmt.Errorf("alwaysdata: list records: %w", err) + } + + for _, record := range records { + if record.Type != "TXT" || record.Value != info.Value { + continue + } + + err = d.client.DeleteRecord(ctx, record.ID) + if err != nil { + return fmt.Errorf("alwaysdata: delete TXT record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Domain, error) { + domains, err := d.client.ListDomains(ctx) + if err != nil { + return nil, fmt.Errorf("list domains: %w", err) + } + + for a := range dns01.UnFqdnDomainsSeq(fqdn) { + for _, domain := range domains { + if a == domain.Name { + return &domain, nil + } + } + } + + return nil, errors.New("domain not found") +} diff --git a/providers/dns/alwaysdata/alwaysdata.toml b/providers/dns/alwaysdata/alwaysdata.toml new file mode 100644 index 000000000..96d8d9616 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata.toml @@ -0,0 +1,26 @@ +Name = "Alwaysdata" +Description = '''''' +URL = "https://alwaysdata.com/" +Code = "alwaysdata" +Since = "v4.31.0" + +Example = ''' +ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --email you@example.com --dns alwaysdata -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ALWAYSDATA_API_KEY = "API Key" + [Configuration.Additional] + ALWAYSDATA_ACCOUNT = "Account name" + ALWAYSDATA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ALWAYSDATA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ALWAYSDATA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ALWAYSDATA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://help.alwaysdata.com/en/api/resources/" + APIDocDomains = "https://api.alwaysdata.com/v1/domain/doc/" + APIDocRecords = "https://api.alwaysdata.com/v1/record/doc/" + APIExamples = "https://help.alwaysdata.com/en/api/examples/" diff --git a/providers/dns/alwaysdata/alwaysdata_test.go b/providers/dns/alwaysdata/alwaysdata_test.go new file mode 100644 index 000000000..6084c2ae4 --- /dev/null +++ b/providers/dns/alwaysdata/alwaysdata_test.go @@ -0,0 +1,187 @@ +package alwaysdata + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey, EnvAccount).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "secret", + }, + }, + { + desc: "success with an account", + envVars: map[string]string{ + EnvAPIKey: "secret", + EnvAccount: "foo", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "alwaysdata: some credentials information are missing: ALWAYSDATA_API_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 + apiKey string + account string + expected string + }{ + { + desc: "success", + apiKey: "secret", + }, + { + desc: "success with an account", + apiKey: "secret", + account: "foo", + }, + { + desc: "missing credentials", + expected: "alwaysdata: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.Account = test.account + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithBasicAuth("secret", ""), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromInternal("domains.json")). + Route("POST /record/", + servermock.Noop().WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("record_add-request.json")). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromInternal("domains.json")). + Route("GET /record/", + servermock.ResponseFromInternal("records.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "132"). + With("name", "_acme-challenge"), + ). + Route("DELETE /record/789/", + servermock.Noop()). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/alwaysdata/internal/client.go b/providers/dns/alwaysdata/internal/client.go new file mode 100644 index 000000000..5db11dcd1 --- /dev/null +++ b/providers/dns/alwaysdata/internal/client.go @@ -0,0 +1,177 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.alwaysdata.com/v1" + +// Client the Alwaysdata API client. +type Client struct { + apiKey string + account string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiKey, account string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + account: account, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { + endpoint := c.BaseURL.JoinPath("domain", "/") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []Domain + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) AddRecord(ctx context.Context, record RecordRequest) error { + endpoint := c.BaseURL.JoinPath("record", "/") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + err = c.do(req, nil) + if err != nil { + return err + } + + return nil +} + +func (c *Client) DeleteRecord(ctx context.Context, recordID int64) error { + endpoint := c.BaseURL.JoinPath("record", strconv.FormatInt(recordID, 10), "/") + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) ListRecords(ctx context.Context, domainID int64, name string) ([]Record, error) { + endpoint := c.BaseURL.JoinPath("record", "/") + + query := endpoint.Query() + query.Set("domain", strconv.FormatInt(domainID, 10)) + query.Set("name", name) + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []Record + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + user := c.apiKey + + if c.account != "" { + user += "account=" + c.account + } + + req.SetBasicAuth(user, "") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/alwaysdata/internal/client_test.go b/providers/dns/alwaysdata/internal/client_test.go new file mode 100644 index 000000000..e6a349662 --- /dev/null +++ b/providers/dns/alwaysdata/internal/client_test.go @@ -0,0 +1,124 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret", "") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = clientdebug.Wrap(server.Client()) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + WithBasicAuth("secret", ""), + ) +} + +func TestClient_ListDomains(t *testing.T) { + client := mockBuilder(). + Route("GET /domain/", + servermock.ResponseFromFixture("domains.json")). + Build(t) + + result, err := client.ListDomains(t.Context()) + require.NoError(t, err) + + expected := []Domain{ + {ID: 132, Name: "example.com", Annotation: "test"}, + {ID: 133, Name: "example.net", IsInternal: true}, + {ID: 134, Name: "example.org"}, + } + + assert.Equal(t, expected, result) +} + +func TestClient_AddRecord(t *testing.T) { + t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") + + client := mockBuilder(). + Route("POST /record/", + servermock.Noop().WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromFixture("record_add-request.json")). + Build(t) + + record := RecordRequest{ + DomainID: 132, + Name: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + Annotation: "lego", + } + + err := client.AddRecord(t.Context(), record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /record/789/", + servermock.Noop()). + Build(t) + + err := client.DeleteRecord(t.Context(), 789) + require.NoError(t, err) +} + +func TestClient_ListRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /record/", + servermock.ResponseFromFixture("records.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "132"). + With("name", "_acme-challenge"), + ). + Build(t) + + result, err := client.ListRecords(t.Context(), 132, "_acme-challenge") + require.NoError(t, err) + + expected := []Record{ + { + ID: 789, + Domain: &Domain{ + Href: "/v1/domain/132/", + }, + Type: "TXT", + Name: "_acme-challenge", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + Annotation: "lego", + }, + { + ID: 11619270, + Domain: &Domain{ + Href: "/v1/domain/118935/", + }, + Name: "home", + Type: "A", + Value: "149.202.90.65", + TTL: 300, + IsUserDefined: true, + IsActive: true, + }, + } + + assert.Equal(t, expected, result) +} diff --git a/providers/dns/alwaysdata/internal/fixtures/domains.json b/providers/dns/alwaysdata/internal/fixtures/domains.json new file mode 100644 index 000000000..dc34a948f --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/domains.json @@ -0,0 +1,16 @@ +[ + { + "id": 132, + "name": "example.com", + "annotation": "test" + }, + { + "id": 133, + "name": "example.net", + "is_internal": true + }, + { + "id": 134, + "name": "example.org" + } +] diff --git a/providers/dns/alwaysdata/internal/fixtures/record_add-request.json b/providers/dns/alwaysdata/internal/fixtures/record_add-request.json new file mode 100644 index 000000000..5b6db2646 --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/record_add-request.json @@ -0,0 +1,8 @@ +{ + "domain": 132, + "name": "_acme-challenge", + "type": "TXT", + "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "annotation": "lego" +} diff --git a/providers/dns/alwaysdata/internal/fixtures/records.json b/providers/dns/alwaysdata/internal/fixtures/records.json new file mode 100644 index 000000000..fa207395a --- /dev/null +++ b/providers/dns/alwaysdata/internal/fixtures/records.json @@ -0,0 +1,28 @@ +[ + { + "id": 789, + "domain": { + "href": "/v1/domain/132/" + }, + "name": "_acme-challenge", + "type": "TXT", + "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "annotation": "lego" + }, + { + "id": 11619270, + "domain": { + "href": "/v1/domain/118935/" + }, + "type": "A", + "name": "home", + "value": "149.202.90.65", + "priority": null, + "ttl": 300, + "href": "/v1/record/11619270/", + "annotation": "", + "is_user_defined": true, + "is_active": true + } +] diff --git a/providers/dns/alwaysdata/internal/types.go b/providers/dns/alwaysdata/internal/types.go new file mode 100644 index 000000000..b1e66fa5b --- /dev/null +++ b/providers/dns/alwaysdata/internal/types.go @@ -0,0 +1,33 @@ +package internal + +type RecordRequest struct { + ID int64 `json:"id,omitempty"` + DomainID int64 `json:"domain,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + TTL int `json:"ttl,omitempty"` + Annotation string `json:"annotation,omitempty"` + IsUserDefined bool `json:"is_user_defined,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type Record struct { + ID int64 `json:"id,omitempty"` + Domain *Domain `json:"domain,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + TTL int `json:"ttl,omitempty"` + Annotation string `json:"annotation,omitempty"` + IsUserDefined bool `json:"is_user_defined,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type Domain struct { + ID int64 `json:"id,omitempty"` + Href string `json:"href,omitempty"` + Name string `json:"name,omitempty"` + IsInternal bool `json:"is_internal,omitempty"` + Annotation string `json:"annotation,omitempty"` +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 38155e164..c5db54109 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -11,6 +11,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/go-acme/lego/v4/providers/dns/aliesa" "github.com/go-acme/lego/v4/providers/dns/allinkl" + "github.com/go-acme/lego/v4/providers/dns/alwaysdata" "github.com/go-acme/lego/v4/providers/dns/anexia" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/go-acme/lego/v4/providers/dns/auroradns" @@ -199,6 +200,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return aliesa.NewDNSProvider() case "allinkl": return allinkl.NewDNSProvider() + case "alwaysdata": + return alwaysdata.NewDNSProvider() case "anexia": return anexia.NewDNSProvider() case "arvancloud": From 1b634097c13285c9829dc9c16b74a9c86c4ff81e Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 29 Dec 2025 18:33:53 +0100 Subject: [PATCH 53/95] docs: remove email from examples (#2773) --- docs/content/dns/zz_gen_acme-dns.md | 4 ++-- docs/content/dns/zz_gen_active24.md | 2 +- docs/content/dns/zz_gen_alidns.md | 4 ++-- docs/content/dns/zz_gen_aliesa.md | 4 ++-- docs/content/dns/zz_gen_allinkl.md | 2 +- docs/content/dns/zz_gen_alwaysdata.md | 2 +- docs/content/dns/zz_gen_anexia.md | 2 +- docs/content/dns/zz_gen_arvancloud.md | 2 +- docs/content/dns/zz_gen_auroradns.md | 2 +- docs/content/dns/zz_gen_autodns.md | 2 +- docs/content/dns/zz_gen_axelname.md | 2 +- docs/content/dns/zz_gen_azion.md | 2 +- docs/content/dns/zz_gen_azuredns.md | 10 +++++----- docs/content/dns/zz_gen_baiducloud.md | 2 +- docs/content/dns/zz_gen_beget.md | 2 +- docs/content/dns/zz_gen_binarylane.md | 2 +- docs/content/dns/zz_gen_bindman.md | 2 +- docs/content/dns/zz_gen_bluecat.md | 2 +- docs/content/dns/zz_gen_bookmyname.md | 2 +- docs/content/dns/zz_gen_brandit.md | 2 +- docs/content/dns/zz_gen_bunny.md | 2 +- docs/content/dns/zz_gen_checkdomain.md | 2 +- docs/content/dns/zz_gen_civo.md | 2 +- docs/content/dns/zz_gen_clouddns.md | 2 +- docs/content/dns/zz_gen_cloudflare.md | 4 ++-- docs/content/dns/zz_gen_cloudns.md | 2 +- docs/content/dns/zz_gen_cloudru.md | 2 +- docs/content/dns/zz_gen_cloudxns.md | 2 +- docs/content/dns/zz_gen_conoha.md | 2 +- docs/content/dns/zz_gen_conohav3.md | 2 +- docs/content/dns/zz_gen_constellix.md | 2 +- docs/content/dns/zz_gen_corenetworks.md | 2 +- docs/content/dns/zz_gen_cpanel.md | 4 ++-- docs/content/dns/zz_gen_derak.md | 2 +- docs/content/dns/zz_gen_desec.md | 2 +- docs/content/dns/zz_gen_designate.md | 6 +++--- docs/content/dns/zz_gen_digitalocean.md | 2 +- docs/content/dns/zz_gen_directadmin.md | 2 +- docs/content/dns/zz_gen_dnshomede.md | 4 ++-- docs/content/dns/zz_gen_dnsimple.md | 2 +- docs/content/dns/zz_gen_dnsmadeeasy.md | 2 +- docs/content/dns/zz_gen_dnspod.md | 2 +- docs/content/dns/zz_gen_dode.md | 2 +- docs/content/dns/zz_gen_domeneshop.md | 2 +- docs/content/dns/zz_gen_dreamhost.md | 2 +- docs/content/dns/zz_gen_duckdns.md | 2 +- docs/content/dns/zz_gen_dyn.md | 2 +- docs/content/dns/zz_gen_dyndnsfree.md | 2 +- docs/content/dns/zz_gen_dynu.md | 2 +- docs/content/dns/zz_gen_easydns.md | 2 +- docs/content/dns/zz_gen_edgecenter.md | 2 +- docs/content/dns/zz_gen_edgedns.md | 2 +- docs/content/dns/zz_gen_edgeone.md | 2 +- docs/content/dns/zz_gen_efficientip.md | 2 +- docs/content/dns/zz_gen_epik.md | 2 +- docs/content/dns/zz_gen_exec.md | 6 +++--- docs/content/dns/zz_gen_exoscale.md | 2 +- docs/content/dns/zz_gen_f5xc.md | 2 +- docs/content/dns/zz_gen_freemyip.md | 2 +- docs/content/dns/zz_gen_gandi.md | 2 +- docs/content/dns/zz_gen_gandiv5.md | 2 +- docs/content/dns/zz_gen_gcloud.md | 6 +++--- docs/content/dns/zz_gen_gcore.md | 2 +- docs/content/dns/zz_gen_gigahostno.md | 2 +- docs/content/dns/zz_gen_glesys.md | 2 +- docs/content/dns/zz_gen_godaddy.md | 2 +- docs/content/dns/zz_gen_googledomains.md | 2 +- docs/content/dns/zz_gen_gravity.md | 2 +- docs/content/dns/zz_gen_hetzner.md | 2 +- docs/content/dns/zz_gen_hostingde.md | 2 +- docs/content/dns/zz_gen_hostinger.md | 2 +- docs/content/dns/zz_gen_hostingnl.md | 2 +- docs/content/dns/zz_gen_hosttech.md | 2 +- docs/content/dns/zz_gen_httpnet.md | 2 +- docs/content/dns/zz_gen_httpreq.md | 2 +- docs/content/dns/zz_gen_huaweicloud.md | 2 +- docs/content/dns/zz_gen_hurricane.md | 4 ++-- docs/content/dns/zz_gen_hyperone.md | 2 +- docs/content/dns/zz_gen_ibmcloud.md | 2 +- docs/content/dns/zz_gen_iij.md | 2 +- docs/content/dns/zz_gen_iijdpf.md | 2 +- docs/content/dns/zz_gen_infoblox.md | 2 +- docs/content/dns/zz_gen_infomaniak.md | 2 +- docs/content/dns/zz_gen_internetbs.md | 2 +- docs/content/dns/zz_gen_inwx.md | 4 ++-- docs/content/dns/zz_gen_ionos.md | 2 +- docs/content/dns/zz_gen_ionoscloud.md | 2 +- docs/content/dns/zz_gen_ipv64.md | 2 +- docs/content/dns/zz_gen_ispconfig.md | 2 +- docs/content/dns/zz_gen_ispconfigddns.md | 2 +- docs/content/dns/zz_gen_iwantmyname.md | 2 +- docs/content/dns/zz_gen_joker.md | 6 +++--- docs/content/dns/zz_gen_keyhelp.md | 2 +- docs/content/dns/zz_gen_liara.md | 2 +- docs/content/dns/zz_gen_limacity.md | 2 +- docs/content/dns/zz_gen_linode.md | 2 +- docs/content/dns/zz_gen_liquidweb.md | 2 +- docs/content/dns/zz_gen_loopia.md | 2 +- docs/content/dns/zz_gen_luadns.md | 2 +- docs/content/dns/zz_gen_mailinabox.md | 2 +- docs/content/dns/zz_gen_manageengine.md | 2 +- docs/content/dns/zz_gen_manual.md | 4 ++-- docs/content/dns/zz_gen_metaname.md | 2 +- docs/content/dns/zz_gen_metaregistrar.md | 2 +- docs/content/dns/zz_gen_mijnhost.md | 2 +- docs/content/dns/zz_gen_mittwald.md | 2 +- docs/content/dns/zz_gen_myaddr.md | 2 +- docs/content/dns/zz_gen_mydnsjp.md | 2 +- docs/content/dns/zz_gen_mythicbeasts.md | 2 +- docs/content/dns/zz_gen_namecheap.md | 2 +- docs/content/dns/zz_gen_namedotcom.md | 2 +- docs/content/dns/zz_gen_namesilo.md | 2 +- docs/content/dns/zz_gen_nearlyfreespeech.md | 2 +- docs/content/dns/zz_gen_neodigit.md | 2 +- docs/content/dns/zz_gen_netcup.md | 2 +- docs/content/dns/zz_gen_netlify.md | 2 +- docs/content/dns/zz_gen_nicmanager.md | 4 ++-- docs/content/dns/zz_gen_nicru.md | 2 +- docs/content/dns/zz_gen_nifcloud.md | 2 +- docs/content/dns/zz_gen_njalla.md | 2 +- docs/content/dns/zz_gen_nodion.md | 2 +- docs/content/dns/zz_gen_ns1.md | 2 +- docs/content/dns/zz_gen_octenium.md | 2 +- docs/content/dns/zz_gen_oraclecloud.md | 4 ++-- docs/content/dns/zz_gen_otc.md | 2 +- docs/content/dns/zz_gen_ovh.md | 6 +++--- docs/content/dns/zz_gen_pdns.md | 2 +- docs/content/dns/zz_gen_plesk.md | 2 +- docs/content/dns/zz_gen_porkbun.md | 2 +- docs/content/dns/zz_gen_rackspace.md | 2 +- docs/content/dns/zz_gen_rainyun.md | 2 +- docs/content/dns/zz_gen_rcodezero.md | 2 +- docs/content/dns/zz_gen_regfish.md | 2 +- docs/content/dns/zz_gen_regru.md | 2 +- docs/content/dns/zz_gen_rfc2136.md | 4 ++-- docs/content/dns/zz_gen_rimuhosting.md | 2 +- docs/content/dns/zz_gen_route53.md | 2 +- docs/content/dns/zz_gen_safedns.md | 2 +- docs/content/dns/zz_gen_sakuracloud.md | 2 +- docs/content/dns/zz_gen_scaleway.md | 2 +- docs/content/dns/zz_gen_selectel.md | 2 +- docs/content/dns/zz_gen_selectelv2.md | 2 +- docs/content/dns/zz_gen_selfhostde.md | 2 +- docs/content/dns/zz_gen_servercow.md | 2 +- docs/content/dns/zz_gen_shellrent.md | 2 +- docs/content/dns/zz_gen_simply.md | 2 +- docs/content/dns/zz_gen_sonic.md | 2 +- docs/content/dns/zz_gen_spaceship.md | 2 +- docs/content/dns/zz_gen_stackpath.md | 2 +- docs/content/dns/zz_gen_syse.md | 4 ++-- docs/content/dns/zz_gen_technitium.md | 2 +- docs/content/dns/zz_gen_tencentcloud.md | 2 +- docs/content/dns/zz_gen_timewebcloud.md | 2 +- docs/content/dns/zz_gen_transip.md | 2 +- docs/content/dns/zz_gen_ultradns.md | 2 +- docs/content/dns/zz_gen_uniteddomains.md | 2 +- docs/content/dns/zz_gen_variomedia.md | 2 +- docs/content/dns/zz_gen_vercel.md | 2 +- docs/content/dns/zz_gen_versio.md | 2 +- docs/content/dns/zz_gen_vinyldns.md | 2 +- docs/content/dns/zz_gen_virtualname.md | 2 +- docs/content/dns/zz_gen_vkcloud.md | 2 +- docs/content/dns/zz_gen_volcengine.md | 2 +- docs/content/dns/zz_gen_vscale.md | 2 +- docs/content/dns/zz_gen_vultr.md | 2 +- docs/content/dns/zz_gen_webnames.md | 2 +- docs/content/dns/zz_gen_webnamesca.md | 2 +- docs/content/dns/zz_gen_websupport.md | 2 +- docs/content/dns/zz_gen_wedos.md | 2 +- docs/content/dns/zz_gen_westcn.md | 2 +- docs/content/dns/zz_gen_yandex.md | 2 +- docs/content/dns/zz_gen_yandex360.md | 2 +- docs/content/dns/zz_gen_yandexcloud.md | 4 ++-- docs/content/dns/zz_gen_zoneedit.md | 2 +- docs/content/dns/zz_gen_zoneee.md | 2 +- docs/content/dns/zz_gen_zonomi.md | 2 +- providers/dns/acmedns/acmedns.toml | 4 ++-- providers/dns/active24/active24.toml | 2 +- providers/dns/alidns/alidns.toml | 4 ++-- providers/dns/aliesa/aliesa.toml | 4 ++-- providers/dns/allinkl/allinkl.toml | 2 +- providers/dns/alwaysdata/alwaysdata.toml | 2 +- providers/dns/anexia/anexia.toml | 2 +- providers/dns/arvancloud/arvancloud.toml | 2 +- providers/dns/auroradns/auroradns.toml | 2 +- providers/dns/autodns/autodns.toml | 2 +- providers/dns/axelname/axelname.toml | 2 +- providers/dns/azion/azion.toml | 2 +- providers/dns/azuredns/azuredns.toml | 10 +++++----- providers/dns/baiducloud/baiducloud.toml | 2 +- providers/dns/beget/beget.toml | 2 +- providers/dns/binarylane/binarylane.toml | 2 +- providers/dns/bindman/bindman.toml | 2 +- providers/dns/bluecat/bluecat.toml | 2 +- providers/dns/bookmyname/bookmyname.toml | 2 +- providers/dns/brandit/brandit.toml | 2 +- providers/dns/bunny/bunny.toml | 2 +- providers/dns/checkdomain/checkdomain.toml | 2 +- providers/dns/civo/civo.toml | 2 +- providers/dns/clouddns/clouddns.toml | 2 +- providers/dns/cloudflare/cloudflare.toml | 4 ++-- providers/dns/cloudns/cloudns.toml | 2 +- providers/dns/cloudru/cloudru.toml | 2 +- providers/dns/cloudxns/cloudxns.toml | 2 +- providers/dns/conoha/conoha.toml | 2 +- providers/dns/conohav3/conohav3.toml | 2 +- providers/dns/constellix/constellix.toml | 2 +- providers/dns/corenetworks/corenetworks.toml | 2 +- providers/dns/cpanel/cpanel.toml | 4 ++-- providers/dns/derak/derak.toml | 2 +- providers/dns/desec/desec.toml | 2 +- providers/dns/designate/designate.toml | 6 +++--- providers/dns/digitalocean/digitalocean.toml | 2 +- providers/dns/directadmin/directadmin.toml | 2 +- providers/dns/dnshomede/dnshomede.toml | 4 ++-- providers/dns/dnsimple/dnsimple.toml | 2 +- providers/dns/dnsmadeeasy/dnsmadeeasy.toml | 2 +- providers/dns/dnspod/dnspod.toml | 2 +- providers/dns/dode/dode.toml | 2 +- providers/dns/domeneshop/domeneshop.toml | 2 +- providers/dns/dreamhost/dreamhost.toml | 2 +- providers/dns/duckdns/duckdns.toml | 2 +- providers/dns/dyn/dyn.toml | 2 +- providers/dns/dyndnsfree/dyndnsfree.toml | 2 +- providers/dns/dynu/dynu.toml | 2 +- providers/dns/easydns/easydns.toml | 2 +- providers/dns/edgecenter/edgecenter.toml | 2 +- providers/dns/edgedns/edgedns.toml | 2 +- providers/dns/edgeone/edgeone.toml | 2 +- providers/dns/efficientip/efficientip.toml | 2 +- providers/dns/epik/epik.toml | 2 +- providers/dns/exec/exec.toml | 6 +++--- providers/dns/exoscale/exoscale.toml | 2 +- providers/dns/f5xc/f5xc.toml | 2 +- providers/dns/freemyip/freemyip.toml | 2 +- providers/dns/gandi/gandi.toml | 2 +- providers/dns/gandiv5/gandiv5.toml | 2 +- providers/dns/gcloud/gcloud.toml | 6 +++--- providers/dns/gcore/gcore.toml | 2 +- providers/dns/gigahostno/gigahostno.toml | 2 +- providers/dns/glesys/glesys.toml | 2 +- providers/dns/godaddy/godaddy.toml | 2 +- providers/dns/googledomains/googledomains.toml | 2 +- providers/dns/gravity/gravity.toml | 2 +- providers/dns/hetzner/hetzner.toml | 2 +- providers/dns/hostingde/hostingde.toml | 2 +- providers/dns/hostinger/hostinger.toml | 2 +- providers/dns/hostingnl/hostingnl.toml | 2 +- providers/dns/hosttech/hosttech.toml | 2 +- providers/dns/httpnet/httpnet.toml | 2 +- providers/dns/httpreq/httpreq.toml | 2 +- providers/dns/huaweicloud/huaweicloud.toml | 2 +- providers/dns/hurricane/hurricane.toml | 4 ++-- providers/dns/hyperone/hyperone.toml | 2 +- providers/dns/ibmcloud/ibmcloud.toml | 2 +- providers/dns/iij/iij.toml | 2 +- providers/dns/iijdpf/iijdpf.toml | 2 +- providers/dns/infoblox/infoblox.toml | 2 +- providers/dns/infomaniak/infomaniak.toml | 2 +- providers/dns/internetbs/internetbs.toml | 2 +- providers/dns/inwx/inwx.toml | 4 ++-- providers/dns/ionos/ionos.toml | 2 +- providers/dns/ionoscloud/ionoscloud.toml | 2 +- providers/dns/ipv64/ipv64.toml | 2 +- providers/dns/ispconfig/ispconfig.toml | 2 +- providers/dns/ispconfigddns/ispconfigddns.toml | 2 +- providers/dns/iwantmyname/iwantmyname.toml | 2 +- providers/dns/joker/joker.toml | 6 +++--- providers/dns/keyhelp/keyhelp.toml | 2 +- providers/dns/liara/liara.toml | 2 +- providers/dns/limacity/limacity.toml | 2 +- providers/dns/linode/linode.toml | 2 +- providers/dns/liquidweb/liquidweb.toml | 2 +- providers/dns/loopia/loopia.toml | 2 +- providers/dns/luadns/luadns.toml | 2 +- providers/dns/mailinabox/mailinabox.toml | 2 +- providers/dns/manageengine/manageengine.toml | 2 +- providers/dns/manual/manual.toml | 4 ++-- providers/dns/metaname/metaname.toml | 2 +- providers/dns/metaregistrar/metaregistrar.toml | 2 +- providers/dns/mijnhost/mijnhost.toml | 2 +- providers/dns/mittwald/mittwald.toml | 2 +- providers/dns/myaddr/myaddr.toml | 2 +- providers/dns/mydnsjp/mydnsjp.toml | 2 +- providers/dns/mythicbeasts/mythicbeasts.toml | 2 +- providers/dns/namecheap/namecheap.toml | 2 +- providers/dns/namedotcom/namedotcom.toml | 2 +- providers/dns/namesilo/namesilo.toml | 2 +- providers/dns/nearlyfreespeech/nearlyfreespeech.toml | 2 +- providers/dns/neodigit/neodigit.toml | 2 +- providers/dns/netcup/netcup.toml | 2 +- providers/dns/netlify/netlify.toml | 2 +- providers/dns/nicmanager/nicmanager.toml | 4 ++-- providers/dns/nicru/nicru.toml | 2 +- providers/dns/nifcloud/nifcloud.toml | 2 +- providers/dns/njalla/njalla.toml | 2 +- providers/dns/nodion/nodion.toml | 2 +- providers/dns/ns1/ns1.toml | 2 +- providers/dns/octenium/octenium.toml | 2 +- providers/dns/oraclecloud/oraclecloud.toml | 4 ++-- providers/dns/otc/otc.toml | 2 +- providers/dns/ovh/ovh.toml | 6 +++--- providers/dns/pdns/pdns.toml | 2 +- providers/dns/plesk/plesk.toml | 2 +- providers/dns/porkbun/porkbun.toml | 2 +- providers/dns/rackspace/rackspace.toml | 2 +- providers/dns/rainyun/rainyun.toml | 2 +- providers/dns/rcodezero/rcodezero.toml | 2 +- providers/dns/regfish/regfish.toml | 2 +- providers/dns/regru/regru.toml | 2 +- providers/dns/rfc2136/rfc2136.toml | 4 ++-- providers/dns/rimuhosting/rimuhosting.toml | 2 +- providers/dns/route53/route53.toml | 2 +- providers/dns/safedns/safedns.toml | 2 +- providers/dns/sakuracloud/sakuracloud.toml | 2 +- providers/dns/scaleway/scaleway.toml | 2 +- providers/dns/selectel/selectel.toml | 2 +- providers/dns/selectelv2/selectelv2.toml | 2 +- providers/dns/selfhostde/selfhostde.toml | 2 +- providers/dns/servercow/servercow.toml | 2 +- providers/dns/shellrent/shellrent.toml | 2 +- providers/dns/simply/simply.toml | 2 +- providers/dns/sonic/sonic.toml | 2 +- providers/dns/spaceship/spaceship.toml | 2 +- providers/dns/stackpath/stackpath.toml | 2 +- providers/dns/syse/syse.toml | 4 ++-- providers/dns/technitium/technitium.toml | 2 +- providers/dns/tencentcloud/tencentcloud.toml | 2 +- providers/dns/timewebcloud/timewebcloud.toml | 2 +- providers/dns/transip/transip.toml | 2 +- providers/dns/ultradns/ultradns.toml | 2 +- providers/dns/uniteddomains/uniteddomains.toml | 2 +- providers/dns/variomedia/variomedia.toml | 2 +- providers/dns/vercel/vercel.toml | 2 +- providers/dns/versio/versio.toml | 2 +- providers/dns/vinyldns/vinyldns.toml | 2 +- providers/dns/virtualname/virtualname.toml | 2 +- providers/dns/vkcloud/vkcloud.toml | 2 +- providers/dns/volcengine/volcengine.toml | 2 +- providers/dns/vscale/vscale.toml | 2 +- providers/dns/vultr/vultr.toml | 2 +- providers/dns/webnames/webnames.toml | 2 +- providers/dns/webnamesca/webnamesca.toml | 2 +- providers/dns/websupport/websupport.toml | 2 +- providers/dns/wedos/wedos.toml | 2 +- providers/dns/westcn/westcn.toml | 2 +- providers/dns/yandex/yandex.toml | 2 +- providers/dns/yandex360/yandex360.toml | 2 +- providers/dns/yandexcloud/yandexcloud.toml | 4 ++-- providers/dns/zoneedit/zoneedit.toml | 2 +- providers/dns/zoneee/zoneee.toml | 2 +- providers/dns/zonomi/zonomi.toml | 2 +- 352 files changed, 408 insertions(+), 408 deletions(-) diff --git a/docs/content/dns/zz_gen_acme-dns.md b/docs/content/dns/zz_gen_acme-dns.md index cb3d24016..5564dba1b 100644 --- a/docs/content/dns/zz_gen_acme-dns.md +++ b/docs/content/dns/zz_gen_acme-dns.md @@ -28,13 +28,13 @@ Here is an example bash command using the Joohoi's ACME-DNS provider: ```bash ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run # or ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_active24.md b/docs/content/dns/zz_gen_active24.md index cadc6660c..6ec5c467a 100644 --- a/docs/content/dns/zz_gen_active24.md +++ b/docs/content/dns/zz_gen_active24.md @@ -28,7 +28,7 @@ Here is an example bash command using the Active24 provider: ```bash ACTIVE24_API_KEY="xxx" \ ACTIVE24_SECRET="yyy" \ -lego --email you@example.com --dns active24 -d '*.example.com' -d example.com run +lego --dns active24 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_alidns.md b/docs/content/dns/zz_gen_alidns.md index 7a7a36e8a..e498a31dd 100644 --- a/docs/content/dns/zz_gen_alidns.md +++ b/docs/content/dns/zz_gen_alidns.md @@ -28,13 +28,13 @@ Here is an example bash command using the Alibaba Cloud DNS provider: ```bash # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run +lego --dns alidns -d '*.example.com' -d example.com run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run +lego --dns alidns - -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_aliesa.md b/docs/content/dns/zz_gen_aliesa.md index b286a718a..af28f9a4e 100644 --- a/docs/content/dns/zz_gen_aliesa.md +++ b/docs/content/dns/zz_gen_aliesa.md @@ -28,13 +28,13 @@ Here is an example bash command using the AlibabaCloud ESA provider: ```bash # Setup using instance RAM role ALIESA_RAM_ROLE=lego \ -lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run +lego --dns aliesa -d '*.example.com' -d example.com run # Or, using credentials ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALIESA_SECRET_KEY=your-secret-key \ ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +lego --dns aliesa - -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_allinkl.md b/docs/content/dns/zz_gen_allinkl.md index 2415c812f..2db6ae2c5 100644 --- a/docs/content/dns/zz_gen_allinkl.md +++ b/docs/content/dns/zz_gen_allinkl.md @@ -28,7 +28,7 @@ Here is an example bash command using the all-inkl provider: ```bash ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run +lego --dns allinkl -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_alwaysdata.md b/docs/content/dns/zz_gen_alwaysdata.md index 75f3cb859..6ec332d16 100644 --- a/docs/content/dns/zz_gen_alwaysdata.md +++ b/docs/content/dns/zz_gen_alwaysdata.md @@ -27,7 +27,7 @@ Here is an example bash command using the Alwaysdata provider: ```bash ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns alwaysdata -d '*.example.com' -d example.com run +lego --dns alwaysdata -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_anexia.md b/docs/content/dns/zz_gen_anexia.md index 4256d957c..e12ec7cfd 100644 --- a/docs/content/dns/zz_gen_anexia.md +++ b/docs/content/dns/zz_gen_anexia.md @@ -27,7 +27,7 @@ Here is an example bash command using the Anexia CloudDNS provider: ```bash ANEXIA_TOKEN=xxx \ -lego --email you@example.com --dns anexia -d '*.example.com' -d example.com run +lego --dns anexia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_arvancloud.md b/docs/content/dns/zz_gen_arvancloud.md index b9fa1af8d..96d495f71 100644 --- a/docs/content/dns/zz_gen_arvancloud.md +++ b/docs/content/dns/zz_gen_arvancloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the ArvanCloud provider: ```bash ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --email you@example.com --dns arvancloud -d '*.example.com' -d example.com run +lego --dns arvancloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_auroradns.md b/docs/content/dns/zz_gen_auroradns.md index 9fffe34bc..d608c85bb 100644 --- a/docs/content/dns/zz_gen_auroradns.md +++ b/docs/content/dns/zz_gen_auroradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Aurora DNS provider: ```bash AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --email you@example.com --dns auroradns -d '*.example.com' -d example.com run +lego --dns auroradns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_autodns.md b/docs/content/dns/zz_gen_autodns.md index 73f41b980..f1f25e916 100644 --- a/docs/content/dns/zz_gen_autodns.md +++ b/docs/content/dns/zz_gen_autodns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Autodns provider: ```bash AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --email you@example.com --dns autodns -d '*.example.com' -d example.com run +lego --dns autodns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_axelname.md b/docs/content/dns/zz_gen_axelname.md index b1bb3e166..91476e521 100644 --- a/docs/content/dns/zz_gen_axelname.md +++ b/docs/content/dns/zz_gen_axelname.md @@ -28,7 +28,7 @@ Here is an example bash command using the Axelname provider: ```bash AXELNAME_NICKNAME="yyy" \ AXELNAME_TOKEN="xxx" \ -lego --email you@example.com --dns axelname -d '*.example.com' -d example.com run +lego --dns axelname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_azion.md b/docs/content/dns/zz_gen_azion.md index af2a281b0..c5ca33552 100644 --- a/docs/content/dns/zz_gen_azion.md +++ b/docs/content/dns/zz_gen_azion.md @@ -27,7 +27,7 @@ Here is an example bash command using the Azion provider: ```bash AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns azion -d '*.example.com' -d example.com run +lego --dns azion -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_azuredns.md b/docs/content/dns/zz_gen_azuredns.md index 85feaae88..3b2586711 100644 --- a/docs/content/dns/zz_gen_azuredns.md +++ b/docs/content/dns/zz_gen_azuredns.md @@ -31,32 +31,32 @@ Here is an example bash command using the Azure DNS provider: AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_baiducloud.md b/docs/content/dns/zz_gen_baiducloud.md index 9f59aa156..59a2f9a2d 100644 --- a/docs/content/dns/zz_gen_baiducloud.md +++ b/docs/content/dns/zz_gen_baiducloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Baidu Cloud provider: ```bash BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --email you@example.com --dns baiducloud -d '*.example.com' -d example.com run +lego --dns baiducloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_beget.md b/docs/content/dns/zz_gen_beget.md index ae1d16a7c..3f03a2ac5 100644 --- a/docs/content/dns/zz_gen_beget.md +++ b/docs/content/dns/zz_gen_beget.md @@ -28,7 +28,7 @@ Here is an example bash command using the Beget.com provider: ```bash BEGET_USERNAME=xxxxxx \ BEGET_PASSWORD=yyyyyy \ -lego --email you@example.com --dns beget -d '*.example.com' -d example.com run +lego --dns beget -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_binarylane.md b/docs/content/dns/zz_gen_binarylane.md index 4d65bb0bc..eebf3c54e 100644 --- a/docs/content/dns/zz_gen_binarylane.md +++ b/docs/content/dns/zz_gen_binarylane.md @@ -27,7 +27,7 @@ Here is an example bash command using the Binary Lane provider: ```bash BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run +lego --dns binarylane -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bindman.md b/docs/content/dns/zz_gen_bindman.md index e12f25b7a..fcceb8962 100644 --- a/docs/content/dns/zz_gen_bindman.md +++ b/docs/content/dns/zz_gen_bindman.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bindman provider: ```bash BINDMAN_MANAGER_ADDRESS= \ -lego --email you@example.com --dns bindman -d '*.example.com' -d example.com run +lego --dns bindman -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bluecat.md b/docs/content/dns/zz_gen_bluecat.md index ee45c7f8b..2d9eb5b48 100644 --- a/docs/content/dns/zz_gen_bluecat.md +++ b/docs/content/dns/zz_gen_bluecat.md @@ -32,7 +32,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --email you@example.com --dns bluecat -d '*.example.com' -d example.com run +lego --dns bluecat -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bookmyname.md b/docs/content/dns/zz_gen_bookmyname.md index 3f5d1f2c3..cb7e1d3a1 100644 --- a/docs/content/dns/zz_gen_bookmyname.md +++ b/docs/content/dns/zz_gen_bookmyname.md @@ -28,7 +28,7 @@ Here is an example bash command using the BookMyName provider: ```bash BOOKMYNAME_USERNAME="xxx" \ BOOKMYNAME_PASSWORD="yyy" \ -lego --email you@example.com --dns bookmyname -d '*.example.com' -d example.com run +lego --dns bookmyname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_brandit.md b/docs/content/dns/zz_gen_brandit.md index 5d1f91214..fdb538684 100644 --- a/docs/content/dns/zz_gen_brandit.md +++ b/docs/content/dns/zz_gen_brandit.md @@ -31,7 +31,7 @@ Here is an example bash command using the Brandit (deprecated) provider: ```bash BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns brandit -d '*.example.com' -d example.com run +lego --dns brandit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_bunny.md b/docs/content/dns/zz_gen_bunny.md index 884c61aea..63c30782a 100644 --- a/docs/content/dns/zz_gen_bunny.md +++ b/docs/content/dns/zz_gen_bunny.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bunny provider: ```bash BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns bunny -d '*.example.com' -d example.com run +lego --dns bunny -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_checkdomain.md b/docs/content/dns/zz_gen_checkdomain.md index 516d87880..e0275f6c9 100644 --- a/docs/content/dns/zz_gen_checkdomain.md +++ b/docs/content/dns/zz_gen_checkdomain.md @@ -27,7 +27,7 @@ Here is an example bash command using the Checkdomain provider: ```bash CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --email you@example.com --dns checkdomain -d '*.example.com' -d example.com run +lego --dns checkdomain -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_civo.md b/docs/content/dns/zz_gen_civo.md index a2cffe27c..61303b539 100644 --- a/docs/content/dns/zz_gen_civo.md +++ b/docs/content/dns/zz_gen_civo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Civo provider: ```bash CIVO_TOKEN=xxxxxx \ -lego --email you@example.com --dns civo -d '*.example.com' -d example.com run +lego --dns civo -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_clouddns.md b/docs/content/dns/zz_gen_clouddns.md index 27a254873..d10d1d6a1 100644 --- a/docs/content/dns/zz_gen_clouddns.md +++ b/docs/content/dns/zz_gen_clouddns.md @@ -29,7 +29,7 @@ Here is an example bash command using the CloudDNS provider: CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --email you@example.com --dns clouddns -d '*.example.com' -d example.com run +lego --dns clouddns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudflare.md b/docs/content/dns/zz_gen_cloudflare.md index 0fd1d440e..f3390a5fd 100644 --- a/docs/content/dns/zz_gen_cloudflare.md +++ b/docs/content/dns/zz_gen_cloudflare.md @@ -28,12 +28,12 @@ Here is an example bash command using the Cloudflare provider: ```bash CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudns.md b/docs/content/dns/zz_gen_cloudns.md index 01d4b7815..26bd838f2 100644 --- a/docs/content/dns/zz_gen_cloudns.md +++ b/docs/content/dns/zz_gen_cloudns.md @@ -28,7 +28,7 @@ Here is an example bash command using the ClouDNS provider: ```bash CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --email you@example.com --dns cloudns -d '*.example.com' -d example.com run +lego --dns cloudns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudru.md b/docs/content/dns/zz_gen_cloudru.md index 52190b031..6dc3b0030 100644 --- a/docs/content/dns/zz_gen_cloudru.md +++ b/docs/content/dns/zz_gen_cloudru.md @@ -29,7 +29,7 @@ Here is an example bash command using the Cloud.ru provider: CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --email you@example.com --dns cloudru -d '*.example.com' -d example.com run +lego --dns cloudru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cloudxns.md b/docs/content/dns/zz_gen_cloudxns.md index 0b290b693..b26e5ddb5 100644 --- a/docs/content/dns/zz_gen_cloudxns.md +++ b/docs/content/dns/zz_gen_cloudxns.md @@ -28,7 +28,7 @@ Here is an example bash command using the CloudXNS (Deprecated) provider: ```bash CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --email you@example.com --dns cloudxns -d '*.example.com' -d example.com run +lego --dns cloudxns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_conoha.md b/docs/content/dns/zz_gen_conoha.md index 4d5f84660..08a979b31 100644 --- a/docs/content/dns/zz_gen_conoha.md +++ b/docs/content/dns/zz_gen_conoha.md @@ -29,7 +29,7 @@ Here is an example bash command using the ConoHa v2 provider: CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conoha -d '*.example.com' -d example.com run +lego --dns conoha -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_conohav3.md b/docs/content/dns/zz_gen_conohav3.md index 208f2f91b..e473f9434 100644 --- a/docs/content/dns/zz_gen_conohav3.md +++ b/docs/content/dns/zz_gen_conohav3.md @@ -29,7 +29,7 @@ Here is an example bash command using the ConoHa v3 provider: CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHAV3_API_USER_ID=xxxx \ CONOHAV3_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conohav3 -d '*.example.com' -d example.com run +lego --dns conohav3 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_constellix.md b/docs/content/dns/zz_gen_constellix.md index 23628e001..d4ce02bac 100644 --- a/docs/content/dns/zz_gen_constellix.md +++ b/docs/content/dns/zz_gen_constellix.md @@ -28,7 +28,7 @@ Here is an example bash command using the Constellix provider: ```bash CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns constellix -d '*.example.com' -d example.com run +lego --dns constellix -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_corenetworks.md b/docs/content/dns/zz_gen_corenetworks.md index dc756647e..05468b1a3 100644 --- a/docs/content/dns/zz_gen_corenetworks.md +++ b/docs/content/dns/zz_gen_corenetworks.md @@ -28,7 +28,7 @@ Here is an example bash command using the Core-Networks provider: ```bash CORENETWORKS_LOGIN="xxxx" \ CORENETWORKS_PASSWORD="yyyy" \ -lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run +lego --dns corenetworks -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_cpanel.md b/docs/content/dns/zz_gen_cpanel.md index 48cb229e7..e5c0cc047 100644 --- a/docs/content/dns/zz_gen_cpanel.md +++ b/docs/content/dns/zz_gen_cpanel.md @@ -31,7 +31,7 @@ Here is an example bash command using the CPanel/WHM provider: CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2083" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ## WHM @@ -39,7 +39,7 @@ CPANEL_MODE=whm \ CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2087" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_derak.md b/docs/content/dns/zz_gen_derak.md index fedbf4683..c5c8c7bc6 100644 --- a/docs/content/dns/zz_gen_derak.md +++ b/docs/content/dns/zz_gen_derak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Derak Cloud provider: ```bash DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns derak -d '*.example.com' -d example.com run +lego --dns derak -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_desec.md b/docs/content/dns/zz_gen_desec.md index 977a00e06..4dbc713d6 100644 --- a/docs/content/dns/zz_gen_desec.md +++ b/docs/content/dns/zz_gen_desec.md @@ -27,7 +27,7 @@ Here is an example bash command using the deSEC.io provider: ```bash DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns desec -d '*.example.com' -d example.com run +lego --dns desec -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_designate.md b/docs/content/dns/zz_gen_designate.md index 74cd04920..9703f094d 100644 --- a/docs/content/dns/zz_gen_designate.md +++ b/docs/content/dns/zz_gen_designate.md @@ -28,7 +28,7 @@ Here is an example bash command using the Designate DNSaaS for Openstack provide ```bash # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -37,7 +37,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -46,7 +46,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_digitalocean.md b/docs/content/dns/zz_gen_digitalocean.md index 24307cfb0..4dc43886d 100644 --- a/docs/content/dns/zz_gen_digitalocean.md +++ b/docs/content/dns/zz_gen_digitalocean.md @@ -27,7 +27,7 @@ Here is an example bash command using the Digital Ocean provider: ```bash DO_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns digitalocean -d '*.example.com' -d example.com run +lego --dns digitalocean -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_directadmin.md b/docs/content/dns/zz_gen_directadmin.md index 006cb87d6..1d03dcc4e 100644 --- a/docs/content/dns/zz_gen_directadmin.md +++ b/docs/content/dns/zz_gen_directadmin.md @@ -29,7 +29,7 @@ Here is an example bash command using the DirectAdmin provider: DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run +lego --dns directadmin -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnshomede.md b/docs/content/dns/zz_gen_dnshomede.md index b865578e6..ca7f83523 100644 --- a/docs/content/dns/zz_gen_dnshomede.md +++ b/docs/content/dns/zz_gen_dnshomede.md @@ -27,10 +27,10 @@ Here is an example bash command using the dnsHome.de provider: ```bash DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com run +lego --dns dnshomede -d '*.example.com' -d example.com run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --email you@example.com --dns dnshomede -d my.example.org -d demo.example.org +lego --dns dnshomede -d my.example.org -d demo.example.org ``` diff --git a/docs/content/dns/zz_gen_dnsimple.md b/docs/content/dns/zz_gen_dnsimple.md index d73122273..7799ece88 100644 --- a/docs/content/dns/zz_gen_dnsimple.md +++ b/docs/content/dns/zz_gen_dnsimple.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSimple provider: ```bash DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dnsimple -d '*.example.com' -d example.com run +lego --dns dnsimple -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnsmadeeasy.md b/docs/content/dns/zz_gen_dnsmadeeasy.md index 572676fbd..e7f260889 100644 --- a/docs/content/dns/zz_gen_dnsmadeeasy.md +++ b/docs/content/dns/zz_gen_dnsmadeeasy.md @@ -28,7 +28,7 @@ Here is an example bash command using the DNS Made Easy provider: ```bash DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --email you@example.com --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --dns dnsmadeeasy -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dnspod.md b/docs/content/dns/zz_gen_dnspod.md index b9e906052..86112a5ce 100644 --- a/docs/content/dns/zz_gen_dnspod.md +++ b/docs/content/dns/zz_gen_dnspod.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSPod (deprecated) provider: ```bash DNSPOD_API_KEY=xxxxxx \ -lego --email you@example.com --dns dnspod -d '*.example.com' -d example.com run +lego --dns dnspod -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dode.md b/docs/content/dns/zz_gen_dode.md index 153650406..28eebe5fa 100644 --- a/docs/content/dns/zz_gen_dode.md +++ b/docs/content/dns/zz_gen_dode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Domain Offensive (do.de) provider: ```bash DODE_TOKEN=xxxxxx \ -lego --email you@example.com --dns dode -d '*.example.com' -d example.com run +lego --dns dode -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_domeneshop.md b/docs/content/dns/zz_gen_domeneshop.md index a519cfbef..0530ab365 100644 --- a/docs/content/dns/zz_gen_domeneshop.md +++ b/docs/content/dns/zz_gen_domeneshop.md @@ -28,7 +28,7 @@ Here is an example bash command using the Domeneshop provider: ```bash DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --email example@example.com --dns domeneshop -d '*.example.com' -d example.com run +lego --dns domeneshop -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dreamhost.md b/docs/content/dns/zz_gen_dreamhost.md index e713b8ad2..b9d273099 100644 --- a/docs/content/dns/zz_gen_dreamhost.md +++ b/docs/content/dns/zz_gen_dreamhost.md @@ -27,7 +27,7 @@ Here is an example bash command using the DreamHost provider: ```bash DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --email you@example.com --dns dreamhost -d '*.example.com' -d example.com run +lego --dns dreamhost -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_duckdns.md b/docs/content/dns/zz_gen_duckdns.md index 1290b82fd..8b60780d2 100644 --- a/docs/content/dns/zz_gen_duckdns.md +++ b/docs/content/dns/zz_gen_duckdns.md @@ -27,7 +27,7 @@ Here is an example bash command using the Duck DNS provider: ```bash DUCKDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns duckdns -d '*.example.com' -d example.com run +lego --dns duckdns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dyn.md b/docs/content/dns/zz_gen_dyn.md index f241ea930..e31a90e45 100644 --- a/docs/content/dns/zz_gen_dyn.md +++ b/docs/content/dns/zz_gen_dyn.md @@ -29,7 +29,7 @@ Here is an example bash command using the Dyn provider: DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --email you@example.com --dns dyn -d '*.example.com' -d example.com run +lego --dns dyn -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dyndnsfree.md b/docs/content/dns/zz_gen_dyndnsfree.md index 6f4cf46ff..ea549b4e2 100644 --- a/docs/content/dns/zz_gen_dyndnsfree.md +++ b/docs/content/dns/zz_gen_dyndnsfree.md @@ -28,7 +28,7 @@ Here is an example bash command using the DynDnsFree.de provider: ```bash DYNDNSFREE_USERNAME="xxx" \ DYNDNSFREE_PASSWORD="yyy" \ -lego --email you@example.com --dns dyndnsfree -d '*.example.com' -d example.com run +lego --dns dyndnsfree -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_dynu.md b/docs/content/dns/zz_gen_dynu.md index 4db76456f..a1f3e762e 100644 --- a/docs/content/dns/zz_gen_dynu.md +++ b/docs/content/dns/zz_gen_dynu.md @@ -27,7 +27,7 @@ Here is an example bash command using the Dynu provider: ```bash DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dynu -d '*.example.com' -d example.com run +lego --dns dynu -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_easydns.md b/docs/content/dns/zz_gen_easydns.md index 196e6ab7c..12f69e09c 100644 --- a/docs/content/dns/zz_gen_easydns.md +++ b/docs/content/dns/zz_gen_easydns.md @@ -28,7 +28,7 @@ Here is an example bash command using the EasyDNS provider: ```bash EASYDNS_TOKEN=xxx \ EASYDNS_KEY=yyy \ -lego --email you@example.com --dns easydns -d '*.example.com' -d example.com run +lego --dns easydns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgecenter.md b/docs/content/dns/zz_gen_edgecenter.md index 7c7dd9379..1fd9fe5fa 100644 --- a/docs/content/dns/zz_gen_edgecenter.md +++ b/docs/content/dns/zz_gen_edgecenter.md @@ -27,7 +27,7 @@ Here is an example bash command using the EdgeCenter provider: ```bash EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +lego --dns edgecenter -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgedns.md b/docs/content/dns/zz_gen_edgedns.md index 21d819d2c..31b191168 100644 --- a/docs/content/dns/zz_gen_edgedns.md +++ b/docs/content/dns/zz_gen_edgedns.md @@ -30,7 +30,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --email you@example.com --dns edgedns -d '*.example.com' -d example.com run +lego --dns edgedns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_edgeone.md b/docs/content/dns/zz_gen_edgeone.md index 227127d65..ba5de5ba2 100644 --- a/docs/content/dns/zz_gen_edgeone.md +++ b/docs/content/dns/zz_gen_edgeone.md @@ -28,7 +28,7 @@ Here is an example bash command using the Tencent EdgeOne provider: ```bash EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ EDGEONE_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns edgeone -d '*.example.com' -d example.com run +lego --dns edgeone -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_efficientip.md b/docs/content/dns/zz_gen_efficientip.md index 7c151e67a..acca3ebb7 100644 --- a/docs/content/dns/zz_gen_efficientip.md +++ b/docs/content/dns/zz_gen_efficientip.md @@ -30,7 +30,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --email you@example.com --dns efficientip -d '*.example.com' -d example.com run +lego --dns efficientip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_epik.md b/docs/content/dns/zz_gen_epik.md index 50f66e8da..a7fc029d3 100644 --- a/docs/content/dns/zz_gen_epik.md +++ b/docs/content/dns/zz_gen_epik.md @@ -27,7 +27,7 @@ Here is an example bash command using the Epik provider: ```bash EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns epik -d '*.example.com' -d example.com run +lego --dns epik -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_exec.md b/docs/content/dns/zz_gen_exec.md index fb2b17e3d..ad2e6906e 100644 --- a/docs/content/dns/zz_gen_exec.md +++ b/docs/content/dns/zz_gen_exec.md @@ -26,7 +26,7 @@ Here is an example bash command using the External program provider: ```bash EXEC_PATH=/the/path/to/myscript.sh \ -lego --email you@example.com --dns exec -d '*.example.com' -d example.com run +lego --dns exec -d '*.example.com' -d example.com run ``` @@ -61,7 +61,7 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec --d my.example.org run +lego --dns exec --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -81,7 +81,7 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec -d my.example.org run +lego --dns exec -d my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/docs/content/dns/zz_gen_exoscale.md b/docs/content/dns/zz_gen_exoscale.md index 5392ff573..e599d6487 100644 --- a/docs/content/dns/zz_gen_exoscale.md +++ b/docs/content/dns/zz_gen_exoscale.md @@ -28,7 +28,7 @@ Here is an example bash command using the Exoscale provider: ```bash EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --email you@example.com --dns exoscale -d '*.example.com' -d example.com run +lego --dns exoscale -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_f5xc.md b/docs/content/dns/zz_gen_f5xc.md index 52488f1f7..0fd8fe58a 100644 --- a/docs/content/dns/zz_gen_f5xc.md +++ b/docs/content/dns/zz_gen_f5xc.md @@ -29,7 +29,7 @@ Here is an example bash command using the F5 XC provider: F5XC_API_TOKEN="xxx" \ F5XC_TENANT_NAME="yyy" \ F5XC_GROUP_NAME="zzz" \ -lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run +lego --dns f5xc -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_freemyip.md b/docs/content/dns/zz_gen_freemyip.md index d89e17c27..215f8eb84 100644 --- a/docs/content/dns/zz_gen_freemyip.md +++ b/docs/content/dns/zz_gen_freemyip.md @@ -27,7 +27,7 @@ Here is an example bash command using the freemyip.com provider: ```bash FREEMYIP_TOKEN=xxxxxx \ -lego --email you@example.com --dns freemyip -d '*.example.com' -d example.com run +lego --dns freemyip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gandi.md b/docs/content/dns/zz_gen_gandi.md index 961ed6873..b02d97819 100644 --- a/docs/content/dns/zz_gen_gandi.md +++ b/docs/content/dns/zz_gen_gandi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi provider: ```bash GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandi -d '*.example.com' -d example.com run +lego --dns gandi -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gandiv5.md b/docs/content/dns/zz_gen_gandiv5.md index 773bd3b08..78824abbe 100644 --- a/docs/content/dns/zz_gen_gandiv5.md +++ b/docs/content/dns/zz_gen_gandiv5.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi Live DNS (v5) provider: ```bash GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandiv5 -d '*.example.com' -d example.com run +lego --dns gandiv5 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gcloud.md b/docs/content/dns/zz_gen_gcloud.md index ff228a1c8..64acc1d1e 100644 --- a/docs/content/dns/zz_gen_gcloud.md +++ b/docs/content/dns/zz_gen_gcloud.md @@ -29,18 +29,18 @@ Here is an example bash command using the Google Cloud provider: # Using a service account file GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using default credentials with impersonation GCE_PROJECT="gc-project-id" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using service account key with impersonation GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gcore.md b/docs/content/dns/zz_gen_gcore.md index f2a17c3fb..21a7ee9b1 100644 --- a/docs/content/dns/zz_gen_gcore.md +++ b/docs/content/dns/zz_gen_gcore.md @@ -27,7 +27,7 @@ Here is an example bash command using the G-Core provider: ```bash GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns gcore -d '*.example.com' -d example.com run +lego --dns gcore -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gigahostno.md b/docs/content/dns/zz_gen_gigahostno.md index afb7c64c9..a59ffc401 100644 --- a/docs/content/dns/zz_gen_gigahostno.md +++ b/docs/content/dns/zz_gen_gigahostno.md @@ -28,7 +28,7 @@ Here is an example bash command using the Gigahost.no provider: ```bash GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +lego --dns gigahostno -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_glesys.md b/docs/content/dns/zz_gen_glesys.md index ff43cfe9a..2d2608330 100644 --- a/docs/content/dns/zz_gen_glesys.md +++ b/docs/content/dns/zz_gen_glesys.md @@ -28,7 +28,7 @@ Here is an example bash command using the Glesys provider: ```bash GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --email you@example.com --dns glesys -d '*.example.com' -d example.com run +lego --dns glesys -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_godaddy.md b/docs/content/dns/zz_gen_godaddy.md index c5392a878..bc51cd69b 100644 --- a/docs/content/dns/zz_gen_godaddy.md +++ b/docs/content/dns/zz_gen_godaddy.md @@ -28,7 +28,7 @@ Here is an example bash command using the Go Daddy provider: ```bash GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --email you@example.com --dns godaddy -d '*.example.com' -d example.com run +lego --dns godaddy -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_googledomains.md b/docs/content/dns/zz_gen_googledomains.md index c6f6d0577..2421184c0 100644 --- a/docs/content/dns/zz_gen_googledomains.md +++ b/docs/content/dns/zz_gen_googledomains.md @@ -27,7 +27,7 @@ Here is an example bash command using the Google Domains provider: ```bash GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns googledomains -d '*.example.com' -d example.com run +lego --dns googledomains -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_gravity.md b/docs/content/dns/zz_gen_gravity.md index 42d5e6128..654ad8424 100644 --- a/docs/content/dns/zz_gen_gravity.md +++ b/docs/content/dns/zz_gen_gravity.md @@ -29,7 +29,7 @@ Here is an example bash command using the Gravity provider: GRAVITY_SERVER_URL="https://example.org:1234" \ GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +lego --dns gravity -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hetzner.md b/docs/content/dns/zz_gen_hetzner.md index 5778a64ce..4e81bd4d9 100644 --- a/docs/content/dns/zz_gen_hetzner.md +++ b/docs/content/dns/zz_gen_hetzner.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hetzner provider: ```bash HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hetzner -d '*.example.com' -d example.com run +lego --dns hetzner -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostingde.md b/docs/content/dns/zz_gen_hostingde.md index cc86116e1..4a66fe0f1 100644 --- a/docs/content/dns/zz_gen_hostingde.md +++ b/docs/content/dns/zz_gen_hostingde.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosting.de provider: ```bash HOSTINGDE_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns hostingde -d '*.example.com' -d example.com run +lego --dns hostingde -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostinger.md b/docs/content/dns/zz_gen_hostinger.md index 193455f63..c05b3f003 100644 --- a/docs/content/dns/zz_gen_hostinger.md +++ b/docs/content/dns/zz_gen_hostinger.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hostinger provider: ```bash HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostinger -d '*.example.com' -d example.com run +lego --dns hostinger -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hostingnl.md b/docs/content/dns/zz_gen_hostingnl.md index 0577affd4..09cb69b47 100644 --- a/docs/content/dns/zz_gen_hostingnl.md +++ b/docs/content/dns/zz_gen_hostingnl.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosting.nl provider: ```bash HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +lego --dns hostingnl -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hosttech.md b/docs/content/dns/zz_gen_hosttech.md index 4f9f117ba..9435cc562 100644 --- a/docs/content/dns/zz_gen_hosttech.md +++ b/docs/content/dns/zz_gen_hosttech.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosttech provider: ```bash HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns hosttech -d '*.example.com' -d example.com run +lego --dns hosttech -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_httpnet.md b/docs/content/dns/zz_gen_httpnet.md index 06883b3f8..862909697 100644 --- a/docs/content/dns/zz_gen_httpnet.md +++ b/docs/content/dns/zz_gen_httpnet.md @@ -27,7 +27,7 @@ Here is an example bash command using the http.net provider: ```bash HTTPNET_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns httpnet -d '*.example.com' -d example.com run +lego --dns httpnet -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_httpreq.md b/docs/content/dns/zz_gen_httpreq.md index 9c6476802..7f6a8d576 100644 --- a/docs/content/dns/zz_gen_httpreq.md +++ b/docs/content/dns/zz_gen_httpreq.md @@ -27,7 +27,7 @@ Here is an example bash command using the HTTP request provider: ```bash HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --email you@example.com --dns httpreq -d '*.example.com' -d example.com run +lego --dns httpreq -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_huaweicloud.md b/docs/content/dns/zz_gen_huaweicloud.md index 9a37a8878..46d121265 100644 --- a/docs/content/dns/zz_gen_huaweicloud.md +++ b/docs/content/dns/zz_gen_huaweicloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the Huawei Cloud provider: HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ HUAWEICLOUD_REGION=cn-south-1 \ -lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run +lego --dns huaweicloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_hurricane.md b/docs/content/dns/zz_gen_hurricane.md index da78630d4..0c195d19c 100644 --- a/docs/content/dns/zz_gen_hurricane.md +++ b/docs/content/dns/zz_gen_hurricane.md @@ -27,10 +27,10 @@ Here is an example bash command using the Hurricane Electric DNS provider: ```bash HURRICANE_TOKENS=example.org:token \ -lego --email you@example.com --dns hurricane -d '*.example.com' -d example.com run +lego --dns hurricane -d '*.example.com' -d example.com run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --email you@example.com --dns hurricane -d my.example.org -d demo.example.org +lego --dns hurricane -d my.example.org -d demo.example.org ``` diff --git a/docs/content/dns/zz_gen_hyperone.md b/docs/content/dns/zz_gen_hyperone.md index 83dfdb111..bc496f7bc 100644 --- a/docs/content/dns/zz_gen_hyperone.md +++ b/docs/content/dns/zz_gen_hyperone.md @@ -26,7 +26,7 @@ Configuration for [HyperOne](https://www.hyperone.com). Here is an example bash command using the HyperOne provider: ```bash -lego --email you@example.com --dns hyperone -d '*.example.com' -d example.com run +lego --dns hyperone -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ibmcloud.md b/docs/content/dns/zz_gen_ibmcloud.md index 94997b703..c5a48d2ad 100644 --- a/docs/content/dns/zz_gen_ibmcloud.md +++ b/docs/content/dns/zz_gen_ibmcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the IBM Cloud (SoftLayer) provider: ```bash SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --email you@example.com --dns ibmcloud -d '*.example.com' -d example.com run +lego --dns ibmcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_iij.md b/docs/content/dns/zz_gen_iij.md index 8c73f58a5..c7acfe3a0 100644 --- a/docs/content/dns/zz_gen_iij.md +++ b/docs/content/dns/zz_gen_iij.md @@ -29,7 +29,7 @@ Here is an example bash command using the Internet Initiative Japan provider: IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --email you@example.com --dns iij -d '*.example.com' -d example.com run +lego --dns iij -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_iijdpf.md b/docs/content/dns/zz_gen_iijdpf.md index 7c694fc32..12e126f49 100644 --- a/docs/content/dns/zz_gen_iijdpf.md +++ b/docs/content/dns/zz_gen_iijdpf.md @@ -28,7 +28,7 @@ Here is an example bash command using the IIJ DNS Platform Service provider: ```bash IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --email you@example.com --dns iijdpf -d '*.example.com' -d example.com run +lego --dns iijdpf -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_infoblox.md b/docs/content/dns/zz_gen_infoblox.md index 2d07628f3..74b80b2d1 100644 --- a/docs/content/dns/zz_gen_infoblox.md +++ b/docs/content/dns/zz_gen_infoblox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Infoblox provider: INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --email you@example.com --dns infoblox -d '*.example.com' -d example.com run +lego --dns infoblox -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_infomaniak.md b/docs/content/dns/zz_gen_infomaniak.md index be02d8ee8..7254241b1 100644 --- a/docs/content/dns/zz_gen_infomaniak.md +++ b/docs/content/dns/zz_gen_infomaniak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Infomaniak provider: ```bash INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --email you@example.com --dns infomaniak -d '*.example.com' -d example.com run +lego --dns infomaniak -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_internetbs.md b/docs/content/dns/zz_gen_internetbs.md index e98fbf4b9..f0d9df3c1 100644 --- a/docs/content/dns/zz_gen_internetbs.md +++ b/docs/content/dns/zz_gen_internetbs.md @@ -28,7 +28,7 @@ Here is an example bash command using the Internet.bs provider: ```bash INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns internetbs -d '*.example.com' -d example.com run +lego --dns internetbs -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_inwx.md b/docs/content/dns/zz_gen_inwx.md index a46ff061e..3e7d999e9 100644 --- a/docs/content/dns/zz_gen_inwx.md +++ b/docs/content/dns/zz_gen_inwx.md @@ -28,13 +28,13 @@ Here is an example bash command using the INWX provider: ```bash INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ionos.md b/docs/content/dns/zz_gen_ionos.md index 60a2ede03..78bd3ffb1 100644 --- a/docs/content/dns/zz_gen_ionos.md +++ b/docs/content/dns/zz_gen_ionos.md @@ -27,7 +27,7 @@ Here is an example bash command using the Ionos provider: ```bash IONOS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns ionos -d '*.example.com' -d example.com run +lego --dns ionos -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ionoscloud.md b/docs/content/dns/zz_gen_ionoscloud.md index 9d33a95e5..6007670a7 100644 --- a/docs/content/dns/zz_gen_ionoscloud.md +++ b/docs/content/dns/zz_gen_ionoscloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the Ionos Cloud provider: ```bash IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +lego --dns ionoscloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ipv64.md b/docs/content/dns/zz_gen_ipv64.md index 21327caaf..00a0292a6 100644 --- a/docs/content/dns/zz_gen_ipv64.md +++ b/docs/content/dns/zz_gen_ipv64.md @@ -27,7 +27,7 @@ Here is an example bash command using the IPv64 provider: ```bash IPV64_API_KEY=xxxxxx \ -lego --email you@example.com --dns ipv64 -d '*.example.com' -d example.com run +lego --dns ipv64 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ispconfig.md b/docs/content/dns/zz_gen_ispconfig.md index bd3b375da..e56f1f0b1 100644 --- a/docs/content/dns/zz_gen_ispconfig.md +++ b/docs/content/dns/zz_gen_ispconfig.md @@ -29,7 +29,7 @@ Here is an example bash command using the ISPConfig 3 provider: ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ ISPCONFIG_USERNAME="xxx" \ ISPCONFIG_PASSWORD="yyy" \ -lego --email you@example.com --dns ispconfig -d '*.example.com' -d example.com run +lego --dns ispconfig -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ispconfigddns.md b/docs/content/dns/zz_gen_ispconfigddns.md index c59bddda4..3d1dd83c3 100644 --- a/docs/content/dns/zz_gen_ispconfigddns.md +++ b/docs/content/dns/zz_gen_ispconfigddns.md @@ -28,7 +28,7 @@ Here is an example bash command using the ISPConfig 3 - Dynamic DNS (DDNS) Modul ```bash ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ ISPCONFIG_DDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns ispconfigddns -d '*.example.com' -d example.com run +lego --dns ispconfigddns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_iwantmyname.md b/docs/content/dns/zz_gen_iwantmyname.md index cbdb29cb3..4638e1379 100644 --- a/docs/content/dns/zz_gen_iwantmyname.md +++ b/docs/content/dns/zz_gen_iwantmyname.md @@ -30,7 +30,7 @@ Here is an example bash command using the iwantmyname (Deprecated) provider: ```bash IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns iwantmyname -d '*.example.com' -d example.com run +lego --dns iwantmyname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_joker.md b/docs/content/dns/zz_gen_joker.md index c8d55b2f7..a5ecd47de 100644 --- a/docs/content/dns/zz_gen_joker.md +++ b/docs/content/dns/zz_gen_joker.md @@ -30,17 +30,17 @@ Here is an example bash command using the Joker provider: JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_keyhelp.md b/docs/content/dns/zz_gen_keyhelp.md index 2886a0a8e..e39d3ce82 100644 --- a/docs/content/dns/zz_gen_keyhelp.md +++ b/docs/content/dns/zz_gen_keyhelp.md @@ -28,7 +28,7 @@ Here is an example bash command using the KeyHelp provider: ```bash KEYHELP_BASE_URL="https://keyhelp.example.com" \ KEYHELP_API_KEY="xxx" \ -lego --email you@example.com --dns keyhelp -d '*.example.com' -d example.com run +lego --dns keyhelp -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_liara.md b/docs/content/dns/zz_gen_liara.md index 2c3d59ae0..8a6ddbd99 100644 --- a/docs/content/dns/zz_gen_liara.md +++ b/docs/content/dns/zz_gen_liara.md @@ -27,7 +27,7 @@ Here is an example bash command using the Liara provider: ```bash LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns liara -d '*.example.com' -d example.com run +lego --dns liara -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_limacity.md b/docs/content/dns/zz_gen_limacity.md index 2a01814e5..29bc6e0a7 100644 --- a/docs/content/dns/zz_gen_limacity.md +++ b/docs/content/dns/zz_gen_limacity.md @@ -27,7 +27,7 @@ Here is an example bash command using the Lima-City provider: ```bash LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns limacity -d '*.example.com' -d example.com run +lego --dns limacity -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_linode.md b/docs/content/dns/zz_gen_linode.md index 8c8487541..e41ba7cd9 100644 --- a/docs/content/dns/zz_gen_linode.md +++ b/docs/content/dns/zz_gen_linode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Linode (v4) provider: ```bash LINODE_TOKEN=xxxxx \ -lego --email you@example.com --dns linode -d '*.example.com' -d example.com run +lego --dns linode -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_liquidweb.md b/docs/content/dns/zz_gen_liquidweb.md index 9d8fe8c9c..bd2ce63b6 100644 --- a/docs/content/dns/zz_gen_liquidweb.md +++ b/docs/content/dns/zz_gen_liquidweb.md @@ -28,7 +28,7 @@ Here is an example bash command using the Liquid Web provider: ```bash LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --email you@example.com --dns liquidweb -d '*.example.com' -d example.com run +lego --dns liquidweb -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_loopia.md b/docs/content/dns/zz_gen_loopia.md index 3951de8e1..bb3120c00 100644 --- a/docs/content/dns/zz_gen_loopia.md +++ b/docs/content/dns/zz_gen_loopia.md @@ -28,7 +28,7 @@ Here is an example bash command using the Loopia provider: ```bash LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --email you@example.com --dns loopia -d '*.example.com' -d example.com run +lego --dns loopia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_luadns.md b/docs/content/dns/zz_gen_luadns.md index c987cc9bf..8bf718ba3 100644 --- a/docs/content/dns/zz_gen_luadns.md +++ b/docs/content/dns/zz_gen_luadns.md @@ -28,7 +28,7 @@ Here is an example bash command using the LuaDNS provider: ```bash LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --email you@example.com --dns luadns -d '*.example.com' -d example.com run +lego --dns luadns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mailinabox.md b/docs/content/dns/zz_gen_mailinabox.md index 3ffed1cc7..62a6bdb57 100644 --- a/docs/content/dns/zz_gen_mailinabox.md +++ b/docs/content/dns/zz_gen_mailinabox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Mail-in-a-Box provider: MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --email you@example.com --dns mailinabox -d '*.example.com' -d example.com run +lego --dns mailinabox -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_manageengine.md b/docs/content/dns/zz_gen_manageengine.md index 32b3a3aeb..a39db8208 100644 --- a/docs/content/dns/zz_gen_manageengine.md +++ b/docs/content/dns/zz_gen_manageengine.md @@ -28,7 +28,7 @@ Here is an example bash command using the ManageEngine CloudDNS provider: ```bash MANAGEENGINE_CLIENT_ID="xxx" \ MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run +lego --dns manageengine -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_manual.md b/docs/content/dns/zz_gen_manual.md index 0300d8400..056726c74 100644 --- a/docs/content/dns/zz_gen_manual.md +++ b/docs/content/dns/zz_gen_manual.md @@ -25,7 +25,7 @@ Solving the DNS-01 challenge using CLI prompt. Here is an example bash command using the Manual provider: ```bash -lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +lego --dns manual -d '*.example.com' -d example.com run ``` @@ -36,7 +36,7 @@ lego --email you@example.com --dns manual -d '*.example.com' -d example.com run To start using the CLI prompt "provider", start lego with `--dns manual`: ```console -$ lego --email "you@example.com" --domains="example.com" --dns "manual" run +$ lego --dns manual -d example.com run ``` What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: diff --git a/docs/content/dns/zz_gen_metaname.md b/docs/content/dns/zz_gen_metaname.md index a90d0170b..156cf15eb 100644 --- a/docs/content/dns/zz_gen_metaname.md +++ b/docs/content/dns/zz_gen_metaname.md @@ -28,7 +28,7 @@ Here is an example bash command using the Metaname provider: ```bash METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --email you@example.com --dns metaname -d '*.example.com' -d example.com run +lego --dns metaname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_metaregistrar.md b/docs/content/dns/zz_gen_metaregistrar.md index 63cc2bebc..22de046e2 100644 --- a/docs/content/dns/zz_gen_metaregistrar.md +++ b/docs/content/dns/zz_gen_metaregistrar.md @@ -27,7 +27,7 @@ Here is an example bash command using the Metaregistrar provider: ```bash METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns metaregistrar -d '*.example.com' -d example.com run +lego --dns metaregistrar -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mijnhost.md b/docs/content/dns/zz_gen_mijnhost.md index 42abc6558..3d8f71aff 100644 --- a/docs/content/dns/zz_gen_mijnhost.md +++ b/docs/content/dns/zz_gen_mijnhost.md @@ -27,7 +27,7 @@ Here is an example bash command using the mijn.host provider: ```bash MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns mijnhost -d '*.example.com' -d example.com run +lego --dns mijnhost -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mittwald.md b/docs/content/dns/zz_gen_mittwald.md index 943397ee9..7714ef54f 100644 --- a/docs/content/dns/zz_gen_mittwald.md +++ b/docs/content/dns/zz_gen_mittwald.md @@ -27,7 +27,7 @@ Here is an example bash command using the Mittwald provider: ```bash MITTWALD_TOKEN=my-token \ -lego --email you@example.com --dns mittwald -d '*.example.com' -d example.com run +lego --dns mittwald -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_myaddr.md b/docs/content/dns/zz_gen_myaddr.md index 277a0bf06..4a52a058b 100644 --- a/docs/content/dns/zz_gen_myaddr.md +++ b/docs/content/dns/zz_gen_myaddr.md @@ -27,7 +27,7 @@ Here is an example bash command using the myaddr.{tools,dev,io} provider: ```bash MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --email you@example.com --dns myaddr -d '*.example.com' -d example.com run +lego --dns myaddr -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mydnsjp.md b/docs/content/dns/zz_gen_mydnsjp.md index 5b29266db..0a49404bb 100644 --- a/docs/content/dns/zz_gen_mydnsjp.md +++ b/docs/content/dns/zz_gen_mydnsjp.md @@ -28,7 +28,7 @@ Here is an example bash command using the MyDNS.jp provider: ```bash MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --email you@example.com --dns mydnsjp -d '*.example.com' -d example.com run +lego --dns mydnsjp -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_mythicbeasts.md b/docs/content/dns/zz_gen_mythicbeasts.md index 37feebf8c..70e38d249 100644 --- a/docs/content/dns/zz_gen_mythicbeasts.md +++ b/docs/content/dns/zz_gen_mythicbeasts.md @@ -28,7 +28,7 @@ Here is an example bash command using the MythicBeasts provider: ```bash MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --email you@example.com --dns mythicbeasts -d '*.example.com' -d example.com run +lego --dns mythicbeasts -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namecheap.md b/docs/content/dns/zz_gen_namecheap.md index 706651660..9d7143d84 100644 --- a/docs/content/dns/zz_gen_namecheap.md +++ b/docs/content/dns/zz_gen_namecheap.md @@ -33,7 +33,7 @@ Here is an example bash command using the Namecheap provider: ```bash NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --email you@example.com --dns namecheap -d '*.example.com' -d example.com run +lego --dns namecheap -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namedotcom.md b/docs/content/dns/zz_gen_namedotcom.md index 36a423faa..2860ff0ae 100644 --- a/docs/content/dns/zz_gen_namedotcom.md +++ b/docs/content/dns/zz_gen_namedotcom.md @@ -28,7 +28,7 @@ Here is an example bash command using the Name.com provider: ```bash NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --email you@example.com --dns namedotcom -d '*.example.com' -d example.com run +lego --dns namedotcom -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_namesilo.md b/docs/content/dns/zz_gen_namesilo.md index 397a1a3ca..207a1603f 100644 --- a/docs/content/dns/zz_gen_namesilo.md +++ b/docs/content/dns/zz_gen_namesilo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Namesilo provider: ```bash NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns namesilo -d '*.example.com' -d example.com run +lego --dns namesilo -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nearlyfreespeech.md b/docs/content/dns/zz_gen_nearlyfreespeech.md index 86f6152f9..31402d2d2 100644 --- a/docs/content/dns/zz_gen_nearlyfreespeech.md +++ b/docs/content/dns/zz_gen_nearlyfreespeech.md @@ -28,7 +28,7 @@ Here is an example bash command using the NearlyFreeSpeech.NET provider: ```bash NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --email you@example.com --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --dns nearlyfreespeech -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_neodigit.md b/docs/content/dns/zz_gen_neodigit.md index 70dfb6343..aefeef4bf 100644 --- a/docs/content/dns/zz_gen_neodigit.md +++ b/docs/content/dns/zz_gen_neodigit.md @@ -27,7 +27,7 @@ Here is an example bash command using the Neodigit provider: ```bash NEODIGIT_TOKEN=xxxxxx \ -lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +lego --dns neodigit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_netcup.md b/docs/content/dns/zz_gen_netcup.md index 337baf59d..29def3285 100644 --- a/docs/content/dns/zz_gen_netcup.md +++ b/docs/content/dns/zz_gen_netcup.md @@ -29,7 +29,7 @@ Here is an example bash command using the Netcup provider: NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --email you@example.com --dns netcup -d '*.example.com' -d example.com run +lego --dns netcup -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_netlify.md b/docs/content/dns/zz_gen_netlify.md index b08f650f0..76651d9ef 100644 --- a/docs/content/dns/zz_gen_netlify.md +++ b/docs/content/dns/zz_gen_netlify.md @@ -27,7 +27,7 @@ Here is an example bash command using the Netlify provider: ```bash NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns netlify -d '*.example.com' -d example.com run +lego --dns netlify -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nicmanager.md b/docs/content/dns/zz_gen_nicmanager.md index 0b6e1b2cb..a29d72120 100644 --- a/docs/content/dns/zz_gen_nicmanager.md +++ b/docs/content/dns/zz_gen_nicmanager.md @@ -34,7 +34,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ## Login using account name + username @@ -45,7 +45,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nicru.md b/docs/content/dns/zz_gen_nicru.md index d55477a32..3ac8d99cf 100644 --- a/docs/content/dns/zz_gen_nicru.md +++ b/docs/content/dns/zz_gen_nicru.md @@ -30,7 +30,7 @@ NICRU_USER="" \ NICRU_PASSWORD="" \ NICRU_SERVICE_ID="" \ NICRU_SECRET="" \ -lego --dns nicru --domains "*.example.com" --email you@example.com run +lego --dns nicru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nifcloud.md b/docs/content/dns/zz_gen_nifcloud.md index 9b9929ce2..66f38223b 100644 --- a/docs/content/dns/zz_gen_nifcloud.md +++ b/docs/content/dns/zz_gen_nifcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the NIFCloud provider: ```bash NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --email you@example.com --dns nifcloud -d '*.example.com' -d example.com run +lego --dns nifcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_njalla.md b/docs/content/dns/zz_gen_njalla.md index cf268041c..9a312df8b 100644 --- a/docs/content/dns/zz_gen_njalla.md +++ b/docs/content/dns/zz_gen_njalla.md @@ -27,7 +27,7 @@ Here is an example bash command using the Njalla provider: ```bash NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns njalla -d '*.example.com' -d example.com run +lego --dns njalla -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_nodion.md b/docs/content/dns/zz_gen_nodion.md index c11759e8e..8d61eb834 100644 --- a/docs/content/dns/zz_gen_nodion.md +++ b/docs/content/dns/zz_gen_nodion.md @@ -27,7 +27,7 @@ Here is an example bash command using the Nodion provider: ```bash NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns nodion -d '*.example.com' -d example.com run +lego --dns nodion -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ns1.md b/docs/content/dns/zz_gen_ns1.md index 547a51c1c..b2262169d 100644 --- a/docs/content/dns/zz_gen_ns1.md +++ b/docs/content/dns/zz_gen_ns1.md @@ -27,7 +27,7 @@ Here is an example bash command using the NS1 provider: ```bash NS1_API_KEY=xxxx \ -lego --email you@example.com --dns ns1 -d '*.example.com' -d example.com run +lego --dns ns1 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_octenium.md b/docs/content/dns/zz_gen_octenium.md index 874c4e780..f25da4f44 100644 --- a/docs/content/dns/zz_gen_octenium.md +++ b/docs/content/dns/zz_gen_octenium.md @@ -27,7 +27,7 @@ Here is an example bash command using the Octenium provider: ```bash OCTENIUM_API_KEY="xxx" \ -lego --email you@example.com --dns octenium -d '*.example.com' -d example.com run +lego --dns octenium -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_oraclecloud.md b/docs/content/dns/zz_gen_oraclecloud.md index c43c24b21..b7192f380 100644 --- a/docs/content/dns/zz_gen_oraclecloud.md +++ b/docs/content/dns/zz_gen_oraclecloud.md @@ -34,13 +34,13 @@ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run # Using Instance Principal authentication (when running on OCI compute instances): # https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm OCI_AUTH_TYPE="instance_principal" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_otc.md b/docs/content/dns/zz_gen_otc.md index 4f3679fa2..9da69c694 100644 --- a/docs/content/dns/zz_gen_otc.md +++ b/docs/content/dns/zz_gen_otc.md @@ -30,7 +30,7 @@ OTC_DOMAIN_NAME=domain_name \ OTC_USER_NAME=user_name \ OTC_PASSWORD=password \ OTC_PROJECT_NAME=project_name \ -lego --email you@example.com --dns otc -d '*.example.com' -d example.com run +lego --dns otc -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ovh.md b/docs/content/dns/zz_gen_ovh.md index 7abc01b92..aaafded85 100644 --- a/docs/content/dns/zz_gen_ovh.md +++ b/docs/content/dns/zz_gen_ovh.md @@ -32,20 +32,20 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or Access Token: OVH_ACCESS_TOKEN=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_pdns.md b/docs/content/dns/zz_gen_pdns.md index 34a22cf84..7c2a8c663 100644 --- a/docs/content/dns/zz_gen_pdns.md +++ b/docs/content/dns/zz_gen_pdns.md @@ -28,7 +28,7 @@ Here is an example bash command using the PowerDNS provider: ```bash PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --email you@example.com --dns pdns -d '*.example.com' -d example.com run +lego --dns pdns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_plesk.md b/docs/content/dns/zz_gen_plesk.md index b18b2656a..73ec9a55d 100644 --- a/docs/content/dns/zz_gen_plesk.md +++ b/docs/content/dns/zz_gen_plesk.md @@ -29,7 +29,7 @@ Here is an example bash command using the plesk.com provider: PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --email you@example.com --dns plesk -d '*.example.com' -d example.com run +lego --dns plesk -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_porkbun.md b/docs/content/dns/zz_gen_porkbun.md index 9fd230d0d..f54e6f688 100644 --- a/docs/content/dns/zz_gen_porkbun.md +++ b/docs/content/dns/zz_gen_porkbun.md @@ -28,7 +28,7 @@ Here is an example bash command using the Porkbun provider: ```bash PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --email you@example.com --dns porkbun -d '*.example.com' -d example.com run +lego --dns porkbun -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rackspace.md b/docs/content/dns/zz_gen_rackspace.md index 6dcf6b2b2..b9a2ab710 100644 --- a/docs/content/dns/zz_gen_rackspace.md +++ b/docs/content/dns/zz_gen_rackspace.md @@ -28,7 +28,7 @@ Here is an example bash command using the Rackspace provider: ```bash RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --email you@example.com --dns rackspace -d '*.example.com' -d example.com run +lego --dns rackspace -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rainyun.md b/docs/content/dns/zz_gen_rainyun.md index 74ced9f54..680eb845a 100644 --- a/docs/content/dns/zz_gen_rainyun.md +++ b/docs/content/dns/zz_gen_rainyun.md @@ -27,7 +27,7 @@ Here is an example bash command using the Rain Yun/雨云 provider: ```bash RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns rainyun -d '*.example.com' -d example.com run +lego --dns rainyun -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rcodezero.md b/docs/content/dns/zz_gen_rcodezero.md index 98eaea9ca..a544df420 100644 --- a/docs/content/dns/zz_gen_rcodezero.md +++ b/docs/content/dns/zz_gen_rcodezero.md @@ -27,7 +27,7 @@ Here is an example bash command using the RcodeZero provider: ```bash RCODEZERO_API_TOKEN= \ -lego --email you@example.com --dns rcodezero -d '*.example.com' -d example.com run +lego --dns rcodezero -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_regfish.md b/docs/content/dns/zz_gen_regfish.md index 149338e5e..357ce0764 100644 --- a/docs/content/dns/zz_gen_regfish.md +++ b/docs/content/dns/zz_gen_regfish.md @@ -27,7 +27,7 @@ Here is an example bash command using the Regfish provider: ```bash REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns regfish -d '*.example.com' -d example.com run +lego --dns regfish -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_regru.md b/docs/content/dns/zz_gen_regru.md index 1d0e0053d..eaf163a13 100644 --- a/docs/content/dns/zz_gen_regru.md +++ b/docs/content/dns/zz_gen_regru.md @@ -28,7 +28,7 @@ Here is an example bash command using the reg.ru provider: ```bash REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --email you@example.com --dns regru -d '*.example.com' -d example.com run +lego --dns regru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rfc2136.md b/docs/content/dns/zz_gen_rfc2136.md index ffdbc4b54..1b1d43dd5 100644 --- a/docs/content/dns/zz_gen_rfc2136.md +++ b/docs/content/dns/zz_gen_rfc2136.md @@ -30,7 +30,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ## --- @@ -38,7 +38,7 @@ keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_FILE="$keyfile" \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_rimuhosting.md b/docs/content/dns/zz_gen_rimuhosting.md index 2a703dec7..acb829e93 100644 --- a/docs/content/dns/zz_gen_rimuhosting.md +++ b/docs/content/dns/zz_gen_rimuhosting.md @@ -27,7 +27,7 @@ Here is an example bash command using the RimuHosting provider: ```bash RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns rimuhosting -d '*.example.com' -d example.com run +lego --dns rimuhosting -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_route53.md b/docs/content/dns/zz_gen_route53.md index a0967a57e..59e489d6a 100644 --- a/docs/content/dns/zz_gen_route53.md +++ b/docs/content/dns/zz_gen_route53.md @@ -30,7 +30,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --email you@example.com --dns route53 -d '*.example.com' -d example.com run +lego --dns route53 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_safedns.md b/docs/content/dns/zz_gen_safedns.md index 2a9e179f5..e040a8a9f 100644 --- a/docs/content/dns/zz_gen_safedns.md +++ b/docs/content/dns/zz_gen_safedns.md @@ -27,7 +27,7 @@ Here is an example bash command using the UKFast SafeDNS provider: ```bash SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns safedns -d '*.example.com' -d example.com run +lego --dns safedns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_sakuracloud.md b/docs/content/dns/zz_gen_sakuracloud.md index e08e73e70..b43f83ef4 100644 --- a/docs/content/dns/zz_gen_sakuracloud.md +++ b/docs/content/dns/zz_gen_sakuracloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sakura Cloud provider: ```bash SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --email you@example.com --dns sakuracloud -d '*.example.com' -d example.com run +lego --dns sakuracloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_scaleway.md b/docs/content/dns/zz_gen_scaleway.md index 2f6af9d8a..4033a9bd6 100644 --- a/docs/content/dns/zz_gen_scaleway.md +++ b/docs/content/dns/zz_gen_scaleway.md @@ -27,7 +27,7 @@ Here is an example bash command using the Scaleway provider: ```bash SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --email you@example.com --dns scaleway -d '*.example.com' -d example.com run +lego --dns scaleway -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selectel.md b/docs/content/dns/zz_gen_selectel.md index 33dc859bb..d994d6633 100644 --- a/docs/content/dns/zz_gen_selectel.md +++ b/docs/content/dns/zz_gen_selectel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Selectel provider: ```bash SELECTEL_API_TOKEN=xxxxx \ -lego --email you@example.com --dns selectel -d '*.example.com' -d example.com run +lego --dns selectel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selectelv2.md b/docs/content/dns/zz_gen_selectelv2.md index 933ca201f..0873d810c 100644 --- a/docs/content/dns/zz_gen_selectelv2.md +++ b/docs/content/dns/zz_gen_selectelv2.md @@ -30,7 +30,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com run +lego --dns selectelv2 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_selfhostde.md b/docs/content/dns/zz_gen_selfhostde.md index 12df0c10d..363f782e0 100644 --- a/docs/content/dns/zz_gen_selfhostde.md +++ b/docs/content/dns/zz_gen_selfhostde.md @@ -29,7 +29,7 @@ Here is an example bash command using the SelfHost.(de|eu) provider: SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run +lego --dns selfhostde -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_servercow.md b/docs/content/dns/zz_gen_servercow.md index 3851325d1..7d00a6306 100644 --- a/docs/content/dns/zz_gen_servercow.md +++ b/docs/content/dns/zz_gen_servercow.md @@ -28,7 +28,7 @@ Here is an example bash command using the Servercow provider: ```bash SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns servercow -d '*.example.com' -d example.com run +lego --dns servercow -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_shellrent.md b/docs/content/dns/zz_gen_shellrent.md index 6c1365b7e..cbbc172e2 100644 --- a/docs/content/dns/zz_gen_shellrent.md +++ b/docs/content/dns/zz_gen_shellrent.md @@ -28,7 +28,7 @@ Here is an example bash command using the Shellrent provider: ```bash SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --email you@example.com --dns shellrent -d '*.example.com' -d example.com run +lego --dns shellrent -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_simply.md b/docs/content/dns/zz_gen_simply.md index 32df66f05..edfa14380 100644 --- a/docs/content/dns/zz_gen_simply.md +++ b/docs/content/dns/zz_gen_simply.md @@ -28,7 +28,7 @@ Here is an example bash command using the Simply.com provider: ```bash SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --email you@example.com --dns simply -d '*.example.com' -d example.com run +lego --dns simply -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_sonic.md b/docs/content/dns/zz_gen_sonic.md index f56a23151..20729bc1a 100644 --- a/docs/content/dns/zz_gen_sonic.md +++ b/docs/content/dns/zz_gen_sonic.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sonic provider: ```bash SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --email you@example.com --dns sonic -d '*.example.com' -d example.com run +lego --dns sonic -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_spaceship.md b/docs/content/dns/zz_gen_spaceship.md index 4594fe217..9f3b51e43 100644 --- a/docs/content/dns/zz_gen_spaceship.md +++ b/docs/content/dns/zz_gen_spaceship.md @@ -28,7 +28,7 @@ Here is an example bash command using the Spaceship provider: ```bash SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns spaceship -d '*.example.com' -d example.com run +lego --dns spaceship -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_stackpath.md b/docs/content/dns/zz_gen_stackpath.md index ce0a02eac..b881176f4 100644 --- a/docs/content/dns/zz_gen_stackpath.md +++ b/docs/content/dns/zz_gen_stackpath.md @@ -29,7 +29,7 @@ Here is an example bash command using the Stackpath provider: STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --email you@example.com --dns stackpath -d '*.example.com' -d example.com run +lego --dns stackpath -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_syse.md b/docs/content/dns/zz_gen_syse.md index 1d9d957d5..a1a952bc5 100644 --- a/docs/content/dns/zz_gen_syse.md +++ b/docs/content/dns/zz_gen_syse.md @@ -27,10 +27,10 @@ Here is an example bash command using the Syse provider: ```bash SYSE_CREDENTIALS=example.com:password \ -lego --email you@example.com --dns syse -d '*.example.com' -d example.com run +lego --dns syse -d '*.example.com' -d example.com run SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com ``` diff --git a/docs/content/dns/zz_gen_technitium.md b/docs/content/dns/zz_gen_technitium.md index 80f7c6a1f..ff7f2e6ed 100644 --- a/docs/content/dns/zz_gen_technitium.md +++ b/docs/content/dns/zz_gen_technitium.md @@ -28,7 +28,7 @@ Here is an example bash command using the Technitium provider: ```bash TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run +lego --dns technitium -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md index ef1e6cdf8..178ffcf43 100644 --- a/docs/content/dns/zz_gen_tencentcloud.md +++ b/docs/content/dns/zz_gen_tencentcloud.md @@ -28,7 +28,7 @@ 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 you@example.com --dns tencentcloud -d '*.example.com' -d example.com run +lego --dns tencentcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_timewebcloud.md b/docs/content/dns/zz_gen_timewebcloud.md index af218ddce..83d5b831b 100644 --- a/docs/content/dns/zz_gen_timewebcloud.md +++ b/docs/content/dns/zz_gen_timewebcloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the Timeweb Cloud provider: ```bash TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns timewebcloud -d '*.example.com' -d example.com run +lego --dns timewebcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_transip.md b/docs/content/dns/zz_gen_transip.md index 769fbc734..a66a25879 100644 --- a/docs/content/dns/zz_gen_transip.md +++ b/docs/content/dns/zz_gen_transip.md @@ -28,7 +28,7 @@ Here is an example bash command using the TransIP provider: ```bash TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --email you@example.com --dns transip -d '*.example.com' -d example.com run +lego --dns transip -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_ultradns.md b/docs/content/dns/zz_gen_ultradns.md index 8e0fa9b20..d6d89c77b 100644 --- a/docs/content/dns/zz_gen_ultradns.md +++ b/docs/content/dns/zz_gen_ultradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Ultradns provider: ```bash ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --email you@example.com --dns ultradns -d '*.example.com' -d example.com run +lego --dns ultradns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_uniteddomains.md b/docs/content/dns/zz_gen_uniteddomains.md index 7f94dd09f..e837644d5 100644 --- a/docs/content/dns/zz_gen_uniteddomains.md +++ b/docs/content/dns/zz_gen_uniteddomains.md @@ -27,7 +27,7 @@ Here is an example bash command using the United-Domains provider: ```bash UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +lego --dns uniteddomains -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_variomedia.md b/docs/content/dns/zz_gen_variomedia.md index 282ec9da3..f9771c867 100644 --- a/docs/content/dns/zz_gen_variomedia.md +++ b/docs/content/dns/zz_gen_variomedia.md @@ -27,7 +27,7 @@ Here is an example bash command using the Variomedia provider: ```bash VARIOMEDIA_API_TOKEN=xxxx \ -lego --email you@example.com --dns variomedia -d '*.example.com' -d example.com run +lego --dns variomedia -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vercel.md b/docs/content/dns/zz_gen_vercel.md index d9e24eee3..71f2eeed5 100644 --- a/docs/content/dns/zz_gen_vercel.md +++ b/docs/content/dns/zz_gen_vercel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vercel provider: ```bash VERCEL_API_TOKEN=xxxxxx \ -lego --email you@example.com --dns vercel -d '*.example.com' -d example.com run +lego --dns vercel -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_versio.md b/docs/content/dns/zz_gen_versio.md index 0e2edfa1e..5d2cc0118 100644 --- a/docs/content/dns/zz_gen_versio.md +++ b/docs/content/dns/zz_gen_versio.md @@ -28,7 +28,7 @@ Here is an example bash command using the Versio.[nl|eu|uk] provider: ```bash VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --email you@example.com --dns versio -d '*.example.com' -d example.com run +lego --dns versio -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vinyldns.md b/docs/content/dns/zz_gen_vinyldns.md index 666bc39c4..3280d6f0a 100644 --- a/docs/content/dns/zz_gen_vinyldns.md +++ b/docs/content/dns/zz_gen_vinyldns.md @@ -29,7 +29,7 @@ Here is an example bash command using the VinylDNS provider: VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --email you@example.com --dns vinyldns -d '*.example.com' -d example.com run +lego --dns vinyldns -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_virtualname.md b/docs/content/dns/zz_gen_virtualname.md index afba24ad0..a00e5105f 100644 --- a/docs/content/dns/zz_gen_virtualname.md +++ b/docs/content/dns/zz_gen_virtualname.md @@ -27,7 +27,7 @@ Here is an example bash command using the Virtualname provider: ```bash VIRTUALNAME_TOKEN=xxxxxx \ -lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +lego --dns virtualname -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vkcloud.md b/docs/content/dns/zz_gen_vkcloud.md index eede62cf5..76fd557a5 100644 --- a/docs/content/dns/zz_gen_vkcloud.md +++ b/docs/content/dns/zz_gen_vkcloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the VK Cloud provider: VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --email you@example.com --dns vkcloud -d '*.example.com' -d example.com run +lego --dns vkcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_volcengine.md b/docs/content/dns/zz_gen_volcengine.md index 9d3c92d0d..587ce1e74 100644 --- a/docs/content/dns/zz_gen_volcengine.md +++ b/docs/content/dns/zz_gen_volcengine.md @@ -28,7 +28,7 @@ Here is an example bash command using the Volcano Engine/火山引擎 provider: ```bash VOLC_ACCESSKEY=xxx \ VOLC_SECRETKEY=yyy \ -lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run +lego --dns volcengine -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vscale.md b/docs/content/dns/zz_gen_vscale.md index 660542d61..c33e2f7b5 100644 --- a/docs/content/dns/zz_gen_vscale.md +++ b/docs/content/dns/zz_gen_vscale.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vscale provider: ```bash VSCALE_API_TOKEN=xxxxx \ -lego --email you@example.com --dns vscale -d '*.example.com' -d example.com run +lego --dns vscale -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_vultr.md b/docs/content/dns/zz_gen_vultr.md index a3807c1a1..4160fbcf3 100644 --- a/docs/content/dns/zz_gen_vultr.md +++ b/docs/content/dns/zz_gen_vultr.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vultr provider: ```bash VULTR_API_KEY=xxxxx \ -lego --email you@example.com --dns vultr -d '*.example.com' -d example.com run +lego --dns vultr -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_webnames.md b/docs/content/dns/zz_gen_webnames.md index 4945775a5..cad02c287 100644 --- a/docs/content/dns/zz_gen_webnames.md +++ b/docs/content/dns/zz_gen_webnames.md @@ -27,7 +27,7 @@ Here is an example bash command using the webnames.ru provider: ```bash WEBNAMESRU_API_KEY=xxxxxx \ -lego --email you@example.com --dns webnamesru -d '*.example.com' -d example.com run +lego --dns webnamesru -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_webnamesca.md b/docs/content/dns/zz_gen_webnamesca.md index 41a33cb82..4a7d3794f 100644 --- a/docs/content/dns/zz_gen_webnamesca.md +++ b/docs/content/dns/zz_gen_webnamesca.md @@ -28,7 +28,7 @@ Here is an example bash command using the webnames.ca provider: ```bash WEBNAMESCA_API_USER="xxx" \ WEBNAMESCA_API_KEY="yyy" \ -lego --email you@example.com --dns webnamesca -d '*.example.com' -d example.com run +lego --dns webnamesca -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_websupport.md b/docs/content/dns/zz_gen_websupport.md index 5fe44a860..67ae394d7 100644 --- a/docs/content/dns/zz_gen_websupport.md +++ b/docs/content/dns/zz_gen_websupport.md @@ -28,7 +28,7 @@ Here is an example bash command using the Websupport provider: ```bash WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns websupport -d '*.example.com' -d example.com run +lego --dns websupport -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_wedos.md b/docs/content/dns/zz_gen_wedos.md index 8fe6ba00d..16139f4d4 100644 --- a/docs/content/dns/zz_gen_wedos.md +++ b/docs/content/dns/zz_gen_wedos.md @@ -28,7 +28,7 @@ Here is an example bash command using the WEDOS provider: ```bash WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns wedos -d '*.example.com' -d example.com run +lego --dns wedos -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_westcn.md b/docs/content/dns/zz_gen_westcn.md index 434e5b601..a5523b955 100644 --- a/docs/content/dns/zz_gen_westcn.md +++ b/docs/content/dns/zz_gen_westcn.md @@ -28,7 +28,7 @@ Here is an example bash command using the West.cn/西部数码 provider: ```bash WESTCN_USERNAME="xxx" \ WESTCN_PASSWORD="yyy" \ -lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run +lego --dns westcn -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandex.md b/docs/content/dns/zz_gen_yandex.md index 6100c02fe..4a1cf1f99 100644 --- a/docs/content/dns/zz_gen_yandex.md +++ b/docs/content/dns/zz_gen_yandex.md @@ -27,7 +27,7 @@ Here is an example bash command using the Yandex PDD provider: ```bash YANDEX_PDD_TOKEN= \ -lego --email you@example.com --dns yandex -d '*.example.com' -d example.com run +lego --dns yandex -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandex360.md b/docs/content/dns/zz_gen_yandex360.md index 66b90e049..d831fdfc2 100644 --- a/docs/content/dns/zz_gen_yandex360.md +++ b/docs/content/dns/zz_gen_yandex360.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex 360 provider: ```bash YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --email you@example.com --dns yandex360 -d '*.example.com' -d example.com run +lego --dns yandex360 -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_yandexcloud.md b/docs/content/dns/zz_gen_yandexcloud.md index f5aeba09d..0564e93d2 100644 --- a/docs/content/dns/zz_gen_yandexcloud.md +++ b/docs/content/dns/zz_gen_yandexcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex Cloud provider: ```bash YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run # --- @@ -41,7 +41,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zoneedit.md b/docs/content/dns/zz_gen_zoneedit.md index e259a2a04..c7f88b3fe 100644 --- a/docs/content/dns/zz_gen_zoneedit.md +++ b/docs/content/dns/zz_gen_zoneedit.md @@ -28,7 +28,7 @@ Here is an example bash command using the ZoneEdit provider: ```bash ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns zoneedit -d '*.example.com' -d example.com run +lego --dns zoneedit -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zoneee.md b/docs/content/dns/zz_gen_zoneee.md index cfc6be692..65678a3dc 100644 --- a/docs/content/dns/zz_gen_zoneee.md +++ b/docs/content/dns/zz_gen_zoneee.md @@ -28,7 +28,7 @@ Here is an example bash command using the Zone.ee provider: ```bash ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --email you@example.com --dns zoneee -d '*.example.com' -d example.com run +lego --dns zoneee -d '*.example.com' -d example.com run ``` diff --git a/docs/content/dns/zz_gen_zonomi.md b/docs/content/dns/zz_gen_zonomi.md index 1e90a7285..fd8757f82 100644 --- a/docs/content/dns/zz_gen_zonomi.md +++ b/docs/content/dns/zz_gen_zonomi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Zonomi provider: ```bash ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns zonomi -d '*.example.com' -d example.com run +lego --dns zonomi -d '*.example.com' -d example.com run ``` diff --git a/providers/dns/acmedns/acmedns.toml b/providers/dns/acmedns/acmedns.toml index 6d68a013d..e491569b0 100644 --- a/providers/dns/acmedns/acmedns.toml +++ b/providers/dns/acmedns/acmedns.toml @@ -8,13 +8,13 @@ Since = "v1.1.0" Example = ''' ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run # or ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run +lego --dns "acme-dns" -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/active24/active24.toml b/providers/dns/active24/active24.toml index 6a54d4695..b0eaabab8 100644 --- a/providers/dns/active24/active24.toml +++ b/providers/dns/active24/active24.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' ACTIVE24_API_KEY="xxx" \ ACTIVE24_SECRET="yyy" \ -lego --email you@example.com --dns active24 -d '*.example.com' -d example.com run +lego --dns active24 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/alidns/alidns.toml b/providers/dns/alidns/alidns.toml index 49a9aeeab..9a93bd24f 100644 --- a/providers/dns/alidns/alidns.toml +++ b/providers/dns/alidns/alidns.toml @@ -7,13 +7,13 @@ Since = "v1.1.0" Example = ''' # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run +lego --dns alidns -d '*.example.com' -d example.com run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run +lego --dns alidns - -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/aliesa/aliesa.toml b/providers/dns/aliesa/aliesa.toml index d0f6cdb91..5e7345e40 100644 --- a/providers/dns/aliesa/aliesa.toml +++ b/providers/dns/aliesa/aliesa.toml @@ -7,13 +7,13 @@ Since = "v4.29.0" Example = ''' # Setup using instance RAM role ALIESA_RAM_ROLE=lego \ -lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run +lego --dns aliesa -d '*.example.com' -d example.com run # Or, using credentials ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALIESA_SECRET_KEY=your-secret-key \ ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run +lego --dns aliesa - -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/allinkl/allinkl.toml b/providers/dns/allinkl/allinkl.toml index d9c937ee1..774f8fb9f 100644 --- a/providers/dns/allinkl/allinkl.toml +++ b/providers/dns/allinkl/allinkl.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run +lego --dns allinkl -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/alwaysdata/alwaysdata.toml b/providers/dns/alwaysdata/alwaysdata.toml index 96d8d9616..d00c6f032 100644 --- a/providers/dns/alwaysdata/alwaysdata.toml +++ b/providers/dns/alwaysdata/alwaysdata.toml @@ -6,7 +6,7 @@ Since = "v4.31.0" Example = ''' ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns alwaysdata -d '*.example.com' -d example.com run +lego --dns alwaysdata -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/anexia/anexia.toml b/providers/dns/anexia/anexia.toml index 4fad8ea48..332f0b8b1 100644 --- a/providers/dns/anexia/anexia.toml +++ b/providers/dns/anexia/anexia.toml @@ -6,7 +6,7 @@ Since = "v4.28.0" Example = ''' ANEXIA_TOKEN=xxx \ -lego --email you@example.com --dns anexia -d '*.example.com' -d example.com run +lego --dns anexia -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/arvancloud/arvancloud.toml b/providers/dns/arvancloud/arvancloud.toml index e94452a8b..aa5cafb51 100644 --- a/providers/dns/arvancloud/arvancloud.toml +++ b/providers/dns/arvancloud/arvancloud.toml @@ -6,7 +6,7 @@ Since = "v3.8.0" Example = ''' ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --email you@example.com --dns arvancloud -d '*.example.com' -d example.com run +lego --dns arvancloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/auroradns/auroradns.toml b/providers/dns/auroradns/auroradns.toml index e000e015e..59b5e7ab1 100644 --- a/providers/dns/auroradns/auroradns.toml +++ b/providers/dns/auroradns/auroradns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --email you@example.com --dns auroradns -d '*.example.com' -d example.com run +lego --dns auroradns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/autodns/autodns.toml b/providers/dns/autodns/autodns.toml index 78015e431..2798d4cee 100644 --- a/providers/dns/autodns/autodns.toml +++ b/providers/dns/autodns/autodns.toml @@ -7,7 +7,7 @@ Since = "v3.2.0" Example = ''' AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --email you@example.com --dns autodns -d '*.example.com' -d example.com run +lego --dns autodns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/axelname/axelname.toml b/providers/dns/axelname/axelname.toml index ee348d5d8..1e2ad6e72 100644 --- a/providers/dns/axelname/axelname.toml +++ b/providers/dns/axelname/axelname.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' AXELNAME_NICKNAME="yyy" \ AXELNAME_TOKEN="xxx" \ -lego --email you@example.com --dns axelname -d '*.example.com' -d example.com run +lego --dns axelname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/azion/azion.toml b/providers/dns/azion/azion.toml index eacfe74a6..52df20ab5 100644 --- a/providers/dns/azion/azion.toml +++ b/providers/dns/azion/azion.toml @@ -6,7 +6,7 @@ URL = "https://www.azion.com/en/products/edge-dns/" Example = ''' AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns azion -d '*.example.com' -d example.com run +lego --dns azion -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/azuredns/azuredns.toml b/providers/dns/azuredns/azuredns.toml index 6c1e1ccff..7c800ce7e 100644 --- a/providers/dns/azuredns/azuredns.toml +++ b/providers/dns/azuredns/azuredns.toml @@ -10,32 +10,32 @@ Example = ''' AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run +lego --dns azuredns -d '*.example.com' -d example.com run ''' diff --git a/providers/dns/baiducloud/baiducloud.toml b/providers/dns/baiducloud/baiducloud.toml index 8422eafd5..54f1f6312 100644 --- a/providers/dns/baiducloud/baiducloud.toml +++ b/providers/dns/baiducloud/baiducloud.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --email you@example.com --dns baiducloud -d '*.example.com' -d example.com run +lego --dns baiducloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/beget/beget.toml b/providers/dns/beget/beget.toml index 3cef2f38c..4ed26d850 100644 --- a/providers/dns/beget/beget.toml +++ b/providers/dns/beget/beget.toml @@ -7,7 +7,7 @@ Since = "v4.27.0" Example = ''' BEGET_USERNAME=xxxxxx \ BEGET_PASSWORD=yyyyyy \ -lego --email you@example.com --dns beget -d '*.example.com' -d example.com run +lego --dns beget -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/binarylane/binarylane.toml b/providers/dns/binarylane/binarylane.toml index 5038fc3e6..8b382f3b2 100644 --- a/providers/dns/binarylane/binarylane.toml +++ b/providers/dns/binarylane/binarylane.toml @@ -6,7 +6,7 @@ Since = "v4.26.0" Example = ''' BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run +lego --dns binarylane -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bindman/bindman.toml b/providers/dns/bindman/bindman.toml index 5c69e18ff..768601588 100644 --- a/providers/dns/bindman/bindman.toml +++ b/providers/dns/bindman/bindman.toml @@ -6,7 +6,7 @@ Since = "v2.6.0" Example = ''' BINDMAN_MANAGER_ADDRESS= \ -lego --email you@example.com --dns bindman -d '*.example.com' -d example.com run +lego --dns bindman -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bluecat/bluecat.toml b/providers/dns/bluecat/bluecat.toml index a01a5918d..15df6ed34 100644 --- a/providers/dns/bluecat/bluecat.toml +++ b/providers/dns/bluecat/bluecat.toml @@ -11,7 +11,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --email you@example.com --dns bluecat -d '*.example.com' -d example.com run +lego --dns bluecat -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bookmyname/bookmyname.toml b/providers/dns/bookmyname/bookmyname.toml index 5111c4fbd..76fcb85e7 100644 --- a/providers/dns/bookmyname/bookmyname.toml +++ b/providers/dns/bookmyname/bookmyname.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' BOOKMYNAME_USERNAME="xxx" \ BOOKMYNAME_PASSWORD="yyy" \ -lego --email you@example.com --dns bookmyname -d '*.example.com' -d example.com run +lego --dns bookmyname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/brandit/brandit.toml b/providers/dns/brandit/brandit.toml index 32d15c15c..4c43e27a9 100644 --- a/providers/dns/brandit/brandit.toml +++ b/providers/dns/brandit/brandit.toml @@ -12,7 +12,7 @@ Since = "v4.11.0" Example = ''' BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns brandit -d '*.example.com' -d example.com run +lego --dns brandit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/bunny/bunny.toml b/providers/dns/bunny/bunny.toml index cbe22d6db..758c4f202 100644 --- a/providers/dns/bunny/bunny.toml +++ b/providers/dns/bunny/bunny.toml @@ -6,7 +6,7 @@ Since = "v4.11.0" Example = ''' BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns bunny -d '*.example.com' -d example.com run +lego --dns bunny -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/checkdomain/checkdomain.toml b/providers/dns/checkdomain/checkdomain.toml index c3ac14e36..0b93058ba 100644 --- a/providers/dns/checkdomain/checkdomain.toml +++ b/providers/dns/checkdomain/checkdomain.toml @@ -6,7 +6,7 @@ Since = "v3.3.0" Example = ''' CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --email you@example.com --dns checkdomain -d '*.example.com' -d example.com run +lego --dns checkdomain -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/civo/civo.toml b/providers/dns/civo/civo.toml index 9458f01c3..b525712c8 100644 --- a/providers/dns/civo/civo.toml +++ b/providers/dns/civo/civo.toml @@ -6,7 +6,7 @@ Since = "v4.9.0" Example = ''' CIVO_TOKEN=xxxxxx \ -lego --email you@example.com --dns civo -d '*.example.com' -d example.com run +lego --dns civo -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/clouddns/clouddns.toml b/providers/dns/clouddns/clouddns.toml index 154d4da67..6f516e834 100644 --- a/providers/dns/clouddns/clouddns.toml +++ b/providers/dns/clouddns/clouddns.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --email you@example.com --dns clouddns -d '*.example.com' -d example.com run +lego --dns clouddns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudflare/cloudflare.toml b/providers/dns/cloudflare/cloudflare.toml index caf132bb4..c46130fe6 100644 --- a/providers/dns/cloudflare/cloudflare.toml +++ b/providers/dns/cloudflare/cloudflare.toml @@ -7,12 +7,12 @@ Since = "v0.3.0" Example = ''' CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run +lego --dns cloudflare -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/cloudns/cloudns.toml b/providers/dns/cloudns/cloudns.toml index dd191f06a..ad52ef5b1 100644 --- a/providers/dns/cloudns/cloudns.toml +++ b/providers/dns/cloudns/cloudns.toml @@ -7,7 +7,7 @@ Since = "v2.3.0" Example = ''' CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --email you@example.com --dns cloudns -d '*.example.com' -d example.com run +lego --dns cloudns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudru/cloudru.toml b/providers/dns/cloudru/cloudru.toml index a6563a3df..b74098a72 100644 --- a/providers/dns/cloudru/cloudru.toml +++ b/providers/dns/cloudru/cloudru.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --email you@example.com --dns cloudru -d '*.example.com' -d example.com run +lego --dns cloudru -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cloudxns/cloudxns.toml b/providers/dns/cloudxns/cloudxns.toml index e87a741df..32eae8beb 100644 --- a/providers/dns/cloudxns/cloudxns.toml +++ b/providers/dns/cloudxns/cloudxns.toml @@ -9,7 +9,7 @@ Since = "v0.5.0" Example = ''' CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --email you@example.com --dns cloudxns -d '*.example.com' -d example.com run +lego --dns cloudxns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/conoha/conoha.toml b/providers/dns/conoha/conoha.toml index 8bd83247e..be90acb0d 100644 --- a/providers/dns/conoha/conoha.toml +++ b/providers/dns/conoha/conoha.toml @@ -8,7 +8,7 @@ Example = ''' CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conoha -d '*.example.com' -d example.com run +lego --dns conoha -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/conohav3/conohav3.toml b/providers/dns/conohav3/conohav3.toml index 7608e6742..e2c80259d 100644 --- a/providers/dns/conohav3/conohav3.toml +++ b/providers/dns/conohav3/conohav3.toml @@ -8,7 +8,7 @@ Example = ''' CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHAV3_API_USER_ID=xxxx \ CONOHAV3_API_PASSWORD=yyyy \ -lego --email you@example.com --dns conohav3 -d '*.example.com' -d example.com run +lego --dns conohav3 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/constellix/constellix.toml b/providers/dns/constellix/constellix.toml index c4ae0a194..171a0de99 100644 --- a/providers/dns/constellix/constellix.toml +++ b/providers/dns/constellix/constellix.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --email you@example.com --dns constellix -d '*.example.com' -d example.com run +lego --dns constellix -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/corenetworks/corenetworks.toml b/providers/dns/corenetworks/corenetworks.toml index 8546d8723..09840bb1b 100644 --- a/providers/dns/corenetworks/corenetworks.toml +++ b/providers/dns/corenetworks/corenetworks.toml @@ -7,7 +7,7 @@ Since = "v4.20.0" Example = ''' CORENETWORKS_LOGIN="xxxx" \ CORENETWORKS_PASSWORD="yyyy" \ -lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run +lego --dns corenetworks -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/cpanel/cpanel.toml b/providers/dns/cpanel/cpanel.toml index faed2abe2..b64adf0cf 100644 --- a/providers/dns/cpanel/cpanel.toml +++ b/providers/dns/cpanel/cpanel.toml @@ -10,7 +10,7 @@ Example = ''' CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2083" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ## WHM @@ -18,7 +18,7 @@ CPANEL_MODE=whm \ CPANEL_USERNAME="yyyy" \ CPANEL_TOKEN="xxxx" \ CPANEL_BASE_URL="https://example.com:2087" \ -lego --email you@example.com --dns cpanel -d '*.example.com' -d example.com run +lego --dns cpanel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/derak/derak.toml b/providers/dns/derak/derak.toml index 45d7e1fcf..72f49883a 100644 --- a/providers/dns/derak/derak.toml +++ b/providers/dns/derak/derak.toml @@ -6,7 +6,7 @@ Since = "v4.12.0" Example = ''' DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns derak -d '*.example.com' -d example.com run +lego --dns derak -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/desec/desec.toml b/providers/dns/desec/desec.toml index a79b38cd3..f7e66ae07 100644 --- a/providers/dns/desec/desec.toml +++ b/providers/dns/desec/desec.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns desec -d '*.example.com' -d example.com run +lego --dns desec -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/designate/designate.toml b/providers/dns/designate/designate.toml index 3ea6260a6..a36034f64 100644 --- a/providers/dns/designate/designate.toml +++ b/providers/dns/designate/designate.toml @@ -7,7 +7,7 @@ Since = "v2.2.0" Example = ''' # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -16,7 +16,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run # or @@ -25,7 +25,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --email you@example.com --dns designate -d '*.example.com' -d example.com run +lego --dns designate -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/digitalocean/digitalocean.toml b/providers/dns/digitalocean/digitalocean.toml index b30d986f2..8f9107c26 100644 --- a/providers/dns/digitalocean/digitalocean.toml +++ b/providers/dns/digitalocean/digitalocean.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DO_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns digitalocean -d '*.example.com' -d example.com run +lego --dns digitalocean -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/directadmin/directadmin.toml b/providers/dns/directadmin/directadmin.toml index bd1c9316a..294eaca1c 100644 --- a/providers/dns/directadmin/directadmin.toml +++ b/providers/dns/directadmin/directadmin.toml @@ -8,7 +8,7 @@ Example = ''' DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run +lego --dns directadmin -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dnshomede/dnshomede.toml b/providers/dns/dnshomede/dnshomede.toml index bc52bb6dd..9c3b65277 100644 --- a/providers/dns/dnshomede/dnshomede.toml +++ b/providers/dns/dnshomede/dnshomede.toml @@ -6,10 +6,10 @@ Since = "v4.10.0" Example = ''' DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com run +lego --dns dnshomede -d '*.example.com' -d example.com run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --email you@example.com --dns dnshomede -d my.example.org -d demo.example.org +lego --dns dnshomede -d my.example.org -d demo.example.org ''' [Configuration] diff --git a/providers/dns/dnsimple/dnsimple.toml b/providers/dns/dnsimple/dnsimple.toml index dcf999136..158fb7011 100644 --- a/providers/dns/dnsimple/dnsimple.toml +++ b/providers/dns/dnsimple/dnsimple.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dnsimple -d '*.example.com' -d example.com run +lego --dns dnsimple -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml index 11a5f85ac..d71ab5303 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --email you@example.com --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --dns dnsmadeeasy -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dnspod/dnspod.toml b/providers/dns/dnspod/dnspod.toml index a0bf50e31..162685d76 100644 --- a/providers/dns/dnspod/dnspod.toml +++ b/providers/dns/dnspod/dnspod.toml @@ -8,7 +8,7 @@ Since = "v0.4.0" Example = ''' DNSPOD_API_KEY=xxxxxx \ -lego --email you@example.com --dns dnspod -d '*.example.com' -d example.com run +lego --dns dnspod -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dode/dode.toml b/providers/dns/dode/dode.toml index a96e9ee43..eb629bb3e 100644 --- a/providers/dns/dode/dode.toml +++ b/providers/dns/dode/dode.toml @@ -6,7 +6,7 @@ Since = "v2.4.0" Example = ''' DODE_TOKEN=xxxxxx \ -lego --email you@example.com --dns dode -d '*.example.com' -d example.com run +lego --dns dode -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/domeneshop/domeneshop.toml b/providers/dns/domeneshop/domeneshop.toml index a8d2a1064..b74af598e 100644 --- a/providers/dns/domeneshop/domeneshop.toml +++ b/providers/dns/domeneshop/domeneshop.toml @@ -8,7 +8,7 @@ Since = "v4.3.0" Example = ''' DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --email example@example.com --dns domeneshop -d '*.example.com' -d example.com run +lego --dns domeneshop -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/dreamhost/dreamhost.toml b/providers/dns/dreamhost/dreamhost.toml index 4345e9ece..c3a9db360 100644 --- a/providers/dns/dreamhost/dreamhost.toml +++ b/providers/dns/dreamhost/dreamhost.toml @@ -6,7 +6,7 @@ Since = "v1.1.0" Example = ''' DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --email you@example.com --dns dreamhost -d '*.example.com' -d example.com run +lego --dns dreamhost -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/duckdns/duckdns.toml b/providers/dns/duckdns/duckdns.toml index 9c0b3a6be..6866da57c 100644 --- a/providers/dns/duckdns/duckdns.toml +++ b/providers/dns/duckdns/duckdns.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' DUCKDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns duckdns -d '*.example.com' -d example.com run +lego --dns duckdns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dyn/dyn.toml b/providers/dns/dyn/dyn.toml index 4b0d3e652..c4b3563e0 100644 --- a/providers/dns/dyn/dyn.toml +++ b/providers/dns/dyn/dyn.toml @@ -8,7 +8,7 @@ Example = ''' DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --email you@example.com --dns dyn -d '*.example.com' -d example.com run +lego --dns dyn -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dyndnsfree/dyndnsfree.toml b/providers/dns/dyndnsfree/dyndnsfree.toml index dd354fb33..e64bb0080 100644 --- a/providers/dns/dyndnsfree/dyndnsfree.toml +++ b/providers/dns/dyndnsfree/dyndnsfree.toml @@ -7,7 +7,7 @@ Since = "v4.23.0" Example = ''' DYNDNSFREE_USERNAME="xxx" \ DYNDNSFREE_PASSWORD="yyy" \ -lego --email you@example.com --dns dyndnsfree -d '*.example.com' -d example.com run +lego --dns dyndnsfree -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/dynu/dynu.toml b/providers/dns/dynu/dynu.toml index ba59034dd..ae2367087 100644 --- a/providers/dns/dynu/dynu.toml +++ b/providers/dns/dynu/dynu.toml @@ -6,7 +6,7 @@ Since = "v3.5.0" Example = ''' DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --email you@example.com --dns dynu -d '*.example.com' -d example.com run +lego --dns dynu -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/easydns/easydns.toml b/providers/dns/easydns/easydns.toml index 71521bbd6..307c86a09 100644 --- a/providers/dns/easydns/easydns.toml +++ b/providers/dns/easydns/easydns.toml @@ -7,7 +7,7 @@ Since = "v2.6.0" Example = ''' EASYDNS_TOKEN=xxx \ EASYDNS_KEY=yyy \ -lego --email you@example.com --dns easydns -d '*.example.com' -d example.com run +lego --dns easydns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/edgecenter/edgecenter.toml b/providers/dns/edgecenter/edgecenter.toml index 0cd4b0cb6..1c9e9b2a9 100644 --- a/providers/dns/edgecenter/edgecenter.toml +++ b/providers/dns/edgecenter/edgecenter.toml @@ -6,7 +6,7 @@ Since = "v4.29.0" Example = ''' EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns edgecenter -d '*.example.com' -d example.com run +lego --dns edgecenter -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/edgedns/edgedns.toml b/providers/dns/edgedns/edgedns.toml index d40d5cc03..7c7c5b3aa 100644 --- a/providers/dns/edgedns/edgedns.toml +++ b/providers/dns/edgedns/edgedns.toml @@ -12,7 +12,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --email you@example.com --dns edgedns -d '*.example.com' -d example.com run +lego --dns edgedns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/edgeone/edgeone.toml b/providers/dns/edgeone/edgeone.toml index a33af75b2..05b8bc516 100644 --- a/providers/dns/edgeone/edgeone.toml +++ b/providers/dns/edgeone/edgeone.toml @@ -7,7 +7,7 @@ Since = "v4.26.0" Example = ''' EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ EDGEONE_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns edgeone -d '*.example.com' -d example.com run +lego --dns edgeone -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/efficientip/efficientip.toml b/providers/dns/efficientip/efficientip.toml index 565c9575b..6e1874319 100644 --- a/providers/dns/efficientip/efficientip.toml +++ b/providers/dns/efficientip/efficientip.toml @@ -9,7 +9,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --email you@example.com --dns efficientip -d '*.example.com' -d example.com run +lego --dns efficientip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/epik/epik.toml b/providers/dns/epik/epik.toml index 7b4688609..faf453581 100644 --- a/providers/dns/epik/epik.toml +++ b/providers/dns/epik/epik.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns epik -d '*.example.com' -d example.com run +lego --dns epik -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/exec/exec.toml b/providers/dns/exec/exec.toml index 4c8d70b1c..2f9c77c67 100644 --- a/providers/dns/exec/exec.toml +++ b/providers/dns/exec/exec.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' EXEC_PATH=/the/path/to/myscript.sh \ -lego --email you@example.com --dns exec -d '*.example.com' -d example.com run +lego --dns exec -d '*.example.com' -d example.com run ''' Additional = ''' @@ -39,7 +39,7 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec --d my.example.org run +lego --dns exec --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -59,7 +59,7 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --email you@example.com --dns exec -d my.example.org run +lego --dns exec -d my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/providers/dns/exoscale/exoscale.toml b/providers/dns/exoscale/exoscale.toml index 82c005d26..bcc912b07 100644 --- a/providers/dns/exoscale/exoscale.toml +++ b/providers/dns/exoscale/exoscale.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --email you@example.com --dns exoscale -d '*.example.com' -d example.com run +lego --dns exoscale -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/f5xc/f5xc.toml b/providers/dns/f5xc/f5xc.toml index f5a843c97..6be604ddd 100644 --- a/providers/dns/f5xc/f5xc.toml +++ b/providers/dns/f5xc/f5xc.toml @@ -8,7 +8,7 @@ Example = ''' F5XC_API_TOKEN="xxx" \ F5XC_TENANT_NAME="yyy" \ F5XC_GROUP_NAME="zzz" \ -lego --email you@example.com --dns f5xc -d '*.example.com' -d example.com run +lego --dns f5xc -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/freemyip/freemyip.toml b/providers/dns/freemyip/freemyip.toml index 4821e2a9c..adbf9e213 100644 --- a/providers/dns/freemyip/freemyip.toml +++ b/providers/dns/freemyip/freemyip.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' FREEMYIP_TOKEN=xxxxxx \ -lego --email you@example.com --dns freemyip -d '*.example.com' -d example.com run +lego --dns freemyip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gandi/gandi.toml b/providers/dns/gandi/gandi.toml index 96d5233be..23d7de5db 100644 --- a/providers/dns/gandi/gandi.toml +++ b/providers/dns/gandi/gandi.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandi -d '*.example.com' -d example.com run +lego --dns gandi -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gandiv5/gandiv5.toml b/providers/dns/gandiv5/gandiv5.toml index 246b03524..31568e89b 100644 --- a/providers/dns/gandiv5/gandiv5.toml +++ b/providers/dns/gandiv5/gandiv5.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --email you@example.com --dns gandiv5 -d '*.example.com' -d example.com run +lego --dns gandiv5 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gcloud/gcloud.toml b/providers/dns/gcloud/gcloud.toml index 471e2e9d1..63d22bed3 100644 --- a/providers/dns/gcloud/gcloud.toml +++ b/providers/dns/gcloud/gcloud.toml @@ -8,18 +8,18 @@ Example = ''' # Using a service account file GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using default credentials with impersonation GCE_PROJECT="gc-project-id" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run # Using service account key with impersonation GCE_PROJECT="gc-project-id" \ GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --email you@example.com --dns gcloud -d '*.example.com' -d example.com run +lego --dns gcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/gcore/gcore.toml b/providers/dns/gcore/gcore.toml index 986455e80..983c35f8a 100644 --- a/providers/dns/gcore/gcore.toml +++ b/providers/dns/gcore/gcore.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --email you@example.com --dns gcore -d '*.example.com' -d example.com run +lego --dns gcore -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gigahostno/gigahostno.toml b/providers/dns/gigahostno/gigahostno.toml index 689b96569..b8d3fad2b 100644 --- a/providers/dns/gigahostno/gigahostno.toml +++ b/providers/dns/gigahostno/gigahostno.toml @@ -7,7 +7,7 @@ Since = "v4.29.0" Example = ''' GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gigahostno -d '*.example.com' -d example.com run +lego --dns gigahostno -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/glesys/glesys.toml b/providers/dns/glesys/glesys.toml index 1bdd43c2b..c0e2613b8 100644 --- a/providers/dns/glesys/glesys.toml +++ b/providers/dns/glesys/glesys.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --email you@example.com --dns glesys -d '*.example.com' -d example.com run +lego --dns glesys -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/godaddy/godaddy.toml b/providers/dns/godaddy/godaddy.toml index acf0bf404..b906605b3 100644 --- a/providers/dns/godaddy/godaddy.toml +++ b/providers/dns/godaddy/godaddy.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --email you@example.com --dns godaddy -d '*.example.com' -d example.com run +lego --dns godaddy -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/googledomains/googledomains.toml b/providers/dns/googledomains/googledomains.toml index 1ac7e5e54..52330795d 100644 --- a/providers/dns/googledomains/googledomains.toml +++ b/providers/dns/googledomains/googledomains.toml @@ -8,7 +8,7 @@ Since = "v4.11.0" Example = ''' GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns googledomains -d '*.example.com' -d example.com run +lego --dns googledomains -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/gravity/gravity.toml b/providers/dns/gravity/gravity.toml index 6010e26e1..87a303839 100644 --- a/providers/dns/gravity/gravity.toml +++ b/providers/dns/gravity/gravity.toml @@ -8,7 +8,7 @@ Example = ''' GRAVITY_SERVER_URL="https://example.org:1234" \ GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns gravity -d '*.example.com' -d example.com run +lego --dns gravity -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hetzner/hetzner.toml b/providers/dns/hetzner/hetzner.toml index ee1f9a970..40d4cea72 100644 --- a/providers/dns/hetzner/hetzner.toml +++ b/providers/dns/hetzner/hetzner.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hetzner -d '*.example.com' -d example.com run +lego --dns hetzner -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hostingde/hostingde.toml b/providers/dns/hostingde/hostingde.toml index 569e8a781..502a7fe9e 100644 --- a/providers/dns/hostingde/hostingde.toml +++ b/providers/dns/hostingde/hostingde.toml @@ -6,7 +6,7 @@ Since = "v1.1.0" Example = ''' HOSTINGDE_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns hostingde -d '*.example.com' -d example.com run +lego --dns hostingde -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hostinger/hostinger.toml b/providers/dns/hostinger/hostinger.toml index f49e447ed..a6f152e73 100644 --- a/providers/dns/hostinger/hostinger.toml +++ b/providers/dns/hostinger/hostinger.toml @@ -6,7 +6,7 @@ Since = "v4.27.0" Example = ''' HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostinger -d '*.example.com' -d example.com run +lego --dns hostinger -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hostingnl/hostingnl.toml b/providers/dns/hostingnl/hostingnl.toml index a26c07ab2..943264ed3 100644 --- a/providers/dns/hostingnl/hostingnl.toml +++ b/providers/dns/hostingnl/hostingnl.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns hostingnl -d '*.example.com' -d example.com run +lego --dns hostingnl -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hosttech/hosttech.toml b/providers/dns/hosttech/hosttech.toml index 5d7555499..52c01fd31 100644 --- a/providers/dns/hosttech/hosttech.toml +++ b/providers/dns/hosttech/hosttech.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns hosttech -d '*.example.com' -d example.com run +lego --dns hosttech -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/httpnet/httpnet.toml b/providers/dns/httpnet/httpnet.toml index 204f5bc54..3dd581204 100644 --- a/providers/dns/httpnet/httpnet.toml +++ b/providers/dns/httpnet/httpnet.toml @@ -6,7 +6,7 @@ Since = "v4.15.0" Example = ''' HTTPNET_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns httpnet -d '*.example.com' -d example.com run +lego --dns httpnet -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/httpreq/httpreq.toml b/providers/dns/httpreq/httpreq.toml index 6c3f8719b..d64d61a6c 100644 --- a/providers/dns/httpreq/httpreq.toml +++ b/providers/dns/httpreq/httpreq.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --email you@example.com --dns httpreq -d '*.example.com' -d example.com run +lego --dns httpreq -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/huaweicloud/huaweicloud.toml b/providers/dns/huaweicloud/huaweicloud.toml index f7991dfae..e8d417c11 100644 --- a/providers/dns/huaweicloud/huaweicloud.toml +++ b/providers/dns/huaweicloud/huaweicloud.toml @@ -8,7 +8,7 @@ Example = ''' HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ HUAWEICLOUD_REGION=cn-south-1 \ -lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run +lego --dns huaweicloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/hurricane/hurricane.toml b/providers/dns/hurricane/hurricane.toml index 033c73984..10b370e4f 100644 --- a/providers/dns/hurricane/hurricane.toml +++ b/providers/dns/hurricane/hurricane.toml @@ -6,10 +6,10 @@ Since = "v4.3.0" Example = ''' HURRICANE_TOKENS=example.org:token \ -lego --email you@example.com --dns hurricane -d '*.example.com' -d example.com run +lego --dns hurricane -d '*.example.com' -d example.com run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --email you@example.com --dns hurricane -d my.example.org -d demo.example.org +lego --dns hurricane -d my.example.org -d demo.example.org ''' Additional = """ diff --git a/providers/dns/hyperone/hyperone.toml b/providers/dns/hyperone/hyperone.toml index 0f23976c4..88814356f 100644 --- a/providers/dns/hyperone/hyperone.toml +++ b/providers/dns/hyperone/hyperone.toml @@ -5,7 +5,7 @@ Code = "hyperone" Since = "v3.9.0" Example = ''' -lego --email you@example.com --dns hyperone -d '*.example.com' -d example.com run +lego --dns hyperone -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/ibmcloud/ibmcloud.toml b/providers/dns/ibmcloud/ibmcloud.toml index 2a6c12f82..01088f09b 100644 --- a/providers/dns/ibmcloud/ibmcloud.toml +++ b/providers/dns/ibmcloud/ibmcloud.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --email you@example.com --dns ibmcloud -d '*.example.com' -d example.com run +lego --dns ibmcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/iij/iij.toml b/providers/dns/iij/iij.toml index 8dbf5ba1a..95355200a 100644 --- a/providers/dns/iij/iij.toml +++ b/providers/dns/iij/iij.toml @@ -8,7 +8,7 @@ Example = ''' IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --email you@example.com --dns iij -d '*.example.com' -d example.com run +lego --dns iij -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/iijdpf/iijdpf.toml b/providers/dns/iijdpf/iijdpf.toml index 4aaa9ca37..650285f95 100644 --- a/providers/dns/iijdpf/iijdpf.toml +++ b/providers/dns/iijdpf/iijdpf.toml @@ -7,7 +7,7 @@ Since = "v4.7.0" Example = ''' IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --email you@example.com --dns iijdpf -d '*.example.com' -d example.com run +lego --dns iijdpf -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/infoblox/infoblox.toml b/providers/dns/infoblox/infoblox.toml index 3c2632042..0e6972d3a 100644 --- a/providers/dns/infoblox/infoblox.toml +++ b/providers/dns/infoblox/infoblox.toml @@ -8,7 +8,7 @@ Example = ''' INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --email you@example.com --dns infoblox -d '*.example.com' -d example.com run +lego --dns infoblox -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/infomaniak/infomaniak.toml b/providers/dns/infomaniak/infomaniak.toml index 283838053..d924e3a26 100644 --- a/providers/dns/infomaniak/infomaniak.toml +++ b/providers/dns/infomaniak/infomaniak.toml @@ -6,7 +6,7 @@ Since = "v4.1.0" Example = ''' INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --email you@example.com --dns infomaniak -d '*.example.com' -d example.com run +lego --dns infomaniak -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/internetbs/internetbs.toml b/providers/dns/internetbs/internetbs.toml index d25418f22..f22850253 100644 --- a/providers/dns/internetbs/internetbs.toml +++ b/providers/dns/internetbs/internetbs.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --email you@example.com --dns internetbs -d '*.example.com' -d example.com run +lego --dns internetbs -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/inwx/inwx.toml b/providers/dns/inwx/inwx.toml index aeab5a242..da4c6d959 100644 --- a/providers/dns/inwx/inwx.toml +++ b/providers/dns/inwx/inwx.toml @@ -7,13 +7,13 @@ Since = "v2.0.0" Example = ''' INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --email you@example.com --dns inwx -d '*.example.com' -d example.com run +lego --dns inwx -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ionos/ionos.toml b/providers/dns/ionos/ionos.toml index 0c905273f..a2c9518fb 100644 --- a/providers/dns/ionos/ionos.toml +++ b/providers/dns/ionos/ionos.toml @@ -6,7 +6,7 @@ Since = "v4.2.0" Example = ''' IONOS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns ionos -d '*.example.com' -d example.com run +lego --dns ionos -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ionoscloud/ionoscloud.toml b/providers/dns/ionoscloud/ionoscloud.toml index a8fedce6c..6e1d080e4 100644 --- a/providers/dns/ionoscloud/ionoscloud.toml +++ b/providers/dns/ionoscloud/ionoscloud.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns ionoscloud -d '*.example.com' -d example.com run +lego --dns ionoscloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ipv64/ipv64.toml b/providers/dns/ipv64/ipv64.toml index fba210bdb..aa1720c9e 100644 --- a/providers/dns/ipv64/ipv64.toml +++ b/providers/dns/ipv64/ipv64.toml @@ -6,7 +6,7 @@ Since = "v4.13.0" Example = ''' IPV64_API_KEY=xxxxxx \ -lego --email you@example.com --dns ipv64 -d '*.example.com' -d example.com run +lego --dns ipv64 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ispconfig/ispconfig.toml b/providers/dns/ispconfig/ispconfig.toml index 399544742..4defd5509 100644 --- a/providers/dns/ispconfig/ispconfig.toml +++ b/providers/dns/ispconfig/ispconfig.toml @@ -8,7 +8,7 @@ Example = ''' ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ ISPCONFIG_USERNAME="xxx" \ ISPCONFIG_PASSWORD="yyy" \ -lego --email you@example.com --dns ispconfig -d '*.example.com' -d example.com run +lego --dns ispconfig -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ispconfigddns/ispconfigddns.toml b/providers/dns/ispconfigddns/ispconfigddns.toml index 84e82904f..158ee9fbd 100644 --- a/providers/dns/ispconfigddns/ispconfigddns.toml +++ b/providers/dns/ispconfigddns/ispconfigddns.toml @@ -7,7 +7,7 @@ Since = "v4.31.0" Example = ''' ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ ISPCONFIG_DDNS_TOKEN=xxxxxx \ -lego --email you@example.com --dns ispconfigddns -d '*.example.com' -d example.com run +lego --dns ispconfigddns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/iwantmyname/iwantmyname.toml b/providers/dns/iwantmyname/iwantmyname.toml index a138dee9e..a82c2b749 100644 --- a/providers/dns/iwantmyname/iwantmyname.toml +++ b/providers/dns/iwantmyname/iwantmyname.toml @@ -11,7 +11,7 @@ Since = "v4.7.0" Example = ''' IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns iwantmyname -d '*.example.com' -d example.com run +lego --dns iwantmyname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/joker/joker.toml b/providers/dns/joker/joker.toml index 35713df18..20e481a6d 100644 --- a/providers/dns/joker/joker.toml +++ b/providers/dns/joker/joker.toml @@ -9,17 +9,17 @@ Example = ''' JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --email you@example.com --dns joker -d '*.example.com' -d example.com run +lego --dns joker -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/keyhelp/keyhelp.toml b/providers/dns/keyhelp/keyhelp.toml index d6f84e34e..e622794ca 100644 --- a/providers/dns/keyhelp/keyhelp.toml +++ b/providers/dns/keyhelp/keyhelp.toml @@ -7,7 +7,7 @@ Since = "v4.26.0" Example = ''' KEYHELP_BASE_URL="https://keyhelp.example.com" \ KEYHELP_API_KEY="xxx" \ -lego --email you@example.com --dns keyhelp -d '*.example.com' -d example.com run +lego --dns keyhelp -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/liara/liara.toml b/providers/dns/liara/liara.toml index 1259999a2..4ed53ec75 100644 --- a/providers/dns/liara/liara.toml +++ b/providers/dns/liara/liara.toml @@ -6,7 +6,7 @@ Since = "v4.10.0" Example = ''' LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns liara -d '*.example.com' -d example.com run +lego --dns liara -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/limacity/limacity.toml b/providers/dns/limacity/limacity.toml index b9b9f0018..d236577d0 100644 --- a/providers/dns/limacity/limacity.toml +++ b/providers/dns/limacity/limacity.toml @@ -6,7 +6,7 @@ Since = "v4.18.0" Example = ''' LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns limacity -d '*.example.com' -d example.com run +lego --dns limacity -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/linode/linode.toml b/providers/dns/linode/linode.toml index f046d3f9b..9ea30b92b 100644 --- a/providers/dns/linode/linode.toml +++ b/providers/dns/linode/linode.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' LINODE_TOKEN=xxxxx \ -lego --email you@example.com --dns linode -d '*.example.com' -d example.com run +lego --dns linode -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/liquidweb/liquidweb.toml b/providers/dns/liquidweb/liquidweb.toml index 22789f41e..386b99cab 100644 --- a/providers/dns/liquidweb/liquidweb.toml +++ b/providers/dns/liquidweb/liquidweb.toml @@ -7,7 +7,7 @@ Since = "v3.1.0" Example = ''' LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --email you@example.com --dns liquidweb -d '*.example.com' -d example.com run +lego --dns liquidweb -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/loopia/loopia.toml b/providers/dns/loopia/loopia.toml index 4a127ec55..a201852c9 100644 --- a/providers/dns/loopia/loopia.toml +++ b/providers/dns/loopia/loopia.toml @@ -7,7 +7,7 @@ Since = "v4.2.0" Example = ''' LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --email you@example.com --dns loopia -d '*.example.com' -d example.com run +lego --dns loopia -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/luadns/luadns.toml b/providers/dns/luadns/luadns.toml index c80929c21..e56fac0b6 100644 --- a/providers/dns/luadns/luadns.toml +++ b/providers/dns/luadns/luadns.toml @@ -7,7 +7,7 @@ Since = "v3.7.0" Example = ''' LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --email you@example.com --dns luadns -d '*.example.com' -d example.com run +lego --dns luadns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mailinabox/mailinabox.toml b/providers/dns/mailinabox/mailinabox.toml index e0072ebdd..74d8aabbc 100644 --- a/providers/dns/mailinabox/mailinabox.toml +++ b/providers/dns/mailinabox/mailinabox.toml @@ -8,7 +8,7 @@ Example = ''' MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --email you@example.com --dns mailinabox -d '*.example.com' -d example.com run +lego --dns mailinabox -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/manageengine/manageengine.toml b/providers/dns/manageengine/manageengine.toml index 7708fa74f..43a782841 100644 --- a/providers/dns/manageengine/manageengine.toml +++ b/providers/dns/manageengine/manageengine.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' MANAGEENGINE_CLIENT_ID="xxx" \ MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run +lego --dns manageengine -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/manual/manual.toml b/providers/dns/manual/manual.toml index 88acf4750..aca67536d 100644 --- a/providers/dns/manual/manual.toml +++ b/providers/dns/manual/manual.toml @@ -4,7 +4,7 @@ Code = "manual" Since = "v0.3.0" Example = ''' -lego --email you@example.com --dns manual -d '*.example.com' -d example.com run +lego --dns manual -d '*.example.com' -d example.com run ''' Additional = ''' @@ -13,7 +13,7 @@ Additional = ''' To start using the CLI prompt "provider", start lego with `--dns manual`: ```console -$ lego --email "you@example.com" --domains="example.com" --dns "manual" run +$ lego --dns manual -d example.com run ``` What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: diff --git a/providers/dns/metaname/metaname.toml b/providers/dns/metaname/metaname.toml index 4a147d043..654dcaed0 100644 --- a/providers/dns/metaname/metaname.toml +++ b/providers/dns/metaname/metaname.toml @@ -7,7 +7,7 @@ Since = "v4.13.0" Example = ''' METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --email you@example.com --dns metaname -d '*.example.com' -d example.com run +lego --dns metaname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/metaregistrar/metaregistrar.toml b/providers/dns/metaregistrar/metaregistrar.toml index 952c7ea61..e505e0ce2 100644 --- a/providers/dns/metaregistrar/metaregistrar.toml +++ b/providers/dns/metaregistrar/metaregistrar.toml @@ -6,7 +6,7 @@ Since = "v4.23.0" Example = ''' METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns metaregistrar -d '*.example.com' -d example.com run +lego --dns metaregistrar -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mijnhost/mijnhost.toml b/providers/dns/mijnhost/mijnhost.toml index 00152e132..416fdde53 100644 --- a/providers/dns/mijnhost/mijnhost.toml +++ b/providers/dns/mijnhost/mijnhost.toml @@ -6,7 +6,7 @@ Since = "v4.18.0" Example = ''' MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns mijnhost -d '*.example.com' -d example.com run +lego --dns mijnhost -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mittwald/mittwald.toml b/providers/dns/mittwald/mittwald.toml index 937b9c172..36a9f6c16 100644 --- a/providers/dns/mittwald/mittwald.toml +++ b/providers/dns/mittwald/mittwald.toml @@ -6,7 +6,7 @@ Since = "v1.48.0" Example = ''' MITTWALD_TOKEN=my-token \ -lego --email you@example.com --dns mittwald -d '*.example.com' -d example.com run +lego --dns mittwald -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/myaddr/myaddr.toml b/providers/dns/myaddr/myaddr.toml index 5ff306526..2f5fe6c1f 100644 --- a/providers/dns/myaddr/myaddr.toml +++ b/providers/dns/myaddr/myaddr.toml @@ -6,7 +6,7 @@ Since = "v4.22.0" Example = ''' MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --email you@example.com --dns myaddr -d '*.example.com' -d example.com run +lego --dns myaddr -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mydnsjp/mydnsjp.toml b/providers/dns/mydnsjp/mydnsjp.toml index ab842e37f..eb9e73acc 100644 --- a/providers/dns/mydnsjp/mydnsjp.toml +++ b/providers/dns/mydnsjp/mydnsjp.toml @@ -7,7 +7,7 @@ Since = "v1.2.0" Example = ''' MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --email you@example.com --dns mydnsjp -d '*.example.com' -d example.com run +lego --dns mydnsjp -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/mythicbeasts/mythicbeasts.toml b/providers/dns/mythicbeasts/mythicbeasts.toml index 011abba1f..cada3041d 100644 --- a/providers/dns/mythicbeasts/mythicbeasts.toml +++ b/providers/dns/mythicbeasts/mythicbeasts.toml @@ -7,7 +7,7 @@ Since = "v0.3.7" Example = ''' MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --email you@example.com --dns mythicbeasts -d '*.example.com' -d example.com run +lego --dns mythicbeasts -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/namecheap/namecheap.toml b/providers/dns/namecheap/namecheap.toml index 3a5be870c..b0f92a1bd 100644 --- a/providers/dns/namecheap/namecheap.toml +++ b/providers/dns/namecheap/namecheap.toml @@ -14,7 +14,7 @@ More information in the section [Enabling API Access](https://www.namecheap.com/ Example = ''' NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --email you@example.com --dns namecheap -d '*.example.com' -d example.com run +lego --dns namecheap -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/namedotcom/namedotcom.toml b/providers/dns/namedotcom/namedotcom.toml index e6de796d1..3651c424b 100644 --- a/providers/dns/namedotcom/namedotcom.toml +++ b/providers/dns/namedotcom/namedotcom.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --email you@example.com --dns namedotcom -d '*.example.com' -d example.com run +lego --dns namedotcom -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/namesilo/namesilo.toml b/providers/dns/namesilo/namesilo.toml index bab7905bf..113ddb5c5 100644 --- a/providers/dns/namesilo/namesilo.toml +++ b/providers/dns/namesilo/namesilo.toml @@ -6,7 +6,7 @@ Since = "v2.7.0" Example = ''' NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --email you@example.com --dns namesilo -d '*.example.com' -d example.com run +lego --dns namesilo -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml index 80d4fd6bc..3a1e25942 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml @@ -7,7 +7,7 @@ Since = "v4.8.0" Example = ''' NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --email you@example.com --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --dns nearlyfreespeech -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/neodigit/neodigit.toml b/providers/dns/neodigit/neodigit.toml index b391a6512..91b3cfb07 100644 --- a/providers/dns/neodigit/neodigit.toml +++ b/providers/dns/neodigit/neodigit.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' NEODIGIT_TOKEN=xxxxxx \ -lego --email you@example.com --dns neodigit -d '*.example.com' -d example.com run +lego --dns neodigit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/netcup/netcup.toml b/providers/dns/netcup/netcup.toml index 0df09b0df..4ef8688c6 100644 --- a/providers/dns/netcup/netcup.toml +++ b/providers/dns/netcup/netcup.toml @@ -8,7 +8,7 @@ Example = ''' NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --email you@example.com --dns netcup -d '*.example.com' -d example.com run +lego --dns netcup -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/netlify/netlify.toml b/providers/dns/netlify/netlify.toml index c5cb670f9..9d3c0f6b5 100644 --- a/providers/dns/netlify/netlify.toml +++ b/providers/dns/netlify/netlify.toml @@ -6,7 +6,7 @@ Since = "v3.7.0" Example = ''' NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns netlify -d '*.example.com' -d example.com run +lego --dns netlify -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/nicmanager/nicmanager.toml b/providers/dns/nicmanager/nicmanager.toml index 7fdf296c4..d5921de5a 100644 --- a/providers/dns/nicmanager/nicmanager.toml +++ b/providers/dns/nicmanager/nicmanager.toml @@ -13,7 +13,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ## Login using account name + username @@ -24,7 +24,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --email you@example.com --dns nicmanager -d '*.example.com' -d example.com run +lego --dns nicmanager -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/nicru/nicru.toml b/providers/dns/nicru/nicru.toml index 6bffe74a5..f955511a2 100644 --- a/providers/dns/nicru/nicru.toml +++ b/providers/dns/nicru/nicru.toml @@ -9,7 +9,7 @@ NICRU_USER="" \ NICRU_PASSWORD="" \ NICRU_SERVICE_ID="" \ NICRU_SECRET="" \ -lego --dns nicru --domains "*.example.com" --email you@example.com run +lego --dns nicru -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/nifcloud/nifcloud.toml b/providers/dns/nifcloud/nifcloud.toml index b692bb9d3..3c43b1dc0 100644 --- a/providers/dns/nifcloud/nifcloud.toml +++ b/providers/dns/nifcloud/nifcloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --email you@example.com --dns nifcloud -d '*.example.com' -d example.com run +lego --dns nifcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/njalla/njalla.toml b/providers/dns/njalla/njalla.toml index ef1fe158e..ff4750b7d 100644 --- a/providers/dns/njalla/njalla.toml +++ b/providers/dns/njalla/njalla.toml @@ -6,7 +6,7 @@ Since = "v4.3.0" Example = ''' NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns njalla -d '*.example.com' -d example.com run +lego --dns njalla -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/nodion/nodion.toml b/providers/dns/nodion/nodion.toml index 0888f96c3..c9db46e61 100644 --- a/providers/dns/nodion/nodion.toml +++ b/providers/dns/nodion/nodion.toml @@ -6,7 +6,7 @@ Since = "v4.11.0" Example = ''' NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns nodion -d '*.example.com' -d example.com run +lego --dns nodion -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ns1/ns1.toml b/providers/dns/ns1/ns1.toml index 2a6b10deb..829663bf5 100644 --- a/providers/dns/ns1/ns1.toml +++ b/providers/dns/ns1/ns1.toml @@ -6,7 +6,7 @@ Since = "v0.4.0" Example = ''' NS1_API_KEY=xxxx \ -lego --email you@example.com --dns ns1 -d '*.example.com' -d example.com run +lego --dns ns1 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/octenium/octenium.toml b/providers/dns/octenium/octenium.toml index 5084526fd..e3c9d894f 100644 --- a/providers/dns/octenium/octenium.toml +++ b/providers/dns/octenium/octenium.toml @@ -6,7 +6,7 @@ Since = "v4.27.0" Example = ''' OCTENIUM_API_KEY="xxx" \ -lego --email you@example.com --dns octenium -d '*.example.com' -d example.com run +lego --dns octenium -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/oraclecloud/oraclecloud.toml b/providers/dns/oraclecloud/oraclecloud.toml index f13cb1e1e..f6155052e 100644 --- a/providers/dns/oraclecloud/oraclecloud.toml +++ b/providers/dns/oraclecloud/oraclecloud.toml @@ -13,13 +13,13 @@ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run # Using Instance Principal authentication (when running on OCI compute instances): # https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm OCI_AUTH_TYPE="instance_principal" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --email you@example.com --dns oraclecloud -d '*.example.com' -d example.com run +lego --dns oraclecloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/otc/otc.toml b/providers/dns/otc/otc.toml index 91f9f5455..e63077fda 100644 --- a/providers/dns/otc/otc.toml +++ b/providers/dns/otc/otc.toml @@ -9,7 +9,7 @@ OTC_DOMAIN_NAME=domain_name \ OTC_USER_NAME=user_name \ OTC_PASSWORD=password \ OTC_PROJECT_NAME=project_name \ -lego --email you@example.com --dns otc -d '*.example.com' -d example.com run +lego --dns otc -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ovh/ovh.toml b/providers/dns/ovh/ovh.toml index 95162185b..abf22bd7a 100644 --- a/providers/dns/ovh/ovh.toml +++ b/providers/dns/ovh/ovh.toml @@ -11,20 +11,20 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or Access Token: OVH_ACCESS_TOKEN=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run +lego --dns ovh -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/pdns/pdns.toml b/providers/dns/pdns/pdns.toml index 53b5547b9..a83d80922 100644 --- a/providers/dns/pdns/pdns.toml +++ b/providers/dns/pdns/pdns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --email you@example.com --dns pdns -d '*.example.com' -d example.com run +lego --dns pdns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/plesk/plesk.toml b/providers/dns/plesk/plesk.toml index 5fb4ce073..0ef89d6b7 100644 --- a/providers/dns/plesk/plesk.toml +++ b/providers/dns/plesk/plesk.toml @@ -8,7 +8,7 @@ Example = ''' PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --email you@example.com --dns plesk -d '*.example.com' -d example.com run +lego --dns plesk -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/porkbun/porkbun.toml b/providers/dns/porkbun/porkbun.toml index d7ed3aedc..9ae036da6 100644 --- a/providers/dns/porkbun/porkbun.toml +++ b/providers/dns/porkbun/porkbun.toml @@ -8,7 +8,7 @@ Since = "v4.4.0" Example = ''' PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --email you@example.com --dns porkbun -d '*.example.com' -d example.com run +lego --dns porkbun -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rackspace/rackspace.toml b/providers/dns/rackspace/rackspace.toml index 7ca2c3b7a..0a4a80ffc 100644 --- a/providers/dns/rackspace/rackspace.toml +++ b/providers/dns/rackspace/rackspace.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --email you@example.com --dns rackspace -d '*.example.com' -d example.com run +lego --dns rackspace -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rainyun/rainyun.toml b/providers/dns/rainyun/rainyun.toml index cca16cffe..fe2b3c07d 100644 --- a/providers/dns/rainyun/rainyun.toml +++ b/providers/dns/rainyun/rainyun.toml @@ -6,7 +6,7 @@ Since = "v4.21.0" Example = ''' RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns rainyun -d '*.example.com' -d example.com run +lego --dns rainyun -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rcodezero/rcodezero.toml b/providers/dns/rcodezero/rcodezero.toml index bba5588da..c2a4a1e7b 100644 --- a/providers/dns/rcodezero/rcodezero.toml +++ b/providers/dns/rcodezero/rcodezero.toml @@ -6,7 +6,7 @@ Since = "v4.13" Example = ''' RCODEZERO_API_TOKEN= \ -lego --email you@example.com --dns rcodezero -d '*.example.com' -d example.com run +lego --dns rcodezero -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/regfish/regfish.toml b/providers/dns/regfish/regfish.toml index 9869ed96e..fbaacbde4 100644 --- a/providers/dns/regfish/regfish.toml +++ b/providers/dns/regfish/regfish.toml @@ -6,7 +6,7 @@ Since = "v4.20.0" Example = ''' REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns regfish -d '*.example.com' -d example.com run +lego --dns regfish -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/regru/regru.toml b/providers/dns/regru/regru.toml index 2ccf3a58f..728bb2bf7 100644 --- a/providers/dns/regru/regru.toml +++ b/providers/dns/regru/regru.toml @@ -7,7 +7,7 @@ Since = "v3.5.0" Example = ''' REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --email you@example.com --dns regru -d '*.example.com' -d example.com run +lego --dns regru -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rfc2136/rfc2136.toml b/providers/dns/rfc2136/rfc2136.toml index 9243440a4..6b5bbe599 100644 --- a/providers/dns/rfc2136/rfc2136.toml +++ b/providers/dns/rfc2136/rfc2136.toml @@ -9,7 +9,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ## --- @@ -17,7 +17,7 @@ keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_FILE="$keyfile" \ -lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run +lego --dns rfc2136 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/rimuhosting/rimuhosting.toml b/providers/dns/rimuhosting/rimuhosting.toml index 0a4f983e2..c1994e2cc 100644 --- a/providers/dns/rimuhosting/rimuhosting.toml +++ b/providers/dns/rimuhosting/rimuhosting.toml @@ -6,7 +6,7 @@ Since = "v0.3.5" Example = ''' RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns rimuhosting -d '*.example.com' -d example.com run +lego --dns rimuhosting -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/route53/route53.toml b/providers/dns/route53/route53.toml index 9e3b049a6..607d9ef31 100644 --- a/providers/dns/route53/route53.toml +++ b/providers/dns/route53/route53.toml @@ -9,7 +9,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --email you@example.com --dns route53 -d '*.example.com' -d example.com run +lego --dns route53 -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/safedns/safedns.toml b/providers/dns/safedns/safedns.toml index dcc7bc90e..188db66a4 100644 --- a/providers/dns/safedns/safedns.toml +++ b/providers/dns/safedns/safedns.toml @@ -6,7 +6,7 @@ Since = "v4.6.0" Example = ''' SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns safedns -d '*.example.com' -d example.com run +lego --dns safedns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/sakuracloud/sakuracloud.toml b/providers/dns/sakuracloud/sakuracloud.toml index f754e0c89..a197cd27c 100644 --- a/providers/dns/sakuracloud/sakuracloud.toml +++ b/providers/dns/sakuracloud/sakuracloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --email you@example.com --dns sakuracloud -d '*.example.com' -d example.com run +lego --dns sakuracloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/scaleway/scaleway.toml b/providers/dns/scaleway/scaleway.toml index 212cea295..8b556e8b1 100644 --- a/providers/dns/scaleway/scaleway.toml +++ b/providers/dns/scaleway/scaleway.toml @@ -6,7 +6,7 @@ Since = "v3.4.0" Example = ''' SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --email you@example.com --dns scaleway -d '*.example.com' -d example.com run +lego --dns scaleway -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selectel/selectel.toml b/providers/dns/selectel/selectel.toml index f9add7ea9..087c97b5b 100644 --- a/providers/dns/selectel/selectel.toml +++ b/providers/dns/selectel/selectel.toml @@ -6,7 +6,7 @@ Since = "v1.2.0" Example = ''' SELECTEL_API_TOKEN=xxxxx \ -lego --email you@example.com --dns selectel -d '*.example.com' -d example.com run +lego --dns selectel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selectelv2/selectelv2.toml b/providers/dns/selectelv2/selectelv2.toml index fd8dbda9f..480c7756e 100644 --- a/providers/dns/selectelv2/selectelv2.toml +++ b/providers/dns/selectelv2/selectelv2.toml @@ -9,7 +9,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com run +lego --dns selectelv2 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/selfhostde/selfhostde.toml b/providers/dns/selfhostde/selfhostde.toml index 619f2cae8..bd22c6c41 100644 --- a/providers/dns/selfhostde/selfhostde.toml +++ b/providers/dns/selfhostde/selfhostde.toml @@ -8,7 +8,7 @@ Example = ''' SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run +lego --dns selfhostde -d '*.example.com' -d example.com run ''' Additional = """ diff --git a/providers/dns/servercow/servercow.toml b/providers/dns/servercow/servercow.toml index de9727163..5cbacbb88 100644 --- a/providers/dns/servercow/servercow.toml +++ b/providers/dns/servercow/servercow.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns servercow -d '*.example.com' -d example.com run +lego --dns servercow -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/shellrent/shellrent.toml b/providers/dns/shellrent/shellrent.toml index 48a5b9ad9..05b6517fc 100644 --- a/providers/dns/shellrent/shellrent.toml +++ b/providers/dns/shellrent/shellrent.toml @@ -7,7 +7,7 @@ Since = "v4.16.0" Example = ''' SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --email you@example.com --dns shellrent -d '*.example.com' -d example.com run +lego --dns shellrent -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/simply/simply.toml b/providers/dns/simply/simply.toml index c586e0db5..a838e245a 100644 --- a/providers/dns/simply/simply.toml +++ b/providers/dns/simply/simply.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --email you@example.com --dns simply -d '*.example.com' -d example.com run +lego --dns simply -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/sonic/sonic.toml b/providers/dns/sonic/sonic.toml index 921fe4988..cb501e923 100644 --- a/providers/dns/sonic/sonic.toml +++ b/providers/dns/sonic/sonic.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --email you@example.com --dns sonic -d '*.example.com' -d example.com run +lego --dns sonic -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/spaceship/spaceship.toml b/providers/dns/spaceship/spaceship.toml index 645abd171..e9abcd408 100644 --- a/providers/dns/spaceship/spaceship.toml +++ b/providers/dns/spaceship/spaceship.toml @@ -7,7 +7,7 @@ Since = "v4.22.0" Example = ''' SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns spaceship -d '*.example.com' -d example.com run +lego --dns spaceship -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/stackpath/stackpath.toml b/providers/dns/stackpath/stackpath.toml index cc14cdfba..b50e7035f 100644 --- a/providers/dns/stackpath/stackpath.toml +++ b/providers/dns/stackpath/stackpath.toml @@ -8,7 +8,7 @@ Example = ''' STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --email you@example.com --dns stackpath -d '*.example.com' -d example.com run +lego --dns stackpath -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/syse/syse.toml b/providers/dns/syse/syse.toml index 0ae585854..b5b1fdf47 100644 --- a/providers/dns/syse/syse.toml +++ b/providers/dns/syse/syse.toml @@ -6,10 +6,10 @@ Since = "v4.30.0" Example = ''' SYSE_CREDENTIALS=example.com:password \ -lego --email you@example.com --dns syse -d '*.example.com' -d example.com run +lego --dns syse -d '*.example.com' -d example.com run SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --email you@example.com --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com +lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com ''' [Configuration] diff --git a/providers/dns/technitium/technitium.toml b/providers/dns/technitium/technitium.toml index 13b40c304..ac1fc6466 100644 --- a/providers/dns/technitium/technitium.toml +++ b/providers/dns/technitium/technitium.toml @@ -7,7 +7,7 @@ Since = "v4.20.0" Example = ''' TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run +lego --dns technitium -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/tencentcloud/tencentcloud.toml b/providers/dns/tencentcloud/tencentcloud.toml index 7f06d9386..50f4ee9d5 100644 --- a/providers/dns/tencentcloud/tencentcloud.toml +++ b/providers/dns/tencentcloud/tencentcloud.toml @@ -7,7 +7,7 @@ Since = "v4.6.0" Example = ''' TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ -lego --email you@example.com --dns tencentcloud -d '*.example.com' -d example.com run +lego --dns tencentcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/timewebcloud/timewebcloud.toml b/providers/dns/timewebcloud/timewebcloud.toml index 8c20b37b9..c8bde636a 100644 --- a/providers/dns/timewebcloud/timewebcloud.toml +++ b/providers/dns/timewebcloud/timewebcloud.toml @@ -6,7 +6,7 @@ Since = "v4.20.0" Example = ''' TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --email you@example.com --dns timewebcloud -d '*.example.com' -d example.com run +lego --dns timewebcloud -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/transip/transip.toml b/providers/dns/transip/transip.toml index a894cc3e3..bf7d58ee3 100644 --- a/providers/dns/transip/transip.toml +++ b/providers/dns/transip/transip.toml @@ -7,7 +7,7 @@ Since = "v2.0.0" Example = ''' TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --email you@example.com --dns transip -d '*.example.com' -d example.com run +lego --dns transip -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/ultradns/ultradns.toml b/providers/dns/ultradns/ultradns.toml index a403a3dcf..4c3dbbe72 100644 --- a/providers/dns/ultradns/ultradns.toml +++ b/providers/dns/ultradns/ultradns.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --email you@example.com --dns ultradns -d '*.example.com' -d example.com run +lego --dns ultradns -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/uniteddomains/uniteddomains.toml b/providers/dns/uniteddomains/uniteddomains.toml index 3663cb867..fe8b9e574 100644 --- a/providers/dns/uniteddomains/uniteddomains.toml +++ b/providers/dns/uniteddomains/uniteddomains.toml @@ -6,7 +6,7 @@ Since = "v4.29.0" Example = ''' UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --email you@example.com --dns uniteddomains -d '*.example.com' -d example.com run +lego --dns uniteddomains -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/variomedia/variomedia.toml b/providers/dns/variomedia/variomedia.toml index fe6f2797a..8390d1922 100644 --- a/providers/dns/variomedia/variomedia.toml +++ b/providers/dns/variomedia/variomedia.toml @@ -6,7 +6,7 @@ Since = "v4.8.0" Example = ''' VARIOMEDIA_API_TOKEN=xxxx \ -lego --email you@example.com --dns variomedia -d '*.example.com' -d example.com run +lego --dns variomedia -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vercel/vercel.toml b/providers/dns/vercel/vercel.toml index 2545b9c48..4700d6d78 100644 --- a/providers/dns/vercel/vercel.toml +++ b/providers/dns/vercel/vercel.toml @@ -6,7 +6,7 @@ Since = "v4.7.0" Example = ''' VERCEL_API_TOKEN=xxxxxx \ -lego --email you@example.com --dns vercel -d '*.example.com' -d example.com run +lego --dns vercel -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/versio/versio.toml b/providers/dns/versio/versio.toml index 33f7125c8..733947095 100644 --- a/providers/dns/versio/versio.toml +++ b/providers/dns/versio/versio.toml @@ -7,7 +7,7 @@ Since = "v2.7.0" Example = ''' VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --email you@example.com --dns versio -d '*.example.com' -d example.com run +lego --dns versio -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/vinyldns/vinyldns.toml b/providers/dns/vinyldns/vinyldns.toml index 5789d10ab..d6dd5810e 100644 --- a/providers/dns/vinyldns/vinyldns.toml +++ b/providers/dns/vinyldns/vinyldns.toml @@ -8,7 +8,7 @@ Example = ''' VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --email you@example.com --dns vinyldns -d '*.example.com' -d example.com run +lego --dns vinyldns -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/virtualname/virtualname.toml b/providers/dns/virtualname/virtualname.toml index 7cc4c5344..881f09797 100644 --- a/providers/dns/virtualname/virtualname.toml +++ b/providers/dns/virtualname/virtualname.toml @@ -6,7 +6,7 @@ Since = "v4.30.0" Example = ''' VIRTUALNAME_TOKEN=xxxxxx \ -lego --email you@example.com --dns virtualname -d '*.example.com' -d example.com run +lego --dns virtualname -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vkcloud/vkcloud.toml b/providers/dns/vkcloud/vkcloud.toml index 366039694..04f57fea3 100644 --- a/providers/dns/vkcloud/vkcloud.toml +++ b/providers/dns/vkcloud/vkcloud.toml @@ -8,7 +8,7 @@ Example = ''' VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --email you@example.com --dns vkcloud -d '*.example.com' -d example.com run +lego --dns vkcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/volcengine/volcengine.toml b/providers/dns/volcengine/volcengine.toml index cb7c219f7..ceedcb18a 100644 --- a/providers/dns/volcengine/volcengine.toml +++ b/providers/dns/volcengine/volcengine.toml @@ -7,7 +7,7 @@ Since = "v4.19.0" Example = ''' VOLC_ACCESSKEY=xxx \ VOLC_SECRETKEY=yyy \ -lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run +lego --dns volcengine -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vscale/vscale.toml b/providers/dns/vscale/vscale.toml index 78b6c99e5..f7dc0d943 100644 --- a/providers/dns/vscale/vscale.toml +++ b/providers/dns/vscale/vscale.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' VSCALE_API_TOKEN=xxxxx \ -lego --email you@example.com --dns vscale -d '*.example.com' -d example.com run +lego --dns vscale -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/vultr/vultr.toml b/providers/dns/vultr/vultr.toml index 7d31bd52b..78e878bea 100644 --- a/providers/dns/vultr/vultr.toml +++ b/providers/dns/vultr/vultr.toml @@ -6,7 +6,7 @@ Since = "v0.3.1" Example = ''' VULTR_API_KEY=xxxxx \ -lego --email you@example.com --dns vultr -d '*.example.com' -d example.com run +lego --dns vultr -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/webnames/webnames.toml b/providers/dns/webnames/webnames.toml index 04dea25c5..b038deaf5 100644 --- a/providers/dns/webnames/webnames.toml +++ b/providers/dns/webnames/webnames.toml @@ -7,7 +7,7 @@ Since = "v4.15.0" Example = ''' WEBNAMESRU_API_KEY=xxxxxx \ -lego --email you@example.com --dns webnamesru -d '*.example.com' -d example.com run +lego --dns webnamesru -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/webnamesca/webnamesca.toml b/providers/dns/webnamesca/webnamesca.toml index c7d30751b..ab68a04a0 100644 --- a/providers/dns/webnamesca/webnamesca.toml +++ b/providers/dns/webnamesca/webnamesca.toml @@ -7,7 +7,7 @@ Since = "v4.28.0" Example = ''' WEBNAMESCA_API_USER="xxx" \ WEBNAMESCA_API_KEY="yyy" \ -lego --email you@example.com --dns webnamesca -d '*.example.com' -d example.com run +lego --dns webnamesca -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/websupport/websupport.toml b/providers/dns/websupport/websupport.toml index 1f34b431b..4908f0235 100644 --- a/providers/dns/websupport/websupport.toml +++ b/providers/dns/websupport/websupport.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --email you@example.com --dns websupport -d '*.example.com' -d example.com run +lego --dns websupport -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/wedos/wedos.toml b/providers/dns/wedos/wedos.toml index 2ed351a2d..89abfc16c 100644 --- a/providers/dns/wedos/wedos.toml +++ b/providers/dns/wedos/wedos.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --email you@example.com --dns wedos -d '*.example.com' -d example.com run +lego --dns wedos -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/westcn/westcn.toml b/providers/dns/westcn/westcn.toml index acf3a4e49..1b0cb0a7a 100644 --- a/providers/dns/westcn/westcn.toml +++ b/providers/dns/westcn/westcn.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' WESTCN_USERNAME="xxx" \ WESTCN_PASSWORD="yyy" \ -lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run +lego --dns westcn -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/yandex/yandex.toml b/providers/dns/yandex/yandex.toml index 78da1444d..a36df069e 100644 --- a/providers/dns/yandex/yandex.toml +++ b/providers/dns/yandex/yandex.toml @@ -7,7 +7,7 @@ Since = "v3.7.0" Example = ''' YANDEX_PDD_TOKEN= \ -lego --email you@example.com --dns yandex -d '*.example.com' -d example.com run +lego --dns yandex -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/yandex360/yandex360.toml b/providers/dns/yandex360/yandex360.toml index 69ea02a28..444b1cc38 100644 --- a/providers/dns/yandex360/yandex360.toml +++ b/providers/dns/yandex360/yandex360.toml @@ -8,7 +8,7 @@ Since = "v4.14.0" Example = ''' YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --email you@example.com --dns yandex360 -d '*.example.com' -d example.com run +lego --dns yandex360 -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/yandexcloud/yandexcloud.toml b/providers/dns/yandexcloud/yandexcloud.toml index a4bf3ebbb..d4b40bb1d 100644 --- a/providers/dns/yandexcloud/yandexcloud.toml +++ b/providers/dns/yandexcloud/yandexcloud.toml @@ -7,7 +7,7 @@ Since = "v4.9.0" Example = ''' YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run # --- @@ -20,7 +20,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --email you@example.com --dns yandexcloud -d '*.example.com' -d example.com run +lego --dns yandexcloud -d '*.example.com' -d example.com run ''' Additional = ''' diff --git a/providers/dns/zoneedit/zoneedit.toml b/providers/dns/zoneedit/zoneedit.toml index d3c547c23..cdc53b33a 100644 --- a/providers/dns/zoneedit/zoneedit.toml +++ b/providers/dns/zoneedit/zoneedit.toml @@ -7,7 +7,7 @@ Since = "v4.25.0" Example = ''' ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --email you@example.com --dns zoneedit -d '*.example.com' -d example.com run +lego --dns zoneedit -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/zoneee/zoneee.toml b/providers/dns/zoneee/zoneee.toml index 75ccabbda..ab7133180 100644 --- a/providers/dns/zoneee/zoneee.toml +++ b/providers/dns/zoneee/zoneee.toml @@ -7,7 +7,7 @@ Since = "v2.1.0" Example = ''' ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --email you@example.com --dns zoneee -d '*.example.com' -d example.com run +lego --dns zoneee -d '*.example.com' -d example.com run ''' [Configuration] diff --git a/providers/dns/zonomi/zonomi.toml b/providers/dns/zonomi/zonomi.toml index a5577999a..b91bcaac6 100644 --- a/providers/dns/zonomi/zonomi.toml +++ b/providers/dns/zonomi/zonomi.toml @@ -6,7 +6,7 @@ Since = "v3.5.0" Example = ''' ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --email you@example.com --dns zonomi -d '*.example.com' -d example.com run +lego --dns zonomi -d '*.example.com' -d example.com run ''' [Configuration] From 2eede6d6206a06e0b16fb3de45b8fa7aa0807de3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 1 Jan 2026 22:11:38 +0100 Subject: [PATCH 54/95] hetzner: fix compatibility with _FILE suffix (#2775) --- providers/dns/hetzner/hetzner.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/providers/dns/hetzner/hetzner.go b/providers/dns/hetzner/hetzner.go index 1b02590d6..bae985b3e 100644 --- a/providers/dns/hetzner/hetzner.go +++ b/providers/dns/hetzner/hetzner.go @@ -4,7 +4,6 @@ package hetzner import ( "errors" "net/http" - "os" "time" "github.com/go-acme/lego/v4/challenge" @@ -62,10 +61,9 @@ type DNSProvider struct { } // NewDNSProvider returns a DNSProvider instance configured for hetzner. -// Credentials must be passed in the environment variable: HETZNER_API_KEY. func NewDNSProvider() (*DNSProvider, error) { - _, foundAPIToken := os.LookupEnv(EnvAPIToken) - _, foundAPIKey := os.LookupEnv(EnvAPIKey) + foundAPIToken := env.GetOrFile(EnvAPIToken) != "" + foundAPIKey := env.GetOrFile(EnvAPIKey) != "" switch { case foundAPIToken: From 4783c128fad713c4bc0a4e50da48e0f10da79c23 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 5 Jan 2026 00:05:16 +0100 Subject: [PATCH 55/95] chore: minor changes (#2776) --- README.md | 4 +- docs/content/_index.md | 2 +- providers/dns/allinkl/allinkl.go | 8 +-- providers/dns/autodns/autodns.go | 4 +- providers/dns/liquidweb/liquidweb_test.go | 36 ++++++------- providers/dns/liquidweb/servermock_test.go | 60 +++++++++++----------- providers/dns/neodigit/neodigit.go | 4 +- providers/dns/virtualname/virtualname.go | 4 +- 8 files changed, 64 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index aff5052ca..e9a8caacc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ So if you think that lego is worth it, please consider [donating](https://donate - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [170 DNS providers](https://go-acme.github.io/lego/dns) +- Comes with about [180 DNS providers](https://go-acme.github.io/lego/dns) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates @@ -56,6 +56,8 @@ Documentation is hosted live at https://go-acme.github.io/lego/. Detailed documentation is available [here](https://go-acme.github.io/lego/dns). +If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml). + diff --git a/docs/content/_index.md b/docs/content/_index.md index d3787cf19..95e411afc 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -24,7 +24,7 @@ I've been maintaining it for about 10 years. - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [170 DNS providers]({{% ref "dns" %}}) +- Comes with about [180 DNS providers]({{% ref "dns" %}}) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 7e8f5ab4e..4a0aadd2b 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -130,7 +130,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: authentication: %w", err) } ctx = internal.WithContext(ctx, credential) @@ -149,7 +149,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { recordID, err := d.client.AddDNSSettings(ctx, record) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: add DNS settings: %w", err) } d.recordIDsMu.Lock() @@ -167,7 +167,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: authentication: %w", err) } ctx = internal.WithContext(ctx, credential) @@ -183,7 +183,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, err = d.client.DeleteDNSSettings(ctx, recordID) if err != nil { - return fmt.Errorf("allinkl: %w", err) + return fmt.Errorf("allinkl: delete DNS settings: %w", err) } d.recordIDsMu.Lock() diff --git a/providers/dns/autodns/autodns.go b/providers/dns/autodns/autodns.go index fc8e793b6..8a9361bc0 100644 --- a/providers/dns/autodns/autodns.go +++ b/providers/dns/autodns/autodns.go @@ -130,7 +130,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { _, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { - return fmt.Errorf("autodns: %w", err) + return fmt.Errorf("autodns: add record: %w", err) } return nil @@ -149,7 +149,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { - return fmt.Errorf("autodns: %w", err) + return fmt.Errorf("autodns: remove record: %w", err) } return nil diff --git a/providers/dns/liquidweb/liquidweb_test.go b/providers/dns/liquidweb/liquidweb_test.go index 26dc5bdc0..a34d19037 100644 --- a/providers/dns/liquidweb/liquidweb_test.go +++ b/providers/dns/liquidweb/liquidweb_test.go @@ -27,16 +27,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "minimum-success", envVars: map[string]string{ - EnvUsername: "blars", - EnvPassword: "tacoman", + EnvUsername: "user", + EnvPassword: "secret", }, }, { desc: "set-everything", envVars: map[string]string{ - EnvURL: "https://storm.com", - EnvUsername: "blars", - EnvPassword: "tacoman", + EnvURL: "https://storm.example", + EnvUsername: "user", + EnvPassword: "secret", EnvZone: "blars.com", }, }, @@ -48,16 +48,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing username", envVars: map[string]string{ - EnvPassword: "tacoman", - EnvZone: "blars.com", + EnvPassword: "secret", + EnvZone: "blars.example", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME", }, { desc: "missing password", envVars: map[string]string{ - EnvUsername: "blars", - EnvZone: "blars.com", + EnvUsername: "user", + EnvZone: "blars.example", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", }, @@ -148,13 +148,13 @@ func TestNewDNSProviderConfig(t *testing.T) { func TestDNSProvider_Present(t *testing.T) { provider := mockProvider(t) - err := provider.Present("tacoman.com", "", "") + err := provider.Present("tacoman.example", "", "") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { provider := mockProvider(t, network.DNSRecord{ - Name: "_acme-challenge.tacoman.com", + Name: "_acme-challenge.tacoman.example", RData: "123d==", Type: "TXT", TTL: 300, @@ -164,7 +164,7 @@ func TestDNSProvider_CleanUp(t *testing.T) { provider.recordIDs["123d=="] = 1234567 - err := provider.CleanUp("tacoman.com.", "123d==", "") + err := provider.CleanUp("tacoman.example.", "123d==", "") require.NoError(t, err) } @@ -181,7 +181,7 @@ func TestDNSProvider(t *testing.T) { }{ { desc: "expected successful", - domain: "tacoman.com", + domain: "tacoman.example", token: "123", keyAuth: "456", present: true, @@ -189,7 +189,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "other successful", - domain: "banana.com", + domain: "banana.example", token: "123", keyAuth: "456", present: true, @@ -197,16 +197,16 @@ func TestDNSProvider(t *testing.T) { }, { desc: "zone not on account", - domain: "huckleberry.com", + domain: "huckleberry.example", token: "123", keyAuth: "456", present: true, - expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.com'", + expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.example'", cleanup: false, }, { desc: "ssl for domain", - domain: "sundae.cherry.com", + domain: "sundae.cherry.example", token: "5847953", keyAuth: "34872934", present: true, @@ -214,7 +214,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "complicated domain", - domain: "always.money.stand.banana.com", + domain: "always.money.stand.banana.example", token: "5847953", keyAuth: "there is always money in the banana stand", present: true, diff --git a/providers/dns/liquidweb/servermock_test.go b/providers/dns/liquidweb/servermock_test.go index f211e7253..4886e17f1 100644 --- a/providers/dns/liquidweb/servermock_test.go +++ b/providers/dns/liquidweb/servermock_test.go @@ -26,14 +26,14 @@ func mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { return servermock.NewBuilder( func(server *httptest.Server) (*DNSProvider, error) { config := NewDefaultConfig() - config.Username = "blars" - config.Password = "tacoman" + config.Username = "user" + config.Password = "secret" config.BaseURL = server.URL return NewDNSProviderConfig(config) }, servermock.CheckHeader(). - WithBasicAuth("blars", "tacoman"), + WithBasicAuth("user", "secret"), ). Route("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)). Route("/v1/Network/DNS/Record/create", mockAPICreate(recs)). @@ -172,38 +172,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 1, - Name: "blars.com", + Name: "blars.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 2, - Name: "tacoman.com", + Name: "tacoman.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 3, - Name: "storm.com", + Name: "storm.example", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 4, - Name: "not-apple.com", + Name: "not-apple.example", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 5, Name: "example.com", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, @@ -211,38 +211,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 6, - Name: "banana.com", + Name: "banana.example", Active: 1, DelegationStatus: "NXDOMAIN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 7, - Name: "cherry.com", + Name: "cherry.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 8, - Name: "dates.com", + Name: "dates.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 9, - Name: "eggplant.com", + Name: "eggplant.example", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 10, - Name: "fig.com", + Name: "fig.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, @@ -250,31 +250,31 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 11, - Name: "grapes.com", + Name: "grapes.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 12, - Name: "money.banana.com", + Name: "money.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 13, - Name: "money.stand.banana.com", + Name: "money.stand.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, { ID: 14, - Name: "stand.banana.com", + Name: "stand.banana.example", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.liquidweb.com", + PrimaryNameserver: "ns.example.org", }, }, }, diff --git a/providers/dns/neodigit/neodigit.go b/providers/dns/neodigit/neodigit.go index eb4530479..d41846307 100644 --- a/providers/dns/neodigit/neodigit.go +++ b/providers/dns/neodigit/neodigit.go @@ -25,6 +25,8 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.neodigit.net/v1" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. @@ -66,7 +68,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("neodigit: the configuration of the DNS provider is nil") } - provider, err := tecnocratica.NewDNSProviderConfig(config, "") + provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("neodigit: %w", err) } diff --git a/providers/dns/virtualname/virtualname.go b/providers/dns/virtualname/virtualname.go index 6b04e8169..34637d280 100644 --- a/providers/dns/virtualname/virtualname.go +++ b/providers/dns/virtualname/virtualname.go @@ -25,6 +25,8 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const defaultBaseURL = "https://api.virtualname.net/v1" + var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. @@ -66,7 +68,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("virtualname: the configuration of the DNS provider is nil") } - provider, err := tecnocratica.NewDNSProviderConfig(config, "https://api.virtualname.net/v1") + provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) if err != nil { return nil, fmt.Errorf("virtualname: %w", err) } From c5a259564fe8e0b183fe12fb926f5b3634497967 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 6 Jan 2026 19:06:05 +0100 Subject: [PATCH 56/95] =?UTF-8?q?Add=20DNS=20provider=20for=2035.com/?= =?UTF-8?q?=E4=B8=89=E4=BA=94=E4=BA=92=E8=81=94=20(#2779)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 90 +++++------ cmd/zz_gen_cmd_dnshelp.go | 22 +++ docs/content/dns/zz_gen_com35.md | 69 +++++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/com35/com35.go | 104 +++++++++++++ providers/dns/com35/com35.toml | 24 +++ providers/dns/com35/com35_test.go | 144 ++++++++++++++++++ .../{ => internal}/westcn/internal/client.go | 6 +- .../westcn/internal/client_test.go | 5 +- .../internal/fixtures/adddnsrecord.json | 0 .../internal/fixtures/deldnsrecord.json | 0 .../westcn/internal/fixtures/error.json | 0 .../{ => internal}/westcn/internal/types.go | 0 providers/dns/internal/westcn/provider.go | 140 +++++++++++++++++ .../dns/internal/westcn/provider_test.go | 127 +++++++++++++++ providers/dns/westcn/westcn.go | 91 ++--------- providers/dns/westcn/westcn_test.go | 6 +- providers/dns/zz_gen_dns_providers.go | 3 + 18 files changed, 698 insertions(+), 135 deletions(-) create mode 100644 docs/content/dns/zz_gen_com35.md create mode 100644 providers/dns/com35/com35.go create mode 100644 providers/dns/com35/com35.toml create mode 100644 providers/dns/com35/com35_test.go rename providers/dns/{ => internal}/westcn/internal/client.go (98%) rename providers/dns/{ => internal}/westcn/internal/client_test.go (98%) rename providers/dns/{ => internal}/westcn/internal/fixtures/adddnsrecord.json (100%) rename providers/dns/{ => internal}/westcn/internal/fixtures/deldnsrecord.json (100%) rename providers/dns/{ => internal}/westcn/internal/fixtures/error.json (100%) rename providers/dns/{ => internal}/westcn/internal/types.go (100%) create mode 100644 providers/dns/internal/westcn/provider.go create mode 100644 providers/dns/internal/westcn/provider_test.go 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": From dd6ab7ca95c90bde76719e9c8121ec8274f3aff8 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 7 Jan 2026 18:03:32 +0100 Subject: [PATCH 57/95] Add DNS provider for JDCloud (#2782) --- README.md | 47 ++-- cmd/zz_gen_cmd_dnshelp.go | 23 ++ docs/content/dns/zz_gen_jdcloud.md | 71 +++++ docs/data/zz_cli_help.toml | 2 +- go.mod | 2 + go.sum | 4 + .../fixtures/create_record-request.json | 15 ++ .../dns/jdcloud/fixtures/create_record.json | 25 ++ .../dns/jdcloud/fixtures/delete_record.json | 9 + .../fixtures/describe_domains_page1.json | 55 ++++ .../fixtures/describe_domains_page2.json | 55 ++++ providers/dns/jdcloud/jdcloud.go | 217 ++++++++++++++++ providers/dns/jdcloud/jdcloud.toml | 27 ++ providers/dns/jdcloud/jdcloud_test.go | 242 ++++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 15 files changed, 775 insertions(+), 22 deletions(-) create mode 100644 docs/content/dns/zz_gen_jdcloud.md create mode 100644 providers/dns/jdcloud/fixtures/create_record-request.json create mode 100644 providers/dns/jdcloud/fixtures/create_record.json create mode 100644 providers/dns/jdcloud/fixtures/delete_record.json create mode 100644 providers/dns/jdcloud/fixtures/describe_domains_page1.json create mode 100644 providers/dns/jdcloud/fixtures/describe_domains_page2.json create mode 100644 providers/dns/jdcloud/jdcloud.go create mode 100644 providers/dns/jdcloud/jdcloud.toml create mode 100644 providers/dns/jdcloud/jdcloud_test.go diff --git a/README.md b/README.md index 1ad4f4fb6..c02347e23 100644 --- a/README.md +++ b/README.md @@ -177,114 +177,119 @@ If your DNS provider is not supported, please open an [issue](https://github.com ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) + JD Cloud Joker Joohoi's ACME-DNS - KeyHelp + KeyHelp Liara Lima-City Linode (v4) - Liquid Web + Liquid Web Loopia LuaDNS Mail-in-a-Box - ManageEngine CloudDNS + ManageEngine CloudDNS Manual Metaname Metaregistrar - mijn.host + mijn.host Mittwald myaddr.{tools,dev,io} MyDNS.jp - MythicBeasts + MythicBeasts Name.com Namecheap Namesilo - NearlyFreeSpeech.NET + NearlyFreeSpeech.NET Neodigit Netcup Netlify - Nicmanager + Nicmanager NIFCloud Njalla Nodion - NS1 + NS1 Octenium Open Telekom Cloud Oracle Cloud - OVH + OVH plesk.com Porkbun PowerDNS - Rackspace + Rackspace Rain Yun/雨云 RcodeZero reg.ru - Regfish + Regfish RFC2136 RimuHosting RU CENTER - Sakura Cloud + Sakura Cloud Scaleway Selectel Selectel v2 - SelfHost.(de|eu) + SelfHost.(de|eu) Servercow Shellrent Simply.com - Sonic + Sonic Spaceship Stackpath Syse - Technitium + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TransIP + TransIP UKFast SafeDNS Ultradns United-Domains - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS Virtualname VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 44fec8e54..a918a1484 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -105,6 +105,7 @@ func allDNSCodes() string { "ispconfig", "ispconfigddns", "iwantmyname", + "jdcloud", "joker", "keyhelp", "liara", @@ -2195,6 +2196,28 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iwantmyname`) + case "jdcloud": + // generated from: providers/dns/jdcloud/jdcloud.toml + ew.writeln(`Configuration for JD Cloud.`) + ew.writeln(`Code: 'jdcloud'`) + ew.writeln(`Since: 'v4.31.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "JDCLOUD_ACCESS_KEY_ID": Access key ID`) + ew.writeln(` - "JDCLOUD_ACCESS_KEY_SECRET": Access key secret`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "JDCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "JDCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "JDCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "JDCLOUD_REGION_ID": Region ID (Default: cn-north-1)`) + ew.writeln(` - "JDCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/jdcloud`) + case "joker": // generated from: providers/dns/joker/joker.toml ew.writeln(`Configuration for Joker.`) diff --git a/docs/content/dns/zz_gen_jdcloud.md b/docs/content/dns/zz_gen_jdcloud.md new file mode 100644 index 000000000..a37cc3520 --- /dev/null +++ b/docs/content/dns/zz_gen_jdcloud.md @@ -0,0 +1,71 @@ +--- +title: "JD Cloud" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: jdcloud +dnsprovider: + since: "v4.31.0" + code: "jdcloud" + url: "https://www.jdcloud.com/" +--- + + + + + + +Configuration for [JD Cloud](https://www.jdcloud.com/). + + + + +- Code: `jdcloud` +- Since: v4.31.0 + + +Here is an example bash command using the JD Cloud provider: + +```bash +JDCLOUD_ACCESS_KEY_ID="xxx" \ +JDCLOUD_ACCESS_KEY_SECRET="yyy" \ +lego --dns jdcloud -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `JDCLOUD_ACCESS_KEY_ID` | Access key ID | +| `JDCLOUD_ACCESS_KEY_SECRET` | Access key secret | + +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 | +|--------------------------------|-------------| +| `JDCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `JDCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `JDCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `JDCLOUD_REGION_ID` | Region ID (Default: cn-north-1) | +| `JDCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://docs.jdcloud.com/cn/jd-cloud-dns/api/overview) +- [Go client](https://github.com/jdcloud-api/jdcloud-sdk-go) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 2ee3e9006..7d1a4b2c3 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, 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 + 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, jdcloud, 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/go.mod b/go.mod index dd1a5b4b7..59da5e349 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/exoscale/egoscale/v3 v3.1.31 github.com/go-acme/alidns-20150109/v4 v4.7.0 github.com/go-acme/esa-20240910/v2 v2.40.3 + github.com/go-acme/jdcloud-sdk-go v1.64.0 github.com/go-acme/tencentclouddnspod v1.1.25 github.com/go-acme/tencentedgdeone v1.1.48 github.com/go-jose/go-jose/v4 v4.1.3 @@ -154,6 +155,7 @@ require ( github.com/go-resty/resty/v2 v2.17.0 // indirect github.com/goccy/go-yaml v1.9.8 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/go.sum b/go.sum index 5e63fdba3..993e97c7d 100644 --- a/go.sum +++ b/go.sum @@ -317,6 +317,8 @@ github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQL github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= github.com/go-acme/esa-20240910/v2 v2.40.3 h1:xXOMRex148wKEHbv7Izn73/HdAxSmz5GOaF4HdnqN+M= github.com/go-acme/esa-20240910/v2 v2.40.3/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= +github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs= +github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU= github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s= github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE= github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk= @@ -374,6 +376,8 @@ github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXK github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/providers/dns/jdcloud/fixtures/create_record-request.json b/providers/dns/jdcloud/fixtures/create_record-request.json new file mode 100644 index 000000000..581c00fea --- /dev/null +++ b/providers/dns/jdcloud/fixtures/create_record-request.json @@ -0,0 +1,15 @@ +{ + "domainId": "20", + "regionId": "cn-north-1", + "req": { + "hostRecord": "_acme-challenge", + "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "jcloudRes": null, + "mxPriority": null, + "port": null, + "ttl": 120, + "type": "TXT", + "viewValue": -1, + "weight": null + } +} diff --git a/providers/dns/jdcloud/fixtures/create_record.json b/providers/dns/jdcloud/fixtures/create_record.json new file mode 100644 index 000000000..08bd3db26 --- /dev/null +++ b/providers/dns/jdcloud/fixtures/create_record.json @@ -0,0 +1,25 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": { + "id": 123, + "hostRecord": "_acme-challenge", + "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "jcloudRes": false, + "mxPriority": 0, + "port": 0, + "ttl": 120, + "type": "TXT", + "weight": 0, + "viewValue": [ + 1, + 2 + ] + } + } +} diff --git a/providers/dns/jdcloud/fixtures/delete_record.json b/providers/dns/jdcloud/fixtures/delete_record.json new file mode 100644 index 000000000..20525751c --- /dev/null +++ b/providers/dns/jdcloud/fixtures/delete_record.json @@ -0,0 +1,9 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": {} +} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page1.json b/providers/dns/jdcloud/fixtures/describe_domains_page1.json new file mode 100644 index 000000000..cde6dcd6f --- /dev/null +++ b/providers/dns/jdcloud/fixtures/describe_domains_page1.json @@ -0,0 +1,55 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": [ + { + "id": 1, + "domainName": "1.example" + }, + { + "id": 2, + "domainName": "2.example" + }, + { + "id": 3, + "domainName": "3.example" + }, + { + "id": 4, + "domainName": "4.example" + }, + { + "id": 5, + "domainName": "5.example" + }, + { + "id": 6, + "domainName": "6.example" + }, + { + "id": 7, + "domainName": "7.example" + }, + { + "id": 8, + "domainName": "8.example" + }, + { + "id": 9, + "domainName": "9.example" + }, + { + "id": 10, + "domainName": "10.example" + } + ], + "currentCount": 10, + "totalCount": 20, + "totalPage": 2 + } +} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page2.json b/providers/dns/jdcloud/fixtures/describe_domains_page2.json new file mode 100644 index 000000000..b1e1560ab --- /dev/null +++ b/providers/dns/jdcloud/fixtures/describe_domains_page2.json @@ -0,0 +1,55 @@ +{ + "requestId": "azerty", + "error": { + "code": 0, + "status": "", + "message": "" + }, + "result": { + "dataList": [ + { + "id": 11, + "domainName": "11.example" + }, + { + "id": 12, + "domainName": "12.example" + }, + { + "id": 13, + "domainName": "13.example" + }, + { + "id": 14, + "domainName": "14.example" + }, + { + "id": 15, + "domainName": "15.example" + }, + { + "id": 16, + "domainName": "16.example" + }, + { + "id": 17, + "domainName": "17.example" + }, + { + "id": 18, + "domainName": "18.example" + }, + { + "id": 19, + "domainName": "19.example" + }, + { + "id": 20, + "domainName": "example.com" + } + ], + "currentCount": 10, + "totalCount": 20, + "totalPage": 2 + } +} diff --git a/providers/dns/jdcloud/jdcloud.go b/providers/dns/jdcloud/jdcloud.go new file mode 100644 index 000000000..7d9ad4e6b --- /dev/null +++ b/providers/dns/jdcloud/jdcloud.go @@ -0,0 +1,217 @@ +// Package jdcloud implements a DNS provider for solving the DNS-01 challenge using JD Cloud. +package jdcloud + +import ( + "errors" + "fmt" + "strconv" + "sync" + "time" + + "github.com/go-acme/jdcloud-sdk-go/core" + "github.com/go-acme/jdcloud-sdk-go/services/domainservice/apis" + jdcclient "github.com/go-acme/jdcloud-sdk-go/services/domainservice/client" + domainservice "github.com/go-acme/jdcloud-sdk-go/services/domainservice/models" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +// Environment variables names. +const ( + envNamespace = "JDCLOUD_" + + EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" + EnvAccessKeySecret = envNamespace + "ACCESS_KEY_SECRET" + EnvRegionID = envNamespace + "REGION_ID" + + 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 { + AccessKeyID string + AccessKeySecret string + RegionID 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, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *jdcclient.DomainserviceClient + + recordIDs map[string]int + domainIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for JD Cloud. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret) + if err != nil { + return nil, fmt.Errorf("jdcloud: %w", err) + } + + config := NewDefaultConfig() + config.AccessKeyID = values[EnvAccessKeyID] + config.AccessKeySecret = values[EnvAccessKeySecret] + + // https://docs.jdcloud.com/en/common-declaration/api/introduction#Region%20Code + config.RegionID = env.GetOrDefaultString(EnvRegionID, "cn-north-1") + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for JD Cloud. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("jdcloud: the configuration of the DNS provider is nil") + } + + if config.AccessKeyID == "" || config.AccessKeySecret == "" { + return nil, errors.New("jdcloud: missing credentials") + } + + cred := core.NewCredentials(config.AccessKeyID, config.AccessKeySecret) + + client := jdcclient.NewDomainserviceClient(cred) + client.DisableLogger() + client.Config.SetTimeout(config.HTTPTimeout) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int), + domainIDs: 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("jdcloud: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("jdcloud: %w", err) + } + + zone, err := d.findZone(dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("jdcloud: %w", err) + } + + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/createresourcerecord + crrr := apis.NewCreateResourceRecordRequestWithAllParams( + d.config.RegionID, + strconv.Itoa(zone.Id), + &domainservice.AddRR{ + HostRecord: subDomain, + HostValue: info.Value, + Ttl: d.config.TTL, + Type: "TXT", + ViewValue: -1, + }, + ) + + record, err := jdcclient.CreateResourceRecord(d.client, crrr) + if err != nil { + return fmt.Errorf("jdcloud: create resource record: %w", err) + } + + d.recordIDsMu.Lock() + d.domainIDs[token] = zone.Id + d.recordIDs[token] = record.Result.DataList.Id + 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) + + d.recordIDsMu.Lock() + recordID, recordOK := d.recordIDs[token] + domainID, domainOK := d.domainIDs[token] + d.recordIDsMu.Unlock() + + if !recordOK { + return fmt.Errorf("jdcloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + if !domainOK { + return fmt.Errorf("jdcloud: unknown domain ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/deleteresourcerecord + drrr := apis.NewDeleteResourceRecordRequestWithAllParams( + d.config.RegionID, + strconv.Itoa(domainID), + strconv.Itoa(recordID), + ) + + _, err := jdcclient.DeleteResourceRecord(d.client, drrr) + if err != nil { + return fmt.Errorf("jdcloud: delete resource record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(zone string) (*domainservice.DomainInfo, error) { + // https://docs.jdcloud.com/cn/jd-cloud-dns/api/describedomains + ddr := apis.NewDescribeDomainsRequestWithoutParam() + ddr.SetRegionId(d.config.RegionID) + ddr.SetPageNumber(1) + ddr.SetPageSize(10) + ddr.SetDomainName(zone) + + for { + response, err := jdcclient.DescribeDomains(d.client, ddr) + if err != nil { + return nil, fmt.Errorf("describe domains: %w", err) + } + + for _, d := range response.Result.DataList { + if d.DomainName == zone { + return &d, nil + } + } + + if len(response.Result.DataList) < ddr.PageSize || response.Result.TotalPage <= ddr.PageNumber { + break + } + + ddr.SetPageNumber(ddr.PageNumber + 1) + } + + return nil, errors.New("zone not found") +} diff --git a/providers/dns/jdcloud/jdcloud.toml b/providers/dns/jdcloud/jdcloud.toml new file mode 100644 index 000000000..7ab403822 --- /dev/null +++ b/providers/dns/jdcloud/jdcloud.toml @@ -0,0 +1,27 @@ +Name = "JD Cloud" +Description = '''''' +URL = "https://www.jdcloud.com/" +Code = "jdcloud" +Since = "v4.31.0" + +Example = ''' +JDCLOUD_ACCESS_KEY_ID="xxx" \ +JDCLOUD_ACCESS_KEY_SECRET="yyy" \ +lego --dns jdcloud -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + JDCLOUD_ACCESS_KEY_ID = "Access key ID" + JDCLOUD_ACCESS_KEY_SECRET = "Access key secret" + [Configuration.Additional] + JDCLOUD_REGION_ID = "Region ID (Default: cn-north-1)" + JDCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + JDCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + JDCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + JDCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview" + Common = "https://docs.jdcloud.com/en/common-declaration/api/introduction" + GoClient = "https://github.com/jdcloud-api/jdcloud-sdk-go" diff --git a/providers/dns/jdcloud/jdcloud_test.go b/providers/dns/jdcloud/jdcloud_test.go new file mode 100644 index 000000000..6b3368938 --- /dev/null +++ b/providers/dns/jdcloud/jdcloud_test.go @@ -0,0 +1,242 @@ +package jdcloud + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAccessKeyID, + EnvAccessKeySecret, + EnvRegionID, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAccessKeyID: "abc123", + EnvAccessKeySecret: "secret", + }, + }, + { + desc: "missing access key ID", + envVars: map[string]string{ + EnvAccessKeyID: "", + EnvAccessKeySecret: "secret", + }, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID", + }, + { + desc: "missing access key secret", + envVars: map[string]string{ + EnvAccessKeyID: "abc123", + EnvAccessKeySecret: "", + }, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_SECRET", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID,JDCLOUD_ACCESS_KEY_SECRET", + }, + } + + 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 + accessKeyID string + accessKeySecret string + expected string + }{ + { + desc: "success", + accessKeyID: "abc123", + accessKeySecret: "secret", + }, + { + desc: "missing access key ID", + accessKeySecret: "secret", + expected: "jdcloud: missing credentials", + }, + { + desc: "missing access key secret", + accessKeyID: "abc123", + expected: "jdcloud: missing credentials", + }, + { + desc: "missing credentials", + expected: "jdcloud: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AccessKeyID = test.accessKeyID + config.AccessKeySecret = test.accessKeySecret + config.RegionID = "cn-north-1" + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.AccessKeyID = "abc123" + config.AccessKeySecret = "secret" + config.RegionID = "cn-north-1" + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + serverURL, _ := url.Parse(server.URL) + + p.client.Config.SetEndpoint(net.JoinHostPort(serverURL.Hostname(), serverURL.Port())) + p.client.Config.SetScheme(serverURL.Scheme) + p.client.Config.SetTimeout(server.Client().Timeout) + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /v2/regions/cn-north-1/domain", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + pageNumber := req.URL.Query().Get("pageNumber") + + servermock.ResponseFromFixture( + fmt.Sprintf("describe_domains_page%s.json", pageNumber), + ).ServeHTTP(rw, req) + }), + servermock.CheckQueryParameter().Strict(). + With("domainName", "example.com"). + WithRegexp("pageNumber", `(1|2)`). + With("pageSize", "10"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Route("POST /v2/regions/cn-north-1/domain/20/ResourceRecord", + servermock.ResponseFromFixture("create_record.json"), + servermock.CheckRequestJSONBodyFromFixture("create_record-request.json"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) + + require.Len(t, provider.domainIDs, 1) + require.Len(t, provider.recordIDs, 1) + + assert.Equal(t, 20, provider.domainIDs["abc"]) + assert.Equal(t, 123, provider.recordIDs["abc"]) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /v2/regions/cn-north-1/domain/20/ResourceRecord/123", + servermock.ResponseFromFixture("delete_record.json"), + servermock.CheckHeader(). + WithRegexp("Authorization", + `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). + WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). + WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), + ). + Build(t) + + provider.domainIDs["abc"] = 20 + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 0d9ad26e8..7099a3f4c 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -99,6 +99,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/ispconfig" "github.com/go-acme/lego/v4/providers/dns/ispconfigddns" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" + "github.com/go-acme/lego/v4/providers/dns/jdcloud" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/keyhelp" "github.com/go-acme/lego/v4/providers/dns/liara" @@ -377,6 +378,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return ispconfigddns.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() + case "jdcloud": + return jdcloud.NewDNSProvider() case "joker": return joker.NewDNSProvider() case "keyhelp": From b7a9b7dad0a2ffe4f90f88c0141f337e781253db Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 8 Jan 2026 17:33:11 +0100 Subject: [PATCH 58/95] chore: update dependencies (#2783) --- go.mod | 60 ++++++++++++++-------------- go.sum | 123 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 92 insertions(+), 91 deletions(-) diff --git a/go.mod b/go.mod index 59da5e349..42e11820b 100644 --- a/go.mod +++ b/go.mod @@ -13,43 +13,43 @@ require ( github.com/Azure/go-autorest/autorest v0.11.30 github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 github.com/Azure/go-autorest/autorest/to v0.4.1 - github.com/BurntSushi/toml v1.5.0 + github.com/BurntSushi/toml v1.6.0 github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 - github.com/alibabacloud-go/tea v1.3.14 + github.com/alibabacloud-go/tea v1.4.0 github.com/aliyun/credentials-go v1.4.7 github.com/aws/aws-sdk-go-v2 v1.41.0 - github.com/aws/aws-sdk-go-v2/config v1.32.5 - github.com/aws/aws-sdk-go-v2/credentials v1.19.5 + github.com/aws/aws-sdk-go-v2/config v1.32.6 + github.com/aws/aws-sdk-go-v2/credentials v1.19.6 github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 github.com/aziontech/azionapi-go-sdk v0.144.0 - github.com/baidubce/bce-sdk-go v0.9.254 + github.com/baidubce/bce-sdk-go v0.9.256 github.com/cenkalti/backoff/v5 v5.0.3 github.com/dnsimple/dnsimple-go/v4 v4.0.0 - github.com/exoscale/egoscale/v3 v3.1.31 + github.com/exoscale/egoscale/v3 v3.1.33 github.com/go-acme/alidns-20150109/v4 v4.7.0 - github.com/go-acme/esa-20240910/v2 v2.40.3 + github.com/go-acme/esa-20240910/v2 v2.44.0 github.com/go-acme/jdcloud-sdk-go v1.64.0 github.com/go-acme/tencentclouddnspod v1.1.25 github.com/go-acme/tencentedgdeone v1.1.48 github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-querystring v1.1.0 + github.com/google/go-querystring v1.2.0 github.com/google/uuid v1.6.0 github.com/gophercloud/gophercloud v1.14.1 github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-version v1.8.0 - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/ldez/grignotin v0.10.1 - github.com/linode/linodego v1.62.0 + github.com/linode/linodego v1.64.0 github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.69 @@ -65,8 +65,8 @@ require ( github.com/nrdcg/mailinabox v0.3.0 github.com/nrdcg/namesilo v0.5.0 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 + github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 + github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 github.com/nrdcg/porkbun v0.4.0 github.com/nrdcg/vegadns v0.3.0 github.com/nzdjb/go-metaname v1.0.0 @@ -76,34 +76,34 @@ require ( github.com/regfish/regfish-dnsapi-go v0.1.1 github.com/sacloud/api-client-go v0.3.3 github.com/sacloud/iaas-api-go v1.23.1 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 github.com/selectel/domains-go v1.1.0 github.com/selectel/go-selvpcclient/v4 v4.1.0 github.com/softlayer/softlayer-go v1.2.1 github.com/stretchr/testify v1.11.1 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 github.com/transip/gotransip/v6 v6.26.1 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 github.com/urfave/cli/v2 v2.27.7 github.com/vinyldns/go-vinyldns v0.9.17 - github.com/volcengine/volc-sdk-golang v1.0.230 - github.com/vultr/govultr/v3 v3.26.0 - github.com/yandex-cloud/go-genproto v0.41.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.23 - github.com/yandex-cloud/go-sdk/v2 v2.33.0 + github.com/volcengine/volc-sdk-golang v1.0.233 + github.com/vultr/govultr/v3 v3.26.1 + github.com/yandex-cloud/go-genproto v0.43.0 + github.com/yandex-cloud/go-sdk/services/dns v0.0.25 + github.com/yandex-cloud/go-sdk/v2 v2.37.0 golang.org/x/crypto v0.46.0 golang.org/x/net v0.48.0 golang.org/x/oauth2 v0.34.0 golang.org/x/text v0.32.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.257.0 + google.golang.org/api v0.259.0 gopkg.in/ns1/ns1-go.v2 v2.16.0 gopkg.in/yaml.v2 v2.4.0 - software.sslmate.com/src/go-pkcs12 v0.6.0 + software.sslmate.com/src/go-pkcs12 v0.7.0 ) require ( - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth v0.18.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect @@ -129,7 +129,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -152,7 +152,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/go-resty/resty/v2 v2.17.0 // indirect + github.com/go-resty/resty/v2 v2.17.1 // indirect github.com/goccy/go-yaml v1.9.8 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect @@ -161,7 +161,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -217,10 +217,10 @@ require ( golang.org/x/sys v0.39.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 993e97c7d..889e6b5b5 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= +cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -88,8 +88,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= @@ -144,8 +144,8 @@ github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= -github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c= -github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= +github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= @@ -173,10 +173,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgP github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= -github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= +github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= +github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= @@ -200,12 +200,12 @@ github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCM github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= @@ -215,8 +215,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo= github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU= -github.com/baidubce/bce-sdk-go v0.9.254 h1:A7GtBOt7z2lnV7fqlZPZefhcBFg7z6iliUAhEOiIhoE= -github.com/baidubce/bce-sdk-go v0.9.254/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/baidubce/bce-sdk-go v0.9.256 h1:/6UwBzDp+dRFpKRIb5WsvxfSiG4SLOIOghvagOK/q4Y= +github.com/baidubce/bce-sdk-go v0.9.256/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -285,8 +285,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.31 h1:/dySEUSAxU+hlAS/eLxAoY8ZYmtOtaoL1P+lDwH7ojY= -github.com/exoscale/egoscale/v3 v3.1.31/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= +github.com/exoscale/egoscale/v3 v3.1.33 h1:5Lk/pwZ+K0sjNu9obS0VYPfhZQffRkvvO0BpdPoir4o= +github.com/exoscale/egoscale/v3 v3.1.33/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -315,8 +315,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= -github.com/go-acme/esa-20240910/v2 v2.40.3 h1:xXOMRex148wKEHbv7Izn73/HdAxSmz5GOaF4HdnqN+M= -github.com/go-acme/esa-20240910/v2 v2.40.3/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= +github.com/go-acme/esa-20240910/v2 v2.44.0 h1:ACi2uFb7ig4ousFs/YiFBR+aw3A4SHtOxvkMWB2Hbcs= +github.com/go-acme/esa-20240910/v2 v2.44.0/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs= github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU= github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s= @@ -359,8 +359,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0= -github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= +github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -444,8 +444,9 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -470,8 +471,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAV github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= @@ -536,8 +537,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 h1:uia+R3K1izQRGpxTV+bS4q3/ueMW7ProAMWqM6OlqOU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 h1:B3W9acgpqu5XsN8v+W8SOTfqn/6n4JsjgoKBsm30HFY= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -611,8 +612,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/linode/linodego v1.62.0 h1:eCo1sepsIPGkI66Cz9IaCylWxKDD2aSc5UYq20iBMfw= -github.com/linode/linodego v1.62.0/go.mod h1:FoIEsuZMRlXiUt6RnuGcPTek5iAO3VfE6bjMpGlcQ2U= +github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY= +github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -710,10 +711,10 @@ github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1 h1:yHD01L6wN7mhGikS08izrMuEp9PRtvingePXkjRHrSg= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1 h1:9ApYlc4bjup9WnxOFmgvh00bDqd6KMqAbAR4klKzluA= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.1/go.mod h1:iOzhDeDcQGJZVgSDKrl5p3HUWexNo3LOlicf0D9ltgk= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU= @@ -829,8 +830,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= @@ -905,8 +906,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12 h1:/ABtv4x4FSGxGW0d6Sc88iQn6Up2LalWKwt/Tj7Dtz8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 h1:Rj1WXXNPm9AsPf0PJhWCvlsqfcKPUYdyVnkmEc3O8sI= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= @@ -921,10 +922,10 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI= github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4= -github.com/volcengine/volc-sdk-golang v1.0.230 h1:84/MOF0zUPtAHt3e1+MsFq5qrnQRC+e3XzTUwIOzZxw= -github.com/volcengine/volc-sdk-golang v1.0.230/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.26.0 h1:pm/GM+RZo9T1JLQzrUti5HiNAIFZFEHcPFMOWGvvNIY= -github.com/vultr/govultr/v3 v3.26.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= +github.com/volcengine/volc-sdk-golang v1.0.233 h1:Hh2pzwu/Wq19rsZgNo3HdpjQB28D/F0+m6EjLVggmhM= +github.com/volcengine/volc-sdk-golang v1.0.233/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= +github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw= +github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= @@ -933,12 +934,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.41.0 h1:l0HWC7S82XgfioqOQ+d2wx7PRB5Eo71KiUb4PiWbDXQ= -github.com/yandex-cloud/go-genproto v0.41.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.23 h1:fR4tqSRKTpzh4RczXJbU7EOXh4+kJnp+dccRpL2PLPQ= -github.com/yandex-cloud/go-sdk/services/dns v0.0.23/go.mod h1:Lgly3dyKBGrAIpIo6nrkEpQOoSQYlnik1HLKMeZcA98= -github.com/yandex-cloud/go-sdk/v2 v2.33.0 h1:wuvpirhYcHSejLDXSxLGsNoZHqkjrXevzVxw7SrrN/0= -github.com/yandex-cloud/go-sdk/v2 v2.33.0/go.mod h1:OqkwauVaBxbrrfN+JOYBIuE8GrHz1g0Z42VIkbsGvPI= +github.com/yandex-cloud/go-genproto v0.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw= +github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk/services/dns v0.0.25 h1:BcGEuOnwq2X3LS2kvFC6BOdZkOq4Lc7XAYvzap/SJJY= +github.com/yandex-cloud/go-sdk/services/dns v0.0.25/go.mod h1:B4QHijALUHIjRxL3aqmOwDrHYUI2XdeeG4WKItth3jI= +github.com/yandex-cloud/go-sdk/v2 v2.37.0 h1:WvttW6p9xcWag9j+GQv+GJXPggggXGwOlIJNfkWmFWw= +github.com/yandex-cloud/go-sdk/v2 v2.37.0/go.mod h1:Dt4a81enjRsm4xMJyW5E1Y/vaUYwXJvUGRdDLuM2k6I= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= @@ -1380,8 +1381,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= +google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1420,12 +1421,12 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1443,8 +1444,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1459,8 +1460,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1509,5 +1510,5 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU= -software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= +software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= From eed3f0dcc8ffaa5457c31c92342259dd1e222463 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 8 Jan 2026 17:33:57 +0100 Subject: [PATCH 59/95] chore: update linter (#2785) --- .github/workflows/pr.yml | 2 +- providers/dns/internal/selectel/internal/client.go | 4 ++-- providers/dns/selectelv2/selectelv2.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 151a2a6e0..5df64db7f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.7.1 + GOLANGCI_LINT_VERSION: v2.8.0 HUGO_VERSION: 0.148.2 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI diff --git a/providers/dns/internal/selectel/internal/client.go b/providers/dns/internal/selectel/internal/client.go index b17df6d83..d441c9894 100644 --- a/providers/dns/internal/selectel/internal/client.go +++ b/providers/dns/internal/selectel/internal/client.go @@ -53,8 +53,8 @@ func (c *Client) GetDomainByName(ctx context.Context, domainName string) (*Domai if err != nil { if statusCode == http.StatusNotFound && strings.Count(domainName, ".") > 1 { // Look up for the next subdomain - subIndex := strings.Index(domainName, ".") - return c.GetDomainByName(ctx, domainName[subIndex+1:]) + _, after, _ := strings.Cut(domainName, ".") + return c.GetDomainByName(ctx, after) } return nil, err diff --git a/providers/dns/selectelv2/selectelv2.go b/providers/dns/selectelv2/selectelv2.go index 6e3c1f42c..1fcb48583 100644 --- a/providers/dns/selectelv2/selectelv2.go +++ b/providers/dns/selectelv2/selectelv2.go @@ -297,10 +297,10 @@ func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi. return nil, fmt.Errorf("zone '%s' for challenge has not been found", name) } - // -1 can not be returned since if no dots present we exit above - i := strings.Index(name, ".") + // after is always defined since if no dots present we exit above. + _, after, _ := strings.Cut(name, ".") - return w.getZone(ctx, name[i+1:]) + return w.getZone(ctx, after) } func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) { From b77b8709b6802da29a702b44bb0a5279c35eb337 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 8 Jan 2026 17:34:37 +0100 Subject: [PATCH 60/95] namedotcom: follow CNAME (#2390) --- providers/dns/namedotcom/namedotcom.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/providers/dns/namedotcom/namedotcom.go b/providers/dns/namedotcom/namedotcom.go index 3d1f33af1..04c8b5967 100644 --- a/providers/dns/namedotcom/namedotcom.go +++ b/providers/dns/namedotcom/namedotcom.go @@ -116,7 +116,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - // TODO(ldez) replace domain by FQDN to follow CNAME. + if info.EffectiveFQDN != info.FQDN { + domain = dns01.UnFqdn(info.EffectiveFQDN) + } + domainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain}) if err != nil { return fmt.Errorf("namedotcom: API call failed: %w", err) @@ -127,7 +130,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("namedotcom: %w", err) } - // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.Record{ DomainName: domain, Host: subDomain, @@ -148,7 +150,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - // TODO(ldez) replace domain by FQDN to follow CNAME. + if info.EffectiveFQDN != info.FQDN { + domain = dns01.UnFqdn(info.EffectiveFQDN) + } + records, err := d.getRecords(domain) if err != nil { return fmt.Errorf("namedotcom: %w", err) @@ -156,7 +161,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { for _, rec := range records { if rec.Fqdn == info.EffectiveFQDN && rec.Type == "TXT" { - // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.DeleteRecordRequest{ DomainName: domain, ID: rec.ID, From 9f3dde3f6d2ca2204b823f7f58ba5c2a07bc35e3 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 8 Jan 2026 17:42:25 +0100 Subject: [PATCH 61/95] Prepare release v4.31.0 --- CHANGELOG.md | 24 +++++++++++++++++++ acme/api/internal/sender/useragent.go | 4 ++-- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 4 ++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9974d550..ee191cdb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.31.0 + +- Release date: 2026-01-08 +- Tag: [v4.31.0](https://github.com/go-acme/lego/releases/tag/v4.31.0) + +### Added + +- **[dnsprovider]** Add DNS provider for ISPConfig +- **[dnsprovider]** Add DNS Provider for ISPConfig (DDNS Module) +- **[dnsprovider]** Add DNS provider for Alwaysdata +- **[dnsprovider]** Add DNS provider for JDCloud +- **[dnsprovider]** Add DNS provider for 35.com/三五互联 +- **[dnsprovider]** f5xc: add an option to configure the domain of the server + +### Changed + +- **[lib]** feat: improve ACME error types +- **[dnsprovider,cname]** namedotcom: follow CNAME + +### Fixed + +- **[dnsprovider]** hetzner: fix compatibility with _FILE suffix +- **[dnsprovider]** gandiv5: fix API Key header + ## v4.30.1 - Release date: 2025-12-16 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 25443fa05..6b1ebb1c7 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.30.1" + ourUserAgent = "xenolf-acme/4.31.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index da867f0cd..f3fb3d253 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.30.1+dev-detach" +const defaultVersion = "v4.31.0+dev-release" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 4f8e693c2..480c35af1 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.30.1" + ourUserAgent = "goacme-lego/4.31.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From ac1092710d4c7b5032d894ccaac15498fc9cbabc Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 8 Jan 2026 17:42:41 +0100 Subject: [PATCH 62/95] Detach v4.31.0 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 6b1ebb1c7..570b3b67c 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index f3fb3d253..57899e15e 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.31.0+dev-release" +const defaultVersion = "v4.31.0+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 480c35af1..da24329cc 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From 7f10c131f438d65b418cbb92840af978fbd19c67 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 12 Jan 2026 17:50:21 +0100 Subject: [PATCH 63/95] =?UTF-8?q?Add=20DNS=20provider=20for=20TodayNIC/?= =?UTF-8?q?=E6=97=B6=E4=BB=A3=E4=BA=92=E8=81=94=20(#2788)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- cmd/zz_gen_cmd_dnshelp.go | 22 ++ docs/content/dns/zz_gen_todaynic.md | 69 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/todaynic/internal/client.go | 141 ++++++++++++ .../dns/todaynic/internal/client_test.go | 94 ++++++++ .../internal/fixtures/add_record.json | 4 + .../dns/todaynic/internal/fixtures/error.json | 4 + providers/dns/todaynic/internal/types.go | 26 +++ providers/dns/todaynic/todaynic.go | 164 ++++++++++++++ providers/dns/todaynic/todaynic.toml | 25 +++ providers/dns/todaynic/todaynic_test.go | 207 ++++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 767 insertions(+), 8 deletions(-) create mode 100644 docs/content/dns/zz_gen_todaynic.md create mode 100644 providers/dns/todaynic/internal/client.go create mode 100644 providers/dns/todaynic/internal/client_test.go create mode 100644 providers/dns/todaynic/internal/fixtures/add_record.json create mode 100644 providers/dns/todaynic/internal/fixtures/error.json create mode 100644 providers/dns/todaynic/internal/types.go create mode 100644 providers/dns/todaynic/todaynic.go create mode 100644 providers/dns/todaynic/todaynic.toml create mode 100644 providers/dns/todaynic/todaynic_test.go diff --git a/README.md b/README.md index c02347e23..8ca213962 100644 --- a/README.md +++ b/README.md @@ -256,40 +256,40 @@ If your DNS provider is not supported, please open an [issue](https://github.com Tencent EdgeOne Timeweb Cloud + TodayNIC/时代互联 TransIP UKFast SafeDNS Ultradns - United-Domains + United-Domains Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS Virtualname VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index a918a1484..a2e0d4aaa 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -169,6 +169,7 @@ func allDNSCodes() string { "technitium", "tencentcloud", "timewebcloud", + "todaynic", "transip", "ultradns", "uniteddomains", @@ -3574,6 +3575,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/timewebcloud`) + case "todaynic": + // generated from: providers/dns/todaynic/todaynic.toml + ew.writeln(`Configuration for TodayNIC/时代互联.`) + ew.writeln(`Code: 'todaynic'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "TODAYNIC_API_KEY": API key`) + ew.writeln(` - "TODAYNIC_AUTH_USER_ID": account ID`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "TODAYNIC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "TODAYNIC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "TODAYNIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "TODAYNIC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/todaynic`) + case "transip": // generated from: providers/dns/transip/transip.toml ew.writeln(`Configuration for TransIP.`) diff --git a/docs/content/dns/zz_gen_todaynic.md b/docs/content/dns/zz_gen_todaynic.md new file mode 100644 index 000000000..7b06c012d --- /dev/null +++ b/docs/content/dns/zz_gen_todaynic.md @@ -0,0 +1,69 @@ +--- +title: "TodayNIC/时代互联" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: todaynic +dnsprovider: + since: "v4.32.0" + code: "todaynic" + url: "https://www.todaynic.com/" +--- + + + + + + +Configuration for [TodayNIC/时代互联](https://www.todaynic.com/). + + + + +- Code: `todaynic` +- Since: v4.32.0 + + +Here is an example bash command using the TodayNIC/时代互联 provider: + +```bash +TODAYNIC_AUTH_USER_ID="xxx" \ +TODAYNIC_API_KEY="yyy" \ +lego --dns todaynic -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `TODAYNIC_API_KEY` | API key | +| `TODAYNIC_AUTH_USER_ID` | account ID | + +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 | +|--------------------------------|-------------| +| `TODAYNIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `TODAYNIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `TODAYNIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `TODAYNIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | + +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://www.todaynic.com/partner/mode_Http_Api_detail.php) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 7d1a4b2c3..8de6475e1 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, 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, jdcloud, 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, jdcloud, 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, todaynic, 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/todaynic/internal/client.go b/providers/dns/todaynic/internal/client.go new file mode 100644 index 000000000..2c537f4a7 --- /dev/null +++ b/providers/dns/todaynic/internal/client.go @@ -0,0 +1,141 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +const defaultBaseURL = "https://todapi.now.cn:2443" + +// Client the TodayNIC API client. +type Client struct { + authUserID string + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(authUserID, apiKey string) (*Client, error) { + if authUserID == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + authUserID: authUserID, + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddRecord(ctx context.Context, record Record) (int, error) { + endpoint := c.BaseURL.JoinPath("api", "dns", "add-domain-record.json") + + query, err := querystring.Values(record) + if err != nil { + return 0, err + } + + req, err := c.newRequest(ctx, endpoint, query) + if err != nil { + return 0, err + } + + var result APIResponse + + err = c.do(req, &result) + if err != nil { + return 0, err + } + + return result.ID, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, recordID int) error { + endpoint := c.BaseURL.JoinPath("api", "dns", "delete-domain-record.json") + + query := endpoint.Query() + query.Set("Id", strconv.Itoa(recordID)) + + req, err := c.newRequest(ctx, endpoint, query) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func (c *Client) newRequest(ctx context.Context, endpoint *url.URL, query url.Values) (*http.Request, error) { + query.Set("auth-userid", c.authUserID) + query.Set("api-key", c.apiKey) + + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/todaynic/internal/client_test.go b/providers/dns/todaynic/internal/client_test.go new file mode 100644 index 000000000..71ee7f8b7 --- /dev/null +++ b/providers/dns/todaynic/internal/client_test.go @@ -0,0 +1,94 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("user123", "secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromFixture("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Domain", "example.com"). + With("Host", "_acme-challenge"). + With("Type", "TXT"). + With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("Ttl", "600"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + record := Record{ + Domain: "example.com", + Host: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "600", + } + + recordID, err := client.AddRecord(t.Context(), record) + require.NoError(t, err) + + assert.Equal(t, 11554102, recordID) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusNotFound), + ). + Build(t) + + record := Record{ + Domain: "example.com", + Host: "_acme-challenge", + Type: "TXT", + Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "600", + } + + _, err := client.AddRecord(t.Context(), record) + require.EqualError(t, err, "host.repeat (2d5876b2-f272-43e9-acc1-4c6a3d3683b1)") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /api/dns/delete-domain-record.json", + servermock.ResponseFromFixture("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Id", "123"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + err := client.DeleteRecord(t.Context(), 123) + require.NoError(t, err) +} diff --git a/providers/dns/todaynic/internal/fixtures/add_record.json b/providers/dns/todaynic/internal/fixtures/add_record.json new file mode 100644 index 000000000..27f34d71c --- /dev/null +++ b/providers/dns/todaynic/internal/fixtures/add_record.json @@ -0,0 +1,4 @@ +{ + "RequestId": "f60ea4d9-67ef-49fa-bbae-06178a6e7293", + "Id": 11554102 +} diff --git a/providers/dns/todaynic/internal/fixtures/error.json b/providers/dns/todaynic/internal/fixtures/error.json new file mode 100644 index 000000000..3ea9c9310 --- /dev/null +++ b/providers/dns/todaynic/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "RequestId": "2d5876b2-f272-43e9-acc1-4c6a3d3683b1", + "error": "host.repeat" +} diff --git a/providers/dns/todaynic/internal/types.go b/providers/dns/todaynic/internal/types.go new file mode 100644 index 000000000..0a15c7da8 --- /dev/null +++ b/providers/dns/todaynic/internal/types.go @@ -0,0 +1,26 @@ +package internal + +import "fmt" + +type APIError struct { + RequestID string `json:"RequestId"` + Message string `json:"error"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("%s (%s)", a.Message, a.RequestID) +} + +type Record struct { + Domain string `url:"Domain,omitempty"` + Host string `url:"Host,omitempty"` + Type string `url:"Type,omitempty"` + Value string `url:"Value,omitempty"` + Mx string `url:"Mx,omitempty"` + TTL string `url:"Ttl,omitempty"` +} + +type APIResponse struct { + RequestID string `json:"RequestId"` + ID int `json:"Id"` +} diff --git a/providers/dns/todaynic/todaynic.go b/providers/dns/todaynic/todaynic.go new file mode 100644 index 000000000..3a3734033 --- /dev/null +++ b/providers/dns/todaynic/todaynic.go @@ -0,0 +1,164 @@ +// Package todaynic implements a DNS provider for solving the DNS-01 challenge using TodayNIC. +package todaynic + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "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/todaynic/internal" +) + +// Environment variables names. +const ( + envNamespace = "TODAYNIC_" + + EnvAuthUserID = envNamespace + "AUTH_USER_ID" + EnvAPIKey = envNamespace + "API_KEY" + + 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 { + AuthUserID string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// 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), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + recordIDs map[string]int + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for TodayNIC. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAuthUserID, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("todaynic: %w", err) + } + + config := NewDefaultConfig() + config.AuthUserID = values[EnvAuthUserID] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for TodayNIC. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("todaynic: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.AuthUserID, config.APIKey) + if err != nil { + return nil, fmt.Errorf("todaynic: %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 +} + +// 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("todaynic: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("todaynic: %w", err) + } + + record := internal.Record{ + Domain: dns01.UnFqdn(authZone), + Host: subDomain, + Type: "TXT", + Value: info.Value, + TTL: strconv.Itoa(d.config.TTL), + } + + recordID, err := d.client.AddRecord(context.Background(), record) + if err != nil { + return fmt.Errorf("todaynic: 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) + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("todaynic: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + err := d.client.DeleteRecord(context.Background(), recordID) + if err != nil { + return fmt.Errorf("todaynic: delete record: %w", err) + } + + 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/todaynic/todaynic.toml b/providers/dns/todaynic/todaynic.toml new file mode 100644 index 000000000..16d55ccc0 --- /dev/null +++ b/providers/dns/todaynic/todaynic.toml @@ -0,0 +1,25 @@ +Name = "TodayNIC/时代互联" +Description = '''''' +URL = "https://www.todaynic.com/" +Code = "todaynic" +Since = "v4.32.0" + +Example = ''' +TODAYNIC_AUTH_USER_ID="xxx" \ +TODAYNIC_API_KEY="yyy" \ +lego --dns todaynic -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + TODAYNIC_AUTH_USER_ID = "account ID" + TODAYNIC_API_KEY = "API key" + [Configuration.Additional] + TODAYNIC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + TODAYNIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + TODAYNIC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" + TODAYNIC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://www.todaynic.com/partner/mode_Http_Api_detail.php" + apipost = "https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=0" diff --git a/providers/dns/todaynic/todaynic_test.go b/providers/dns/todaynic/todaynic_test.go new file mode 100644 index 000000000..c73bf6cc5 --- /dev/null +++ b/providers/dns/todaynic/todaynic_test.go @@ -0,0 +1,207 @@ +package todaynic + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAuthUserID, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAuthUserID: "user123", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing user ID", + envVars: map[string]string{ + EnvAuthUserID: "", + EnvAPIKey: "secret", + }, + expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID", + }, + { + desc: "missing API key", + envVars: map[string]string{ + EnvAuthUserID: "user123", + EnvAPIKey: "", + }, + expected: "todaynic: some credentials information are missing: TODAYNIC_API_KEY", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID,TODAYNIC_API_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 + authUserID string + apiKey string + expected string + }{ + { + desc: "success", + authUserID: "user123", + apiKey: "secret", + }, + { + desc: "missing user ID", + apiKey: "secret", + expected: "todaynic: credentials missing", + }, + { + desc: "missing API key", + authUserID: "user123", + expected: "todaynic: credentials missing", + }, + { + desc: "missing credentials", + expected: "todaynic: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AuthUserID = test.authUserID + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.AuthUserID = "user123" + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /api/dns/add-domain-record.json", + servermock.ResponseFromInternal("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Domain", "example.com"). + With("Host", "_acme-challenge"). + With("Type", "TXT"). + With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("Ttl", "600"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /api/dns/delete-domain-record.json", + servermock.ResponseFromInternal("add_record.json"), + servermock.CheckQueryParameter().Strict(). + With("Id", "123"). + With("auth-userid", "user123"). + With("api-key", "secret"), + ). + Build(t) + + provider.recordIDs["abc"] = 123 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 7099a3f4c..ddf991209 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -163,6 +163,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/technitium" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/timewebcloud" + "github.com/go-acme/lego/v4/providers/dns/todaynic" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/ultradns" "github.com/go-acme/lego/v4/providers/dns/uniteddomains" @@ -506,6 +507,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return tencentcloud.NewDNSProvider() case "timewebcloud": return timewebcloud.NewDNSProvider() + case "todaynic": + return todaynic.NewDNSProvider() case "transip": return transip.NewDNSProvider() case "ultradns": From 527d51d4858a8b1dcad8b57e0f7d7d3f5dc7b72d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 12 Jan 2026 18:04:28 +0100 Subject: [PATCH 64/95] Add DNS provider for DNSExit (#2787) --- README.md | 72 ++++---- cmd/zz_gen_cmd_dnshelp.go | 21 +++ docs/content/dns/zz_gen_dnsexit.md | 67 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/dnsexit/dnsexit.go | 163 +++++++++++++++++ providers/dns/dnsexit/dnsexit.toml | 22 +++ providers/dns/dnsexit/dnsexit_test.go | 165 ++++++++++++++++++ providers/dns/dnsexit/internal/client.go | 156 +++++++++++++++++ providers/dns/dnsexit/internal/client_test.go | 111 ++++++++++++ .../internal/fixtures/add_record-request.json | 11 ++ .../fixtures/delete_record-request.json | 10 ++ .../dns/dnsexit/internal/fixtures/error.json | 4 + .../dnsexit/internal/fixtures/success.json | 7 + providers/dns/dnsexit/internal/types.go | 41 +++++ providers/dns/zz_gen_dns_providers.go | 3 + 15 files changed, 818 insertions(+), 37 deletions(-) create mode 100644 docs/content/dns/zz_gen_dnsexit.md create mode 100644 providers/dns/dnsexit/dnsexit.go create mode 100644 providers/dns/dnsexit/dnsexit.toml create mode 100644 providers/dns/dnsexit/dnsexit_test.go create mode 100644 providers/dns/dnsexit/internal/client.go create mode 100644 providers/dns/dnsexit/internal/client_test.go create mode 100644 providers/dns/dnsexit/internal/fixtures/add_record-request.json create mode 100644 providers/dns/dnsexit/internal/fixtures/delete_record-request.json create mode 100644 providers/dns/dnsexit/internal/fixtures/error.json create mode 100644 providers/dns/dnsexit/internal/fixtures/success.json create mode 100644 providers/dns/dnsexit/internal/types.go diff --git a/README.md b/README.md index 8ca213962..eff3bda32 100644 --- a/README.md +++ b/README.md @@ -114,182 +114,182 @@ If your DNS provider is not supported, please open an [issue](https://github.com Digital Ocean DirectAdmin DNS Made Easy - dnsHome.de + DNSExit + dnsHome.de DNSimple DNSPod (deprecated) Domain Offensive (do.de) - Domeneshop + Domeneshop DreamHost Duck DNS Dyn - DynDnsFree.de + DynDnsFree.de Dynu EasyDNS EdgeCenter - Efficient IP + Efficient IP Epik Exoscale External program - F5 XC + F5 XC freemyip.com G-Core Gandi - Gandi Live DNS (v5) + Gandi Live DNS (v5) Gigahost.no Glesys Go Daddy - Google Cloud + Google Cloud Google Domains Gravity Hetzner - Hosting.de + Hosting.de Hosting.nl Hostinger Hosttech - HTTP request + HTTP request http.net Huawei Cloud Hurricane Electric DNS - HyperOne + HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox - Infomaniak + Infomaniak Internet Initiative Japan Internet.bs INWX - Ionos + Ionos Ionos Cloud IPv64 ISPConfig 3 - ISPConfig 3 - Dynamic DNS (DDNS) Module + ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) JD Cloud Joker - Joohoi's ACME-DNS + Joohoi's ACME-DNS KeyHelp Liara Lima-City - Linode (v4) + Linode (v4) Liquid Web Loopia LuaDNS - Mail-in-a-Box + Mail-in-a-Box ManageEngine CloudDNS Manual Metaname - Metaregistrar + Metaregistrar mijn.host Mittwald myaddr.{tools,dev,io} - MyDNS.jp + MyDNS.jp MythicBeasts Name.com Namecheap - Namesilo + Namesilo NearlyFreeSpeech.NET Neodigit Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Syse + Syse Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TodayNIC/时代互联 TransIP UKFast SafeDNS - Ultradns + Ultradns United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS Virtualname - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index a2e0d4aaa..4c3bcb694 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -52,6 +52,7 @@ func allDNSCodes() string { "designate", "digitalocean", "directadmin", + "dnsexit", "dnshomede", "dnsimple", "dnsmadeeasy", @@ -1087,6 +1088,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/directadmin`) + case "dnsexit": + // generated from: providers/dns/dnsexit/dnsexit.toml + ew.writeln(`Configuration for DNSExit.`) + ew.writeln(`Code: 'dnsexit'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "DNSEXIT_API_KEY": API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "DNSEXIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "DNSEXIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "DNSEXIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "DNSEXIT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsexit`) + case "dnshomede": // generated from: providers/dns/dnshomede/dnshomede.toml ew.writeln(`Configuration for dnsHome.de.`) diff --git a/docs/content/dns/zz_gen_dnsexit.md b/docs/content/dns/zz_gen_dnsexit.md new file mode 100644 index 000000000..aca5357e8 --- /dev/null +++ b/docs/content/dns/zz_gen_dnsexit.md @@ -0,0 +1,67 @@ +--- +title: "DNSExit" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: dnsexit +dnsprovider: + since: "v4.32.0" + code: "dnsexit" + url: "https://dnsexit.com" +--- + + + + + + +Configuration for [DNSExit](https://dnsexit.com). + + + + +- Code: `dnsexit` +- Since: v4.32.0 + + +Here is an example bash command using the DNSExit provider: + +```bash +DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns dnsexit -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `DNSEXIT_API_KEY` | API key | + +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 | +|--------------------------------|-------------| +| `DNSEXIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `DNSEXIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `DNSEXIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `DNSEXIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://dnsexit.com/dns/dns-api/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 8de6475e1..c2f6ea4c1 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, 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, jdcloud, 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, todaynic, 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, dnsexit, 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, jdcloud, 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, todaynic, 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/dnsexit/dnsexit.go b/providers/dns/dnsexit/dnsexit.go new file mode 100644 index 000000000..ce9373a50 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit.go @@ -0,0 +1,163 @@ +// Package dnsexit implements a DNS provider for solving the DNS-01 challenge using DNSExit. +package dnsexit + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/dnsexit/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "DNSEXIT_" + + EnvAPIKey = envNamespace + "API_KEY" + + 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 { + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*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 { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for DNSExit. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("dnsexit: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for DNSExit. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("dnsexit: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey) + if err != nil { + return nil, fmt.Errorf("dnsexit: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, 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("dnsexit: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("dnsexit: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + TTL: toMinutes(d.config.TTL), + } + + err = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("dnsexit: add record: %w", err) + } + + 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("dnsexit: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("dnsexit: %w", err) + } + + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + } + + err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("dnsexit: add record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func toMinutes(seconds int) int { + i := seconds / 60 + if seconds%60 > 0 { + i++ + } + + return i +} diff --git a/providers/dns/dnsexit/dnsexit.toml b/providers/dns/dnsexit/dnsexit.toml new file mode 100644 index 000000000..0d5321835 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit.toml @@ -0,0 +1,22 @@ +Name = "DNSExit" +Description = '''''' +URL = "https://dnsexit.com" +Code = "dnsexit" +Since = "v4.32.0" + +Example = ''' +DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns dnsexit -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + DNSEXIT_API_KEY = "API key" + [Configuration.Additional] + DNSEXIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + DNSEXIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + DNSEXIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + DNSEXIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://dnsexit.com/dns/dns-api/" diff --git a/providers/dns/dnsexit/dnsexit_test.go b/providers/dns/dnsexit/dnsexit_test.go new file mode 100644 index 000000000..31fe61497 --- /dev/null +++ b/providers/dns/dnsexit/dnsexit_test.go @@ -0,0 +1,165 @@ +package dnsexit + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "key", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "dnsexit: some credentials information are missing: DNSEXIT_API_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 + apiKey string + expected string + }{ + { + desc: "success", + apiKey: "key", + }, + { + desc: "missing credentials", + expected: "dnsexit: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("apikey", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /", + servermock.ResponseFromInternal("success.json"), + servermock.CheckRequestJSONBodyFromInternal("add_record-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /", + servermock.ResponseFromInternal("success.json"), + servermock.CheckRequestJSONBodyFromInternal("delete_record-request.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/dnsexit/internal/client.go b/providers/dns/dnsexit/internal/client.go new file mode 100644 index 000000000..9b0164846 --- /dev/null +++ b/providers/dns/dnsexit/internal/client.go @@ -0,0 +1,156 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.dnsexit.com/dns/" + +// Client the DNSExit API client. +type Client struct { + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiKey string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// AddRecord adds a record. +// https://dnsexit.com/dns/dns-api/#example-add-spf +// https://dnsexit.com/dns/dns-api/#example-lse +func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { + payload := APIRequest{ + Domain: domain, + Add: []Record{record}, + } + + req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) + if err != nil { + return err + } + + err = c.do(req) + if err != nil { + return err + } + + return nil +} + +// DeleteRecord deletes a record. +// https://dnsexit.com/dns/dns-api/#delete-a-record +func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { + payload := APIRequest{ + Domain: domain, + Delete: []Record{record}, + } + + req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) + if err != nil { + return err + } + + err = c.do(req) + if err != nil { + return err + } + + return nil +} + +func (c *Client) do(req *http.Request) error { + useragent.SetHeader(req.Header) + + req.Header.Set("apikey", c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode > http.StatusBadRequest { + return parseError(req, resp) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + result := &APIResponse{} + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if result.Code != 0 { + return result + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIResponse + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/dnsexit/internal/client_test.go b/providers/dns/dnsexit/internal/client_test.go new file mode 100644 index 000000000..26ea01203 --- /dev/null +++ b/providers/dns/dnsexit/internal/client_test.go @@ -0,0 +1,111 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("apikey", "secret"), + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("success.json"), + servermock.CheckRequestJSONBodyFromFixture("add_record-request.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 2, + } + + err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusBadRequest), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 480, + Overwrite: true, + } + + err := client.AddRecord(context.Background(), "example.com", record) + require.Error(t, err) + + require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("success.json"), + servermock.CheckRequestJSONBodyFromFixture("delete_record-request.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + } + + err := client.DeleteRecord(context.Background(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusBadRequest), + ). + Build(t) + + record := Record{ + Type: "TXT", + Name: "foo", + Content: "txtTXTtxt", + } + + err := client.DeleteRecord(context.Background(), "example.com", record) + + require.Error(t, err) + + require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") +} diff --git a/providers/dns/dnsexit/internal/fixtures/add_record-request.json b/providers/dns/dnsexit/internal/fixtures/add_record-request.json new file mode 100644 index 000000000..6e5e2b520 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/add_record-request.json @@ -0,0 +1,11 @@ +{ + "domain": "example.com", + "add": [ + { + "type": "TXT", + "name": "_acme-challenge", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 2 + } + ] +} diff --git a/providers/dns/dnsexit/internal/fixtures/delete_record-request.json b/providers/dns/dnsexit/internal/fixtures/delete_record-request.json new file mode 100644 index 000000000..dcfef9cdf --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/delete_record-request.json @@ -0,0 +1,10 @@ +{ + "domain": "example.com", + "delete": [ + { + "type": "TXT", + "name": "_acme-challenge", + "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + } + ] +} diff --git a/providers/dns/dnsexit/internal/fixtures/error.json b/providers/dns/dnsexit/internal/fixtures/error.json new file mode 100644 index 000000000..9ba835895 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "code": 6, + "message": "JSON Defined Record Type not Supported" +} diff --git a/providers/dns/dnsexit/internal/fixtures/success.json b/providers/dns/dnsexit/internal/fixtures/success.json new file mode 100644 index 000000000..3af47a936 --- /dev/null +++ b/providers/dns/dnsexit/internal/fixtures/success.json @@ -0,0 +1,7 @@ +{ + "code": 0, + "details": [ + "UPDATE Record A example.com. TTL(hh:mm) 08:00 IP 1.1.1.10" + ], + "message": "Success" +} diff --git a/providers/dns/dnsexit/internal/types.go b/providers/dns/dnsexit/internal/types.go new file mode 100644 index 000000000..060dd883e --- /dev/null +++ b/providers/dns/dnsexit/internal/types.go @@ -0,0 +1,41 @@ +package internal + +import ( + "fmt" + "strings" +) + +type Record struct { + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` // NOTE: ttl value is in minutes. + Overwrite bool `json:"overwrite,omitempty"` +} + +type APIRequest struct { + Domain string `json:"domain,omitempty"` + Add []Record `json:"add,omitempty"` + Delete []Record `json:"delete,omitempty"` + Update []Record `json:"update,omitempty"` +} + +// https://dnsexit.com/dns/dns-api/#server-reply + +type APIResponse struct { + Code int `json:"code,omitempty"` + Details []string `json:"details,omitempty"` + Message string `json:"message,omitempty"` +} + +func (a APIResponse) Error() string { + var msg strings.Builder + + msg.WriteString(fmt.Sprintf("%s (code=%d)", a.Message, a.Code)) + + for _, detail := range a.Details { + msg.WriteString(fmt.Sprintf(", %s", detail)) + } + + return msg.String() +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index ddf991209..fbeef67d0 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -46,6 +46,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/designate" "github.com/go-acme/lego/v4/providers/dns/digitalocean" "github.com/go-acme/lego/v4/providers/dns/directadmin" + "github.com/go-acme/lego/v4/providers/dns/dnsexit" "github.com/go-acme/lego/v4/providers/dns/dnshomede" "github.com/go-acme/lego/v4/providers/dns/dnsimple" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy" @@ -273,6 +274,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return digitalocean.NewDNSProvider() case "directadmin": return directadmin.NewDNSProvider() + case "dnsexit": + return dnsexit.NewDNSProvider() case "dnshomede": return dnshomede.NewDNSProvider() case "dnsimple": From d063b15c0266d7dd6709597f67f5c41d7764adc3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 15 Jan 2026 01:04:30 +0100 Subject: [PATCH 65/95] azure: reinforces deprecation (#2792) --- providers/dns/azure/azure.go | 15 +++++++++++++++ providers/dns/azure/azure_test.go | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index fd00bcbe2..8bfc6cfe1 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "strings" "time" "github.com/Azure/go-autorest/autorest" @@ -37,6 +38,8 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) +const EnvLegoAzureBypassDeprecation = "LEGO_AZURE_BYPASS_DEPRECATION" + const defaultMetadataEndpoint = "http://169.254.169.254" var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -133,6 +136,18 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("azure: the configuration of the DNS provider is nil") } + if !env.GetOrDefaultBool(EnvLegoAzureBypassDeprecation, false) { + var msg strings.Builder + + msg.WriteString("azure: ") + msg.WriteString("The `azure` provider has been deprecated since 2023, and replaced by `azuredns` provider. ") + msg.WriteString("It can be TEMPORARILY reactivated by using the environment variable `LEGO_AZURE_BYPASS_DEPRECATION=true`. ") + msg.WriteString("The `azure` provider will be removed in a future release, please migrate to the `azuredns` provider. ") + msg.WriteString("The documentation of the `azuredns` provider can be found at https://go-acme.github.io/lego/dns/azuredns/") + + return nil, errors.New(msg.String()) + } + if config.HTTPClient == nil { config.HTTPClient = &http.Client{Timeout: 5 * time.Second} } diff --git a/providers/dns/azure/azure_test.go b/providers/dns/azure/azure_test.go index 44fb81eef..c4fec4359 100644 --- a/providers/dns/azure/azure_test.go +++ b/providers/dns/azure/azure_test.go @@ -14,6 +14,7 @@ import ( const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( + EnvLegoAzureBypassDeprecation, EnvEnvironment, EnvClientID, EnvClientSecret, @@ -57,6 +58,8 @@ func TestNewDNSProvider(t *testing.T) { envTest.ClearEnv() + test.envVars[EnvLegoAzureBypassDeprecation] = "true" + envTest.Apply(test.envVars) p, err := NewDNSProvider() @@ -140,6 +143,11 @@ func TestNewDNSProviderConfig(t *testing.T) { }, } + defer envTest.RestoreEnv() + + envTest.ClearEnv() + envTest.Apply(map[string]string{EnvLegoAzureBypassDeprecation: "true"}) + for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() From 4d41c52db80b5d0517475c2e50da7f3830ccc403 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 15 Jan 2026 23:16:02 +0100 Subject: [PATCH 66/95] Add DNS provider for DDNSS (#2795) --- README.md | 74 ++++---- cmd/zz_gen_cmd_dnshelp.go | 22 +++ docs/content/dns/zz_gen_ddnss.md | 68 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/ddnss/ddnss.go | 130 ++++++++++++++ providers/dns/ddnss/ddnss.toml | 23 +++ providers/dns/ddnss/ddnss_test.go | 168 ++++++++++++++++++ providers/dns/ddnss/internal/client.go | 137 ++++++++++++++ providers/dns/ddnss/internal/client_test.go | 56 ++++++ .../dns/ddnss/internal/fixtures/error.html | 12 ++ .../dns/ddnss/internal/fixtures/success.html | 8 + providers/dns/ddnss/internal/types.go | 39 ++++ providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 704 insertions(+), 38 deletions(-) create mode 100644 docs/content/dns/zz_gen_ddnss.md create mode 100644 providers/dns/ddnss/ddnss.go create mode 100644 providers/dns/ddnss/ddnss.toml create mode 100644 providers/dns/ddnss/ddnss_test.go create mode 100644 providers/dns/ddnss/internal/client.go create mode 100644 providers/dns/ddnss/internal/client_test.go create mode 100644 providers/dns/ddnss/internal/fixtures/error.html create mode 100644 providers/dns/ddnss/internal/fixtures/success.html create mode 100644 providers/dns/ddnss/internal/types.go diff --git a/README.md b/README.md index eff3bda32..557542ca0 100644 --- a/README.md +++ b/README.md @@ -107,189 +107,189 @@ If your DNS provider is not supported, please open an [issue](https://github.com Core-Networks CPanel/WHM + DDnss (DynDNS Service) Derak Cloud deSEC.io - Designate DNSaaS for Openstack + Designate DNSaaS for Openstack Digital Ocean DirectAdmin DNS Made Easy - DNSExit + DNSExit dnsHome.de DNSimple DNSPod (deprecated) - Domain Offensive (do.de) + Domain Offensive (do.de) Domeneshop DreamHost Duck DNS - Dyn + Dyn DynDnsFree.de Dynu EasyDNS - EdgeCenter + EdgeCenter Efficient IP Epik Exoscale - External program + External program F5 XC freemyip.com G-Core - Gandi + Gandi Gandi Live DNS (v5) Gigahost.no Glesys - Go Daddy + Go Daddy Google Cloud Google Domains Gravity - Hetzner + Hetzner Hosting.de Hosting.nl Hostinger - Hosttech + Hosttech HTTP request http.net Huawei Cloud - Hurricane Electric DNS + Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service - Infoblox + Infoblox Infomaniak Internet Initiative Japan Internet.bs - INWX + INWX Ionos Ionos Cloud IPv64 - ISPConfig 3 + ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) JD Cloud - Joker + Joker Joohoi's ACME-DNS KeyHelp Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Neodigit - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Syse Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联 TransIP - UKFast SafeDNS + UKFast SafeDNS Ultradns United-Domains Variomedia - VegaDNS + VegaDNS Vercel Versio.[nl|eu|uk] VinylDNS - Virtualname + Virtualname VK Cloud Volcano Engine/火山引擎 Vscale - Vultr + Vultr webnames.ca webnames.ru Websupport - WEDOS + WEDOS West.cn/西部数码 Yandex 360 Yandex Cloud - Yandex PDD + Yandex PDD Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 4c3bcb694..cf2da8563 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -47,6 +47,7 @@ func allDNSCodes() string { "constellix", "corenetworks", "cpanel", + "ddnss", "derak", "desec", "designate", @@ -973,6 +974,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`) + case "ddnss": + // generated from: providers/dns/ddnss/ddnss.toml + ew.writeln(`Configuration for DDnss (DynDNS Service).`) + ew.writeln(`Code: 'ddnss'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "DDNSS_KEY": Update key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "DDNSS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "DDNSS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "DDNSS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "DDNSS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) + ew.writeln(` - "DDNSS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/ddnss`) + case "derak": // generated from: providers/dns/derak/derak.toml ew.writeln(`Configuration for Derak Cloud.`) diff --git a/docs/content/dns/zz_gen_ddnss.md b/docs/content/dns/zz_gen_ddnss.md new file mode 100644 index 000000000..e159d58b4 --- /dev/null +++ b/docs/content/dns/zz_gen_ddnss.md @@ -0,0 +1,68 @@ +--- +title: "DDnss (DynDNS Service)" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ddnss +dnsprovider: + since: "v4.32.0" + code: "ddnss" + url: "https://ddnss.de/" +--- + + + + + + +Configuration for [DDnss (DynDNS Service)](https://ddnss.de/). + + + + +- Code: `ddnss` +- Since: v4.32.0 + + +Here is an example bash command using the DDnss (DynDNS Service) provider: + +```bash +DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns ddnss -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `DDNSS_KEY` | Update key | + +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 | +|--------------------------------|-------------| +| `DDNSS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `DDNSS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `DDNSS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `DDNSS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | +| `DDNSS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://ddnss.de/info.php) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index c2f6ea4c1..3d3043690 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, com35, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, 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, todaynic, 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, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, 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, todaynic, 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/ddnss/ddnss.go b/providers/dns/ddnss/ddnss.go new file mode 100644 index 000000000..381151c55 --- /dev/null +++ b/providers/dns/ddnss/ddnss.go @@ -0,0 +1,130 @@ +// Package ddnss implements a DNS provider for solving the DNS-01 challenge using DynDNS Service. +package ddnss + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/ddnss/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "DDNSS_" + + EnvKey = envNamespace + "KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Key string + + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for DynDNS Service. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvKey) + if err != nil { + return nil, fmt.Errorf("ddnss: %w", err) + } + + config := NewDefaultConfig() + config.Key = values[EnvKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for DynDNS Service. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ddnss: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(&internal.Authentication{Key: config.Key}) + if err != nil { + return nil, fmt.Errorf("ddnss: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + err := d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value) + if err != nil { + return fmt.Errorf("ddnss: add TXT record: %w", err) + } + + 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) + + err := d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("ddnss: remove TXT record: %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.config.PropagationTimeout, d.config.PollingInterval +} + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} diff --git a/providers/dns/ddnss/ddnss.toml b/providers/dns/ddnss/ddnss.toml new file mode 100644 index 000000000..0d0a7132c --- /dev/null +++ b/providers/dns/ddnss/ddnss.toml @@ -0,0 +1,23 @@ +Name = "DDnss (DynDNS Service)" +Description = '''''' +URL = "https://ddnss.de/" +Code = "ddnss" +Since = "v4.32.0" + +Example = ''' +DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns ddnss -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + DDNSS_KEY = "Update key" + [Configuration.Additional] + DDNSS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + DDNSS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + DDNSS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" + DDNSS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + DDNSS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://ddnss.de/info.php" diff --git a/providers/dns/ddnss/ddnss_test.go b/providers/dns/ddnss/ddnss_test.go new file mode 100644 index 000000000..5b1d7df58 --- /dev/null +++ b/providers/dns/ddnss/ddnss_test.go @@ -0,0 +1,168 @@ +package ddnss + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvKey: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "ddnss: some credentials information are missing: DDNSS_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 + Key string + expected string + }{ + { + desc: "success", + Key: "secret", + }, + { + desc: "missing credentials", + expected: "ddnss: missing credentials", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Key = test.Key + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Key = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL = server.URL + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /", + servermock.ResponseFromInternal("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("txtm", "1"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /", + servermock.ResponseFromInternal("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txtm", "2"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/ddnss/internal/client.go b/providers/dns/ddnss/internal/client.go new file mode 100644 index 000000000..a0cf4b4a6 --- /dev/null +++ b/providers/dns/ddnss/internal/client.go @@ -0,0 +1,137 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + "golang.org/x/net/html" +) + +const defaultBaseURL = "https://ddnss.de/upd.php" + +// Client the DDns API client. +type Client struct { + auth *Authentication + + BaseURL string + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(auth *Authentication) (*Client, error) { + if auth == nil { + return nil, errors.New("credentials missing") + } + + err := auth.validate() + if err != nil { + return nil, err + } + + return &Client{ + auth: auth, + BaseURL: defaultBaseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, host, value string) error { + return c.update(ctx, map[string]string{ + "host": host, + "txt": value, + "txtm": "1", + }) +} + +func (c *Client) RemoveTXTRecord(ctx context.Context, host string) error { + return c.update(ctx, map[string]string{ + "host": host, + "txtm": "2", + }) +} + +func (c *Client) update(ctx context.Context, params map[string]string) error { + endpoint, err := url.Parse(c.BaseURL) + if err != nil { + return err + } + + query := endpoint.Query() + + for k, v := range params { + query.Set(k, v) + } + + c.auth.set(query) + + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + content, err := readPage(raw) + if err != nil { + return err + } + + if strings.Contains(content, "Updated 1 hostname.") { + return nil + } + + return fmt.Errorf("unexpected response: %s", content) +} + +func readPage(raw []byte) (string, error) { + page, err := html.Parse(strings.NewReader(string(raw))) + if err != nil { + return "", err + } + + var b strings.Builder + extractText(page, &b) + + return strings.TrimSpace(b.String()), nil +} + +func extractText(n *html.Node, b *strings.Builder) { + if n.Type == html.TextNode { + text := strings.TrimSpace(n.Data) + if text != "" { + b.WriteString(text + " ") + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + extractText(c, b) + } +} diff --git a/providers/dns/ddnss/internal/client_test.go b/providers/dns/ddnss/internal/client_test.go new file mode 100644 index 000000000..3faddded0 --- /dev/null +++ b/providers/dns/ddnss/internal/client_test.go @@ -0,0 +1,56 @@ +package internal + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(&Authentication{Key: "secret"}) + if err != nil { + return nil, err + } + + client.BaseURL = server.URL + client.HTTPClient = server.Client() + + return client, nil + }, + ) +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /", + servermock.ResponseFromFixture("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("txtm", "1"), + ). + Build(t) + + err := client.AddTXTRecord(t.Context(), "_acme-challenge.example.com", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY") + require.NoError(t, err) +} + +func TestClient_RemoveTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("GET /", + servermock.ResponseFromFixture("success.html"), + servermock.CheckQueryParameter().Strict(). + With("host", "_acme-challenge.example.com"). + With("key", "secret"). + With("txtm", "2"), + ). + Build(t) + + err := client.RemoveTXTRecord(t.Context(), "_acme-challenge.example.com") + require.NoError(t, err) +} diff --git a/providers/dns/ddnss/internal/fixtures/error.html b/providers/dns/ddnss/internal/fixtures/error.html new file mode 100644 index 000000000..f0599ad9a --- /dev/null +++ b/providers/dns/ddnss/internal/fixtures/error.html @@ -0,0 +1,12 @@ + + + DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v + + +

+

Error Occurred While Processing Request :

+
+ - badysys : Der System Parameter ist ungültig.
+ - badauth : Die Authorisation ist fehlgeschlagen. Die Parameter username und/oder password sind falsch.
+ - notfqdn : Hostname fehlt oder ist falsch.
+ diff --git a/providers/dns/ddnss/internal/fixtures/success.html b/providers/dns/ddnss/internal/fixtures/success.html new file mode 100644 index 000000000..f51957334 --- /dev/null +++ b/providers/dns/ddnss/internal/fixtures/success.html @@ -0,0 +1,8 @@ + + + DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v + + +

+

Updated 1 hostname.

+ diff --git a/providers/dns/ddnss/internal/types.go b/providers/dns/ddnss/internal/types.go new file mode 100644 index 000000000..37d41e076 --- /dev/null +++ b/providers/dns/ddnss/internal/types.go @@ -0,0 +1,39 @@ +package internal + +import ( + "errors" + "net/url" +) + +type Authentication struct { + Username string `url:"user,omitempty"` + Password string `url:"pwd,omitempty"` + Key string `url:"key,omitempty"` +} + +func (a *Authentication) validate() error { + if a.Username == "" && a.Password == "" && a.Key == "" { + return errors.New("missing credentials") + } + + if a.Username != "" && a.Password != "" && a.Key != "" { + return errors.New("only one of username, password or key can be set") + } + + if (a.Username != "" && a.Password == "") || a.Username == "" && a.Password != "" { + return errors.New("username and password must be set together") + } + + return nil +} + +func (a *Authentication) set(query url.Values) { + if a.Key != "" { + query.Set("key", a.Key) + + return + } + + query.Set("user", a.Username) + query.Set("pwd", a.Password) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index fbeef67d0..b4b98e23f 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -41,6 +41,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/constellix" "github.com/go-acme/lego/v4/providers/dns/corenetworks" "github.com/go-acme/lego/v4/providers/dns/cpanel" + "github.com/go-acme/lego/v4/providers/dns/ddnss" "github.com/go-acme/lego/v4/providers/dns/derak" "github.com/go-acme/lego/v4/providers/dns/desec" "github.com/go-acme/lego/v4/providers/dns/designate" @@ -264,6 +265,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return corenetworks.NewDNSProvider() case "cpanel": return cpanel.NewDNSProvider() + case "ddnss": + return ddnss.NewDNSProvider() case "derak": return derak.NewDNSProvider() case "desec": From 05333f3c84a2b54a3d882bca7ac0b190a78ea052 Mon Sep 17 00:00:00 2001 From: Ameer Ghani Date: Fri, 16 Jan 2026 20:13:42 +0000 Subject: [PATCH 67/95] chore: improve warning message about backups (#2797) --- cmd/cmd_run.go | 6 +++--- docs/content/dns/zz_gen_manual.md | 12 ++++++------ providers/dns/manual/manual.toml | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 16814b4de..5924c4b66 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -104,9 +104,9 @@ Your account credentials have been saved in your configuration directory at "%s". You should make a secure backup of this folder now. This -configuration directory will also contain certificates and -private keys obtained from the ACME server so making regular -backups of this folder is ideal. +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. ` func run(ctx *cli.Context) error { diff --git a/docs/content/dns/zz_gen_manual.md b/docs/content/dns/zz_gen_manual.md index 056726c74..832ccaf58 100644 --- a/docs/content/dns/zz_gen_manual.md +++ b/docs/content/dns/zz_gen_manual.md @@ -54,13 +54,13 @@ If you accept the linked Terms of Service, hit `Enter`. [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! - Your account credentials have been saved in your Let's Encrypt - configuration directory at "./.lego/accounts". +Your account credentials have been saved in your +configuration directory at "./.lego/accounts". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal. +You should make a secure backup of this folder now. This +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 diff --git a/providers/dns/manual/manual.toml b/providers/dns/manual/manual.toml index aca67536d..fc47a8fae 100644 --- a/providers/dns/manual/manual.toml +++ b/providers/dns/manual/manual.toml @@ -31,13 +31,13 @@ If you accept the linked Terms of Service, hit `Enter`. [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! - Your account credentials have been saved in your Let's Encrypt - configuration directory at "./.lego/accounts". +Your account credentials have been saved in your +configuration directory at "./.lego/accounts". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal. +You should make a secure backup of this folder now. This +configuration directory will also contain private keys +generated by lego and certificates obtained from the ACME +server. Making regular backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 From de869c8a7ebce8beb0397b470eda7d04dc89dbe2 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 19 Jan 2026 17:31:56 +0100 Subject: [PATCH 68/95] Add DNS provider for Bluecat v2 (#2791) --- README.md | 85 ++-- cmd/zz_gen_cmd_dnshelp.go | 26 ++ docs/content/dns/zz_gen_bluecatv2.md | 76 ++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/bluecatv2/bluecatv2.go | 249 +++++++++++ providers/dns/bluecatv2/bluecatv2.toml | 33 ++ providers/dns/bluecatv2/bluecatv2_test.go | 414 ++++++++++++++++++ providers/dns/bluecatv2/internal/client.go | 221 ++++++++++ .../dns/bluecatv2/internal/client_test.go | 208 +++++++++ .../fixtures/deleteResourceRecord.json | 75 ++++ .../bluecatv2/internal/fixtures/error.json | 6 + .../internal/fixtures/getZoneDeployments.json | 46 ++ .../fixtures/postSession-request.json | 4 + .../internal/fixtures/postSession.json | 50 +++ .../fixtures/postZoneDeployment-request.json | 3 + .../internal/fixtures/postZoneDeployment.json | 40 ++ .../postZoneResourceRecord-request.json | 7 + .../fixtures/postZoneResourceRecord.json | 25 ++ .../bluecatv2/internal/fixtures/zones.json | 185 ++++++++ providers/dns/bluecatv2/internal/identity.go | 60 +++ .../dns/bluecatv2/internal/identity_test.go | 82 ++++ .../dns/bluecatv2/internal/predicates.go | 64 +++ .../dns/bluecatv2/internal/predicates_test.go | 78 ++++ providers/dns/bluecatv2/internal/types.go | 122 ++++++ providers/dns/zz_gen_dns_providers.go | 3 + 25 files changed, 2123 insertions(+), 41 deletions(-) create mode 100644 docs/content/dns/zz_gen_bluecatv2.md create mode 100644 providers/dns/bluecatv2/bluecatv2.go create mode 100644 providers/dns/bluecatv2/bluecatv2.toml create mode 100644 providers/dns/bluecatv2/bluecatv2_test.go create mode 100644 providers/dns/bluecatv2/internal/client.go create mode 100644 providers/dns/bluecatv2/internal/client_test.go create mode 100644 providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/error.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postSession-request.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postSession.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json create mode 100644 providers/dns/bluecatv2/internal/fixtures/zones.json create mode 100644 providers/dns/bluecatv2/internal/identity.go create mode 100644 providers/dns/bluecatv2/internal/identity_test.go create mode 100644 providers/dns/bluecatv2/internal/predicates.go create mode 100644 providers/dns/bluecatv2/internal/predicates_test.go create mode 100644 providers/dns/bluecatv2/internal/types.go diff --git a/README.md b/README.md index 557542ca0..6324ece67 100644 --- a/README.md +++ b/README.md @@ -88,208 +88,213 @@ If your DNS provider is not supported, please open an [issue](https://github.com Bindman Bluecat + Bluecat v2 BookMyName - Brandit (deprecated) + Brandit (deprecated) Bunny Checkdomain Civo - Cloud.ru + Cloud.ru CloudDNS Cloudflare ClouDNS - CloudXNS (Deprecated) + CloudXNS (Deprecated) ConoHa v2 ConoHa v3 Constellix - Core-Networks + Core-Networks CPanel/WHM DDnss (DynDNS Service) Derak Cloud - deSEC.io + deSEC.io Designate DNSaaS for Openstack Digital Ocean DirectAdmin - DNS Made Easy + DNS Made Easy DNSExit dnsHome.de DNSimple - DNSPod (deprecated) + DNSPod (deprecated) Domain Offensive (do.de) Domeneshop DreamHost - Duck DNS + Duck DNS Dyn DynDnsFree.de Dynu - EasyDNS + EasyDNS EdgeCenter Efficient IP Epik - Exoscale + Exoscale External program F5 XC freemyip.com - G-Core + G-Core Gandi Gandi Live DNS (v5) Gigahost.no - Glesys + Glesys Go Daddy Google Cloud Google Domains - Gravity + Gravity Hetzner Hosting.de Hosting.nl - Hostinger + Hostinger Hosttech HTTP request http.net - Huawei Cloud + Huawei Cloud Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) - IIJ DNS Platform Service + IIJ DNS Platform Service Infoblox Infomaniak Internet Initiative Japan - Internet.bs + Internet.bs INWX Ionos Ionos Cloud - IPv64 + IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) - JD Cloud + JD Cloud Joker Joohoi's ACME-DNS KeyHelp - Liara + Liara Lima-City Linode (v4) Liquid Web - Loopia + Loopia LuaDNS Mail-in-a-Box ManageEngine CloudDNS - Manual + Manual Metaname Metaregistrar mijn.host - Mittwald + Mittwald myaddr.{tools,dev,io} MyDNS.jp MythicBeasts - Name.com + Name.com Namecheap Namesilo NearlyFreeSpeech.NET - Neodigit + Neodigit Netcup Netlify Nicmanager - NIFCloud + NIFCloud Njalla Nodion NS1 - Octenium + Octenium Open Telekom Cloud Oracle Cloud OVH - plesk.com + plesk.com Porkbun PowerDNS Rackspace - Rain Yun/雨云 + Rain Yun/雨云 RcodeZero reg.ru Regfish - RFC2136 + RFC2136 RimuHosting RU CENTER Sakura Cloud - Scaleway + Scaleway Selectel Selectel v2 SelfHost.(de|eu) - Servercow + Servercow Shellrent Simply.com Sonic - Spaceship + Spaceship Stackpath Syse Technitium - Tencent Cloud DNS + Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联 - TransIP + TransIP UKFast SafeDNS Ultradns United-Domains - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS Virtualname VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index cf2da8563..600e49753 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -31,6 +31,7 @@ func allDNSCodes() string { "binarylane", "bindman", "bluecat", + "bluecatv2", "bookmyname", "brandit", "bunny", @@ -623,6 +624,31 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecat`) + case "bluecatv2": + // generated from: providers/dns/bluecatv2/bluecatv2.toml + ew.writeln(`Configuration for Bluecat v2.`) + ew.writeln(`Code: 'bluecatv2'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "BLUECATV2_CONFIG_NAME": Configuration name`) + ew.writeln(` - "BLUECATV2_PASSWORD": API password`) + ew.writeln(` - "BLUECATV2_USERNAME": API username`) + ew.writeln(` - "BLUECATV2_VIEW_NAME": DNS View Name`) + ew.writeln(` - "BLUECAT_SERVER_URL": The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "BLUECATV2_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "BLUECATV2_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "BLUECATV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "BLUECATV2_SKIP_DEPLOY": Skip quick deployements`) + ew.writeln(` - "BLUECATV2_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecatv2`) + case "bookmyname": // generated from: providers/dns/bookmyname/bookmyname.toml ew.writeln(`Configuration for BookMyName.`) diff --git a/docs/content/dns/zz_gen_bluecatv2.md b/docs/content/dns/zz_gen_bluecatv2.md new file mode 100644 index 000000000..7d748df99 --- /dev/null +++ b/docs/content/dns/zz_gen_bluecatv2.md @@ -0,0 +1,76 @@ +--- +title: "Bluecat v2" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: bluecatv2 +dnsprovider: + since: "v4.32.0" + code: "bluecatv2" + url: "https://www.bluecatnetworks.com" +--- + + + + + + +Configuration for [Bluecat v2](https://www.bluecatnetworks.com). + + + + +- Code: `bluecatv2` +- Since: v4.32.0 + + +Here is an example bash command using the Bluecat v2 provider: + +```bash +BLUECATV2_SERVER_URL="https://example.com" \ +BLUECATV2_USERNAME="xxx" \ +BLUECATV2_PASSWORD="yyy" \ +BLUECATV2_CONFIG_NAME="myConfiguration" \ +BLUECATV2_VIEW_NAME="myView" \ +lego --dns bluecatv2 -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `BLUECATV2_CONFIG_NAME` | Configuration name | +| `BLUECATV2_PASSWORD` | API password | +| `BLUECATV2_USERNAME` | API username | +| `BLUECATV2_VIEW_NAME` | DNS View Name | +| `BLUECAT_SERVER_URL` | The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve | + +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 | +|--------------------------------|-------------| +| `BLUECATV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `BLUECATV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `BLUECATV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `BLUECATV2_SKIP_DEPLOY` | Skip quick deployements | +| `BLUECATV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 3d3043690..e31633567 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, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, 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, todaynic, 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, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, 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, todaynic, 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/bluecatv2/bluecatv2.go b/providers/dns/bluecatv2/bluecatv2.go new file mode 100644 index 000000000..0efe99661 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2.go @@ -0,0 +1,249 @@ +// Package bluecatv2 implements a DNS provider for solving the DNS-01 challenge using Bluecat v2. +package bluecatv2 + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "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/bluecatv2/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "BLUECATV2_" + + EnvServerURL = envNamespace + "SERVER_URL" + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + EnvConfigName = envNamespace + "CONFIG_NAME" + EnvViewName = envNamespace + "VIEW_NAME" + EnvSkipDeploy = envNamespace + "SKIP_DEPLOY" + + 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 { + ServerURL string + Username string + Password string + ConfigName string + ViewName string + SkipDeploy bool + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + SkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false), + + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + zoneIDs map[string]int64 + recordIDs map[string]int64 + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Bluecat v2. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword, EnvConfigName, EnvViewName) + if err != nil { + return nil, fmt.Errorf("bluecatv2: %w", err) + } + + config := NewDefaultConfig() + config.ServerURL = values[EnvServerURL] + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.ConfigName = values[EnvConfigName] + config.ViewName = values[EnvViewName] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat v2. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("bluecatv2: the configuration of the DNS provider is nil") + } + + if config.ServerURL == "" { + return nil, errors.New("bluecatv2: missing server URL") + } + + if config.ConfigName == "" { + return nil, errors.New("bluecatv2: missing configuration name") + } + + if config.ViewName == "" { + return nil, errors.New("bluecatv2: missing view name") + } + + client, err := internal.NewClient(config.ServerURL, config.Username, config.Password) + if err != nil { + return nil, fmt.Errorf("bluecatv2: %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]int64), + zoneIDs: make(map[string]int64), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.AbsoluteName) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + record := internal.RecordTXT{ + CommonResource: internal.CommonResource{ + Type: "TXTRecord", + Name: subDomain, + }, + Text: info.Value, + TTL: d.config.TTL, + RecordType: "TXT", + } + + newRecord, err := d.client.CreateZoneResourceRecord(ctx, zone.ID, record) + if err != nil { + return fmt.Errorf("bluecatv2: create resource record: %w", err) + } + + d.recordIDsMu.Lock() + d.zoneIDs[token] = zone.ID + d.recordIDs[token] = newRecord.ID + d.recordIDsMu.Unlock() + + if d.config.SkipDeploy { + return nil + } + + _, err = d.client.CreateZoneDeployment(ctx, zone.ID) + if err != nil { + return fmt.Errorf("bluecat: deploy zone: %w", err) + } + + 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) + + d.recordIDsMu.Lock() + recordID, recordOK := d.recordIDs[token] + zoneID, zoneOK := d.zoneIDs[token] + d.recordIDsMu.Unlock() + + if !recordOK { + return fmt.Errorf("bluecatv2: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + if !zoneOK { + return fmt.Errorf("bluecatv2: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("bluecatv2: %w", err) + } + + err = d.client.DeleteResourceRecord(ctx, recordID) + if err != nil { + return fmt.Errorf("bluecatv2: delete resource record: %w", err) + } + + if d.config.SkipDeploy { + return nil + } + + _, err = d.client.CreateZoneDeployment(ctx, zoneID) + if err != nil { + return fmt.Errorf("bluecat: deploy zone: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.ZoneResource, error) { + for name := range dns01.UnFqdnDomainsSeq(fqdn) { + opts := &internal.CollectionOptions{ + Fields: "id,absoluteName,configuration.id,configuration.name,view.id,view.name", + Filter: internal.And( + internal.Eq("absoluteName", name), + internal.Eq("configuration.name", d.config.ConfigName), + internal.Eq("view.name", d.config.ViewName), + ).String(), + } + + zones, err := d.client.RetrieveZones(ctx, opts) + if err != nil { + // TODO(ldez) maybe add a log in v5. + continue + } + + for _, zone := range zones { + if zone.AbsoluteName == name { + return &zone, nil + } + } + } + + return nil, fmt.Errorf("no zone found for fqdn: %s", fqdn) +} diff --git a/providers/dns/bluecatv2/bluecatv2.toml b/providers/dns/bluecatv2/bluecatv2.toml new file mode 100644 index 000000000..6ec3781c6 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2.toml @@ -0,0 +1,33 @@ +Name = "Bluecat v2" +Description = '''''' +URL = "https://www.bluecatnetworks.com" +Code = "bluecatv2" +Since = "v4.32.0" + +Example = ''' +BLUECATV2_SERVER_URL="https://example.com" \ +BLUECATV2_USERNAME="xxx" \ +BLUECATV2_PASSWORD="yyy" \ +BLUECATV2_CONFIG_NAME="myConfiguration" \ +BLUECATV2_VIEW_NAME="myView" \ +lego --dns bluecatv2 -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + BLUECAT_SERVER_URL = "The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve" + BLUECATV2_USERNAME = "API username" + BLUECATV2_PASSWORD = "API password" + BLUECATV2_CONFIG_NAME = "Configuration name" + BLUECATV2_VIEW_NAME = "DNS View Name" + [Configuration.Additional] + BLUECATV2_SKIP_DEPLOY = "Skip quick deployements" + BLUECATV2_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + BLUECATV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + BLUECATV2_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + BLUECATV2_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0" + Swagger = "http://{Address_Manager_IP}/api/openapi.json" + SwaggerDump = "https://github.com/go-acme/lego/discussions/2218#discussioncomment-13060545" diff --git a/providers/dns/bluecatv2/bluecatv2_test.go b/providers/dns/bluecatv2/bluecatv2_test.go new file mode 100644 index 000000000..d852f0e18 --- /dev/null +++ b/providers/dns/bluecatv2/bluecatv2_test.go @@ -0,0 +1,414 @@ +package bluecatv2 + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvServerURL, + EnvUsername, + EnvPassword, + EnvConfigName, + EnvViewName, + EnvSkipDeploy, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvServerURL: "https://example.com/", + EnvUsername: "userA", + EnvPassword: "secret", + EnvConfigName: "myConfig", + EnvViewName: "myView", + }, + }, + { + desc: "missing server URL", + envVars: map[string]string{ + EnvServerURL: "", + EnvUsername: "userA", + EnvPassword: "secret", + EnvConfigName: "myConfig", + EnvViewName: "myView", + }, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvServerURL: "https://example.com/", + EnvUsername: "", + EnvPassword: "secret", + EnvConfigName: "myConfig", + EnvViewName: "myView", + }, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvServerURL: "https://example.com/", + EnvUsername: "userA", + EnvPassword: "", + EnvConfigName: "myConfig", + EnvViewName: "myView", + }, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_PASSWORD", + }, + { + desc: "missing configuration name", + envVars: map[string]string{ + EnvServerURL: "https://example.com/", + EnvUsername: "userA", + EnvPassword: "secret", + EnvConfigName: "", + EnvViewName: "myView", + }, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_CONFIG_NAME", + }, + { + desc: "missing view name", + envVars: map[string]string{ + EnvServerURL: "https://example.com/", + EnvUsername: "userA", + EnvPassword: "secret", + EnvConfigName: "myConfig", + EnvViewName: "", + }, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_VIEW_NAME", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL,BLUECATV2_USERNAME,BLUECATV2_PASSWORD,BLUECATV2_CONFIG_NAME,BLUECATV2_VIEW_NAME", + }, + } + + 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 + serverURL string + username string + password string + configName string + viewName string + expected string + }{ + { + desc: "success", + serverURL: "https://example.com/", + username: "userA", + password: "secret", + configName: "myConfig", + viewName: "myView", + }, + { + desc: "missing server URL", + username: "userA", + password: "secret", + configName: "myConfig", + viewName: "myView", + expected: "bluecatv2: missing server URL", + }, + { + desc: "missing username", + serverURL: "https://example.com/", + password: "secret", + configName: "myConfig", + viewName: "myView", + expected: "bluecatv2: credentials missing", + }, + { + desc: "missing password", + serverURL: "https://example.com/", + username: "userA", + configName: "myConfig", + viewName: "myView", + expected: "bluecatv2: credentials missing", + }, + { + desc: "missing configuration name", + serverURL: "https://example.com/", + username: "userA", + password: "secret", + viewName: "myView", + expected: "bluecatv2: missing configuration name", + }, + { + desc: "missing view name", + serverURL: "https://example.com/", + username: "userA", + password: "secret", + configName: "myConfig", + expected: "bluecatv2: missing view name", + }, + { + desc: "missing credentials", + expected: "bluecatv2: missing server URL", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ServerURL = test.serverURL + config.Username = test.username + config.Password = test.password + config.ConfigName = test.configName + config.ViewName = test.viewName + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + + config.ServerURL = server.URL + config.Username = "userA" + config.Password = "secret" + config.ConfigName = "myConfiguration" + config.ViewName = "myView" + + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromInternal("postSession.json"), + servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), + ). + Route("GET /api/v2/configurations", + servermock.ResponseFromInternal("configurations.json"), + servermock.CheckQueryParameter().Strict(). + With("filter", "name:eq('myConfiguration')"), + ). + Route("GET /api/v2/configurations/12345/views", + servermock.ResponseFromInternal("views.json"), + servermock.CheckQueryParameter().Strict(). + With("filter", "name:eq('myView')"), + ). + Route("GET /api/v2/zones", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + filter := req.URL.Query().Get("filter") + + if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) { + servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) + + return + } + + servermock.ResponseFromInternal("error.json"). + WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req) + }), + ). + Route("POST /api/v2/zones/12345/resourceRecords", + servermock.ResponseFromInternal("postZoneResourceRecord.json"), + servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"), + ). + Route("POST /api/v2/zones/12345/deployments", + servermock.ResponseFromInternal("postZoneDeployment.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_Present_skipDeploy(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(map[string]string{ + EnvSkipDeploy: "true", + }) + + provider := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromInternal("postSession.json"), + servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), + ). + Route("GET /api/v2/configurations", + servermock.ResponseFromInternal("configurations.json"), + servermock.CheckQueryParameter().Strict(). + With("filter", "name:eq('myConfiguration')"), + ). + Route("GET /api/v2/configurations/12345/views", + servermock.ResponseFromInternal("views.json"), + servermock.CheckQueryParameter().Strict(). + With("filter", "name:eq('myView')"), + ). + Route("GET /api/v2/zones", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + filter := req.URL.Query().Get("filter") + + if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) { + servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) + + return + } + + servermock.ResponseFromInternal("error.json"). + WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req) + }), + ). + Route("POST /api/v2/zones/12345/resourceRecords", + servermock.ResponseFromInternal("postZoneResourceRecord.json"), + servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"), + ). + Route("POST /api/v2/zones/456789/deployments", + servermock.Noop(). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromInternal("postSession.json"), + servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), + ). + Route("DELETE /api/v2/resourceRecords/12345", + servermock.ResponseFromInternal("deleteResourceRecord.json"), + ). + Route("POST /api/v2/zones/456789/deployments", + servermock.ResponseFromInternal("postZoneDeployment.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"), + ). + Build(t) + + provider.zoneIDs["abc"] = 456789 + provider.recordIDs["abc"] = 12345 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp_skipDeploy(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(map[string]string{ + EnvSkipDeploy: "true", + }) + + provider := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromInternal("postSession.json"), + servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), + ). + Route("DELETE /api/v2/resourceRecords/12345", + servermock.ResponseFromInternal("deleteResourceRecord.json"), + ). + Route("POST /api/v2/zones/456789/deployments", + servermock.Noop(). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + provider.zoneIDs["abc"] = 456789 + provider.recordIDs["abc"] = 12345 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/bluecatv2/internal/client.go b/providers/dns/bluecatv2/internal/client.go new file mode 100644 index 000000000..d3c801154 --- /dev/null +++ b/providers/dns/bluecatv2/internal/client.go @@ -0,0 +1,221 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +// Client the Bluecat v2 API client. +type Client struct { + username string + password string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(serverURL, username, password string) (*Client, error) { + if serverURL == "" { + return nil, errors.New("server URL missing") + } + + if username == "" || password == "" { + return nil, errors.New("credentials missing") + } + + baseURL, err := url.Parse(serverURL) + if err != nil { + return nil, err + } + + return &Client{ + username: username, + password: password, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// RetrieveZones retrieves all zones. +func (c *Client) RetrieveZones(ctx context.Context, opts *CollectionOptions) ([]ZoneResource, error) { + endpoint := c.baseURL.JoinPath("api", "v2", "zones") + + collection, err := retrieveCollection[ZoneResource](ctx, c, endpoint, opts) + if err != nil { + return nil, err + } + + return collection.Data, nil +} + +// RetrieveZoneDeployments retrieves all deployments for a zone. +func (c *Client) RetrieveZoneDeployments(ctx context.Context, zoneID int64, opts *CollectionOptions) ([]QuickDeployment, error) { + endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments") + + collection, err := retrieveCollection[QuickDeployment](ctx, c, endpoint, opts) + if err != nil { + return nil, err + } + + return collection.Data, nil +} + +// CreateZoneDeployment creates a new deployment for a zone. +func (c *Client) CreateZoneDeployment(ctx context.Context, zoneID int64) (*QuickDeployment, error) { + endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments") + + payload := CommonResource{ + Type: "QuickDeployment", + } + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) + if err != nil { + return nil, err + } + + result := new(QuickDeployment) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// CreateZoneResourceRecord creates a new TXT record in a zone. +func (c *Client) CreateZoneResourceRecord(ctx context.Context, zoneID int64, record RecordTXT) (*RecordTXT, error) { + endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "resourceRecords") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return nil, err + } + + result := new(RecordTXT) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteResourceRecord deletes a resource record. +func (c *Client) DeleteResourceRecord(ctx context.Context, recordID int64) error { + endpoint := c.baseURL.JoinPath("api", "v2", "resourceRecords", strconv.FormatInt(recordID, 10)) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.doAuthenticated(ctx, req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func retrieveCollection[T any](ctx context.Context, client *Client, endpoint *url.URL, opts *CollectionOptions) (*Collection[T], error) { + if opts != nil { + values, err := querystring.Values(opts) + if err != nil { + return nil, err + } + + endpoint.RawQuery = values.Encode() + } + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := &Collection[T]{} + + err = client.doAuthenticated(ctx, req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return &errAPI +} diff --git a/providers/dns/bluecatv2/internal/client_test.go b/providers/dns/bluecatv2/internal/client_test.go new file mode 100644 index 000000000..2559af66e --- /dev/null +++ b/providers/dns/bluecatv2/internal/client_test.go @@ -0,0 +1,208 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilderAuthenticated() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "userA", "secret") + if err != nil { + return nil, err + } + + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + servermock.CheckHeader(). + WithAuthorization("Basic secretToken"), + ) +} + +func TestClient_RetrieveZones(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("GET /api/v2/zones", + servermock.ResponseFromFixture("zones.json"), + servermock.CheckQueryParameter().Strict(). + With( + "filter", + "absoluteName:eq('example.com') and configuration.name:eq('myConfiguration') and view.name:eq('myView')", + ), + ). + Build(t) + + opts := &CollectionOptions{ + Filter: And( + Eq("absoluteName", "example.com"), + Eq("configuration.name", "myConfiguration"), + Eq("view.name", "myView"), + ).String(), + } + + result, err := client.RetrieveZones(mockToken(t.Context()), opts) + require.NoError(t, err) + + expected := []ZoneResource{ + { + CommonResource: CommonResource{ID: 12345, Type: "ENUMZone", Name: "5678"}, + AbsoluteName: "string", + }, + { + CommonResource: CommonResource{ID: 12345, Type: "ExternalHostsZone", Name: "name"}, + }, + { + CommonResource: CommonResource{ID: 12345, Type: "InternalRootZone", Name: "name"}, + }, + { + CommonResource: CommonResource{ID: 12345, Type: "ResponsePolicyZone", Name: "name"}, + }, + { + CommonResource: CommonResource{ID: 12345, Type: "Zone", Name: "example.com"}, + AbsoluteName: "example.com", + }, + } + + assert.Equal(t, expected, result) +} + +func TestClient_RetrieveZones_error(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("GET /api/v2/zones", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + opts := &CollectionOptions{ + Filter: And( + Eq("absoluteName", "example.com"), + Eq("configuration.name", "myConfiguration"), + Eq("view.name", "myView"), + ).String(), + } + + _, err := client.RetrieveZones(mockToken(t.Context()), opts) + require.EqualError(t, err, "401: Unauthorized: InvalidAuthorizationToken: The provided authorization token is invalid") +} + +func TestClient_RetrieveZoneDeployments(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("GET /api/v2/zones/456789/deployments", + servermock.ResponseFromFixture("getZoneDeployments.json"), + servermock.CheckQueryParameter().Strict(). + With("filter", "id:eq('12345')"), + ). + Build(t) + + opts := &CollectionOptions{ + Filter: Eq("id", "12345").String(), + } + + result, err := client.RetrieveZoneDeployments(mockToken(t.Context()), 456789, opts) + require.NoError(t, err) + + expected := []QuickDeployment{ + { + CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment", Name: ""}, + State: "PENDING", + Status: "CANCEL", + Message: "string", + PercentComplete: 50, + CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC), + StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC), + CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC), + Method: "SCHEDULED", + }, + } + + assert.Equal(t, expected, result) +} + +func TestClient_CreateZoneDeployment(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("POST /api/v2/zones/12345/deployments", + servermock.ResponseFromFixture("postZoneDeployment.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBodyFromFixture("postZoneDeployment-request.json"), + ). + Build(t) + + quickDeployment, err := client.CreateZoneDeployment(mockToken(t.Context()), 12345) + require.NoError(t, err) + + expected := &QuickDeployment{ + CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment"}, + State: "PENDING", + Status: "CANCEL", + Message: "string", + PercentComplete: 50, + CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC), + StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC), + CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC), + Method: "SCHEDULED", + } + + assert.Equal(t, expected, quickDeployment) +} + +func TestClient_CreateZoneResourceRecord(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("POST /api/v2/zones/12345/resourceRecords", + servermock.ResponseFromFixture("postZoneResourceRecord.json"), + servermock.CheckRequestJSONBodyFromFixture("postZoneResourceRecord-request.json"), + ). + Build(t) + + record := RecordTXT{ + CommonResource: CommonResource{ + Type: "TXTRecord", + Name: "_acme-challenge", + }, + Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + RecordType: "TXT", + } + + result, err := client.CreateZoneResourceRecord(mockToken(t.Context()), 12345, record) + require.NoError(t, err) + + expected := &RecordTXT{ + CommonResource: CommonResource{ + ID: 12345, + Type: "ResourceRecord", + Name: "name", + }, + TTL: 3600, + AbsoluteName: "host1.example.com", + Comment: "Sample comment.", + Dynamic: true, + RecordType: "CNAME", + Text: "", + } + + assert.Equal(t, expected, result) +} + +func TestClient_DeleteResourceRecord(t *testing.T) { + client := mockBuilderAuthenticated(). + Route("DELETE /api/v2/resourceRecords/12345", + servermock.ResponseFromFixture("deleteResourceRecord.json"), + ). + Build(t) + + err := client.DeleteResourceRecord(mockToken(t.Context()), 12345) + require.NoError(t, err) +} diff --git a/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json new file mode 100644 index 000000000..38ae2db6e --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json @@ -0,0 +1,75 @@ +{ + "id": 12345, + "type": "WorkflowRequest", + "state": "APPROVED", + "operation": "ADD_ALIAS_RECORD", + "creator": { + "id": 103307, + "type": "User", + "name": "admin", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator" + }, + "email": "user@example.com", + "phoneNumber": "555-1234", + "securityPrivilege": "NO_ACCESS", + "historyPrivilege": "HIDE", + "accessType": "GUI", + "passwordResetRequired": true, + "accountLocked": true, + "x509Required": true, + "administrativeAccessRights": [ + { + "resourceType": "Event", + "accessLevel": "HIDE" + } + ] + }, + "resourceId": 0, + "resourceType": "ACL", + "fieldUpdates": [ + { + "name": "string", + "value": {}, + "previousValue": {} + } + ], + "dependentRequest": "string", + "modifier": { + "id": 103307, + "type": "User", + "name": "admin", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator" + }, + "email": "user@example.com", + "phoneNumber": "555-1234", + "securityPrivilege": "NO_ACCESS", + "historyPrivilege": "HIDE", + "accessType": "GUI", + "passwordResetRequired": true, + "accountLocked": true, + "x509Required": true, + "administrativeAccessRights": [ + { + "resourceType": "Event", + "accessLevel": "HIDE" + } + ] + }, + "creationDateTime": "2022-10-17T19:11:45Z", + "modificationDateTime": "2022-10-18T19:11:45Z", + "comment": "Sample comment." +} diff --git a/providers/dns/bluecatv2/internal/fixtures/error.json b/providers/dns/bluecatv2/internal/fixtures/error.json new file mode 100644 index 000000000..d3d2b8b5f --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/error.json @@ -0,0 +1,6 @@ +{ + "status": 401, + "reason": "Unauthorized", + "code": "InvalidAuthorizationToken", + "message": "The provided authorization token is invalid" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json b/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json new file mode 100644 index 000000000..b1a4938ad --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json @@ -0,0 +1,46 @@ +{ + "count": 0, + "totalCount": 0, + "data": [ + { + "id": 12345, + "type": "QuickDeployment", + "state": "PENDING", + "status": "CANCEL", + "message": "string", + "percentComplete": 50, + "creationDateTime": "2022-11-23T02:53:00Z", + "startDateTime": "2022-11-23T02:53:03Z", + "completionDateTime": "2022-11-23T02:54:05Z", + "user": { + "id": 103307, + "type": "User", + "name": "admin", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator" + }, + "email": "user@example.com", + "phoneNumber": "555-1234", + "securityPrivilege": "NO_ACCESS", + "historyPrivilege": "HIDE", + "accessType": "GUI", + "passwordResetRequired": true, + "accountLocked": true, + "x509Required": true, + "administrativeAccessRights": [ + { + "resourceType": "Event", + "accessLevel": "HIDE" + } + ] + }, + "method": "SCHEDULED" + } + ] +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession-request.json b/providers/dns/bluecatv2/internal/fixtures/postSession-request.json new file mode 100644 index 000000000..e62048eb9 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postSession-request.json @@ -0,0 +1,4 @@ +{ + "username": "userA", + "password": "secret" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession.json b/providers/dns/bluecatv2/internal/fixtures/postSession.json new file mode 100644 index 000000000..4599ad0ad --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postSession.json @@ -0,0 +1,50 @@ +{ + "id": 12345, + "type": "UserSession", + "apiToken": "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez", + "apiTokenExpirationDateTime": "2022-09-15T17:52:07Z", + "basicAuthenticationCredentials": "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", + "remoteAddress": "192.168.1.1", + "readOnly": true, + "loginDateTime": "2022-09-14T17:45:03Z", + "logoutDateTime": "2022-09-14T19:45:03Z", + "state": "LOGGED_IN", + "response": "Authentication Error: Ensure that your username and password are correct.", + "user": { + "id": 103307, + "type": "User", + "name": "admin", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator" + }, + "email": "user@example.com", + "phoneNumber": "555-1234", + "securityPrivilege": "NO_ACCESS", + "historyPrivilege": "HIDE", + "accessType": "GUI", + "passwordResetRequired": true, + "accountLocked": true, + "x509Required": true, + "administrativeAccessRights": [ + { + "resourceType": "Event", + "accessLevel": "HIDE" + } + ] + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + } + } +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json new file mode 100644 index 000000000..099573a84 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json @@ -0,0 +1,3 @@ +{ + "type": "QuickDeployment" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json new file mode 100644 index 000000000..fd26781fb --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json @@ -0,0 +1,40 @@ +{ + "id": 12345, + "type": "QuickDeployment", + "state": "PENDING", + "status": "CANCEL", + "message": "string", + "percentComplete": 50, + "creationDateTime": "2022-11-23T02:53:00Z", + "startDateTime": "2022-11-23T02:53:03Z", + "completionDateTime": "2022-11-23T02:54:05Z", + "user": { + "id": 103307, + "type": "User", + "name": "admin", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "authenticator": { + "id": 12345, + "type": "Authenticator", + "name": "LDAP authenticator" + }, + "email": "user@example.com", + "phoneNumber": "555-1234", + "securityPrivilege": "NO_ACCESS", + "historyPrivilege": "HIDE", + "accessType": "GUI", + "passwordResetRequired": true, + "accountLocked": true, + "x509Required": true, + "administrativeAccessRights": [ + { + "resourceType": "Event", + "accessLevel": "HIDE" + } + ] + }, + "method": "SCHEDULED" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json new file mode 100644 index 000000000..2de733c71 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json @@ -0,0 +1,7 @@ +{ + "type": "TXTRecord", + "name": "_acme-challenge", + "ttl": 120, + "recordType": "TXT", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" +} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json new file mode 100644 index 000000000..78d028ee3 --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json @@ -0,0 +1,25 @@ +{ + "id": 12345, + "type": "ResourceRecord", + "name": "name", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "ttl": 3600, + "absoluteName": "host1.example.com", + "comment": "Sample comment.", + "dynamic": true, + "recordType": "CNAME", + "linkedRecord": { + "id": 12345, + "type": "ResourceRecord", + "name": "name", + "absoluteName": "host1.example.com" + } +} diff --git a/providers/dns/bluecatv2/internal/fixtures/zones.json b/providers/dns/bluecatv2/internal/fixtures/zones.json new file mode 100644 index 000000000..b9f2dfa8f --- /dev/null +++ b/providers/dns/bluecatv2/internal/fixtures/zones.json @@ -0,0 +1,185 @@ +{ + "count": 0, + "totalCount": 0, + "data": [ + { + "id": 12345, + "type": "ENUMZone", + "name": "5678", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "view": { + "id": 12345, + "type": "View", + "name": "default", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "deviceRegistrationEnabled": true, + "deviceRegistrationPortalAddress": "10.10.10.10" + }, + "deploymentEnabled": true, + "absoluteName": "string" + }, + { + "id": 12345, + "type": "ExternalHostsZone", + "name": "name", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "view": { + "id": 12345, + "type": "View", + "name": "default", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "deviceRegistrationEnabled": true, + "deviceRegistrationPortalAddress": "10.10.10.10" + } + }, + { + "id": 12345, + "type": "InternalRootZone", + "name": "name", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "view": { + "id": 12345, + "type": "View", + "name": "default", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "deviceRegistrationEnabled": true, + "deviceRegistrationPortalAddress": "10.10.10.10" + }, + "deploymentEnabled": true + }, + { + "id": 12345, + "type": "ResponsePolicyZone", + "name": "name", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "view": { + "id": 12345, + "type": "View", + "name": "default", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "deviceRegistrationEnabled": true, + "deviceRegistrationPortalAddress": "10.10.10.10" + }, + "responsePolicyZoneType": "LOCAL", + "responsePolicy": { + "id": 12345, + "type": "ResponsePolicy", + "name": "Block Response Policy" + }, + "overridePolicyType": "ALLOWLIST", + "overrideRefreshTime": "string", + "redirectTarget": "string", + "feedCategories": [ + "string" + ] + }, + { + "id": 12345, + "type": "Zone", + "name": "example.com", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "view": { + "id": 12345, + "type": "View", + "name": "default", + "userDefinedFields": { + "udf1": "value1", + "udf2": "value2" + }, + "configuration": { + "id": 12345, + "type": "Configuration", + "name": "name" + }, + "deviceRegistrationEnabled": true, + "deviceRegistrationPortalAddress": "10.10.10.10" + }, + "deploymentEnabled": true, + "dynamicUpdateEnabled": true, + "template": { + "id": 12345, + "type": "ZoneTemplate", + "name": "name" + }, + "signed": true, + "signingPolicy": { + "id": 12345, + "type": "DNSSECSigningPolicy", + "name": "name" + }, + "absoluteName": "example.com" + } + ] +} diff --git a/providers/dns/bluecatv2/internal/identity.go b/providers/dns/bluecatv2/internal/identity.go new file mode 100644 index 000000000..af9355ab2 --- /dev/null +++ b/providers/dns/bluecatv2/internal/identity.go @@ -0,0 +1,60 @@ +package internal + +import ( + "context" + "fmt" + "net/http" +) + +type token string + +const tokenKey token = "token" + +const authorizationHeader = "Authorization" + +// CreateSession creates a new session. +func (c *Client) CreateSession(ctx context.Context, info LoginInfo) (*Session, error) { + endpoint := c.baseURL.JoinPath("api", "v2", "sessions") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, info) + if err != nil { + return nil, err + } + + result := new(Session) + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// CreateAuthenticatedContext creates a new authenticated context. +func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { + tok, err := c.CreateSession(ctx, LoginInfo{Username: c.username, Password: c.password}) + if err != nil { + return nil, fmt.Errorf("create session: %w", err) + } + + return context.WithValue(ctx, tokenKey, tok.BasicAuthenticationCredentials), nil +} + +func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result any) error { + tok := getToken(ctx) + if tok != "" { + req.Header.Set(authorizationHeader, "Basic "+tok) + } + + return c.do(req, result) +} + +func getToken(ctx context.Context) string { + tok, ok := ctx.Value(tokenKey).(string) + if !ok { + return "" + } + + return tok +} diff --git a/providers/dns/bluecatv2/internal/identity_test.go b/providers/dns/bluecatv2/internal/identity_test.go new file mode 100644 index 000000000..3a1c4d2a2 --- /dev/null +++ b/providers/dns/bluecatv2/internal/identity_test.go @@ -0,0 +1,82 @@ +package internal + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "userA", "secret") + if err != nil { + return nil, err + } + + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func mockToken(ctx context.Context) context.Context { + return context.WithValue(ctx, tokenKey, "secretToken") +} + +func TestClient_CreateSession(t *testing.T) { + client := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromFixture("postSession.json"), + servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"), + ). + Build(t) + + info := LoginInfo{ + Username: "userA", + Password: "secret", + } + + result, err := client.CreateSession(mockToken(t.Context()), info) + require.NoError(t, err) + + expected := &Session{ + ID: 12345, + Type: "UserSession", + APIToken: "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez", + APITokenExpirationDateTime: time.Date(2022, time.September, 15, 17, 52, 7, 0, time.UTC), + BasicAuthenticationCredentials: "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", + RemoteAddress: "192.168.1.1", + ReadOnly: true, + LoginDateTime: time.Date(2022, time.September, 14, 17, 45, 3, 0, time.UTC), + LogoutDateTime: time.Date(2022, time.September, 14, 19, 45, 3, 0, time.UTC), + State: "LOGGED_IN", + Response: "Authentication Error: Ensure that your username and password are correct.", + } + + assert.Equal(t, expected, result) +} + +func TestClient_CreateAuthenticatedContext(t *testing.T) { + client := mockBuilder(). + Route("POST /api/v2/sessions", + servermock.ResponseFromFixture("postSession.json"), + servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"), + ). + Build(t) + + ctx, err := client.CreateAuthenticatedContext(t.Context()) + require.NoError(t, err) + + assert.Equal(t, "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", getToken(ctx)) +} diff --git a/providers/dns/bluecatv2/internal/predicates.go b/providers/dns/bluecatv2/internal/predicates.go new file mode 100644 index 000000000..8ed6f714b --- /dev/null +++ b/providers/dns/bluecatv2/internal/predicates.go @@ -0,0 +1,64 @@ +package internal + +import ( + "fmt" + "strings" +) + +type Predicate struct { + field string + operator string + values []string +} + +func (p *Predicate) String() string { + var values []string + for _, v := range p.values { + values = append(values, fmt.Sprintf("'%s'", v)) + } + + return fmt.Sprintf("%s:%s(%s)", p.field, p.operator, strings.Join(values, ", ")) +} + +func Eq(field, value string) *Predicate { + return &Predicate{field: field, operator: "eq", values: []string{value}} +} + +func Contains(field, value string) *Predicate { + return &Predicate{field: field, operator: "contains", values: []string{value}} +} + +func StartsWith(field, value string) *Predicate { + return &Predicate{field: field, operator: "startsWith", values: []string{value}} +} + +func EndsWith(field, value string) *Predicate { + return &Predicate{field: field, operator: "endsWith", values: []string{value}} +} + +func In(field string, values ...string) *Predicate { + return &Predicate{field: field, operator: "in", values: values} +} + +type Combined struct { + predicates []*Predicate + operator string +} + +func (o *Combined) String() string { + var parts []string + + for _, predicate := range o.predicates { + parts = append(parts, predicate.String()) + } + + return strings.Join(parts, " "+o.operator+" ") +} + +func And(predicates ...*Predicate) *Combined { + return &Combined{predicates: predicates, operator: "and"} +} + +func Or(predicates ...*Predicate) *Combined { + return &Combined{predicates: predicates, operator: "or"} +} diff --git a/providers/dns/bluecatv2/internal/predicates_test.go b/providers/dns/bluecatv2/internal/predicates_test.go new file mode 100644 index 000000000..6913e8729 --- /dev/null +++ b/providers/dns/bluecatv2/internal/predicates_test.go @@ -0,0 +1,78 @@ +package internal + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPredicate(t *testing.T) { + testCases := []struct { + desc string + predicate fmt.Stringer + expected string + }{ + { + desc: "Equals", + predicate: Eq("foo", "bar"), + expected: "foo:eq('bar')", + }, + { + desc: "Contains", + predicate: Contains("foo", "bar"), + expected: "foo:contains('bar')", + }, + { + desc: "Starts with", + predicate: StartsWith("foo", "bar"), + expected: "foo:startsWith('bar')", + }, + { + desc: "Ends with", + predicate: EndsWith("foo", "bar"), + expected: "foo:endsWith('bar')", + }, + { + desc: "Match a list of values", + predicate: In("foo", "bar", "bir"), + expected: "foo:in('bar', 'bir')", + }, + { + desc: "Combined: and", + predicate: And(Eq("foo", "bar"), Eq("fii", "bir")), + expected: "foo:eq('bar') and fii:eq('bir')", + }, + { + desc: "Combined: multiple and", + predicate: And( + Eq("foo", "bar"), + Eq("fii", "bir"), + Eq("fuu", "bur"), + ), + expected: "foo:eq('bar') and fii:eq('bir') and fuu:eq('bur')", + }, + { + desc: "Combined: or", + predicate: Or(Eq("foo", "bar"), Eq("foo", "bir")), + expected: "foo:eq('bar') or foo:eq('bir')", + }, + { + desc: "Combined: multiple or", + predicate: Or( + Eq("foo", "bar"), + Eq("foo", "bir"), + Eq("foo", "bur"), + ), + expected: "foo:eq('bar') or foo:eq('bir') or foo:eq('bur')", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, test.predicate.String()) + }) + } +} diff --git a/providers/dns/bluecatv2/internal/types.go b/providers/dns/bluecatv2/internal/types.go new file mode 100644 index 000000000..562fd60b0 --- /dev/null +++ b/providers/dns/bluecatv2/internal/types.go @@ -0,0 +1,122 @@ +package internal + +import ( + "fmt" + "time" +) + +// Quick deployment states. +// +//nolint:misspell // US vs UK +const ( + QDStatePending = "PENDING" + QDStateQueued = "QUEUED" + QDStateRunning = "RUNNING" + QDStateCancelled = "CANCELLED" + QDStateCancelling = "CANCELLING" + QDStateCompleted = "COMPLETED" + QDStateCompletedWithErrors = "COMPLETED_WITH_ERRORS" + QDStateCompletedWithWarnings = "COMPLETED_WITH_WARNINGS" + QDStateFailed = "FAILED" + QDStateUnknown = "UNKNOWN" +) + +// APIError represents an error. +// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Errors/9.6.0 +type APIError struct { + Status int `json:"status"` + Reason string `json:"reason"` + Code string `json:"code"` + Message string `json:"message"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("%d: %s: %s: %s", a.Status, a.Reason, a.Code, a.Message) +} + +// CommonResource represents the common resource fields. +// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Resources/9.6.0 +type CommonResource struct { + ID int64 `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` +} + +// Collection represents a collection of resources. +// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Collections/9.6.0 +type Collection[T any] struct { + Count int64 `json:"count"` + TotalCount int64 `json:"totalCount"` + Data []T `json:"data"` +} + +type CollectionOptions struct { + // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Fields/9.6.0 + Fields string `url:"fields,omitempty"` + + // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Pagination/9.6.0 + Limit int `url:"limit,omitempty"` + Offset int `url:"offset,omitempty"` + + // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Filter/9.6.0 + Filter string `url:"filter,omitempty"` + + // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Ordering/9.6.0 + OrderBy string `url:"orderBy,omitempty"` + + // Should return or not the total number of resources matching the query. + Total bool `url:"total,omitempty"` +} + +type RecordTXT struct { + CommonResource + + TTL int `json:"ttl,omitempty"` + AbsoluteName string `json:"absoluteName,omitempty"` + Comment string `json:"comment,omitempty"` + Dynamic bool `json:"dynamic,omitempty"` + RecordType string `json:"recordType,omitempty"` + Text string `json:"text,omitempty"` +} + +type ZoneResource struct { + CommonResource + + AbsoluteName string `json:"absoluteName,omitempty"` +} + +type QuickDeployment struct { + CommonResource + + State string `json:"state,omitempty"` + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + PercentComplete int `json:"percentComplete,omitempty"` + CreationDateTime time.Time `json:"creationDateTime,omitzero"` + StartDateTime time.Time `json:"startDateTime,omitzero"` + CompletionDateTime time.Time `json:"completionDateTime,omitzero"` + Method string `json:"method,omitempty"` +} + +// LoginInfo represents the login information. +// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0 +type LoginInfo struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// Session represents the session. +// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0 +type Session struct { + ID int `json:"id"` + Type string `json:"type"` + APIToken string `json:"apiToken"` + APITokenExpirationDateTime time.Time `json:"apiTokenExpirationDateTime"` + BasicAuthenticationCredentials string `json:"basicAuthenticationCredentials"` + RemoteAddress string `json:"remoteAddress"` + ReadOnly bool `json:"readOnly"` + LoginDateTime time.Time `json:"loginDateTime"` + LogoutDateTime time.Time `json:"logoutDateTime"` + State string `json:"state"` + Response string `json:"response"` +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index b4b98e23f..ae41f6a20 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -25,6 +25,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/binarylane" "github.com/go-acme/lego/v4/providers/dns/bindman" "github.com/go-acme/lego/v4/providers/dns/bluecat" + "github.com/go-acme/lego/v4/providers/dns/bluecatv2" "github.com/go-acme/lego/v4/providers/dns/bookmyname" "github.com/go-acme/lego/v4/providers/dns/brandit" "github.com/go-acme/lego/v4/providers/dns/bunny" @@ -233,6 +234,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return bindman.NewDNSProvider() case "bluecat": return bluecat.NewDNSProvider() + case "bluecatv2": + return bluecatv2.NewDNSProvider() case "bookmyname": return bookmyname.NewDNSProvider() case "brandit": From 16894fb99e3aa60fe0a5f9edcbea7a5fb9d32f34 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 20 Jan 2026 17:59:42 +0100 Subject: [PATCH 69/95] allinkl: detect zone through API (#2721) --- providers/dns/allinkl/allinkl.go | 24 +++- providers/dns/allinkl/allinkl_test.go | 114 ++++++++++++++++++ providers/dns/allinkl/internal/client.go | 87 ++++++++----- providers/dns/allinkl/internal/client_test.go | 27 ++++- .../internal/fixtures/auth-request.xml | 7 ++ .../internal/fixtures/flood_protection.xml | 11 ++ .../get_dns_settings-zone_not_found.xml | 11 ++ ...get_dns_settings-zone_syntax_incorrect.xml | 11 ++ providers/dns/allinkl/internal/identity.go | 22 ++-- .../dns/allinkl/internal/identity_test.go | 12 +- providers/dns/allinkl/internal/types.go | 5 +- providers/dns/allinkl/internal/types_api.go | 12 +- 12 files changed, 277 insertions(+), 66 deletions(-) create mode 100644 providers/dns/allinkl/internal/fixtures/auth-request.xml create mode 100644 providers/dns/allinkl/internal/fixtures/flood_protection.xml create mode 100644 providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml create mode 100644 providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 4a0aadd2b..0ccce7226 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -11,6 +11,7 @@ import ( "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" @@ -121,11 +122,6 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { 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("allinkl: could not find zone for domain %q: %w", domain, err) - } - ctx := context.Background() credential, err := d.identifier.Authentication(ctx, 60, true) @@ -135,6 +131,24 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx = internal.WithContext(ctx, credential) + var authZone string + + for z := range dns01.DomainsSeq(info.EffectiveFQDN) { + _, errG := d.client.GetDNSSettings(ctx, z, "") + if errG != nil { + log.Infof("allinkl: get DNS settings zone[%q] %v", z, errG) + continue + } + + authZone = z + + break + } + + if authZone == "" { + return fmt.Errorf("allinkl: unable to find auth zone for '%s'", info.EffectiveFQDN) + } + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("allinkl: %w", err) diff --git a/providers/dns/allinkl/allinkl_test.go b/providers/dns/allinkl/allinkl_test.go index b42adce5d..7da47aee4 100644 --- a/providers/dns/allinkl/allinkl_test.go +++ b/providers/dns/allinkl/allinkl_test.go @@ -1,9 +1,18 @@ package allinkl import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" "github.com/stretchr/testify/require" ) @@ -143,3 +152,108 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Login = "user" + config.Password = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + p.identifier.BaseURL, _ = url.Parse(server.URL) + + return p, err + }, + ).Route("POST /KasAuth.php", + servermock.ResponseFromInternal("auth.xml"), + servermock.CheckRequestBodyFromInternal("auth-request.xml"). + IgnoreWhitespace(), + ) +} + +func extractKasRequest(reader io.Reader) (*internal.KasRequest, error) { + type ReqEnvelope struct { + XMLName xml.Name `xml:"Envelope"` + Body struct { + KasAPI struct { + Params string `xml:"Params"` + } `xml:"KasApi"` + } `xml:"Body"` + } + + raw, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + reqEnvelope := ReqEnvelope{} + + err = xml.Unmarshal(raw, &reqEnvelope) + if err != nil { + return nil, err + } + + var kReq internal.KasRequest + + err = json.Unmarshal([]byte(reqEnvelope.Body.KasAPI.Params), &kReq) + if err != nil { + return nil, err + } + + return &kReq, nil +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /KasApi.php", + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + kReq, err := extractKasRequest(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + switch kReq.Action { + case "get_dns_settings": + params := kReq.RequestParams.(map[string]any) + + if params["zone_host"] == "_acme-challenge.example.com." { + servermock.ResponseFromInternal("get_dns_settings_not_found.xml").ServeHTTP(rw, req) + } else { + servermock.ResponseFromInternal("get_dns_settings.xml").ServeHTTP(rw, req) + } + + case "add_dns_settings": + servermock.ResponseFromInternal("add_dns_settings.xml").ServeHTTP(rw, req) + + default: + http.Error(rw, fmt.Sprintf("unknown action: %v", kReq.Action), http.StatusBadRequest) + } + }), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("POST /KasApi.php", + servermock.ResponseFromInternal("delete_dns_settings.xml"), + servermock.CheckRequestBodyFromInternal("delete_dns_settings-request.xml"). + IgnoreWhitespace()). + Build(t) + + provider.recordIDs["abc"] = "57347450" + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/allinkl/internal/client.go b/providers/dns/allinkl/internal/client.go index d747e9b36..d4403cac5 100644 --- a/providers/dns/allinkl/internal/client.go +++ b/providers/dns/allinkl/internal/client.go @@ -6,16 +6,21 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strconv" "strings" "sync" "time" + "github.com/cenkalti/backoff/v5" + "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" "github.com/go-viper/mapstructure/v2" ) -const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php" +const defaultBaseURL = "https://kasapi.kasserver.com/soap/" + +const apiPath = "KasApi.php" type Authentication interface { Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error) @@ -28,16 +33,21 @@ type Client struct { floodTime time.Time muFloodTime sync.Mutex - baseURL string + maxElapsedTime time.Duration + + BaseURL *url.URL HTTPClient *http.Client } // NewClient creates a new Client. func NewClient(login string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + return &Client{ - login: login, - baseURL: apiEndpoint, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, + login: login, + BaseURL: baseURL, + maxElapsedTime: 3 * time.Minute, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -51,14 +61,9 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R requestParams["record_id"] = recordID } - req, err := c.newRequest(ctx, "get_dns_settings", requestParams) - if err != nil { - return nil, err - } + var g APIResponse[GetDNSSettingsResponse] - var g GetDNSSettingsAPIResponse - - err = c.do(req, &g) + err := c.doRequest(ctx, "get_dns_settings", requestParams, &g) if err != nil { return nil, err } @@ -70,14 +75,9 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R // AddDNSSettings Creation of a DNS resource record. func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) { - req, err := c.newRequest(ctx, "add_dns_settings", record) - if err != nil { - return "", err - } + var g APIResponse[AddDNSSettingsResponse] - var g AddDNSSettingsAPIResponse - - err = c.do(req, &g) + err := c.doRequest(ctx, "add_dns_settings", record, &g) if err != nil { return "", err } @@ -91,14 +91,9 @@ func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) { requestParams := map[string]string{"record_id": recordID} - req, err := c.newRequest(ctx, "delete_dns_settings", requestParams) - if err != nil { - return "", err - } + var g APIResponse[DeleteDNSSettingsResponse] - var g DeleteDNSSettingsAPIResponse - - err = c.do(req, &g) + err := c.doRequest(ctx, "delete_dns_settings", requestParams, &g) if err != nil { return "", err } @@ -124,7 +119,9 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body))) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload)) + endpoint := c.BaseURL.JoinPath(apiPath) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("unable to create request: %w", err) } @@ -132,6 +129,21 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an return req, nil } +func (c *Client) doRequest(ctx context.Context, action string, requestParams, result any) error { + return wait.Retry(ctx, + func() error { + req, err := c.newRequest(ctx, action, requestParams) + if err != nil { + return backoff.Permanent(err) + } + + return c.do(req, result) + }, + backoff.WithBackOff(&backoff.ZeroBackOff{}), + backoff.WithMaxElapsedTime(c.maxElapsedTime), + ) +} + func (c *Client) do(req *http.Request, result any) error { c.muFloodTime.Lock() time.Sleep(time.Until(c.floodTime)) @@ -139,29 +151,40 @@ func (c *Client) do(req *http.Request, result any) error { resp, err := c.HTTPClient.Do(req) if err != nil { - return errutils.NewHTTPDoError(req, err) + return backoff.Permanent(errutils.NewHTTPDoError(req, err)) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) + return backoff.Permanent(errutils.NewUnexpectedResponseStatusCodeError(req, resp)) } envlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body) if err != nil { - return err + return backoff.Permanent(err) } if envlp.Body.Fault != nil { - return envlp.Body.Fault + if envlp.Body.Fault.Message == "flood_protection" { + ft, errP := strconv.ParseFloat(envlp.Body.Fault.Detail, 64) + if errP != nil { + return fmt.Errorf("parse flood protection delay: %w", envlp.Body.Fault) + } + + c.updateFloodTime(ft) + + return envlp.Body.Fault + } + + return backoff.Permanent(envlp.Body.Fault) } raw := getValue(envlp.Body.KasAPIResponse.Return) err = mapstructure.Decode(raw, result) if err != nil { - return fmt.Errorf("response struct decode: %w", err) + return backoff.Permanent(fmt.Errorf("response struct decode: %w", err)) } return nil diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go index 4b111e31c..949f45bf9 100644 --- a/providers/dns/allinkl/internal/client_test.go +++ b/providers/dns/allinkl/internal/client_test.go @@ -2,7 +2,9 @@ package internal import ( "net/http/httptest" + "net/url" "testing" + "time" "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" @@ -11,15 +13,17 @@ import ( func setupClient(server *httptest.Server) (*Client, error) { client := NewClient("user") - client.baseURL = server.URL + client.BaseURL, _ = url.Parse(server.URL) client.HTTPClient = server.Client() + client.maxElapsedTime = 1 * time.Second + return client, nil } func TestClient_GetDNSSettings(t *testing.T) { client := servermock.NewBuilder[*Client](setupClient). - Route("POST /", servermock.ResponseFromFixture("get_dns_settings.xml"), + Route("POST /KasApi.php", servermock.ResponseFromFixture("get_dns_settings.xml"), servermock.CheckRequestBodyFromFixture("get_dns_settings-request.xml"). IgnoreWhitespace()). Build(t) @@ -96,9 +100,24 @@ func TestClient_GetDNSSettings(t *testing.T) { assert.Equal(t, expected, records) } +func TestClient_GetDNSSettings_error_flood_protection(t *testing.T) { + client := servermock.NewBuilder[*Client](setupClient). + Route("POST /KasApi.php", + servermock.ResponseFromFixture("flood_protection.xml"), + ). + Build(t) + + assert.Zero(t, client.floodTime) + + _, err := client.GetDNSSettings(mockContext(t), "example.com", "") + require.EqualError(t, err, "KasApi: SOAP-ENV:Server: flood_protection: 0.0688529014587") + + assert.NotZero(t, client.floodTime) +} + func TestClient_AddDNSSettings(t *testing.T) { client := servermock.NewBuilder[*Client](setupClient). - Route("POST /", servermock.ResponseFromFixture("add_dns_settings.xml"), + Route("POST /KasApi.php", servermock.ResponseFromFixture("add_dns_settings.xml"), servermock.CheckRequestBodyFromFixture("add_dns_settings-request.xml"). IgnoreWhitespace()). Build(t) @@ -118,7 +137,7 @@ func TestClient_AddDNSSettings(t *testing.T) { func TestClient_DeleteDNSSettings(t *testing.T) { client := servermock.NewBuilder[*Client](setupClient). - Route("POST /", servermock.ResponseFromFixture("delete_dns_settings.xml"), + Route("POST /KasApi.php", servermock.ResponseFromFixture("delete_dns_settings.xml"), servermock.CheckRequestBodyFromFixture("delete_dns_settings-request.xml"). IgnoreWhitespace()). Build(t) diff --git a/providers/dns/allinkl/internal/fixtures/auth-request.xml b/providers/dns/allinkl/internal/fixtures/auth-request.xml new file mode 100644 index 000000000..1cba86f10 --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/auth-request.xml @@ -0,0 +1,7 @@ + + + + {"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"} + + + diff --git a/providers/dns/allinkl/internal/fixtures/flood_protection.xml b/providers/dns/allinkl/internal/fixtures/flood_protection.xml new file mode 100644 index 000000000..b8da10fab --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/flood_protection.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + flood_protection + KasApi + 0.0688529014587 + + + diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml new file mode 100644 index 000000000..478d07a3a --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + zone_not_found + KasApi + example.com + + + diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml new file mode 100644 index 000000000..c77d733db --- /dev/null +++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml @@ -0,0 +1,11 @@ + + + + + SOAP-ENV:Server + zone_syntax_incorrect + KasApi + _acme-challenge.example.com + + + diff --git a/providers/dns/allinkl/internal/identity.go b/providers/dns/allinkl/internal/identity.go index ba8d4d90e..e95e78899 100644 --- a/providers/dns/allinkl/internal/identity.go +++ b/providers/dns/allinkl/internal/identity.go @@ -6,14 +6,14 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strings" "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -// authEndpoint represents the Identity API endpoint to call. -const authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php" +const authPath = "KasAuth.php" type token string @@ -24,17 +24,19 @@ type Identifier struct { login string password string - authEndpoint string - HTTPClient *http.Client + BaseURL *url.URL + HTTPClient *http.Client } // NewIdentifier creates a new Identifier. func NewIdentifier(login, password string) *Identifier { + baseURL, _ := url.Parse(defaultBaseURL) + return &Identifier{ - login: login, - password: password, - authEndpoint: authEndpoint, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, + login: login, + password: password, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -62,7 +64,9 @@ func (c *Identifier) Authentication(ctx context.Context, sessionLifetime int, se payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body))) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.authEndpoint, bytes.NewReader(payload)) + endpoint := c.BaseURL.JoinPath(authPath) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload)) if err != nil { return "", fmt.Errorf("unable to create request: %w", err) } diff --git a/providers/dns/allinkl/internal/identity_test.go b/providers/dns/allinkl/internal/identity_test.go index 7b93b7688..41d092b13 100644 --- a/providers/dns/allinkl/internal/identity_test.go +++ b/providers/dns/allinkl/internal/identity_test.go @@ -3,6 +3,7 @@ package internal import ( "context" "net/http/httptest" + "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester/servermock" @@ -12,7 +13,7 @@ import ( func setupIdentifierClient(server *httptest.Server) (*Identifier, error) { client := NewIdentifier("user", "secret") - client.authEndpoint = server.URL + client.BaseURL, _ = url.Parse(server.URL) client.HTTPClient = server.Client() return client, nil @@ -26,10 +27,13 @@ func mockContext(t *testing.T) context.Context { func TestIdentifier_Authentication(t *testing.T) { client := servermock.NewBuilder[*Identifier](setupIdentifierClient). - Route("POST /", servermock.ResponseFromFixture("auth.xml")). + Route("POST /KasAuth.php", + servermock.ResponseFromFixture("auth.xml"), + servermock.CheckRequestBodyFromFixture("auth-request.xml"). + IgnoreWhitespace()). Build(t) - credentialToken, err := client.Authentication(t.Context(), 60, false) + credentialToken, err := client.Authentication(t.Context(), 60, true) require.NoError(t, err) assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken) @@ -37,7 +41,7 @@ func TestIdentifier_Authentication(t *testing.T) { func TestIdentifier_Authentication_error(t *testing.T) { client := servermock.NewBuilder[*Identifier](setupIdentifierClient). - Route("POST /", servermock.ResponseFromFixture("auth_fault.xml")). + Route("POST /KasAuth.php", servermock.ResponseFromFixture("auth_fault.xml")). Build(t) _, err := client.Authentication(t.Context(), 60, false) diff --git a/providers/dns/allinkl/internal/types.go b/providers/dns/allinkl/internal/types.go index b0aa9b4ff..51f7065b5 100644 --- a/providers/dns/allinkl/internal/types.go +++ b/providers/dns/allinkl/internal/types.go @@ -26,10 +26,11 @@ type Fault struct { Code string `xml:"faultcode"` Message string `xml:"faultstring"` Actor string `xml:"faultactor"` + Detail string `xml:"detail"` } -func (f Fault) Error() string { - return fmt.Sprintf("%s: %s: %s", f.Actor, f.Code, f.Message) +func (f *Fault) Error() string { + return fmt.Sprintf("%s: %s: %s: %s", f.Actor, f.Code, f.Message, f.Detail) } // KasResponse a KAS SOAP response. diff --git a/providers/dns/allinkl/internal/types_api.go b/providers/dns/allinkl/internal/types_api.go index 22f2c32ed..a11f3aac0 100644 --- a/providers/dns/allinkl/internal/types_api.go +++ b/providers/dns/allinkl/internal/types_api.go @@ -53,8 +53,8 @@ type DNSRequest struct { // --- -type GetDNSSettingsAPIResponse struct { - Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` +type APIResponse[T any] struct { + Response T `json:"Response" mapstructure:"Response"` } type GetDNSSettingsResponse struct { @@ -73,20 +73,12 @@ type ReturnInfo struct { Aux int `json:"record_aux,omitempty" mapstructure:"record_aux"` } -type AddDNSSettingsAPIResponse struct { - Response AddDNSSettingsResponse `json:"Response" mapstructure:"Response"` -} - type AddDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"` ReturnInfo string `json:"ReturnInfo" mapstructure:"ReturnInfo"` ReturnString string `json:"ReturnString" mapstructure:"ReturnString"` } -type DeleteDNSSettingsAPIResponse struct { - Response DeleteDNSSettingsResponse `json:"Response"` -} - type DeleteDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay"` ReturnString string `json:"ReturnString"` From 44b89b7e929c78575d28a9c35be0427ed06b8628 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 22 Jan 2026 05:10:35 +0100 Subject: [PATCH 70/95] allinkl: factorize findZone --- providers/dns/allinkl/allinkl.go | 33 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 0ccce7226..376b0903c 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -131,22 +131,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx = internal.WithContext(ctx, credential) - var authZone string - - for z := range dns01.DomainsSeq(info.EffectiveFQDN) { - _, errG := d.client.GetDNSSettings(ctx, z, "") - if errG != nil { - log.Infof("allinkl: get DNS settings zone[%q] %v", z, errG) - continue - } - - authZone = z - - break - } - - if authZone == "" { - return fmt.Errorf("allinkl: unable to find auth zone for '%s'", info.EffectiveFQDN) + authZone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("allinkl: %w", err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) @@ -206,3 +193,17 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { + for z := range dns01.DomainsSeq(fqdn) { + _, errG := d.client.GetDNSSettings(ctx, z, "") + if errG != nil { + log.Infof("get DNS settings zone[%q] %v", z, errG) + continue + } + + return z, nil + } + + return "", fmt.Errorf("unable to find auth zone for '%s'", fqdn) +} From 2ce04a6586ea27253975e10d3ab7d7bb6214c79d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 25 Jan 2026 14:51:12 +0100 Subject: [PATCH 71/95] alidns: add line record option (#2814) --- cmd/zz_gen_cmd_dnshelp.go | 2 ++ docs/content/dns/zz_gen_alidns.md | 2 ++ providers/dns/alidns/alidns.go | 13 +++++++++++-- providers/dns/alidns/alidns.toml | 2 ++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 600e49753..357834a3c 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -263,8 +263,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "ALICLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "ALICLOUD_LINE": Line (Default: default)`) ew.writeln(` - "ALICLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "ALICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ALICLOUD_REGION_ID": Region ID (Default: cn-hangzhou)`) ew.writeln(` - "ALICLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) ew.writeln() diff --git a/docs/content/dns/zz_gen_alidns.md b/docs/content/dns/zz_gen_alidns.md index e498a31dd..4ded782ab 100644 --- a/docs/content/dns/zz_gen_alidns.md +++ b/docs/content/dns/zz_gen_alidns.md @@ -58,8 +58,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `ALICLOUD_LINE` | Line (Default: default) | | `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | | `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) | | `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. diff --git a/providers/dns/alidns/alidns.go b/providers/dns/alidns/alidns.go index a5c883fcb..cdd8e75e0 100644 --- a/providers/dns/alidns/alidns.go +++ b/providers/dns/alidns/alidns.go @@ -27,6 +27,7 @@ const ( EnvSecretKey = envNamespace + "SECRET_KEY" EnvSecurityToken = envNamespace + "SECURITY_TOKEN" EnvRegionID = envNamespace + "REGION_ID" + EnvLine = envNamespace + "LINE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -45,6 +46,7 @@ type Config struct { SecretKey string SecurityToken string RegionID string + Line string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -74,6 +76,7 @@ type DNSProvider struct { func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.RegionID = env.GetOrFile(EnvRegionID) + config.Line = env.GetOrFile(EnvLine) values, err := env.Get(EnvRAMRole) if err == nil { @@ -254,12 +257,18 @@ func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) (*alidns.AddDomainR return nil, err } - return new(alidns.AddDomainRecordRequest). + adrr := new(alidns.AddDomainRecordRequest). SetType("TXT"). SetDomainName(zone). SetRR(rr). SetValue(value). - SetTTL(int64(d.config.TTL)), nil + SetTTL(int64(d.config.TTL)) + + if d.config.Line != "" { + adrr.SetLine(d.config.Line) + } + + return adrr, nil } func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord, error) { diff --git a/providers/dns/alidns/alidns.toml b/providers/dns/alidns/alidns.toml index 9a93bd24f..b78e1859d 100644 --- a/providers/dns/alidns/alidns.toml +++ b/providers/dns/alidns/alidns.toml @@ -23,6 +23,8 @@ lego --dns alidns - -d '*.example.com' -d example.com run ALICLOUD_SECRET_KEY = "Access Key secret" ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)" [Configuration.Additional] + ALICLOUD_REGION_ID = "Region ID (Default: cn-hangzhou)" + ALICLOUD_LINE = "Line (Default: default)" ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" From a7145a29ac5efc83c670248641ae25ff824876b3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 28 Jan 2026 18:41:23 +0100 Subject: [PATCH 72/95] fix: use IPs to define the main domain (#2817) --- certcrypto/crypto.go | 14 +++++++++----- cmd/cmd_list.go | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/certcrypto/crypto.go b/certcrypto/crypto.go index 00f0654b9..800bb3f5b 100644 --- a/certcrypto/crypto.go +++ b/certcrypto/crypto.go @@ -242,15 +242,15 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) { } func GetCertificateMainDomain(cert *x509.Certificate) (string, error) { - return getMainDomain(cert.Subject, cert.DNSNames) + return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses) } func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) { - return getMainDomain(cert.Subject, cert.DNSNames) + return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses) } -func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) { - if subject.CommonName == "" && len(dnsNames) == 0 { +func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) { + if subject.CommonName == "" && len(dnsNames) == 0 && len(ips) == 0 { return "", errors.New("missing domain") } @@ -258,7 +258,11 @@ func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) { return subject.CommonName, nil } - return dnsNames[0], nil + if len(dnsNames) > 0 { + return dnsNames[0], nil + } + + return ips[0].String(), nil } func ExtractDomains(cert *x509.Certificate) []string { diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index 483592d47..53cd12c3c 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "net" "net/url" "os" "path/filepath" @@ -100,6 +101,11 @@ func listCertificates(ctx *cli.Context) error { } else { fmt.Println(" Certificate Name:", name) fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", ")) + + if len(pCert.IPAddresses) > 0 { + fmt.Println(" IPs:", formatIPAddresses(pCert.IPAddresses)) + } + fmt.Println(" Expiry Date:", pCert.NotAfter) fmt.Println(" Certificate Path:", filename) fmt.Println() @@ -150,3 +156,12 @@ func listAccount(ctx *cli.Context) error { return nil } + +func formatIPAddresses(ipAddresses []net.IP) string { + var ips []string + for _, ip := range ipAddresses { + ips = append(ips, ip.String()) + } + + return strings.Join(ips, ", ") +} From fac5c39f5f9d36798a270af2d71578334001c2cf Mon Sep 17 00:00:00 2001 From: Mortie Torabi Date: Fri, 30 Jan 2026 19:36:46 +0000 Subject: [PATCH 73/95] fix: implement parsing for Retry-After header according to RFC 7231 (#2830) Co-authored-by: Fernandez Ludovic --- acme/api/service.go | 29 ++++++++++++++++++++++ acme/api/service_test.go | 37 ++++++++++++++++++++++++++++ certificate/renewal.go | 5 ++-- certificate/renewal_test.go | 36 +++++++++++++++++++++++++++ challenge/resolver/solver_manager.go | 17 ++++++------- 5 files changed, 112 insertions(+), 12 deletions(-) diff --git a/acme/api/service.go b/acme/api/service.go index 65518e1d9..22ce05124 100644 --- a/acme/api/service.go +++ b/acme/api/service.go @@ -1,8 +1,11 @@ package api import ( + "fmt" "net/http" "regexp" + "strconv" + "time" ) type service struct { @@ -56,3 +59,29 @@ func getRetryAfter(resp *http.Response) string { return resp.Header.Get("Retry-After") } + +// ParseRetryAfter parses the Retry-After header value according to RFC 7231. +// The header can be either delay-seconds (numeric) or HTTP-date (RFC 1123 format). +// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3 +// Returns the duration until the retry time. +// TODO(ldez): unexposed this function in v5. +func ParseRetryAfter(value string) (time.Duration, error) { + if value == "" { + return 0, nil + } + + if seconds, err := strconv.ParseInt(value, 10, 64); err == nil { + return time.Duration(seconds) * time.Second, nil + } + + if retryTime, err := time.Parse(time.RFC1123, value); err == nil { + duration := time.Until(retryTime) + if duration < 0 { + return 0, nil + } + + return duration, nil + } + + return 0, fmt.Errorf("invalid Retry-After value: %q", value) +} diff --git a/acme/api/service_test.go b/acme/api/service_test.go index 2dbd795c9..57ea45708 100644 --- a/acme/api/service_test.go +++ b/acme/api/service_test.go @@ -3,8 +3,10 @@ package api import ( "net/http" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_getLink(t *testing.T) { @@ -53,3 +55,38 @@ func Test_getLink(t *testing.T) { }) } } + +func TestParseRetryAfter(t *testing.T) { + testCases := []struct { + desc string + value string + expected time.Duration + }{ + { + desc: "empty header value", + value: "", + expected: time.Duration(0), + }, + { + desc: "delay-seconds", + value: "123", + expected: 123 * time.Second, + }, + { + desc: "HTTP-date", + value: time.Now().Add(3 * time.Second).Format(time.RFC1123), + expected: 3 * time.Second, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rt, err := ParseRetryAfter(test.value) + require.NoError(t, err) + + assert.InDelta(t, test.expected.Seconds(), rt.Seconds(), 1) + }) + } +} diff --git a/certificate/renewal.go b/certificate/renewal.go index 15e804745..59d31cfb5 100644 --- a/certificate/renewal.go +++ b/certificate/renewal.go @@ -11,6 +11,7 @@ import ( "time" "github.com/go-acme/lego/v4/acme" + "github.com/go-acme/lego/v4/acme/api" ) // RenewalInfoRequest contains the necessary renewal information. @@ -92,9 +93,9 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse } if retry := resp.Header.Get("Retry-After"); retry != "" { - info.RetryAfter, err = time.ParseDuration(retry + "s") + info.RetryAfter, err = api.ParseRetryAfter(retry) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse Retry-After header: %w", err) } } diff --git a/certificate/renewal_test.go b/certificate/renewal_test.go index 6ce43e0aa..23209638a 100644 --- a/certificate/renewal_test.go +++ b/certificate/renewal_test.go @@ -74,6 +74,42 @@ func TestCertifier_GetRenewalInfo(t *testing.T) { assert.Equal(t, time.Duration(21600000000000), ri.RetryAfter) } +func TestCertifier_GetRenewalInfo_retryAfter(t *testing.T) { + leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM)) + require.NoError(t, err) + + server := tester.MockACMEServer(). + Route("GET /renewalInfo/"+ariLeafCertID, + servermock.RawStringResponse(`{ + "suggestedWindow": { + "start": "2020-03-17T17:51:09Z", + "end": "2020-03-17T18:21:09Z" + }, + "explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/" + } + }`). + WithHeader("Content-Type", "application/json"). + WithHeader("Retry-After", time.Now().UTC().Add(6*time.Hour).Format(time.RFC1123))). + BuildHTTPS(t) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + require.NoError(t, err) + + certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) + + ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf}) + require.NoError(t, err) + require.NotNil(t, ri) + assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339)) + assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339)) + assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL) + + assert.InDelta(t, 6, ri.RetryAfter.Hours(), 0.001) +} + func TestCertifier_GetRenewalInfo_errors(t *testing.T) { leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM)) require.NoError(t, err) diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go index 48d9194b9..87cf6e2d8 100644 --- a/challenge/resolver/solver_manager.go +++ b/challenge/resolver/solver_manager.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "sort" - "strconv" "time" "github.com/cenkalti/backoff/v5" @@ -94,22 +93,20 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { return nil } - ra, err := strconv.Atoi(chlng.RetryAfter) - if err != nil { + retryAfter, err := api.ParseRetryAfter(chlng.RetryAfter) + if err != nil || retryAfter == 0 { // The ACME server MUST return a Retry-After. - // If it doesn't, we'll just poll hard. + // If it doesn't, or if it's invalid, we'll just poll hard. // Boulder does not implement the ability to retry challenges or the Retry-After header. // https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82 - ra = 5 + retryAfter = 5 * time.Second } - initialInterval := time.Duration(ra) * time.Second - ctx := context.Background() bo := backoff.NewExponentialBackOff() - bo.InitialInterval = initialInterval - bo.MaxInterval = 10 * initialInterval + bo.InitialInterval = retryAfter + bo.MaxInterval = 10 * retryAfter // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. @@ -134,7 +131,7 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { return wait.Retry(ctx, operation, backoff.WithBackOff(bo), - backoff.WithMaxElapsedTime(100*initialInterval)) + backoff.WithMaxElapsedTime(100*retryAfter)) } func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { From c1aaf19aac0953ddffdea549b31b176dfcdddb1f Mon Sep 17 00:00:00 2001 From: Andy Warner Date: Mon, 9 Feb 2026 18:51:54 -0700 Subject: [PATCH 74/95] docs: make it more clear that any ACME CA may be used (#2841) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6324ece67..07e4f7dd6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Lego -Let's Encrypt client and ACME library written in Go. +[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go. [![Go Reference](https://pkg.go.dev/badge/github.com/go-acme/lego/v4.svg)](https://pkg.go.dev/github.com/go-acme/lego/v4) [![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions) From 4c6d29882e14beea0287e67f36b3d75b15f27576 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 12 Feb 2026 17:32:23 +0100 Subject: [PATCH 75/95] chore: update linter, and workflows --- .github/workflows/pr.yml | 2 +- .golangci.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5df64db7f..fe3d35359 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.8.0 + GOLANGCI_LINT_VERSION: v2.9.0 HUGO_VERSION: 0.148.2 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI diff --git a/.golangci.yml b/.golangci.yml index b851169ff..b6ab51ccc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -183,6 +183,9 @@ linters: - text: "var-naming: avoid meaningless package names" linters: - revive + - text: "var-naming: avoid package names that conflict with Go standard library package names" + linters: + - revive - path: certcrypto/crypto.go text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable linters: From dc51d5ee65cde20de9d8a98356d629a221418acf Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Feb 2026 18:01:57 +0100 Subject: [PATCH 76/95] chore: use memcache Docker image directly (#2846) --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fe3d35359..9c845ea62 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -50,7 +50,7 @@ jobs: run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 - name: Set up a Memcached server - uses: niden/actions-memcached@v7 + run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine - name: Make run: | From 1991339cc15bc9468e4db101db465f50e568df88 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Feb 2026 18:20:05 +0100 Subject: [PATCH 77/95] timewebcloud: fix subdomain support (#2845) --- providers/dns/timewebcloud/internal/types.go | 8 +++++--- providers/dns/timewebcloud/timewebcloud.go | 7 +------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/providers/dns/timewebcloud/internal/types.go b/providers/dns/timewebcloud/internal/types.go index 81da4df5c..80cdb2c70 100644 --- a/providers/dns/timewebcloud/internal/types.go +++ b/providers/dns/timewebcloud/internal/types.go @@ -3,9 +3,11 @@ package internal import "fmt" type DNSRecord struct { - ID int `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Value string `json:"value,omitempty"` + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + + // SubDomain is the full name of a subdomain (not only the subdomain label). SubDomain string `json:"subdomain,omitempty"` } diff --git a/providers/dns/timewebcloud/timewebcloud.go b/providers/dns/timewebcloud/timewebcloud.go index d71beea4b..a599566e3 100644 --- a/providers/dns/timewebcloud/timewebcloud.go +++ b/providers/dns/timewebcloud/timewebcloud.go @@ -110,15 +110,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("timewebcloud: %w", err) - } - record := internal.DNSRecord{ Type: "TXT", Value: info.Value, - SubDomain: subDomain, + SubDomain: dns01.UnFqdn(info.EffectiveFQDN), } response, err := d.client.CreateRecord(context.Background(), authZone, record) From 4a61728ff0db8179e060d185b73a8c0d539d4c91 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Feb 2026 19:13:49 +0100 Subject: [PATCH 78/95] fix: deduplicate authz for DNS01 challenge (#2828) --- challenge/resolver/prober.go | 60 +++++++++++++++++++++++--- challenge/resolver/prober_mock_test.go | 55 ++++++++++++++++++----- challenge/resolver/prober_test.go | 48 +++++++++++++++++++-- 3 files changed, 142 insertions(+), 21 deletions(-) diff --git a/challenge/resolver/prober.go b/challenge/resolver/prober.go index aac1016d8..66b12c7a7 100644 --- a/challenge/resolver/prober.go +++ b/challenge/resolver/prober.go @@ -98,11 +98,24 @@ func (p *Prober) Solve(authorizations []acme.Authorization) error { } func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { + // Some CA are using the same token, + // this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records. + // In the sequential mode, this is not a problem because we can solve the challenges in order. + // But it can reduce the number of call the DNS provider APIs. + uniq := make(map[string]struct{}) + for i, authSolver := range authSolvers { // Submit the challenge domain := challenge.GetTargetedDomain(authSolver.authz) + chlg, _ := challenge.FindChallenge(challenge.DNS01, authSolver.authz) + if solvr, ok := authSolver.solver.(preSolver); ok { + if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok && chlg.Token != "" { + log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) + continue + } + err := solvr.PreSolve(authSolver.authz) if err != nil { failures[domain] = err @@ -111,6 +124,8 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { continue } + + uniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{} } // Solve challenge @@ -123,22 +138,43 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { continue } - // Clean challenge - cleanUp(authSolver.solver, authSolver.authz) + if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" { + // Clean challenge + cleanUp(authSolver.solver, authSolver.authz) - if len(authSolvers)-1 > i { - solvr := authSolver.solver.(sequential) - _, interval := solvr.Sequential() - log.Infof("sequence: wait for %s", interval) - time.Sleep(interval) + if len(authSolvers)-1 > i { + solvr := authSolver.solver.(sequential) + _, interval := solvr.Sequential() + log.Infof("sequence: wait for %s", interval) + time.Sleep(interval) + } + + delete(uniq, authSolver.authz.Identifier.Value+chlg.Token) + } else { + log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) } } } func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { + // Some CA are using the same token, + // this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records. + uniq := make(map[string]struct{}) + // For all valid preSolvers, first submit the challenges, so they have max time to propagate for _, authSolver := range authSolvers { authz := authSolver.authz + + chlg, err := challenge.FindChallenge(challenge.DNS01, authz) + if err == nil { + if _, ok := uniq[authz.Identifier.Value+chlg.Token]; ok { + log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) + continue + } + + uniq[authz.Identifier.Value+chlg.Token] = struct{}{} + } + if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authz) if err != nil { @@ -150,6 +186,16 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { defer func() { // Clean all created TXT records for _, authSolver := range authSolvers { + chlg, err := challenge.FindChallenge(challenge.DNS01, authSolver.authz) + if err == nil { + if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok { + delete(uniq, authSolver.authz.Identifier.Value+chlg.Token) + } else { + log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) + continue + } + } + cleanUp(authSolver.solver, authSolver.authz) } }() diff --git a/challenge/resolver/prober_mock_test.go b/challenge/resolver/prober_mock_test.go index 5a91fe075..dc7ad8dec 100644 --- a/challenge/resolver/prober_mock_test.go +++ b/challenge/resolver/prober_mock_test.go @@ -1,6 +1,7 @@ package resolver import ( + "fmt" "time" "github.com/go-acme/lego/v4/acme" @@ -11,34 +12,68 @@ type preSolverMock struct { preSolve map[string]error solve map[string]error cleanUp map[string]error + + preSolveCounter int + solveCounter int + cleanUpCounter int } func (s *preSolverMock) PreSolve(authorization acme.Authorization) error { + s.preSolveCounter++ + return s.preSolve[authorization.Identifier.Value] } func (s *preSolverMock) Solve(authorization acme.Authorization) error { + s.solveCounter++ + return s.solve[authorization.Identifier.Value] } func (s *preSolverMock) CleanUp(authorization acme.Authorization) error { + s.cleanUpCounter++ + return s.cleanUp[authorization.Identifier.Value] } +func (s *preSolverMock) String() string { + return fmt.Sprintf("PreSolve: %d, Solve: %d, CleanUp: %d", s.preSolveCounter, s.solveCounter, s.cleanUpCounter) +} + func createStubAuthorizationHTTP01(domain, status string) acme.Authorization { + return createStubAuthorization(domain, status, false, acme.Challenge{ + Type: challenge.HTTP01.String(), + Validated: time.Now(), + }) +} + +func createStubAuthorizationDNS01(domain string, wildcard bool) acme.Authorization { + var chlgs []acme.Challenge + + if wildcard { + chlgs = append(chlgs, acme.Challenge{ + Type: challenge.HTTP01.String(), + Validated: time.Now(), + }) + } + + chlgs = append(chlgs, acme.Challenge{ + Type: challenge.DNS01.String(), + Validated: time.Now(), + }) + + return createStubAuthorization(domain, acme.StatusProcessing, wildcard, chlgs...) +} + +func createStubAuthorization(domain, status string, wildcard bool, chlgs ...acme.Challenge) acme.Authorization { return acme.Authorization{ - Status: status, - Expires: time.Now(), + Wildcard: wildcard, + Status: status, + Expires: time.Now(), Identifier: acme.Identifier{ - Type: challenge.HTTP01.String(), + Type: "dns", Value: domain, }, - Challenges: []acme.Challenge{ - { - Type: challenge.HTTP01.String(), - Validated: time.Now(), - Error: nil, - }, - }, + Challenges: chlgs, } } diff --git a/challenge/resolver/prober_test.go b/challenge/resolver/prober_test.go index 06ff07d2c..829b16883 100644 --- a/challenge/resolver/prober_test.go +++ b/challenge/resolver/prober_test.go @@ -2,19 +2,22 @@ package resolver import ( "errors" + "fmt" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/challenge" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProber_Solve(t *testing.T) { testCases := []struct { - desc string - solvers map[challenge.Type]solver - authz []acme.Authorization - expectedError string + desc string + solvers map[challenge.Type]solver + authz []acme.Authorization + expectedError string + expectedCounters map[challenge.Type]string }{ { desc: "success", @@ -30,6 +33,30 @@ func TestProber_Solve(t *testing.T) { createStubAuthorizationHTTP01("example.org", acme.StatusProcessing), createStubAuthorizationHTTP01("example.net", acme.StatusProcessing), }, + expectedCounters: map[challenge.Type]string{ + challenge.HTTP01: "PreSolve: 3, Solve: 3, CleanUp: 3", + }, + }, + { + desc: "DNS-01 deduplicate", + solvers: map[challenge.Type]solver{ + challenge.DNS01: &preSolverMock{ + preSolve: map[string]error{}, + solve: map[string]error{}, + cleanUp: map[string]error{}, + }, + }, + authz: []acme.Authorization{ + createStubAuthorizationDNS01("a.example", false), + createStubAuthorizationDNS01("a.example", true), + createStubAuthorizationDNS01("b.example", false), + createStubAuthorizationDNS01("b.example", true), + createStubAuthorizationDNS01("c.example", true), + createStubAuthorizationDNS01("d.example", false), + }, + expectedCounters: map[challenge.Type]string{ + challenge.DNS01: "PreSolve: 4, Solve: 6, CleanUp: 4", + }, }, { desc: "already valid", @@ -45,6 +72,9 @@ func TestProber_Solve(t *testing.T) { createStubAuthorizationHTTP01("example.org", acme.StatusValid), createStubAuthorizationHTTP01("example.net", acme.StatusValid), }, + expectedCounters: map[challenge.Type]string{ + challenge.HTTP01: "PreSolve: 0, Solve: 0, CleanUp: 0", + }, }, { desc: "when preSolve fail, auth is flagged as error and skipped", @@ -69,6 +99,9 @@ func TestProber_Solve(t *testing.T) { expectedError: `error: one or more domains had a problem: [example.com] preSolve error example.com `, + expectedCounters: map[challenge.Type]string{ + challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3", + }, }, { desc: "errors at different stages", @@ -95,6 +128,9 @@ func TestProber_Solve(t *testing.T) { [example.com] preSolve error example.com [example.org] solve error example.org `, + expectedCounters: map[challenge.Type]string{ + challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3", + }, }, } @@ -112,6 +148,10 @@ func TestProber_Solve(t *testing.T) { } else { require.NoError(t, err) } + + for n, s := range test.solvers { + assert.Equal(t, test.expectedCounters[n], fmt.Sprintf("%s", s)) + } }) } } From 2e095b95a57621177a10ee1be2650406d8707524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grigas=20=C5=A0ukys?= <135010329+grigassukys@users.noreply.github.com> Date: Sun, 15 Feb 2026 04:22:35 +0200 Subject: [PATCH 79/95] Add DNS provider for FusionLayer NameSurfer (#2852) Co-authored-by: Fernandez Ludovic --- README.md | 62 ++--- cmd/zz_gen_cmd_dnshelp.go | 25 ++ docs/content/dns/zz_gen_namesurfer.md | 73 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/namesurfer/internal/client.go | 226 ++++++++++++++++++ .../dns/namesurfer/internal/client_test.go | 158 ++++++++++++ .../fixtures/addDNSRecord-request.json | 16 ++ .../internal/fixtures/addDNSRecord.json | 4 + .../namesurfer/internal/fixtures/error.json | 24 ++ .../internal/fixtures/listZones-request.json | 9 + .../internal/fixtures/listZones.json | 17 ++ .../fixtures/searchDNSHosts-request.json | 9 + .../internal/fixtures/searchDNSHosts.json | 23 ++ .../fixtures/updateDNSHost-request.json | 17 ++ .../internal/fixtures/updateDNSHost.json | 4 + providers/dns/namesurfer/internal/types.go | 72 ++++++ providers/dns/namesurfer/namesurfer.go | 214 +++++++++++++++++ providers/dns/namesurfer/namesurfer.toml | 28 +++ providers/dns/namesurfer/namesurfer_test.go | 174 ++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 20 files changed, 1128 insertions(+), 32 deletions(-) create mode 100644 docs/content/dns/zz_gen_namesurfer.md create mode 100644 providers/dns/namesurfer/internal/client.go create mode 100644 providers/dns/namesurfer/internal/client_test.go create mode 100644 providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json create mode 100644 providers/dns/namesurfer/internal/fixtures/addDNSRecord.json create mode 100644 providers/dns/namesurfer/internal/fixtures/error.json create mode 100644 providers/dns/namesurfer/internal/fixtures/listZones-request.json create mode 100644 providers/dns/namesurfer/internal/fixtures/listZones.json create mode 100644 providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json create mode 100644 providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json create mode 100644 providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json create mode 100644 providers/dns/namesurfer/internal/fixtures/updateDNSHost.json create mode 100644 providers/dns/namesurfer/internal/types.go create mode 100644 providers/dns/namesurfer/namesurfer.go create mode 100644 providers/dns/namesurfer/namesurfer.toml create mode 100644 providers/dns/namesurfer/namesurfer_test.go diff --git a/README.md b/README.md index 07e4f7dd6..105ea53aa 100644 --- a/README.md +++ b/README.md @@ -141,160 +141,160 @@ If your DNS provider is not supported, please open an [issue](https://github.com F5 XC freemyip.com + FusionLayer NameSurfer G-Core Gandi Gandi Live DNS (v5) - Gigahost.no + Gigahost.no Glesys Go Daddy Google Cloud - Google Domains + Google Domains Gravity Hetzner Hosting.de - Hosting.nl + Hosting.nl Hostinger Hosttech HTTP request - http.net + http.net Huawei Cloud Hurricane Electric DNS HyperOne - IBM Cloud (SoftLayer) + IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox Infomaniak - Internet Initiative Japan + Internet Initiative Japan Internet.bs INWX Ionos - Ionos Cloud + Ionos Cloud IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module - iwantmyname (Deprecated) + iwantmyname (Deprecated) JD Cloud Joker Joohoi's ACME-DNS - KeyHelp + KeyHelp Liara Lima-City Linode (v4) - Liquid Web + Liquid Web Loopia LuaDNS Mail-in-a-Box - ManageEngine CloudDNS + ManageEngine CloudDNS Manual Metaname Metaregistrar - mijn.host + mijn.host Mittwald myaddr.{tools,dev,io} MyDNS.jp - MythicBeasts + MythicBeasts Name.com Namecheap Namesilo - NearlyFreeSpeech.NET + NearlyFreeSpeech.NET Neodigit Netcup Netlify - Nicmanager + Nicmanager NIFCloud Njalla Nodion - NS1 + NS1 Octenium Open Telekom Cloud Oracle Cloud - OVH + OVH plesk.com Porkbun PowerDNS - Rackspace + Rackspace Rain Yun/雨云 RcodeZero reg.ru - Regfish + Regfish RFC2136 RimuHosting RU CENTER - Sakura Cloud + Sakura Cloud Scaleway Selectel Selectel v2 - SelfHost.(de|eu) + SelfHost.(de|eu) Servercow Shellrent Simply.com - Sonic + Sonic Spaceship Stackpath Syse - Technitium + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TodayNIC/时代互联 + TodayNIC/时代互联 TransIP UKFast SafeDNS Ultradns - United-Domains + United-Domains Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS Virtualname VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 357834a3c..cdee65371 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -131,6 +131,7 @@ func allDNSCodes() string { "namecheap", "namedotcom", "namesilo", + "namesurfer", "nearlyfreespeech", "neodigit", "netcup", @@ -2742,6 +2743,30 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesilo`) + case "namesurfer": + // generated from: providers/dns/namesurfer/namesurfer.toml + ew.writeln(`Configuration for FusionLayer NameSurfer.`) + ew.writeln(`Code: 'namesurfer'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "NAMESURFER_API_KEY": API key name`) + ew.writeln(` - "NAMESURFER_API_SECRET": API secret`) + ew.writeln(` - "NAMESURFER_BASE_URL": The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "NAMESURFER_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "NAMESURFER_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`) + ew.writeln(` - "NAMESURFER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "NAMESURFER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "NAMESURFER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "NAMESURFER_VIEW": DNS view name (optional, default: empty string)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesurfer`) + case "nearlyfreespeech": // generated from: providers/dns/nearlyfreespeech/nearlyfreespeech.toml ew.writeln(`Configuration for NearlyFreeSpeech.NET.`) diff --git a/docs/content/dns/zz_gen_namesurfer.md b/docs/content/dns/zz_gen_namesurfer.md new file mode 100644 index 000000000..9a2802d0e --- /dev/null +++ b/docs/content/dns/zz_gen_namesurfer.md @@ -0,0 +1,73 @@ +--- +title: "FusionLayer NameSurfer" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: namesurfer +dnsprovider: + since: "v4.32.0" + code: "namesurfer" + url: "https://www.fusionlayer.com/" +--- + + + + + + +Configuration for [FusionLayer NameSurfer](https://www.fusionlayer.com/). + + + + +- Code: `namesurfer` +- Since: v4.32.0 + + +Here is an example bash command using the FusionLayer NameSurfer provider: + +```bash +NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \ +NAMESURFER_API_KEY=xxx \ +NAMESURFER_API_SECRET=yyy \ +lego --dns namesurfer -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `NAMESURFER_API_KEY` | API key name | +| `NAMESURFER_API_SECRET` | API secret | +| `NAMESURFER_BASE_URL` | The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10) | + +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 | +|--------------------------------|-------------| +| `NAMESURFER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `NAMESURFER_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate | +| `NAMESURFER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `NAMESURFER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `NAMESURFER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `NAMESURFER_VIEW` | DNS view name (optional, default: empty string) | + +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://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index e31633567..759b8e84f 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, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, 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, todaynic, 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, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/namesurfer/internal/client.go b/providers/dns/namesurfer/internal/client.go new file mode 100644 index 000000000..e40a7988c --- /dev/null +++ b/providers/dns/namesurfer/internal/client.go @@ -0,0 +1,226 @@ +package internal + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +type Client struct { + apiKey string + apiSecret string + + BaseURL *url.URL + HTTPClient *http.Client +} + +func NewClient(baseURL, apiKey, apiSecret string) (*Client, error) { + if apiKey == "" || apiSecret == "" { + return nil, errors.New("credentials missing") + } + + if baseURL == "" { + return nil, errors.New("base URL missing") + } + + apiEndpoint, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + return &Client{ + apiKey: apiKey, + apiSecret: apiSecret, + BaseURL: apiEndpoint.JoinPath("jsonrpc10"), + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + }, + }, nil +} + +// AddDNSRecord adds a DNS record. +// http://95.128.3.201:8053/API/NSService_10#addDNSRecord +func (d *Client) AddDNSRecord(ctx context.Context, zoneName, viewName string, record DNSNode) error { + digest := d.computeDigest( + zoneName, + viewName, + record.Name, + record.Type, + strconv.Itoa(record.TTL), + record.Data, + ) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + zoneName, + viewName, + record, + } + + var ok bool + + err := d.doRequest(ctx, "addDNSRecord", params, &ok) + if err != nil { + return err + } + + if !ok { + return errors.New("addDNSRecord failed") + } + + return nil +} + +// UpdateDNSHost updates a DNS host record. +// Passing an empty newNode removes the oldNode. +// http://95.128.3.201:8053/API/NSService_10#updateDNSHost +func (d *Client) UpdateDNSHost(ctx context.Context, zoneName, viewName string, oldNode, newNode DNSNode) error { + digest := d.computeDigest(zoneName, viewName) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + zoneName, + viewName, + oldNode, + newNode, + } + + var ok bool + + err := d.doRequest(ctx, "updateDNSHost", params, &ok) + if err != nil { + return err + } + + if !ok { + return errors.New("updateDNSHost failed") + } + + return nil +} + +// SearchDNSHosts searches for DNS host records. +// http://95.128.3.201:8053/API/NSService_10#searchDNSHosts +func (d *Client) SearchDNSHosts(ctx context.Context, pattern string) ([]DNSNode, error) { + digest := d.computeDigest(pattern) + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + pattern, + } + + var nodes []DNSNode + + err := d.doRequest(ctx, "searchDNSHosts", params, &nodes) + if err != nil { + return nil, err + } + + return nodes, nil +} + +// ListZones lists DNS zones. +// http://95.128.3.201:8053/API/NSService_10#listZones +func (d *Client) ListZones(ctx context.Context, mode string) ([]DNSZone, error) { + digest := d.computeDigest() + + // JSON-RPC 1.0 requires positional parameters array + params := []any{ + digest, + mode, + } + + var zones []DNSZone + + err := d.doRequest(ctx, "listZones", params, &zones) + if err != nil { + return nil, err + } + + return zones, nil +} + +func (d *Client) doRequest(ctx context.Context, method string, params []any, result any) error { + payload := APIRequest{ + ID: 1, + Method: method, + Params: slices.Concat([]any{d.apiKey}, params), + } + + buf := new(bytes.Buffer) + + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return fmt.Errorf("failed to create request JSON body: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.BaseURL.String(), buf) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := d.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + if resp.StatusCode/100 != 2 { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + var rpcResp APIResponse + + err = json.Unmarshal(raw, &rpcResp) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if rpcResp.Error != nil { + return rpcResp.Error + } + + err = json.Unmarshal(rpcResp.Result, result) + if err != nil { + return fmt.Errorf("unable to unmarshal response: %w: %s", err, rpcResp.Result) + } + + return nil +} + +func (d *Client) computeDigest(parts ...string) string { + params := []string{d.apiKey} + params = append(params, parts...) + params = append(params, d.apiSecret) + + mac := hmac.New(sha256.New, []byte(d.apiSecret)) + mac.Write([]byte(strings.Join(params, "&"))) + + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/providers/dns/namesurfer/internal/client_test.go b/providers/dns/namesurfer/internal/client_test.go new file mode 100644 index 000000000..9e8f917bc --- /dev/null +++ b/providers/dns/namesurfer/internal/client_test.go @@ -0,0 +1,158 @@ +package internal + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "user", "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(), + ) +} + +func TestClient_AddDNSRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("addDNSRecord.json"), + servermock.CheckRequestJSONBodyFromFixture("addDNSRecord-request.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) + require.NoError(t, err) +} + +func TestClient_AddDNSRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) + require.EqualError(t, err, "code: Server.Keyfailure, "+ + "filename: service, line: 13, "+ + "message: Unknown keyname user, "+ + `detail: Traceback (most recent call last): File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 159, in dispatch_request result = self.call_method(method,req_dict,tc,export_dict,log_line) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 96, in call_method result = getattr(service_class_instance,req_dict['methodname'])(*args) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py", line 77, in injector res = f(*args,**kw) File "/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py", line 502, in addDNSRecord key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data]) File "/usr/local/namesurfer/webui2/webui/service/base/implementation.py", line 63, in validate_key raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname) ApiFault: service(13): Unknown keyname user `) +} + +func TestClient_UpdateDNSHost(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("updateDNSHost.json"), + servermock.CheckRequestJSONBodyFromFixture("updateDNSHost-request.json"), + ). + Build(t) + + record := DNSNode{ + Name: "_acme-challenge", + Type: "TXT", + Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 300, + } + + err := client.UpdateDNSHost(t.Context(), "example.com", "viewA", record, DNSNode{}) + require.NoError(t, err) +} + +func TestClient_SearchDNSHosts(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("searchDNSHosts.json"), + servermock.CheckRequestJSONBodyFromFixture("searchDNSHosts-request.json"), + ). + Build(t) + + records, err := client.SearchDNSHosts(t.Context(), "value") + require.NoError(t, err) + + expected := []DNSNode{ + {Name: "foo", Type: "TXT", Data: "xxx", TTL: 300}, + {Name: "_acme-challenge", Type: "TXT", Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", TTL: 300}, + {Name: "bar", Type: "A", Data: "yyy", TTL: 300}, + } + + assert.Equal(t, expected, records) +} + +func TestClient_ListZones(t *testing.T) { + client := mockBuilder(). + Route("POST /jsonrpc10", + servermock.ResponseFromFixture("listZones.json"), + servermock.CheckRequestJSONBodyFromFixture("listZones-request.json"), + ). + Build(t) + + zones, err := client.ListZones(t.Context(), "value") + require.NoError(t, err) + + expected := []DNSZone{ + {Name: "example.com", View: "viewA"}, + {Name: "example.org", View: "viewB"}, + {Name: "example.net", View: "viewC"}, + } + + assert.Equal(t, expected, zones) +} + +func TestClient_computeDigest(t *testing.T) { + client, err := NewClient("https://test.example.com", "testkey", "testsecret") + require.NoError(t, err) + + testCases := []struct { + desc string + parts []string + expected string + }{ + { + desc: "no parts", + parts: []string{}, + expected: "99b5dcdc19bfc0ce2af3fe848f4bcb6f7beb352e9599e8ba50544d86de567282", + }, + { + desc: "parts", + parts: []string{"zone.example.com", "default"}, + expected: "94efef76383889b1ae620582a25d1c3aa9bd9ba9ac4bdccdf4aefbc3ae6e8329", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + digest := client.computeDigest(test.parts...) + + assert.Equal(t, test.expected, digest) + }) + } +} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json new file mode 100644 index 000000000..660109aae --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json @@ -0,0 +1,16 @@ +{ + "id": 1, + "method": "addDNSRecord", + "params": [ + "user", + "4fcc5fa29531709b0381c8debea127a6a26e71cb9491727876819cf5805c4990", + "example.com", + "viewA", + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json new file mode 100644 index 000000000..f41779e30 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "result": true +} diff --git a/providers/dns/namesurfer/internal/fixtures/error.json b/providers/dns/namesurfer/internal/fixtures/error.json new file mode 100644 index 000000000..8ddf8df25 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/error.json @@ -0,0 +1,24 @@ +{ + "result": null, + "error": { + "filename": "service", + "lineno": 13, + "code": "Server.Keyfailure", + "string": "Unknown keyname user", + "detail": [ + "Traceback (most recent call last):", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 159, in dispatch_request", + " result = self.call_method(method,req_dict,tc,export_dict,log_line)", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 96, in call_method", + " result = getattr(service_class_instance,req_dict['methodname'])(*args)", + " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py\", line 77, in injector", + " res = f(*args,**kw)", + " File \"/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py\", line 502, in addDNSRecord", + " key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data])", + " File \"/usr/local/namesurfer/webui2/webui/service/base/implementation.py\", line 63, in validate_key", + " raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname)", + "ApiFault: service(13): Unknown keyname user", + "" + ] + } +} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones-request.json b/providers/dns/namesurfer/internal/fixtures/listZones-request.json new file mode 100644 index 000000000..06689de7a --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/listZones-request.json @@ -0,0 +1,9 @@ +{ + "id": 1, + "method": "listZones", + "params": [ + "user", + "2739461ea1a3dc51302993f724f40228409c53b78025d8d7b1d7bba3c1bf2d66", + "value" + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones.json b/providers/dns/namesurfer/internal/fixtures/listZones.json new file mode 100644 index 000000000..37fa2053b --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/listZones.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "result": [ + { + "name": "example.com", + "view": "viewA" + }, + { + "name": "example.org", + "view": "viewB" + }, + { + "name": "example.net", + "view": "viewC" + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json new file mode 100644 index 000000000..4a88340e2 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json @@ -0,0 +1,9 @@ +{ + "id": 1, + "method": "searchDNSHosts", + "params": [ + "user", + "02cf1a2f6e124507d16738d595f583932185313fc96afc2d8404960acaec29b4", + "value" + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json new file mode 100644 index 000000000..822459148 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json @@ -0,0 +1,23 @@ +{ + "id": 1, + "result": [ + { + "name": "foo", + "type": "TXT", + "data": "xxx", + "ttl": 300 + }, + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + }, + { + "name": "bar", + "type": "A", + "data": "yyy", + "ttl": 300 + } + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json new file mode 100644 index 000000000..c99218ec5 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "method": "updateDNSHost", + "params": [ + "user", + "510e63288ac874c1d5ba313a9411591daa346e5621fb0153263adc278794e378", + "example.com", + "viewA", + { + "name": "_acme-challenge", + "type": "TXT", + "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 300 + }, + {} + ] +} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json new file mode 100644 index 000000000..f41779e30 --- /dev/null +++ b/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "result": true +} diff --git a/providers/dns/namesurfer/internal/types.go b/providers/dns/namesurfer/internal/types.go new file mode 100644 index 000000000..f95593745 --- /dev/null +++ b/providers/dns/namesurfer/internal/types.go @@ -0,0 +1,72 @@ +package internal + +import ( + "encoding/json" + "fmt" + "strings" +) + +// DNSNode represents a DNS record. +// http://95.128.3.201:8053/API/NSService_10#DNSNode +type DNSNode struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Data string `json:"data,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +// DNSZone represents a DNS zone. +// http://95.128.3.201:8053/API/NSService_10#DNSZone +type DNSZone struct { + Name string `json:"name,omitempty"` + View string `json:"view,omitempty"` +} + +// APIRequest represents a JSON-RPC request. +// https://www.jsonrpc.org/specification_v1#a1.1Requestmethodinvocation +type APIRequest struct { + ID any `json:"id"` // Can be int or string depending on API + Method string `json:"method"` + Params []any `json:"params"` +} + +// APIResponse represents a JSON-RPC response. +// https://www.jsonrpc.org/specification_v1#a1.2Response +type APIResponse struct { + ID any `json:"id"` // Can be int or string depending on API + Result json.RawMessage `json:"result"` + Error *APIError `json:"error"` +} + +// APIError represents an error. +type APIError struct { + Code any `json:"code"` // Can be int or string depending on API + Filename string `json:"filename"` + LineNumber int `json:"lineno"` + Message string `json:"string"` + Detail []string `json:"detail"` +} + +func (e *APIError) Error() string { + var msg strings.Builder + + msg.WriteString(fmt.Sprintf("code: %v", e.Code)) + + if e.Filename != "" { + msg.WriteString(fmt.Sprintf(", filename: %s", e.Filename)) + } + + if e.LineNumber > 0 { + msg.WriteString(fmt.Sprintf(", line: %d", e.LineNumber)) + } + + if e.Message != "" { + msg.WriteString(fmt.Sprintf(", message: %s", e.Message)) + } + + if len(e.Detail) > 0 { + msg.WriteString(fmt.Sprintf(", detail: %v", strings.Join(e.Detail, " "))) + } + + return msg.String() +} diff --git a/providers/dns/namesurfer/namesurfer.go b/providers/dns/namesurfer/namesurfer.go new file mode 100644 index 000000000..6b7f48402 --- /dev/null +++ b/providers/dns/namesurfer/namesurfer.go @@ -0,0 +1,214 @@ +// Package namesurfer implements a DNS provider for solving the DNS-01 challenge using FusionLayer NameSurfer API. +package namesurfer + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "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/namesurfer/internal" +) + +// Environment variables names. +const ( + envNamespace = "NAMESURFER_" + + EnvBaseURL = envNamespace + "BASE_URL" + EnvAPIKey = envNamespace + "API_KEY" + EnvAPISecret = envNamespace + "API_SECRET" + EnvView = envNamespace + "VIEW" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + BaseURL string + APIKey string + APISecret string + View string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 300), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client + + zones map[string]string + zonesMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for FusionLayer NameSurfer. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvBaseURL, EnvAPIKey, EnvAPISecret) + if err != nil { + return nil, fmt.Errorf("namesurfer: %w", err) + } + + config := NewDefaultConfig() + config.BaseURL = values[EnvBaseURL] + config.APIKey = values[EnvAPIKey] + config.APISecret = values[EnvAPISecret] + config.View = env.GetOrDefaultString(EnvView, "") + + if env.GetOrDefaultBool(EnvInsecureSkipVerify, false) { + config.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for FusionLayer NameSurfer. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("namesurfer: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.BaseURL, config.APIKey, config.APISecret) + if err != nil { + return nil, fmt.Errorf("namesurfer: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + zones: make(map[string]string), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("namesurfer: %w", err) + } + + d.zonesMu.Lock() + d.zones[token] = zone + d.zonesMu.Unlock() + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("namesurfer: %w", err) + } + + record := internal.DNSNode{ + Name: subDomain, + Type: "TXT", + TTL: d.config.TTL, + Data: info.Value, + } + + err = d.client.AddDNSRecord(ctx, zone, d.config.View, record) + if err != nil { + return fmt.Errorf("namesurfer: add DNS record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + d.zonesMu.Lock() + zone, ok := d.zones[token] + d.zonesMu.Unlock() + + if !ok { + return fmt.Errorf("namesurfer: unknown zone for '%s'", info.EffectiveFQDN) + } + + d.zonesMu.Lock() + delete(d.zones, token) + d.zonesMu.Unlock() + + existing, err := d.client.SearchDNSHosts(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("namesurfer: search DNS hosts: %w", err) + } + + for _, node := range existing { + if node.Type != "TXT" || node.Data != info.Value { + continue + } + + err = d.client.UpdateDNSHost(ctx, zone, d.config.View, node, internal.DNSNode{}) + if err != nil { + return fmt.Errorf("namesurfer: update DNS host: %w", err) + } + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { + zones, err := d.client.ListZones(ctx, "forward") + if err != nil { + return "", fmt.Errorf("list zones: %w", err) + } + + domain := dns01.UnFqdn(fqdn) + + var zoneName string + + for _, zone := range zones { + if strings.HasSuffix(domain, zone.Name) && len(zone.Name) > len(zoneName) { + zoneName = zone.Name + } + } + + if zoneName == "" { + return "", fmt.Errorf("no zone found for %s", fqdn) + } + + return zoneName, nil +} diff --git a/providers/dns/namesurfer/namesurfer.toml b/providers/dns/namesurfer/namesurfer.toml new file mode 100644 index 000000000..fd914ec0c --- /dev/null +++ b/providers/dns/namesurfer/namesurfer.toml @@ -0,0 +1,28 @@ +Name = "FusionLayer NameSurfer" +Description = '''''' +URL = "https://www.fusionlayer.com/" +Code = "namesurfer" +Since = "v4.32.0" + +Example = ''' +NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \ +NAMESURFER_API_KEY=xxx \ +NAMESURFER_API_SECRET=yyy \ +lego --dns namesurfer -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + NAMESURFER_BASE_URL = "The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)" + NAMESURFER_API_KEY = "API key name" + NAMESURFER_API_SECRET = "API secret" + [Configuration.Additional] + NAMESURFER_VIEW = "DNS view name (optional, default: empty string)" + NAMESURFER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + NAMESURFER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + NAMESURFER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + NAMESURFER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NAMESURFER_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" + +[Links] + API = "https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10" diff --git a/providers/dns/namesurfer/namesurfer_test.go b/providers/dns/namesurfer/namesurfer_test.go new file mode 100644 index 000000000..ce3aa37af --- /dev/null +++ b/providers/dns/namesurfer/namesurfer_test.go @@ -0,0 +1,174 @@ +package namesurfer + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvBaseURL, + EnvAPIKey, + EnvAPISecret, + EnvView, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "user", + EnvAPISecret: "secret", + }, + }, + { + desc: "missing base URL", + envVars: map[string]string{ + EnvBaseURL: "", + EnvAPIKey: "user", + EnvAPISecret: "secret", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL", + }, + { + desc: "missing API key", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "", + EnvAPISecret: "secret", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_API_KEY", + }, + { + desc: "missing API secret", + envVars: map[string]string{ + EnvBaseURL: "https://example.com", + EnvAPIKey: "user", + EnvAPISecret: "", + }, + expected: "namesurfer: some credentials information are missing: NAMESURFER_API_SECRET", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL,NAMESURFER_API_KEY,NAMESURFER_API_SECRET", + }, + } + + 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 + baseURL string + apiKey string + apiSecret string + expected string + }{ + { + desc: "success", + baseURL: "https://example.com", + apiKey: "user", + apiSecret: "secret", + }, + { + desc: "missing base URL", + apiKey: "user", + apiSecret: "secret", + expected: "namesurfer: base URL missing", + }, + { + desc: "missing API key", + baseURL: "https://example.com", + apiSecret: "secret", + expected: "namesurfer: credentials missing", + }, + { + desc: "missing API secret", + baseURL: "https://example.com", + apiKey: "user", + expected: "namesurfer: credentials missing", + }, + { + desc: "missing credentials", + expected: "namesurfer: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.BaseURL = test.baseURL + config.APIKey = test.apiKey + config.APISecret = test.apiSecret + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index ae41f6a20..10fda2df1 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -125,6 +125,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" + "github.com/go-acme/lego/v4/providers/dns/namesurfer" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech" "github.com/go-acme/lego/v4/providers/dns/neodigit" "github.com/go-acme/lego/v4/providers/dns/netcup" @@ -434,6 +435,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return namedotcom.NewDNSProvider() case "namesilo": return namesilo.NewDNSProvider() + case "namesurfer": + return namesurfer.NewDNSProvider() case "nearlyfreespeech": return nearlyfreespeech.NewDNSProvider() case "neodigit": From c06f378f0ed626fe9f8edddfbe8647d50f4f36f3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 15 Feb 2026 10:42:48 +0100 Subject: [PATCH 80/95] namesurfer: fix updateDNSHost (#2854) --- .../internal/fixtures/updateDNSHost-request.json | 7 ++++++- providers/dns/namesurfer/internal/types.go | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json index c99218ec5..494de20c6 100644 --- a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json +++ b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json @@ -12,6 +12,11 @@ "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", "ttl": 300 }, - {} + { + "name": "", + "type": "", + "data": "", + "ttl": 0 + } ] } diff --git a/providers/dns/namesurfer/internal/types.go b/providers/dns/namesurfer/internal/types.go index f95593745..7b3f08013 100644 --- a/providers/dns/namesurfer/internal/types.go +++ b/providers/dns/namesurfer/internal/types.go @@ -9,10 +9,10 @@ import ( // DNSNode represents a DNS record. // http://95.128.3.201:8053/API/NSService_10#DNSNode type DNSNode struct { - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Data string `json:"data,omitempty"` - TTL int `json:"ttl,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Data string `json:"data"` + TTL int `json:"ttl"` } // DNSZone represents a DNS zone. From 94e3bfb96af1e16a7defa7795e33d5185ed20e0f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 17 Feb 2026 23:34:27 +0100 Subject: [PATCH 81/95] chore: update linter (#2857) --- .github/workflows/pr.yml | 2 +- acme/errors.go | 10 +++++----- providers/dns/binarylane/internal/types.go | 6 +++--- providers/dns/cloudflare/internal/types.go | 6 +++--- providers/dns/dnsexit/internal/types.go | 6 +++--- providers/dns/godaddy/internal/types.go | 4 ++-- providers/dns/gravity/internal/types.go | 8 ++++---- .../dns/hetzner/internal/hetznerv1/internal/types.go | 10 +++++----- providers/dns/hostinger/internal/types.go | 6 +++--- providers/dns/hosttech/internal/types.go | 6 +++--- providers/dns/mittwald/internal/types.go | 8 ++++---- providers/dns/namesurfer/internal/types.go | 12 ++++++------ 12 files changed, 42 insertions(+), 42 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9c845ea62..33ca106cc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.9.0 + GOLANGCI_LINT_VERSION: v2.10 HUGO_VERSION: 0.148.2 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI diff --git a/acme/errors.go b/acme/errors.go index be4721c9d..cd447d7b4 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -29,18 +29,18 @@ type ProblemDetails struct { } func (p *ProblemDetails) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("acme: error: %d", p.HTTPStatus)) + _, _ = fmt.Fprintf(msg, "acme: error: %d", p.HTTPStatus) if p.Method != "" || p.URL != "" { - msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)) + _, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Method, p.URL) } - msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)) + _, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Type, p.Detail) for _, sub := range p.SubProblems { - msg.WriteString(fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)) + _, _ = fmt.Fprintf(msg, ", problem: %q :: %s", sub.Type, sub.Detail) } if p.Instance != "" { diff --git a/providers/dns/binarylane/internal/types.go b/providers/dns/binarylane/internal/types.go index 987e5c356..06d4be5c0 100644 --- a/providers/dns/binarylane/internal/types.go +++ b/providers/dns/binarylane/internal/types.go @@ -15,12 +15,12 @@ type APIError struct { } func (a *APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance)) + _, _ = fmt.Fprintf(msg, "%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance) for s, values := range a.Errors { - msg.WriteString(fmt.Sprintf(": %s: %s", s, strings.Join(values, ", "))) + _, _ = fmt.Fprintf(msg, ": %s: %s", s, strings.Join(values, ", ")) } return msg.String() diff --git a/providers/dns/cloudflare/internal/types.go b/providers/dns/cloudflare/internal/types.go index 4a7f9e031..50a7bbbf9 100644 --- a/providers/dns/cloudflare/internal/types.go +++ b/providers/dns/cloudflare/internal/types.go @@ -42,13 +42,13 @@ type ErrorChain struct { type Errors []Message func (e Errors) Error() string { - var msg strings.Builder + msg := new(strings.Builder) for _, item := range e { - msg.WriteString(fmt.Sprintf("%d: %s", item.Code, item.Message)) + _, _ = fmt.Fprintf(msg, "%d: %s", item.Code, item.Message) for _, link := range item.ErrorChain { - msg.WriteString(fmt.Sprintf("; %d: %s", link.Code, link.Message)) + _, _ = fmt.Fprintf(msg, "; %d: %s", link.Code, link.Message) } } diff --git a/providers/dns/dnsexit/internal/types.go b/providers/dns/dnsexit/internal/types.go index 060dd883e..db254549f 100644 --- a/providers/dns/dnsexit/internal/types.go +++ b/providers/dns/dnsexit/internal/types.go @@ -29,12 +29,12 @@ type APIResponse struct { } func (a APIResponse) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s (code=%d)", a.Message, a.Code)) + _, _ = fmt.Fprintf(msg, "%s (code=%d)", a.Message, a.Code) for _, detail := range a.Details { - msg.WriteString(fmt.Sprintf(", %s", detail)) + _, _ = fmt.Fprintf(msg, ", %s", detail) } return msg.String() diff --git a/providers/dns/godaddy/internal/types.go b/providers/dns/godaddy/internal/types.go index c1e6d6638..3bd5c9560 100644 --- a/providers/dns/godaddy/internal/types.go +++ b/providers/dns/godaddy/internal/types.go @@ -26,9 +26,9 @@ type APIError struct { } func (a APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s: %s", a.Code, a.Message)) + _, _ = fmt.Fprintf(msg, "%s: %s", a.Code, a.Message) for _, field := range a.Fields { msg.WriteString(" ") diff --git a/providers/dns/gravity/internal/types.go b/providers/dns/gravity/internal/types.go index cb6e99083..872bc070f 100644 --- a/providers/dns/gravity/internal/types.go +++ b/providers/dns/gravity/internal/types.go @@ -13,17 +13,17 @@ type APIError struct { } func (a *APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("status: %s, error: %s", a.Status, a.ErrorMsg)) + _, _ = fmt.Fprintf(msg, "status: %s, error: %s", a.Status, a.ErrorMsg) if a.Code != 0 { - msg.WriteString(fmt.Sprintf(", code: %d", a.Code)) + _, _ = fmt.Fprintf(msg, ", code: %d", a.Code) } if len(a.Context) != 0 { for k, v := range a.Context { - msg.WriteString(fmt.Sprintf(", %s: %s", k, v)) + _, _ = fmt.Fprintf(msg, ", %s: %s", k, v) } } diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/types.go b/providers/dns/hetzner/internal/hetznerv1/internal/types.go index 47e8a3f91..2b38a8a8c 100644 --- a/providers/dns/hetzner/internal/hetznerv1/internal/types.go +++ b/providers/dns/hetzner/internal/hetznerv1/internal/types.go @@ -16,20 +16,20 @@ type ErrorInfo struct { } func (i *ErrorInfo) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s: %s", i.Code, i.Message)) + _, _ = fmt.Fprintf(msg, "%s: %s", i.Code, i.Message) if i.Details.Announcement != "" { - msg.WriteString(fmt.Sprintf(": %s", i.Details.Announcement)) + _, _ = fmt.Fprintf(msg, ": %s", i.Details.Announcement) } for _, limit := range i.Details.Limits { - msg.WriteString(fmt.Sprintf("limit: %s", limit.Name)) + _, _ = fmt.Fprintf(msg, "limit: %s", limit.Name) } for _, field := range i.Details.Fields { - msg.WriteString(fmt.Sprintf("field: %s: %s", field.Name, strings.Join(field.Messages, ", "))) + _, _ = fmt.Fprintf(msg, "field: %s: %s", field.Name, strings.Join(field.Messages, ", ")) } return msg.String() diff --git a/providers/dns/hostinger/internal/types.go b/providers/dns/hostinger/internal/types.go index 534dfa5e5..c1a02ff8c 100644 --- a/providers/dns/hostinger/internal/types.go +++ b/providers/dns/hostinger/internal/types.go @@ -12,12 +12,12 @@ type APIError struct { } func (a *APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s: %s", a.CorrelationID, a.Message)) + _, _ = fmt.Fprintf(msg, "%s: %s", a.CorrelationID, a.Message) for field, values := range a.Errors { - msg.WriteString(fmt.Sprintf(": %s: %s", field, strings.Join(values, ", "))) + _, _ = fmt.Fprintf(msg, ": %s: %s", field, strings.Join(values, ", ")) } return msg.String() diff --git a/providers/dns/hosttech/internal/types.go b/providers/dns/hosttech/internal/types.go index 854fc4883..a4b5b564d 100644 --- a/providers/dns/hosttech/internal/types.go +++ b/providers/dns/hosttech/internal/types.go @@ -16,12 +16,12 @@ type APIError struct { } func (a APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%d: %s", a.StatusCode, a.Message)) + _, _ = fmt.Fprintf(msg, "%d: %s", a.StatusCode, a.Message) for k, v := range a.Errors { - msg.WriteString(fmt.Sprintf(" %s: %v", k, v)) + _, _ = fmt.Fprintf(msg, " %s: %v", k, v) } return msg.String() diff --git a/providers/dns/mittwald/internal/types.go b/providers/dns/mittwald/internal/types.go index ce49cb820..86cdf065c 100644 --- a/providers/dns/mittwald/internal/types.go +++ b/providers/dns/mittwald/internal/types.go @@ -61,14 +61,14 @@ type APIError struct { } func (a APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("%s: %s", a.Type, a.Message)) + _, _ = fmt.Fprintf(msg, "%s: %s", a.Type, a.Message) if len(a.ValidationErrors) > 0 { for _, validationError := range a.ValidationErrors { - msg.WriteString(fmt.Sprintf(" [%s: %s (%s, %s)]", - validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format)) + _, _ = fmt.Fprintf(msg, " [%s: %s (%s, %s)]", + validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format) } } diff --git a/providers/dns/namesurfer/internal/types.go b/providers/dns/namesurfer/internal/types.go index 7b3f08013..d364c1876 100644 --- a/providers/dns/namesurfer/internal/types.go +++ b/providers/dns/namesurfer/internal/types.go @@ -48,24 +48,24 @@ type APIError struct { } func (e *APIError) Error() string { - var msg strings.Builder + msg := new(strings.Builder) - msg.WriteString(fmt.Sprintf("code: %v", e.Code)) + _, _ = fmt.Fprintf(msg, "code: %v", e.Code) if e.Filename != "" { - msg.WriteString(fmt.Sprintf(", filename: %s", e.Filename)) + _, _ = fmt.Fprintf(msg, ", filename: %s", e.Filename) } if e.LineNumber > 0 { - msg.WriteString(fmt.Sprintf(", line: %d", e.LineNumber)) + _, _ = fmt.Fprintf(msg, ", line: %d", e.LineNumber) } if e.Message != "" { - msg.WriteString(fmt.Sprintf(", message: %s", e.Message)) + _, _ = fmt.Fprintf(msg, ", message: %s", e.Message) } if len(e.Detail) > 0 { - msg.WriteString(fmt.Sprintf(", detail: %v", strings.Join(e.Detail, " "))) + _, _ = fmt.Fprintf(msg, ", detail: %v", strings.Join(e.Detail, " ")) } return msg.String() From 84f3be40f0cfa471abae47db3d9e11a54ff1ae34 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 18 Feb 2026 23:26:31 +0100 Subject: [PATCH 82/95] chore: update dependencies (#2860) --- go.mod | 109 ++++++++++++++------------- go.sum | 233 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 174 insertions(+), 168 deletions(-) diff --git a/go.mod b/go.mod index 42e11820b..b8e88428e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( cloud.google.com/go/compute/metadata v0.9.0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 @@ -15,28 +15,28 @@ require ( github.com/Azure/go-autorest/autorest/to v0.4.1 github.com/BurntSushi/toml v1.6.0 github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 - github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 + github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 github.com/alibabacloud-go/tea v1.4.0 github.com/aliyun/credentials-go v1.4.7 - github.com/aws/aws-sdk-go-v2 v1.41.0 - github.com/aws/aws-sdk-go-v2/config v1.32.6 - github.com/aws/aws-sdk-go-v2/credentials v1.19.6 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 - github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.8 + github.com/aws/aws-sdk-go-v2/credentials v1.19.8 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 github.com/aziontech/azionapi-go-sdk v0.144.0 - github.com/baidubce/bce-sdk-go v0.9.256 + github.com/baidubce/bce-sdk-go v0.9.260 github.com/cenkalti/backoff/v5 v5.0.3 github.com/dnsimple/dnsimple-go/v4 v4.0.0 github.com/exoscale/egoscale/v3 v3.1.33 github.com/go-acme/alidns-20150109/v4 v4.7.0 - github.com/go-acme/esa-20240910/v2 v2.44.0 + github.com/go-acme/esa-20240910/v2 v2.48.0 github.com/go-acme/jdcloud-sdk-go v1.64.0 - github.com/go-acme/tencentclouddnspod v1.1.25 - github.com/go-acme/tencentedgdeone v1.1.48 + github.com/go-acme/tencentclouddnspod v1.3.24 + github.com/go-acme/tencentedgdeone v1.3.38 github.com/go-jose/go-jose/v4 v4.1.3 - github.com/go-viper/mapstructure/v2 v2.4.0 + github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/go-cmp v0.7.0 github.com/google/go-querystring v1.2.0 github.com/google/uuid v1.6.0 @@ -44,18 +44,18 @@ require ( github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-version v1.8.0 - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/ldez/grignotin v0.10.1 - github.com/linode/linodego v1.64.0 + github.com/linode/linodego v1.65.0 github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.20 - github.com/miekg/dns v1.1.69 + github.com/miekg/dns v1.1.72 github.com/mimuret/golang-iij-dpf v0.9.1 github.com/namedotcom/go/v4 v4.0.2 - github.com/nrdcg/auroradns v1.1.0 + github.com/nrdcg/auroradns v1.2.0 github.com/nrdcg/bunny-go v0.1.0 github.com/nrdcg/desec v0.11.1 github.com/nrdcg/dnspod-go v0.4.0 @@ -65,8 +65,8 @@ require ( github.com/nrdcg/mailinabox v0.3.0 github.com/nrdcg/namesilo v0.5.0 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 + github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 + github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 github.com/nrdcg/porkbun v0.4.0 github.com/nrdcg/vegadns v0.3.0 github.com/nzdjb/go-metaname v1.0.0 @@ -81,29 +81,29 @@ require ( github.com/selectel/go-selvpcclient/v4 v4.1.0 github.com/softlayer/softlayer-go v1.2.1 github.com/stretchr/testify v1.11.1 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 github.com/transip/gotransip/v6 v6.26.1 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 github.com/urfave/cli/v2 v2.27.7 github.com/vinyldns/go-vinyldns v0.9.17 - github.com/volcengine/volc-sdk-golang v1.0.233 - github.com/vultr/govultr/v3 v3.26.1 - github.com/yandex-cloud/go-genproto v0.43.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.25 - github.com/yandex-cloud/go-sdk/v2 v2.37.0 - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/text v0.32.0 + github.com/volcengine/volc-sdk-golang v1.0.237 + github.com/vultr/govultr/v3 v3.27.0 + github.com/yandex-cloud/go-genproto v0.54.0 + github.com/yandex-cloud/go-sdk/services/dns v0.0.36 + github.com/yandex-cloud/go-sdk/v2 v2.56.0 + golang.org/x/crypto v0.48.0 + golang.org/x/net v0.50.0 + golang.org/x/oauth2 v0.35.0 + golang.org/x/text v0.34.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.259.0 - gopkg.in/ns1/ns1-go.v2 v2.16.0 + google.golang.org/api v0.267.0 + gopkg.in/ns1/ns1-go.v2 v2.17.2 gopkg.in/yaml.v2 v2.4.0 software.sslmate.com/src/go-pkcs12 v0.7.0 ) require ( - cloud.google.com/go/auth v0.18.0 // indirect + cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect @@ -119,22 +119,23 @@ require ( github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -160,8 +161,8 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.16.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -205,23 +206,23 @@ require ( go.mongodb.org/mongo-driver v1.13.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.32.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 889e6b5b5..f5b87c9fe 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= -cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -42,8 +42,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= @@ -121,8 +121,10 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= @@ -169,54 +171,54 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= -github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= -github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs= +github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0= +github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 h1:MQuZZ6Tq1qQabPlkVxrCMdyVl70Ogl4AERZKo+y9Wzo= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10/go.mod h1:U5C3JME1ibKESmpzBAqlRpTYZfVbTqrb5ICJm+sVVd8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQxsd+w6vSzVqpT1FGiwE= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo= github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU= -github.com/baidubce/bce-sdk-go v0.9.256 h1:/6UwBzDp+dRFpKRIb5WsvxfSiG4SLOIOghvagOK/q4Y= -github.com/baidubce/bce-sdk-go v0.9.256/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw= +github.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -239,6 +241,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -315,14 +319,14 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= -github.com/go-acme/esa-20240910/v2 v2.44.0 h1:ACi2uFb7ig4ousFs/YiFBR+aw3A4SHtOxvkMWB2Hbcs= -github.com/go-acme/esa-20240910/v2 v2.44.0/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g= +github.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc= +github.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg= github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs= github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU= -github.com/go-acme/tencentclouddnspod v1.1.25 h1:7H3ZKshkaHzCXfRpAHVB5nvxeDDl2XLeNZfrNHiZj/s= -github.com/go-acme/tencentclouddnspod v1.1.25/go.mod h1:XXfzp0AYV7UAUsHKT6R0KAUJFhqAUXmWGF07Elpa5cE= -github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk= -github.com/go-acme/tencentedgdeone v1.1.48/go.mod h1:mu6tA+bPhlSd+CKUfzRikE0mfxmTlBI6dVTn9LY9dRI= +github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8= +github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0= +github.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ= +github.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -366,8 +370,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= @@ -467,12 +471,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= -github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= @@ -537,8 +541,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 h1:B3W9acgpqu5XsN8v+W8SOTfqn/6n4JsjgoKBsm30HFY= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -612,8 +616,8 @@ github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufp github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY= -github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= +github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k= +github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -649,8 +653,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= @@ -691,8 +695,8 @@ github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1t github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= -github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= +github.com/nrdcg/auroradns v1.2.0 h1:Jg407vTdXZvZKsART9CNWMp8rQOyhBk04q0MsOf0YR4= +github.com/nrdcg/auroradns v1.2.0/go.mod h1:hnByA4Z7MOmV4EPRw5eOmEaNRFavcCIz6kONpNxp9LI= github.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA= github.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A= github.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s= @@ -711,10 +715,10 @@ github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk= +github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU= +github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU= @@ -904,10 +908,10 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.25/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 h1:Rj1WXXNPm9AsPf0PJhWCvlsqfcKPUYdyVnkmEc3O8sI= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= @@ -922,10 +926,10 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI= github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4= -github.com/volcengine/volc-sdk-golang v1.0.233 h1:Hh2pzwu/Wq19rsZgNo3HdpjQB28D/F0+m6EjLVggmhM= -github.com/volcengine/volc-sdk-golang v1.0.233/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw= -github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= +github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4= +github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= +github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8= +github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= @@ -934,12 +938,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw= -github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.25 h1:BcGEuOnwq2X3LS2kvFC6BOdZkOq4Lc7XAYvzap/SJJY= -github.com/yandex-cloud/go-sdk/services/dns v0.0.25/go.mod h1:B4QHijALUHIjRxL3aqmOwDrHYUI2XdeeG4WKItth3jI= -github.com/yandex-cloud/go-sdk/v2 v2.37.0 h1:WvttW6p9xcWag9j+GQv+GJXPggggXGwOlIJNfkWmFWw= -github.com/yandex-cloud/go-sdk/v2 v2.37.0/go.mod h1:Dt4a81enjRsm4xMJyW5E1Y/vaUYwXJvUGRdDLuM2k6I= +github.com/yandex-cloud/go-genproto v0.54.0 h1:LjEwDPBAtF39HvcPQe8I+ImCnFasCPCOVh2b2Sr2eAg= +github.com/yandex-cloud/go-genproto v0.54.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk/services/dns v0.0.36 h1:sD622+baDvJ2ujhCfoFsCH0XeNsaZNW6loRqvRavjtE= +github.com/yandex-cloud/go-sdk/services/dns v0.0.36/go.mod h1:Hh7IKJxULaRzmyM19lQZw+yUDyMM8M3Qrk1LbWqhCkc= +github.com/yandex-cloud/go-sdk/v2 v2.56.0 h1:rihPAZbPbHU/BKTLuT64nU1uhbBrO20HhdlLR3Hyoz0= +github.com/yandex-cloud/go-sdk/v2 v2.56.0/go.mod h1:jzVBQgamNHoiDsmjog2dPZHMXuGZqmxf/epH+Qb7Emc= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= @@ -969,16 +973,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1031,8 +1035,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1076,8 +1080,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1135,16 +1139,16 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1248,8 +1252,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1264,8 +1268,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1284,8 +1288,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1351,8 +1355,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1381,8 +1385,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= -google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= +google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= +google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1421,12 +1425,12 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1475,11 +1479,12 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= +gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.16.0 h1:mUczKFnrCystSV7yIODzVSbENoud3T7DwstmyVZfqg4= -gopkg.in/ns1/ns1-go.v2 v2.16.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs= +gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= From d896c1f0366a5b60112c9a8b87861acacf037417 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 19 Feb 2026 12:25:10 +0100 Subject: [PATCH 83/95] fix: preserve domain order (#2862) --- acme/api/identifier.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/acme/api/identifier.go b/acme/api/identifier.go index 42a8fd391..245ed8515 100644 --- a/acme/api/identifier.go +++ b/acme/api/identifier.go @@ -2,7 +2,6 @@ package api import ( "cmp" - "maps" "net" "slices" @@ -10,7 +9,9 @@ import ( ) func createIdentifiers(domains []string) []acme.Identifier { - uniqIdentifiers := make(map[string]acme.Identifier) + uniqIdentifiers := make(map[string]struct{}) + + var identifiers []acme.Identifier for _, domain := range domains { if _, ok := uniqIdentifiers[domain]; ok { @@ -23,10 +24,12 @@ func createIdentifiers(domains []string) []acme.Identifier { ident.Type = "ip" } - uniqIdentifiers[domain] = ident + identifiers = append(identifiers, ident) + + uniqIdentifiers[domain] = struct{}{} } - return slices.AppendSeq(make([]acme.Identifier, 0, len(uniqIdentifiers)), maps.Values(uniqIdentifiers)) + return identifiers } // compareIdentifiers compares 2 slices of [acme.Identifier]. From 078a1889c87c750f6051a3dd9dc1e5e24e690ec8 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 19 Feb 2026 12:26:53 +0100 Subject: [PATCH 84/95] Add DNS provider for ArtFiles (#2859) --- README.md | 90 +++---- cmd/zz_gen_cmd_dnshelp.go | 22 ++ docs/content/dns/zz_gen_artfiles.md | 69 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/artfiles/artfiles.go | 204 ++++++++++++++++ providers/dns/artfiles/artfiles.toml | 24 ++ providers/dns/artfiles/artfiles_test.go | 228 ++++++++++++++++++ providers/dns/artfiles/internal/client.go | 133 ++++++++++ .../dns/artfiles/internal/client_test.go | 89 +++++++ .../artfiles/internal/fixtures/domains.txt | 3 + .../artfiles/internal/fixtures/get_dns.json | 16 ++ .../artfiles/internal/fixtures/set_dns.json | 4 + .../internal/fixtures/txt_record-multiple.txt | 8 + .../artfiles/internal/fixtures/txt_record.txt | 7 + providers/dns/artfiles/internal/types.go | 109 +++++++++ providers/dns/artfiles/internal/types_test.go | 183 ++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 17 files changed, 1148 insertions(+), 46 deletions(-) create mode 100644 docs/content/dns/zz_gen_artfiles.md create mode 100644 providers/dns/artfiles/artfiles.go create mode 100644 providers/dns/artfiles/artfiles.toml create mode 100644 providers/dns/artfiles/artfiles_test.go create mode 100644 providers/dns/artfiles/internal/client.go create mode 100644 providers/dns/artfiles/internal/client_test.go create mode 100644 providers/dns/artfiles/internal/fixtures/domains.txt create mode 100644 providers/dns/artfiles/internal/fixtures/get_dns.json create mode 100644 providers/dns/artfiles/internal/fixtures/set_dns.json create mode 100644 providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt create mode 100644 providers/dns/artfiles/internal/fixtures/txt_record.txt create mode 100644 providers/dns/artfiles/internal/types.go create mode 100644 providers/dns/artfiles/internal/types_test.go diff --git a/README.md b/README.md index 105ea53aa..7e015e70f 100644 --- a/README.md +++ b/README.md @@ -73,228 +73,228 @@ If your DNS provider is not supported, please open an [issue](https://github.com Amazon Route 53 Anexia CloudDNS + ArtFiles ArvanCloud - Aurora DNS + Aurora DNS Autodns Axelname Azion - Azure (deprecated) + Azure (deprecated) Azure DNS Baidu Cloud Beget.com - Binary Lane + Binary Lane Bindman Bluecat Bluecat v2 - BookMyName + BookMyName Brandit (deprecated) Bunny Checkdomain - Civo + Civo Cloud.ru CloudDNS Cloudflare - ClouDNS + ClouDNS CloudXNS (Deprecated) ConoHa v2 ConoHa v3 - Constellix + Constellix Core-Networks CPanel/WHM DDnss (DynDNS Service) - Derak Cloud + Derak Cloud deSEC.io Designate DNSaaS for Openstack Digital Ocean - DirectAdmin + DirectAdmin DNS Made Easy DNSExit dnsHome.de - DNSimple + DNSimple DNSPod (deprecated) Domain Offensive (do.de) Domeneshop - DreamHost + DreamHost Duck DNS Dyn DynDnsFree.de - Dynu + Dynu EasyDNS EdgeCenter Efficient IP - Epik + Epik Exoscale External program F5 XC - freemyip.com + freemyip.com FusionLayer NameSurfer G-Core Gandi - Gandi Live DNS (v5) + Gandi Live DNS (v5) Gigahost.no Glesys Go Daddy - Google Cloud + Google Cloud Google Domains Gravity Hetzner - Hosting.de + Hosting.de Hosting.nl Hostinger Hosttech - HTTP request + HTTP request http.net Huawei Cloud Hurricane Electric DNS - HyperOne + HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox - Infomaniak + Infomaniak Internet Initiative Japan Internet.bs INWX - Ionos + Ionos Ionos Cloud IPv64 ISPConfig 3 - ISPConfig 3 - Dynamic DNS (DDNS) Module + ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) JD Cloud Joker - Joohoi's ACME-DNS + Joohoi's ACME-DNS KeyHelp Liara Lima-City - Linode (v4) + Linode (v4) Liquid Web Loopia LuaDNS - Mail-in-a-Box + Mail-in-a-Box ManageEngine CloudDNS Manual Metaname - Metaregistrar + Metaregistrar mijn.host Mittwald myaddr.{tools,dev,io} - MyDNS.jp + MyDNS.jp MythicBeasts Name.com Namecheap - Namesilo + Namesilo NearlyFreeSpeech.NET Neodigit Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Syse + Syse Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TodayNIC/时代互联 TransIP UKFast SafeDNS - Ultradns + Ultradns United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS Virtualname - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index cdee65371..9e83a7b25 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -19,6 +19,7 @@ func allDNSCodes() string { "allinkl", "alwaysdata", "anexia", + "artfiles", "arvancloud", "auroradns", "autodns", @@ -358,6 +359,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/anexia`) + case "artfiles": + // generated from: providers/dns/artfiles/artfiles.toml + ew.writeln(`Configuration for ArtFiles.`) + ew.writeln(`Code: 'artfiles'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ARTFILES_PASSWORD": API password`) + ew.writeln(` - "ARTFILES_USERNAME": API username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ARTFILES_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "ARTFILES_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ARTFILES_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) + ew.writeln(` - "ARTFILES_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/artfiles`) + case "arvancloud": // generated from: providers/dns/arvancloud/arvancloud.toml ew.writeln(`Configuration for ArvanCloud.`) diff --git a/docs/content/dns/zz_gen_artfiles.md b/docs/content/dns/zz_gen_artfiles.md new file mode 100644 index 000000000..15ac2d964 --- /dev/null +++ b/docs/content/dns/zz_gen_artfiles.md @@ -0,0 +1,69 @@ +--- +title: "ArtFiles" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: artfiles +dnsprovider: + since: "v4.32.0" + code: "artfiles" + url: "https://www.artfiles.de/extras/domains/" +--- + + + + + + +Configuration for [ArtFiles](https://www.artfiles.de/extras/domains/). + + + + +- Code: `artfiles` +- Since: v4.32.0 + + +Here is an example bash command using the ArtFiles provider: + +```bash +ARTFILES_USERNAME="xxx" \ +ARTFILES_PASSWORD="yyy" \ +lego --dns artfiles -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ARTFILES_PASSWORD` | API password | +| `ARTFILES_USERNAME` | API 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 | +|--------------------------------|-------------| +| `ARTFILES_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `ARTFILES_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ARTFILES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | +| `ARTFILES_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://support.artfiles.de/DCP-API#dns) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 759b8e84f..c6d845bf2 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, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/artfiles/artfiles.go b/providers/dns/artfiles/artfiles.go new file mode 100644 index 000000000..c918d77f6 --- /dev/null +++ b/providers/dns/artfiles/artfiles.go @@ -0,0 +1,204 @@ +// Package artfiles implements a DNS provider for solving the DNS-01 challenge using ArtFiles. +package artfiles + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "slices" + "time" + + "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/artfiles/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "ARTFILES_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + 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 { + Username string + Password string + + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for ArtFiles. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("artfiles: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ArtFiles. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("artfiles: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.Username, config.Password) + if err != nil { + return nil, fmt.Errorf("artfiles: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("artfiles: %w", err) + } + + records, err := d.client.GetRecords(ctx, zone) + if err != nil { + return fmt.Errorf("artfiles: get records: %w", err) + } + + rv := internal.RecordValue{} + + if len(records["TXT"]) > 0 { + var raw string + + err = json.Unmarshal(records["TXT"], &raw) + if err != nil { + return fmt.Errorf("artfiles: unmarshal TXT records: %w", err) + } + + rv = internal.ParseRecordValue(raw) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("artfiles: %w", err) + } + + rv.Add(subDomain, info.Value) + + err = d.client.SetRecords(ctx, zone, "TXT", rv) + if err != nil { + return fmt.Errorf("artfiles: set TXT records: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("artfiles: %w", err) + } + + records, err := d.client.GetRecords(ctx, zone) + if err != nil { + return fmt.Errorf("artfiles: get records: %w", err) + } + + var raw string + + err = json.Unmarshal(records["TXT"], &raw) + if err != nil { + return fmt.Errorf("artfiles: unmarshal TXT records: %w", err) + } + + rv := internal.ParseRecordValue(raw) + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("artfiles: %w", err) + } + + rv.RemoveValue(subDomain, info.Value) + + err = d.client.SetRecords(ctx, zone, "TXT", rv) + if err != nil { + return fmt.Errorf("artfiles: set TXT records: %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.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { + domains, err := d.client.GetDomains(ctx) + if err != nil { + return "", fmt.Errorf("artfiles: get domains: %w", err) + } + + var zone string + + for s := range dns01.UnFqdnDomainsSeq(fqdn) { + if slices.Contains(domains, s) { + zone = s + } + } + + if zone == "" { + return "", fmt.Errorf("artfiles: could not find the zone for domain %q", fqdn) + } + + return zone, nil +} diff --git a/providers/dns/artfiles/artfiles.toml b/providers/dns/artfiles/artfiles.toml new file mode 100644 index 000000000..00ff12342 --- /dev/null +++ b/providers/dns/artfiles/artfiles.toml @@ -0,0 +1,24 @@ +Name = "ArtFiles" +Description = '''''' +URL = "https://www.artfiles.de/extras/domains/" +Code = "artfiles" +Since = "v4.32.0" + +Example = ''' +ARTFILES_USERNAME="xxx" \ +ARTFILES_PASSWORD="yyy" \ +lego --dns artfiles -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ARTFILES_USERNAME = "API username" + ARTFILES_PASSWORD = "API password" + [Configuration.Additional] + ARTFILES_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ARTFILES_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" + ARTFILES_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ARTFILES_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://support.artfiles.de/DCP-API#dns" diff --git a/providers/dns/artfiles/artfiles_test.go b/providers/dns/artfiles/artfiles_test.go new file mode 100644 index 000000000..42490f10d --- /dev/null +++ b/providers/dns/artfiles/artfiles_test.go @@ -0,0 +1,228 @@ +package artfiles + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "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: "artfiles: some credentials information are missing: ARTFILES_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "", + }, + expected: "artfiles: some credentials information are missing: ARTFILES_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "artfiles: some credentials information are missing: ARTFILES_USERNAME,ARTFILES_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.config) + require.NotNil(t, p.client) + } 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: "artfiles: credentials missing", + }, + { + desc: "missing Example", + username: "user", + expected: "artfiles: credentials missing", + }, + { + desc: "missing credentials", + expected: "artfiles: 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.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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Username = "user" + config.Password = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithBasicAuth("user", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/get_domains.html", + servermock.ResponseFromInternal("domains.txt"), + ). + Route("GET /dns/get_dns.html", + servermock.ResponseFromInternal("get_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "example.com"), + ). + Route("POST /dns/set_dns.html", + servermock.ResponseFromInternal("set_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("TXT", `@ "v=spf1 a mx ~all" +_acme-challenge "TheAcmeChallenge" +_acme-challenge "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" +_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" +_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" +_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" +selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" +selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`). + With("domain", "example.com"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /domain/get_domains.html", + servermock.ResponseFromInternal("domains.txt"), + ). + Route("GET /dns/get_dns.html", + servermock.ResponseFromInternal("get_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "example.com"), + ). + Route("POST /dns/set_dns.html", + servermock.ResponseFromInternal("set_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("TXT", `@ "v=spf1 a mx ~all" +_acme-challenge "TheAcmeChallenge" +_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" +_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" +_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" +selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" +selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`). + With("domain", "example.com"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/artfiles/internal/client.go b/providers/dns/artfiles/internal/client.go new file mode 100644 index 000000000..61b350511 --- /dev/null +++ b/providers/dns/artfiles/internal/client.go @@ -0,0 +1,133 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://dcp.c.artfiles.de/api/" + +// Client the ArtFiles API client. +type Client struct { + username string + password string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(username, password string) (*Client, error) { + if username == "" || password == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + username: username, + password: password, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) GetDomains(ctx context.Context) ([]string, error) { + endpoint := c.BaseURL.JoinPath("domain", "get_domains.html") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + raw, err := c.do(req) + if err != nil { + return nil, err + } + + return parseDomains(string(raw)) +} + +func (c *Client) GetRecords(ctx context.Context, domain string) (map[string]json.RawMessage, error) { + endpoint := c.BaseURL.JoinPath("dns", "get_dns.html") + + query := endpoint.Query() + query.Set("domain", domain) + + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + raw, err := c.do(req) + if err != nil { + return nil, err + } + + var result Records + + err = json.Unmarshal(raw, &result) + if err != nil { + return nil, errutils.NewUnmarshalError(req, http.StatusOK, raw, err) + } + + return result.Data, nil +} + +func (c *Client) SetRecords(ctx context.Context, domain, rType string, value RecordValue) error { + endpoint := c.BaseURL.JoinPath("dns", "set_dns.html") + + query := endpoint.Query() + query.Set("domain", domain) + query.Set(rType, value.String()) + + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + _, err = c.do(req) + + return err +} + +func (c *Client) do(req *http.Request) ([]byte, error) { + useragent.SetHeader(req.Header) + + req.SetBasicAuth(c.username, c.password) + + if req.Method == http.MethodPost { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + if resp.StatusCode/100 != 2 { + return nil, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return raw, nil +} diff --git a/providers/dns/artfiles/internal/client_test.go b/providers/dns/artfiles/internal/client_test.go new file mode 100644 index 000000000..cc76f06f5 --- /dev/null +++ b/providers/dns/artfiles/internal/client_test.go @@ -0,0 +1,89 @@ +package internal + +import ( + "encoding/json" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("user", "secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithBasicAuth("user", "secret"), + ) +} + +func TestClient_GetDomains(t *testing.T) { + client := mockBuilder(). + Route("GET /domain/get_domains.html", + servermock.ResponseFromFixture("domains.txt"), + ). + Build(t) + + zones, err := client.GetDomains(t.Context()) + require.NoError(t, err) + + expected := []string{"example.com", "example.org", "example.net"} + + assert.Equal(t, expected, zones) +} + +func TestClient_GetRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/get_dns.html", + servermock.ResponseFromFixture("get_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("domain", "example.com"), + ). + Build(t) + + records, err := client.GetRecords(t.Context(), "example.com") + require.NoError(t, err) + + expected := map[string]json.RawMessage{ + "A": json.RawMessage(strconv.Quote("sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4")), + "AAAA": json.RawMessage(strconv.Quote("")), + "CAA": json.RawMessage(strconv.Quote("@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"")), + "CName": json.RawMessage(strconv.Quote("some cname.to.example.tld.")), + "MX": json.RawMessage(strconv.Quote("10 mail.example.tld.")), + "SRV": json.RawMessage(strconv.Quote("_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .")), + "TLSA": json.RawMessage(strconv.Quote("_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2")), + "TXT": json.RawMessage(strconv.Quote("_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"")), + "TTL": json.RawMessage("3600"), + "comment": json.RawMessage(strconv.Quote("TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php")), + "nameserver": json.RawMessage(strconv.Quote("auth1.artfiles.de.\nauth2.artfiles.de.")), + } + + assert.Equal(t, expected, records) +} + +func TestClient_SetRecords(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/set_dns.html", + servermock.ResponseFromFixture("set_dns.json"), + servermock.CheckQueryParameter().Strict(). + With("TXT", "a b\nc \"d\""). + With("domain", "example.com"), + ). + Build(t) + + err := client.SetRecords(t.Context(), "example.com", "TXT", RecordValue{"c": []string{`"d"`}, "a": []string{"b"}}) + require.NoError(t, err) +} diff --git a/providers/dns/artfiles/internal/fixtures/domains.txt b/providers/dns/artfiles/internal/fixtures/domains.txt new file mode 100644 index 000000000..b8a1247d2 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/domains.txt @@ -0,0 +1,3 @@ +example.com normal 2026-10-01 2017-09-18 163477 +example.org normal 2026-08-01 2016-07-07 156216 +example.net normal 2026-07-01 2017-06-06 162462 diff --git a/providers/dns/artfiles/internal/fixtures/get_dns.json b/providers/dns/artfiles/internal/fixtures/get_dns.json new file mode 100644 index 000000000..fa672e0e1 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/get_dns.json @@ -0,0 +1,16 @@ +{ + "data": { + "SRV": "_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .", + "AAAA": "", + "MX": "10 mail.example.tld.", + "CAA": "@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"", + "TTL": 3600, + "comment": "TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php", + "TXT": "_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"", + "A": "sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4", + "nameserver": "auth1.artfiles.de.\nauth2.artfiles.de.", + "CName": "some cname.to.example.tld.", + "TLSA": "_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2" + }, + "status": "OK" +} diff --git a/providers/dns/artfiles/internal/fixtures/set_dns.json b/providers/dns/artfiles/internal/fixtures/set_dns.json new file mode 100644 index 000000000..7cacb33e5 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/set_dns.json @@ -0,0 +1,4 @@ +{ + "status": "OK", + "error": "" +} diff --git a/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt b/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt new file mode 100644 index 000000000..461489c77 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt @@ -0,0 +1,8 @@ +_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" +_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" +_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" +@ "v=spf1 a mx ~all" +selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" +selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff" +_acme-challenge "xxx" +_acme-challenge "yyy" diff --git a/providers/dns/artfiles/internal/fixtures/txt_record.txt b/providers/dns/artfiles/internal/fixtures/txt_record.txt new file mode 100644 index 000000000..5a6259b14 --- /dev/null +++ b/providers/dns/artfiles/internal/fixtures/txt_record.txt @@ -0,0 +1,7 @@ +_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" +_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" +_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" +@ "v=spf1 a mx ~all" +selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" +selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff" +_acme-challenge "TheAcmeChallenge" diff --git a/providers/dns/artfiles/internal/types.go b/providers/dns/artfiles/internal/types.go new file mode 100644 index 000000000..c70ab34da --- /dev/null +++ b/providers/dns/artfiles/internal/types.go @@ -0,0 +1,109 @@ +package internal + +import ( + "encoding/csv" + "encoding/json" + "errors" + "io" + "maps" + "slices" + "strconv" + "strings" + "unicode" +) + +type Records struct { + Data map[string]json.RawMessage `json:"data"` + Status string `json:"status"` +} + +type RecordValue map[string][]string + +func (r RecordValue) Set(key, value string) { + r[key] = []string{strconv.Quote(value)} +} + +func (r RecordValue) Add(key, value string) { + r[key] = append(r[key], strconv.Quote(value)) +} + +func (r RecordValue) Delete(key string) { + delete(r, key) +} + +func (r RecordValue) RemoveValue(key, value string) { + if len(r[key]) == 0 { + return + } + + quotedValue := strconv.Quote(value) + + var data []string + + for _, s := range r[key] { + if s != quotedValue { + data = append(data, s) + } + } + + r[key] = data + + if len(r[key]) == 0 { + r.Delete(key) + } +} + +func (r RecordValue) String() string { + var parts []string + + for _, key := range slices.Sorted(maps.Keys(r)) { + for _, s := range r[key] { + parts = append(parts, key+" "+s) + } + } + + return strings.Join(parts, "\n") +} + +func ParseRecordValue(lines string) RecordValue { + data := make(RecordValue) + + for line := range strings.Lines(lines) { + line = strings.TrimSpace(line) + + idx := strings.IndexFunc(line, unicode.IsSpace) + + data[line[:idx]] = append(data[line[:idx]], line[idx+1:]) + } + + return data +} + +func parseDomains(input string) ([]string, error) { + reader := csv.NewReader(strings.NewReader(input)) + reader.Comma = '\t' + reader.TrimLeadingSpace = true + reader.LazyQuotes = true + + var data []string + + for { + record, err := reader.Read() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, err + } + + if len(record) < 1 { + // Malformed line + continue + } + + data = append(data, record[0]) + } + + return data, nil +} diff --git a/providers/dns/artfiles/internal/types_test.go b/providers/dns/artfiles/internal/types_test.go new file mode 100644 index 000000000..3b219f39f --- /dev/null +++ b/providers/dns/artfiles/internal/types_test.go @@ -0,0 +1,183 @@ +package internal + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecordValue_Set(t *testing.T) { + rv := make(RecordValue) + + rv.Set("a", "1") + rv.Set("b", "2") + rv.Set("b", "3") + + assert.Equal(t, "a \"1\"\nb \"3\"", rv.String()) +} + +func TestRecordValue_Add(t *testing.T) { + rv := make(RecordValue) + + rv.Add("a", "1") + rv.Add("b", "2") + rv.Add("b", "3") + + assert.Equal(t, "a \"1\"\nb \"2\"\nb \"3\"", rv.String()) +} + +func TestRecordValue_Delete(t *testing.T) { + rv := make(RecordValue) + + rv.Set("a", "1") + rv.Add("b", "2") + + rv.Delete("b") + + assert.Equal(t, "a \"1\"", rv.String()) +} + +func TestRecordValue_RemoveValue(t *testing.T) { + testCases := []struct { + desc string + data map[string][]string + toRemove map[string][]string + expected string + }{ + { + desc: "remove the only value", + data: map[string][]string{ + "a": {"1"}, + }, + toRemove: map[string][]string{ + "a": {"1"}, + }, + expected: ``, + }, + { + desc: "remove value in the middle", + data: map[string][]string{ + "a": {"1", "2", "3"}, + }, + toRemove: map[string][]string{ + "a": {"2"}, + }, + expected: "a \"1\"\na \"3\"", + }, + { + desc: "remove value at the beginning", + data: map[string][]string{ + "a": {"1", "2", "3"}, + }, + toRemove: map[string][]string{ + "a": {"1"}, + }, + expected: "a \"2\"\na \"3\"", + }, + { + desc: "remove value at the end", + data: map[string][]string{ + "a": {"1", "2", "3"}, + }, + toRemove: map[string][]string{ + "a": {"3"}, + }, + expected: "a \"1\"\na \"2\"", + }, + { + desc: "remove all (delete)", + data: map[string][]string{ + "a": {"1", "2", "3"}, + }, + toRemove: map[string][]string{ + "a": {"1", "2", "3"}, + }, + expected: ``, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rv := make(RecordValue) + + for k, values := range test.data { + for _, v := range values { + rv.Add(k, v) + } + } + + for k, values := range test.toRemove { + for _, v := range values { + rv.RemoveValue(k, v) + } + } + + assert.Equal(t, test.expected, rv.String()) + }) + } +} + +func TestParseRecordValue(t *testing.T) { + testCases := []struct { + desc string + filename string + expected RecordValue + }{ + { + desc: "simple", + filename: "txt_record.txt", + expected: RecordValue{ + "@": []string{"\"v=spf1 a mx ~all\""}, + "_acme-challenge": []string{"\"TheAcmeChallenge\""}, + "_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""}, + "_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""}, + "_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""}, + "selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""}, + "selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""}, + }, + }, + { + desc: "multiple values with the same key", + filename: "txt_record-multiple.txt", + expected: RecordValue{ + "@": []string{"\"v=spf1 a mx ~all\""}, + "_acme-challenge": []string{"\"xxx\"", "\"yyy\""}, + "_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""}, + "_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""}, + "_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""}, + "selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""}, + "selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + file, err := os.ReadFile(filepath.Join("fixtures", test.filename)) + require.NoError(t, err) + + data := ParseRecordValue(string(file)) + + assert.Equal(t, test.expected, data) + }) + } +} + +func Test_parseDomains(t *testing.T) { + file, err := os.ReadFile(filepath.FromSlash("./fixtures/domains.txt")) + require.NoError(t, err) + + domains, err := parseDomains(string(file)) + require.NoError(t, err) + + expected := []string{"example.com", "example.org", "example.net"} + + assert.Equal(t, expected, domains) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 10fda2df1..24474cf2f 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -13,6 +13,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/allinkl" "github.com/go-acme/lego/v4/providers/dns/alwaysdata" "github.com/go-acme/lego/v4/providers/dns/anexia" + "github.com/go-acme/lego/v4/providers/dns/artfiles" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/go-acme/lego/v4/providers/dns/auroradns" "github.com/go-acme/lego/v4/providers/dns/autodns" @@ -211,6 +212,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return alwaysdata.NewDNSProvider() case "anexia": return anexia.NewDNSProvider() + case "artfiles": + return artfiles.NewDNSProvider() case "arvancloud": return arvancloud.NewDNSProvider() case "auroradns": From dd1ea80c08bb2a3551590f64ca40fc1fb2a7eb21 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 19 Feb 2026 12:52:04 +0100 Subject: [PATCH 85/95] Add DNS provider for Leaseweb (#2856) --- README.md | 44 ++-- cmd/zz_gen_cmd_dnshelp.go | 21 ++ docs/content/dns/zz_gen_leaseweb.md | 67 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/leaseweb/internal/client.go | 216 ++++++++++++++++++ .../dns/leaseweb/internal/client_test.go | 149 ++++++++++++ .../createResourceRecordSet-request.json | 8 + .../fixtures/createResourceRecordSet.json | 17 ++ .../leaseweb/internal/fixtures/error_400.json | 6 + .../leaseweb/internal/fixtures/error_401.json | 5 + .../leaseweb/internal/fixtures/error_404.json | 5 + .../fixtures/getResourceRecordSet.json | 18 ++ .../fixtures/getResourceRecordSet2.json | 17 ++ .../updateResourceRecordSet-request.json | 8 + .../updateResourceRecordSet-request2.json | 6 + .../fixtures/updateResourceRecordSet.json | 19 ++ providers/dns/leaseweb/internal/types.go | 35 +++ providers/dns/leaseweb/leaseweb.go | 187 +++++++++++++++ providers/dns/leaseweb/leaseweb.toml | 22 ++ providers/dns/leaseweb/leaseweb_test.go | 204 +++++++++++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 21 files changed, 1036 insertions(+), 23 deletions(-) create mode 100644 docs/content/dns/zz_gen_leaseweb.md create mode 100644 providers/dns/leaseweb/internal/client.go create mode 100644 providers/dns/leaseweb/internal/client_test.go create mode 100644 providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json create mode 100644 providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json create mode 100644 providers/dns/leaseweb/internal/fixtures/error_400.json create mode 100644 providers/dns/leaseweb/internal/fixtures/error_401.json create mode 100644 providers/dns/leaseweb/internal/fixtures/error_404.json create mode 100644 providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json create mode 100644 providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json create mode 100644 providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json create mode 100644 providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json create mode 100644 providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json create mode 100644 providers/dns/leaseweb/internal/types.go create mode 100644 providers/dns/leaseweb/leaseweb.go create mode 100644 providers/dns/leaseweb/leaseweb.toml create mode 100644 providers/dns/leaseweb/leaseweb_test.go diff --git a/README.md b/README.md index 7e015e70f..9925979bd 100644 --- a/README.md +++ b/README.md @@ -188,113 +188,113 @@ If your DNS provider is not supported, please open an [issue](https://github.com Joohoi's ACME-DNS KeyHelp + Leaseweb Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Neodigit - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Syse Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联 TransIP - UKFast SafeDNS + UKFast SafeDNS Ultradns United-Domains Variomedia - VegaDNS + VegaDNS Vercel Versio.[nl|eu|uk] VinylDNS - Virtualname + Virtualname VK Cloud Volcano Engine/火山引擎 Vscale - Vultr + Vultr webnames.ca webnames.ru Websupport - WEDOS + WEDOS West.cn/西部数码 Yandex 360 Yandex Cloud - Yandex PDD + Yandex PDD Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 9e83a7b25..161729c79 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -112,6 +112,7 @@ func allDNSCodes() string { "jdcloud", "joker", "keyhelp", + "leaseweb", "liara", "lightsail", "limacity", @@ -2358,6 +2359,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/keyhelp`) + case "leaseweb": + // generated from: providers/dns/leaseweb/leaseweb.toml + ew.writeln(`Configuration for Leaseweb.`) + ew.writeln(`Code: 'leaseweb'`) + ew.writeln(`Since: 'v4.32.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "LEASEWEB_API_KEY": API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "LEASEWEB_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "LEASEWEB_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "LEASEWEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "LEASEWEB_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/leaseweb`) + case "liara": // generated from: providers/dns/liara/liara.toml ew.writeln(`Configuration for Liara.`) diff --git a/docs/content/dns/zz_gen_leaseweb.md b/docs/content/dns/zz_gen_leaseweb.md new file mode 100644 index 000000000..13ded490a --- /dev/null +++ b/docs/content/dns/zz_gen_leaseweb.md @@ -0,0 +1,67 @@ +--- +title: "Leaseweb" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: leaseweb +dnsprovider: + since: "v4.32.0" + code: "leaseweb" + url: "https://www.leaseweb.com/en/" +--- + + + + + + +Configuration for [Leaseweb](https://www.leaseweb.com/en/). + + + + +- Code: `leaseweb` +- Since: v4.32.0 + + +Here is an example bash command using the Leaseweb provider: + +```bash +LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns leaseweb -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `LEASEWEB_API_KEY` | API key | + +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 | +|--------------------------------|-------------| +| `LEASEWEB_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `LEASEWEB_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `LEASEWEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `LEASEWEB_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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://developer.leaseweb.com/docs/#tag/DNS) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index c6d845bf2..925ef0b21 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/leaseweb/internal/client.go b/providers/dns/leaseweb/internal/client.go new file mode 100644 index 000000000..01619d49b --- /dev/null +++ b/providers/dns/leaseweb/internal/client.go @@ -0,0 +1,216 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.leaseweb.com/hosting/v2" + +const AuthHeader = "X-LSW-Auth" + +// Client the Leaseweb API client. +type Client struct { + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiKey string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// CreateRRSet creates a resource record set. +// https://developer.leaseweb.com/docs/#tag/DNS/operation/createResourceRecordSet +func (c *Client) CreateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) { + endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, rrset) + if err != nil { + return nil, err + } + + result := &RRSet{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// GetRRSet gets a resource record set. +// https://developer.leaseweb.com/docs/#tag/DNS/operation/getResourceRecordSet +func (c *Client) GetRRSet(ctx context.Context, domainName, name, rType string) (*RRSet, error) { + endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := &RRSet{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// UpdateRRSet updates a resource record set. +// https://developer.leaseweb.com/docs/#tag/DNS/operation/updateResourceRecordSet +func (c *Client) UpdateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) { + endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", rrset.Name, rrset.Type) + + // Reset values that are not allowed to be updated. + rrset.Name = "" + rrset.Type = "" + rrset.Editable = false + + req, err := newJSONRequest(ctx, http.MethodPut, endpoint, rrset) + if err != nil { + return nil, err + } + + result := &RRSet{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteRRSet deletes a resource record set. +// https://developer.leaseweb.com/docs/#tag/DNS/operation/deleteResourceRecordSet +func (c *Client) DeleteRRSet(ctx context.Context, domainName, name, rType string) error { + endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Add(AuthHeader, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return &NotFoundError{APIError{ + CorrelationID: resp.Header.Get("Correlation-Id"), + ErrorCode: strconv.Itoa(http.StatusNotFound), + ErrorMessage: string(raw), + }} + } + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if errAPI.ErrorCode == strconv.Itoa(http.StatusNotFound) { + return &NotFoundError{APIError: errAPI} + } + + return &errAPI +} + +// TTLRounder rounds the given TTL in seconds to the next accepted value. +// Accepted TTL values are: 60, 300, 1800, 3600, 14400, 28800, 43200, 86400. +func TTLRounder(ttl int) int { + for _, validTTL := range []int{60, 300, 1800, 3600, 14400, 28800, 43200, 86400} { + if ttl <= validTTL { + return validTTL + } + } + + return 3600 +} diff --git a/providers/dns/leaseweb/internal/client_test.go b/providers/dns/leaseweb/internal/client_test.go new file mode 100644 index 000000000..5762aad4b --- /dev/null +++ b/providers/dns/leaseweb/internal/client_test.go @@ -0,0 +1,149 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(AuthHeader, "secret"), + ) +} + +func TestClient_CreateRRSet(t *testing.T) { + client := mockBuilder(). + Route("POST /domains/example.com/resourceRecordSets", + servermock.ResponseFromFixture("createResourceRecordSet.json"), + servermock.CheckRequestJSONBodyFromFixture("createResourceRecordSet-request.json"), + ). + Build(t) + + rrset := RRSet{ + Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, + Name: "_acme-challenge.example.com.", + TTL: 300, + Type: "TXT", + } + + result, err := client.CreateRRSet(t.Context(), "example.com", rrset) + require.NoError(t, err) + + expected := &RRSet{ + Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, + Name: "_acme-challenge.example.com.", + Editable: true, + TTL: 300, + Type: "TXT", + } + + assert.Equal(t, expected, result) +} + +func TestClient_GetRRSet(t *testing.T) { + client := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromFixture("getResourceRecordSet.json"), + ). + Build(t) + + result, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") + require.NoError(t, err) + + expected := &RRSet{ + Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"}, + Name: "_acme-challenge.example.com.", + Editable: true, + TTL: 3600, + Type: "TXT", + } + + assert.Equal(t, expected, result) +} + +func TestClient_GetRRSet_error_404(t *testing.T) { + client := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromFixture("error_404.json"). + WithStatusCode(http.StatusNotFound), + ). + Build(t) + + _, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") + require.EqualError(t, err, "404: Resource not found (289346a1-3eaf-4da4-b707-62ef12eb08be)") + + target := &NotFoundError{} + require.ErrorAs(t, err, &target) +} + +func TestClient_UpdateRRSet(t *testing.T) { + client := mockBuilder(). + Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromFixture("updateResourceRecordSet.json"), + servermock.CheckRequestJSONBodyFromFixture("updateResourceRecordSet-request.json"), + ). + Build(t) + + rrset := RRSet{ + Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, + Name: "_acme-challenge.example.com.", + TTL: 3600, + Type: "TXT", + } + + result, err := client.UpdateRRSet(t.Context(), "example.com", rrset) + require.NoError(t, err) + + expected := &RRSet{ + Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, + Name: "_acme-challenge.example.com.", + Editable: true, + TTL: 3600, + Type: "TXT", + } + + assert.Equal(t, expected, result) +} + +func TestClient_DeleteRRSet(t *testing.T) { + client := mockBuilder(). + Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + ). + Build(t) + + err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") + require.NoError(t, err) +} + +func TestClient_DeleteRRSet_error(t *testing.T) { + client := mockBuilder(). + Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromFixture("error_401.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") + require.EqualError(t, err, "401: You are not authorized to view this resource. (289346a1-3eaf-4da4-b707-62ef12eb08be)") +} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json new file mode 100644 index 000000000..af53fcf04 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json @@ -0,0 +1,8 @@ +{ + "content": [ + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "name": "_acme-challenge.example.com.", + "ttl": 300, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json new file mode 100644 index 000000000..8ca040d63 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json @@ -0,0 +1,17 @@ +{ + "_links": { + "self": { + "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" + }, + "collection": { + "href": "/domains/example.com/resourceRecordSets" + } + }, + "content": [ + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "editable": true, + "name": "_acme-challenge.example.com.", + "ttl": 300, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_400.json b/providers/dns/leaseweb/internal/fixtures/error_400.json new file mode 100644 index 000000000..1a980b6bb --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_400.json @@ -0,0 +1,6 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "400", + "errorDetails": {}, + "errorMessage": "The API could not interpret your request correctly." +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_401.json b/providers/dns/leaseweb/internal/fixtures/error_401.json new file mode 100644 index 000000000..47d8a311d --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_401.json @@ -0,0 +1,5 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "401", + "errorMessage": "You are not authorized to view this resource." +} diff --git a/providers/dns/leaseweb/internal/fixtures/error_404.json b/providers/dns/leaseweb/internal/fixtures/error_404.json new file mode 100644 index 000000000..1deaf5606 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/error_404.json @@ -0,0 +1,5 @@ +{ + "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", + "errorCode": "404", + "errorMessage": "Resource not found" +} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json new file mode 100644 index 000000000..fd48f60c6 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json @@ -0,0 +1,18 @@ +{ + "_links": { + "self": { + "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" + }, + "collection": { + "href": "/domains/example.com/resourceRecordSets" + } + }, + "content": [ + "foo", + "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" + ], + "editable": true, + "name": "_acme-challenge.example.com.", + "ttl": 3600, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json new file mode 100644 index 000000000..abf3fb4c3 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json @@ -0,0 +1,17 @@ +{ + "_links": { + "self": { + "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" + }, + "collection": { + "href": "/domains/example.com/resourceRecordSets" + } + }, + "content": [ + "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" + ], + "editable": true, + "name": "_acme-challenge.example.com.", + "ttl": 3600, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json new file mode 100644 index 000000000..e781958c8 --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json @@ -0,0 +1,8 @@ +{ + "content": [ + "foo", + "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "ttl": 3600 +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json new file mode 100644 index 000000000..0acc314de --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json @@ -0,0 +1,6 @@ +{ + "content": [ + "foo" + ], + "ttl": 3600 +} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json new file mode 100644 index 000000000..2b877982c --- /dev/null +++ b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json @@ -0,0 +1,19 @@ +{ + "_links": { + "self": { + "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" + }, + "collection": { + "href": "/domains/example.com/resourceRecordSets" + } + }, + "content": [ + "foo", + "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", + "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + ], + "editable": true, + "name": "_acme-challenge.example.com.", + "ttl": 3600, + "type": "TXT" +} diff --git a/providers/dns/leaseweb/internal/types.go b/providers/dns/leaseweb/internal/types.go new file mode 100644 index 000000000..7a4547584 --- /dev/null +++ b/providers/dns/leaseweb/internal/types.go @@ -0,0 +1,35 @@ +package internal + +import ( + "encoding/json" + "fmt" +) + +type NotFoundError struct { + APIError +} + +type APIError struct { + CorrelationID string `json:"correlationId,omitempty"` + ErrorCode string `json:"errorCode,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + ErrorDetails json.RawMessage `json:"errorDetails,omitempty"` +} + +func (a *APIError) Error() string { + msg := fmt.Sprintf("%s: %s (%s)", a.ErrorCode, a.ErrorMessage, a.CorrelationID) + + if len(a.ErrorDetails) > 0 { + msg += fmt.Sprintf(": %s", string(a.ErrorDetails)) + } + + return msg +} + +type RRSet struct { + Content []string `json:"content,omitempty"` + Name string `json:"name,omitempty"` + Editable bool `json:"editable,omitempty"` + TTL int `json:"ttl,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/providers/dns/leaseweb/leaseweb.go b/providers/dns/leaseweb/leaseweb.go new file mode 100644 index 000000000..fafaf1c4d --- /dev/null +++ b/providers/dns/leaseweb/leaseweb.go @@ -0,0 +1,187 @@ +// Package leaseweb implements a DNS provider for solving the DNS-01 challenge using Leaseweb. +package leaseweb + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/leaseweb/internal" +) + +// Environment variables names. +const ( + envNamespace = "LEASEWEB_" + + EnvAPIKey = envNamespace + "API_KEY" + + 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 { + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Leaseweb. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("leaseweb: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Leaseweb. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("leaseweb: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey) + if err != nil { + return nil, fmt.Errorf("leaseweb: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err) + } + + existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") + if err != nil { + notfoundErr := &internal.NotFoundError{} + if !errors.As(err, ¬foundErr) { + return fmt.Errorf("leaseweb: get RRSet: %w", err) + } + + // Create the RRSet. + + rrset := internal.RRSet{ + Content: []string{info.Value}, + Name: info.EffectiveFQDN, + TTL: internal.TTLRounder(d.config.TTL), + Type: "TXT", + } + + _, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), rrset) + if err != nil { + return fmt.Errorf("leaseweb: create RRSet: %w", err) + } + + return nil + } + + // Update the RRSet. + + existingRRSet.Content = append(existingRRSet.Content, info.Value) + + _, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet) + if err != nil { + return fmt.Errorf("leaseweb: update RRSet: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err) + } + + existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") + if err != nil { + return fmt.Errorf("leaseweb: get RRSet: %w", err) + } + + var content []string + + for _, s := range existingRRSet.Content { + if s != info.Value { + content = append(content, s) + } + } + + if len(content) == 0 { + err = d.client.DeleteRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") + if err != nil { + return fmt.Errorf("leaseweb: delete RRSet: %w", err) + } + + return nil + } + + existingRRSet.Content = content + + _, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet) + if err != nil { + return fmt.Errorf("leaseweb: update RRSet: %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.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/leaseweb/leaseweb.toml b/providers/dns/leaseweb/leaseweb.toml new file mode 100644 index 000000000..2c3503291 --- /dev/null +++ b/providers/dns/leaseweb/leaseweb.toml @@ -0,0 +1,22 @@ +Name = "Leaseweb" +Description = '''''' +URL = "https://www.leaseweb.com/en/" +Code = "leaseweb" +Since = "v4.32.0" + +Example = ''' +LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns leaseweb -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + LEASEWEB_API_KEY = "API key" + [Configuration.Additional] + LEASEWEB_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + LEASEWEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + LEASEWEB_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + LEASEWEB_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://developer.leaseweb.com/docs/#tag/DNS" diff --git a/providers/dns/leaseweb/leaseweb_test.go b/providers/dns/leaseweb/leaseweb_test.go new file mode 100644 index 000000000..0450cd2c2 --- /dev/null +++ b/providers/dns/leaseweb/leaseweb_test.go @@ -0,0 +1,204 @@ +package leaseweb + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/leaseweb/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "leaseweb: some credentials information are missing: LEASEWEB_API_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 + apiKey string + expected string + }{ + { + desc: "success", + apiKey: "secret", + }, + { + desc: "missing credentials", + expected: "leaseweb: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(internal.AuthHeader, "secret"), + ) +} + +func TestDNSProvider_Present_create(t *testing.T) { + provider := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("error_404.json"). + WithStatusCode(http.StatusNotFound), + ). + Route("POST /domains/example.com/resourceRecordSets", + servermock.ResponseFromInternal("createResourceRecordSet.json"), + servermock.CheckRequestJSONBodyFromInternal("createResourceRecordSet-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_Present_update(t *testing.T) { + provider := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("getResourceRecordSet.json"), + ). + Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("updateResourceRecordSet.json"), + servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp_delete(t *testing.T) { + provider := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("getResourceRecordSet2.json"), + ). + Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "1234d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp_update(t *testing.T) { + provider := mockBuilder(). + Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("getResourceRecordSet.json"), + ). + Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", + servermock.ResponseFromInternal("updateResourceRecordSet.json"), + servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request2.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "1234d==") + require.NoError(t, err) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 24474cf2f..e1b2cc989 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -106,6 +106,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/jdcloud" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/keyhelp" + "github.com/go-acme/lego/v4/providers/dns/leaseweb" "github.com/go-acme/lego/v4/providers/dns/liara" "github.com/go-acme/lego/v4/providers/dns/lightsail" "github.com/go-acme/lego/v4/providers/dns/limacity" @@ -398,6 +399,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return joker.NewDNSProvider() case "keyhelp": return keyhelp.NewDNSProvider() + case "leaseweb": + return leaseweb.NewDNSProvider() case "liara": return liara.NewDNSProvider() case "lightsail": From c50918c54ee9d6797587158241d5faf40513fec4 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 19 Feb 2026 12:55:08 +0100 Subject: [PATCH 86/95] Prepare release v4.32.0 --- CHANGELOG.md | 30 +++++++++++++++++++ acme/api/internal/sender/useragent.go | 4 +-- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 4 +-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee191cdb4..ae73f70f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ Everybody thinks that the others will donate, but in the end, nobody does. So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). +## v4.32.0 + +- Release date: 2026-02-19 +- Tag: [v4.32.0](https://github.com/go-acme/lego/releases/tag/v4.32.0) + +### Added + +- **[dnsprovider]** Add DNS provider for ArtFiles +- **[dnsprovider]** Add DNS provider for Leaseweb +- **[dnsprovider]** Add DNS provider for FusionLayer NameSurfer +- **[dnsprovider]** Add DNS provider for DDNSS +- **[dnsprovider]** Add DNS provider for Bluecat v2 +- **[dnsprovider]** Add DNS provider for TodayNIC/时代互联 +- **[dnsprovider]** Add DNS provider for DNSExit +- **[dnsprovider]** alidns: add line record option + +### Changed + +- **[dnsprovider]** azure: reinforces deprecation +- **[dnsprovider]** allinkl: detect zone through API + +### Fixed + +- **[ari]** fix: implement parsing for Retry-After header according to RFC 7231 +- **[dnsprovider]** namesurfer: fix updateDNSHost +- **[dnsprovider]** timewebcloud: fix subdomain support +- **[dnsprovider]** fix: deduplicate authz for DNS01 challenge +- **[lib,cli]** fix: use IPs to define the main domain +- **[lib]** fix: preserve domain order + ## v4.31.0 - Release date: 2026-01-08 diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 570b3b67c..feda18cc7 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.31.0" + ourUserAgent = "xenolf-acme/4.32.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index 57899e15e..f0ca21326 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.31.0+dev-detach" +const defaultVersion = "v4.32.0+dev-release" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index da24329cc..43e77b23d 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.31.0" + ourUserAgent = "goacme-lego/4.32.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) // Get builds and returns the User-Agent string. From 4547c4317e7a3158d1927aa0c6b887404d7e8ac9 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Thu, 19 Feb 2026 12:55:20 +0100 Subject: [PATCH 87/95] Detach v4.32.0 --- acme/api/internal/sender/useragent.go | 2 +- cmd/lego/zz_gen_version.go | 2 +- providers/dns/internal/useragent/useragent.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index feda18cc7..51a1b4770 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -9,5 +9,5 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go index f0ca21326..cf9ad00ef 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.32.0+dev-release" +const defaultVersion = "v4.32.0+dev-detach" var version = "" diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go index 43e77b23d..090c9109a 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -15,7 +15,7 @@ const ( // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "release" + ourUserAgentComment = "detach" ) // Get builds and returns the User-Agent string. From 7d459b59c5882aac5cd8545cbda77e18852f5cd3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 22 Feb 2026 14:14:18 +0100 Subject: [PATCH 88/95] liara: add support for team ID (#2867) --- cmd/zz_gen_cmd_dnshelp.go | 1 + docs/content/dns/zz_gen_liara.md | 1 + providers/dns/liara/internal/client.go | 27 +++++++--- providers/dns/liara/internal/client_test.go | 55 ++++++++++++++++++--- providers/dns/liara/liara.go | 7 ++- providers/dns/liara/liara.toml | 1 + 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 161729c79..8e3b4ebce 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -2394,6 +2394,7 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`) ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) ew.writeln() diff --git a/docs/content/dns/zz_gen_liara.md b/docs/content/dns/zz_gen_liara.md index 8a6ddbd99..658ce8077 100644 --- a/docs/content/dns/zz_gen_liara.md +++ b/docs/content/dns/zz_gen_liara.md @@ -50,6 +50,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `LIARA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | | `LIARA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | | `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `LIARA_TEAM_ID` | The team ID to access services in a team | | `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. diff --git a/providers/dns/liara/internal/client.go b/providers/dns/liara/internal/client.go index 93cdcf7c8..95c39695b 100644 --- a/providers/dns/liara/internal/client.go +++ b/providers/dns/liara/internal/client.go @@ -20,17 +20,23 @@ const defaultBaseURL = "https://dns-service.iran.liara.ir" type Client struct { baseURL *url.URL httpClient *http.Client + + teamID string } // NewClient creates a new Client. -func NewClient(hc *http.Client) *Client { +func NewClient(hc *http.Client, teamID string) *Client { baseURL, _ := url.Parse(defaultBaseURL) if hc == nil { hc = &http.Client{Timeout: 10 * time.Second} } - return &Client{httpClient: hc, baseURL: baseURL} + return &Client{ + httpClient: hc, + baseURL: baseURL, + teamID: teamID, + } } // GetRecords gets the records of a domain. @@ -38,7 +44,7 @@ func NewClient(hc *http.Client) *Client { func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records") - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -73,7 +79,7 @@ func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, e func (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records") - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -108,7 +114,7 @@ func (c *Client) CreateRecord(ctx context.Context, domainName string, record Rec func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID) - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -143,7 +149,7 @@ func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*R func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID) - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return fmt.Errorf("create request: %w", err) } @@ -162,7 +168,14 @@ func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) return nil } -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { +func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + if c.teamID != "" { + query := endpoint.Query() + query.Set("teamID", c.teamID) + + endpoint.RawQuery = query.Encode() + } + buf := new(bytes.Buffer) if payload != nil { diff --git a/providers/dns/liara/internal/client_test.go b/providers/dns/liara/internal/client_test.go index 57ac7e8b3..b6d007046 100644 --- a/providers/dns/liara/internal/client_test.go +++ b/providers/dns/liara/internal/client_test.go @@ -13,10 +13,10 @@ import ( const apiKey = "key" -func mockBuilder() *servermock.Builder[*Client] { +func mockBuilder(teamID string) *servermock.Builder[*Client] { return servermock.NewBuilder[*Client]( func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey)) + client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey), teamID) client.baseURL, _ = url.Parse(server.URL) return client, nil @@ -26,7 +26,7 @@ func mockBuilder() *servermock.Builder[*Client] { } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("GET /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordsResponse.json")). Build(t) @@ -50,7 +50,7 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("GET /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("RecordResponse.json")). Build(t) @@ -72,7 +72,7 @@ func TestClient_GetRecord(t *testing.T) { } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("POST /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordResponse.json"). WithStatusCode(http.StatusCreated), @@ -108,8 +108,47 @@ func TestClient_CreateRecord(t *testing.T) { assert.Equal(t, expected, record) } +func TestClient_CreateRecord_withTeamID(t *testing.T) { + client := mockBuilder("123"). + Route("POST /api/v1/zones/example.com/dns-records", + servermock.ResponseFromFixture("RecordResponse.json"). + WithStatusCode(http.StatusCreated), + servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`), + servermock.CheckQueryParameter().Strict().With("teamID", "123"), + ). + Build(t) + + data := Record{ + Type: "string", + Name: "string", + Contents: []Content{ + { + Text: "string", + }, + }, + TTL: 3600, + } + + record, err := client.CreateRecord(t.Context(), "example.com", data) + require.NoError(t, err) + + expected := &Record{ + ID: "string", + Type: "string", + Name: "string", + Contents: []Content{ + { + Text: "string", + }, + }, + TTL: 3600, + } + + assert.Equal(t, expected, record) +} + func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("DELETE /api/v1/zones/example.com/dns-records/123", servermock.Noop(). WithStatusCode(http.StatusNoContent)). @@ -120,7 +159,7 @@ func TestClient_DeleteRecord(t *testing.T) { } func TestClient_DeleteRecord_NotFound_Response(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("DELETE /api/v1/zones/example.com/dns-records/123", servermock.Noop(). WithStatusCode(http.StatusNotFound)). @@ -131,7 +170,7 @@ func TestClient_DeleteRecord_NotFound_Response(t *testing.T) { } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). + client := mockBuilder(""). Route("DELETE /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("error.json"). WithStatusCode(http.StatusUnauthorized)). diff --git a/providers/dns/liara/liara.go b/providers/dns/liara/liara.go index b91b004cc..c7e403eed 100644 --- a/providers/dns/liara/liara.go +++ b/providers/dns/liara/liara.go @@ -23,6 +23,7 @@ const ( envNamespace = "LIARA_" EnvAPIKey = envNamespace + "API_KEY" + EnvTeamID = envNamespace + "TEAM_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -39,7 +40,9 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. type Config struct { - APIKey string + APIKey string + TeamID string + TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -77,6 +80,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] + config.TeamID = env.GetOrFile(EnvTeamID) return NewDNSProviderConfig(config) } @@ -112,6 +116,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { clientdebug.Wrap( internal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey), ), + config.TeamID, ) return &DNSProvider{ diff --git a/providers/dns/liara/liara.toml b/providers/dns/liara/liara.toml index 4ed53ec75..f471de04e 100644 --- a/providers/dns/liara/liara.toml +++ b/providers/dns/liara/liara.toml @@ -13,6 +13,7 @@ lego --dns liara -d '*.example.com' -d example.com run [Configuration.Credentials] LIARA_API_KEY = "The API key" [Configuration.Additional] + LIARA_TEAM_ID = "The team ID to access services in a team" LIARA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" LIARA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" From 491dcaad1d4b77f3ec703a581e9a8d900869953d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 25 Feb 2026 23:12:43 +0100 Subject: [PATCH 89/95] feat: allow to Unwrap obtainError (#2874) --- challenge/resolver/errors.go | 6 +++ challenge/resolver/errors_test.go | 70 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 challenge/resolver/errors_test.go diff --git a/challenge/resolver/errors.go b/challenge/resolver/errors.go index 6a859922c..65a6ccdb7 100644 --- a/challenge/resolver/errors.go +++ b/challenge/resolver/errors.go @@ -3,6 +3,8 @@ package resolver import ( "bytes" "fmt" + "maps" + "slices" "sort" ) @@ -25,3 +27,7 @@ func (e obtainError) Error() string { return buffer.String() } + +func (e obtainError) Unwrap() []error { + return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e)) +} diff --git a/challenge/resolver/errors_test.go b/challenge/resolver/errors_test.go new file mode 100644 index 000000000..d4ab3c481 --- /dev/null +++ b/challenge/resolver/errors_test.go @@ -0,0 +1,70 @@ +package resolver + +import ( + "errors" + "testing" + + "github.com/go-acme/lego/v4/acme" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_obtainError_Error(t *testing.T) { + err := obtainError{ + "a": &acme.ProblemDetails{Type: "001"}, + "b": errors.New("oops"), + "c": errors.New("I did it again"), + } + + require.EqualError(t, err, `error: one or more domains had a problem: +[a] acme: error: 0 :: 001 :: +[b] oops +[c] I did it again +`) +} + +func Test_obtainError_Unwrap(t *testing.T) { + testCases := []struct { + desc string + err obtainError + assert assert.BoolAssertionFunc + }{ + { + desc: "one ok", + err: obtainError{ + "a": &acme.ProblemDetails{}, + "b": errors.New("oops"), + "c": errors.New("I did it again"), + }, + assert: assert.True, + }, + { + desc: "all ok", + err: obtainError{ + "a": &acme.ProblemDetails{Type: "001"}, + "b": &acme.ProblemDetails{Type: "002"}, + "c": &acme.ProblemDetails{Type: "002"}, + }, + assert: assert.True, + }, + { + desc: "nope", + err: obtainError{ + "a": errors.New("hello"), + "b": errors.New("oops"), + "c": errors.New("I did it again"), + }, + assert: assert.False, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var pd *acme.ProblemDetails + + test.assert(t, errors.As(test.err, &pd)) + }) + } +} From da51631cd36b0640e559926d96d71a2e12555d92 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 4 Mar 2026 17:59:58 +0100 Subject: [PATCH 90/95] chore: improve issue template --- .github/ISSUE_TEMPLATE/new_dns_provider.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/new_dns_provider.yml b/.github/ISSUE_TEMPLATE/new_dns_provider.yml index cfd6e5c8c..b319bc287 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -14,9 +14,15 @@ body: required: true - label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world. required: true + + - type: checkboxes + id: pr + attributes: + label: Implementation + options: - label: Yes, I'm able to create a pull request and be able to maintain the implementation. required: false - - label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider. + - label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request. required: false - type: dropdown From 847c763504888c511d7fcec82d65004caf25853e Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 4 Mar 2026 18:04:04 +0100 Subject: [PATCH 91/95] feat: Add DNS provider for Czechia (#2885) --- README.md | 79 +++++---- cmd/zz_gen_cmd_dnshelp.go | 21 +++ docs/content/dns/zz_gen_czechia.md | 67 +++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/czechia/czechia.go | 159 +++++++++++++++++ providers/dns/czechia/czechia.toml | 22 +++ providers/dns/czechia/czechia_test.go | 165 ++++++++++++++++++ providers/dns/czechia/internal/client.go | 124 +++++++++++++ providers/dns/czechia/internal/client_test.go | 67 +++++++ .../fixtures/add_txt_record-request.json | 6 + .../fixtures/delete_txt_record-request.json | 6 + providers/dns/czechia/internal/types.go | 8 + providers/dns/zz_gen_dns_providers.go | 3 + 13 files changed, 691 insertions(+), 38 deletions(-) create mode 100644 docs/content/dns/zz_gen_czechia.md create mode 100644 providers/dns/czechia/czechia.go create mode 100644 providers/dns/czechia/czechia.toml create mode 100644 providers/dns/czechia/czechia_test.go create mode 100644 providers/dns/czechia/internal/client.go create mode 100644 providers/dns/czechia/internal/client_test.go create mode 100644 providers/dns/czechia/internal/fixtures/add_txt_record-request.json create mode 100644 providers/dns/czechia/internal/fixtures/delete_txt_record-request.json create mode 100644 providers/dns/czechia/internal/types.go diff --git a/README.md b/README.md index 9925979bd..5f183a458 100644 --- a/README.md +++ b/README.md @@ -109,192 +109,197 @@ If your DNS provider is not supported, please open an [issue](https://github.com Constellix Core-Networks CPanel/WHM - DDnss (DynDNS Service) + Czechia + DDnss (DynDNS Service) Derak Cloud deSEC.io Designate DNSaaS for Openstack - Digital Ocean + Digital Ocean DirectAdmin DNS Made Easy DNSExit - dnsHome.de + dnsHome.de DNSimple DNSPod (deprecated) Domain Offensive (do.de) - Domeneshop + Domeneshop DreamHost Duck DNS Dyn - DynDnsFree.de + DynDnsFree.de Dynu EasyDNS EdgeCenter - Efficient IP + Efficient IP Epik Exoscale External program - F5 XC + F5 XC freemyip.com FusionLayer NameSurfer G-Core - Gandi + Gandi Gandi Live DNS (v5) Gigahost.no Glesys - Go Daddy + Go Daddy Google Cloud Google Domains Gravity - Hetzner + Hetzner Hosting.de Hosting.nl Hostinger - Hosttech + Hosttech HTTP request http.net Huawei Cloud - Hurricane Electric DNS + Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service - Infoblox + Infoblox Infomaniak Internet Initiative Japan Internet.bs - INWX + INWX Ionos Ionos Cloud IPv64 - ISPConfig 3 + ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) JD Cloud - Joker + Joker Joohoi's ACME-DNS KeyHelp Leaseweb - Liara + Liara Lima-City Linode (v4) Liquid Web - Loopia + Loopia LuaDNS Mail-in-a-Box ManageEngine CloudDNS - Manual + Manual Metaname Metaregistrar mijn.host - Mittwald + Mittwald myaddr.{tools,dev,io} MyDNS.jp MythicBeasts - Name.com + Name.com Namecheap Namesilo NearlyFreeSpeech.NET - Neodigit + Neodigit Netcup Netlify Nicmanager - NIFCloud + NIFCloud Njalla Nodion NS1 - Octenium + Octenium Open Telekom Cloud Oracle Cloud OVH - plesk.com + plesk.com Porkbun PowerDNS Rackspace - Rain Yun/雨云 + Rain Yun/雨云 RcodeZero reg.ru Regfish - RFC2136 + RFC2136 RimuHosting RU CENTER Sakura Cloud - Scaleway + Scaleway Selectel Selectel v2 SelfHost.(de|eu) - Servercow + Servercow Shellrent Simply.com Sonic - Spaceship + Spaceship Stackpath Syse Technitium - Tencent Cloud DNS + Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联 - TransIP + TransIP UKFast SafeDNS Ultradns United-Domains - Variomedia + Variomedia VegaDNS Vercel Versio.[nl|eu|uk] - VinylDNS + VinylDNS Virtualname VK Cloud Volcano Engine/火山引擎 - Vscale + Vscale Vultr webnames.ca webnames.ru - Websupport + Websupport WEDOS West.cn/西部数码 Yandex 360 - Yandex Cloud + Yandex Cloud Yandex PDD Zone.ee ZoneEdit + Zonomi + + + diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 8e3b4ebce..3dff0ee7a 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -49,6 +49,7 @@ func allDNSCodes() string { "constellix", "corenetworks", "cpanel", + "czechia", "ddnss", "derak", "desec", @@ -1026,6 +1027,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`) + case "czechia": + // generated from: providers/dns/czechia/czechia.toml + ew.writeln(`Configuration for Czechia.`) + ew.writeln(`Code: 'czechia'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "CZECHIA_TOKEN": Authorization token`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "CZECHIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "CZECHIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "CZECHIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "CZECHIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`) + case "ddnss": // generated from: providers/dns/ddnss/ddnss.toml ew.writeln(`Configuration for DDnss (DynDNS Service).`) diff --git a/docs/content/dns/zz_gen_czechia.md b/docs/content/dns/zz_gen_czechia.md new file mode 100644 index 000000000..7b1cdd1ae --- /dev/null +++ b/docs/content/dns/zz_gen_czechia.md @@ -0,0 +1,67 @@ +--- +title: "Czechia" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: czechia +dnsprovider: + since: "v4.33.0" + code: "czechia" + url: "https://www.czechia.com/" +--- + + + + + + +Configuration for [Czechia](https://www.czechia.com/). + + + + +- Code: `czechia` +- Since: v4.33.0 + + +Here is an example bash command using the Czechia provider: + +```bash +CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns czechia -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `CZECHIA_TOKEN` | Authorization token | + +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 | +|--------------------------------|-------------| +| `CZECHIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `CZECHIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `CZECHIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `CZECHIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +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.czechia.com/swagger/index.html) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 925ef0b21..b68c8dbf6 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/czechia/czechia.go b/providers/dns/czechia/czechia.go new file mode 100644 index 000000000..3ff397c35 --- /dev/null +++ b/providers/dns/czechia/czechia.go @@ -0,0 +1,159 @@ +// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia. +package czechia + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/czechia/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "CZECHIA_" + + EnvToken = envNamespace + "TOKEN" + + 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 { + Token string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Czechia. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvToken) + if err != nil { + return nil, fmt.Errorf("czechia: %w", err) + } + + config := NewDefaultConfig() + config.Token = values[EnvToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Czechia. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("czechia: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.Token) + if err != nil { + return nil, fmt.Errorf("czechia: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("czechia: %w", err) + } + + record := internal.TXTRecord{ + Hostname: subDomain, + Text: info.Value, + TTL: d.config.TTL, + PublishZone: 1, + } + + err = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("czechia: add TXT record: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("czechia: %w", err) + } + + record := internal.TXTRecord{ + Hostname: subDomain, + Text: info.Value, + TTL: d.config.TTL, + PublishZone: 1, + } + + err = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("czechia: delete TXT record: %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.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/czechia/czechia.toml b/providers/dns/czechia/czechia.toml new file mode 100644 index 000000000..2a66d2054 --- /dev/null +++ b/providers/dns/czechia/czechia.toml @@ -0,0 +1,22 @@ +Name = "Czechia" +Description = '''''' +URL = "https://www.czechia.com/" +Code = "czechia" +Since = "v4.33.0" + +Example = ''' +CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ +lego --dns czechia -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + CZECHIA_TOKEN = "Authorization token" + [Configuration.Additional] + CZECHIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + CZECHIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + CZECHIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + CZECHIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://api.czechia.com/swagger/index.html" diff --git a/providers/dns/czechia/czechia_test.go b/providers/dns/czechia/czechia_test.go new file mode 100644 index 000000000..7d9a2676c --- /dev/null +++ b/providers/dns/czechia/czechia_test.go @@ -0,0 +1,165 @@ +package czechia + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvToken: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "czechia: some credentials information are missing: CZECHIA_TOKEN", + }, + } + + 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 + token string + expected string + }{ + { + desc: "success", + token: "secret", + }, + { + desc: "missing credentials", + expected: "czechia: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Token = test.token + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.Token = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + p.client.BaseURL, _ = url.Parse(server.URL) + + return p, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With("AuthorizationToken", "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("POST /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("DELETE /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/czechia/internal/client.go b/providers/dns/czechia/internal/client.go new file mode 100644 index 000000000..f3e0e462e --- /dev/null +++ b/providers/dns/czechia/internal/client.go @@ -0,0 +1,124 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" +) + +const defaultBaseURL = "https://api.czechia.com/api" + +const authorizationTokenHeader = "AuthorizationToken" + +// Client the Czechia API client. +type Client struct { + token string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(token string) (*Client, error) { + if token == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + token: token, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error { + endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error { + endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set(authorizationTokenHeader, c.token) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/dns/czechia/internal/client_test.go b/providers/dns/czechia/internal/client_test.go new file mode 100644 index 000000000..c6f1141c5 --- /dev/null +++ b/providers/dns/czechia/internal/client_test.go @@ -0,0 +1,67 @@ +package internal + +import ( + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("secret") + if err != nil { + return nil, err + } + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(authorizationTokenHeader, "secret"), + ) +} + +func TestClient_AddTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), + ). + Build(t) + + record := TXTRecord{ + Hostname: "_acme-challenge", + Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + PublishZone: 1, + } + + err := client.AddTXTRecord(t.Context(), "example.com", record) + require.NoError(t, err) +} + +func TestClient_DeleteTXTRecord(t *testing.T) { + client := mockBuilder(). + Route("DELETE /DNS/example.com/TXT", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), + ). + Build(t) + + record := TXTRecord{ + Hostname: "_acme-challenge", + Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 120, + PublishZone: 1, + } + + err := client.DeleteTXTRecord(t.Context(), "example.com", record) + require.NoError(t, err) +} diff --git a/providers/dns/czechia/internal/fixtures/add_txt_record-request.json b/providers/dns/czechia/internal/fixtures/add_txt_record-request.json new file mode 100644 index 000000000..ed5830093 --- /dev/null +++ b/providers/dns/czechia/internal/fixtures/add_txt_record-request.json @@ -0,0 +1,6 @@ +{ + "hostName": "_acme-challenge", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "publishZone": 1 +} diff --git a/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json b/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json new file mode 100644 index 000000000..ed5830093 --- /dev/null +++ b/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json @@ -0,0 +1,6 @@ +{ + "hostName": "_acme-challenge", + "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "ttl": 120, + "publishZone": 1 +} diff --git a/providers/dns/czechia/internal/types.go b/providers/dns/czechia/internal/types.go new file mode 100644 index 000000000..f4a9bfef7 --- /dev/null +++ b/providers/dns/czechia/internal/types.go @@ -0,0 +1,8 @@ +package internal + +type TXTRecord struct { + Hostname string `json:"hostName,omitempty"` + Text string `json:"text,omitempty"` + TTL int `json:"ttl,omitempty"` + PublishZone int `json:"publishZone,omitempty"` +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index e1b2cc989..66457c550 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -43,6 +43,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/constellix" "github.com/go-acme/lego/v4/providers/dns/corenetworks" "github.com/go-acme/lego/v4/providers/dns/cpanel" + "github.com/go-acme/lego/v4/providers/dns/czechia" "github.com/go-acme/lego/v4/providers/dns/ddnss" "github.com/go-acme/lego/v4/providers/dns/derak" "github.com/go-acme/lego/v4/providers/dns/desec" @@ -273,6 +274,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return corenetworks.NewDNSProvider() case "cpanel": return cpanel.NewDNSProvider() + case "czechia": + return czechia.NewDNSProvider() case "ddnss": return ddnss.NewDNSProvider() case "derak": From a56697ed1cd7eddeedfb459f9200b936afeeb34a Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sun, 8 Mar 2026 23:32:56 +0100 Subject: [PATCH 92/95] Add DNS provider for EuroDNS (#2898) --- README.md | 66 ++-- cmd/zz_gen_cmd_dnshelp.go | 22 ++ docs/content/dns/zz_gen_eurodns.md | 69 ++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/eurodns/eurodns.go | 197 +++++++++++ providers/dns/eurodns/eurodns.toml | 24 ++ providers/dns/eurodns/eurodns_test.go | 215 ++++++++++++ providers/dns/eurodns/internal/client.go | 199 +++++++++++ providers/dns/eurodns/internal/client_test.go | 310 ++++++++++++++++++ .../dns/eurodns/internal/fixtures/error.json | 8 + .../eurodns/internal/fixtures/zone_add.json | 46 +++ .../fixtures/zone_add_empty_forwards.json | 28 ++ .../fixtures/zone_add_validate_ko.json | 139 ++++++++ .../fixtures/zone_add_validate_ok.json | 49 +++ .../eurodns/internal/fixtures/zone_get.json | 37 +++ .../internal/fixtures/zone_remove.json | 37 +++ providers/dns/eurodns/internal/types.go | 136 ++++++++ providers/dns/zz_gen_dns_providers.go | 3 + 18 files changed, 1553 insertions(+), 34 deletions(-) create mode 100644 docs/content/dns/zz_gen_eurodns.md create mode 100644 providers/dns/eurodns/eurodns.go create mode 100644 providers/dns/eurodns/eurodns.toml create mode 100644 providers/dns/eurodns/eurodns_test.go create mode 100644 providers/dns/eurodns/internal/client.go create mode 100644 providers/dns/eurodns/internal/client_test.go create mode 100644 providers/dns/eurodns/internal/fixtures/error.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_add.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_get.json create mode 100644 providers/dns/eurodns/internal/fixtures/zone_remove.json create mode 100644 providers/dns/eurodns/internal/types.go diff --git a/README.md b/README.md index 5f183a458..3d815387a 100644 --- a/README.md +++ b/README.md @@ -138,168 +138,168 @@ If your DNS provider is not supported, please open an [issue](https://github.com Efficient IP Epik + EuroDNS Exoscale - External program + External program F5 XC freemyip.com FusionLayer NameSurfer - G-Core + G-Core Gandi Gandi Live DNS (v5) Gigahost.no - Glesys + Glesys Go Daddy Google Cloud Google Domains - Gravity + Gravity Hetzner Hosting.de Hosting.nl - Hostinger + Hostinger Hosttech HTTP request http.net - Huawei Cloud + Huawei Cloud Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer) - IIJ DNS Platform Service + IIJ DNS Platform Service Infoblox Infomaniak Internet Initiative Japan - Internet.bs + Internet.bs INWX Ionos Ionos Cloud - IPv64 + IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) - JD Cloud + JD Cloud Joker Joohoi's ACME-DNS KeyHelp - Leaseweb + Leaseweb Liara Lima-City Linode (v4) - Liquid Web + Liquid Web Loopia LuaDNS Mail-in-a-Box - ManageEngine CloudDNS + ManageEngine CloudDNS Manual Metaname Metaregistrar - mijn.host + mijn.host Mittwald myaddr.{tools,dev,io} MyDNS.jp - MythicBeasts + MythicBeasts Name.com Namecheap Namesilo - NearlyFreeSpeech.NET + NearlyFreeSpeech.NET Neodigit Netcup Netlify - Nicmanager + Nicmanager NIFCloud Njalla Nodion - NS1 + NS1 Octenium Open Telekom Cloud Oracle Cloud - OVH + OVH plesk.com Porkbun PowerDNS - Rackspace + Rackspace Rain Yun/雨云 RcodeZero reg.ru - Regfish + Regfish RFC2136 RimuHosting RU CENTER - Sakura Cloud + Sakura Cloud Scaleway Selectel Selectel v2 - SelfHost.(de|eu) + SelfHost.(de|eu) Servercow Shellrent Simply.com - Sonic + Sonic Spaceship Stackpath Syse - Technitium + Technitium Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud - TodayNIC/时代互联 + TodayNIC/时代互联 TransIP UKFast SafeDNS Ultradns - United-Domains + United-Domains Variomedia VegaDNS Vercel - Versio.[nl|eu|uk] + Versio.[nl|eu|uk] VinylDNS Virtualname VK Cloud - Volcano Engine/火山引擎 + Volcano Engine/火山引擎 Vscale Vultr webnames.ca - webnames.ru + webnames.ru Websupport WEDOS West.cn/西部数码 - Yandex 360 + Yandex 360 Yandex Cloud Yandex PDD Zone.ee - ZoneEdit + ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 3dff0ee7a..3a6439f00 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -74,6 +74,7 @@ func allDNSCodes() string { "edgeone", "efficientip", "epik", + "eurodns", "exec", "exoscale", "f5xc", @@ -1562,6 +1563,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/epik`) + case "eurodns": + // generated from: providers/dns/eurodns/eurodns.toml + ew.writeln(`Configuration for EuroDNS.`) + ew.writeln(`Code: 'eurodns'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "EURODNS_API_KEY": API key`) + ew.writeln(` - "EURODNS_APP_ID": Application ID`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EURODNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "EURODNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "EURODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "EURODNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/eurodns`) + case "exec": // generated from: providers/dns/exec/exec.toml ew.writeln(`Configuration for External program.`) diff --git a/docs/content/dns/zz_gen_eurodns.md b/docs/content/dns/zz_gen_eurodns.md new file mode 100644 index 000000000..cb5a0418d --- /dev/null +++ b/docs/content/dns/zz_gen_eurodns.md @@ -0,0 +1,69 @@ +--- +title: "EuroDNS" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: eurodns +dnsprovider: + since: "v4.33.0" + code: "eurodns" + url: "https://www.eurodns.com/" +--- + + + + + + +Configuration for [EuroDNS](https://www.eurodns.com/). + + + + +- Code: `eurodns` +- Since: v4.33.0 + + +Here is an example bash command using the EuroDNS provider: + +```bash +EURODNS_APP_ID="xxx" \ +EURODNS_API_KEY="yyy" \ +lego --dns eurodns -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EURODNS_API_KEY` | API key | +| `EURODNS_APP_ID` | Application ID | + +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 | +|--------------------------------|-------------| +| `EURODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `EURODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `EURODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `EURODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | + +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://docapi.eurodns.com/) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index b68c8dbf6..5736d0ae8 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/eurodns/eurodns.go b/providers/dns/eurodns/eurodns.go new file mode 100644 index 000000000..21ff3c3a9 --- /dev/null +++ b/providers/dns/eurodns/eurodns.go @@ -0,0 +1,197 @@ +// Package eurodns implements a DNS provider for solving the DNS-01 challenge using EuroDNS. +package eurodns + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "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/eurodns/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "EURODNS_" + + EnvApplicationID = envNamespace + "APP_ID" + EnvAPIKey = envNamespace + "API_KEY" + + 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 { + ApplicationID string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, internal.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for EuroDNS. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvApplicationID, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("eurodns: %w", err) + } + + config := NewDefaultConfig() + config.ApplicationID = values[EnvApplicationID] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for EuroDNS. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("eurodns: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.ApplicationID, config.APIKey) + if err != nil { + return nil, fmt.Errorf("eurodns: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("eurodns: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.client.GetZone(ctx, authZone) + if err != nil { + return fmt.Errorf("eurodns: get zone: %w", err) + } + + zone.Records = append(zone.Records, internal.Record{ + Type: "TXT", + Host: subDomain, + TTL: internal.TTLRounder(d.config.TTL), + RData: info.Value, + }) + + validation, err := d.client.ValidateZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: validate zone: %w", err) + } + + if validation.Report != nil && !validation.Report.IsValid { + return fmt.Errorf("eurodns: validation report: %w", validation.Report) + } + + err = d.client.SaveZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: save zone: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("eurodns: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zone, err := d.client.GetZone(ctx, authZone) + if err != nil { + return fmt.Errorf("eurodns: get zone: %w", err) + } + + var recordsToKeep []internal.Record + + for _, record := range zone.Records { + if record.Type == "TXT" && record.Host == subDomain && record.RData == info.Value { + continue + } + + recordsToKeep = append(recordsToKeep, record) + } + + zone.Records = recordsToKeep + + validation, err := d.client.ValidateZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: validate zone: %w", err) + } + + if validation.Report != nil && !validation.Report.IsValid { + return fmt.Errorf("eurodns: validation report: %w", validation.Report) + } + + err = d.client.SaveZone(ctx, authZone, zone) + if err != nil { + return fmt.Errorf("eurodns: save zone: %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.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/eurodns/eurodns.toml b/providers/dns/eurodns/eurodns.toml new file mode 100644 index 000000000..302b15d00 --- /dev/null +++ b/providers/dns/eurodns/eurodns.toml @@ -0,0 +1,24 @@ +Name = "EuroDNS" +Description = '''''' +URL = "https://www.eurodns.com/" +Code = "eurodns" +Since = "v4.33.0" + +Example = ''' +EURODNS_APP_ID="xxx" \ +EURODNS_API_KEY="yyy" \ +lego --dns eurodns -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + EURODNS_APP_ID = "Application ID" + EURODNS_API_KEY = "API key" + [Configuration.Additional] + EURODNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + EURODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + EURODNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" + EURODNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "https://docapi.eurodns.com/" diff --git a/providers/dns/eurodns/eurodns_test.go b/providers/dns/eurodns/eurodns_test.go new file mode 100644 index 000000000..abbb4717e --- /dev/null +++ b/providers/dns/eurodns/eurodns_test.go @@ -0,0 +1,215 @@ +package eurodns + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/eurodns/internal" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvApplicationID, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvApplicationID: "abc", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing application ID", + envVars: map[string]string{ + EnvApplicationID: "", + EnvAPIKey: "secret", + }, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", + }, + { + desc: "missing API secret", + envVars: map[string]string{ + EnvApplicationID: "", + EnvAPIKey: "secret", + }, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "eurodns: some credentials information are missing: EURODNS_APP_ID,EURODNS_API_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 + appID string + apiKey string + expected string + }{ + { + desc: "success", + appID: "abc", + apiKey: "secret", + }, + { + desc: "missing application ID", + expected: "eurodns: credentials missing", + apiKey: "secret", + }, + { + desc: "missing API secret", + expected: "eurodns: credentials missing", + appID: "abc", + }, + { + desc: "missing credentials", + expected: "eurodns: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ApplicationID = test.appID + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIKey = "secret" + config.ApplicationID = "abc" + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + provider.client.BaseURL, _ = url.Parse(server.URL) + + return provider, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(internal.HeaderAppID, "abc"). + With(internal.HeaderAPIKey, "secret"), + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromInternal("zone_get.json"), + ). + Route("POST /example.com/check", + servermock.ResponseFromInternal("zone_add_validate_ok.json"), + servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), + ). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromInternal("zone_add.json"), + ). + Route("POST /example.com/check", + servermock.ResponseFromInternal("zone_remove.json"), + servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), + ). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), + ). + Build(t) + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/eurodns/internal/client.go b/providers/dns/eurodns/internal/client.go new file mode 100644 index 000000000..1ebf8d143 --- /dev/null +++ b/providers/dns/eurodns/internal/client.go @@ -0,0 +1,199 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +const defaultBaseURL = "https://rest-api.eurodns.com/dns-zones/" + +const ( + HeaderAppID = "X-APP-ID" + HeaderAPIKey = "X-API-KEY" +) + +// Client the EuroDNS API client. +type Client struct { + appID string + apiKey string + + BaseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(appID, apiKey string) (*Client, error) { + if appID == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + appID: appID, + apiKey: apiKey, + BaseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// GetZone gets a DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/getdnszone +func (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error) { + endpoint := c.BaseURL.JoinPath(domain) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := &Zone{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// SaveZone saves a DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/savednszone +func (c *Client) SaveZone(ctx context.Context, domain string, zone *Zone) error { + endpoint := c.BaseURL.JoinPath(domain) + + if len(zone.URLForwards) == 0 { + zone.URLForwards = make([]URLForward, 0) + } + + if len(zone.MailForwards) == 0 { + zone.MailForwards = make([]MailForward, 0) + } + + req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone) + if err != nil { + return err + } + + return c.do(req, nil) +} + +// ValidateZone validates DNS Zone. +// https://docapi.eurodns.com/#/dnsprovider/checkdnszone +func (c *Client) ValidateZone(ctx context.Context, domain string, zone *Zone) (*Zone, error) { + endpoint := c.BaseURL.JoinPath(domain, "check") + + if len(zone.URLForwards) == 0 { + zone.URLForwards = make([]URLForward, 0) + } + + if len(zone.MailForwards) == 0 { + zone.MailForwards = make([]MailForward, 0) + } + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone) + if err != nil { + return nil, err + } + + result := &Zone{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Client) do(req *http.Request, result any) error { + req.Header.Set(HeaderAppID, c.appID) + req.Header.Set(HeaderAPIKey, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var errAPI APIError + + err := json.Unmarshal(raw, &errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("%d: %w", resp.StatusCode, &errAPI) +} + +const DefaultTTL = 600 + +// TTLRounder rounds the given TTL in seconds to the next accepted value. +// Accepted TTL values are: 600, 900, 1800,3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800. +func TTLRounder(ttl int) int { + for _, validTTL := range []int{DefaultTTL, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800} { + if ttl <= validTTL { + return validTTL + } + } + + return DefaultTTL +} diff --git a/providers/dns/eurodns/internal/client_test.go b/providers/dns/eurodns/internal/client_test.go new file mode 100644 index 000000000..68d1fda84 --- /dev/null +++ b/providers/dns/eurodns/internal/client_test.go @@ -0,0 +1,310 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/internal/ptr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient("abc", "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(HeaderAppID, "abc"). + With(HeaderAPIKey, "secret"), + ) +} + +func TestClient_GetZone(t *testing.T) { + client := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromFixture("zone_get.json"), + ). + Build(t) + + zone, err := client.GetZone(context.Background(), "example.com") + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: slices.Concat([]Record{fakeARecord()}), + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + assert.Equal(t, expected, zone) +} + +func TestClient_GetZone_error(t *testing.T) { + client := mockBuilder(). + Route("GET /example.com", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + _, err := client.GetZone(context.Background(), "example.com") + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func TestClient_SaveZone(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.NoError(t, err) +} + +func TestClient_SaveZone_emptyForwards(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.Noop(). + WithStatusCode(http.StatusNoContent), + servermock.CheckRequestJSONBodyFromFixture("zone_add_empty_forwards.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: slices.Concat([]Record{fakeARecord(), record}), + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.NoError(t, err) +} + +func TestClient_SaveZone_error(t *testing.T) { + client := mockBuilder(). + Route("PUT /example.com", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + err := client.SaveZone(context.Background(), "example.com", zone) + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func TestClient_ValidateZone(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("zone_add_validate_ok.json"), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + zone, err := client.ValidateZone(context.Background(), "example.com", zone) + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + Report: &Report{IsValid: true}, + } + + assert.Equal(t, expected, zone) +} + +func TestClient_ValidateZone_report(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("zone_add_validate_ko.json"), + servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), + ). + Build(t) + + record := Record{ + Type: "TXT", + Host: "_acme-challenge", + RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: 600, + } + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + zone, err := client.ValidateZone(context.Background(), "example.com", zone) + require.NoError(t, err) + + expected := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord(), record}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + Report: fakeReport(), + } + + assert.EqualError(t, zone.Report, `record error (ERROR): "120" is not a valid TTL, URL forward error (ERROR): string, mail forward error (ERROR): string, zone error (ERROR): string`) + + assert.Equal(t, expected, zone) +} + +func TestClient_ValidateZone_error(t *testing.T) { + client := mockBuilder(). + Route("POST /example.com/check", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized), + ). + Build(t) + + zone := &Zone{ + Name: "example.com", + DomainConnect: true, + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + } + + _, err := client.ValidateZone(context.Background(), "example.com", zone) + require.Error(t, err) + + require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") +} + +func fakeARecord() Record { + return Record{ + ID: 1000, + Type: "A", + Host: "@", + TTL: 600, + RData: "string", + Updated: ptr.Pointer(true), + Locked: ptr.Pointer(true), + IsDynDNS: ptr.Pointer(true), + Proxy: "ON", + } +} + +func fakeURLForward() URLForward { + return URLForward{ + ID: 2000, + ForwardType: "FRAME", + Host: "string", + URL: "string", + Title: "string", + Keywords: "string", + Description: "string", + Updated: ptr.Pointer(true), + } +} + +func fakeMailForward() MailForward { + return MailForward{ + ID: 3000, + Source: "string", + Destination: "string", + Updated: ptr.Pointer(true), + } +} + +func fakeReport() *Report { + return &Report{ + IsValid: false, + RecordErrors: []RecordError{{ + Messages: []string{`"120" is not a valid TTL`}, + Severity: "ERROR", + Record: fakeARecord(), + }}, + URLForwardErrors: []URLForwardError{{ + Messages: []string{"string"}, + Severity: "ERROR", + URLForward: fakeURLForward(), + }}, + MailForwardErrors: []MailForwardError{{ + Messages: []string{"string"}, + MailForward: fakeMailForward(), + Severity: "ERROR", + }}, + ZoneErrors: []ZoneError{{ + Message: "string", + Severity: "ERROR", + Records: []Record{fakeARecord()}, + URLForwards: []URLForward{fakeURLForward()}, + MailForwards: []MailForward{fakeMailForward()}, + }}, + } +} diff --git a/providers/dns/eurodns/internal/fixtures/error.json b/providers/dns/eurodns/internal/fixtures/error.json new file mode 100644 index 000000000..82a334598 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/error.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "code": "INVALID_API_KEY", + "title": "Invalid API Key" + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add.json b/providers/dns/eurodns/internal/fixtures/zone_add.json new file mode 100644 index 000000000..db8142357 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add.json @@ -0,0 +1,46 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json b/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json new file mode 100644 index 000000000..64f8530c9 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json @@ -0,0 +1,28 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [], + "mailForwards": [] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json new file mode 100644 index 000000000..e07d42299 --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json @@ -0,0 +1,139 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "report": { + "isValid": false, + "recordErrors": [ + { + "messages": [ + "\"120\" is not a valid TTL" + ], + "record": { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + "severity": "ERROR" + } + ], + "urlForwardErrors": [ + { + "messages": [ + "string" + ], + "urlForward": { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + }, + "severity": "ERROR" + } + ], + "mailForwardErrors": [ + { + "messages": [ + "string" + ], + "mailForward": { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + }, + "severity": "ERROR" + } + ], + "zoneErrors": [ + { + "message": "string", + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "severity": "ERROR" + } + ] + } +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json new file mode 100644 index 000000000..ba0ddfefb --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json @@ -0,0 +1,49 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + }, + { + "type": "TXT", + "host": "_acme-challenge", + "ttl": 600, + "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + "updated": null, + "locked": null, + "isDynDns": null + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ], + "report": { + "isValid": true + } +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_get.json b/providers/dns/eurodns/internal/fixtures/zone_get.json new file mode 100644 index 000000000..ebbc8593e --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_get.json @@ -0,0 +1,37 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/fixtures/zone_remove.json b/providers/dns/eurodns/internal/fixtures/zone_remove.json new file mode 100644 index 000000000..ebbc8593e --- /dev/null +++ b/providers/dns/eurodns/internal/fixtures/zone_remove.json @@ -0,0 +1,37 @@ +{ + "name": "example.com", + "domainConnect": true, + "records": [ + { + "id": 1000, + "type": "A", + "host": "@", + "ttl": 600, + "rdata": "string", + "updated": true, + "locked": true, + "isDynDns": true, + "proxy": "ON" + } + ], + "urlForwards": [ + { + "id": 2000, + "forwardType": "FRAME", + "host": "string", + "url": "string", + "title": "string", + "keywords": "string", + "description": "string", + "updated": true + } + ], + "mailForwards": [ + { + "id": 3000, + "source": "string", + "destination": "string", + "updated": true + } + ] +} diff --git a/providers/dns/eurodns/internal/types.go b/providers/dns/eurodns/internal/types.go new file mode 100644 index 000000000..891b02e14 --- /dev/null +++ b/providers/dns/eurodns/internal/types.go @@ -0,0 +1,136 @@ +package internal + +import ( + "fmt" + "strings" +) + +type APIError struct { + Errors []Error `json:"errors"` +} + +func (a *APIError) Error() string { + var msg []string + + for _, e := range a.Errors { + msg = append(msg, fmt.Sprintf("%s: %s", e.Code, e.Title)) + } + + return strings.Join(msg, ", ") +} + +type Error struct { + Code string `json:"code"` + Title string `json:"title"` +} + +type Zone struct { + Name string `json:"name,omitempty"` + DomainConnect bool `json:"domainConnect,omitempty"` + Records []Record `json:"records"` + URLForwards []URLForward `json:"urlForwards"` + MailForwards []MailForward `json:"mailForwards"` + Report *Report `json:"report,omitempty"` +} + +type Record struct { + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Host string `json:"host,omitempty"` + TTL int `json:"ttl,omitempty"` + RData string `json:"rdata,omitempty"` + Updated *bool `json:"updated"` + Locked *bool `json:"locked"` + IsDynDNS *bool `json:"isDynDns"` + Proxy string `json:"proxy,omitempty"` +} + +type URLForward struct { + ID int `json:"id,omitempty"` + ForwardType string `json:"forwardType,omitempty"` + Host string `json:"host,omitempty"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Keywords string `json:"keywords,omitempty"` + Description string `json:"description,omitempty"` + Updated *bool `json:"updated,omitempty"` +} + +type MailForward struct { + ID int `json:"id,omitempty"` + Source string `json:"source,omitempty"` + Destination string `json:"destination,omitempty"` + Updated *bool `json:"updated,omitempty"` +} + +type Report struct { + IsValid bool `json:"isValid,omitempty"` + RecordErrors []RecordError `json:"recordErrors,omitempty"` + URLForwardErrors []URLForwardError `json:"urlForwardErrors,omitempty"` + MailForwardErrors []MailForwardError `json:"mailForwardErrors,omitempty"` + ZoneErrors []ZoneError `json:"zoneErrors,omitempty"` +} + +func (r *Report) Error() string { + var msg []string + + for _, e := range r.RecordErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.URLForwardErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.MailForwardErrors { + msg = append(msg, e.Error()) + } + + for _, e := range r.ZoneErrors { + msg = append(msg, e.Error()) + } + + return strings.Join(msg, ", ") +} + +type RecordError struct { + Messages []string `json:"messages,omitempty"` + Record Record `json:"record"` + Severity string `json:"severity,omitempty"` +} + +func (e *RecordError) Error() string { + return fmt.Sprintf("record error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type URLForwardError struct { + Messages []string `json:"messages,omitempty"` + URLForward URLForward `json:"urlForward"` + Severity string `json:"severity,omitempty"` +} + +func (e *URLForwardError) Error() string { + return fmt.Sprintf("URL forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type MailForwardError struct { + Messages []string `json:"messages,omitempty"` + MailForward MailForward `json:"mailForward"` + Severity string `json:"severity,omitempty"` +} + +func (e *MailForwardError) Error() string { + return fmt.Sprintf("mail forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) +} + +type ZoneError struct { + Message string `json:"message,omitempty"` + Records []Record `json:"records,omitempty"` + URLForwards []URLForward `json:"urlForwards,omitempty"` + MailForwards []MailForward `json:"mailForwards,omitempty"` + Severity string `json:"severity,omitempty"` +} + +func (e *ZoneError) Error() string { + return fmt.Sprintf("zone error (%s): %s", e.Severity, e.Message) +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 66457c550..519cc93ec 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -68,6 +68,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/edgeone" "github.com/go-acme/lego/v4/providers/dns/efficientip" "github.com/go-acme/lego/v4/providers/dns/epik" + "github.com/go-acme/lego/v4/providers/dns/eurodns" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/go-acme/lego/v4/providers/dns/exoscale" "github.com/go-acme/lego/v4/providers/dns/f5xc" @@ -324,6 +325,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return efficientip.NewDNSProvider() case "epik": return epik.NewDNSProvider() + case "eurodns": + return eurodns.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": From 7b1aa50081643440c853a682ad8a9c2bf706929b Mon Sep 17 00:00:00 2001 From: Dane Date: Sun, 8 Mar 2026 23:02:14 +0000 Subject: [PATCH 93/95] safedns: rename UKFast SafeDNS to ANS SafeDNS (#2877) Co-authored-by: Fernandez Ludovic --- README.md | 80 ++++++++++++------------ cmd/zz_gen_cmd_dnshelp.go | 2 +- docs/content/dns/zz_gen_safedns.md | 8 +-- providers/dns/safedns/internal/client.go | 2 +- providers/dns/safedns/safedns.go | 4 +- providers/dns/safedns/safedns.toml | 4 +- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 3d815387a..45ab93455 100644 --- a/README.md +++ b/README.md @@ -73,202 +73,202 @@ If your DNS provider is not supported, please open an [issue](https://github.com Amazon Route 53 Anexia CloudDNS + ANS SafeDNS ArtFiles - ArvanCloud + ArvanCloud Aurora DNS Autodns Axelname - Azion + Azion Azure (deprecated) Azure DNS Baidu Cloud - Beget.com + Beget.com Binary Lane Bindman Bluecat - Bluecat v2 + Bluecat v2 BookMyName Brandit (deprecated) Bunny - Checkdomain + Checkdomain Civo Cloud.ru CloudDNS - Cloudflare + Cloudflare ClouDNS CloudXNS (Deprecated) ConoHa v2 - ConoHa v3 + ConoHa v3 Constellix Core-Networks CPanel/WHM - Czechia + Czechia DDnss (DynDNS Service) Derak Cloud deSEC.io - Designate DNSaaS for Openstack + Designate DNSaaS for Openstack Digital Ocean DirectAdmin DNS Made Easy - DNSExit + DNSExit dnsHome.de DNSimple DNSPod (deprecated) - Domain Offensive (do.de) + Domain Offensive (do.de) Domeneshop DreamHost Duck DNS - Dyn + Dyn DynDnsFree.de Dynu EasyDNS - EdgeCenter + EdgeCenter Efficient IP Epik EuroDNS - Exoscale + Exoscale External program F5 XC freemyip.com - FusionLayer NameSurfer + FusionLayer NameSurfer G-Core Gandi Gandi Live DNS (v5) - Gigahost.no + Gigahost.no Glesys Go Daddy Google Cloud - Google Domains + Google Domains Gravity Hetzner Hosting.de - Hosting.nl + Hosting.nl Hostinger Hosttech HTTP request - http.net + http.net Huawei Cloud Hurricane Electric DNS HyperOne - IBM Cloud (SoftLayer) + IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox Infomaniak - Internet Initiative Japan + Internet Initiative Japan Internet.bs INWX Ionos - Ionos Cloud + Ionos Cloud IPv64 ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module - iwantmyname (Deprecated) + iwantmyname (Deprecated) JD Cloud Joker Joohoi's ACME-DNS - KeyHelp + KeyHelp Leaseweb Liara Lima-City - Linode (v4) + Linode (v4) Liquid Web Loopia LuaDNS - Mail-in-a-Box + Mail-in-a-Box ManageEngine CloudDNS Manual Metaname - Metaregistrar + Metaregistrar mijn.host Mittwald myaddr.{tools,dev,io} - MyDNS.jp + MyDNS.jp MythicBeasts Name.com Namecheap - Namesilo + Namesilo NearlyFreeSpeech.NET Neodigit Netcup - Netlify + Netlify Nicmanager NIFCloud Njalla - Nodion + Nodion NS1 Octenium Open Telekom Cloud - Oracle Cloud + Oracle Cloud OVH plesk.com Porkbun - PowerDNS + PowerDNS Rackspace Rain Yun/雨云 RcodeZero - reg.ru + reg.ru Regfish RFC2136 RimuHosting - RU CENTER + RU CENTER Sakura Cloud Scaleway Selectel - Selectel v2 + Selectel v2 SelfHost.(de|eu) Servercow Shellrent - Simply.com + Simply.com Sonic Spaceship Stackpath - Syse + Syse Technitium Tencent Cloud DNS Tencent EdgeOne - Timeweb Cloud + Timeweb Cloud TodayNIC/时代互联 TransIP - UKFast SafeDNS Ultradns United-Domains diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 3a6439f00..0ecc012b3 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -3417,7 +3417,7 @@ func displayDNSHelp(w io.Writer, name string) error { case "safedns": // generated from: providers/dns/safedns/safedns.toml - ew.writeln(`Configuration for UKFast SafeDNS.`) + ew.writeln(`Configuration for ANS SafeDNS.`) ew.writeln(`Code: 'safedns'`) ew.writeln(`Since: 'v4.6.0'`) ew.writeln() diff --git a/docs/content/dns/zz_gen_safedns.md b/docs/content/dns/zz_gen_safedns.md index e040a8a9f..4c20fca6a 100644 --- a/docs/content/dns/zz_gen_safedns.md +++ b/docs/content/dns/zz_gen_safedns.md @@ -1,12 +1,12 @@ --- -title: "UKFast SafeDNS" +title: "ANS SafeDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: safedns dnsprovider: since: "v4.6.0" code: "safedns" - url: "https://www.ukfast.co.uk/dns-hosting.html" + url: "https://www.ans.co.uk/" --- @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). +Configuration for [ANS SafeDNS](https://www.ans.co.uk/). @@ -23,7 +23,7 @@ Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). - Since: v4.6.0 -Here is an example bash command using the UKFast SafeDNS provider: +Here is an example bash command using the ANS SafeDNS provider: ```bash SAFEDNS_AUTH_TOKEN=xxxxxx \ diff --git a/providers/dns/safedns/internal/client.go b/providers/dns/safedns/internal/client.go index 51b12e99d..628618032 100644 --- a/providers/dns/safedns/internal/client.go +++ b/providers/dns/safedns/internal/client.go @@ -19,7 +19,7 @@ const defaultBaseURL = "https://api.ukfast.io/safedns/v1" const authorizationHeader = "Authorization" -// Client the UKFast SafeDNS client. +// Client the ANS SafeDNS client. type Client struct { authToken string diff --git a/providers/dns/safedns/safedns.go b/providers/dns/safedns/safedns.go index be8ca4fe6..154cfc5ee 100644 --- a/providers/dns/safedns/safedns.go +++ b/providers/dns/safedns/safedns.go @@ -1,4 +1,4 @@ -// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS. +// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS. package safedns import ( @@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS. +// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("safedns: supplied configuration was nil") diff --git a/providers/dns/safedns/safedns.toml b/providers/dns/safedns/safedns.toml index 188db66a4..f387f2535 100644 --- a/providers/dns/safedns/safedns.toml +++ b/providers/dns/safedns/safedns.toml @@ -1,6 +1,6 @@ -Name = "UKFast SafeDNS" +Name = "ANS SafeDNS" Description = '''''' -URL = "https://www.ukfast.co.uk/dns-hosting.html" +URL = "https://www.ans.co.uk/" Code = "safedns" Since = "v4.6.0" From 9be8cd43ae5de725b39643598e00da367969cab6 Mon Sep 17 00:00:00 2001 From: exsesa <48995997+exsesa@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:58:13 +0100 Subject: [PATCH 94/95] Add DNS provider for Excedo (#2910) Co-authored-by: Fernandez Ludovic --- README.md | 64 +++--- cmd/zz_gen_cmd_dnshelp.go | 22 ++ docs/content/dns/zz_gen_excedo.md | 69 ++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/excedo/excedo.go | 176 +++++++++++++++ providers/dns/excedo/excedo.toml | 24 ++ providers/dns/excedo/excedo_test.go | 210 ++++++++++++++++++ providers/dns/excedo/internal/client.go | 205 +++++++++++++++++ providers/dns/excedo/internal/client_test.go | 137 ++++++++++++ .../excedo/internal/fixtures/addrecord.json | 15 ++ .../internal/fixtures/deleterecord.json | 14 ++ .../dns/excedo/internal/fixtures/error.json | 18 ++ .../excedo/internal/fixtures/getrecords.json | 23 ++ .../dns/excedo/internal/fixtures/login.json | 7 + providers/dns/excedo/internal/identity.go | 75 +++++++ .../dns/excedo/internal/identity_test.go | 35 +++ providers/dns/excedo/internal/types.go | 65 ++++++ providers/dns/zz_gen_dns_providers.go | 3 + 18 files changed, 1131 insertions(+), 33 deletions(-) create mode 100644 docs/content/dns/zz_gen_excedo.md create mode 100644 providers/dns/excedo/excedo.go create mode 100644 providers/dns/excedo/excedo.toml create mode 100644 providers/dns/excedo/excedo_test.go create mode 100644 providers/dns/excedo/internal/client.go create mode 100644 providers/dns/excedo/internal/client_test.go create mode 100644 providers/dns/excedo/internal/fixtures/addrecord.json create mode 100644 providers/dns/excedo/internal/fixtures/deleterecord.json create mode 100644 providers/dns/excedo/internal/fixtures/error.json create mode 100644 providers/dns/excedo/internal/fixtures/getrecords.json create mode 100644 providers/dns/excedo/internal/fixtures/login.json create mode 100644 providers/dns/excedo/internal/identity.go create mode 100644 providers/dns/excedo/internal/identity_test.go create mode 100644 providers/dns/excedo/internal/types.go diff --git a/README.md b/README.md index 45ab93455..e90e94962 100644 --- a/README.md +++ b/README.md @@ -141,165 +141,165 @@ If your DNS provider is not supported, please open an [issue](https://github.com Epik EuroDNS + Excedo Exoscale External program F5 XC - freemyip.com + freemyip.com FusionLayer NameSurfer G-Core Gandi - Gandi Live DNS (v5) + Gandi Live DNS (v5) Gigahost.no Glesys Go Daddy - Google Cloud + Google Cloud Google Domains Gravity Hetzner - Hosting.de + Hosting.de Hosting.nl Hostinger Hosttech - HTTP request + HTTP request http.net Huawei Cloud Hurricane Electric DNS - HyperOne + HyperOne IBM Cloud (SoftLayer) IIJ DNS Platform Service Infoblox - Infomaniak + Infomaniak Internet Initiative Japan Internet.bs INWX - Ionos + Ionos Ionos Cloud IPv64 ISPConfig 3 - ISPConfig 3 - Dynamic DNS (DDNS) Module + ISPConfig 3 - Dynamic DNS (DDNS) Module iwantmyname (Deprecated) JD Cloud Joker - Joohoi's ACME-DNS + Joohoi's ACME-DNS KeyHelp Leaseweb Liara - Lima-City + Lima-City Linode (v4) Liquid Web Loopia - LuaDNS + LuaDNS Mail-in-a-Box ManageEngine CloudDNS Manual - Metaname + Metaname Metaregistrar mijn.host Mittwald - myaddr.{tools,dev,io} + myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.com - Namecheap + Namecheap Namesilo NearlyFreeSpeech.NET Neodigit - Netcup + Netcup Netlify Nicmanager NIFCloud - Njalla + Njalla Nodion NS1 Octenium - Open Telekom Cloud + Open Telekom Cloud Oracle Cloud OVH plesk.com - Porkbun + Porkbun PowerDNS Rackspace Rain Yun/雨云 - RcodeZero + RcodeZero reg.ru Regfish RFC2136 - RimuHosting + RimuHosting RU CENTER Sakura Cloud Scaleway - Selectel + Selectel Selectel v2 SelfHost.(de|eu) Servercow - Shellrent + Shellrent Simply.com Sonic Spaceship - Stackpath + Stackpath Syse Technitium Tencent Cloud DNS - Tencent EdgeOne + Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联 TransIP - Ultradns + Ultradns United-Domains Variomedia VegaDNS - Vercel + Vercel Versio.[nl|eu|uk] VinylDNS Virtualname - VK Cloud + VK Cloud Volcano Engine/火山引擎 Vscale Vultr - webnames.ca + webnames.ca webnames.ru Websupport WEDOS - West.cn/西部数码 + West.cn/西部数码 Yandex 360 Yandex Cloud Yandex PDD - Zone.ee + Zone.ee ZoneEdit Zonomi - diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 0ecc012b3..f73f3920b 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -75,6 +75,7 @@ func allDNSCodes() string { "efficientip", "epik", "eurodns", + "excedo", "exec", "exoscale", "f5xc", @@ -1584,6 +1585,27 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/eurodns`) + case "excedo": + // generated from: providers/dns/excedo/excedo.toml + ew.writeln(`Configuration for Excedo.`) + ew.writeln(`Code: 'excedo'`) + ew.writeln(`Since: 'v4.33.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "EXCEDO_API_KEY": API key`) + ew.writeln(` - "EXCEDO_API_URL": API base URL`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EXCEDO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "EXCEDO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) + ew.writeln(` - "EXCEDO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "EXCEDO_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/excedo`) + case "exec": // generated from: providers/dns/exec/exec.toml ew.writeln(`Configuration for External program.`) diff --git a/docs/content/dns/zz_gen_excedo.md b/docs/content/dns/zz_gen_excedo.md new file mode 100644 index 000000000..456e6f60a --- /dev/null +++ b/docs/content/dns/zz_gen_excedo.md @@ -0,0 +1,69 @@ +--- +title: "Excedo" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: excedo +dnsprovider: + since: "v4.33.0" + code: "excedo" + url: "https://excedo.se/" +--- + + + + + + +Configuration for [Excedo](https://excedo.se/). + + + + +- Code: `excedo` +- Since: v4.33.0 + + +Here is an example bash command using the Excedo provider: + +```bash +EXCEDO_API_KEY=your-api-key \ +EXCEDO_API_URL=your-base-url \ +lego --dns excedo -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EXCEDO_API_KEY` | API key | +| `EXCEDO_API_URL` | API base URL | + +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 | +|--------------------------------|-------------| +| `EXCEDO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `EXCEDO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | +| `EXCEDO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `EXCEDO_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](none) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 5736d0ae8..139143b17 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, 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, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, 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, todaynic, 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/excedo/excedo.go b/providers/dns/excedo/excedo.go new file mode 100644 index 000000000..ae9128b94 --- /dev/null +++ b/providers/dns/excedo/excedo.go @@ -0,0 +1,176 @@ +// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo. +package excedo + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "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/excedo/internal" + "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "EXCEDO_" + + EnvAPIURL = envNamespace + "API_URL" + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIURL string + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 60), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*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 { + config *Config + client *internal.Client + + recordsMu sync.Mutex + records map[string]int64 +} + +// NewDNSProvider returns a DNSProvider instance configured for Excedo. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIURL, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("excedo: %w", err) + } + + config := NewDefaultConfig() + config.APIURL = values[EnvAPIURL] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Excedo. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("excedo: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIURL, config.APIKey) + if err != nil { + return nil, fmt.Errorf("excedo: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + records: make(map[string]int64), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("excedo: %w", err) + } + + record := internal.Record{ + DomainName: dns01.UnFqdn(authZone), + Name: subDomain, + Type: "TXT", + Content: info.Value, + TTL: strconv.Itoa(d.config.TTL), + } + + recordID, err := d.client.AddRecord(ctx, record) + if err != nil { + return fmt.Errorf("excedo: add record: %w", err) + } + + d.recordsMu.Lock() + d.records[token] = recordID + d.recordsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) + } + + d.recordsMu.Lock() + recordID, ok := d.records[token] + d.recordsMu.Unlock() + + if !ok { + return fmt.Errorf("excedo: unknown record ID for '%s'", info.EffectiveFQDN) + } + + err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10)) + if err != nil { + return fmt.Errorf("excedo: delete record: %w", err) + } + + d.recordsMu.Lock() + delete(d.records, token) + d.recordsMu.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/excedo/excedo.toml b/providers/dns/excedo/excedo.toml new file mode 100644 index 000000000..9f9874c62 --- /dev/null +++ b/providers/dns/excedo/excedo.toml @@ -0,0 +1,24 @@ +Name = "Excedo" +Description = '''''' +URL = "https://excedo.se/" +Code = "excedo" +Since = "v4.33.0" + +Example = ''' +EXCEDO_API_KEY=your-api-key \ +EXCEDO_API_URL=your-base-url \ +lego --dns excedo -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + EXCEDO_API_KEY = "API key" + EXCEDO_API_URL = "API base URL" + [Configuration.Additional] + EXCEDO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" + EXCEDO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" + EXCEDO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + EXCEDO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + +[Links] + API = "none" diff --git a/providers/dns/excedo/excedo_test.go b/providers/dns/excedo/excedo_test.go new file mode 100644 index 000000000..f2350c035 --- /dev/null +++ b/providers/dns/excedo/excedo_test.go @@ -0,0 +1,210 @@ +package excedo + +import ( + "net/http/httptest" + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIURL: "https://example.com", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing the API key", + envVars: map[string]string{ + EnvAPIURL: "https://example.com", + EnvAPIKey: "", + }, + expected: "excedo: some credentials information are missing: EXCEDO_API_KEY", + }, + { + desc: "missing the API URL", + envVars: map[string]string{ + EnvAPIURL: "", + EnvAPIKey: "secret", + }, + expected: "excedo: some credentials information are missing: EXCEDO_API_URL", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_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 + apiURL string + apiKey string + expected string + }{ + { + desc: "success", + apiURL: "https://example.com", + apiKey: "secret", + }, + { + desc: "missing the API key", + apiURL: "https://example.com", + expected: "excedo: credentials missing", + }, + { + desc: "missing the API URL", + apiKey: "secret", + expected: "excedo: credentials missing", + }, + { + desc: "missing credentials", + expected: "excedo: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIURL = test.apiURL + config.APIKey = test.apiKey + + 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) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func mockBuilder() *servermock.Builder[*DNSProvider] { + return servermock.NewBuilder( + func(server *httptest.Server) (*DNSProvider, error) { + config := NewDefaultConfig() + config.APIURL = server.URL + config.APIKey = "secret" + config.HTTPClient = server.Client() + + p, err := NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + return p, nil + }, + ) +} + +func TestDNSProvider_Present(t *testing.T) { + provider := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromInternal("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Route("POST /dns/addrecord/", + servermock.ResponseFromInternal("addrecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckForm().Strict(). + With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("domainName", "example.com"). + With("name", "_acme-challenge"). + With("ttl", "60"). + With("type", "TXT"), + ). + Build(t) + + err := provider.Present("example.com", "abc", "123d==") + require.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + provider := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromInternal("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Route("POST /dns/deleterecord/", + servermock.ResponseFromInternal("deleterecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + ). + Build(t) + + provider.records["abc"] = 19695822 + + err := provider.CleanUp("example.com", "abc", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/excedo/internal/client.go b/providers/dns/excedo/internal/client.go new file mode 100644 index 000000000..a5d8be88b --- /dev/null +++ b/providers/dns/excedo/internal/client.go @@ -0,0 +1,205 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + "github.com/go-acme/lego/v4/providers/dns/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +type responseChecker interface { + Check() error +} + +// Client the Excedo API client. +type Client struct { + apiKey string + + baseURL *url.URL + HTTPClient *http.Client + + token *ExpirableToken + muToken sync.Mutex +} + +// NewClient creates a new Client. +func NewClient(apiURL, apiKey string) (*Client, error) { + if apiURL == "" || apiKey == "" { + return nil, errors.New("credentials missing") + } + + baseURL, err := url.Parse(apiURL) + if err != nil { + return nil, err + } + + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +func (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) { + payload, err := querystring.Values(record) + if err != nil { + return 0, err + } + + endpoint := c.baseURL.JoinPath("/dns/addrecord/") + + req, err := newFormRequest(ctx, http.MethodPost, endpoint, payload) + if err != nil { + return 0, err + } + + result := new(AddRecordResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return 0, err + } + + return result.RecordID, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { + endpoint := c.baseURL.JoinPath("/dns/deleterecord/") + + data := map[string]string{ + "domainname": dns01.UnFqdn(zone), + "recordid": recordID, + } + + req, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data) + if err != nil { + return err + } + + result := new(BaseResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) { + endpoint := c.baseURL.JoinPath("/dns/getrecords/") + + query := endpoint.Query() + query.Set("domainname", zone) + + endpoint.RawQuery = query.Encode() + + req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + result := new(GetRecordsResponse) + + err = c.doAuthenticated(ctx, req, result) + if err != nil { + return nil, err + } + + return result.DNS, nil +} + +func (c *Client) do(req *http.Request, result responseChecker) error { + useragent.SetHeader(req.Header) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + raw, _ := io.ReadAll(resp.Body) + + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return result.Check() +} + +func newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) { + buf := new(bytes.Buffer) + + writer := multipart.NewWriter(buf) + + for k, v := range data { + err := writer.WriteField(k, v) + if err != nil { + return nil, err + } + } + + err := writer.Close() + if err != nil { + return nil, err + } + + body := bytes.NewReader(buf.Bytes()) + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + return req, nil +} + +func newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) { + var body io.Reader + + if len(form) > 0 { + body = bytes.NewReader([]byte(form.Encode())) + } else { + body = http.NoBody + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + if method == http.MethodPost { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + return req, nil +} diff --git a/providers/dns/excedo/internal/client_test.go b/providers/dns/excedo/internal/client_test.go new file mode 100644 index 000000000..f4fd52c00 --- /dev/null +++ b/providers/dns/excedo/internal/client_test.go @@ -0,0 +1,137 @@ +package internal + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder[*Client]( + func(server *httptest.Server) (*Client, error) { + client, err := NewClient(server.URL, "secret") + if err != nil { + return nil, err + } + + client.HTTPClient = server.Client() + + return client, nil + }, + ) +} + +func TestClient_AddRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/addrecord/", + servermock.ResponseFromFixture("addrecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckForm().Strict(). + With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). + With("domainName", "example.com"). + With("name", "_acme-challenge"). + With("ttl", "60"). + With("type", "TXT"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + record := Record{ + DomainName: "example.com", + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "60", + } + + recordID, err := client.AddRecord(t.Context(), record) + require.NoError(t, err) + + assert.EqualValues(t, 19695822, recordID) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/addrecord/", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + record := Record{ + DomainName: "example.com", + Name: "_acme-challenge", + Type: "TXT", + Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", + TTL: "60", + } + + _, err := client.AddRecord(t.Context(), record) + require.EqualError(t, err, "2003: Required parameter missing") +} + +func TestClient_DeleteRecord(t *testing.T) { + client := mockBuilder(). + Route("POST /dns/deleterecord/", + servermock.ResponseFromFixture("deleterecord.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + err := client.DeleteRecord(t.Context(), "example.com", "19695822") + require.NoError(t, err) +} + +func TestClient_GetRecords(t *testing.T) { + client := mockBuilder(). + Route("GET /dns/getrecords/", + servermock.ResponseFromFixture("getrecords.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer session-token"), + servermock.CheckQueryParameter().Strict(). + With("domainname", "example.com"), + ). + Build(t) + + client.token = &ExpirableToken{ + Token: "session-token", + Expires: time.Now().Add(6 * time.Hour), + } + + zones, err := client.GetRecords(t.Context(), "example.com") + require.NoError(t, err) + + expected := map[string]Zone{ + "example.com": { + DNSType: "type", + Records: []Record{{ + RecordID: "1234", + Name: "_acme-challenge.example.com", + Type: "TXT", + Content: "txt-value", + TTL: "60", + }}, + }, + } + + assert.Equal(t, expected, zones) +} diff --git a/providers/dns/excedo/internal/fixtures/addrecord.json b/providers/dns/excedo/internal/fixtures/addrecord.json new file mode 100644 index 000000000..f1f7bf958 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/addrecord.json @@ -0,0 +1,15 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "recordid": 19695822, + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5678 + } + }, + "runtime": 0.2852 +} diff --git a/providers/dns/excedo/internal/fixtures/deleterecord.json b/providers/dns/excedo/internal/fixtures/deleterecord.json new file mode 100644 index 000000000..5c2431b1c --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/deleterecord.json @@ -0,0 +1,14 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5678 + } + }, + "runtime": 0.2852 +} diff --git a/providers/dns/excedo/internal/fixtures/error.json b/providers/dns/excedo/internal/fixtures/error.json new file mode 100644 index 000000000..5a24ec247 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/error.json @@ -0,0 +1,18 @@ +{ + "code": 2003, + "desc": "Required parameter missing", + "missing": [ + "domainname", + "recordid" + ], + "session": { + "accID": "1234", + "usrID": "1234", + "status": "active", + "expire": { + "date": "2026-03-10 19:03:18", + "seconds": 5485 + } + }, + "runtime": 0.0534 +} diff --git a/providers/dns/excedo/internal/fixtures/getrecords.json b/providers/dns/excedo/internal/fixtures/getrecords.json new file mode 100644 index 000000000..215a8abb2 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/getrecords.json @@ -0,0 +1,23 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "dns": { + "example.com": { + "dnstype": "type", + "recordusage": { + "used": 74 + }, + "records": [ + { + "recordid": "1234", + "name": "_acme-challenge.example.com", + "type": "TXT", + "content": "txt-value", + "ttl": "60", + "prio": null, + "change_date": null + } + ] + } + } +} diff --git a/providers/dns/excedo/internal/fixtures/login.json b/providers/dns/excedo/internal/fixtures/login.json new file mode 100644 index 000000000..2defb9843 --- /dev/null +++ b/providers/dns/excedo/internal/fixtures/login.json @@ -0,0 +1,7 @@ +{ + "code": 1000, + "desc": "Command completed successfully", + "parameters": { + "token": "session-token" + } +} diff --git a/providers/dns/excedo/internal/identity.go b/providers/dns/excedo/internal/identity.go new file mode 100644 index 000000000..5c9ca119d --- /dev/null +++ b/providers/dns/excedo/internal/identity.go @@ -0,0 +1,75 @@ +package internal + +import ( + "context" + "fmt" + "net/http" + "time" +) + +type ExpirableToken struct { + Token string + Expires time.Time +} + +func (t *ExpirableToken) IsExpired() bool { + return time.Now().After(t.Expires) +} + +func (c *Client) Login(ctx context.Context) (string, error) { + endpoint := c.baseURL.JoinPath("/authenticate/login/") + + req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + result := new(LoginResponse) + + err = c.do(req, result) + if err != nil { + return "", err + } + + if result.Code != 1000 && result.Code != 1300 { + return "", fmt.Errorf("%d: %s", result.Code, result.Description) + } + + return result.Parameters.Token, nil +} + +func (c *Client) authenticate(ctx context.Context) (string, error) { + c.muToken.Lock() + defer c.muToken.Unlock() + + if c.token == nil || c.token.IsExpired() { + token, err := c.Login(ctx) + if err != nil { + return "", err + } + + c.token = &ExpirableToken{ + Token: token, + Expires: time.Now().Add(2*time.Hour - time.Minute), + } + + return token, nil + } + + return c.token.Token, nil +} + +func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error { + token, err := c.authenticate(ctx) + if err != nil { + return err + } + + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + return c.do(req, result) +} diff --git a/providers/dns/excedo/internal/identity_test.go b/providers/dns/excedo/internal/identity_test.go new file mode 100644 index 000000000..86b7eb9d8 --- /dev/null +++ b/providers/dns/excedo/internal/identity_test.go @@ -0,0 +1,35 @@ +package internal + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_Login(t *testing.T) { + client := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromFixture("login.json"), + servermock.CheckHeader(). + WithAuthorization("Bearer secret"), + ). + Build(t) + + token, err := client.Login(t.Context()) + require.NoError(t, err) + + assert.Equal(t, "session-token", token) +} + +func TestClient_Login_error(t *testing.T) { + client := mockBuilder(). + Route("GET /authenticate/login/", + servermock.ResponseFromFixture("error.json"), + ). + Build(t) + + _, err := client.Login(t.Context()) + require.EqualError(t, err, "2003: Required parameter missing") +} diff --git a/providers/dns/excedo/internal/types.go b/providers/dns/excedo/internal/types.go new file mode 100644 index 000000000..eb6ce8462 --- /dev/null +++ b/providers/dns/excedo/internal/types.go @@ -0,0 +1,65 @@ +package internal + +import "fmt" + +type BaseResponse struct { + Code int `json:"code"` + Description string `json:"desc"` +} + +func (r BaseResponse) Check() error { + // Response codes: + // - 1000: Command completed successfully + // - 1300: Command completed successfully; no messages + // - 2001: Command syntax error + // - 2002: Command use error + // - 2003: Required parameter missing + // - 2004: Parameter value range error + // - 2104: Billing failure + // - 2200: Authentication error + // - 2201: Authorization error + // - 2303: Object does not exist + // - 2304: Object status prohibits operation + // - 2309: Object duplicate found + // - 2400: Command failed + // - 2500: Command failed; server closing connection + if r.Code != 1000 && r.Code != 1300 { + return fmt.Errorf("%d: %s", r.Code, r.Description) + } + + return nil +} + +type GetRecordsResponse struct { + BaseResponse + + DNS map[string]Zone `json:"dns"` +} + +type Zone struct { + DNSType string `json:"dnstype"` + Records []Record `json:"records"` +} + +type Record struct { + DomainName string `json:"domainName,omitempty" url:"domainName,omitempty"` + RecordID string `json:"recordid,omitempty" url:"recordid,omitempty"` + Name string `json:"name,omitempty" url:"name,omitempty"` + Type string `json:"type,omitempty" url:"type,omitempty"` + Content string `json:"content,omitempty" url:"content,omitempty"` + TTL string `json:"ttl,omitempty" url:"ttl,omitempty"` +} + +type AddRecordResponse struct { + BaseResponse + + RecordID int64 `json:"recordid"` +} + +type LoginResponse struct { + BaseResponse + + Parameters struct { + Token string `json:"token"` + } `json:"parameters"` +} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 519cc93ec..9c4bc9e61 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -69,6 +69,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/efficientip" "github.com/go-acme/lego/v4/providers/dns/epik" "github.com/go-acme/lego/v4/providers/dns/eurodns" + "github.com/go-acme/lego/v4/providers/dns/excedo" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/go-acme/lego/v4/providers/dns/exoscale" "github.com/go-acme/lego/v4/providers/dns/f5xc" @@ -327,6 +328,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return epik.NewDNSProvider() case "eurodns": return eurodns.NewDNSProvider() + case "excedo": + return excedo.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": From 87b172f103b26c8ea40c5e811576ad454f7b6891 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Mar 2026 21:27:46 +0100 Subject: [PATCH 95/95] gigahostno: remove unused Zone fields (#2913) --- .../dns/gigahostno/internal/client_test.go | 60 +++++-------------- .../gigahostno/internal/fixtures/zones.json | 6 +- providers/dns/gigahostno/internal/types.go | 20 ++----- 3 files changed, 23 insertions(+), 63 deletions(-) diff --git a/providers/dns/gigahostno/internal/client_test.go b/providers/dns/gigahostno/internal/client_test.go index aac65bceb..8d1298947 100644 --- a/providers/dns/gigahostno/internal/client_test.go +++ b/providers/dns/gigahostno/internal/client_test.go @@ -38,55 +38,25 @@ func TestClient_GetZones(t *testing.T) { expected := []Zone{ { - ID: "123", - Name: "example.com", - NameDisplay: "example.com", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-11-23 15:17:38", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 4, + ID: "123", + Name: "example.com", + NameDisplay: "example.com", + Type: "NATIVE", + Active: "1", }, { - ID: "226", - Name: "example.org", - NameDisplay: "example.org", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-11-23 14:15:01", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 5, + ID: "226", + Name: "example.org", + NameDisplay: "example.org", + Type: "NATIVE", + Active: "1", }, { - ID: "229", - Name: "example.xn--zckzah", - NameDisplay: "example.テスト", - Type: "NATIVE", - Active: "1", - Protected: "1", - IsRegistered: "1", - Updated: false, - CustomerID: "16030", - DomainRegistrar: "norid", - DomainStatus: "active", - DomainExpiryDate: "2026-12-01 12:40:48", - DomainAutoRenew: "1", - ExternalDNS: "0", - RecordCount: 4, + ID: "229", + Name: "example.xn--zckzah", + NameDisplay: "example.テスト", + Type: "NATIVE", + Active: "1", }, } diff --git a/providers/dns/gigahostno/internal/fixtures/zones.json b/providers/dns/gigahostno/internal/fixtures/zones.json index f4d927335..d45b0ac49 100644 --- a/providers/dns/gigahostno/internal/fixtures/zones.json +++ b/providers/dns/gigahostno/internal/fixtures/zones.json @@ -30,7 +30,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 16:17:29", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 4, "zone_name_display": "example.com" @@ -59,7 +59,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 15:13:27", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 5, "zone_name_display": "example.org" @@ -88,7 +88,7 @@ "domain_dnssec_data": null, "domain_protected_email": null, "zone_created": "2025-11-23 16:37:15", - "zone_updated": false, + "zone_updated": 1700000000, "external_dns": "0", "record_count": 4, "zone_name_display": "example.\u30C6\u30B9\u30C8" diff --git a/providers/dns/gigahostno/internal/types.go b/providers/dns/gigahostno/internal/types.go index cbb7b8b23..e998dc084 100644 --- a/providers/dns/gigahostno/internal/types.go +++ b/providers/dns/gigahostno/internal/types.go @@ -26,21 +26,11 @@ type APIResponse[T any] struct { } type Zone struct { - ID string `json:"zone_id,omitempty"` - Name string `json:"zone_name,omitempty"` - NameDisplay string `json:"zone_name_display,omitempty"` - Type string `json:"zone_type,omitempty"` - Active string `json:"zone_active,omitempty"` - Protected string `json:"zone_protected,omitempty"` - IsRegistered string `json:"zone_is_registered,omitempty"` - Updated bool `json:"zone_updated,omitempty"` - CustomerID string `json:"cust_id,omitempty"` - DomainRegistrar string `json:"domain_registrar,omitempty"` - DomainStatus string `json:"domain_status,omitempty"` - DomainExpiryDate string `json:"domain_expiry_date,omitempty"` - DomainAutoRenew string `json:"domain_auto_renew,omitempty"` - ExternalDNS string `json:"external_dns,omitempty"` - RecordCount int `json:"record_count,omitempty"` + ID string `json:"zone_id,omitempty"` + Name string `json:"zone_name,omitempty"` + NameDisplay string `json:"zone_name_display,omitempty"` + Type string `json:"zone_type,omitempty"` + Active string `json:"zone_active,omitempty"` } type Record struct {