diff --git a/README.md b/README.md
index f5715277d1..753b270f34 100644
--- a/README.md
+++ b/README.md
@@ -110,180 +110,185 @@ If your DNS provider is not supported, please open an [issue](https://github.com
Constellix |
Core-Networks |
CPanel/WHM |
- Czechia |
+ Curanet |
+ | Czechia |
+ DanDomain |
DDnss (DynDNS Service) |
Derak Cloud |
+
| deSEC.io |
Designate DNSaaS for Openstack |
-
| Digital Ocean |
DirectAdmin |
+
| DNS Made Easy |
DNS Update (RFC2136) |
-
| DNSExit |
dnsHome.de |
+
| DNSimple |
Domain Offensive (do.de) |
-
| Domeneshop |
DreamHost |
+
| Duck DNS |
Dyn |
-
| DynDnsFree.de |
Dynu |
+
| EasyDNS |
EdgeCenter |
-
| Efficient IP |
Epik |
+
| EuroDNS |
Excedo |
-
| Exoscale |
External program |
+
| F5 XC |
freemyip.com |
-
| FusionLayer NameSurfer |
G-Core |
+
| Gandi |
Gandi Live DNS (v5) |
-
| Gigahost.no |
Glesys |
+
| Go Daddy |
Google Cloud |
-
| Gravity |
Hetzner |
+
| Hosting.de |
Hosting.nl |
-
| Hostinger |
Hosttech |
+
| HTTP request |
http.net |
-
| Huawei Cloud |
Hurricane Electric DNS |
+
| HyperOne |
IBM Cloud (SoftLayer) |
-
| IIJ DNS Platform Service |
Infoblox |
+
| Infomaniak |
Internet.bs |
-
| INWX |
Ionos |
+
| Ionos Cloud |
IPv64 |
-
| ISPConfig 3 |
ISPConfig 3 - Dynamic DNS (DDNS) Module |
+
| JD Cloud |
Joker |
-
| Joohoi's ACME-DNS |
KeyHelp |
+
| Leaseweb |
Liara |
-
| Lima-City |
Linode (v4) |
+
| Liquid Web |
Loopia |
-
| LuaDNS |
Mail-in-a-Box |
+
| ManageEngine CloudDNS |
Manual |
-
| Metaname |
Metaregistrar |
+
| mijn.host |
Mittwald |
-
| myaddr.{tools,dev,io} |
MyDNS.jp |
+
| MythicBeasts |
Name.com |
-
| Namecheap |
Namesilo |
+
| NearlyFreeSpeech.NET |
Neodigit |
-
| Netcup |
Netlify |
+
| Netnod |
Nicmanager |
-
| NIFCloud |
Njalla |
+
| Nodion |
NS1 |
-
| Octenium |
Online.net |
+
| Open Telekom Cloud |
Oracle Cloud |
-
| OVH |
plesk.com |
+
| Porkbun |
PowerDNS |
-
| Rackspace |
Rain Yun/雨云 |
+
| RcodeZero |
reg.ru |
-
| Regfish |
RimuHosting |
+
| RU CENTER |
Sakura Cloud |
-
| Scaleway |
+ ScanNet |
+
| Selectel |
Selectel v2 |
SelfHost.(de|eu) |
-
| Servercow |
+
| Shellrent |
Simply.com |
Sonic |
-
| Spaceship |
+
| Stackpath |
Syse |
Technitium |
-
| Tencent Cloud DNS |
+
| Tencent EdgeOne |
Timeweb Cloud |
TodayNIC/时代互联 |
-
| TransIP |
+
| UCloud |
Ultradns |
United-Domains |
-
| Variomedia |
+
| VegaDNS |
Vercel |
Versio.[nl|eu|uk] |
-
| VinylDNS |
+
| Virtualname |
VK Cloud |
Volcano Engine/火山引擎 |
-
| Vscale |
+
| Vultr |
+ Wannafind |
webnames.ca |
webnames.ru |
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index ff69e7beee..7f9b93cee0 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -47,7 +47,9 @@ func allDNSCodes() string {
"constellix",
"corenetworks",
"cpanel",
+ "curanet",
"czechia",
+ "dandomain",
"ddnss",
"derak",
"desec",
@@ -162,6 +164,7 @@ func allDNSCodes() string {
"safedns",
"sakuracloud",
"scaleway",
+ "scannet",
"selectel",
"selectelv2",
"selfhostde",
@@ -190,6 +193,7 @@ func allDNSCodes() string {
"volcengine",
"vscale",
"vultr",
+ "wannafind",
"webnamesca",
"webnamesru",
"websupport",
@@ -977,6 +981,26 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`)
+ case "curanet":
+ // generated from: providers/dns/curanet/curanet.toml
+ ew.writeln(`Configuration for Curanet.`)
+ ew.writeln(`Code: 'curanet'`)
+ ew.writeln(`Since: 'v5.0.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "CURANET_API_KEY": API key`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "CURANET_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "CURANET_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "CURANET_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "CURANET_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/curanet`)
+
case "czechia":
// generated from: providers/dns/czechia/czechia.toml
ew.writeln(`Configuration for Czechia.`)
@@ -997,6 +1021,26 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`)
+ case "dandomain":
+ // generated from: providers/dns/dandomain/dandomain.toml
+ ew.writeln(`Configuration for DanDomain.`)
+ ew.writeln(`Code: 'dandomain'`)
+ ew.writeln(`Since: 'v5.0.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "DANDOMAIN_API_KEY": API key`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "DANDOMAIN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "DANDOMAIN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "DANDOMAIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "DANDOMAIN_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/dandomain`)
+
case "ddnss":
// generated from: providers/dns/ddnss/ddnss.toml
ew.writeln(`Configuration for DDnss (DynDNS Service).`)
@@ -3438,6 +3482,26 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/scaleway`)
+ case "scannet":
+ // generated from: providers/dns/scannet/scannet.toml
+ ew.writeln(`Configuration for ScanNet.`)
+ ew.writeln(`Code: 'scannet'`)
+ ew.writeln(`Since: 'v5.0.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "SCANNET_API_KEY": API key`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "SCANNET_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "SCANNET_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "SCANNET_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "SCANNET_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/scannet`)
+
case "selectel":
// generated from: providers/dns/selectel/selectel.toml
ew.writeln(`Configuration for Selectel.`)
@@ -4040,6 +4104,26 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`)
+ case "wannafind":
+ // generated from: providers/dns/wannafind/wannafind.toml
+ ew.writeln(`Configuration for Wannafind.`)
+ ew.writeln(`Code: 'wannafind'`)
+ ew.writeln(`Since: 'v5.0.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "WANNAFIND_API_KEY": API key`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "WANNAFIND_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "WANNAFIND_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "WANNAFIND_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "WANNAFIND_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/wannafind`)
+
case "webnamesca":
// generated from: providers/dns/webnamesca/webnamesca.toml
ew.writeln(`Configuration for webnames.ca.`)
diff --git a/docs/content/dns/zz_gen_curanet.md b/docs/content/dns/zz_gen_curanet.md
new file mode 100644
index 0000000000..27814d16a8
--- /dev/null
+++ b/docs/content/dns/zz_gen_curanet.md
@@ -0,0 +1,67 @@
+---
+title: "Curanet"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: curanet
+dnsprovider:
+ since: "v5.0.0"
+ code: "curanet"
+ url: "https://curanet.dk/"
+---
+
+
+
+
+
+
+Configuration for [Curanet](https://curanet.dk/).
+
+
+
+
+- Code: `curanet`
+- Since: v5.0.0
+
+
+Here is an example bash command using the Curanet provider:
+
+```bash
+CURANET_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns curanet -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `CURANET_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 |
+|--------------------------------|-------------|
+| `CURANET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `CURANET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `CURANET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `CURANET_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.curanet.dk/dns/swagger/index.html)
+
+
+
+
diff --git a/docs/content/dns/zz_gen_dandomain.md b/docs/content/dns/zz_gen_dandomain.md
new file mode 100644
index 0000000000..6553467d72
--- /dev/null
+++ b/docs/content/dns/zz_gen_dandomain.md
@@ -0,0 +1,67 @@
+---
+title: "DanDomain"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: dandomain
+dnsprovider:
+ since: "v5.0.0"
+ code: "dandomain"
+ url: "https://dandomain.dk/"
+---
+
+
+
+
+
+
+Configuration for [DanDomain](https://dandomain.dk/).
+
+
+
+
+- Code: `dandomain`
+- Since: v5.0.0
+
+
+Here is an example bash command using the DanDomain provider:
+
+```bash
+DANDOMAIN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns dandomain -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `DANDOMAIN_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 |
+|--------------------------------|-------------|
+| `DANDOMAIN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `DANDOMAIN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `DANDOMAIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `DANDOMAIN_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.dandomain.dk/dns/swagger/index.html)
+
+
+
+
diff --git a/docs/content/dns/zz_gen_scannet.md b/docs/content/dns/zz_gen_scannet.md
new file mode 100644
index 0000000000..ae5dfad223
--- /dev/null
+++ b/docs/content/dns/zz_gen_scannet.md
@@ -0,0 +1,67 @@
+---
+title: "ScanNet"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: scannet
+dnsprovider:
+ since: "v5.0.0"
+ code: "scannet"
+ url: "https://www.scannet.dk/"
+---
+
+
+
+
+
+
+Configuration for [ScanNet](https://www.scannet.dk/).
+
+
+
+
+- Code: `scannet`
+- Since: v5.0.0
+
+
+Here is an example bash command using the ScanNet provider:
+
+```bash
+SCANNET_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns scannet -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `SCANNET_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 |
+|--------------------------------|-------------|
+| `SCANNET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `SCANNET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `SCANNET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `SCANNET_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.scannet.dk/dns/swagger/index.html)
+
+
+
+
diff --git a/docs/content/dns/zz_gen_wannafind.md b/docs/content/dns/zz_gen_wannafind.md
new file mode 100644
index 0000000000..f4df65101f
--- /dev/null
+++ b/docs/content/dns/zz_gen_wannafind.md
@@ -0,0 +1,67 @@
+---
+title: "Wannafind"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: wannafind
+dnsprovider:
+ since: "v5.0.0"
+ code: "wannafind"
+ url: "https://www.wannafind.dk/"
+---
+
+
+
+
+
+
+Configuration for [Wannafind](https://www.wannafind.dk/).
+
+
+
+
+- Code: `wannafind`
+- Since: v5.0.0
+
+
+Here is an example bash command using the Wannafind provider:
+
+```bash
+WANNAFIND_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns wannafind -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `WANNAFIND_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 |
+|--------------------------------|-------------|
+| `WANNAFIND_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `WANNAFIND_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `WANNAFIND_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `WANNAFIND_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.wannafind.dk/dns/swagger/index.html)
+
+
+
+
diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml
index 4e0f33dda4..27970cb55c 100644
--- a/docs/data/zz_cli_help.toml
+++ b/docs/data/zz_cli_help.toml
@@ -467,7 +467,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
- abion, acmedns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnsupdate, 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, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, 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, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ucloud, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
+ abion, acmedns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, com35, conoha, conohav3, constellix, corenetworks, cpanel, curanet, czechia, dandomain, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnsupdate, 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, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, 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, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rimuhosting, route53, safedns, sakuracloud, scaleway, scannet, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ucloud, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, wannafind, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""
diff --git a/providers/dns/curanet/curanet.go b/providers/dns/curanet/curanet.go
new file mode 100644
index 0000000000..c87499d016
--- /dev/null
+++ b/providers/dns/curanet/curanet.go
@@ -0,0 +1,100 @@
+// Package curanet implements a DNS provider for solving the DNS-01 challenge using Curanet.
+package curanet
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/go-acme/lego/v5/challenge"
+ "github.com/go-acme/lego/v5/challenge/dns01"
+ "github.com/go-acme/lego/v5/internal/env"
+ "github.com/go-acme/lego/v5/providers/dns/internal/curanet"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "CURANET_"
+
+ 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 = curanet.Config
+
+// 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 {
+ prv challenge.ProviderTimeout
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Curanet.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIKey)
+ if err != nil {
+ return nil, fmt.Errorf("curanet: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIKey = values[EnvAPIKey]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for Curanet.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("curanet: the configuration of the DNS provider is nil")
+ }
+
+ provider, err := curanet.NewDNSProviderConfig(config, "")
+ if err != nil {
+ return nil, fmt.Errorf("curanet: %w", err)
+ }
+
+ return &DNSProvider{prv: provider}, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.Present(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("curanet: %w", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.CleanUp(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("curanet: %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/curanet/curanet.toml b/providers/dns/curanet/curanet.toml
new file mode 100644
index 0000000000..603c455fb9
--- /dev/null
+++ b/providers/dns/curanet/curanet.toml
@@ -0,0 +1,22 @@
+Name = "Curanet"
+Description = ''''''
+URL = "https://curanet.dk/"
+Code = "curanet"
+Since = "v5.0.0"
+
+Example = '''
+CURANET_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns curanet -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ CURANET_API_KEY = "API key"
+ [Configuration.Additional]
+ CURANET_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ CURANET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ CURANET_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ CURANET_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://api.curanet.dk/dns/swagger/index.html"
diff --git a/providers/dns/curanet/curanet_test.go b/providers/dns/curanet/curanet_test.go
new file mode 100644
index 0000000000..682057de40
--- /dev/null
+++ b/providers/dns/curanet/curanet_test.go
@@ -0,0 +1,114 @@
+package curanet
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/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: "secret",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{},
+ expected: "curanet: some credentials information are missing: CURANET_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.prv)
+ } 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: "curanet: 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.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(t.Context(), 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(t.Context(), envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/dandomain/dandomain.go b/providers/dns/dandomain/dandomain.go
new file mode 100644
index 0000000000..c71b6140ab
--- /dev/null
+++ b/providers/dns/dandomain/dandomain.go
@@ -0,0 +1,100 @@
+// Package dandomain implements a DNS provider for solving the DNS-01 challenge using DanDomain.
+package dandomain
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/go-acme/lego/v5/challenge"
+ "github.com/go-acme/lego/v5/challenge/dns01"
+ "github.com/go-acme/lego/v5/internal/env"
+ "github.com/go-acme/lego/v5/providers/dns/internal/curanet"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "DANDOMAIN_"
+
+ 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 = curanet.Config
+
+// 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 {
+ prv challenge.ProviderTimeout
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for DanDomain.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIKey)
+ if err != nil {
+ return nil, fmt.Errorf("dandomain: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIKey = values[EnvAPIKey]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for DanDomain.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("dandomain: the configuration of the DNS provider is nil")
+ }
+
+ provider, err := curanet.NewDNSProviderConfig(config, "")
+ if err != nil {
+ return nil, fmt.Errorf("dandomain: %w", err)
+ }
+
+ return &DNSProvider{prv: provider}, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.Present(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("dandomain: %w", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.CleanUp(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("dandomain: %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/dandomain/dandomain.toml b/providers/dns/dandomain/dandomain.toml
new file mode 100644
index 0000000000..0d20fa9eca
--- /dev/null
+++ b/providers/dns/dandomain/dandomain.toml
@@ -0,0 +1,22 @@
+Name = "DanDomain"
+Description = ''''''
+URL = "https://dandomain.dk/"
+Code = "dandomain"
+Since = "v5.0.0"
+
+Example = '''
+DANDOMAIN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns dandomain -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ DANDOMAIN_API_KEY = "API key"
+ [Configuration.Additional]
+ DANDOMAIN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ DANDOMAIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ DANDOMAIN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ DANDOMAIN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://api.dandomain.dk/dns/swagger/index.html"
diff --git a/providers/dns/dandomain/dandomain_test.go b/providers/dns/dandomain/dandomain_test.go
new file mode 100644
index 0000000000..3a1d34e9fd
--- /dev/null
+++ b/providers/dns/dandomain/dandomain_test.go
@@ -0,0 +1,114 @@
+package dandomain
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/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: "secret",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{},
+ expected: "dandomain: some credentials information are missing: DANDOMAIN_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.prv)
+ } 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: "dandomain: 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.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(t.Context(), 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(t.Context(), envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/internal/curanet/internal/client.go b/providers/dns/internal/curanet/internal/client.go
new file mode 100644
index 0000000000..f736058294
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/client.go
@@ -0,0 +1,164 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+
+ "github.com/go-acme/lego/v5/internal/errutils"
+ "github.com/go-acme/lego/v5/internal/useragent"
+)
+
+const defaultBaseURL = "https://api.curanet.dk/"
+
+// Client the Curanet 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
+}
+
+func (c *Client) CreateRecord(ctx context.Context, domain string, record Record) error {
+ endpoint := c.BaseURL.JoinPath("dns", "v2", "Domains", domain, "Records")
+
+ req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
+ if err != nil {
+ return err
+ }
+
+ return c.do(req, nil)
+}
+
+func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error {
+ endpoint := c.BaseURL.JoinPath("dns", "v2", "Domains", domain, "Records", 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) GetRecords(ctx context.Context, domain, name, rType string) ([]Record, error) {
+ endpoint := c.BaseURL.JoinPath("dns", "v2", "Domains", domain, "Records")
+
+ query := endpoint.Query()
+
+ if name != "" {
+ query.Set("name", name)
+ }
+
+ if rType != "" {
+ query.Set("type", rType)
+ }
+
+ 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
+}
+
+func (c *Client) do(req *http.Request, result any) error {
+ useragent.SetHeader(req.Header)
+
+ req.Header.Set("Authorization", "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/internal/curanet/internal/client_test.go b/providers/dns/internal/curanet/internal/client_test.go
new file mode 100644
index 0000000000..e7ed428e66
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/client_test.go
@@ -0,0 +1,99 @@
+package internal
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/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_CreateRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /dns/v2/Domains/example.com/Records",
+ servermock.Noop().
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBodyFromFixture("records_create-request.json"),
+ ).
+ Build(t)
+
+ record := Record{
+ Name: "_acme-challenge",
+ Type: "TXT",
+ TTL: 120,
+ Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
+ }
+
+ err := client.CreateRecord(t.Context(), "example.com", record)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /dns/v2/Domains/example.com/Records/1234",
+ servermock.Noop().
+ WithStatusCode(http.StatusOK),
+ ).
+ 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/v2/Domains/example.com/Records/1234",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest),
+ ).
+ Build(t)
+
+ err := client.DeleteRecord(t.Context(), "example.com", 1234)
+ require.EqualError(t, err, "type: string, title: string, detail: string, instance: string")
+}
+
+func TestClient_GetRecords(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /dns/v2/Domains/example.com/Records",
+ servermock.ResponseFromFixture("records_get.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge").
+ With("type", "TXT"),
+ ).
+ Build(t)
+
+ records, err := client.GetRecords(t.Context(), "example.com", "_acme-challenge", "TXT")
+ require.NoError(t, err)
+
+ expected := []Record{{
+ ID: 1234,
+ Name: "_acme-challenge",
+ Type: "TXT",
+ TTL: 120,
+ Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
+ }}
+
+ assert.Equal(t, expected, records)
+}
diff --git a/providers/dns/internal/curanet/internal/fixtures/error.json b/providers/dns/internal/curanet/internal/fixtures/error.json
new file mode 100644
index 0000000000..6ce06ac523
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/fixtures/error.json
@@ -0,0 +1,10 @@
+{
+ "type": "string",
+ "title": "string",
+ "status": 0,
+ "detail": "string",
+ "instance": "string",
+ "additionalProp1": "string",
+ "additionalProp2": "string",
+ "additionalProp3": "string"
+}
diff --git a/providers/dns/internal/curanet/internal/fixtures/records_create-request.json b/providers/dns/internal/curanet/internal/fixtures/records_create-request.json
new file mode 100644
index 0000000000..5f99b70293
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/fixtures/records_create-request.json
@@ -0,0 +1,6 @@
+{
+ "name": "_acme-challenge",
+ "type": "TXT",
+ "ttl": 120,
+ "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
+}
diff --git a/providers/dns/internal/curanet/internal/fixtures/records_get.json b/providers/dns/internal/curanet/internal/fixtures/records_get.json
new file mode 100644
index 0000000000..c007a47a3b
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/fixtures/records_get.json
@@ -0,0 +1,9 @@
+[
+ {
+ "id": 1234,
+ "name": "_acme-challenge",
+ "type": "TXT",
+ "ttl": 120,
+ "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
+ }
+]
diff --git a/providers/dns/internal/curanet/internal/types.go b/providers/dns/internal/curanet/internal/types.go
new file mode 100644
index 0000000000..e0ca45ee17
--- /dev/null
+++ b/providers/dns/internal/curanet/internal/types.go
@@ -0,0 +1,51 @@
+package internal
+
+import (
+ "fmt"
+ "strings"
+)
+
+type APIError struct {
+ Type string `json:"type"`
+ Title string `json:"title"`
+ Status int `json:"status"`
+ Detail string `json:"detail"`
+ Instance string `json:"instance"`
+
+ // TODO(ldez): handle additional properties when `json/v2` will land.
+}
+
+func (a *APIError) Error() string {
+ var msg []string
+
+ if a.Type != "" {
+ msg = append(msg, "type: "+a.Type)
+ }
+
+ if a.Title != "" {
+ msg = append(msg, "title: "+a.Title)
+ }
+
+ if a.Status != 0 {
+ msg = append(msg, fmt.Sprintf("status: %d", a.Status))
+ }
+
+ if a.Detail != "" {
+ msg = append(msg, "detail: "+a.Detail)
+ }
+
+ if a.Instance != "" {
+ msg = append(msg, "instance: "+a.Instance)
+ }
+
+ return strings.Join(msg, ", ")
+}
+
+type Record struct {
+ ID int64 `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Type string `json:"type,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Data string `json:"data,omitempty"`
+}
diff --git a/providers/dns/internal/curanet/provider.go b/providers/dns/internal/curanet/provider.go
new file mode 100644
index 0000000000..cfa8b43bd4
--- /dev/null
+++ b/providers/dns/internal/curanet/provider.go
@@ -0,0 +1,141 @@
+// Package curanet implements a DNS provider for solving the DNS-01 challenge using Curanet.
+package curanet
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/go-acme/lego/v5/challenge"
+ "github.com/go-acme/lego/v5/challenge/dns01"
+ "github.com/go-acme/lego/v5/providers/dns/internal/clientdebug"
+ "github.com/go-acme/lego/v5/providers/dns/internal/curanet/internal"
+)
+
+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 Curanet.
+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.APIKey)
+ if err != nil {
+ return nil, 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,
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
+
+ authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, 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
+ }
+
+ record := internal.Record{
+ Name: subDomain,
+ Type: "TXT",
+ TTL: d.config.TTL,
+ Data: info.Value,
+ }
+
+ err = d.client.CreateRecord(ctx, dns01.UnFqdn(authZone), 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(ctx context.Context, domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
+
+ authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, 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
+ }
+
+ record, err := d.findRecord(ctx, dns01.UnFqdn(authZone), subDomain, info.Value)
+ if err != nil {
+ return err
+ }
+
+ err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), record.ID)
+ 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) findRecord(ctx context.Context, zone, subDomain, value string) (*internal.Record, error) {
+ records, err := d.client.GetRecords(ctx, zone, subDomain, "TXT")
+ if err != nil {
+ return nil, fmt.Errorf("get records: %w", err)
+ }
+
+ for _, record := range records {
+ if record.Name == subDomain && record.Type == "TXT" && record.Data == value {
+ return &record, nil
+ }
+ }
+
+ return nil, errors.New("record not found")
+}
diff --git a/providers/dns/internal/curanet/provider_test.go b/providers/dns/internal/curanet/provider_test.go
new file mode 100644
index 0000000000..a30f899d3c
--- /dev/null
+++ b/providers/dns/internal/curanet/provider_test.go
@@ -0,0 +1,101 @@
+package curanet
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/tester/servermock"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewDNSProviderConfig(t *testing.T) {
+ testCases := []struct {
+ desc string
+ apiKey string
+ expected string
+ }{
+ {
+ desc: "success",
+ apiKey: "secret",
+ },
+ {
+ desc: "missing credentials",
+ expected: "credentials missing",
+ },
+ }
+
+ 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)
+ }
+ })
+ }
+}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := &Config{
+ APIKey: "secret",
+ TTL: 120,
+ 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("POST /dns/v2/Domains/example.com/Records",
+ servermock.Noop().
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBodyFromInternal("records_create-request.json"),
+ ).
+ Build(t)
+
+ err := provider.Present(t.Context(), "example.com", "abc", "123d==")
+ require.NoError(t, err)
+}
+
+func TestDNSProvider_CleanUp(t *testing.T) {
+ provider := mockBuilder().
+ Route("GET /dns/v2/Domains/example.com/Records",
+ servermock.ResponseFromInternal("records_get.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge").
+ With("type", "TXT"),
+ ).
+ Route("DELETE /dns/v2/Domains/example.com/Records/1234",
+ servermock.Noop().
+ WithStatusCode(http.StatusOK),
+ ).
+ Build(t)
+
+ err := provider.CleanUp(t.Context(), "example.com", "abc", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/scannet/scannet.go b/providers/dns/scannet/scannet.go
new file mode 100644
index 0000000000..d9d700d41b
--- /dev/null
+++ b/providers/dns/scannet/scannet.go
@@ -0,0 +1,100 @@
+// Package scannet implements a DNS provider for solving the DNS-01 challenge using ScanNet.
+package scannet
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/go-acme/lego/v5/challenge"
+ "github.com/go-acme/lego/v5/challenge/dns01"
+ "github.com/go-acme/lego/v5/internal/env"
+ "github.com/go-acme/lego/v5/providers/dns/internal/curanet"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "SCANNET_"
+
+ 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 = curanet.Config
+
+// 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 {
+ prv challenge.ProviderTimeout
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for ScanNet.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIKey)
+ if err != nil {
+ return nil, fmt.Errorf("scannet: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIKey = values[EnvAPIKey]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for ScanNet.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("scannet: the configuration of the DNS provider is nil")
+ }
+
+ provider, err := curanet.NewDNSProviderConfig(config, "")
+ if err != nil {
+ return nil, fmt.Errorf("scannet: %w", err)
+ }
+
+ return &DNSProvider{prv: provider}, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.Present(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("scannet: %w", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.CleanUp(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("scannet: %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/scannet/scannet.toml b/providers/dns/scannet/scannet.toml
new file mode 100644
index 0000000000..4bf503ca23
--- /dev/null
+++ b/providers/dns/scannet/scannet.toml
@@ -0,0 +1,22 @@
+Name = "ScanNet"
+Description = ''''''
+URL = "https://www.scannet.dk/"
+Code = "scannet"
+Since = "v5.0.0"
+
+Example = '''
+SCANNET_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns scannet -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ SCANNET_API_KEY = "API key"
+ [Configuration.Additional]
+ SCANNET_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ SCANNET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ SCANNET_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ SCANNET_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://api.scannet.dk/dns/swagger/index.html"
diff --git a/providers/dns/scannet/scannet_test.go b/providers/dns/scannet/scannet_test.go
new file mode 100644
index 0000000000..29166d1785
--- /dev/null
+++ b/providers/dns/scannet/scannet_test.go
@@ -0,0 +1,114 @@
+package scannet
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/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: "secret",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{},
+ expected: "scannet: some credentials information are missing: SCANNET_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.prv)
+ } 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: "scannet: 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.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(t.Context(), 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(t.Context(), envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/wannafind/wannafind.go b/providers/dns/wannafind/wannafind.go
new file mode 100644
index 0000000000..467544ea1c
--- /dev/null
+++ b/providers/dns/wannafind/wannafind.go
@@ -0,0 +1,100 @@
+// Package wannafind implements a DNS provider for solving the DNS-01 challenge using Wannafind.
+package wannafind
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/go-acme/lego/v5/challenge"
+ "github.com/go-acme/lego/v5/challenge/dns01"
+ "github.com/go-acme/lego/v5/internal/env"
+ "github.com/go-acme/lego/v5/providers/dns/internal/curanet"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "WANNAFIND_"
+
+ 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 = curanet.Config
+
+// 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 {
+ prv challenge.ProviderTimeout
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Wannafind.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIKey)
+ if err != nil {
+ return nil, fmt.Errorf("wannafind: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIKey = values[EnvAPIKey]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for Wannafind.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("wannafind: the configuration of the DNS provider is nil")
+ }
+
+ provider, err := curanet.NewDNSProviderConfig(config, "")
+ if err != nil {
+ return nil, fmt.Errorf("wannafind: %w", err)
+ }
+
+ return &DNSProvider{prv: provider}, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.Present(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("wannafind: %w", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
+ err := d.prv.CleanUp(ctx, domain, token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("wannafind: %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/wannafind/wannafind.toml b/providers/dns/wannafind/wannafind.toml
new file mode 100644
index 0000000000..c238f5ad97
--- /dev/null
+++ b/providers/dns/wannafind/wannafind.toml
@@ -0,0 +1,22 @@
+Name = "Wannafind"
+Description = ''''''
+URL = "https://www.wannafind.dk/"
+Code = "wannafind"
+Since = "v5.0.0"
+
+Example = '''
+WANNAFIND_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+lego --dns wannafind -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ WANNAFIND_API_KEY = "API key"
+ [Configuration.Additional]
+ WANNAFIND_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ WANNAFIND_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ WANNAFIND_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ WANNAFIND_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://api.wannafind.dk/dns/swagger/index.html"
diff --git a/providers/dns/wannafind/wannafind_test.go b/providers/dns/wannafind/wannafind_test.go
new file mode 100644
index 0000000000..3847017c91
--- /dev/null
+++ b/providers/dns/wannafind/wannafind_test.go
@@ -0,0 +1,114 @@
+package wannafind
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v5/internal/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: "secret",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{},
+ expected: "wannafind: some credentials information are missing: WANNAFIND_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.prv)
+ } 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: "wannafind: 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.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(t.Context(), 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(t.Context(), 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 4e2b1d2af9..492c2b2251 100644
--- a/providers/dns/zz_gen_dns_providers.go
+++ b/providers/dns/zz_gen_dns_providers.go
@@ -41,7 +41,9 @@ import (
"github.com/go-acme/lego/v5/providers/dns/constellix"
"github.com/go-acme/lego/v5/providers/dns/corenetworks"
"github.com/go-acme/lego/v5/providers/dns/cpanel"
+ "github.com/go-acme/lego/v5/providers/dns/curanet"
"github.com/go-acme/lego/v5/providers/dns/czechia"
+ "github.com/go-acme/lego/v5/providers/dns/dandomain"
"github.com/go-acme/lego/v5/providers/dns/ddnss"
"github.com/go-acme/lego/v5/providers/dns/derak"
"github.com/go-acme/lego/v5/providers/dns/desec"
@@ -156,6 +158,7 @@ import (
"github.com/go-acme/lego/v5/providers/dns/safedns"
"github.com/go-acme/lego/v5/providers/dns/sakuracloud"
"github.com/go-acme/lego/v5/providers/dns/scaleway"
+ "github.com/go-acme/lego/v5/providers/dns/scannet"
"github.com/go-acme/lego/v5/providers/dns/selectel"
"github.com/go-acme/lego/v5/providers/dns/selectelv2"
"github.com/go-acme/lego/v5/providers/dns/selfhostde"
@@ -184,6 +187,7 @@ import (
"github.com/go-acme/lego/v5/providers/dns/volcengine"
"github.com/go-acme/lego/v5/providers/dns/vscale"
"github.com/go-acme/lego/v5/providers/dns/vultr"
+ "github.com/go-acme/lego/v5/providers/dns/wannafind"
"github.com/go-acme/lego/v5/providers/dns/webnamesca"
"github.com/go-acme/lego/v5/providers/dns/webnamesru"
"github.com/go-acme/lego/v5/providers/dns/websupport"
@@ -270,8 +274,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return corenetworks.NewDNSProvider()
case "cpanel":
return cpanel.NewDNSProvider()
+ case "curanet":
+ return curanet.NewDNSProvider()
case "czechia":
return czechia.NewDNSProvider()
+ case "dandomain":
+ return dandomain.NewDNSProvider()
case "ddnss":
return ddnss.NewDNSProvider()
case "derak":
@@ -500,6 +508,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return sakuracloud.NewDNSProvider()
case "scaleway":
return scaleway.NewDNSProvider()
+ case "scannet":
+ return scannet.NewDNSProvider()
case "selectel":
return selectel.NewDNSProvider()
case "selectelv2":
@@ -556,6 +566,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return vscale.NewDNSProvider()
case "vultr":
return vultr.NewDNSProvider()
+ case "wannafind":
+ return wannafind.NewDNSProvider()
case "webnamesca":
return webnamesca.NewDNSProvider()
case "webnamesru", "webnames":