mirror of
https://github.com/go-acme/lego
synced 2026-03-14 22:45:48 +01:00
Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b172f103 |
||
|
|
9be8cd43ae |
||
|
|
7b1aa50081 |
||
|
|
a56697ed1c |
||
|
|
847c763504 |
||
|
|
da51631cd3 |
||
|
|
491dcaad1d |
||
|
|
7d459b59c5 |
||
|
|
4547c4317e |
59 changed files with 3485 additions and 113 deletions
8
.github/ISSUE_TEMPLATE/new_dns_provider.yml
vendored
8
.github/ISSUE_TEMPLATE/new_dns_provider.yml
vendored
|
|
@ -14,9 +14,15 @@ body:
|
|||
required: true
|
||||
- label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world.
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: pr
|
||||
attributes:
|
||||
label: Implementation
|
||||
options:
|
||||
- label: Yes, I'm able to create a pull request and be able to maintain the implementation.
|
||||
required: false
|
||||
- label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider.
|
||||
- label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request.
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
|
|
|
|||
43
README.md
43
README.md
|
|
@ -73,70 +73,75 @@ 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>
|
||||
<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/arvancloud/">ArvanCloud</a></td>
|
||||
<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>
|
||||
<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/azion/">Azion</a></td>
|
||||
<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>
|
||||
<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/beget/">Beget.com</a></td>
|
||||
<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>
|
||||
<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/bluecatv2/">Bluecat v2</a></td>
|
||||
<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>
|
||||
<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/checkdomain/">Checkdomain</a></td>
|
||||
<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>
|
||||
<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/cloudflare/">Cloudflare</a></td>
|
||||
<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>
|
||||
<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/conohav3/">ConoHa v3</a></td>
|
||||
<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>
|
||||
<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/czechia/">Czechia</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ddnss/">DDnss (DynDNS Service)</a></td>
|
||||
<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>
|
||||
</tr><tr>
|
||||
<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/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>
|
||||
|
|
@ -266,35 +271,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>
|
||||
</tr><tr>
|
||||
<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/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>
|
||||
</tr><tr>
|
||||
<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/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>
|
||||
</tr><tr>
|
||||
<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/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>
|
||||
</tr><tr>
|
||||
<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/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>
|
||||
</tr><tr>
|
||||
<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/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 -->
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ const (
|
|||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgentComment = "release"
|
||||
ourUserAgentComment = "detach"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package resolver
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
)
|
||||
|
||||
|
|
@ -25,3 +27,7 @@ func (e obtainError) Error() string {
|
|||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (e obtainError) Unwrap() []error {
|
||||
return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e))
|
||||
}
|
||||
|
|
|
|||
70
challenge/resolver/errors_test.go
Normal file
70
challenge/resolver/errors_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_obtainError_Error(t *testing.T) {
|
||||
err := obtainError{
|
||||
"a": &acme.ProblemDetails{Type: "001"},
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
}
|
||||
|
||||
require.EqualError(t, err, `error: one or more domains had a problem:
|
||||
[a] acme: error: 0 :: 001 ::
|
||||
[b] oops
|
||||
[c] I did it again
|
||||
`)
|
||||
}
|
||||
|
||||
func Test_obtainError_Unwrap(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
err obtainError
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "one ok",
|
||||
err: obtainError{
|
||||
"a": &acme.ProblemDetails{},
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "all ok",
|
||||
err: obtainError{
|
||||
"a": &acme.ProblemDetails{Type: "001"},
|
||||
"b": &acme.ProblemDetails{Type: "002"},
|
||||
"c": &acme.ProblemDetails{Type: "002"},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "nope",
|
||||
err: obtainError{
|
||||
"a": errors.New("hello"),
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
},
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pd *acme.ProblemDetails
|
||||
|
||||
test.assert(t, errors.As(test.err, &pd))
|
||||
})
|
||||
}
|
||||
}
|
||||
2
cmd/lego/zz_gen_version.go
generated
2
cmd/lego/zz_gen_version.go
generated
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
package main
|
||||
|
||||
const defaultVersion = "v4.32.0+dev-release"
|
||||
const defaultVersion = "v4.32.0+dev-detach"
|
||||
|
||||
var version = ""
|
||||
|
||||
|
|
|
|||
68
cmd/zz_gen_cmd_dnshelp.go
generated
68
cmd/zz_gen_cmd_dnshelp.go
generated
|
|
@ -49,6 +49,7 @@ func allDNSCodes() string {
|
|||
"constellix",
|
||||
"corenetworks",
|
||||
"cpanel",
|
||||
"czechia",
|
||||
"ddnss",
|
||||
"derak",
|
||||
"desec",
|
||||
|
|
@ -73,6 +74,8 @@ func allDNSCodes() string {
|
|||
"edgeone",
|
||||
"efficientip",
|
||||
"epik",
|
||||
"eurodns",
|
||||
"excedo",
|
||||
"exec",
|
||||
"exoscale",
|
||||
"f5xc",
|
||||
|
|
@ -1026,6 +1029,26 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`)
|
||||
|
||||
case "czechia":
|
||||
// generated from: providers/dns/czechia/czechia.toml
|
||||
ew.writeln(`Configuration for Czechia.`)
|
||||
ew.writeln(`Code: 'czechia'`)
|
||||
ew.writeln(`Since: 'v4.33.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "CZECHIA_TOKEN": Authorization token`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "CZECHIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "CZECHIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "CZECHIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "CZECHIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`)
|
||||
|
||||
case "ddnss":
|
||||
// generated from: providers/dns/ddnss/ddnss.toml
|
||||
ew.writeln(`Configuration for DDnss (DynDNS Service).`)
|
||||
|
|
@ -1541,6 +1564,48 @@ 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.`)
|
||||
|
|
@ -2394,6 +2459,7 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
|
||||
ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
|
||||
ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
|
||||
ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`)
|
||||
ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)
|
||||
|
||||
ew.writeln()
|
||||
|
|
@ -3373,7 +3439,7 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||
|
||||
case "safedns":
|
||||
// generated from: providers/dns/safedns/safedns.toml
|
||||
ew.writeln(`Configuration for UKFast SafeDNS.`)
|
||||
ew.writeln(`Configuration for ANS SafeDNS.`)
|
||||
ew.writeln(`Code: 'safedns'`)
|
||||
ew.writeln(`Since: 'v4.6.0'`)
|
||||
ew.writeln()
|
||||
|
|
|
|||
67
docs/content/dns/zz_gen_czechia.md
generated
Normal file
67
docs/content/dns/zz_gen_czechia.md
generated
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: "Czechia"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: czechia
|
||||
dnsprovider:
|
||||
since: "v4.33.0"
|
||||
code: "czechia"
|
||||
url: "https://www.czechia.com/"
|
||||
---
|
||||
|
||||
<!-- 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. -->
|
||||
69
docs/content/dns/zz_gen_eurodns.md
generated
Normal file
69
docs/content/dns/zz_gen_eurodns.md
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: "EuroDNS"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: eurodns
|
||||
dnsprovider:
|
||||
since: "v4.33.0"
|
||||
code: "eurodns"
|
||||
url: "https://www.eurodns.com/"
|
||||
---
|
||||
|
||||
<!-- 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. -->
|
||||
69
docs/content/dns/zz_gen_excedo.md
generated
Normal file
69
docs/content/dns/zz_gen_excedo.md
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: "Excedo"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: excedo
|
||||
dnsprovider:
|
||||
since: "v4.33.0"
|
||||
code: "excedo"
|
||||
url: "https://excedo.se/"
|
||||
---
|
||||
|
||||
<!-- 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. -->
|
||||
1
docs/content/dns/zz_gen_liara.md
generated
1
docs/content/dns/zz_gen_liara.md
generated
|
|
@ -50,6 +50,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||
| `LIARA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `LIARA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `LIARA_TEAM_ID` | The team ID to access services in a team |
|
||||
| `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
|
|
|
|||
8
docs/content/dns/zz_gen_safedns.md
generated
8
docs/content/dns/zz_gen_safedns.md
generated
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: "UKFast SafeDNS"
|
||||
title: "ANS SafeDNS"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: safedns
|
||||
dnsprovider:
|
||||
since: "v4.6.0"
|
||||
code: "safedns"
|
||||
url: "https://www.ukfast.co.uk/dns-hosting.html"
|
||||
url: "https://www.ans.co.uk/"
|
||||
---
|
||||
|
||||
<!-- 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 [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html).
|
||||
Configuration for [ANS SafeDNS](https://www.ans.co.uk/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
|
@ -23,7 +23,7 @@ Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html).
|
|||
- Since: v4.6.0
|
||||
|
||||
|
||||
Here is an example bash command using the UKFast SafeDNS provider:
|
||||
Here is an example bash command using the ANS SafeDNS provider:
|
||||
|
||||
```bash
|
||||
SAFEDNS_AUTH_TOKEN=xxxxxx \
|
||||
|
|
|
|||
2
docs/data/zz_cli_help.toml
generated
2
docs/data/zz_cli_help.toml
generated
|
|
@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
|
|||
$ lego dnshelp -c code
|
||||
|
||||
Supported DNS providers:
|
||||
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
|
||||
acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
|
||||
|
||||
More information: https://go-acme.github.io/lego/dns
|
||||
"""
|
||||
|
|
|
|||
159
providers/dns/czechia/czechia.go
Normal file
159
providers/dns/czechia/czechia.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia.
|
||||
package czechia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/go-acme/lego/v4/providers/dns/czechia/internal"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "CZECHIA_"
|
||||
|
||||
EnvToken = envNamespace + "TOKEN"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
Token string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Czechia.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("czechia: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Token = values[EnvToken]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for Czechia.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("czechia: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("czechia: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: %w", err)
|
||||
}
|
||||
|
||||
record := internal.TXTRecord{
|
||||
Hostname: subDomain,
|
||||
Text: info.Value,
|
||||
TTL: d.config.TTL,
|
||||
PublishZone: 1,
|
||||
}
|
||||
|
||||
err = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: add TXT record: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: %w", err)
|
||||
}
|
||||
|
||||
record := internal.TXTRecord{
|
||||
Hostname: subDomain,
|
||||
Text: info.Value,
|
||||
TTL: d.config.TTL,
|
||||
PublishZone: 1,
|
||||
}
|
||||
|
||||
err = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("czechia: delete TXT record: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
22
providers/dns/czechia/czechia.toml
Normal file
22
providers/dns/czechia/czechia.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Name = "Czechia"
|
||||
Description = ''''''
|
||||
URL = "https://www.czechia.com/"
|
||||
Code = "czechia"
|
||||
Since = "v4.33.0"
|
||||
|
||||
Example = '''
|
||||
CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \
|
||||
lego --dns czechia -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
CZECHIA_TOKEN = "Authorization token"
|
||||
[Configuration.Additional]
|
||||
CZECHIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
CZECHIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
CZECHIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
|
||||
CZECHIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "https://api.czechia.com/swagger/index.html"
|
||||
165
providers/dns/czechia/czechia_test.go
Normal file
165
providers/dns/czechia/czechia_test.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package czechia
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvToken: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "czechia: some credentials information are missing: CZECHIA_TOKEN",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
token string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
token: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "czechia: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.Token = test.token
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.Token = "secret"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return p, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With("AuthorizationToken", "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("POST /DNS/example.com/TXT",
|
||||
servermock.Noop(),
|
||||
servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("DELETE /DNS/example.com/TXT",
|
||||
servermock.Noop(),
|
||||
servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
124
providers/dns/czechia/internal/client.go
Normal file
124
providers/dns/czechia/internal/client.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.czechia.com/api"
|
||||
|
||||
const authorizationTokenHeader = "AuthorizationToken"
|
||||
|
||||
// Client the Czechia API client.
|
||||
type Client struct {
|
||||
token string
|
||||
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(token string) (*Client, error) {
|
||||
if token == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
token: token,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error {
|
||||
endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error {
|
||||
endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
useragent.SetHeader(req.Header)
|
||||
|
||||
req.Header.Set(authorizationTokenHeader, c.token)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, result)
|
||||
if err != nil {
|
||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if payload != nil {
|
||||
err := json.NewEncoder(buf).Encode(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
67
providers/dns/czechia/internal/client_test.go
Normal file
67
providers/dns/czechia/internal/client_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient("secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With(authorizationTokenHeader, "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_AddTXTRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /DNS/example.com/TXT",
|
||||
servermock.Noop(),
|
||||
servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := TXTRecord{
|
||||
Hostname: "_acme-challenge",
|
||||
Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 120,
|
||||
PublishZone: 1,
|
||||
}
|
||||
|
||||
err := client.AddTXTRecord(t.Context(), "example.com", record)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_DeleteTXTRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("DELETE /DNS/example.com/TXT",
|
||||
servermock.Noop(),
|
||||
servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := TXTRecord{
|
||||
Hostname: "_acme-challenge",
|
||||
Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 120,
|
||||
PublishZone: 1,
|
||||
}
|
||||
|
||||
err := client.DeleteTXTRecord(t.Context(), "example.com", record)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"hostName": "_acme-challenge",
|
||||
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"ttl": 120,
|
||||
"publishZone": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"hostName": "_acme-challenge",
|
||||
"text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"ttl": 120,
|
||||
"publishZone": 1
|
||||
}
|
||||
8
providers/dns/czechia/internal/types.go
Normal file
8
providers/dns/czechia/internal/types.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package internal
|
||||
|
||||
type TXTRecord struct {
|
||||
Hostname string `json:"hostName,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
PublishZone int `json:"publishZone,omitempty"`
|
||||
}
|
||||
197
providers/dns/eurodns/eurodns.go
Normal file
197
providers/dns/eurodns/eurodns.go
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// Package eurodns implements a DNS provider for solving the DNS-01 challenge using EuroDNS.
|
||||
package eurodns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/go-acme/lego/v4/providers/dns/eurodns/internal"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "EURODNS_"
|
||||
|
||||
EnvApplicationID = envNamespace + "APP_ID"
|
||||
EnvAPIKey = envNamespace + "API_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
ApplicationID string
|
||||
APIKey string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, internal.DefaultTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for EuroDNS.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvApplicationID, EnvAPIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("eurodns: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.ApplicationID = values[EnvApplicationID]
|
||||
config.APIKey = values[EnvAPIKey]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for EuroDNS.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("eurodns: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.ApplicationID, config.APIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("eurodns: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: %w", err)
|
||||
}
|
||||
|
||||
authZone = dns01.UnFqdn(authZone)
|
||||
|
||||
zone, err := d.client.GetZone(ctx, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: get zone: %w", err)
|
||||
}
|
||||
|
||||
zone.Records = append(zone.Records, internal.Record{
|
||||
Type: "TXT",
|
||||
Host: subDomain,
|
||||
TTL: internal.TTLRounder(d.config.TTL),
|
||||
RData: info.Value,
|
||||
})
|
||||
|
||||
validation, err := d.client.ValidateZone(ctx, authZone, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: validate zone: %w", err)
|
||||
}
|
||||
|
||||
if validation.Report != nil && !validation.Report.IsValid {
|
||||
return fmt.Errorf("eurodns: validation report: %w", validation.Report)
|
||||
}
|
||||
|
||||
err = d.client.SaveZone(ctx, authZone, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: save zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: %w", err)
|
||||
}
|
||||
|
||||
authZone = dns01.UnFqdn(authZone)
|
||||
|
||||
zone, err := d.client.GetZone(ctx, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: get zone: %w", err)
|
||||
}
|
||||
|
||||
var recordsToKeep []internal.Record
|
||||
|
||||
for _, record := range zone.Records {
|
||||
if record.Type == "TXT" && record.Host == subDomain && record.RData == info.Value {
|
||||
continue
|
||||
}
|
||||
|
||||
recordsToKeep = append(recordsToKeep, record)
|
||||
}
|
||||
|
||||
zone.Records = recordsToKeep
|
||||
|
||||
validation, err := d.client.ValidateZone(ctx, authZone, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: validate zone: %w", err)
|
||||
}
|
||||
|
||||
if validation.Report != nil && !validation.Report.IsValid {
|
||||
return fmt.Errorf("eurodns: validation report: %w", validation.Report)
|
||||
}
|
||||
|
||||
err = d.client.SaveZone(ctx, authZone, zone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eurodns: save zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
24
providers/dns/eurodns/eurodns.toml
Normal file
24
providers/dns/eurodns/eurodns.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Name = "EuroDNS"
|
||||
Description = ''''''
|
||||
URL = "https://www.eurodns.com/"
|
||||
Code = "eurodns"
|
||||
Since = "v4.33.0"
|
||||
|
||||
Example = '''
|
||||
EURODNS_APP_ID="xxx" \
|
||||
EURODNS_API_KEY="yyy" \
|
||||
lego --dns eurodns -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
EURODNS_APP_ID = "Application ID"
|
||||
EURODNS_API_KEY = "API key"
|
||||
[Configuration.Additional]
|
||||
EURODNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
EURODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
EURODNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)"
|
||||
EURODNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "https://docapi.eurodns.com/"
|
||||
215
providers/dns/eurodns/eurodns_test.go
Normal file
215
providers/dns/eurodns/eurodns_test.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package eurodns
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/go-acme/lego/v4/providers/dns/eurodns/internal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvApplicationID, EnvAPIKey).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvApplicationID: "abc",
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing application ID",
|
||||
envVars: map[string]string{
|
||||
EnvApplicationID: "",
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
expected: "eurodns: some credentials information are missing: EURODNS_APP_ID",
|
||||
},
|
||||
{
|
||||
desc: "missing API secret",
|
||||
envVars: map[string]string{
|
||||
EnvApplicationID: "",
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
expected: "eurodns: some credentials information are missing: EURODNS_APP_ID",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "eurodns: some credentials information are missing: EURODNS_APP_ID,EURODNS_API_KEY",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
appID string
|
||||
apiKey string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
appID: "abc",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing application ID",
|
||||
expected: "eurodns: credentials missing",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing API secret",
|
||||
expected: "eurodns: credentials missing",
|
||||
appID: "abc",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "eurodns: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.ApplicationID = test.appID
|
||||
config.APIKey = test.apiKey
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.APIKey = "secret"
|
||||
config.ApplicationID = "abc"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
provider, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider.client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return provider, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With(internal.HeaderAppID, "abc").
|
||||
With(internal.HeaderAPIKey, "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /example.com",
|
||||
servermock.ResponseFromInternal("zone_get.json"),
|
||||
).
|
||||
Route("POST /example.com/check",
|
||||
servermock.ResponseFromInternal("zone_add_validate_ok.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("zone_add.json"),
|
||||
).
|
||||
Route("PUT /example.com",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
servermock.CheckRequestJSONBodyFromInternal("zone_add.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /example.com",
|
||||
servermock.ResponseFromInternal("zone_add.json"),
|
||||
).
|
||||
Route("POST /example.com/check",
|
||||
servermock.ResponseFromInternal("zone_remove.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"),
|
||||
).
|
||||
Route("PUT /example.com",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
199
providers/dns/eurodns/internal/client.go
Normal file
199
providers/dns/eurodns/internal/client.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://rest-api.eurodns.com/dns-zones/"
|
||||
|
||||
const (
|
||||
HeaderAppID = "X-APP-ID"
|
||||
HeaderAPIKey = "X-API-KEY"
|
||||
)
|
||||
|
||||
// Client the EuroDNS API client.
|
||||
type Client struct {
|
||||
appID string
|
||||
apiKey string
|
||||
|
||||
BaseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(appID, apiKey string) (*Client, error) {
|
||||
if appID == "" || apiKey == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
appID: appID,
|
||||
apiKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetZone gets a DNS Zone.
|
||||
// https://docapi.eurodns.com/#/dnsprovider/getdnszone
|
||||
func (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error) {
|
||||
endpoint := c.BaseURL.JoinPath(domain)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Zone{}
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SaveZone saves a DNS Zone.
|
||||
// https://docapi.eurodns.com/#/dnsprovider/savednszone
|
||||
func (c *Client) SaveZone(ctx context.Context, domain string, zone *Zone) error {
|
||||
endpoint := c.BaseURL.JoinPath(domain)
|
||||
|
||||
if len(zone.URLForwards) == 0 {
|
||||
zone.URLForwards = make([]URLForward, 0)
|
||||
}
|
||||
|
||||
if len(zone.MailForwards) == 0 {
|
||||
zone.MailForwards = make([]MailForward, 0)
|
||||
}
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
// ValidateZone validates DNS Zone.
|
||||
// https://docapi.eurodns.com/#/dnsprovider/checkdnszone
|
||||
func (c *Client) ValidateZone(ctx context.Context, domain string, zone *Zone) (*Zone, error) {
|
||||
endpoint := c.BaseURL.JoinPath(domain, "check")
|
||||
|
||||
if len(zone.URLForwards) == 0 {
|
||||
zone.URLForwards = make([]URLForward, 0)
|
||||
}
|
||||
|
||||
if len(zone.MailForwards) == 0 {
|
||||
zone.MailForwards = make([]MailForward, 0)
|
||||
}
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Zone{}
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
req.Header.Set(HeaderAppID, c.appID)
|
||||
req.Header.Set(HeaderAPIKey, c.apiKey)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return parseError(req, resp)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, result)
|
||||
if err != nil {
|
||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if payload != nil {
|
||||
err := json.NewEncoder(buf).Encode(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseError(req *http.Request, resp *http.Response) error {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var errAPI APIError
|
||||
|
||||
err := json.Unmarshal(raw, &errAPI)
|
||||
if err != nil {
|
||||
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d: %w", resp.StatusCode, &errAPI)
|
||||
}
|
||||
|
||||
const DefaultTTL = 600
|
||||
|
||||
// TTLRounder rounds the given TTL in seconds to the next accepted value.
|
||||
// Accepted TTL values are: 600, 900, 1800,3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800.
|
||||
func TTLRounder(ttl int) int {
|
||||
for _, validTTL := range []int{DefaultTTL, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800} {
|
||||
if ttl <= validTTL {
|
||||
return validTTL
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultTTL
|
||||
}
|
||||
310
providers/dns/eurodns/internal/client_test.go
Normal file
310
providers/dns/eurodns/internal/client_test.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient("abc", "secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.HTTPClient = server.Client()
|
||||
client.BaseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return client, nil
|
||||
},
|
||||
servermock.CheckHeader().
|
||||
WithJSONHeaders().
|
||||
With(HeaderAppID, "abc").
|
||||
With(HeaderAPIKey, "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_GetZone(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /example.com",
|
||||
servermock.ResponseFromFixture("zone_get.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
zone, err := client.GetZone(context.Background(), "example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: slices.Concat([]Record{fakeARecord()}),
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zone)
|
||||
}
|
||||
|
||||
func TestClient_GetZone_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /example.com",
|
||||
servermock.ResponseFromFixture("error.json").
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
_, err := client.GetZone(context.Background(), "example.com")
|
||||
require.Error(t, err)
|
||||
|
||||
require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key")
|
||||
}
|
||||
|
||||
func TestClient_SaveZone(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("PUT /example.com",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
servermock.CheckRequestJSONBodyFromFixture("zone_add.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := Record{
|
||||
Type: "TXT",
|
||||
Host: "_acme-challenge",
|
||||
RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 600,
|
||||
}
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord(), record},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
err := client.SaveZone(context.Background(), "example.com", zone)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_SaveZone_emptyForwards(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("PUT /example.com",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent),
|
||||
servermock.CheckRequestJSONBodyFromFixture("zone_add_empty_forwards.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := Record{
|
||||
Type: "TXT",
|
||||
Host: "_acme-challenge",
|
||||
RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 600,
|
||||
}
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: slices.Concat([]Record{fakeARecord(), record}),
|
||||
}
|
||||
|
||||
err := client.SaveZone(context.Background(), "example.com", zone)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_SaveZone_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("PUT /example.com",
|
||||
servermock.ResponseFromFixture("error.json").
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord()},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
err := client.SaveZone(context.Background(), "example.com", zone)
|
||||
require.Error(t, err)
|
||||
|
||||
require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key")
|
||||
}
|
||||
|
||||
func TestClient_ValidateZone(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /example.com/check",
|
||||
servermock.ResponseFromFixture("zone_add_validate_ok.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("zone_add.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := Record{
|
||||
Type: "TXT",
|
||||
Host: "_acme-challenge",
|
||||
RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 600,
|
||||
}
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord(), record},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
zone, err := client.ValidateZone(context.Background(), "example.com", zone)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord(), record},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
Report: &Report{IsValid: true},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zone)
|
||||
}
|
||||
|
||||
func TestClient_ValidateZone_report(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /example.com/check",
|
||||
servermock.ResponseFromFixture("zone_add_validate_ko.json"),
|
||||
servermock.CheckRequestJSONBodyFromFixture("zone_add.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
record := Record{
|
||||
Type: "TXT",
|
||||
Host: "_acme-challenge",
|
||||
RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: 600,
|
||||
}
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord(), record},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
zone, err := client.ValidateZone(context.Background(), "example.com", zone)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord(), record},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
Report: fakeReport(),
|
||||
}
|
||||
|
||||
assert.EqualError(t, zone.Report, `record error (ERROR): "120" is not a valid TTL, URL forward error (ERROR): string, mail forward error (ERROR): string, zone error (ERROR): string`)
|
||||
|
||||
assert.Equal(t, expected, zone)
|
||||
}
|
||||
|
||||
func TestClient_ValidateZone_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /example.com/check",
|
||||
servermock.ResponseFromFixture("error.json").
|
||||
WithStatusCode(http.StatusUnauthorized),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
zone := &Zone{
|
||||
Name: "example.com",
|
||||
DomainConnect: true,
|
||||
Records: []Record{fakeARecord()},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}
|
||||
|
||||
_, err := client.ValidateZone(context.Background(), "example.com", zone)
|
||||
require.Error(t, err)
|
||||
|
||||
require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key")
|
||||
}
|
||||
|
||||
func fakeARecord() Record {
|
||||
return Record{
|
||||
ID: 1000,
|
||||
Type: "A",
|
||||
Host: "@",
|
||||
TTL: 600,
|
||||
RData: "string",
|
||||
Updated: ptr.Pointer(true),
|
||||
Locked: ptr.Pointer(true),
|
||||
IsDynDNS: ptr.Pointer(true),
|
||||
Proxy: "ON",
|
||||
}
|
||||
}
|
||||
|
||||
func fakeURLForward() URLForward {
|
||||
return URLForward{
|
||||
ID: 2000,
|
||||
ForwardType: "FRAME",
|
||||
Host: "string",
|
||||
URL: "string",
|
||||
Title: "string",
|
||||
Keywords: "string",
|
||||
Description: "string",
|
||||
Updated: ptr.Pointer(true),
|
||||
}
|
||||
}
|
||||
|
||||
func fakeMailForward() MailForward {
|
||||
return MailForward{
|
||||
ID: 3000,
|
||||
Source: "string",
|
||||
Destination: "string",
|
||||
Updated: ptr.Pointer(true),
|
||||
}
|
||||
}
|
||||
|
||||
func fakeReport() *Report {
|
||||
return &Report{
|
||||
IsValid: false,
|
||||
RecordErrors: []RecordError{{
|
||||
Messages: []string{`"120" is not a valid TTL`},
|
||||
Severity: "ERROR",
|
||||
Record: fakeARecord(),
|
||||
}},
|
||||
URLForwardErrors: []URLForwardError{{
|
||||
Messages: []string{"string"},
|
||||
Severity: "ERROR",
|
||||
URLForward: fakeURLForward(),
|
||||
}},
|
||||
MailForwardErrors: []MailForwardError{{
|
||||
Messages: []string{"string"},
|
||||
MailForward: fakeMailForward(),
|
||||
Severity: "ERROR",
|
||||
}},
|
||||
ZoneErrors: []ZoneError{{
|
||||
Message: "string",
|
||||
Severity: "ERROR",
|
||||
Records: []Record{fakeARecord()},
|
||||
URLForwards: []URLForward{fakeURLForward()},
|
||||
MailForwards: []MailForward{fakeMailForward()},
|
||||
}},
|
||||
}
|
||||
}
|
||||
8
providers/dns/eurodns/internal/fixtures/error.json
Normal file
8
providers/dns/eurodns/internal/fixtures/error.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_API_KEY",
|
||||
"title": "Invalid API Key"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
providers/dns/eurodns/internal/fixtures/zone_add.json
Normal file
46
providers/dns/eurodns/internal/fixtures/zone_add.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
},
|
||||
{
|
||||
"type": "TXT",
|
||||
"host": "_acme-challenge",
|
||||
"ttl": 600,
|
||||
"rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"updated": null,
|
||||
"locked": null,
|
||||
"isDynDns": null
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
},
|
||||
{
|
||||
"type": "TXT",
|
||||
"host": "_acme-challenge",
|
||||
"ttl": 600,
|
||||
"rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"updated": null,
|
||||
"locked": null,
|
||||
"isDynDns": null
|
||||
}
|
||||
],
|
||||
"urlForwards": [],
|
||||
"mailForwards": []
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
},
|
||||
{
|
||||
"type": "TXT",
|
||||
"host": "_acme-challenge",
|
||||
"ttl": 600,
|
||||
"rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"updated": null,
|
||||
"locked": null,
|
||||
"isDynDns": null
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"report": {
|
||||
"isValid": false,
|
||||
"recordErrors": [
|
||||
{
|
||||
"messages": [
|
||||
"\"120\" is not a valid TTL"
|
||||
],
|
||||
"record": {
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
},
|
||||
"severity": "ERROR"
|
||||
}
|
||||
],
|
||||
"urlForwardErrors": [
|
||||
{
|
||||
"messages": [
|
||||
"string"
|
||||
],
|
||||
"urlForward": {
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
},
|
||||
"severity": "ERROR"
|
||||
}
|
||||
],
|
||||
"mailForwardErrors": [
|
||||
{
|
||||
"messages": [
|
||||
"string"
|
||||
],
|
||||
"mailForward": {
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
},
|
||||
"severity": "ERROR"
|
||||
}
|
||||
],
|
||||
"zoneErrors": [
|
||||
{
|
||||
"message": "string",
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"severity": "ERROR"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
},
|
||||
{
|
||||
"type": "TXT",
|
||||
"host": "_acme-challenge",
|
||||
"ttl": 600,
|
||||
"rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
"updated": null,
|
||||
"locked": null,
|
||||
"isDynDns": null
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"report": {
|
||||
"isValid": true
|
||||
}
|
||||
}
|
||||
37
providers/dns/eurodns/internal/fixtures/zone_get.json
Normal file
37
providers/dns/eurodns/internal/fixtures/zone_get.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
37
providers/dns/eurodns/internal/fixtures/zone_remove.json
Normal file
37
providers/dns/eurodns/internal/fixtures/zone_remove.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "example.com",
|
||||
"domainConnect": true,
|
||||
"records": [
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "A",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"rdata": "string",
|
||||
"updated": true,
|
||||
"locked": true,
|
||||
"isDynDns": true,
|
||||
"proxy": "ON"
|
||||
}
|
||||
],
|
||||
"urlForwards": [
|
||||
{
|
||||
"id": 2000,
|
||||
"forwardType": "FRAME",
|
||||
"host": "string",
|
||||
"url": "string",
|
||||
"title": "string",
|
||||
"keywords": "string",
|
||||
"description": "string",
|
||||
"updated": true
|
||||
}
|
||||
],
|
||||
"mailForwards": [
|
||||
{
|
||||
"id": 3000,
|
||||
"source": "string",
|
||||
"destination": "string",
|
||||
"updated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
136
providers/dns/eurodns/internal/types.go
Normal file
136
providers/dns/eurodns/internal/types.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type APIError struct {
|
||||
Errors []Error `json:"errors"`
|
||||
}
|
||||
|
||||
func (a *APIError) Error() string {
|
||||
var msg []string
|
||||
|
||||
for _, e := range a.Errors {
|
||||
msg = append(msg, fmt.Sprintf("%s: %s", e.Code, e.Title))
|
||||
}
|
||||
|
||||
return strings.Join(msg, ", ")
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
DomainConnect bool `json:"domainConnect,omitempty"`
|
||||
Records []Record `json:"records"`
|
||||
URLForwards []URLForward `json:"urlForwards"`
|
||||
MailForwards []MailForward `json:"mailForwards"`
|
||||
Report *Report `json:"report,omitempty"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
RData string `json:"rdata,omitempty"`
|
||||
Updated *bool `json:"updated"`
|
||||
Locked *bool `json:"locked"`
|
||||
IsDynDNS *bool `json:"isDynDns"`
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
type URLForward struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
ForwardType string `json:"forwardType,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Keywords string `json:"keywords,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Updated *bool `json:"updated,omitempty"`
|
||||
}
|
||||
|
||||
type MailForward struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
Updated *bool `json:"updated,omitempty"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
IsValid bool `json:"isValid,omitempty"`
|
||||
RecordErrors []RecordError `json:"recordErrors,omitempty"`
|
||||
URLForwardErrors []URLForwardError `json:"urlForwardErrors,omitempty"`
|
||||
MailForwardErrors []MailForwardError `json:"mailForwardErrors,omitempty"`
|
||||
ZoneErrors []ZoneError `json:"zoneErrors,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Report) Error() string {
|
||||
var msg []string
|
||||
|
||||
for _, e := range r.RecordErrors {
|
||||
msg = append(msg, e.Error())
|
||||
}
|
||||
|
||||
for _, e := range r.URLForwardErrors {
|
||||
msg = append(msg, e.Error())
|
||||
}
|
||||
|
||||
for _, e := range r.MailForwardErrors {
|
||||
msg = append(msg, e.Error())
|
||||
}
|
||||
|
||||
for _, e := range r.ZoneErrors {
|
||||
msg = append(msg, e.Error())
|
||||
}
|
||||
|
||||
return strings.Join(msg, ", ")
|
||||
}
|
||||
|
||||
type RecordError struct {
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Record Record `json:"record"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
func (e *RecordError) Error() string {
|
||||
return fmt.Sprintf("record error (%s): %s", e.Severity, strings.Join(e.Messages, ", "))
|
||||
}
|
||||
|
||||
type URLForwardError struct {
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
URLForward URLForward `json:"urlForward"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
func (e *URLForwardError) Error() string {
|
||||
return fmt.Sprintf("URL forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", "))
|
||||
}
|
||||
|
||||
type MailForwardError struct {
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
MailForward MailForward `json:"mailForward"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
func (e *MailForwardError) Error() string {
|
||||
return fmt.Sprintf("mail forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", "))
|
||||
}
|
||||
|
||||
type ZoneError struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Records []Record `json:"records,omitempty"`
|
||||
URLForwards []URLForward `json:"urlForwards,omitempty"`
|
||||
MailForwards []MailForward `json:"mailForwards,omitempty"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ZoneError) Error() string {
|
||||
return fmt.Sprintf("zone error (%s): %s", e.Severity, e.Message)
|
||||
}
|
||||
176
providers/dns/excedo/excedo.go
Normal file
176
providers/dns/excedo/excedo.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo.
|
||||
package excedo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/go-acme/lego/v4/providers/dns/excedo/internal"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "EXCEDO_"
|
||||
|
||||
EnvAPIURL = envNamespace + "API_URL"
|
||||
EnvAPIKey = envNamespace + "API_KEY"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, 60),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
|
||||
recordsMu sync.Mutex
|
||||
records map[string]int64
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Excedo.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAPIURL, EnvAPIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("excedo: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.APIURL = values[EnvAPIURL]
|
||||
config.APIKey = values[EnvAPIKey]
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for Excedo.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("excedo: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.APIURL, config.APIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("excedo: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
|
||||
|
||||
return &DNSProvider{
|
||||
config: config,
|
||||
client: client,
|
||||
records: make(map[string]int64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("excedo: %w", err)
|
||||
}
|
||||
|
||||
record := internal.Record{
|
||||
DomainName: dns01.UnFqdn(authZone),
|
||||
Name: subDomain,
|
||||
Type: "TXT",
|
||||
Content: info.Value,
|
||||
TTL: strconv.Itoa(d.config.TTL),
|
||||
}
|
||||
|
||||
recordID, err := d.client.AddRecord(ctx, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("excedo: add record: %w", err)
|
||||
}
|
||||
|
||||
d.recordsMu.Lock()
|
||||
d.records[token] = recordID
|
||||
d.recordsMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
d.recordsMu.Lock()
|
||||
recordID, ok := d.records[token]
|
||||
d.recordsMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("excedo: unknown record ID for '%s'", info.EffectiveFQDN)
|
||||
}
|
||||
|
||||
err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("excedo: delete record: %w", err)
|
||||
}
|
||||
|
||||
d.recordsMu.Lock()
|
||||
delete(d.records, token)
|
||||
d.recordsMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
24
providers/dns/excedo/excedo.toml
Normal file
24
providers/dns/excedo/excedo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Name = "Excedo"
|
||||
Description = ''''''
|
||||
URL = "https://excedo.se/"
|
||||
Code = "excedo"
|
||||
Since = "v4.33.0"
|
||||
|
||||
Example = '''
|
||||
EXCEDO_API_KEY=your-api-key \
|
||||
EXCEDO_API_URL=your-base-url \
|
||||
lego --dns excedo -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
EXCEDO_API_KEY = "API key"
|
||||
EXCEDO_API_URL = "API base URL"
|
||||
[Configuration.Additional]
|
||||
EXCEDO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)"
|
||||
EXCEDO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)"
|
||||
EXCEDO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)"
|
||||
EXCEDO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
|
||||
|
||||
[Links]
|
||||
API = "none"
|
||||
210
providers/dns/excedo/excedo_test.go
Normal file
210
providers/dns/excedo/excedo_test.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package excedo
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvAPIURL: "https://example.com",
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing the API key",
|
||||
envVars: map[string]string{
|
||||
EnvAPIURL: "https://example.com",
|
||||
EnvAPIKey: "",
|
||||
},
|
||||
expected: "excedo: some credentials information are missing: EXCEDO_API_KEY",
|
||||
},
|
||||
{
|
||||
desc: "missing the API URL",
|
||||
envVars: map[string]string{
|
||||
EnvAPIURL: "",
|
||||
EnvAPIKey: "secret",
|
||||
},
|
||||
expected: "excedo: some credentials information are missing: EXCEDO_API_URL",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
envVars: map[string]string{},
|
||||
expected: "excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_KEY",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
apiURL string
|
||||
apiKey string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
apiURL: "https://example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing the API key",
|
||||
apiURL: "https://example.com",
|
||||
expected: "excedo: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing the API URL",
|
||||
apiKey: "secret",
|
||||
expected: "excedo: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "excedo: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.APIURL = test.apiURL
|
||||
config.APIKey = test.apiKey
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func mockBuilder() *servermock.Builder[*DNSProvider] {
|
||||
return servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
config.APIURL = server.URL
|
||||
config.APIKey = "secret"
|
||||
config.HTTPClient = server.Client()
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /authenticate/login/",
|
||||
servermock.ResponseFromInternal("login.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer secret"),
|
||||
).
|
||||
Route("POST /dns/addrecord/",
|
||||
servermock.ResponseFromInternal("addrecord.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer session-token"),
|
||||
servermock.CheckForm().Strict().
|
||||
With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
|
||||
With("domainName", "example.com").
|
||||
With("name", "_acme-challenge").
|
||||
With("ttl", "60").
|
||||
With("type", "TXT"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /authenticate/login/",
|
||||
servermock.ResponseFromInternal("login.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer secret"),
|
||||
).
|
||||
Route("POST /dns/deleterecord/",
|
||||
servermock.ResponseFromInternal("deleterecord.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer session-token"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
provider.records["abc"] = 19695822
|
||||
|
||||
err := provider.CleanUp("example.com", "abc", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
205
providers/dns/excedo/internal/client.go
Normal file
205
providers/dns/excedo/internal/client.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
|
||||
querystring "github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
type responseChecker interface {
|
||||
Check() error
|
||||
}
|
||||
|
||||
// Client the Excedo API client.
|
||||
type Client struct {
|
||||
apiKey string
|
||||
|
||||
baseURL *url.URL
|
||||
HTTPClient *http.Client
|
||||
|
||||
token *ExpirableToken
|
||||
muToken sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(apiURL, apiKey string) (*Client, error) {
|
||||
if apiURL == "" || apiKey == "" {
|
||||
return nil, errors.New("credentials missing")
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) {
|
||||
payload, err := querystring.Values(record)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
endpoint := c.baseURL.JoinPath("/dns/addrecord/")
|
||||
|
||||
req, err := newFormRequest(ctx, http.MethodPost, endpoint, payload)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result := new(AddRecordResponse)
|
||||
|
||||
err = c.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.RecordID, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {
|
||||
endpoint := c.baseURL.JoinPath("/dns/deleterecord/")
|
||||
|
||||
data := map[string]string{
|
||||
"domainname": dns01.UnFqdn(zone),
|
||||
"recordid": recordID,
|
||||
}
|
||||
|
||||
req, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := new(BaseResponse)
|
||||
|
||||
err = c.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) {
|
||||
endpoint := c.baseURL.JoinPath("/dns/getrecords/")
|
||||
|
||||
query := endpoint.Query()
|
||||
query.Set("domainname", zone)
|
||||
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := new(GetRecordsResponse)
|
||||
|
||||
err = c.doAuthenticated(ctx, req, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.DNS, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result responseChecker) error {
|
||||
useragent.SetHeader(req.Header)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
|
||||
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, result)
|
||||
if err != nil {
|
||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
||||
}
|
||||
|
||||
return result.Check()
|
||||
}
|
||||
|
||||
func newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
writer := multipart.NewWriter(buf)
|
||||
|
||||
for k, v := range data {
|
||||
err := writer.WriteField(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := bytes.NewReader(buf.Bytes())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) {
|
||||
var body io.Reader
|
||||
|
||||
if len(form) > 0 {
|
||||
body = bytes.NewReader([]byte(form.Encode()))
|
||||
} else {
|
||||
body = http.NoBody
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
if method == http.MethodPost {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
137
providers/dns/excedo/internal/client_test.go
Normal file
137
providers/dns/excedo/internal/client_test.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client, err := NewClient(server.URL, "secret")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.HTTPClient = server.Client()
|
||||
|
||||
return client, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestClient_AddRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /dns/addrecord/",
|
||||
servermock.ResponseFromFixture("addrecord.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer session-token"),
|
||||
servermock.CheckForm().Strict().
|
||||
With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY").
|
||||
With("domainName", "example.com").
|
||||
With("name", "_acme-challenge").
|
||||
With("ttl", "60").
|
||||
With("type", "TXT"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
client.token = &ExpirableToken{
|
||||
Token: "session-token",
|
||||
Expires: time.Now().Add(6 * time.Hour),
|
||||
}
|
||||
|
||||
record := Record{
|
||||
DomainName: "example.com",
|
||||
Name: "_acme-challenge",
|
||||
Type: "TXT",
|
||||
Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: "60",
|
||||
}
|
||||
|
||||
recordID, err := client.AddRecord(t.Context(), record)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 19695822, recordID)
|
||||
}
|
||||
|
||||
func TestClient_AddRecord_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /dns/addrecord/",
|
||||
servermock.ResponseFromFixture("error.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
client.token = &ExpirableToken{
|
||||
Token: "session-token",
|
||||
Expires: time.Now().Add(6 * time.Hour),
|
||||
}
|
||||
|
||||
record := Record{
|
||||
DomainName: "example.com",
|
||||
Name: "_acme-challenge",
|
||||
Type: "TXT",
|
||||
Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY",
|
||||
TTL: "60",
|
||||
}
|
||||
|
||||
_, err := client.AddRecord(t.Context(), record)
|
||||
require.EqualError(t, err, "2003: Required parameter missing")
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("POST /dns/deleterecord/",
|
||||
servermock.ResponseFromFixture("deleterecord.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer session-token"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
client.token = &ExpirableToken{
|
||||
Token: "session-token",
|
||||
Expires: time.Now().Add(6 * time.Hour),
|
||||
}
|
||||
|
||||
err := client.DeleteRecord(t.Context(), "example.com", "19695822")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_GetRecords(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /dns/getrecords/",
|
||||
servermock.ResponseFromFixture("getrecords.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer session-token"),
|
||||
servermock.CheckQueryParameter().Strict().
|
||||
With("domainname", "example.com"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
client.token = &ExpirableToken{
|
||||
Token: "session-token",
|
||||
Expires: time.Now().Add(6 * time.Hour),
|
||||
}
|
||||
|
||||
zones, err := client.GetRecords(t.Context(), "example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]Zone{
|
||||
"example.com": {
|
||||
DNSType: "type",
|
||||
Records: []Record{{
|
||||
RecordID: "1234",
|
||||
Name: "_acme-challenge.example.com",
|
||||
Type: "TXT",
|
||||
Content: "txt-value",
|
||||
TTL: "60",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zones)
|
||||
}
|
||||
15
providers/dns/excedo/internal/fixtures/addrecord.json
Normal file
15
providers/dns/excedo/internal/fixtures/addrecord.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"code": 1000,
|
||||
"desc": "Command completed successfully",
|
||||
"recordid": 19695822,
|
||||
"session": {
|
||||
"accID": "1234",
|
||||
"usrID": "1234",
|
||||
"status": "active",
|
||||
"expire": {
|
||||
"date": "2026-03-10 19:03:18",
|
||||
"seconds": 5678
|
||||
}
|
||||
},
|
||||
"runtime": 0.2852
|
||||
}
|
||||
14
providers/dns/excedo/internal/fixtures/deleterecord.json
Normal file
14
providers/dns/excedo/internal/fixtures/deleterecord.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"code": 1000,
|
||||
"desc": "Command completed successfully",
|
||||
"session": {
|
||||
"accID": "1234",
|
||||
"usrID": "1234",
|
||||
"status": "active",
|
||||
"expire": {
|
||||
"date": "2026-03-10 19:03:18",
|
||||
"seconds": 5678
|
||||
}
|
||||
},
|
||||
"runtime": 0.2852
|
||||
}
|
||||
18
providers/dns/excedo/internal/fixtures/error.json
Normal file
18
providers/dns/excedo/internal/fixtures/error.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"code": 2003,
|
||||
"desc": "Required parameter missing",
|
||||
"missing": [
|
||||
"domainname",
|
||||
"recordid"
|
||||
],
|
||||
"session": {
|
||||
"accID": "1234",
|
||||
"usrID": "1234",
|
||||
"status": "active",
|
||||
"expire": {
|
||||
"date": "2026-03-10 19:03:18",
|
||||
"seconds": 5485
|
||||
}
|
||||
},
|
||||
"runtime": 0.0534
|
||||
}
|
||||
23
providers/dns/excedo/internal/fixtures/getrecords.json
Normal file
23
providers/dns/excedo/internal/fixtures/getrecords.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"code": 1000,
|
||||
"desc": "Command completed successfully",
|
||||
"dns": {
|
||||
"example.com": {
|
||||
"dnstype": "type",
|
||||
"recordusage": {
|
||||
"used": 74
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"recordid": "1234",
|
||||
"name": "_acme-challenge.example.com",
|
||||
"type": "TXT",
|
||||
"content": "txt-value",
|
||||
"ttl": "60",
|
||||
"prio": null,
|
||||
"change_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
providers/dns/excedo/internal/fixtures/login.json
Normal file
7
providers/dns/excedo/internal/fixtures/login.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"code": 1000,
|
||||
"desc": "Command completed successfully",
|
||||
"parameters": {
|
||||
"token": "session-token"
|
||||
}
|
||||
}
|
||||
75
providers/dns/excedo/internal/identity.go
Normal file
75
providers/dns/excedo/internal/identity.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExpirableToken struct {
|
||||
Token string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
func (t *ExpirableToken) IsExpired() bool {
|
||||
return time.Now().After(t.Expires)
|
||||
}
|
||||
|
||||
func (c *Client) Login(ctx context.Context) (string, error) {
|
||||
endpoint := c.baseURL.JoinPath("/authenticate/login/")
|
||||
|
||||
req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
|
||||
result := new(LoginResponse)
|
||||
|
||||
err = c.do(req, result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Code != 1000 && result.Code != 1300 {
|
||||
return "", fmt.Errorf("%d: %s", result.Code, result.Description)
|
||||
}
|
||||
|
||||
return result.Parameters.Token, nil
|
||||
}
|
||||
|
||||
func (c *Client) authenticate(ctx context.Context) (string, error) {
|
||||
c.muToken.Lock()
|
||||
defer c.muToken.Unlock()
|
||||
|
||||
if c.token == nil || c.token.IsExpired() {
|
||||
token, err := c.Login(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c.token = &ExpirableToken{
|
||||
Token: token,
|
||||
Expires: time.Now().Add(2*time.Hour - time.Minute),
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return c.token.Token, nil
|
||||
}
|
||||
|
||||
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error {
|
||||
token, err := c.authenticate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
return c.do(req, result)
|
||||
}
|
||||
35
providers/dns/excedo/internal/identity_test.go
Normal file
35
providers/dns/excedo/internal/identity_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_Login(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /authenticate/login/",
|
||||
servermock.ResponseFromFixture("login.json"),
|
||||
servermock.CheckHeader().
|
||||
WithAuthorization("Bearer secret"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
token, err := client.Login(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "session-token", token)
|
||||
}
|
||||
|
||||
func TestClient_Login_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("GET /authenticate/login/",
|
||||
servermock.ResponseFromFixture("error.json"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
_, err := client.Login(t.Context())
|
||||
require.EqualError(t, err, "2003: Required parameter missing")
|
||||
}
|
||||
65
providers/dns/excedo/internal/types.go
Normal file
65
providers/dns/excedo/internal/types.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package internal
|
||||
|
||||
import "fmt"
|
||||
|
||||
type BaseResponse struct {
|
||||
Code int `json:"code"`
|
||||
Description string `json:"desc"`
|
||||
}
|
||||
|
||||
func (r BaseResponse) Check() error {
|
||||
// Response codes:
|
||||
// - 1000: Command completed successfully
|
||||
// - 1300: Command completed successfully; no messages
|
||||
// - 2001: Command syntax error
|
||||
// - 2002: Command use error
|
||||
// - 2003: Required parameter missing
|
||||
// - 2004: Parameter value range error
|
||||
// - 2104: Billing failure
|
||||
// - 2200: Authentication error
|
||||
// - 2201: Authorization error
|
||||
// - 2303: Object does not exist
|
||||
// - 2304: Object status prohibits operation
|
||||
// - 2309: Object duplicate found
|
||||
// - 2400: Command failed
|
||||
// - 2500: Command failed; server closing connection
|
||||
if r.Code != 1000 && r.Code != 1300 {
|
||||
return fmt.Errorf("%d: %s", r.Code, r.Description)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetRecordsResponse struct {
|
||||
BaseResponse
|
||||
|
||||
DNS map[string]Zone `json:"dns"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
DNSType string `json:"dnstype"`
|
||||
Records []Record `json:"records"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
DomainName string `json:"domainName,omitempty" url:"domainName,omitempty"`
|
||||
RecordID string `json:"recordid,omitempty" url:"recordid,omitempty"`
|
||||
Name string `json:"name,omitempty" url:"name,omitempty"`
|
||||
Type string `json:"type,omitempty" url:"type,omitempty"`
|
||||
Content string `json:"content,omitempty" url:"content,omitempty"`
|
||||
TTL string `json:"ttl,omitempty" url:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
type AddRecordResponse struct {
|
||||
BaseResponse
|
||||
|
||||
RecordID int64 `json:"recordid"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
BaseResponse
|
||||
|
||||
Parameters struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"parameters"`
|
||||
}
|
||||
|
|
@ -38,55 +38,25 @@ func TestClient_GetZones(t *testing.T) {
|
|||
|
||||
expected := []Zone{
|
||||
{
|
||||
ID: "123",
|
||||
Name: "example.com",
|
||||
NameDisplay: "example.com",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
Protected: "1",
|
||||
IsRegistered: "1",
|
||||
Updated: false,
|
||||
CustomerID: "16030",
|
||||
DomainRegistrar: "norid",
|
||||
DomainStatus: "active",
|
||||
DomainExpiryDate: "2026-11-23 15:17:38",
|
||||
DomainAutoRenew: "1",
|
||||
ExternalDNS: "0",
|
||||
RecordCount: 4,
|
||||
ID: "123",
|
||||
Name: "example.com",
|
||||
NameDisplay: "example.com",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
},
|
||||
{
|
||||
ID: "226",
|
||||
Name: "example.org",
|
||||
NameDisplay: "example.org",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
Protected: "1",
|
||||
IsRegistered: "1",
|
||||
Updated: false,
|
||||
CustomerID: "16030",
|
||||
DomainRegistrar: "norid",
|
||||
DomainStatus: "active",
|
||||
DomainExpiryDate: "2026-11-23 14:15:01",
|
||||
DomainAutoRenew: "1",
|
||||
ExternalDNS: "0",
|
||||
RecordCount: 5,
|
||||
ID: "226",
|
||||
Name: "example.org",
|
||||
NameDisplay: "example.org",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
},
|
||||
{
|
||||
ID: "229",
|
||||
Name: "example.xn--zckzah",
|
||||
NameDisplay: "example.テスト",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
Protected: "1",
|
||||
IsRegistered: "1",
|
||||
Updated: false,
|
||||
CustomerID: "16030",
|
||||
DomainRegistrar: "norid",
|
||||
DomainStatus: "active",
|
||||
DomainExpiryDate: "2026-12-01 12:40:48",
|
||||
DomainAutoRenew: "1",
|
||||
ExternalDNS: "0",
|
||||
RecordCount: 4,
|
||||
ID: "229",
|
||||
Name: "example.xn--zckzah",
|
||||
NameDisplay: "example.テスト",
|
||||
Type: "NATIVE",
|
||||
Active: "1",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"domain_dnssec_data": null,
|
||||
"domain_protected_email": null,
|
||||
"zone_created": "2025-11-23 16:17:29",
|
||||
"zone_updated": false,
|
||||
"zone_updated": 1700000000,
|
||||
"external_dns": "0",
|
||||
"record_count": 4,
|
||||
"zone_name_display": "example.com"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"domain_dnssec_data": null,
|
||||
"domain_protected_email": null,
|
||||
"zone_created": "2025-11-23 15:13:27",
|
||||
"zone_updated": false,
|
||||
"zone_updated": 1700000000,
|
||||
"external_dns": "0",
|
||||
"record_count": 5,
|
||||
"zone_name_display": "example.org"
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
"domain_dnssec_data": null,
|
||||
"domain_protected_email": null,
|
||||
"zone_created": "2025-11-23 16:37:15",
|
||||
"zone_updated": false,
|
||||
"zone_updated": 1700000000,
|
||||
"external_dns": "0",
|
||||
"record_count": 4,
|
||||
"zone_name_display": "example.\u30C6\u30B9\u30C8"
|
||||
|
|
|
|||
|
|
@ -26,21 +26,11 @@ type APIResponse[T any] struct {
|
|||
}
|
||||
|
||||
type Zone struct {
|
||||
ID string `json:"zone_id,omitempty"`
|
||||
Name string `json:"zone_name,omitempty"`
|
||||
NameDisplay string `json:"zone_name_display,omitempty"`
|
||||
Type string `json:"zone_type,omitempty"`
|
||||
Active string `json:"zone_active,omitempty"`
|
||||
Protected string `json:"zone_protected,omitempty"`
|
||||
IsRegistered string `json:"zone_is_registered,omitempty"`
|
||||
Updated bool `json:"zone_updated,omitempty"`
|
||||
CustomerID string `json:"cust_id,omitempty"`
|
||||
DomainRegistrar string `json:"domain_registrar,omitempty"`
|
||||
DomainStatus string `json:"domain_status,omitempty"`
|
||||
DomainExpiryDate string `json:"domain_expiry_date,omitempty"`
|
||||
DomainAutoRenew string `json:"domain_auto_renew,omitempty"`
|
||||
ExternalDNS string `json:"external_dns,omitempty"`
|
||||
RecordCount int `json:"record_count,omitempty"`
|
||||
ID string `json:"zone_id,omitempty"`
|
||||
Name string `json:"zone_name,omitempty"`
|
||||
NameDisplay string `json:"zone_name_display,omitempty"`
|
||||
Type string `json:"zone_type,omitempty"`
|
||||
Active string `json:"zone_active,omitempty"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const (
|
|||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgentComment = "release"
|
||||
ourUserAgentComment = "detach"
|
||||
)
|
||||
|
||||
// Get builds and returns the User-Agent string.
|
||||
|
|
|
|||
|
|
@ -20,17 +20,23 @@ const defaultBaseURL = "https://dns-service.iran.liara.ir"
|
|||
type Client struct {
|
||||
baseURL *url.URL
|
||||
httpClient *http.Client
|
||||
|
||||
teamID string
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(hc *http.Client) *Client {
|
||||
func NewClient(hc *http.Client, teamID string) *Client {
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
if hc == nil {
|
||||
hc = &http.Client{Timeout: 10 * time.Second}
|
||||
}
|
||||
|
||||
return &Client{httpClient: hc, baseURL: baseURL}
|
||||
return &Client{
|
||||
httpClient: hc,
|
||||
baseURL: baseURL,
|
||||
teamID: teamID,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRecords gets the records of a domain.
|
||||
|
|
@ -38,7 +44,7 @@ func NewClient(hc *http.Client) *Client {
|
|||
func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
|
@ -73,7 +79,7 @@ func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, e
|
|||
func (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
||||
req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
|
@ -108,7 +114,7 @@ func (c *Client) CreateRecord(ctx context.Context, domainName string, record Rec
|
|||
func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) {
|
||||
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
|
@ -143,7 +149,7 @@ func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*R
|
|||
func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error {
|
||||
endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
|
@ -162,7 +168,14 @@ func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string)
|
|||
return nil
|
||||
}
|
||||
|
||||
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
|
||||
if c.teamID != "" {
|
||||
query := endpoint.Query()
|
||||
query.Set("teamID", c.teamID)
|
||||
|
||||
endpoint.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if payload != nil {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import (
|
|||
|
||||
const apiKey = "key"
|
||||
|
||||
func mockBuilder() *servermock.Builder[*Client] {
|
||||
func mockBuilder(teamID string) *servermock.Builder[*Client] {
|
||||
return servermock.NewBuilder[*Client](
|
||||
func(server *httptest.Server) (*Client, error) {
|
||||
client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey))
|
||||
client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey), teamID)
|
||||
client.baseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return client, nil
|
||||
|
|
@ -26,7 +26,7 @@ func mockBuilder() *servermock.Builder[*Client] {
|
|||
}
|
||||
|
||||
func TestClient_GetRecords(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("GET /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordsResponse.json")).
|
||||
Build(t)
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ func TestClient_GetRecords(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClient_GetRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("GET /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("RecordResponse.json")).
|
||||
Build(t)
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ func TestClient_GetRecord(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClient_CreateRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("POST /api/v1/zones/example.com/dns-records",
|
||||
servermock.ResponseFromFixture("RecordResponse.json").
|
||||
WithStatusCode(http.StatusCreated),
|
||||
|
|
@ -108,8 +108,47 @@ func TestClient_CreateRecord(t *testing.T) {
|
|||
assert.Equal(t, expected, record)
|
||||
}
|
||||
|
||||
func TestClient_CreateRecord_withTeamID(t *testing.T) {
|
||||
client := mockBuilder("123").
|
||||
Route("POST /api/v1/zones/example.com/dns-records",
|
||||
servermock.ResponseFromFixture("RecordResponse.json").
|
||||
WithStatusCode(http.StatusCreated),
|
||||
servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`),
|
||||
servermock.CheckQueryParameter().Strict().With("teamID", "123"),
|
||||
).
|
||||
Build(t)
|
||||
|
||||
data := Record{
|
||||
Type: "string",
|
||||
Name: "string",
|
||||
Contents: []Content{
|
||||
{
|
||||
Text: "string",
|
||||
},
|
||||
},
|
||||
TTL: 3600,
|
||||
}
|
||||
|
||||
record, err := client.CreateRecord(t.Context(), "example.com", data)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Record{
|
||||
ID: "string",
|
||||
Type: "string",
|
||||
Name: "string",
|
||||
Contents: []Content{
|
||||
{
|
||||
Text: "string",
|
||||
},
|
||||
},
|
||||
TTL: 3600,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, record)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecord(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("DELETE /api/v1/zones/example.com/dns-records/123",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNoContent)).
|
||||
|
|
@ -120,7 +159,7 @@ func TestClient_DeleteRecord(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClient_DeleteRecord_NotFound_Response(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("DELETE /api/v1/zones/example.com/dns-records/123",
|
||||
servermock.Noop().
|
||||
WithStatusCode(http.StatusNotFound)).
|
||||
|
|
@ -131,7 +170,7 @@ func TestClient_DeleteRecord_NotFound_Response(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClient_DeleteRecord_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
client := mockBuilder("").
|
||||
Route("DELETE /api/v1/zones/example.com/dns-records/123",
|
||||
servermock.ResponseFromFixture("error.json").
|
||||
WithStatusCode(http.StatusUnauthorized)).
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const (
|
|||
envNamespace = "LIARA_"
|
||||
|
||||
EnvAPIKey = envNamespace + "API_KEY"
|
||||
EnvTeamID = envNamespace + "TEAM_ID"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
|
|
@ -39,7 +40,9 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
|
|||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
APIKey string
|
||||
APIKey string
|
||||
TeamID string
|
||||
|
||||
TTL int
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
|
|
@ -77,6 +80,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
|||
|
||||
config := NewDefaultConfig()
|
||||
config.APIKey = values[EnvAPIKey]
|
||||
config.TeamID = env.GetOrFile(EnvTeamID)
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
|
@ -112,6 +116,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
|||
clientdebug.Wrap(
|
||||
internal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey),
|
||||
),
|
||||
config.TeamID,
|
||||
)
|
||||
|
||||
return &DNSProvider{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ lego --dns liara -d '*.example.com' -d example.com run
|
|||
[Configuration.Credentials]
|
||||
LIARA_API_KEY = "The API key"
|
||||
[Configuration.Additional]
|
||||
LIARA_TEAM_ID = "The team ID to access services in a team"
|
||||
LIARA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
|
||||
LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
|
||||
LIARA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const defaultBaseURL = "https://api.ukfast.io/safedns/v1"
|
|||
|
||||
const authorizationHeader = "Authorization"
|
||||
|
||||
// Client the UKFast SafeDNS client.
|
||||
// Client the ANS SafeDNS client.
|
||||
type Client struct {
|
||||
authToken string
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS.
|
||||
// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS.
|
||||
package safedns
|
||||
|
||||
import (
|
||||
|
|
@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
|||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS.
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("safedns: supplied configuration was nil")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Name = "UKFast SafeDNS"
|
||||
Name = "ANS SafeDNS"
|
||||
Description = ''''''
|
||||
URL = "https://www.ukfast.co.uk/dns-hosting.html"
|
||||
URL = "https://www.ans.co.uk/"
|
||||
Code = "safedns"
|
||||
Since = "v4.6.0"
|
||||
|
||||
|
|
|
|||
9
providers/dns/zz_gen_dns_providers.go
generated
9
providers/dns/zz_gen_dns_providers.go
generated
|
|
@ -43,6 +43,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||
"github.com/go-acme/lego/v4/providers/dns/corenetworks"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/czechia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ddnss"
|
||||
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||
|
|
@ -67,6 +68,8 @@ 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"
|
||||
|
|
@ -273,6 +276,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
|||
return corenetworks.NewDNSProvider()
|
||||
case "cpanel":
|
||||
return cpanel.NewDNSProvider()
|
||||
case "czechia":
|
||||
return czechia.NewDNSProvider()
|
||||
case "ddnss":
|
||||
return ddnss.NewDNSProvider()
|
||||
case "derak":
|
||||
|
|
@ -321,6 +326,10 @@ 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":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue