Compare commits

..

No commits in common. "master" and "v4.32.0" have entirely different histories.

59 changed files with 113 additions and 3485 deletions

View file

@ -14,15 +14,9 @@ body:
required: true
- label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world.
required: true
- type: checkboxes
id: pr
attributes:
label: Implementation
options:
- label: Yes, I'm able to create a pull request and be able to maintain the implementation.
required: false
- label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request.
- label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider.
required: false
- type: dropdown

View file

@ -73,75 +73,70 @@ If your DNS provider is not supported, please open an [issue](https://github.com
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/route53/">Amazon Route 53</a></td>
<td><a href="https://go-acme.github.io/lego/dns/anexia/">Anexia CloudDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/safedns/">ANS SafeDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/artfiles/">ArtFiles</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/arvancloud/">ArvanCloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/auroradns/">Aurora DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/autodns/">Autodns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/axelname/">Axelname</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/azion/">Azion</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/azure/">Azure (deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/azuredns/">Azure DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/baiducloud/">Baidu Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/beget/">Beget.com</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/binarylane/">Binary Lane</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bindman/">Bindman</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bluecat/">Bluecat</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bluecatv2/">Bluecat v2</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bookmyname/">BookMyName</a></td>
<td><a href="https://go-acme.github.io/lego/dns/brandit/">Brandit (deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bunny/">Bunny</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/checkdomain/">Checkdomain</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/civo/">Civo</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudru/">Cloud.ru</a></td>
<td><a href="https://go-acme.github.io/lego/dns/clouddns/">CloudDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS (Deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa v2</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/conohav3/">ConoHa v3</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/constellix/">Constellix</a></td>
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cpanel/">CPanel/WHM</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/czechia/">Czechia</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ddnss/">DDnss (DynDNS Service)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/derak/">Derak Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/desec/">deSEC.io</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/designate/">Designate DNSaaS for Openstack</a></td>
<td><a href="https://go-acme.github.io/lego/dns/digitalocean/">Digital Ocean</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/directadmin/">DirectAdmin</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnsmadeeasy/">DNS Made Easy</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dnsexit/">DNSExit</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnshomede/">dnsHome.de</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dnsimple/">DNSimple</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnspod/">DNSPod (deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dode/">Domain Offensive (do.de)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/domeneshop/">Domeneshop</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dreamhost/">DreamHost</a></td>
<td><a href="https://go-acme.github.io/lego/dns/duckdns/">Duck DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dyn/">Dyn</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dyndnsfree/">DynDnsFree.de</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dynu/">Dynu</a></td>
<td><a href="https://go-acme.github.io/lego/dns/easydns/">EasyDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/edgecenter/">EdgeCenter</a></td>
<td><a href="https://go-acme.github.io/lego/dns/efficientip/">Efficient IP</a></td>
<td><a href="https://go-acme.github.io/lego/dns/epik/">Epik</a></td>
<td><a href="https://go-acme.github.io/lego/dns/eurodns/">EuroDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/excedo/">Excedo</a></td>
<td><a href="https://go-acme.github.io/lego/dns/epik/">Epik</a></td>
<td><a href="https://go-acme.github.io/lego/dns/exoscale/">Exoscale</a></td>
<td><a href="https://go-acme.github.io/lego/dns/exec/">External program</a></td>
<td><a href="https://go-acme.github.io/lego/dns/f5xc/">F5 XC</a></td>
@ -271,35 +266,35 @@ If your DNS provider is not supported, please open an [issue](https://github.com
<td><a href="https://go-acme.github.io/lego/dns/todaynic/">TodayNIC/时代互联</a></td>
<td><a href="https://go-acme.github.io/lego/dns/transip/">TransIP</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/safedns/">UKFast SafeDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ultradns/">Ultradns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/uniteddomains/">United-Domains</a></td>
<td><a href="https://go-acme.github.io/lego/dns/variomedia/">Variomedia</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vegadns/">VegaDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vegadns/">VegaDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
<td><a href="https://go-acme.github.io/lego/dns/versio/">Versio.[nl|eu|uk]</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vinyldns/">VinylDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/virtualname/">Virtualname</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/virtualname/">Virtualname</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vkcloud/">VK Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/volcengine/">Volcano Engine/火山引擎</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vscale/">Vscale</a></td>
<td><a href="https://go-acme.github.io/lego/dns/vultr/">Vultr</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/vultr/">Vultr</a></td>
<td><a href="https://go-acme.github.io/lego/dns/webnamesca/">webnames.ca</a></td>
<td><a href="https://go-acme.github.io/lego/dns/webnames/">webnames.ru</a></td>
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex360/">Yandex 360</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneedit/">ZoneEdit</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
<td></td>
</tr></table>
<!-- END DNS PROVIDERS LIST -->

View file

@ -9,5 +9,5 @@ const (
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "detach"
ourUserAgentComment = "release"
)

View file

@ -3,8 +3,6 @@ package resolver
import (
"bytes"
"fmt"
"maps"
"slices"
"sort"
)
@ -27,7 +25,3 @@ func (e obtainError) Error() string {
return buffer.String()
}
func (e obtainError) Unwrap() []error {
return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e))
}

View file

@ -1,70 +0,0 @@
package resolver
import (
"errors"
"testing"
"github.com/go-acme/lego/v4/acme"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_obtainError_Error(t *testing.T) {
err := obtainError{
"a": &acme.ProblemDetails{Type: "001"},
"b": errors.New("oops"),
"c": errors.New("I did it again"),
}
require.EqualError(t, err, `error: one or more domains had a problem:
[a] acme: error: 0 :: 001 ::
[b] oops
[c] I did it again
`)
}
func Test_obtainError_Unwrap(t *testing.T) {
testCases := []struct {
desc string
err obtainError
assert assert.BoolAssertionFunc
}{
{
desc: "one ok",
err: obtainError{
"a": &acme.ProblemDetails{},
"b": errors.New("oops"),
"c": errors.New("I did it again"),
},
assert: assert.True,
},
{
desc: "all ok",
err: obtainError{
"a": &acme.ProblemDetails{Type: "001"},
"b": &acme.ProblemDetails{Type: "002"},
"c": &acme.ProblemDetails{Type: "002"},
},
assert: assert.True,
},
{
desc: "nope",
err: obtainError{
"a": errors.New("hello"),
"b": errors.New("oops"),
"c": errors.New("I did it again"),
},
assert: assert.False,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var pd *acme.ProblemDetails
test.assert(t, errors.As(test.err, &pd))
})
}
}

View file

@ -2,7 +2,7 @@
package main
const defaultVersion = "v4.32.0+dev-detach"
const defaultVersion = "v4.32.0+dev-release"
var version = ""

View file

@ -49,7 +49,6 @@ func allDNSCodes() string {
"constellix",
"corenetworks",
"cpanel",
"czechia",
"ddnss",
"derak",
"desec",
@ -74,8 +73,6 @@ func allDNSCodes() string {
"edgeone",
"efficientip",
"epik",
"eurodns",
"excedo",
"exec",
"exoscale",
"f5xc",
@ -1029,26 +1026,6 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`)
case "czechia":
// generated from: providers/dns/czechia/czechia.toml
ew.writeln(`Configuration for Czechia.`)
ew.writeln(`Code: 'czechia'`)
ew.writeln(`Since: 'v4.33.0'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "CZECHIA_TOKEN": Authorization token`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "CZECHIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
ew.writeln(` - "CZECHIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
ew.writeln(` - "CZECHIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
ew.writeln(` - "CZECHIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`)
case "ddnss":
// generated from: providers/dns/ddnss/ddnss.toml
ew.writeln(`Configuration for DDnss (DynDNS Service).`)
@ -1564,48 +1541,6 @@ 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 "excedo":
// generated from: providers/dns/excedo/excedo.toml
ew.writeln(`Configuration for Excedo.`)
ew.writeln(`Code: 'excedo'`)
ew.writeln(`Since: 'v4.33.0'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "EXCEDO_API_KEY": API key`)
ew.writeln(` - "EXCEDO_API_URL": API base URL`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "EXCEDO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
ew.writeln(` - "EXCEDO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`)
ew.writeln(` - "EXCEDO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`)
ew.writeln(` - "EXCEDO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/excedo`)
case "exec":
// generated from: providers/dns/exec/exec.toml
ew.writeln(`Configuration for External program.`)
@ -2459,7 +2394,6 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`)
ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)
ew.writeln()
@ -3439,7 +3373,7 @@ func displayDNSHelp(w io.Writer, name string) error {
case "safedns":
// generated from: providers/dns/safedns/safedns.toml
ew.writeln(`Configuration for ANS SafeDNS.`)
ew.writeln(`Configuration for UKFast SafeDNS.`)
ew.writeln(`Code: 'safedns'`)
ew.writeln(`Since: 'v4.6.0'`)
ew.writeln()

View file

@ -1,67 +0,0 @@
---
title: "Czechia"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: czechia
dnsprovider:
since: "v4.33.0"
code: "czechia"
url: "https://www.czechia.com/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/czechia/czechia.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Czechia](https://www.czechia.com/).
<!--more-->
- Code: `czechia`
- Since: v4.33.0
Here is an example bash command using the Czechia provider:
```bash
CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \
lego --dns czechia -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `CZECHIA_TOKEN` | Authorization token |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `CZECHIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `CZECHIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `CZECHIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `CZECHIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://api.czechia.com/swagger/index.html)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/czechia/czechia.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -1,69 +0,0 @@
---
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/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/eurodns/eurodns.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [EuroDNS](https://www.eurodns.com/).
<!--more-->
- 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/)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/eurodns/eurodns.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -1,69 +0,0 @@
---
title: "Excedo"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: excedo
dnsprovider:
since: "v4.33.0"
code: "excedo"
url: "https://excedo.se/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/excedo/excedo.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Excedo](https://excedo.se/).
<!--more-->
- Code: `excedo`
- Since: v4.33.0
Here is an example bash command using the Excedo provider:
```bash
EXCEDO_API_KEY=your-api-key \
EXCEDO_API_URL=your-base-url \
lego --dns excedo -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `EXCEDO_API_KEY` | API key |
| `EXCEDO_API_URL` | API base URL |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `EXCEDO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `EXCEDO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |
| `EXCEDO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |
| `EXCEDO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](none)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/excedo/excedo.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -50,7 +50,6 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
| `LIARA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `LIARA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `LIARA_TEAM_ID` | The team ID to access services in a team |
| `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.

View file

@ -1,12 +1,12 @@
---
title: "ANS SafeDNS"
title: "UKFast SafeDNS"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: safedns
dnsprovider:
since: "v4.6.0"
code: "safedns"
url: "https://www.ans.co.uk/"
url: "https://www.ukfast.co.uk/dns-hosting.html"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
@ -14,7 +14,7 @@ dnsprovider:
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [ANS SafeDNS](https://www.ans.co.uk/).
Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html).
<!--more-->
@ -23,7 +23,7 @@ Configuration for [ANS SafeDNS](https://www.ans.co.uk/).
- Since: v4.6.0
Here is an example bash command using the ANS SafeDNS provider:
Here is an example bash command using the UKFast SafeDNS provider:
```bash
SAFEDNS_AUTH_TOKEN=xxxxxx \

View file

@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""

View file

@ -1,159 +0,0 @@
// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia.
package czechia
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/czechia/internal"
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
)
// Environment variables names.
const (
envNamespace = "CZECHIA_"
EnvToken = envNamespace + "TOKEN"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
Token string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Czechia.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvToken)
if err != nil {
return nil, fmt.Errorf("czechia: %w", err)
}
config := NewDefaultConfig()
config.Token = values[EnvToken]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Czechia.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("czechia: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.Token)
if err != nil {
return nil, fmt.Errorf("czechia: %w", err)
}
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
return &DNSProvider{
config: config,
client: client,
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("czechia: %w", err)
}
record := internal.TXTRecord{
Hostname: subDomain,
Text: info.Value,
TTL: d.config.TTL,
PublishZone: 1,
}
err = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record)
if err != nil {
return fmt.Errorf("czechia: add TXT record: %w", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("czechia: %w", err)
}
record := internal.TXTRecord{
Hostname: subDomain,
Text: info.Value,
TTL: d.config.TTL,
PublishZone: 1,
}
err = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record)
if err != nil {
return fmt.Errorf("czechia: delete TXT record: %w", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -1,22 +0,0 @@
Name = "Czechia"
Description = ''''''
URL = "https://www.czechia.com/"
Code = "czechia"
Since = "v4.33.0"
Example = '''
CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \
lego --dns czechia -d '*.example.com' -d example.com run
'''
[Configuration]
[Configuration.Credentials]
CZECHIA_TOKEN = "Authorization token"
[Configuration.Additional]
CZECHIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
CZECHIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
CZECHIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
CZECHIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "https://api.czechia.com/swagger/index.html"

View file

@ -1,165 +0,0 @@
package czechia
import (
"net/http/httptest"
"net/url"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvToken: "secret",
},
},
{
desc: "missing credentials",
envVars: map[string]string{},
expected: "czechia: some credentials information are missing: CZECHIA_TOKEN",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := NewDNSProvider()
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
require.NotNil(t, p.client)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
token string
expected string
}{
{
desc: "success",
token: "secret",
},
{
desc: "missing credentials",
expected: "czechia: credentials missing",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.Token = test.token
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
require.NotNil(t, p.client)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func mockBuilder() *servermock.Builder[*DNSProvider] {
return servermock.NewBuilder(
func(server *httptest.Server) (*DNSProvider, error) {
config := NewDefaultConfig()
config.Token = "secret"
config.HTTPClient = server.Client()
p, err := NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
p.client.BaseURL, _ = url.Parse(server.URL)
return p, nil
},
servermock.CheckHeader().
WithJSONHeaders().
With("AuthorizationToken", "secret"),
)
}
func TestDNSProvider_Present(t *testing.T) {
provider := mockBuilder().
Route("POST /DNS/example.com/TXT",
servermock.Noop(),
servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"),
).
Build(t)
err := provider.Present("example.com", "abc", "123d==")
require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
provider := mockBuilder().
Route("DELETE /DNS/example.com/TXT",
servermock.Noop(),
servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"),
).
Build(t)
err := provider.CleanUp("example.com", "abc", "123d==")
require.NoError(t, err)
}

View file

@ -1,124 +0,0 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
)
const defaultBaseURL = "https://api.czechia.com/api"
const authorizationTokenHeader = "AuthorizationToken"
// Client the Czechia API client.
type Client struct {
token string
BaseURL *url.URL
HTTPClient *http.Client
}
// NewClient creates a new Client.
func NewClient(token string) (*Client, error) {
if token == "" {
return nil, errors.New("credentials missing")
}
baseURL, _ := url.Parse(defaultBaseURL)
return &Client{
token: token,
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}, nil
}
func (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error {
endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT")
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
if err != nil {
return err
}
return c.do(req, nil)
}
func (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error {
endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT")
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record)
if err != nil {
return err
}
return c.do(req, nil)
}
func (c *Client) do(req *http.Request, result any) error {
useragent.SetHeader(req.Header)
req.Header.Set(authorizationTokenHeader, c.token)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode/100 != 2 {
raw, _ := io.ReadAll(resp.Body)
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
}
if result == nil {
return nil
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return errutils.NewReadResponseError(req, resp.StatusCode, err)
}
err = json.Unmarshal(raw, result)
if err != nil {
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}
return nil
}
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)
if payload != nil {
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
req.Header.Set("Accept", "application/json")
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}

View file

@ -1,67 +0,0 @@
package internal
import (
"net/http/httptest"
"net/url"
"testing"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
func mockBuilder() *servermock.Builder[*Client] {
return servermock.NewBuilder[*Client](
func(server *httptest.Server) (*Client, error) {
client, err := NewClient("secret")
if err != nil {
return nil, err
}
client.BaseURL, _ = url.Parse(server.URL)
client.HTTPClient = server.Client()
return client, nil
},
servermock.CheckHeader().
WithJSONHeaders().
With(authorizationTokenHeader, "secret"),
)
}
func TestClient_AddTXTRecord(t *testing.T) {
client := mockBuilder().
Route("POST /DNS/example.com/TXT",
servermock.Noop(),
servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"),
).
Build(t)
record := TXTRecord{
Hostname: "_acme-challenge",
Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
TTL: 120,
PublishZone: 1,
}
err := client.AddTXTRecord(t.Context(), "example.com", record)
require.NoError(t, err)
}
func TestClient_DeleteTXTRecord(t *testing.T) {
client := mockBuilder().
Route("DELETE /DNS/example.com/TXT",
servermock.Noop(),
servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"),
).
Build(t)
record := TXTRecord{
Hostname: "_acme-challenge",
Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
TTL: 120,
PublishZone: 1,
}
err := client.DeleteTXTRecord(t.Context(), "example.com", record)
require.NoError(t, err)
}

View file

@ -1,6 +0,0 @@
{
"hostName": "_acme-challenge",
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
"ttl": 120,
"publishZone": 1
}

View file

@ -1,6 +0,0 @@
{
"hostName": "_acme-challenge",
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
"ttl": 120,
"publishZone": 1
}

View file

@ -1,8 +0,0 @@
package internal
type TXTRecord struct {
Hostname string `json:"hostName,omitempty"`
Text string `json:"text,omitempty"`
TTL int `json:"ttl,omitempty"`
PublishZone int `json:"publishZone,omitempty"`
}

View file

@ -1,197 +0,0 @@
// 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
}

View file

@ -1,24 +0,0 @@
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/"

View file

@ -1,215 +0,0 @@
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)
}

View file

@ -1,199 +0,0 @@
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
}

View file

@ -1,310 +0,0 @@
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()},
}},
}
}

View file

@ -1,8 +0,0 @@
{
"errors": [
{
"code": "INVALID_API_KEY",
"title": "Invalid API Key"
}
]
}

View file

@ -1,46 +0,0 @@
{
"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
}
]
}

View file

@ -1,28 +0,0 @@
{
"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": []
}

View file

@ -1,139 +0,0 @@
{
"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"
}
]
}
}

View file

@ -1,49 +0,0 @@
{
"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
}
}

View file

@ -1,37 +0,0 @@
{
"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
}
]
}

View file

@ -1,37 +0,0 @@
{
"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
}
]
}

View file

@ -1,136 +0,0 @@
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)
}

View file

@ -1,176 +0,0 @@
// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo.
package excedo
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/excedo/internal"
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
)
// Environment variables names.
const (
envNamespace = "EXCEDO_"
EnvAPIURL = envNamespace + "API_URL"
EnvAPIKey = envNamespace + "API_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
APIURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 60),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *internal.Client
recordsMu sync.Mutex
records map[string]int64
}
// NewDNSProvider returns a DNSProvider instance configured for Excedo.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIURL, EnvAPIKey)
if err != nil {
return nil, fmt.Errorf("excedo: %w", err)
}
config := NewDefaultConfig()
config.APIURL = values[EnvAPIURL]
config.APIKey = values[EnvAPIKey]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Excedo.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("excedo: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.APIURL, config.APIKey)
if err != nil {
return nil, fmt.Errorf("excedo: %w", err)
}
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
return &DNSProvider{
config: config,
client: client,
records: make(map[string]int64),
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("excedo: %w", err)
}
record := internal.Record{
DomainName: dns01.UnFqdn(authZone),
Name: subDomain,
Type: "TXT",
Content: info.Value,
TTL: strconv.Itoa(d.config.TTL),
}
recordID, err := d.client.AddRecord(ctx, record)
if err != nil {
return fmt.Errorf("excedo: add record: %w", err)
}
d.recordsMu.Lock()
d.records[token] = recordID
d.recordsMu.Unlock()
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err)
}
d.recordsMu.Lock()
recordID, ok := d.records[token]
d.recordsMu.Unlock()
if !ok {
return fmt.Errorf("excedo: unknown record ID for '%s'", info.EffectiveFQDN)
}
err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10))
if err != nil {
return fmt.Errorf("excedo: delete record: %w", err)
}
d.recordsMu.Lock()
delete(d.records, token)
d.recordsMu.Unlock()
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -1,24 +0,0 @@
Name = "Excedo"
Description = ''''''
URL = "https://excedo.se/"
Code = "excedo"
Since = "v4.33.0"
Example = '''
EXCEDO_API_KEY=your-api-key \
EXCEDO_API_URL=your-base-url \
lego --dns excedo -d '*.example.com' -d example.com run
'''
[Configuration]
[Configuration.Credentials]
EXCEDO_API_KEY = "API key"
EXCEDO_API_URL = "API base URL"
[Configuration.Additional]
EXCEDO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)"
EXCEDO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)"
EXCEDO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)"
EXCEDO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "none"

View file

@ -1,210 +0,0 @@
package excedo
import (
"net/http/httptest"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvAPIURL: "https://example.com",
EnvAPIKey: "secret",
},
},
{
desc: "missing the API key",
envVars: map[string]string{
EnvAPIURL: "https://example.com",
EnvAPIKey: "",
},
expected: "excedo: some credentials information are missing: EXCEDO_API_KEY",
},
{
desc: "missing the API URL",
envVars: map[string]string{
EnvAPIURL: "",
EnvAPIKey: "secret",
},
expected: "excedo: some credentials information are missing: EXCEDO_API_URL",
},
{
desc: "missing credentials",
envVars: map[string]string{},
expected: "excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_KEY",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := NewDNSProvider()
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
require.NotNil(t, p.client)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
apiURL string
apiKey string
expected string
}{
{
desc: "success",
apiURL: "https://example.com",
apiKey: "secret",
},
{
desc: "missing the API key",
apiURL: "https://example.com",
expected: "excedo: credentials missing",
},
{
desc: "missing the API URL",
apiKey: "secret",
expected: "excedo: credentials missing",
},
{
desc: "missing credentials",
expected: "excedo: credentials missing",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIURL = test.apiURL
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
require.NotNil(t, p.client)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func mockBuilder() *servermock.Builder[*DNSProvider] {
return servermock.NewBuilder(
func(server *httptest.Server) (*DNSProvider, error) {
config := NewDefaultConfig()
config.APIURL = server.URL
config.APIKey = "secret"
config.HTTPClient = server.Client()
p, err := NewDNSProviderConfig(config)
if err != nil {
return nil, err
}
return p, nil
},
)
}
func TestDNSProvider_Present(t *testing.T) {
provider := mockBuilder().
Route("GET /authenticate/login/",
servermock.ResponseFromInternal("login.json"),
servermock.CheckHeader().
WithAuthorization("Bearer secret"),
).
Route("POST /dns/addrecord/",
servermock.ResponseFromInternal("addrecord.json"),
servermock.CheckHeader().
WithAuthorization("Bearer session-token"),
servermock.CheckForm().Strict().
With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("domainName", "example.com").
With("name", "_acme-challenge").
With("ttl", "60").
With("type", "TXT"),
).
Build(t)
err := provider.Present("example.com", "abc", "123d==")
require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
provider := mockBuilder().
Route("GET /authenticate/login/",
servermock.ResponseFromInternal("login.json"),
servermock.CheckHeader().
WithAuthorization("Bearer secret"),
).
Route("POST /dns/deleterecord/",
servermock.ResponseFromInternal("deleterecord.json"),
servermock.CheckHeader().
WithAuthorization("Bearer session-token"),
).
Build(t)
provider.records["abc"] = 19695822
err := provider.CleanUp("example.com", "abc", "123d==")
require.NoError(t, err)
}

View file

@ -1,205 +0,0 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"sync"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
querystring "github.com/google/go-querystring/query"
)
type responseChecker interface {
Check() error
}
// Client the Excedo API client.
type Client struct {
apiKey string
baseURL *url.URL
HTTPClient *http.Client
token *ExpirableToken
muToken sync.Mutex
}
// NewClient creates a new Client.
func NewClient(apiURL, apiKey string) (*Client, error) {
if apiURL == "" || apiKey == "" {
return nil, errors.New("credentials missing")
}
baseURL, err := url.Parse(apiURL)
if err != nil {
return nil, err
}
return &Client{
apiKey: apiKey,
baseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}, nil
}
func (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) {
payload, err := querystring.Values(record)
if err != nil {
return 0, err
}
endpoint := c.baseURL.JoinPath("/dns/addrecord/")
req, err := newFormRequest(ctx, http.MethodPost, endpoint, payload)
if err != nil {
return 0, err
}
result := new(AddRecordResponse)
err = c.doAuthenticated(ctx, req, result)
if err != nil {
return 0, err
}
return result.RecordID, nil
}
func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {
endpoint := c.baseURL.JoinPath("/dns/deleterecord/")
data := map[string]string{
"domainname": dns01.UnFqdn(zone),
"recordid": recordID,
}
req, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data)
if err != nil {
return err
}
result := new(BaseResponse)
err = c.doAuthenticated(ctx, req, result)
if err != nil {
return err
}
return nil
}
func (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) {
endpoint := c.baseURL.JoinPath("/dns/getrecords/")
query := endpoint.Query()
query.Set("domainname", zone)
endpoint.RawQuery = query.Encode()
req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
result := new(GetRecordsResponse)
err = c.doAuthenticated(ctx, req, result)
if err != nil {
return nil, err
}
return result.DNS, nil
}
func (c *Client) do(req *http.Request, result responseChecker) error {
useragent.SetHeader(req.Header)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode/100 != 2 {
raw, _ := io.ReadAll(resp.Body)
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
}
if result == nil {
return nil
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return errutils.NewReadResponseError(req, resp.StatusCode, err)
}
err = json.Unmarshal(raw, result)
if err != nil {
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}
return result.Check()
}
func newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) {
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
for k, v := range data {
err := writer.WriteField(k, v)
if err != nil {
return nil, err
}
}
err := writer.Close()
if err != nil {
return nil, err
}
body := bytes.NewReader(buf.Bytes())
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
return req, nil
}
func newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) {
var body io.Reader
if len(form) > 0 {
body = bytes.NewReader([]byte(form.Encode()))
} else {
body = http.NoBody
}
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
if method == http.MethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
return req, nil
}

View file

@ -1,137 +0,0 @@
package internal
import (
"net/http/httptest"
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func mockBuilder() *servermock.Builder[*Client] {
return servermock.NewBuilder[*Client](
func(server *httptest.Server) (*Client, error) {
client, err := NewClient(server.URL, "secret")
if err != nil {
return nil, err
}
client.HTTPClient = server.Client()
return client, nil
},
)
}
func TestClient_AddRecord(t *testing.T) {
client := mockBuilder().
Route("POST /dns/addrecord/",
servermock.ResponseFromFixture("addrecord.json"),
servermock.CheckHeader().
WithAuthorization("Bearer session-token"),
servermock.CheckForm().Strict().
With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
With("domainName", "example.com").
With("name", "_acme-challenge").
With("ttl", "60").
With("type", "TXT"),
).
Build(t)
client.token = &ExpirableToken{
Token: "session-token",
Expires: time.Now().Add(6 * time.Hour),
}
record := Record{
DomainName: "example.com",
Name: "_acme-challenge",
Type: "TXT",
Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
TTL: "60",
}
recordID, err := client.AddRecord(t.Context(), record)
require.NoError(t, err)
assert.EqualValues(t, 19695822, recordID)
}
func TestClient_AddRecord_error(t *testing.T) {
client := mockBuilder().
Route("POST /dns/addrecord/",
servermock.ResponseFromFixture("error.json"),
).
Build(t)
client.token = &ExpirableToken{
Token: "session-token",
Expires: time.Now().Add(6 * time.Hour),
}
record := Record{
DomainName: "example.com",
Name: "_acme-challenge",
Type: "TXT",
Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
TTL: "60",
}
_, err := client.AddRecord(t.Context(), record)
require.EqualError(t, err, "2003: Required parameter missing")
}
func TestClient_DeleteRecord(t *testing.T) {
client := mockBuilder().
Route("POST /dns/deleterecord/",
servermock.ResponseFromFixture("deleterecord.json"),
servermock.CheckHeader().
WithAuthorization("Bearer session-token"),
).
Build(t)
client.token = &ExpirableToken{
Token: "session-token",
Expires: time.Now().Add(6 * time.Hour),
}
err := client.DeleteRecord(t.Context(), "example.com", "19695822")
require.NoError(t, err)
}
func TestClient_GetRecords(t *testing.T) {
client := mockBuilder().
Route("GET /dns/getrecords/",
servermock.ResponseFromFixture("getrecords.json"),
servermock.CheckHeader().
WithAuthorization("Bearer session-token"),
servermock.CheckQueryParameter().Strict().
With("domainname", "example.com"),
).
Build(t)
client.token = &ExpirableToken{
Token: "session-token",
Expires: time.Now().Add(6 * time.Hour),
}
zones, err := client.GetRecords(t.Context(), "example.com")
require.NoError(t, err)
expected := map[string]Zone{
"example.com": {
DNSType: "type",
Records: []Record{{
RecordID: "1234",
Name: "_acme-challenge.example.com",
Type: "TXT",
Content: "txt-value",
TTL: "60",
}},
},
}
assert.Equal(t, expected, zones)
}

View file

@ -1,15 +0,0 @@
{
"code": 1000,
"desc": "Command completed successfully",
"recordid": 19695822,
"session": {
"accID": "1234",
"usrID": "1234",
"status": "active",
"expire": {
"date": "2026-03-10 19:03:18",
"seconds": 5678
}
},
"runtime": 0.2852
}

View file

@ -1,14 +0,0 @@
{
"code": 1000,
"desc": "Command completed successfully",
"session": {
"accID": "1234",
"usrID": "1234",
"status": "active",
"expire": {
"date": "2026-03-10 19:03:18",
"seconds": 5678
}
},
"runtime": 0.2852
}

View file

@ -1,18 +0,0 @@
{
"code": 2003,
"desc": "Required parameter missing",
"missing": [
"domainname",
"recordid"
],
"session": {
"accID": "1234",
"usrID": "1234",
"status": "active",
"expire": {
"date": "2026-03-10 19:03:18",
"seconds": 5485
}
},
"runtime": 0.0534
}

View file

@ -1,23 +0,0 @@
{
"code": 1000,
"desc": "Command completed successfully",
"dns": {
"example.com": {
"dnstype": "type",
"recordusage": {
"used": 74
},
"records": [
{
"recordid": "1234",
"name": "_acme-challenge.example.com",
"type": "TXT",
"content": "txt-value",
"ttl": "60",
"prio": null,
"change_date": null
}
]
}
}
}

View file

@ -1,7 +0,0 @@
{
"code": 1000,
"desc": "Command completed successfully",
"parameters": {
"token": "session-token"
}
}

View file

@ -1,75 +0,0 @@
package internal
import (
"context"
"fmt"
"net/http"
"time"
)
type ExpirableToken struct {
Token string
Expires time.Time
}
func (t *ExpirableToken) IsExpired() bool {
return time.Now().After(t.Expires)
}
func (c *Client) Login(ctx context.Context) (string, error) {
endpoint := c.baseURL.JoinPath("/authenticate/login/")
req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
result := new(LoginResponse)
err = c.do(req, result)
if err != nil {
return "", err
}
if result.Code != 1000 && result.Code != 1300 {
return "", fmt.Errorf("%d: %s", result.Code, result.Description)
}
return result.Parameters.Token, nil
}
func (c *Client) authenticate(ctx context.Context) (string, error) {
c.muToken.Lock()
defer c.muToken.Unlock()
if c.token == nil || c.token.IsExpired() {
token, err := c.Login(ctx)
if err != nil {
return "", err
}
c.token = &ExpirableToken{
Token: token,
Expires: time.Now().Add(2*time.Hour - time.Minute),
}
return token, nil
}
return c.token.Token, nil
}
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error {
token, err := c.authenticate(ctx)
if err != nil {
return err
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
return c.do(req, result)
}

View file

@ -1,35 +0,0 @@
package internal
import (
"testing"
"github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_Login(t *testing.T) {
client := mockBuilder().
Route("GET /authenticate/login/",
servermock.ResponseFromFixture("login.json"),
servermock.CheckHeader().
WithAuthorization("Bearer secret"),
).
Build(t)
token, err := client.Login(t.Context())
require.NoError(t, err)
assert.Equal(t, "session-token", token)
}
func TestClient_Login_error(t *testing.T) {
client := mockBuilder().
Route("GET /authenticate/login/",
servermock.ResponseFromFixture("error.json"),
).
Build(t)
_, err := client.Login(t.Context())
require.EqualError(t, err, "2003: Required parameter missing")
}

View file

@ -1,65 +0,0 @@
package internal
import "fmt"
type BaseResponse struct {
Code int `json:"code"`
Description string `json:"desc"`
}
func (r BaseResponse) Check() error {
// Response codes:
// - 1000: Command completed successfully
// - 1300: Command completed successfully; no messages
// - 2001: Command syntax error
// - 2002: Command use error
// - 2003: Required parameter missing
// - 2004: Parameter value range error
// - 2104: Billing failure
// - 2200: Authentication error
// - 2201: Authorization error
// - 2303: Object does not exist
// - 2304: Object status prohibits operation
// - 2309: Object duplicate found
// - 2400: Command failed
// - 2500: Command failed; server closing connection
if r.Code != 1000 && r.Code != 1300 {
return fmt.Errorf("%d: %s", r.Code, r.Description)
}
return nil
}
type GetRecordsResponse struct {
BaseResponse
DNS map[string]Zone `json:"dns"`
}
type Zone struct {
DNSType string `json:"dnstype"`
Records []Record `json:"records"`
}
type Record struct {
DomainName string `json:"domainName,omitempty" url:"domainName,omitempty"`
RecordID string `json:"recordid,omitempty" url:"recordid,omitempty"`
Name string `json:"name,omitempty" url:"name,omitempty"`
Type string `json:"type,omitempty" url:"type,omitempty"`
Content string `json:"content,omitempty" url:"content,omitempty"`
TTL string `json:"ttl,omitempty" url:"ttl,omitempty"`
}
type AddRecordResponse struct {
BaseResponse
RecordID int64 `json:"recordid"`
}
type LoginResponse struct {
BaseResponse
Parameters struct {
Token string `json:"token"`
} `json:"parameters"`
}

View file

@ -38,25 +38,55 @@ func TestClient_GetZones(t *testing.T) {
expected := []Zone{
{
ID: "123",
Name: "example.com",
NameDisplay: "example.com",
Type: "NATIVE",
Active: "1",
ID: "123",
Name: "example.com",
NameDisplay: "example.com",
Type: "NATIVE",
Active: "1",
Protected: "1",
IsRegistered: "1",
Updated: false,
CustomerID: "16030",
DomainRegistrar: "norid",
DomainStatus: "active",
DomainExpiryDate: "2026-11-23 15:17:38",
DomainAutoRenew: "1",
ExternalDNS: "0",
RecordCount: 4,
},
{
ID: "226",
Name: "example.org",
NameDisplay: "example.org",
Type: "NATIVE",
Active: "1",
ID: "226",
Name: "example.org",
NameDisplay: "example.org",
Type: "NATIVE",
Active: "1",
Protected: "1",
IsRegistered: "1",
Updated: false,
CustomerID: "16030",
DomainRegistrar: "norid",
DomainStatus: "active",
DomainExpiryDate: "2026-11-23 14:15:01",
DomainAutoRenew: "1",
ExternalDNS: "0",
RecordCount: 5,
},
{
ID: "229",
Name: "example.xn--zckzah",
NameDisplay: "example.テスト",
Type: "NATIVE",
Active: "1",
ID: "229",
Name: "example.xn--zckzah",
NameDisplay: "example.テスト",
Type: "NATIVE",
Active: "1",
Protected: "1",
IsRegistered: "1",
Updated: false,
CustomerID: "16030",
DomainRegistrar: "norid",
DomainStatus: "active",
DomainExpiryDate: "2026-12-01 12:40:48",
DomainAutoRenew: "1",
ExternalDNS: "0",
RecordCount: 4,
},
}

View file

@ -30,7 +30,7 @@
"domain_dnssec_data": null,
"domain_protected_email": null,
"zone_created": "2025-11-23 16:17:29",
"zone_updated": 1700000000,
"zone_updated": false,
"external_dns": "0",
"record_count": 4,
"zone_name_display": "example.com"
@ -59,7 +59,7 @@
"domain_dnssec_data": null,
"domain_protected_email": null,
"zone_created": "2025-11-23 15:13:27",
"zone_updated": 1700000000,
"zone_updated": false,
"external_dns": "0",
"record_count": 5,
"zone_name_display": "example.org"
@ -88,7 +88,7 @@
"domain_dnssec_data": null,
"domain_protected_email": null,
"zone_created": "2025-11-23 16:37:15",
"zone_updated": 1700000000,
"zone_updated": false,
"external_dns": "0",
"record_count": 4,
"zone_name_display": "example.\u30C6\u30B9\u30C8"

View file

@ -26,11 +26,21 @@ type APIResponse[T any] struct {
}
type Zone struct {
ID string `json:"zone_id,omitempty"`
Name string `json:"zone_name,omitempty"`
NameDisplay string `json:"zone_name_display,omitempty"`
Type string `json:"zone_type,omitempty"`
Active string `json:"zone_active,omitempty"`
ID string `json:"zone_id,omitempty"`
Name string `json:"zone_name,omitempty"`
NameDisplay string `json:"zone_name_display,omitempty"`
Type string `json:"zone_type,omitempty"`
Active string `json:"zone_active,omitempty"`
Protected string `json:"zone_protected,omitempty"`
IsRegistered string `json:"zone_is_registered,omitempty"`
Updated bool `json:"zone_updated,omitempty"`
CustomerID string `json:"cust_id,omitempty"`
DomainRegistrar string `json:"domain_registrar,omitempty"`
DomainStatus string `json:"domain_status,omitempty"`
DomainExpiryDate string `json:"domain_expiry_date,omitempty"`
DomainAutoRenew string `json:"domain_auto_renew,omitempty"`
ExternalDNS string `json:"external_dns,omitempty"`
RecordCount int `json:"record_count,omitempty"`
}
type Record struct {

View file

@ -15,7 +15,7 @@ const (
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "detach"
ourUserAgentComment = "release"
)
// Get builds and returns the User-Agent string.

View file

@ -20,23 +20,17 @@ const defaultBaseURL = "https://dns-service.iran.liara.ir"
type Client struct {
baseURL *url.URL
httpClient *http.Client
teamID string
}
// NewClient creates a new Client.
func NewClient(hc *http.Client, teamID string) *Client {
func NewClient(hc *http.Client) *Client {
baseURL, _ := url.Parse(defaultBaseURL)
if hc == nil {
hc = &http.Client{Timeout: 10 * time.Second}
}
return &Client{
httpClient: hc,
baseURL: baseURL,
teamID: teamID,
}
return &Client{httpClient: hc, baseURL: baseURL}
}
// GetRecords gets the records of a domain.
@ -44,7 +38,7 @@ func NewClient(hc *http.Client, teamID string) *Client {
func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) {
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
@ -79,7 +73,7 @@ func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, e
func (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) {
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
@ -114,7 +108,7 @@ func (c *Client) CreateRecord(ctx context.Context, domainName string, record Rec
func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) {
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
@ -149,7 +143,7 @@ func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*R
func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error {
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
@ -168,14 +162,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string)
return nil
}
func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
if c.teamID != "" {
query := endpoint.Query()
query.Set("teamID", c.teamID)
endpoint.RawQuery = query.Encode()
}
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)
if payload != nil {

View file

@ -13,10 +13,10 @@ import (
const apiKey = "key"
func mockBuilder(teamID string) *servermock.Builder[*Client] {
func mockBuilder() *servermock.Builder[*Client] {
return servermock.NewBuilder[*Client](
func(server *httptest.Server) (*Client, error) {
client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey), teamID)
client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey))
client.baseURL, _ = url.Parse(server.URL)
return client, nil
@ -26,7 +26,7 @@ func mockBuilder(teamID string) *servermock.Builder[*Client] {
}
func TestClient_GetRecords(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("GET /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordsResponse.json")).
Build(t)
@ -50,7 +50,7 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecord(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("GET /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("RecordResponse.json")).
Build(t)
@ -72,7 +72,7 @@ func TestClient_GetRecord(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("POST /api/v1/zones/example.com/dns-records",
servermock.ResponseFromFixture("RecordResponse.json").
WithStatusCode(http.StatusCreated),
@ -108,47 +108,8 @@ func TestClient_CreateRecord(t *testing.T) {
assert.Equal(t, expected, record)
}
func TestClient_CreateRecord_withTeamID(t *testing.T) {
client := mockBuilder("123").
Route("POST /api/v1/zones/example.com/dns-records",
servermock.ResponseFromFixture("RecordResponse.json").
WithStatusCode(http.StatusCreated),
servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`),
servermock.CheckQueryParameter().Strict().With("teamID", "123"),
).
Build(t)
data := Record{
Type: "string",
Name: "string",
Contents: []Content{
{
Text: "string",
},
},
TTL: 3600,
}
record, err := client.CreateRecord(t.Context(), "example.com", data)
require.NoError(t, err)
expected := &Record{
ID: "string",
Type: "string",
Name: "string",
Contents: []Content{
{
Text: "string",
},
},
TTL: 3600,
}
assert.Equal(t, expected, record)
}
func TestClient_DeleteRecord(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("DELETE /api/v1/zones/example.com/dns-records/123",
servermock.Noop().
WithStatusCode(http.StatusNoContent)).
@ -159,7 +120,7 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_NotFound_Response(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("DELETE /api/v1/zones/example.com/dns-records/123",
servermock.Noop().
WithStatusCode(http.StatusNotFound)).
@ -170,7 +131,7 @@ func TestClient_DeleteRecord_NotFound_Response(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
client := mockBuilder("").
client := mockBuilder().
Route("DELETE /api/v1/zones/example.com/dns-records/123",
servermock.ResponseFromFixture("error.json").
WithStatusCode(http.StatusUnauthorized)).

View file

@ -23,7 +23,6 @@ const (
envNamespace = "LIARA_"
EnvAPIKey = envNamespace + "API_KEY"
EnvTeamID = envNamespace + "TEAM_ID"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
@ -40,9 +39,7 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
APIKey string
TeamID string
APIKey string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
@ -80,7 +77,6 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
config.TeamID = env.GetOrFile(EnvTeamID)
return NewDNSProviderConfig(config)
}
@ -116,7 +112,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
clientdebug.Wrap(
internal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey),
),
config.TeamID,
)
return &DNSProvider{

View file

@ -13,7 +13,6 @@ lego --dns liara -d '*.example.com' -d example.com run
[Configuration.Credentials]
LIARA_API_KEY = "The API key"
[Configuration.Additional]
LIARA_TEAM_ID = "The team ID to access services in a team"
LIARA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
LIARA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)"

View file

@ -19,7 +19,7 @@ const defaultBaseURL = "https://api.ukfast.io/safedns/v1"
const authorizationHeader = "Authorization"
// Client the ANS SafeDNS client.
// Client the UKFast SafeDNS client.
type Client struct {
authToken string

View file

@ -1,4 +1,4 @@
// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS.
// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS.
package safedns
import (
@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) {
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS.
// NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("safedns: supplied configuration was nil")

View file

@ -1,6 +1,6 @@
Name = "ANS SafeDNS"
Name = "UKFast SafeDNS"
Description = ''''''
URL = "https://www.ans.co.uk/"
URL = "https://www.ukfast.co.uk/dns-hosting.html"
Code = "safedns"
Since = "v4.6.0"

View file

@ -43,7 +43,6 @@ import (
"github.com/go-acme/lego/v4/providers/dns/constellix"
"github.com/go-acme/lego/v4/providers/dns/corenetworks"
"github.com/go-acme/lego/v4/providers/dns/cpanel"
"github.com/go-acme/lego/v4/providers/dns/czechia"
"github.com/go-acme/lego/v4/providers/dns/ddnss"
"github.com/go-acme/lego/v4/providers/dns/derak"
"github.com/go-acme/lego/v4/providers/dns/desec"
@ -68,8 +67,6 @@ 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/excedo"
"github.com/go-acme/lego/v4/providers/dns/exec"
"github.com/go-acme/lego/v4/providers/dns/exoscale"
"github.com/go-acme/lego/v4/providers/dns/f5xc"
@ -276,8 +273,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return corenetworks.NewDNSProvider()
case "cpanel":
return cpanel.NewDNSProvider()
case "czechia":
return czechia.NewDNSProvider()
case "ddnss":
return ddnss.NewDNSProvider()
case "derak":
@ -326,10 +321,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return efficientip.NewDNSProvider()
case "epik":
return epik.NewDNSProvider()
case "eurodns":
return eurodns.NewDNSProvider()
case "excedo":
return excedo.NewDNSProvider()
case "exec":
return exec.NewDNSProvider()
case "exoscale":