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":